Help us understand the problem. What is going on with this article?

Railsで簡単可逆暗号(ActiveSupport::MessageEncryptor)

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_generatoriterations は 1000
  • generate_key('salt', key_len)salt は処理の目的ごとに変えること
    • 例) ユーザIDごとに異なるsecretを作りたい場合: generate_key("user_#{@user.id}")

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

secretcipher 両方変わった場合も同様の方法で対応できる

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 は何秒後を数値で指定する
    • 36001.year など
  • expires_at は期限切れとなる時刻を直接指定する
    • 3600.seconds.since1.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きーたキータ"
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした