[Node.js][JavaScript]CryptoAPIの違いでハマったのでまとめ

#Overview

Node.jsはJavaScriptで書けるから、Webの中では"Write once, run anywhere"的な美味しいこともある。
しかし、各環境にbuiltinされているAPIを使ったときはそうはいかない時がある。
今回は暗号化のCryptoで不覚にも1日ハマったのでその記録を残しておく。

#Target reader

- Node.jsで暗号化したデータをブラウザで復号化したいと思っている方。

#Prerequisite

- AESの概要は理解していること。
- 今回はAES256-CBCを使用する。
    - 記憶が正しければAES192はブラウザのAPIでサポートされていない旨のエラーが出たため。


#Body

##どうして片方のAPIで統一しないの？

これはいい質問だ。実際のところ、Node.jsのcryptoをブラウザで実行したことがある。
どうして採用されなかったのか？**なぜなら100KBほどバンドルサイズが増えたから。**
詳しく知りたい場合は、この方の記事を読んでみるといいかもしれない。
https://engineering.mixmax.com/blog/requiring-node-builtins-with-webpack

一言でいうと、以下のブラウザ用cryptoがバンドルされてしまったため。
https://github.com/crypto-browserify/crypto-browserify

ブラウザのAPIを使えば100KBのバンドルを回避できるのだから、別々のAPIを使用するのは当然といってもいい。
もしかしたら差分を吸収するI/Fのパッケージがあるかもしれないが調べてない:joy:

##Node.jsのCrypto

基本的には公式ドキュメントのコードがそのまま使用できる。
https://nodejs.org/api/crypto.html#crypto_class_cipher

大して見どころはないが、私のソースも載せておく。
ライブラリの方のソース。

```javascript:nodeCrypto.js
import crypto from 'crypto';

function createCipheriv(algorithm, key, iv) {
	console.log("crypt.key:", key);
	console.log("crypt.iv:", iv);

	const cipher = crypto.createCipheriv(algorithm, key, iv);
	return cipher;
}

function createDecipheriv(algorithm, key, iv) {
	console.log("decrypt.key:", key);
	console.log("decrypt.iv:", iv);

	const decipher = crypto.createDecipheriv(algorithm, key, iv);
	return decipher;
}

async function cryptByNodeApi(cipher, plainText) {
	console.log('平文: ' + plainText);

	let encrypted = cipher.update(plainText, 'utf8', 'hex');
	encrypted += cipher.final('hex');

	console.log('暗号化:', encrypted);

	return encrypted;
}

async function decryptByNodeApi(decipher, encrypted) {
	// 復号
	let decrypted = decipher.update(encrypted, 'hex', 'utf8');
	decrypted += decipher.final('utf8');

	console.log('復号化: ', decrypted);

	return decrypted;
}

export {
	createCipheriv, createDecipheriv,
	cryptByNodeApi, decryptByNodeApi
}
```

実行部分のソースの抜粋。

```javascript
import { cryptByNodeApi, decryptByNodeApi, createCipheriv, createDecipheriv } from './libs/nodeCrypto';

export default function App() {

  async function handleClickNodeToBrowser() {
    const algorithm = 'aes-256-cbc';
    const key = crypto.randomBytes(32);
    const iv = Buffer.alloc(16, 0);

    // NodeのCryptoAPIで暗号化
    const cipher = createCipheriv(algorithm, key, iv);
    const encrypted = Buffer.from(await cryptByNodeApi(cipher, plainText), "hex").buffer;

    // Nodeのcipherに該当するものを作る
    const keyForbrowser = await importKeyByBrowserApi(key);
    // ブラウザのCryptoAPIで復号化
    await decryptByBrowserApi(encrypted, keyForbrowser, iv);
  }
};
```

注意点として以下のことがあげられる。

- 公式ドキュメントとは異なりAESの256bit(32Byte)なのでキーは32Byteになる。
- IVは16Byte固定。
    - ソースでは0固定にしているが本来は値を与えること。
- cryptByNodeApi()ではhexにしているため、ブラウザAPIへの入力に合わせるためArrayBufferを取り出している。

