新しいことにはウェルカム

技術 | 電子工作 | ガジェット | ゲーム のメモ書き

PHPのmcryptで暗号化されたデータをNode.jsで復号化する方法

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以外で復号化できないという記事が色々でてきます。

そして、衝撃の結論は、

「PHPのmcryptでAES256-CBC(MCRYPT_RIJNDAEL_256・MCRYPT_MODE_CBC)と呼んでいる暗号化は、一般的なAES256-CBC暗号化とは違い、AES256-CBCでは復号化できない」

でした……。

じゃあ、どないすんねん。となるのですが、幸いNode.jsには、PHPのmcryptのライブラリをそのまま使うという、力技なライブラリがあって、それを使うことにしました。

github.com

解決方法

「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」で復号化できませんね。