pgcrypto

pgcrypto

NTT オープンソースソフトウェアセンタ 笠原 辰仁

 

はじめに

PostgreSQLのcontribモジュールの一つに、pgcryptoというものがあります。pgcryptoは、その名の通り暗号化をサポートするツールです。主にはDB内のデータの暗号化を実施したい場合に使われます。オンラインマニュアルに詳しい機能の解説や参考文献へのリンクが載っています。本記事では、pgcryptoの簡単な使い方と、pgcryptoを使う際に有用なテクニックを紹介します。

暗号化について

昨今では、個人情報保護法に備えるため、暗号化を必要とするケースが増えてきました。特にデータベースには重要な個人情報が格納されるため、特にシビアな暗号化を検討されることが多いです。

一口に暗号化と言っても、ストレージのハードウェア部分で暗号化を施す、ファイルシステム(ブロックデバイス)で暗号化を施す、盗聴に備えて暗号化したデータで通信する・・などの様々な方法があります。それぞれの方法により "保護可能な範囲"、"リスク(脅威)として許容する範囲" があります。例えば、先に挙げたファイルシステムでの暗号化の場合、メディアの盗難 (データの物理的な盗難) には有用です。ただし、これらの暗号化手法はサーバにアクセスするユーザに対しては透過的に復号化したデータを見せることが多いため、サーバへの不正アクセスには無力だったりします。まずは、どんなリスクから何を守るべきなのかを熟考しましょう。

pgcryptoの特長

pgcryptoはデータベース内のデータを暗号化することが可能です。暗号化は特定のテーブルの特定のカラム、といった様に細かな粒度で指定できます。暗号化/復号化は、データベースに登録された関数で行うため、復号化パスワードなどが十分に守られていれば、不正アクセスの脅威への対抗手段として有用です。ただし、DB上で復号するため、DBサーバと通信するAPサーバ間の通信盗聴には無力です。このような場合にはSSLの利用などを考慮します。

pgcryptoが提供する機能

pgcryproは、暗号化のための関数をいくつか用意しています。代表的なものは以下となります。

  • ハッシュ関数
    • 汎用的なハッシュ関数やパスワード用のハッシュ関数が用意されています。
  • 暗号化関数
    • 暗号化/復号化を実施できる関数です。単純な暗号化をサポートしているものや、PGPを利用した暗号化関数が用意されています。

本記事では暗号化関数について解説していきます

pgcryptoのインストール

pgcryptoはPostgreSQLのcontirbモジュールです。そのため、インストールは容易です。

ソースからのインストール

ソースからのインストールは以下の様にします。前提として、既にPostgreSQLをソースからビルドし、インストール済みの状態とします。

 -- $PGSRCはPostgreSQLのソースディレクトリです。
 $ cd $PGSRC/contrib/pgcrypto
 $ make && make install

RPMからのインストール

RPMからのインストールは以下の様にします。pgcryptoはpostgresql-contrib-*.rpmに含まれていますので、これを導入するだけです。

 -- 例としてRHEL5(x86_64)用のPostgreSQL8.4.4を使っています
 # cd /some/where
 # rpm -ivh postgresql-contrib-8.4.4-1PGDG.rhel5.x86_64.rpm

pgcrypto関数の登録

他のcontribモジュールと同様、pgcryptoの関数を使用したいDBに関数を登録します。

 -- PostgreSQLをソースで/usr/local/pgsql にインストールした場合
 $ psql -f /usr/local/pgsql/share/contrib/pgcrypto.sql -d <DB名>

 -- PostgreSQLをRPMでインストールした場合
 $ psql -f /usr/share/pgsql/contrib/pgcrypto.sql -d <DB名>

以上で準備は終わりです。

暗号化を試す

簡単に暗号化を試してみましょう。本記事で使用する関数は下記です。それぞれの機能の詳細はマニュアルを参照して下さい。

  • encrypt/decrypt
    • 単純な暗号化/復号化を実施する関数です。機能は限定されており、暗号強度も強くありませんが、暗号化/復号化のオーバーヘッドは低いです。
  • pgp_sym_encrypt/pgp_sym_decrypt
    • 高機能な暗号化/復号化を実施する関数です。豊富な機能を備えていますが、暗号化/復号化のオーバーヘッドは高いです。

