Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9021

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

$
0
0

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

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

nodeCrypto.js
importcryptofrom'crypto';functioncreateCipheriv(algorithm,key,iv){console.log("crypt.key:",key);console.log("crypt.iv:",iv);constcipher=crypto.createCipheriv(algorithm,key,iv);returncipher;}functioncreateDecipheriv(algorithm,key,iv){console.log("decrypt.key:",key);console.log("decrypt.iv:",iv);constdecipher=crypto.createDecipheriv(algorithm,key,iv);returndecipher;}asyncfunctioncryptByNodeApi(cipher,plainText){console.log('平文: '+plainText);letencrypted=cipher.update(plainText,'utf8','hex');encrypted+=cipher.final('hex');console.log('暗号化:',encrypted);returnencrypted;}asyncfunctiondecryptByNodeApi(decipher,encrypted){// 復号letdecrypted=decipher.update(encrypted,'hex','utf8');decrypted+=decipher.final('utf8');console.log('復号化: ',decrypted);returndecrypted;}export{createCipheriv,createDecipheriv,cryptByNodeApi,decryptByNodeApi}

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

import{cryptByNodeApi,decryptByNodeApi,createCipheriv,createDecipheriv}from'./libs/nodeCrypto';exportdefaultfunctionApp(){asyncfunctionhandleClickNodeToBrowser(){constalgorithm='aes-256-cbc';constkey=crypto.randomBytes(32);constiv=Buffer.alloc(16,0);// NodeのCryptoAPIで暗号化constcipher=createCipheriv(algorithm,key,iv);constencrypted=Buffer.from(awaitcryptByNodeApi(cipher,plainText),"hex").buffer;// Nodeのcipherに該当するものを作るconstkeyForbrowser=awaitimportKeyByBrowserApi(key);// ブラウザのCryptoAPIで復号化awaitdecryptByBrowserApi(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

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

browserCrypto.js
asyncfunctioncryptByBrowserApi(plainText,key,iv){console.log('平文: '+plainText);console.log("crypt.key:",key);console.log("crypt.iv:",iv);constencrypted=awaitwindow.crypto.subtle.encrypt({name:"AES-CBC",iv},key,newTextEncoder().encode(plainText));console.log('暗号化:',encrypted);console.log('暗号化:',Buffer.from(encrypted).toString('hex'));returnencrypted;}asyncfunctiondecryptByBrowserApi(encrypted,key,iv){console.log("decrypt.encrypted:",encrypted);console.log("decrypt.key:",key);console.log("decrypt.iv:",iv);constdecrypted=awaitwindow.crypto.subtle.decrypt({name:"AES-CBC",iv,},key,encrypted);constplainText=newTextDecoder().decode(decrypted);console.log('復号化:',plainText);returnplainText;}asyncfunctionimportKeyByBrowserApi(rawKey){constkey=awaitwindow.crypto.subtle.importKey("raw",rawKey,"AES-CBC",true,["encrypt","decrypt"]);returnkey;}asyncfunctiongenerateKeyByBrowserApi(){constkey=window.crypto.subtle.generateKey({name:"AES-CBC",length:256},true,["encrypt","decrypt"]);returnkey;}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

constrawKey=window.crypto.getRandomValues(newUint8Array(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

npm start

純粋なNode.APIでの確認

terminal

node -r esm ./src/cli.js

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


Viewing all articles
Browse latest Browse all 9021

Trending Articles