Snowflake から PGP 暗号化の GCS ファイル を 出力・取り込みしてみた。
こんにちは、みかみです。
最近、カブトエビを育ててみたくてしかたありません。「生きた化石」という響きにロマンを感じます。
調べてみたら、野生でも田んぼなどに生息していて、食べられないこともないらしいです。とはいえ泥臭くてあまり美味しくないらしいです。
卵から育てて餌にも気を配っていれば美味しく食べられるのかどうか、気になってます。
やりたいこと
- Snowflake から、PGP 暗号化ファイルを GCS に出力したい。
- GCS に配置された PGP 暗号化済みファイルの復号データを、Snowflake のテーブルに取り込みたい。
- Snowflake で PGP 暗号化・復号する方法を確認したい。
前提
Google Cloud SDK(gcloud コマンド)の実行環境は準備済みであるものとします。 本エントリでは、Cloud Shell を使用しました。
また、GCS や IAM の操作に必要な API の有効化と権限は付与済みです。
Snowflake 環境側でも、必要なユーザーの作成や権限付与は実施済みです。
なお、文中、鍵情報など一部の文字は伏字に変更しています。
本ブログの検証に使用した Snowflake は 東京リージョンの AWS プラットフォームで動いています。GCS も同じく東京リージョンのバケットを使用しています。
準備:pgpy パッケージの利用可否確認
今回、PGP 暗号化・復号処理に Python コードの UDF 作成を想定しているので、UDF から利用可能な Snowflake 管理の Anaconda に pgpy パッケージが含まれているか確認します。
Snowflake で以下の SQL を実行しました。
SELECT package_name, version, language, runtime_version
FROM MIKAMI_DB.information_schema.packages
WHERE package_name = 'pgpy'
AND language = 'python'
ORDER BY runtime_version DESC;

Python 3.8〜3.12 で、pgpy パッケージが利用できることが確認できました。
準備:PGP キーペアを準備
ローカル環境で以下の Python スクリプトを実行して、PGP の秘密鍵と公開鍵を作成しました。
import pgpy
from pgpy.constants import (
PubKeyAlgorithm, KeyFlags, HashAlgorithm,
SymmetricKeyAlgorithm, CompressionAlgorithm
)
def generate_keypair(name: str, email: str, key_path_prefix: str = "PGP_key"):
"""RSA-4096 の PGP キーペアを生成してファイルに保存する"""
# キーペアの生成
key = pgpy.PGPKey.new(PubKeyAlgorithm.RSAEncryptOrSign, 4096)
# UID(名前・メールアドレス)の追加
uid = pgpy.PGPUID.new(name, email=email)
key.add_uid(
uid,
usage={KeyFlags.Sign, KeyFlags.EncryptCommunications, KeyFlags.EncryptStorage},
hashes=[HashAlgorithm.SHA256],
ciphers=[SymmetricKeyAlgorithm.AES256],
compression=[CompressionAlgorithm.ZLIB]
)
# 公開鍵・秘密鍵をファイルに書き出す
pub_key_path = f"{key_path_prefix}_public.asc"
priv_key_path = f"{key_path_prefix}_private.asc"
with open(pub_key_path, "w") as f:
f.write(str(key.pubkey))
with open(priv_key_path, "w") as f:
f.write(str(key))
print(f"公開鍵 : {pub_key_path}")
print(f"秘密鍵 : {priv_key_path}")
print("鍵ペアの生成が完了しました。")
if __name__ == "__main__":
generate_keypair(
name="Test User",
email="test@example.com",
key_path_prefix="PGP_key"
)
作成したキーペアを、Snowflake のシークレットに登録します。
Snowflake で以下の SQL を実行して、PGP 秘密鍵・公開鍵をシークレットとして登録しました。
CREATE OR REPLACE SECRET MIKAMI_DB.PUBLIC.PGP_PUBLIC_KEY
TYPE = GENERIC_STRING
SECRET_STRING = '-----BEGIN PGP PUBLIC KEY BLOCK-----
(PGP_key_public.asc の内容をペースト)
-----END PGP PUBLIC KEY BLOCK-----';
CREATE OR REPLACE SECRET MIKAMI_DB.PUBLIC.PGP_PRIVATE_KEY
TYPE = GENERIC_STRING
SECRET_STRING = '-----BEGIN PGP PRIVATE KEY BLOCK-----
(PGP_key_private.asc の内容をペースト)
-----END PGP PRIVATE KEY BLOCK-----';
それぞれのキーファイルの内容は、以下のコマンドでクリップボードにコピーできます。
cat PGP_key_public.asc | pbcopy
cat PGP_key_private.asc | pbcopy
SHOW SECRETS IN SCHEMA MIKAMI_DB.PUBLIC;
で、シークレットが正常に作成されたことが確認できました。