では、実際の例です。まず、テーブルを作り、encrypt関数とpgp_sym_encrypt関数のそれぞれを使用して暗号化した情報を入れてみます。

 =# CREATE TABLE test (id int, name bytea); -- 暗号化したデータはbytea型
 =# INSERT INTO test SELECT 1, 
    encrypt(convert_to('鈴木','UTF8'), 'pass'::bytea, 'aes');
 =# INSERT INTO test SELECT 2, pgp_sym_encrypt('佐藤', 'pass');
 =# INSERT INTO test SELECT 3, pgp_sym_encrypt('佐藤', 'pass');

 =# SELECT * FROM test;
  id |                             name
 ----+-----------------------------------------------------------------
   1 | Hd1]\0218\227~JV\304\377\301\346\034\340
   2 | \303\015\004\007\003\002D\270\002\334[\351k\223o\3227
       \001<\225\\\360\255[1\260"w%\005\005Uz\017\322I\366/xo
       \235\300\204f\370.\270D\362j\242\237?\367`+=\374c\242
       c3\270M\356]p\245\353\371\240\371
   3 | \303\015\004\007\003\002\327\317\241yc\244\227\330~\
       3227\001\2271\217;\201A60\262\205\370\\#\233\372\351
       m@k\241\0226J\020\340\262\206=gH\377\346\355t\234\241
       \276O\301\351\024>\302L\212A\316\227\243n7J\252\346
 (3 行)
 ★幅の関係で1行のデータを改行しています。

"鈴木"、"佐藤"の情報が暗号化されて格納されています。なお、pgp_sym_encryptは、同じ文字、同じパスワードでも毎回異なるバイト情報が生成されます。id=2と3は同じ"佐藤"の文字ですが、格納されているバイト情報が異なっています。

では、暗号化した情報を復号してみます。

 =# SELECT id, convert_from(decrypt(name, 'pass'::bytea, 'aes'),
    'UTF8') FROM test;
  id | convert_from
 ----+--------------
   1 | 鈴木
 (1 行)
 
 =# SELECT id, pgp_sym_decrypt(name,'pass') FROM test 
    WHERE id IN (2,3);
  id | pgp_sym_decrypt
 ----+-----------------
   2 | 佐藤
   3 | 佐藤
 (2 行)

復号化ができました。なお、encrypt関数はバイト型のデータのみを暗号化対象として受け付けるため、text型のデータをbytea型に変換する必要があります。一部の状況では不可逆変換となってしまうため、convert_to/fromやencode/decodeなどの関数を使用すると良いでしょう。

暗号化処理のラッピング

暗号化の処理のたびに、毎回上記の様な関数を記述するのは面倒です。また、暗号化関数のオプションの変更時などの修正箇所も多くなります。お奨めは、ビューやトリガでのラッピングです。SELECT時は、復号化した情報を見せるよう、VIEWとして定義します。また、INSERTやUPDATE時などは、自動で暗号化した情報がテーブルに格納されるよう、トリガを使用すると良いでしょう。例えば、下記の様なVIEWを使うと閲覧が楽です。

 =# CREATE TABLE test (id int, name bytea);
 =# INSERT INTO test SELECT 1, pgp_sym_encrypt('鈴木', 'pass');
 =# INSERT INTO test SELECT 2, pgp_sym_encrypt('佐藤', 'pass');
 =# INSERT INTO test SELECT 3, pgp_sym_encrypt('田中', 'pass');
 =# CREATE VIEW dec_test AS SELECT 
    id, pgp_sym_decrypt(name,'pass') FROM test;

 =# SELECT * FROM dec_test;
  id | pgp_sym_decrypt
 ----+-----------------
   1 | 鈴木
   2 | 佐藤
   3 | 田中
 (3 行)

暗号キーの置き場所

さて、上記でビューを使った例を出しましたが、復号化用のパスワードが見えています。ビューの定義はほとんどのユーザで確認できるため、セキュリティの穴になりやすいでしょう。また、パスワードの変更時はパスワードの記述箇所を全て修正する必要があります。パスワードの実体はなるべく1箇所においておくのが無難です。

このような時は、パスワードを返す関数を作成しておくと良いでしょう。お奨めの方法例を下記に挙げます。

current_setting関数を利用する

postgresql.confには、"custom_variable_classes"というパラメータがあり、ユーザ独自のパラメータを定義できます。そこで、ここにパスワードを持たせ、PostgreSQLの各パラメータを返すcurrent_setting関数を利用します。

 -- postgresql.confに以下を記述
 # my_app クラスを定義
 custom_variable_classes = 'my_app'
 # my_app.passwd に 'secret'というパスワードを指定
 my_app.passwd='secret'

 -- 上記を有効にするため reload
 $ pg_ctl reload

 -- パスワードを取得
 $ psql -c "SELECT current_setting('my_app.passwd')"
  current_setting
 -----------------
  secret
 (1 行)

 -- 暗号化時はこれを利用
 $ psql -c "INSERT INTO test SELECT 4, 
   pgp_sym_encrypt('東京', current_setting('my_app.passwd'));

 -- 環境変数にも指定可能(動的に設定できる)
 $ export PGOPTIONS="-c my_app.passwd2=dyn"
 $ psql -c "SELECT current_setting('my_app.passwd2')"
  current_setting
 -----------------
  dyn
 (1 行)

上記の方法を取ると、postgresql.confや環境変数でパスワードを一括管理できます。また、PL/pgsqlなどを使い、関数内にパスワードを記述してそれを返す、という方法もあります。方法は様々あり、それぞれの方法で抱えるメリット、リスクがありますので、要件に一番見合った方法を模索して下さい。

暗号化後のサイズ

暗号化を実施すると、当然ながらデータ量が変化します。また、暗号化のアルゴリズムなどで増えるデータ量が変化します。一般的には、ヘッダの付与等でデータ量が増加しますので、簡単なテストデータと使用予定の暗号化関数を使い、事前に見積りをしておくと安心です。おおよそですが、encrypt関数(AES128bit)を用いた場合は、元データに 1 ~ 15 byte を足したサイズとなります。pgp_sym_encrypt関数の場合は、さらに多く十数~数十バイトを足したサイズになります。

インデックス検索

暗号化をするとよく問題になるのはインデックス検索についてです。暗号化後のデータは元のデータと異なります。インデックスは、テーブルに格納された暗号化後のデータから生成されるため、暗号化前のデータを条件とした検索にインデックスが使用できません。そのため、場合によっては、インデックスだけは復号した情報で作成する必要が出てきます。インデックスを直接閲覧する機能はPostgreSQLにはありませんが、バイナリエディタなどでインデックスのファイルを見られた場合には実データが見えてしまいます。性能とセキュリティのトレードオフになることに注意しましょう。インデックスのみを復号化するには、下記の様に復号化のための関数インデックスを使用します。インデックス定義にパスワードを含めると、先のビューの様に他人に見えてしまうため、暗号化処理をラッピングした関数を用意しておくと良いでしょう。

 =# CREATE OR REPLACE FUNCTION my_decrypt (bytea) RETURNS text AS $$
    SELECT convert_from(decrypt($1, 
     (SELECT current_setting('my_app.passwd'))::bytea, 'aes'), 'UTF8');
    $$ LANGUAGE SQL IMMUTABLE;
 =# CREATE INDEX dec_index ON t (my_decrypt(encrypted_column));
 =# SELECT * FROM t WHERE my_decrypt(encrypted_column) = XXXX;

なお、encrypt関数は、同じ文字からは毎回同じ暗号化データが生成されます。そのため、暗号化したデータでインデックスを作成した状態でも、WHERE条件に暗号化後のデータを使った完全一致検索のみは可能です。

その他

暗号化に関する細かな注意点やTIPSです。

s2k-modeについて

pgp_sym_*関数は、機能が豊富な半面、非常に暗号化/復号化のオーバーヘッドが高いです。オーバーヘッドの主な要因はソルトの計算です。ソルトは、暗号化の際に使用するパスワードを推測されにくくするために使われるものです。pgp_sym_*関数のs2k-modeオプションでソルトのアルゴリズムを変更できます。s2k-modeは、0、1、3の3つを指定可能です。デフォルトは最も複雑性の高いアルゴリズムを使うs2k-mode=3となっています。これをs2k-mode=1に変えることで、かなりのオーバーヘッドを抑えることができます。暗号強度とのトレードオフになりますが、このオプションは覚えておくと良いでしょう。

使用するアルゴリズムについて

pgcryptoでは、いくつかの暗号化やハッシュ化の際に使用するアルゴリズムを指定可能(暗号化であればaesやbfなど)です。どれを使えば良いか悩んでしまう場合は、電子政府推奨の暗号リストを参考にすると良いでしょう。例えば、共通鍵にはAESの鍵長が128bit以上のものが推奨されています。なお、pgcryptoのencryptやpgp_sym_encryptは、デフォルトでAESの鍵長128bitが指定されています。

パスワードの変更について

暗号化用のパスワードは定期的に変更することが望ましいとされています。ただし、パスワードの変更はデータの再作成(新規パスワードでの再暗号化)が必要になります。一旦復号化したデータを外部にダンプし、再度暗号化してDBに格納する方法や、CREATE TABLE AS SELECT?を利用して復号化→暗号化を一緒に実施する方法があります。ただ、いずれにしろ作業用の一時ディスク領域と時間が必要なので、運用設計やストレージ設計の時点で考慮に入れていくと良いでしょう。

サーバログについて

意外と見落とされがちですが、ERRORや長時間かかったSQL文がサーバログに記録された際、復号化した情報が見えてしまうことがあります。典型的には、WHERE句の条件にある氏名情報などです。サーバログの難読化、あるいはOSのレイヤ等の機能を利用して、これらのログへの不正なアクセスにも気をつけましょう。

おわりに

いかがでしたでしょうか?暗号化は昨今に必要とされる機能ですが、制約や注意すべきものがたくさんあります。「情報を守ること」ではなく「暗号化をすること」が目的にならないよう、様々なリスクや制約を考慮しながら最善の方法を模索して下さい。pgcryptoと本記事が、その役に立てられれば幸いです。


(2010年9月22日 公開)