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

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

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

関連カテゴリー記事

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com