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

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

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

## PHPで暗号化
PHPでの暗号化は下記の記事内ソースをそのまま利用させていただきました。感謝。
[PHPのOpenSSL関数を利用して暗号化する例](https://blog.ohgaki.net/encrypt-decrypt-using-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関数を利用して暗号化する例](https://blog.ohgaki.net/encrypt-decrypt-using-openssl)
[Node.jsで複数のバッファを結合する](http://info-i.net/buffer-concat)
[Node.jsでバッファを切り出す](http://info-i.net/buf-slice)
[crypto公式ドキュメント](https://nodejs.org/api/buffer.html)