Ruby on Rails (DBはMySQL) で開発をしている某案件で
運用の都合上、アプリ外から SQL でデータベースの内容を直接参照できる必要があるので、センシティブなデータは AES_ENCRYPT 関数で暗号化して、アプリ以外からも復号できるようにすること
という要件がありました。
単に暗号化すればいいだけなら attr_encrypted gem などを使って透過的に Ruby 側で暗号化/復号すれば楽に実装できますが、いちいち MySQL 側で AES_ENCRYPT/AES_DECRYPT させるとなると、かなり実装が面倒です。
そこで、Ruby 側で MySQL の AES_ENCRYPT/AES_DECRYPT と同一のアルゴリズムで透過的に暗号化/復号する方法を考えてみました。
結論
attr_encrypted gem を使い、キーとパラメータを下記のように設定すれば OK です。
class SomeModel < ActiveRecord::Base
attr_encrypted :email,
:algorithm => "aes-128-ecb",
:iv => "",
:key => :generate_key,
:encode => false
def generate_key()
key = "KEY_STR"
final_key = "\0" * 16
key.length.times do |i|
final_key[i % 16] = (final_key[i % 16].ord ^ key[i].ord).chr
end
return final_key
end
end
ちなみに動作を確認した環境は
- Ruby 1.9.3 p374
- Rails 3.2.13
- attr_encrypted 1.2.1
- MySQL 5.1.68
詳細
attr_encrypted で透過的に暗号化/復号する
attr_encrypted は、透過的に暗号化/復号するための gem です。gem install attr_encrypted
でインストールできます。
Rails で ActiveRecord を使っている場合、
class CreateSomeModels < ActiveRecord::Migration
def change
create_table :some_models do |t|
t.binary :encrypted_email
t.timestamps
end
end
end
というテーブルに対して、
class SomeModel < ActiveRecord::Base
attr_encrypted :email
end
というモデルを作ると、
model = SomeModel.new
model.email = "foo@bar.com"
model.save!
と書けば文字列 "foo@bar.com" が暗号化されて encrypted_email カラムに保存されます。デフォルトではモデルの属性に対応するカラムには encrypted_
というプレフィックスを付けなければなりませんが、オプションでプレフィックスやサフィックスを調整することが可能です。
この gem をベースに、パラメータなどを調整して AES_ENCRYPT/AES_DECRYPT 互換の暗号化/復号を実現します。
暗号化アルゴリズム、初期化ベクトルを変更する
attr_encrypted (というか内部で暗号化/復号に使ってる encryptor gem) はデフォルトで aes-256-cbc アルゴリズムを使うようになっていますが、MySQL 5.1 リファレンスの 12.13. Encryption and Compression Functions によると、MySQL 5.1の実装では
Block Length: 128bit
Block Mode: ECB
Data Padding: Padded by bytes which Asc() equal for number of padded bytes (done automagically)
Key Padding: 0x00 padded to multiple of 16 bytes
IV: None
ということなので、暗号化アルゴリズムを aes-128-ecb に変更します。
また、初期化ベクトルを明示的に指定しないと、内部で OpenSSL::Cipher.pkcs5_keyivgen が使われてしまう (このメソッドは内部で初期化ベクトルを生成する) ので、明示的に空の初期化ベクトルを指定する必要があります。
アルゴリズムは attr_encrypted メソッドの :algorithm、初期化ベクトルは :iv で変更できます。
class SomeModel < ActiveRecord::Base
attr_encrypted :email,
:algorithm => "aes-128-ecb",
:iv => ""
end
キーを変換する
MySQL は AES_ENCRYPT/AES_DECRYPT に指定されたキーを変換した上で暗号化/復号に使用しています。
どんな実装になっているかというと
static int my_aes_create_key(KEYINSTANCE *aes_key,
enum encrypt_dir direction,
const char *key, int key_length)
{
uint8 rkey[AES_KEY_LENGTH/8];
uint8 *rkey_end=rkey+AES_KEY_LENGTH/8;
uint8 *ptr;
const char *sptr;
const char *key_end=key+key_length;
bzero((char*) rkey,AES_KEY_LENGTH/8);
for (ptr= rkey, sptr= key; sptr < key_end; ptr++,sptr++)
{
if (ptr == rkey_end)
ptr= rkey; /* Just loop over tmp_key until we used all key */
*ptr^= (uint8) *sptr;
}
...
}
AES_KEY_LENGTH は 128 と定義されているので、キーの変換は次のような処理になっています。
- 0 で初期化された 16 バイトの配列 rkey を作る。
- 指定されたキー key と rkey の XOR をとって rkey に格納していく。key と rkey の 1 バイト目の XOR を rkey の 1 バイト目に格納、key と rkey の 2 バイト目の XOR を rkey の 2 バイト目に格納... という感じ。
- カウンタが 16 の倍数を超えたら、rkey の方のカウンタを 1 に戻す。key の 17 バイト目 と rkey の 1 バイト目の XOR を rkey の 1 バイト目に格納、key の 18 バイト目と rkey の 2 バイト目の XOR を rkey の 2 バイト目に格納... という感じ。
これを Ruby のコードに起こしたのが、下記の generate_key メソッドです。
def generate_key()
key = "KEY_STR"
final_key = "\0" * 16
key.length.times do |i|
final_key[i % 16] = (final_key[i % 16].ord ^ key[i].ord).chr
end
return final_key
end
attr_encrypted では、:key でキー生成メソッドを指定することができるので、some_model.rb は次のようになります。
class SomeModel < ActiveRecord::Base
attr_encrypted :email,
:algorithm => "aes-128-ecb",
:iv => "",
:key => :generate_key
def generate_key()
key = "KEY_STR"
final_key = "\0" * 16
key.length.times do |i|
final_key[i % 16] = (final_key[i % 16].ord ^ key[i].ord).chr
end
return final_key
end
end
エンコードを抑制する
ActiveRecord で attr_encrypted を使う場合、デフォルトでは暗号化した結果を BASE64 エンコードしたうえで格納するようになっています。AES_DECRYPT するだけで復号できるようにしたいので、:encode => false を指定してエンコード処理を抑制します。
以上で、AES_ENCRYPT/AES_DECRYPT 互換の方式で透過的に暗号化/復号を行えるモデルの完成です。
class SomeModel < ActiveRecord::Base
attr_encrypted :email,
:algorithm => "aes-128-ecb",
:iv => "",
:key => :generate_key,
:encode => false
def generate_key()
key = "KEY_STR"
final_key = "\0" * 16
key.length.times do |i|
final_key[i % 16] = (final_key[i % 16].ord ^ key[i].ord).chr
end
return final_key
end
end