ブラウザAPIの方はArrayBufferを与えないとエラーになるが、実際何がArrayBufferでなくてはいけないのかわからなくてハマった:persevere:
SubtleCrypto.decrypt()のドキュメントを見るとBufferSourceとなっており、リンク先に行かないと気が付かない罠。
>data is a BufferSource containing the data to be decrypted (also known as ciphertext).

##BrowserのCrypto

基本的には公式ドキュメント先のコードがそのまま使用できる。
https://github.com/mdn/dom-examples/blob/master/web-crypto/encrypt-decrypt/aes-cbc.js

大して見どころはないが、私のソースも載せておく。
ライブラリの方のソース。

```javascript:browserCrypto.js

async function cryptByBrowserApi(plainText, key, iv) {
	console.log('平文: ' + plainText);
	console.log("crypt.key:", key);
	console.log("crypt.iv:", iv);

	const encrypted = await window.crypto.subtle.encrypt(
		{
			name: "AES-CBC",
			iv
		},
		key,
		new TextEncoder().encode(plainText)
	);
	console.log('暗号化:', encrypted);
	console.log('暗号化:', Buffer.from(encrypted).toString('hex'));
	return encrypted;
}

async function decryptByBrowserApi(encrypted, key, iv) {
	console.log("decrypt.encrypted:", encrypted);
	console.log("decrypt.key:", key);
	console.log("decrypt.iv:", iv);
	const decrypted = await window.crypto.subtle.decrypt(
		{
			name: "AES-CBC",
			iv,
		},
		key,
		encrypted
	);

	const plainText = new TextDecoder().decode(decrypted);
	console.log('復号化:', plainText);

	return plainText;
}

async function importKeyByBrowserApi(rawKey) {
	const key = await window.crypto.subtle.importKey(
		"raw",
		rawKey,
		"AES-CBC",
		true,
		["encrypt", "decrypt"]
	);
	return key;
}

async function generateKeyByBrowserApi() {
	const key = window.crypto.subtle.generateKey(
		{
			name: "AES-CBC",
			length: 256
		},
		true,
		["encrypt", "decrypt"]
	);
	return key;
}

export {
	cryptByBrowserApi, decryptByBrowserApi, generateKeyByBrowserApi, importKeyByBrowserApi
}

```

注目ポイントは、importKey()とdecrypt()の二つを使用しないといけないところ。
importKey()であっているのだろうか？rawKeyは正しく指定しているのか？ArrayBufferじゃないといけないのエラーって何？
複数の誤りでエラーポイントが特定できず完成までに1日も消耗してしまった。

rawの中身については公式ドキュメントのソースの1行目に具体的にある。
https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey#Raw

>
```javascript
const rawKey = window.crypto.getRandomValues(new Uint8Array(16));
```

しかし、次の行で```function importSecretKey(rawKey) {```ともなっており、rawKeyは引数しかないと思ってしまった。
Uint8Array(16)ってちゃんとあるのに:weary:
ブラウザで暗号化する場合、key指定不要のgenerateKey()を利用するため、Node.jsのkeyを使えるのかもその時はわかっていなかった。

加えて生成されるCryptoKeyの中身が見れないのが、問題解決を遅らせた。
CryptoKeyがおかしいのか、decrypt()がおかしいのか見当がつかなかった。
これを間違わなければ1時間もあれば終わるようなもの。。。

#Conclusion

JavaScriptは型を宣言しないとはいえ、builtinAPIはTypeScriptの型が見みれる。（複数の入力があるためどれがどれに対応するかはわからないが）
それにもかかわらず何とかなるだろうと、詳しく見ずにリトライを繰り返したのがよくなかった。

丁寧に見ていけば大丈夫…なはず。Node.jsは怖くない:relaxed:


Have a great day!

#Appendices

今回のコードをブラウザで動かせるようにしたソースコード。
自分用なので少し不親切なのに注意。

ブラウザで動作確認(Node.APIはbrowserifyが使用される)

```terminal:terminal

npm start
```

純粋なNode.APIでの確認

```terminal:terminal

node -r esm ./src/cli.js
```

https://github.com/qrusadorz/example-decrypt-in-browser