PHPとNodeJSを同時利用するアプリにて、PHP側で暗号化した文字列をNode.jsで復号する方法。
(追記:2018.07.11)
PHP←→node.jsの相互変換ソースを追記しました
PHPで暗号化
PHPでの暗号化は下記の記事内ソースをそのまま利用させていただきました。感謝。
PHPのOpenSSL関数を利用して暗号化する例
(※記事内にはPHPでの復号関数の記載もありますがここでは暗号化関数のみ記載)
<?php
/**
* encrypt AES 256
*
* @param data $data
* @param string $password
* @return base64 encrypted data
*/
function encrypt($data, $password) {
// Set a random salt
$salt = openssl_random_pseudo_bytes(16);
$salted = '';
$dx = '';
// Salt the key(32) and iv(16) = 48
while (strlen($salted) < 48) {
$dx = hash('sha256', $dx.$password.$salt, true);
$salted .= $dx;
}
$key = substr($salted, 0, 32);
$iv = substr($salted, 32,16);
$encrypted_data = openssl_encrypt($data, 'AES-256-CBC', $key, true, $iv);
return base64_encode($salt . $encrypted_data);
}
Node.jsで復号
ここからが本題。上記のPHPプログラムで生成した暗号テキストをNode.jsで復号します。
Node.jsでの復号はcryptoモジュールを利用します。
このモジュールはNode.jsの標準パッケージに含まれるので、requireするだけで使えます。
ということで早速ソースです。
const crypto = require('crypto');
// 復号
const decrypt = (text,password) => {
const data = Buffer.from(text, 'base64');
const salt = data.slice(0,16);
const ct = data.slice(16).toString('binary');
const rounds = 3;
const data00 = Buffer.concat([Buffer.from(password),salt]);
let hash = [];
hash[0] = crypto.createHash("sha256").update(data00).digest("binary");
let result = hash[0];
for (let i = 1; i < rounds; i++) {
let tmp = Buffer.concat([Buffer.from(hash[i - 1],"binary"),data00])
hash[i] = crypto.createHash("sha256").update(tmp).digest("binary");
result += hash[i];
}
const base = Buffer.from(result, "binary");
const key = base.slice(0,32);
const iv = base.slice(32,48);
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
let content = decipher.update(ct, "binary", "utf8");
content += decipher.final("utf8");
return content;
}
解説
基本的な流れは引用元の復号PHPソースに沿っています。というかほぼそのままですので、ロジック的な解説は参考ページのPHPソース側の解説をご確認下さい。
ただしそのままと言ってもNode.jsでのバイナリデータはBufferを使って処理する関係で、完全にそのままの移行では動きません。
ポイントは下記の2つ。
- バイナリ文字列を結合するときはBufferオブジェクトに変換してconcatで結合
- バイト数指定の切り出しはBuffer.sliceで行う
文字列の切り出しは、substrやtoStringでの切り出しが思いつきますが、文字コードの関係で指定しバイト数ではうまく切り出せません。
そのため、一度Bufferオブジェクトに変換した後sliceメソッドで切り出しています。
相互変換
PHP→node.jsの変換だけでなく相互変換も記載しておきます。
PHP
<?php
/**
* encrypt AES 256
*
* @param string $text
* @param string $password
* @return base64 encrypted data
*/
function encrypt($text, $password) {
// Set a random salt
$salt = openssl_random_pseudo_bytes(16);
$salted = '';
$dx = '';
// Salt the key(32) and iv(16) = 48
while (strlen($salted) < 48) {
$dx = hash('sha256', $dx.$password.$salt, true);
$salted .= $dx;
}
$key = substr($salted, 0, 32);
$iv = substr($salted, 32,16);
$encrypted_data = openssl_encrypt($text, 'AES-256-CBC', $key, true, $iv);
return base64_encode($salt . $encrypted_data);
}
/**
* decrypt AES 256
*
* @param string $encText
* @param string $password
* @return string
*/
function decrypt($encText, $password) {
$decodeText = base64_decode($encText);
$salt = substr($decodeText, 0, 16);
$ct = substr($decodeText, 16);
$rounds = 3; // depends on key length
$data00 = $password.$salt;
$hash = array();
$hash[0] = hash('sha256', $data00, true);
$result = $hash[0];
for ($i = 1; $i < $rounds; $i++) {
$hash[$i] = hash('sha256', $hash[$i - 1].$data00, true);
$result .= $hash[$i];
}
$key = substr($result, 0, 32);
$iv = substr($result, 32,16);
return openssl_decrypt($ct, 'AES-256-CBC', $key, true, $iv);
}
node.js
const crypto = require('crypto');
/**
* encrypt AES 256
*
* @param string text
* @param string password
* @return base64 encrypted data
*/
const encrypt = (text,password) => {
// salt文字列を生成
const genRandomString = (length) => {
return crypto.randomBytes(Math.ceil(length/2)).toString('hex').slice(0,length);
}
const salt = genRandomString(16);
let salted = '';
let dx = '';
while (salted.length < 48) {
let tmp = Buffer.concat([dx,Buffer.from(password),salt]);
dx = crypto.createHash("sha256").update(tmp).digest("binary");
salted += dx;
}
const base = Buffer.from(salted, "binary");
const key = base.slice(0,32);
const iv = base.slice(32,48);
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
let content = cipher.update(text, "utf8", "binary");
content += cipher.final("binary");
return content;
}
/**
* decrypt AES 256
*
* @param string encText
* @param string password
* @return string
*/
const decrypt = (encText,password) => {
const decodeText = Buffer.from(encText, 'base64');
const salt = decodeText.slice(0,16);
const ct = decodeText.slice(16).toString('binary');
const rounds = 3;
const data00 = Buffer.concat([Buffer.from(password),salt]);
let hash = [];
hash[0] = crypto.createHash("sha256").update(data00).digest("binary");
let result = hash[0];
for (let i = 1; i < rounds; i++) {
let tmp = Buffer.concat([Buffer.from(hash[i - 1],"binary"),data00])
hash[i] = crypto.createHash("sha256").update(tmp).digest("binary");
result += hash[i];
}
const base = Buffer.from(result, "binary");
const key = base.slice(0,32);
const iv = base.slice(32,48);
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
let content = decipher.update(ct, "binary", "utf8");
content += decipher.final("utf8");
return content;
}
余談
実はこれ、下記ページに全く同じことを目的としたソースがあったので、それをお借りして実装するつもりで組み込んでみたところ動かず。。
https://gist.github.com/schakko/2628689
結局自分で書いてみたところBufferの扱いになかなか苦労したので、同じような境遇の方が同じ目に遭わないよう共有します。
誰かの手助けになりますように。
参考
PHPのOpenSSL関数を利用して暗号化する例
Node.jsで複数のバッファを結合する
Node.jsでバッファを切り出す
crypto公式ドキュメント