LoginSignup
2
6

More than 5 years have passed since last update.

PHPとNode.jsの暗号/復号相互変換

Last updated at Posted at 2018-03-17

PHPとNodeJSを同時利用するアプリにて、PHP側で暗号化した文字列をNode.jsで復号する方法。

(追記:2018.07.11)
PHP←→node.jsの相互変換ソースを追記しました

PHPで暗号化

PHPでの暗号化は下記の記事内ソースをそのまま利用させていただきました。感謝。
PHPのOpenSSL関数を利用して暗号化する例
(※記事内にはPHPでの復号関数の記載もありますがここでは暗号化関数のみ記載)

encrypt.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するだけで使えます。

ということで早速ソースです。

decrypt.js
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

crypt.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

crypt.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公式ドキュメント

2
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
6