BigQueryの列レベル暗号化(Cloud KMS利用)を解説

記事タイトルとURLをコピーする

G-gen の杉村です。BigQuery では、Cloud KMS で管理する暗号鍵を使って、列レベルの暗号化を行うことができます。その仕組みと方法を解説します。

BigQuery における暗号化

ストレージ暗号化とは

まず BigQuery に保存されるデータは、デフォルトで透過的に暗号化されています。Google Cloud によって管理されている鍵を使い、ストレージのレベルで暗号化されています。そのためもし、Google のデータセンターからストレージ機器が盗まれてしまったり、悪意を持った内部犯が物理的なアクセスを試みても、データが閲覧されることはありません。これをデフォルトの暗号化といいます。

デフォルトの暗号化では暗号鍵が Google によって管理されているため、より強固なセキュリティを要する場合はユーザー管理の鍵によってストレージを暗号化することも可能です。これは CMEK 暗号化と呼ばれます。CMEK は Customer-managed encryption keys の略です。

しかしながらこれらはいずれもストレージレベルの暗号化です。対応できる脅威は、前述のようにストレージ機器の物理的な盗難や、物理的なアクセス、脆弱性を突いた非正規アクセスなどです。BigQuery テーブルへのアクセス権限を持った人が、正面玄関(通常の Web UI や API など)からアクセスすれば、データは見えてしまいます。

列レベル暗号化とは

ここで当記事で扱う列レベル暗号化が登場します。列レベル暗号化では、データそのものを暗号鍵を使って暗号化してからテーブルに格納します。

そのため、たとえテーブル自体へのアクセス権限を持っている人でも、通常通り SELECT して取得できるのは暗号化されたバイト列だけです。データを復号するには鍵へのアクセス権限が必要です。

特に機密性が求められる情報を BigQuery テーブルに保存する際は、この列レベル暗号化を使うことで機密性を高めることができます。

権限と読取可能性

列レベルの暗号化を使った場合、権限の考え方は「テーブル自体へのアクセス権限」と「暗号鍵へのアクセス権限」の2軸になります。以下のマトリクス表のような考え方になります。

権限マトリクス

列レベル暗号化された列であっても、テーブルへの権限さえあれば通常どおり SELECT できます。ただし、読み取られたデータは暗号化されたままです。テーブルへの権限に加えて鍵への権限を持っていれば、データを復号することができます。復号は、SQL 上の関数を使って明示的に行う必要があります。具体的な方法は後述します。

暗号化方式

AEAD 暗号化とキーセット

BigQuery の SQL を使って列レベルの暗号化をする際は AEAD 暗号化と呼ばれる方式が用いられます。Authenticated Encryption with Associated Data の略で、認証付き暗号と呼ばれます。HTTPS プロトコルでおなじみの TLS でも使われている方式です。

また、BigQuery における列レベルの暗号化に使う暗号鍵は「鍵セット (キーセット)」と呼ばれます。鍵セットはプライマリ暗号鍵とセカンダリ暗号鍵からなる鍵の集合体であり、BigQuery の BYTES 型のデータです。

エンベロープ暗号化

列レベルの暗号化ではエンベロープ暗号化という方式を使うことができます。

この方法では「データ暗号鍵 (Data Encryption Keys、以下 DEK)」と「鍵暗号鍵 (Key encryption keys、以下 KEK)」の二種類の鍵を使います。前者はデータそのものを暗号化するための鍵で、後者は "DEK を暗号化するための鍵" です。

データを暗号化する鍵自体が暗号化されているので、例え DEK が漏洩しても、KEK が強固に守られていればデータ自体は安全です。

KEK と DEK

BigQuery の列レベル暗号化では、先述の鍵セットが DEK にあたります。DEK は KEK によって暗号化されたあと、BigQuery のテーブル等に保存しておきます。

では KEK はどこに置いておくかというと、Cloud KMS (フルマネージドの鍵管理サービス) の鍵がそれに当たります。Cloud KMS について詳細が知りたい方は、以下の記事もご参照ください。

blog.g-gen.co.jp

たとえ BigQuery への閲覧権限を持った人によって鍵セットを保存したテーブルが読み取られてしまったとしても、鍵セット自体が暗号化されているため、そのままでは鍵としては使えません。鍵セットを復号して使えるようにするには、Cloud KMS 鍵である KEK への権限が必要です。このように二重の暗号化・二重の権限によって保護されていると言えます。

