2012年5月28日月曜日

web2py での暗号化フィールドの設定方法

今回は web2py での暗号化フィールドの設定方法を説明します。

プラグインを作成しました。次のリンクからダウンロード可能です。  →   CRYPT_AES
ダウンロードしたファイルを、プラグインとしてアプリケーションに登録すれば利用可能です。
web2py のハッシュ化設定

web2py で暗号化というと、バリデータでの設定によるハッシュ化を思い浮かべるのではないだろうか。例えば次のような設定だ。

1
2
3
4
5
6
table = db.define_table('user',
    Field('name', 'string', notnull=True, unique=True),
    Field('access_id', 'string', notnull=True),
    Field('password', 'password'),
    format='%(name)s')
table.password.requires=CRYPT()

モデル定義(db.py)で、userテーブルの定義を行なっている。この中の6行目では、passwordフィールドにCRYPT()を指定し、バリデータを設定している。これはフォームで入力するpasswordの値を、MD5ハッシュ値に変換しデータベースに格納する。

普通は MD5 ではなく次のように、HMACによるハッシュ値を算出するように設定することが多い。

table.password.requires=CRYPT(key=Auth.get_or_create_key())

もしくは、

table.password.requires=CRYPT(key=auth.settings.hmac_key)

ハッシュ化は元の値を復元できないため、安全性が高い。用途としてはパスワードの照合など、入力値をハッシュ化して値と比較するなどに使われる。

しかし元の値を復元できる暗号化を行い、暗号をデータベースに格納したいことがある。例えば、システムのユーザIDとパスワードを暗号化して保存し、必要な時に復元し利用する場合などだ。このような場合、どうすればよいだろうか?。暗号化はweb2pyは標準では対応していない。しかし次のように、多少の設定を行えば利用できる。

web2py での暗号化データベースの利用

web2py で暗号化したデータベースを利用するには、二通りの方法がある。一つは暗号化に対応したデータベース管理システムを使用することだ。Oracle、DB2、MYSQL といった主要な RDBMS では、データベースの暗号化機能を提供している。これらを利用すればパフォーマンスなどの利点も多い。しかしデータベース固有の機能を利用するためデータベースパッケージに依存する、暗号化機能を提供していないデータベースでは利用できない、など不利な点がある。

暗号化のもう一つの方法は、web2pyでデータを暗号化した後にデータベースに格納する方法である。これはハッシュ化と同様にバリデータで設定することで実現する。今回はこの方法での設定方法を説明していくが、実は次の元ネタがある。

参考: What is the best way to encrypt stored data in web2py?

元ネタのコードは、若干おかしく正常に動作しない。また、M2Secret というパッケージを使っている。この M2Secret は内部で M2Crypto を使用している。M2Crypto は easy_install コマンドではインストールできないが、各OS向けのインストーラをダウンロードして実行すればインストール可能だ。しかし M2Crypto は Pure Python ではないため、一部環境では動作しない。

このため今回は、PyCrypto を利用する。PyCrypto の注意点としては、easy_install コマンドでインストール可能だが、コンパイラー環境が整っていないとエラーで止まるようだ。環境を整えてもよいが開発環境だけの話なら、コンパイル済みのバイナリーを幾つかのサイトで配布しているので、これらを使ってもよい。

また PyCrypto は、Google App Engine(GAE)では環境に含まれているため、設定するだけで利用可能となる(設定方法は、後ほど説明します)。

参考: importing m2crypto to google app engine

PyCrypto を利用したバリデータクラスの用意

まず最初に、PyCrypto を開発用マシンにインストールする。

次に、web2py 管理画面を開き、アプリケーションに新しいモジュール(Modules)を追加する。このモジュールは PyCrypto を使用し、web2py用のバリデータクラスを定義する。モジュールのソースは次のようになる(モジュールファイル名は crypt_aes.py )。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
from gluon import *
from Crypto.Cipher import AES
import base64
import os

