Railsで簡単に可逆暗号をする方法
情報が古くなったため、rails4.2, rails5.0, rails5.1, rails5.2, rails6.0 で変更点確認し更新
使い方
ActiveSupport::MessageEncryptor
を利用することで簡単に可逆暗号ができる
key_len = ActiveSupport::MessageEncryptor.key_len
secret = Rails.application.key_generator.generate_key('salt', key_len)
crypt = ActiveSupport::MessageEncryptor.new(secret)
encrypted = crypt.encrypt_and_sign('Qiitaきーたキータ')
# => "Adp1SPOkYRqLgTMoAlcyEganoimCdT8k/tj4zQXZiUiN--9A34IcmwD9pG4ysj--+drve0A05vHPXYF33aw4hg=="
crypt.decrypt_and_verify(encrypted)
# => 'Qiitaきーたキータ'
注意事項
- 同一アプリケーション内で完結する処理(DB/Redisに暗号化した値をいれて同一アプリケーションで複合するなど)であれば、
Rails.application.key_generator
を使うこと-
secret_key_base
の値がデータ交換先で異なる場合はActiveSupport::KeyGenerator.new(secret, iterations: 10_000)
を使う -
new
で生成する場合はiterations
の値も合わせておくこと(Default: 65536) - ただし、
Rails.application.key_generator
のiterations
は 1000
-
-
generate_key('salt', key_len)
のsalt
は処理の目的ごとに変えること- 例) ユーザIDごとに異なるsecretを作りたい場合:
generate_key("user_#{@user.id}")
- 例) ユーザIDごとに異なるsecretを作りたい場合:
Rails 5.1 以下から Rails 5.2 以上にする場合
デフォルトの暗号化方式が aes-256-cbc
から aes-256-gcm
へ変化している
その為、暗号化済みのデータはそのまま複合することができない
Rails 5.1 で作った暗号化データをRails 5.2で復号
key_len = ActiveSupport::MessageEncryptor.key_len
secret = Rails.application.key_generator.generate_key('salt', key_len)
crypt = ActiveSupport::MessageEncryptor.new(secret)
message = crypt.encrypt_and_sign('Qiitaきーたキータ')
=> "SGVOekxRajVRUDh1cGNFeEIwRzNwMjdycWwyQkZZR0QvWU5sM3lNcnVSUFUwSUE3WFpTR1ZPbFZBMTFIbUZ2dS0tWG03Z0ordFUxeUJOaXY4bkIzVU5Odz09--8803cdabfddfe05668780d2b942ee431a25536d7"
# NOTE: 5.1で作ったsecret を `secret.unpack('H*').first` で HexString化
secret_hex = 'ae0eaf200c046e08f52c32c0501d129dff016f47bf68fa3b6a38929b58e7467e'
secret = [secret_hex].pack('H*')
message = "SGVOekxRajVRUDh1cGNFeEIwRzNwMjdycWwyQkZZR0QvWU5sM3lNcnVSUFUwSUE3WFpTR1ZPbFZBMTFIbUZ2dS0tWG03Z0ordFUxeUJOaXY4bkIzVU5Odz09--8803cdabfddfe05668780d2b942ee431a25536d7"
crypt = ActiveSupport::MessageEncryptor.new(secret)
crypt.decrypt_and_verify(message)
# => ActiveSupport::MessageEncryptor::InvalidMessage
例外が出ないようにするには
crypt.rotate cipher: 'aes-256-cbc'
crypt.decrypt_and_verify(message)
# => "Qiitaきーたキータ"
on_rotation
を渡すとローテーションされたことの検知もできる
rotation = false
callback = lambda {
rotation = true
puts "rotation!"
}
crypt.decrypt_and_verify(message, on_rotation: callback)
rotation!
=> "Qiitaきーたキータ"
rotation
# => true
secret
と cipher
両方変わった場合も同様の方法で対応できる
crypt.rotate secret2, cipher: 'aes-256-cbc'
Rails 5.2 以降の新機能
PR: https://github.com/rails/rails/pull/29599
暗号化データの有効期限を指定する
expires_at
または expires_in
を渡すことでメッセージの有効期限を決めることができる
有効期限を過ぎた場合には nil
が返るようになる
-
expires_in
は何秒後を数値で指定する-
3600
や1.year
など
-
-
expires_at
は期限切れとなる時刻を直接指定する-
3600.seconds.since
や1.year.since
など
-
secret = Rails.application.key_generator.generate_key('salt', 32)
crypt = ActiveSupport::MessageEncryptor.new(secret)
message = 'Qiitaきーたキータ'
now = Time.zone.now
Timecop.freeze now
# `expires_in: 10.seconds` と同じ
encrypt = crypt.encrypt_and_sign(message, expires_at: now + 10.seconds)
Timecop.freeze now + 9.seconds
crypt.decrypt_and_verify(encrypt)
# => "Qiitaきーたキータ"
Timecop.freeze now + 10.seconds
crypt.decrypt_and_verify(encrypt)
# => nil
purpose
オプション
purpose
の値が一致していない場合に nil
を返す
指定されていない場合は purpose: nil
が指定されているのと同様の挙動となる
secret = Rails.application.key_generator.generate_key('salt', 32)
crypt = ActiveSupport::MessageEncryptor.new(secret)
message = 'Qiitaきーたキータ'
encrypt = crypt.encrypt_and_sign(message, purpose: 'signup')
crypt.decrypt_and_verify(encrypt)
=> nil
crypt.decrypt_and_verify(encrypt, purpose: 'signup')
=> "Qiitaきーたキータ"
おまけ: 他プラットフォームと交換する場合
正直 ActiveSupport::MessageEncryptor
を使わないで OpenSSL
から書いていったほうがわかりやすい...
暗号化側
serializer: JSON
にしておくと読みやすい
secret = Rails.application.key_generator.generate_key('salt', 32)
crypt = ActiveSupport::MessageEncryptor.new(secret, serializer: JSON)
crypt.encrypt_and_sign(message)
# => "77RDuITdVMcceSBImUQxfyl0PWPfLgb6sw==--Cxh9TgrT+vQjOH/r--MY4Ymi5d4RLUQ4tBfjMYlQ=="
secret.unpack('H*').first
# => "e961542bbc011a568d77bf12c7f4bbb1c83dfee0148f1688d95d97de4779992f"
復号側(PHPの場合)
secret.unpack('H*').first
の結果を事前に渡しておく必要がある
// 渡されたHexString
$hex_key = "e961542bbc011a568d77bf12c7f4bbb1c83dfee0148f1688d95d97de4779992f";
// binary変換
$key = hex2bin($hex_key);
// 送られてきたメッセージ
$message = "77RDuITdVMcceSBImUQxfyl0PWPfLgb6sw==--Cxh9TgrT+vQjOH/r--MY4Ymi5d4RLUQ4tBfjMYlQ==";
// `--` で分割(0: 暗号化データ, 1: iv, 2: authtag)
// authtagは aes-256-gcm の場合に必要
$messages = explode('--', $message);
$encrypted_data = base64_decode($messages[0], true);
$iv = base64_decode($messages[1], true);
$auth_tag = base64_decode($messages[2], true);
// 復号処理
$decypted_data = openssl_decrypt($encrypted_data, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $auth_tag);
echo $decypted_data;
// => "Qiitaきーたキータ"