なお、Cloud KMS 鍵 (KEK) によって暗号化された鍵セット (DEK) のことをラップされた鍵セット (Wrapped keysets) と呼びます。

よって、列レベル暗号化されたデータを読み取って復号する際は「データを読み取る」「DEK を KEK で復号」「復号された DEK を使ってデータを復号」という作業になります。大変そうですが、BigQuery にはこれを簡単に実現する SQL 関数が用意されています。書き込むときも同様です。

BigQuery 以外でのエンベロープ暗号化

エンベロープ暗号化という暗号化戦略は、BigQuery に固有のものではありません。

Cloud KMS のドキュメントにも一般的な手法としてエンベロープ暗号化に関する記載があるほか、Amazon Web Services (AWS) の類似サービスである AWS KMS でも同様の戦略が利用されており、Amazon S3 におけるサーバーサイド暗号化 (SSE-KMS) などで透過的にエンベロープ暗号化が行われています。

2つの暗号化関数

列レベル暗号化に用いる関数には二種類あります。確定的暗号化関数 (Deterministic encryption functions) と非確定的暗号化関数 (Non-deterministic encryption functions) です。"Deterministic" は "決定論的" と訳される場合もあります。

前者は、インプットとなる平文が同じであれば、アウトプットも必ず同じになります。

後者は、インプットが同じでも、暗号化するたびに異なるアウトプットとなります。アウトプットは非確定的 (非決定論的) ではありますが、復号は問題なく可能です。

確定論と非確定論

前者の確定的暗号化であれば、データを暗号化した状態のまま、集計や結合などの分析に利用できます。例えば「顧客の氏名」という情報は暗号化すると意味を失いますが、暗号化された状態の氏名をキーにして、集計したりテーブルの結合に利用できます。

後者の非確定的暗号化は、データを暗号化すると毎回結果が異なるので、分析に利用することはできません。個人情報保護の観点等から、集計や結合に暗号化後のデータを使わせたくない場合にこちらを選択します。

検証

作業の流れ

ここから、実際の SQL を交えて、列レベルの暗号化を行う方法を検証します。以下のような流れで検証します。

  1. Cloud KMS 鍵を作成
  2. 鍵セット (DEK) 格納用のテーブルを作成
  3. 鍵セット (DEK) を作成
  4. データ格納用テーブルを作成
  5. データを暗号化してテーブルに格納
  6. データを読み取って復号

当検証は、以下のドキュメントを参考にしています。

1. Cloud KMS 鍵を作成

Cloud KMS で KEK としての鍵を作成します。

Cloud KMS の鍵 (キー) は キーリング というまとまりで管理されます。まずキーリングを、暗号化対象のデータセットと同じロケーション (リージョン) で作成します。鍵とデータセットのリージョンが異なると、暗号化関数から利用できませんのでご注意ください。

当検証では以下のようなキーリングを作成しました。

パラメータ名 説明
キーリング名 my-test-keyring
ロケーションタイプ リージョン : asia-northeast1 (東京) 暗号化対象のデータセットと合わせる

なお Cloud KMS ではグローバル (global) ロケーションにキーリングを作成できますが、BigQuery の列レベル暗号化ではグローバルのキーは利用できません。

そしてそのキーリングの中に、キーを作成します。一般的には以下のようなパラメータになります。

パラメータ名 説明
キー名 my-test-key
保護レベル ソフトウェア マネージドのハードウェアセキュリティモジュール (HSM) を選択することもできる
鍵のマテリアル 生成した鍵 自前の HSM で作成した鍵など、外部からインポートすることもできる
目的とアルゴリズム 対称暗号化 / 復号化 対称鍵 (共通鍵) を選択します
鍵のローテーション 90 日 (開始日は任意) セキュリティ要件に合わせてください

2. 鍵セット (DEK) 格納用のテーブルを作成

鍵セット (DEK) 格納用のテーブルを作成します。ここでは以下のような、シンプルなテーブルとします。

CREATE TABLE `my-project.my_dataset.my_keysets` (
  id STRING,
  keyset BYTES
) 

鍵セットを格納する列は BYTES 型である点に注意してください。

また鍵セットには ID を持たせるようにしました。列レベル暗号化では「行ごとや特定キーごとに異なる鍵を使って暗号化する」こともできますし、「テーブルごとに1個の鍵を利用」したり「複数のテーブルで1個の鍵を利用する」こともできます。共有範囲が狭いほどセキュアです。