class CRYPT_AES:
    block_size = 32
    block_size_list = (16, 24, 32)
    def __init__(self, key=None): 
        self.padding = '{'
        if key is None:
            key = CRYPT_AES.get_or_create_key()
        elif len(key) in self.block_size_list:
            self.block_size = len(key)       
        else:
            raise TypeError, "Key length is not allowed"
        self.cipher = AES.new(key)
    def __call__(self,value):
        value += (self.block_size - len(value) % self.block_size) * self.padding
        return (base64.b64encode(self.cipher.encrypt(value)), None)
    def formatter(self,value):
        try:
            return self.cipher.decrypt(base64.b64decode(value)).\
                                                        rstrip(self.padding)
        except:
            return None
                
    @classmethod
    def get_or_create_key(cls, filename=None, block_size=None):
        if block_size in cls.block_size_list:
            cls.block_size = block_size
        request = current.request
        if not filename:
            filename = os.path.join(request.folder,'private','validator.key')
        if os.path.exists(filename):
            key = base64.b64decode(open(filename,'r').read().strip())
        else:
            key = base64.b64encode(os.urandom(cls.block_size))
            open(filename,'w').write(key)
        return key

このソースは、こちらのサイトからもダウンロードできる。ファイルをダウンロードした後にweb2py管理画面で、ダウンロードファイルを指定して「アップロード」すれば、簡単にモジュールをアプリケーションに追加可能だ。

参考: bitbucket.org - crypt_aes

次に、バリデータクラスの使い方を説明する。

暗号化バリデータクラスの使い方

暗号化用バリデータクラスは、暗号化したいテーブルのフィールドの requires 属性に、CRYPT_AES() とクラスのインスタンスを指定すればよい。設定例を次に示す。

1
2
3
4
5
6
7
8
from crypt_aes import CRYPT_AES
table = db.define_table('user',
    Field('name', 'string', notnull=True, unique=True),
    Field('access_id', 'string', notnull=True),
    Field('password', 'password'),
    format='%(name)s')
table.access_id.requires=crypt_aes=CRYPT_AES()
table.password.requires=crypt_aes

これによって、access_idフィールドとpasswordフィールドは、AES で暗号化される。AESの秘密鍵は、privateフォルダの validator.key ファイルが存在する場合は、そこから読みだす。もしファイルがない場合は、ランダム値を秘密鍵にしてファイルを作成し保存する。

秘密鍵のファイルを指定したり、秘密鍵を直接渡すことも可能だ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import os
from crypt_aes import CRYPT_AES
table = db.define_table('user',
    Field('name', 'string', notnull=True, unique=True),
    Field('access_id', 'string', notnull=True),
    Field('password', 'password'),
    format='%(name)s')
keyf = os.path.join(request.folder,'private','access_id.key')
table.access_id.requires=CRYPT_AES(CRYPT_AES.get_or_create_key(keyf))
table.password.requires=CRYPT_AES('master-key0123456789012345678901')

8-9行目で access_idフィールドに対して get_or_create_key クラスメソッドを使って、privateフォルダの access_id.key ファイルを秘密鍵に使用するよう指定している。また10行目では、passwordフィールドに対して直接、秘密鍵を指定している。特に秘密鍵を直接渡す場合、鍵の長さに注意する必要がある。これは AESでは、16・24・32の各バイト長で、鍵を指定する必要があるためである。

Google App Engine での暗号化バリデータクラスの利用

Google App Engine(GAE)で、暗号化バリデータクラスを利用する場合の注意点を説明する。GAE環境では PyCrypto が含まれているため、次のように app.yaml ファイルに次の設定を追加する必要がある。

libraries:
- name: pycrypto
  version: latest

この設定を追加することによって、アプリケーションで PyCrypto が利用可能となる。

また web2py で利用する外部モジュールは web2pyの site-packages フォルダに設置したりするが、GAEでは PyCrypto を web2pyの site-packages フォルダに設置した場合、動作しなくなるようだ。このため、app.yaml の設定を行うだけにする。

最後に

今回の設定ではバリデータを使っているため、フォームや SQLTable を使用しない処理では暗号化・復号化ができません。この場合、明示的にバリデータを呼び出すメソッドを使う必要があります。これらのメソッドを幾つか挙げてみます。

必要に応じて、これらのメソッドを利用してください。

この他、この記事を書くために参考にして、まだ紹介していないサイトは以下の通りです。
Mattari Diary - GAEのデータストアに、PyCryptoの暗号化した文字列を登録、呼び出してみた
Code Koala - AES Encryption in Python Using PyCrypto
X-LABO - GAE 1.6.0 : python 2.7 PyCrypto ImportError