こんにちは、masm11 です。
Rails に MessageEncryptor というクラスがあります。 何かを暗号化/復号する際に便利ですね。
例えば、
KEY = 'somekeysomekeysomekeysomekeysomekeysomekeysomekey' enc = ActiveSupport::MessageEncryptor.new(KEY, cipher: 'aes-256-cbc')
としておけば、
data = enc.encrypt_and_sign(obj) obj = enc.decrypt_and_verify(data)
で暗号化と復号ができます。
ところが、Ruby 2.4 で仕様が変わってしまったんです。 KEY がちょうど 32文字でないといけなくなりました。
ちょっと試したところでは、元のコードだと
key must be 32 bytes
というエラーが出ますし、
KEY = 'somekeysomekeysomekeysomekeysome'
と 32文字で切り捨てたキーを指定すると、
ActiveSupport::MessageVerifier::InvalidSignature
というエラーが出て復号できません。
さぁ困ったどうしよう、ということで調べた結果です。
結論は一番最後 (まとめ) にあります。
サンプルを作る
まずは Ruby 2.3.3 + Rails 5.2.4.4 でサンプルを作ります。
enc = ::ActiveSupport::MessageEncryptor.new('0123456789abcdefghijklmnopqrstuvwxyz', cipher: 'aes-256-cbc')
長い文字列がキーです。
enc.encrypt_and_sign('short string') => "MGVaT0NTWFkweTdCWFV5SUJVSkh1WkdMaFRscDVUYllGaHlKc2tzZ2hKQT0tLXFXdGJuRmdsV1VmTUVMMUt2VDUyalE9PQ==--8ae97f3fc117a62513fb8ca242f86d893f601c83"
short string
という文字列を暗号化してみました。
その結果、長い文字列が得られています。これが暗号化文字列です。
OpenSSL を直接使って復号してみる
OpenSSL は 1.1.1i を使用しました。
上記の通り、サンプルは --
の左と右に分けられます。
このうち左側を取り出し、base64 デコードします。
$ echo MGVaT0NTWFkweTdCWFV5SUJVSkh1WkdMaFRscDVUYllGaHlKc2tzZ2hKQT0tLXFXdGJuRmdsV1VmTUVMMUt2VDUyalE9PQ== | base64 -d 0eZOCSXY0y7BXUyIBUJHuZGLhTlp5TbYFhyJsksghJA=--qWtbnFglWUfMEL1KvT52jQ==
また base64 っぽい文字列が出てきました。
--
の左側を base64 デコードします。
$ echo 0eZOCSXY0y7BXUyIBUJHuZGLhTlp5TbYFhyJsksghJA= | base64 -d | od -tx1c 0000000 d1 e6 4e 09 25 d8 d3 2e c1 5d 4c 88 05 42 47 b9 321 346 N \t % 330 323 . 301 ] L 210 005 B G 271 0000020 91 8b 85 39 69 e5 36 d8 16 1c 89 b2 4b 20 84 90 221 213 205 9 i 345 6 330 026 034 211 262 K 204 220 0000040
バイナリなので od に食わせています。 これが暗号化されたデータそのものです。
一方、--
の右側を base64 デコードします。
$ echo qWtbnFglWUfMEL1KvT52jQ== | base64 -d | od -tx1c 0000000 a9 6b 5b 9c 58 25 59 47 cc 10 bd 4a bd 3e 76 8d 251 k [ 234 X % Y G 314 020 275 J 275 > v 215 0000020
こちらもバイナリなので od に食わせています。 これは iv (initialization vector) です。 暗号化の計算に使う初期値です。 擬似乱数のシードみたいなものと考えると良いでしょう。
また、キーの16進表記が必要なので、キーも od に食わせます。
$ echo -n 0123456789abcdefghijklmnopqrstuvwxyz | od -tx1c 0000000 30 31 32 33 34 35 36 37 38 39 61 62 63 64 65 66 0 1 2 3 4 5 6 7 8 9 a b c d e f 0000020 67 68 69 6a 6b 6c 6d 6e 6f 70 71 72 73 74 75 76 g h i j k l m n o p q r s t u v 0000040 77 78 79 7a w x y z 0000044
ここまで準備ができたら、いよいよ openssl コマンドで復号します。
$ echo 0eZOCSXY0y7BXUyIBUJHuZGLhTlp5TbYFhyJsksghJA= | base64 -d | openssl enc -d -K 303132333435363738396162636465666768696a6b6c6d6e6f70717273747576 -iv a96b5b9c58255947cc10bd4abd3e768d -aes-256-cbc | od -tx1c 0000000 04 08 49 22 11 73 68 6f 72 74 20 73 74 72 69 6e 004 \b I " 021 s h o r t s t r i n 0000020 67 06 3a 06 45 54 g 006 : 006 E T 0000026
echo 0eZOCSXY0y7BXUyIBUJHuZGLhTlp5TbYFhyJsksghJA= | base64 -d
の部分は先程見ました。
--
の左側を base64 デコードした、暗号化されたデータそのものです。これを openssl の
標準入力に与えています。
openssl enc -d
は復号指示です。
-K 303132333435363738396162636465666768696a6b6c6d6e6f70717273747576
はキーの16進表記です。
先程 od でキーの16進表記を調べたので、それを指定します。ただし、キーの32文字までです。
-iv a96b5b9c58255947cc10bd4abd3e768d
は iv の16進表記です。
こちらも先程 od で16進表記にしていますので、それを指定します。
-aes-256-cbc
はアルゴリズムですね。
その結果を od に食わせています。よく見ると、それらしい文字が見えますね。復号できていそうです。
では、このバイナリから Ruby のオブジェクトを復元してみます。
使用した Ruby は 2.7.2 です。
Marshal.load("\x04\x08\x49\x22\x11\x73\x68\x6f\x72\x74\x20\x73\x74\x72\x69\x6e\x67\x06\x3a\x06\x45\x54") => "short string"
復元できました!
署名を検証する
最初に得たサンプル暗号化文字列のうち、--
より右側は署名です。
これを検証してみます。
OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, '0123456789abcdefghijklmnopqrstuvwxyz', 'MGVaT0NTWFkweTdCWFV5SUJVSkh1WkdMaFRscDVUYllGaHlKc2tzZ2hKQT0tLXFXdGJuRmdsV1VmTUVMMUt2VDUyalE9PQ==') => "8ae97f3fc117a62513fb8ca242f86d893f601c83"
OpenSSL::HMAC.hexdigest
を呼び出しています。
- SHA1 は固定です。
- '0123456789abcdefghijklmnopqrstuvwxyz' はキーです。ここではキー全体を指定しています。
- 'MGVaT0NTWFkweTdCWFV5SUJVSkh1WkdMaFRscDVUYllGaHlKc2tzZ2hKQT0tLXFXdGJuRmdsV1VmTUVMMUt2VDUyalE9PQ==' は暗号化文字列の
--
より左です。
このように計算することで、"8ae97f3fc117a62513fb8ca242f86d893f601c83" が得られました。
これはサンプルの暗号化文字列の --
より右側に一致します。
署名が検証できました。
MessageEncryptor の構造
結局、MessageEncryptor はどのような処理をしているのでしょうか? まとめてみました。
- 暗号化
- シリアライズする (A)
- iv を決める (B)
- B とキー32文字を使って A を暗号化する (C)
- B, C をそれぞれ base64 エンコードする (D, E)
- E と D を
--
を挟んでつなげる (F)
- 署名
- キー全体を使って F の HMAC を計算する (G)
- F と G を
--
を挟んでつなげる
上の復号手順は、これを逆にたどったものです。
MessageEncryptor の互換性
ここまで MessageEncryptor のコードを読みながら調べたのですが、 結局、キー32文字とキー全体の両方が必要そうです。 ここに復号できない原因があるわけです。
と、ここまで来て、ふと気づきました。
MessageEncryptor のコンストラクタは以下のようになっています。
def initialize(secret, *signature_key_or_options) options = signature_key_or_options.extract_options! sign_secret = signature_key_or_options.first @secret = secret @sign_secret = sign_secret # <--------------- !!! @cipher = options[:cipher] || self.class.default_cipher @digest = options[:digest] || "SHA1" unless aead_mode? @verifier = resolve_verifier @serializer = options[:serializer] || Marshal end
なんと、署名用のキーが指定できるではないですか。
enc = ::ActiveSupport::MessageEncryptor.new('0123456789abcdefghijklmnopqrstuv', '0123456789abcdefghijklmnopqrstuvwxyz', cipher: 'aes-256-cbc') enc.decrypt_and_verify('MGVaT0NTWFkweTdCWFV5SUJVSkh1WkdMaFRscDVUYllGaHlKc2tzZ2hKQT0tLXFXdGJuRmdsV1VmTUVMMUt2VDUyalE9PQ==--8ae97f3fc117a62513fb8ca242f86d893f601c83') => "short string"
復号できました!
まとめ
Rails がそう簡単に互換性を捨てるはずがありませんね。解ってしまえば簡単なことでした。
enc = ActiveSupport::MessageEncryptor.new(KEY[0..31], KEY, cipher: 'aes-256-cbc')
こうですね。
インゲージではエンジニアを募集しています。 こんなことに熱中できる方、うちで働きませんか? 詳細は以下のページへ!
ではまた!