特定キー、例えば顧客ごとに鍵セットを分けていれば、鍵を無効化して復号不可にすることでデータを実質的に消去する「暗号消去 (暗号シュレッディング)」が可能になります。顧客が退会したときに、顧客データが複数テーブルに分散していても、暗号鍵を無効化するだけで一度に実質的な消去ができます。

今回の検証では、ある1個のテーブルのデータを1個の鍵セットで暗号化します。

3. 鍵セット (DEK) を作成

INSERT `my-project.my_dataset.my_keysets` (id, keyset)
SELECT
  "my_table",
  KEYS.NEW_WRAPPED_KEYSET(
    'gcp-kms://projects/my-project/locations/asia-northeast1/keyRings/my-test-keyring/cryptoKeys/my-test-key',
    'DETERMINISTIC_AEAD_AES_SIV_CMAC_256'
  )

KEYS.NEW_WRAPPED_KEYSET() が新規の鍵セット (ラップされた鍵セット) を生成する関数です。第一引数に、先ほど作成した KEK として使う KMS キーの ID を指定します。ID のフォーマットは以下のとおりです。

gcp-kms://projects/${プロジェクト ID}/locations/${キーリングのロケーション}/keyRings/${キーリング名}/cryptoKeys/${キー名}

第二引数には、鍵の種類を記載します。確定的な関数では DETERMINISTIC_AEAD_AES_SIV_CMAC_256 を、非確定的関数で使うには AEAD_AES_GCM_256 を指定します。異なる種類の鍵では暗号化できません。

なお id 列の値は今回の対象テーブルである my_table としています。暗号化・復号するときにこの id をもとに鍵を選ぶ想定です。

4. データ格納用テーブルを作成

実データを入れるテーブル my_table を作ります。平文テキストと暗号化後のテキストを入れる列をそれぞれ作りますが、もちろん実際の運用ではこのようなことはしません。検証用に、並べて見比べるためです。

CREATE TABLE `my-project.my_dataset.my_table` (
  id INTEGER,
  normal_text STRING,
  encrypted_text BYTES
) 

暗号化後のデータを入れる列は BYTES 型である点に注意してください。

5. データを暗号化してテーブルに格納

次に、my_table にデータを暗号化して書き込みます。なお次のサンプル SQL を Web UI の編集画面に入力すると、バリデーションで以下のように表示されるかもしれません。

Query error: deterministic_encrypt requires the keyset chain and its arguments (kms_resource_name and first_level_keyset) to be constant. at [7:1]

KEYS.KEYSET_CHAIN() の第一引数に定数を入れよ、というエラーですが、無視して実行することができます。bq query コマンドで --dry_run をかけた場合も同様にエラーが返りますが、--dry_run を外すと問題なく実行できます。

DECLARE selected_keyset BYTES;
DECLARE plain_text STRING;
  
SET selected_keyset = (SELECT keyset FROM `my-project.my_dataset.my_keysets` WHERE id = "my_table");
SET plain_text = 'This is a classified text.';
  
INSERT `my-project.my_dataset.my_table` (id, normal_text, encrypted_text)
SELECT
  1,
  plain_text,
  DETERMINISTIC_ENCRYPT(
    KEYS.KEYSET_CHAIN(
      'gcp-kms://projects/my-project/locations/asia-northeast1/keyRings/my-test-keyring/cryptoKeys/my-test-key',
      selected_keyset
    ),
    plain_text,
    ''
  )

鍵セットは my_keysets テーブルから引いてきています。暗号化対象の平文は This is a classified text. という文字列です。これを DETERMINISTIC_ENCRYPT() 関数で確定的暗号化します。

第一引数には、入れ子で関数 KEYS.KEYSET_CHAIN() が入っています。これはラップされた鍵セット (DEK) を Cloud KMS 鍵 (KEK) で復号して鍵セットを返す関数です。

KEYS.KEYSET_CHAIN() の第一引数には Cloud KMS 鍵を、第2引数には my_keysets から引いてきたラップされた鍵セットを指定します。

第二引数には、暗号化したい平文を指定します。

第三引数は additional_data (追加情報) を指定できます。例えば、ID 列の値などです。復号時に同じ値を指定しないとエラーになります。今回は空白としています。

5. データを読み取る(復号なし)

データが書き込めたので、これを読み取ってみます。