準備:暗号化・復号 UDF 作成
PGP 暗号化・復号 UDF を作成します。
まずは、UDF からシークレットにアクセスするためのネットワークルールと外部アクセス統合を作成しておきます。
今回、UDF 内で get_generic_secret_string() を使ってシークレット情報を取得しますが、この get_generic_secret_string() を使う場合、UDF 作成時に外部アクセス統合(EXTERNAL_ACCESS_INTEGRATIONS)を指定する必要があります。
また、外部アクセス統合の作成時にネットワークルール(ALLOWED_NETWORK_RULES)の指定が必要なため、ネットワークルールも作成する必要があります。
実際に外部ネットワークへアクセスするわけではないので、ネットワークルールの VALUE_LIST はダミー値(example.com)で作成します。
Snowflake で以下の SQL を実行しました。
CREATE OR REPLACE NETWORK RULE MIKAMI_DB.PUBLIC.PGP_DUMMY_NETWORK_RULE
TYPE = HOST_PORT
VALUE_LIST = ('example.com')
MODE = EGRESS;
CREATE OR REPLACE EXTERNAL ACCESS INTEGRATION PGP_SECRETS_INTEGRATION
ALLOWED_NETWORK_RULES = (MIKAMI_DB.PUBLIC.PGP_DUMMY_NETWORK_RULE)
ALLOWED_AUTHENTICATION_SECRETS = (
MIKAMI_DB.PUBLIC.PGP_PUBLIC_KEY,
MIKAMI_DB.PUBLIC.PGP_PRIVATE_KEY
)
ENABLED = TRUE;
PGP 暗号化 UDF を作成します。
CREATE OR REPLACE FUNCTION MIKAMI_DB.PUBLIC.PGP_ENCRYPT(plaintext STRING)
RETURNS STRING
LANGUAGE PYTHON
RUNTIME_VERSION = '3.9'
PACKAGES = ('pgpy')
EXTERNAL_ACCESS_INTEGRATIONS = (PGP_SECRETS_INTEGRATION)
SECRETS = ('public_key' = MIKAMI_DB.PUBLIC.PGP_PUBLIC_KEY)
HANDLER = 'encrypt'
AS $$
import pgpy
import _snowflake
def encrypt(plaintext: str) -> str:
# シークレットから公開鍵を取得
public_key_str = _snowflake.get_generic_secret_string('public_key')
pub_key, _ = pgpy.PGPKey.from_blob(public_key_str)
# メッセージの作成と暗号化
message = pgpy.PGPMessage.new(plaintext.encode('utf-8'))
encrypted = pub_key.encrypt(message)
# ASCII Armor 形式の文字列として返す
return str(encrypted)
$$;
正常に動くか、以下の SQL で確認してみます。
SELECT MIKAMI_DB.PUBLIC.PGP_ENCRYPT('Hello, Snowflake!') AS encrypted_text;

パラメータで渡した文字列が暗号化されていることが確認できました。
続いて、PGP 復号 UDF を作成します。
CREATE OR REPLACE FUNCTION MIKAMI_DB.PUBLIC.PGP_DECRYPT(encrypted_text STRING)
RETURNS STRING
LANGUAGE PYTHON
RUNTIME_VERSION = '3.9'
PACKAGES = ('pgpy')
EXTERNAL_ACCESS_INTEGRATIONS = (PGP_SECRETS_INTEGRATION)
SECRETS = ('private_key' = MIKAMI_DB.PUBLIC.PGP_PRIVATE_KEY)
HANDLER = 'decrypt'
AS $$
import pgpy
import _snowflake
def decrypt(encrypted_text: str) -> str:
# シークレットから秘密鍵を取得
private_key_str = _snowflake.get_generic_secret_string('private_key')
priv_key, _ = pgpy.PGPKey.from_blob(private_key_str)
# 暗号化メッセージの読み込みと復号
encrypted_message = pgpy.PGPMessage.from_blob(encrypted_text)
decrypted = priv_key.decrypt(encrypted_message)
plaintext = decrypted.message
# bytes / bytearray / str の3パターンに対応
if isinstance(plaintext, (bytes, bytearray)):
return plaintext.decode('utf-8')
return plaintext
$$;
以下の SQL で動作確認します。
SELECT MIKAMI_DB.PUBLIC.PGP_DECRYPT(
MIKAMI_DB.PUBLIC.PGP_ENCRYPT('Hello, Snowflake!')
) AS decrypted_text;
こちらも、期待通り復号処理が正常に実行されたことが確認できました。

PGP 暗号化ファイルを GCS に出力
Snowflake の以下の DOGS テーブルのデータを、PGP 暗号化して GCS に CSV ファイルとして出力します。

GCS アクセスのためのストレージ統合と外部ステージは作成済みのものを使用します。
新規作成が必要な場合は、以下をご参照ください。
以下の SQL で、出力先にはまだファイルがないことを確認しておきます。
LIST @MIKAMI_DB.PUBLIC.MIKAMI_GCS_TEST_STAGE/pgp/;

