PHPのmcryptで暗号化されたデータを、Node.jsで復号化しようとしてハマったので、その原因と解決方法メモです。
状況
PHPでAES256-CBCで暗号化した物ですよと言われて受け取ったデータを、Node.jsで復号化しようとするとエラーになりました。
データは暗号化されていて、これ以上どうしようもできないので、暗号化部分のソースを見せてもらうと下記のような感じでした。
<?php function encode($data){ $key = "1234567890123456"; $iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC); $iv = mcrypt_create_iv($iv_size, MCRYPT_RAND); $encoded_data = mcrypt_encrypt( MCRYPT_RIJNDAEL_256, $key, $data, MCRYPT_MODE_CBC, $iv); return base64_encode($iv . $encoded_data); } ?>
PHPの暗号化ライブラリ、mcryptで暗号化されていて、mcryptのマニュアルのサンプルコードとほぼ同じで、問題なさそうでした。
しかし、実際にPHPの環境を構築して、暗号化データを作成し試してみると、同じくエラーが出てダメでした。
原因
途中PHPを触っていて気になったのは、AES256-CBCなのに、鍵の長さが128bit、mcrypt_create_iv()
から取得したIVの長さが32byteと、こちらで想定している長さ(鍵256bit、IVサイズ16byte)と違っていたところです。
調べてみると、mcryptで暗号化したけど、mcrypt以外で復号化できないという記事が色々でてきます。
- https://stackoverflow.com/questions/49997338/mcrypt-rijndael-256-to-openssl-aes-256-ecb-conversion/50000095
- https://stackoverflow.com/questions/56926812/decrypt-aes-cbc-256-mcrypt-rijndael-encrypted-in-php-decrypt-on-golang
- https://stackoverflow.com/questions/6038620/aes-encrypt-in-node-js-decrypt-in-php-fail
そして、衝撃の結論は、
「PHPのmcryptでAES256-CBC(MCRYPT_RIJNDAEL_256・MCRYPT_MODE_CBC)と呼んでいる暗号化は、一般的なAES256-CBC暗号化とは違い、AES256-CBCでは復号化できない」
でした……。
じゃあ、どないすんねん。となるのですが、幸いNode.jsには、PHPのmcryptのライブラリをそのまま使うという、力技なライブラリがあって、それを使うことにしました。
解決方法
「cryptian」は、アルゴリズムとモード、パディングしか移植されていないので、他はPHPで確認しながら進め、結局、前述のコードで暗号化されたデータは、下記のNode.jsで復号化できました。
import * as stream from 'stream' const cryptian = require('cryptian'); const mstream = require('memory-streams'); function decodeMcrypt(data: string) { return new Promise((resolve, reject) => { const buff = Buffer.from(data, 'base64'); // ivサイズは32byte const iv = buff.slice(0, 32); const encdata = buff.slice(32); const key_str = '1234567890123456'; const key = Buffer.from(key_str); const algorithm = cryptian.algorithm.Rijndael256(); algorithm.setKey(key); const decipher = new cryptian.mode.cbc.Decipher(algorithm, iv); const rstrm = new stream.Readable(); const wstrm = new mstream.WritableStream(); // paddingはPkcs7 const dstrm = cryptian.createDecryptStream( decipher, cryptian.padding.Pkcs7 ); wstrm.on('finish', () => { resolve(wstrm.toBuffer().toString('utf-8')); }); rstrm.pipe(dstrm).pipe(wstrm); rstrm.push(encdata); rstrm.push(null); }); } (async () => { const encoded_data = "xxxxxxxxxx"; const decoded_data = await decodeMcrypt(encoded_data); console.log(decoded_data); })();
暗号化手法を決めるパラメータは「アルゴリズム」「モード」「パディング」の3つなのですが、暗号化コードから、「アルゴリズム」は「MCRYPT_RIJNDAEL_256」、「モード」は「MCRYPT_MODE_CBC」と分かるのですが、「パディング」は不明で、総当りで「PKCS7」と判明しました。
ライブラリはストリーム処理しか対応していないので、お膳立てがちょっと手間ですね。
応用
「cryptian」は、PHPのmcryptをそのまま使っているので、mcryptの他の方法で暗号化されたデータも、復号化できるかと思います。
感想
ついつい面倒で、暗号化した時と同じライブラリで復号化を検証してOKとしがちですが、結構怖いですね。
心配になったので、PHPのopensslでAES256-CBC暗号化し、Node.jsのAES256-CBCで復号化してみましたが、そちらは無事復号化できたので、Node.jsのAES256-CBC復号化は大丈夫だと一安心しました。
mcryptについて
mcryptは既に廃止されたライブラリなので、そのままほっとくとmcrypt自体が無くなって、誰も復号化できなくなりかねません…。
なので、早目にmcryptから移行した方がいいですね。
互換性について
Wikipediaの「AES」のページを見ていたら、AESと互換性が無い理由が書いてありました。
mcryptが使っている「RIJNDAEL」という暗号化は「AES」の元となった暗号化で、ブロックサイズ・鍵サイズ・パディング形式が複数あり、どれにするかは実装者が選んで決めます。そして、「AES」は規格としてどれにするかが決まっています。
そして残念なことに、mcryptの「RIJNDAEL」で選ばれたものと、「AES」で規格として選ばれたものが同じでなかったため、互換性がありませんでした。
仕組みは同じでも、設定値が違うので、これじゃぁどう頑張っても「AES」で復号化できませんね。