SELECT
  *
FROM
  `my-project.my_dataset.my_table` 

以下のような結果となるはずです。

id normal_text encrypted_text
1 This is a classified text. AWtbcFQMr21Zs (中略) ujFJ8LGS8=

暗号化が成功しています。読み取れはしましたが、encrypted_text 列は明示的に復号していないので、意味をなさないものに見えます。

6. データを読み取って復号

次はデータを読み取り、復号してみます。次のサンプル SQL も先ほどと同じように、Web UI だと Query error: deterministic_encrypt requires the keyset chain and its arguments (kms_resource_name and first_level_keyset) to be constant. at [7:1] と出るはずですが、無視して実行可能です。

DECLARE selected_keyset BYTES;
  
SET selected_keyset = (SELECT keyset FROM `my-project.my_dataset.my_keysets` WHERE id = "my_table");
  
SELECT
  id,
  normal_text,
  encrypted_text,
  DETERMINISTIC_DECRYPT_STRING(
    KEYS.KEYSET_CHAIN(
      'gcp-kms://projects/my-project/locations/asia-northeast1/keyRings/my-test-keyring/cryptoKeys/my-test-key',
      selected_keyset
    ),
    encrypted_text,
    ''
  ) AS decrypted_text
FROM
  `my-project.my_dataset.my_table` 

DETERMINISTIC_DECRYPT_STRING() 関数により復号します。

第一引数は、暗号化のときと同じく、DEK を KEK で復号するために KEYS.KEYSET_CHAIN() を使います。

第二引数は暗号化データ、第三引数は暗号化時に指定した additional_data と同じ文字列を指定します(今回は空文字列)。

以下のような結果となるはずです。正しく復号できたことが分かります。

id normal_text encrypted_text decrypted_text
1 This is a classified text. AWtb (中略) GS8= This is a classified text.

トラブルシューティング

Incompatible CryptoKey location

Incompatible CryptoKey location. Resource location: us, Key location: asia-northeast1; error in KEYS.NEW_WRAPPED_KEYSET expression

これは、Cloud KMS 鍵のロケーションと BigQuery データセットのロケーション(正確には、ジョブ実行ロケーション)が異なっているために表示されるエラーメッセージです。

以下のようなユースケースは実業務ではあまりありませんが、以下のように FROM 句にテーブルを指定しないクエリを実行したときも、自動的に US マルチリージョンでジョブが実行されるので、上記のメッセージが出ます(明示的にクエリ設定で KMS 鍵と同じリージョンを指定すれば実行できます)。

SELECT
    KEYS.NEW_WRAPPED_KEYSET(
  'gcp-kms://projects/my-project/locations/asia-northeast1/keyRings/my-test-keyring/cryptoKeys/my-test-key',
  'DETERMINISTIC_AEAD_AES_SIV_CMAC_256') AS wrapped_keyset

Query error: Permission 'cloudkms.cryptoKeyVersions.useToDecryptViaDelegation' denied

Query error: Permission 'cloudkms.cryptoKeyVersions.useToDecryptViaDelegation' denied on resource 'projects/my-project/locations/asia-northeast1/keyRings/key-ring-tokyo/cryptoKeys/tokyo-key' (or it may not exist).; error in DETERMINISTIC_DECRYPT_STRING expression at [5:1]

これは、Cloud KMS 鍵 (KEK) に対して権限を持っていないのに復号関数を使おうとしたときのエラーです。あるいは、キー ID が間違っている可能性もあります。

Query error: deterministic_encrypt requires the keyset chain and its arguments to be constant

Query error: deterministic_encrypt requires the keyset chain and its arguments (kms_resource_name and first_level_keyset) to be constant. at [7:1]

もしくは

Query error: deterministic_decrypt_string requires the keyset chain and its arguments (kms_resource_name and first_level_keyset) to be constant. at [5:1]

これは、前述の検証作業の中でも紹介した、バリデーションのみで表示されるエラーです。BigQuery の Web UI や bq query --dry_run で表示されますが、実際にクエリを実行すると、問題なく実行できます。

DETERMINISTIC_ENCRYPT() 関数の第一引数は定数が入ることが想定されているようです。クエリパラメータ ( @〜 ) や変数定義でリテラルを指定していればこの警告は出ませんが、当記事の検証のように、SELECT 文でキーセットを取得するような処理を書くとこの現象がおきます。

Query error: AEAD.DECRYPT_STRING failed

