JSで公開鍵を使って暗号化したデータをPHPで秘密鍵を使って復号する

[公開鍵暗号](https://ja.wikipedia.org/wiki/%E5%85%AC%E9%96%8B%E9%8D%B5%E6%9A%97%E5%8F%B7)を用いて、JSでデータを暗号化したものをPHPで復号してみます。
記載内容に問題があっても責任は取れないので、参考にする場合は自己責任でお願いします。
また、参考にした公式のドキュメントを載せるので、情報が変わっていないかそちらを確認していただければと思います。

ちなみに、パスワードは暗号化せずにハッシュ化しましょう。

# 公開鍵と暗号鍵を作成する
まずは暗号化に使う鍵を作成しておきます。

```bash
$ openssl genrsa -out rsa_2048_priv.pem 2048
$ openssl rsa -pubout -in rsa_2048_priv.pem -out rsa_2048_pub.pem
```

1024bitで作成している記事もありますが、2048bit以上のRSA鍵を使用するようにしましょう。

# JSでの暗号化について
調べてみると、JSで暗号化を行う場合に様々なライブラリが存在しました。
英語ですが、下記のgistにJSで暗号化させる時のライブラリの一覧が載っています。
[JavaScript Crypto Libraries](https://gist.github.com/jo/8619441)

ただ、node.jsだとデフォルトで[crypto](https://nodejs.org/api/crypto.html#crypto_crypto)というモジュールが使えますし、多くのブラウザでは[SubtleCrypto オブジェクト](https://developer.mozilla.org/ja/docs/Web/API/Crypto/subtle)が実装されています。

今回はLaravelのプロジェクトで使うことを想定して、node.jsのcryptoモジュールを使用します。

## cryptoモジュールで暗号化する
公開鍵で暗号化するには[crypto.publicEncrypt](https://nodejs.org/api/crypto.html#crypto_crypto_publicencrypt_key_buffer)というメソッドを使用します。

```js
const crypto = require('crypto');

// 先ほど作成したpublickey
const publicKey = `-----BEGIN PUBLIC KEY-----
hogehogehogehgoe
-----END PUBLIC KEY-----
`;

const plain = 'hoge fuga';
const encrypted = crypto.publicEncrypt(publicKey, Buffer.from(plain));
console.log(encrypted);
console.log(encrypted.toString('base64'));
```

ドキュメントを見るとpublicEncryptの第二引数が `buffer` となっていますが、`new Buffer()` は廃止予定となっており、使用するとwarningが出たと思います。
いくつか記事を調べると、`new Buffer(plain)` を使用している記事が多いですが、[Buffer.from](https://nodejs.org/api/buffer.html#buffer_class_method_buffer_from_array)を使用して、バイナリデータに変換します。

参考
[今年のうちに対応したい、Node.jsのBufferに潜む危険性](https://techblog.yahoo.co.jp/advent-calendar-2016/node_new_buffer/)

### 暗号化した結果を確認してみる
[base64](https://ja.wikipedia.org/wiki/Base64)エンコードした結果をサーバ側へ送り、DBなどに保存をする想定です。

> Base64は、データを64種類の印字可能な英数字のみを用いて、それ以外の文字を扱うことの出来ない通信環境にてマルチバイト文字やバイナリデータを扱うためのエンコード方式である

```bash
$ node publicEncrypt.js
# console.log(encrypted);
<Buffer b7 8c 41 4b 5e 3b 84 59 4a 31 f7 e5 53 a4 41 5d 55 c3 05 7d 27 18 9d 6d d6 eb 15 45 7d 53 01 99 79 eb ea d7 95 d6 2e 31 c1 b1 2c 76 ed cc 48 4a dd ea ... >
# console.log(encrypted.toString('base64'));
t4xBS147hFlKMfflU6RBXVXDBX0nGJ1t1usVRX1TAZl56+rXldYuMcGxLHbtzEhK3equw7qE8cCNulqJfst40B5OSThpkUoEiA9Q1Bo6b5RIhyZi8IQsTSZCkNE2LQaEILl5JZXfioYXmjTfMqDhn/jI6QLagWkRyd2jYIZVQ6ChhjzYG2eVWo5otli/N4Z9j93FKKQ1n8fiIvY62lZsOSv9I0F/ZZzCcYPFvI1DeOuYIw6StHC20lPo49d6quNjAAZwiLI9p43kue1PnD4M2HwwVFWYr4DRveDkR3gIUhOGk8UDI8BeCrwjVsz9jT5MjpAVp/6BT8/W1Q0NgJyctA==
```

# PHPでの復号について
[openssl-private-decrypt](https://www.php.net/manual/ja/function.openssl-private-decrypt.php)を使うことで復号可能です。

```php
// 先ほど作成した秘密鍵
$privatekey = <<<EOD
-----BEGIN RSA PRIVATE KEY-----
hogehogehgoe
-----END RSA PRIVATE KEY-----
EOD;

// 先ほど暗号化した文字列
$encrypted = 't4xBS147hFlKMfflU6RBXVXDBX0nGJ1t1usVRX1TAZl56+rXldYuMcGxLHbtzEhK3equw7qE8cCNulqJfst40B5OSThpkUoEiA9Q1Bo6b5RIhyZi8IQsTSZCkNE2LQaEILl5JZXfioYXmjTfMqDhn/jI6QLagWkRyd2jYIZVQ6ChhjzYG2eVWo5otli/N4Z9j93FKKQ1n8fiIvY62lZsOSv9I0F/ZZzCcYPFvI1DeOuYIw6StHC20lPo49d6quNjAAZwiLI9p43kue1PnD4M2HwwVFWYr4DRveDkR3gIUhOGk8UDI8BeCrwjVsz9jT5MjpAVp/6BT8/W1Q0NgJyctA==';
// base64エンコードしているのでデコードしてから復号する
$decoded= base64_decode($encrypted);
openssl_private_decrypt($decoded, $decrypted, $privatekey, OPENSSL_PKCS1_OAEP_PADDING);

var_dump(decrypted);
```

ポイントとしては、`openssl_private_decrypt` の第四引数に`OPENSSL_PKCS1_OAEP_PADDING` を指定しているところかなと思います。

[crypto.publicEncrypt](https://nodejs.org/api/crypto.html#crypto_crypto_publicencrypt_key_buffer)のドキュメントに、`RSA_PKCS1_OAEP_PADDING` を使用していると記載がありますので、php側でも同じ[パディング](https://ja.wikipedia.org/wiki/RSA%E6%9A%97%E5%8F%B7#%E3%83%91%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0)を指定しています。
> Otherwise, this function uses RSA_PKCS1_OAEP_PADDING.

下記のように第四引数に指定しない場合には、$decryptedがnullとなってしまいます。

```php
openssl_private_decrypt($decoded, $decrypted, $privatekey);
```

## 復号した結果を確認してみる
これでできた

```zsh
$ php privateDecrypt.php
string(9) "hoge fuga"
```

# 連携部分について
公開鍵も秘密鍵もサーバ側で持っておいて、公開鍵のみを`csrf-token`と同じようにhtmlのmetaに埋め込むか、公開鍵を取得するためのAPIなどを用意してJSで取得をできるようにすれば良いと思います。

# 注意点
## 公開鍵暗号では容量の大きいデータをまとめて暗号化できない
当たり前かもしれませんが、公開鍵暗号では大きなサイズのデータを暗号化できません。
そのため、そこそこ長い文章などを暗号化しようとすると、下記のようなエラーが表示されてしまいます。

```
Error: error:0409A06E:rsa routines:RSA_padding_add_PKCS1_OAEP_mgf1:data too large for key size
```
容量の大きいデータを暗号化してサーバ側に送りたい場合には、公開鍵と共通鍵を使った[ハイブリッド暗号](http://e-words.jp/w/%E3%83%8F%E3%82%A4%E3%83%96%E3%83%AA%E3%83%83%E3%83%89%E6%9A%97%E5%8F%B7.html)で対応する必要があると思います。

参考
[java – RSAで暗号化できるデータ量の制限は？](https://codeday.me/jp/qa/20190224/297474.html)

## うまく復号できない場合に確認すること
最初復号できなかったのですが、teratailで同じような質問があり、参考にさせてもらいました。
使うライブラリやメソッドなどによって、設定に差異がないかを確認する必要ありそうです。

```
・暗号アルゴリズム（AES等） 
・鍵長（256ビット等） 
・モード（CBC等） 
・パディング（pcks7等） 
```
参考
[JSとPHPでの公開鍵・秘密鍵による暗号化と復号化](https://teratail.com/questions/71680)

# その他参考情報
[RSA暗号運用でやってはいけない n のこと ](https://www.slideshare.net/sonickun/rsa-n-ssmjp)