以下の SQL を実行して、PGP 暗号化したファイルを GCS に出力します。
COPY INTO @MIKAMI_DB.PUBLIC.MIKAMI_GCS_TEST_STAGE/pgp/dogs_row_encrypted.csv.pgp
FROM (
SELECT
MIKAMI_DB.PUBLIC.PGP_ENCRYPT(
OBJECT_CONSTRUCT('id', id, 'name', name)::STRING
) AS row_encrypted
FROM MIKAMI_DB.PUBLIC.DOGS
)
FILE_FORMAT = (TYPE = 'CSV' COMPRESSION = NONE)
SINGLE = TRUE
OVERWRITE = TRUE;
再度、出力先を確認してみると
LIST @MIKAMI_DB.PUBLIC.MIKAMI_GCS_TEST_STAGE/pgp/;

ファイルが出力されたことが確認できました。
実際のファイルの中身も確認してみます。
$ gcloud storage cat gs://test-mikami/pgp/dogs_row_encrypted.csv.pgp | head -10
-----BEGIN PGP MESSAGE-----\
\
wcFMA2USQwCKCKhMAQ//TydM2Gka5DrnU/7JpZBARae7PJYi0rRjgiXWIHF6bSgE\
nFX4ulIXe9BcwSFOVfS7u3GOuBUADyaOCV3f0pnZTRf1nRCy5EaXV/w9l5xeQS7l\
zcCHTmJfcWpVIlCPVZbkVh+NItjwAfKGl7ouHOIr1+2nB488/sk7110AUNE+8LbV\
USl3JdIMk0SIwHdFoDs2FtNbyNk411bjAhHL/oysyf2vHOgwzNYXaOs6eWNQfz/+\
iwHgXDsPeO1d+NT0fcGEPAm+Fjk3slLlDMMPHqNwX9Fe7wqC1oGiPmqix/sEnlOz\
irC34bQ92yPZKCMQMkigf4RRGegtncZFY3CoA19DJ+p5MZENQv5vof520Hhk7L+M\
AIwF/uZDlIPYkMrIXHXEJFN3xilEPYcU1tf1bA9PDbmUBzs7RR9ENEqV1GkPRg1e\
vY5ZiNtvzNxKtl+buurQgi4dJx1m6uPvjg+fO6Pd/YkGSFkCT9VNXi4SDKHwt88+\
想定通り、暗号化されていることが確認できました。
PGP 暗号化されたファイルを Snowflake に取り込み
先ほど出力した PGP 暗号化済みの CSV ファイルを、新しい DOGS_DECRYPT テーブルに取り込みます。
-- ファイルフォーマット作成
CREATE OR REPLACE FILE FORMAT MIKAMI_DB.PUBLIC.PGP_CSV_FORMAT
TYPE = 'CSV'
FIELD_OPTIONALLY_ENCLOSED_BY = '"';
-- テーブル作成・取り込み
CREATE OR REPLACE TABLE MIKAMI_DB.PUBLIC.DOGS_DECRYPT AS
SELECT
parsed_row:id::INT AS id,
parsed_row:name::STRING AS name
FROM (
SELECT
PARSE_JSON(
MIKAMI_DB.PUBLIC.PGP_DECRYPT($1)
) AS parsed_row
FROM @MIKAMI_DB.PUBLIC.MIKAMI_GCS_TEST_STAGE/pgp/dogs_row_encrypted.csv.pgp
(FILE_FORMAT => MIKAMI_DB.PUBLIC.PGP_CSV_FORMAT)
);
テーブルデータを確認してみます。

想定通り、復号されたデータが DOGS_DECRYPT テーブルに格納できました。
念のためバイト単位でデータ出力元テーブルと比較して、目視では見落としがちな改行やスペースなどのデータ差異も発生していないか確認しておきます。
SELECT 'DOGS' AS table_name, HASH_AGG(id, name) AS hash FROM MIKAMI_DB.PUBLIC.DOGS
UNION ALL
SELECT 'DOGS_DECRYPT' AS table_name, HASH_AGG(id, name) AS hash FROM MIKAMI_DB.PUBLIC.DOGS_DECRYPT;

HASH 値が一致しているので、暗号化前・復号後のデータに差異がないことが確認できました。
まとめ(所感)
Snowflake の UDF を使って、PGP 暗号ファイルを出力・取り込みできることが確認できました。
今回は PGP 暗号化・復号を検証しましたが、Snowflake 管理の Anaconda に含まれる Python パッケージであれば UDF から利用できるので、cryptography パッケージを使って他の AES・RSA アルゴリズムによるデータの暗号化・復号も可能です。
UDF を活用することで、Python の豊富なライブラリをデータ処理に組み込むことができ、Snowflake 上でのデータ処理の可能性がさらに広がるのではないかと思います。