Query error: DETERMINISTIC_ENCRYPT failed: Creation of Deterministic AEAD primitive failed: Primitive type N6crypto4tink17DeterministicAeadE not among supported primitives N6crypto4tink8CordAeadE, N6crypto4tink4AeadE for type URL type.googleapis.com/google.crypto.tink.AesGcmKey; error in DETERMINISTIC_ENCRYPT expression at [7:1]

もしくは

Query error: AEAD.ENCRYPT failed: Creation of AEAD primitive failed: Primitive type N6crypto4tink4AeadE not among supported primitives N6crypto4tink17DeterministicAeadE for type URL type.googleapis.com/google.crypto.tink.AesSivKey; error in AEAD.ENCRYPT expression at [7:1]

暗号化関数に対応していない種類の鍵で暗号化 / 復号しようとすると、このエラーが出ます。

前述の検証作業で見たように、確定的な関数では DETERMINISTIC_AEAD_AES_SIV_CMAC_256 を、非確定的関数で使うには AEAD_AES_GCM_256 を指定して鍵セットを作成する必要があります。

Query error: DETERMINISTIC_DECRYPT_STRING failed

Query error: DETERMINISTIC_DECRYPT_STRING failed: decryption failed; error in DETERMINISTIC_DECRYPT_STRING expression at [5:1]

暗号化時に指定した additional_data (追加情報) と同じものを復号化時に指定しないと、上記のエラーとなります。

深掘り情報

鍵セットのローテーション

ローテーションの概念

鍵セットは SQL の KEYS.ROTATE_WRAPPED_KEYSET() 関数を使ってローテーションすることができます。

鍵セットの中身はプライマリ暗号鍵とセカンダリ暗号鍵 (群) に分かれています。鍵セットに対してローテーションをかけると、プライマリ暗号鍵がセカンダリ暗号鍵に降格し、新しいプライマリ暗号鍵が生成されます。セカンダリ暗号鍵はローテーションするたびに増えていきます。

つまり、latest な最新版の鍵がプライマリ暗号鍵であり、過去バージョンがセカンダリ暗号鍵です。セカンダリ暗号鍵は、過去に暗号化したデータを復号するために残しておく必要があります。セカンダリ暗号鍵は無効化または破棄することができ、無効化や破棄をすると、そのバージョンで暗号化されたデータは復号できなくなります。

これは Cloud KMS 鍵のバージョニングとよく似ています。鍵の定期的なローテーションにより、万一 DEK が漏洩した場合でも、攻撃者が解読可能なデータの範囲が少なくなります。

影響

鍵セットをローテーションすると、確定的暗号関数でアウトプットされる出力結果は、ローテーション前とは違うものになります。

暗号化関数を使った暗号化には常にプライマリ暗号鍵が使われる仕様だからです。鍵セットをローテーションするとプライマリ鍵が新しくなるので、確定的暗号化関数の結果も異なるものになってしまいます。

一方で、復号の際は単に鍵セットを指定すれば、自動的に過去のセカンダリ暗号鍵から適切なものが使われて復号されます。

確定的暗号関数のアウトプットを集計や結合に利用している場合は、鍵セットをローテーションする際に「鍵セットをローテーションする」→「暗号化済みのデータを全て復号して新しい鍵で再度、暗号化する」というプロセスが必要になります。

また古いセカンダリ暗号鍵は無効化や破棄が可能ですが、無効化・破棄すると復号に使えなくなるので、これを行う場合は非確定的暗号化関数を使っているデータでもやはり「鍵セットをローテーションする」→「暗号化済みのデータを全て復号して新しい鍵で再度、暗号化する」というプロセスが必要です。

メモリ上の DEK

列レベル暗号化されたデータを読み取る際、SELECT 時にラップされた鍵セット (DEK) が Cloud KMS 鍵 (KEK) で復号され、一時的に DEK がメモリ上に保持されて、BigQuery はこれを使ってデータの復号を行います。

メモリ上の DEK はクエリの期間中のみメモリに保存され、その後破棄されます。

杉村 勇馬 (記事一覧)

執行役員 CTO / クラウドソリューション部 部長

元警察官という経歴を持つ現 IT エンジニア。クラウド管理・運用やネットワークに知見。AWS 12資格、Google Cloud認定資格11資格。X (旧 Twitter) では Google Cloud や AWS のアップデート情報をつぶやいています。