- 投稿日:2020-02-14T23:38:01+09:00
[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のパッケージがあるかもしれないが調べてないNode.jsのCrypto
基本的には公式ドキュメントのコードがそのまま使用できる。
https://nodejs.org/api/crypto.html#crypto_class_cipher大して見どころはないが、私のソースも載せておく。
ライブラリの方のソース。nodeCrypto.jsimport 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 }実行部分のソースの抜粋。
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でなくてはいけないのかわからなくてハマった
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.jsasync 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#Rawconst rawKey = window.crypto.getRandomValues(new Uint8Array(16));しかし、次の行で
function importSecretKey(rawKey) {
ともなっており、rawKeyは引数しかないと思ってしまった。
Uint8Array(16)ってちゃんとあるのに
ブラウザで暗号化する場合、key指定不要のgenerateKey()を利用するため、Node.jsのkeyを使えるのかもその時はわかっていなかった。加えて生成されるCryptoKeyの中身が見れないのが、問題解決を遅らせた。
CryptoKeyがおかしいのか、decrypt()がおかしいのか見当がつかなかった。
これを間違わなければ1時間もあれば終わるようなもの。。。Conclusion
JavaScriptは型を宣言しないとはいえ、builtinAPIはTypeScriptの型が見みれる。(複数の入力があるためどれがどれに対応するかはわからないが)
それにもかかわらず何とかなるだろうと、詳しく見ずにリトライを繰り返したのがよくなかった。丁寧に見ていけば大丈夫…なはず。Node.jsは怖くない
Have a great day!
Appendices
今回のコードをブラウザで動かせるようにしたソースコード。
自分用なので少し不親切なのに注意。ブラウザで動作確認(Node.APIはbrowserifyが使用される)
terminalnpm start
純粋なNode.APIでの確認
terminalnode -r esm ./src/cli.js
- 投稿日:2020-02-14T21:14:08+09:00
画像をスライダー形式にするswiperをyarnで導入するところまで
yarnとは?
JavaScript(node.js)のパッケージマネージャで、2016年にFaceBookが公開したものです。
他にもパッケージマネージャーとしては「npm」とう言うものものありますが、今回は、yarnを使います。yarnの仕組みは簡単で、$ yarn add 〇〇と言う形で、使いたいパッケージをインストールすると、package.jsonと言うファイルに、インストールしたパッケージに関する情報が記載され、$ yarn installを実行すると、開発環境下にパッケージ(関係するファイル一式)がインストールされ、パッケージを使うことができます。$ yarn installを実行すると、yarn.lockと言うファイルが生成され、固定されます。
実は、gemの管理とよく似ていて、gemの場合は、Gemfileに記載されたgemは、どの環境でも$ bundle installすればgem同士の互換性など考慮して良しなに調整してインストールしてくれて使えるようになりますが、yarnも似たような管理方法で、package.jsonに記載されているパッケージは、$ yarn installすれば、どの環境でも使えるようになります。
例えば、開発現場などで、ローカルにリモートリポジトリを$ git cloneして、手元で開発する場合も、$ yarn installすれば、package.jsonに記載されているnodeのパッケージをローカルにインストールして環境構築できます。
それでは、yarnを使って画像をスライダー形式にできる「swiper」と言うパッケージを導入していきます。
swiperの公式サイトに行くと、以下のようなデモが見れて、ソースコードも見れますので、かなり便利です!
yarnのinstall
まず、homebrewでyarnをPCにインストールします。
$ brew lsでyarnが既に入っているか確認できます。
画面では一番最後のところに「yarn」の記載があるので、この場合はインストールされていますね。
記載がない場合はインストールされていないので、以下コマンでインストールします。$ brew install yarnyarnでパッケージを導入
package.jsonを作成するために以下のコマンドを打ちます。(gemで言う所の$ bundle initに似てますね。)もともとファイルがあればやる必要はないです。
$ yarn init次に、swiperを導入します。
$ yarn add swiperこうすると、package.jsonにswiperの記載がなされるはずです!
インストールします。
$ yarn install必要なファイルがnodeディレクトリ配下に作成されます!
導入したファイルの読み込み設定
マニフェストファイルに導入したファイルのpathを記載して、読み込みの設定を書きます。
例)
assets/javascript/application.js//= require swiper/js/swiper.js //= require swiper.jsディレクトリのpathはnode部分は省略できるので、swiperから書きます。
//= require swiper.js
は、後ほど、viewと連動させるためのファイルの読み込みを書いていますが、ここは各自でやり方は色々あると思います。scssにもスタイルの読み込みを書きます。
assets/stylesheets/application.scss@import 'swiper/css/swiper';この後の部分の記載を失念してしまうのが、ハマりポイントで、私もハマりました。
導入したnode以下のファイルを読み込むようにするための設定を書く必要があります
config/initializers/assets.rbRails.application.config.assets.paths << Rails.root.join('node_modules')これで準備は完了です。
あとはview側の実装や、jsファイルの作成などは、swiperの公式ドキュメントを見れば、コピペで実装できます!
yarnでswiperを導入するところまでの解説でした!
- 投稿日:2020-02-14T18:48:13+09:00
【Electron】ElectronでBootstrap4を使用する際のミスの備忘録
目的
Electronでデスクトップアプリを作成する際に、Bootstrap4を使用して綺麗なデスクトップアプリを作成する。
準備
BootStrapのインストール
npm install bootstrap@4.0.0-betajQueryのインストール
npm install jqueryPopper.jsのインストール
npm install popper.js陥ったミス
ソースコード
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <title>test</title> <meta http-equiv="Content-Security-Policy" content="script-src 'unsafe-inline' 'self';default-src 'self'; style-src 'self' 'unsafe-inline';" /> <link rel="stylesheet" href="./node_modules/bootstrap/dist/css/bootstrap.min.css"> </head> <body> <script src="./node_modules/jquery/dist/jquery.slim.min.js"></script> <script src="./node_modules/popper.js/dist/umd/popper.min.js"></script> <script src="./node_modules/bootstrap/dist/js/bootstrap.min.js"></script> </body> </html>エラーメッセージ
デベロッパーツールに下記のメッセージが表示された。
下記のメッセージを日本語訳にすると、どうやら、jQueryを上部に持ってこないといけないらしい。Uncaught Error: Bootstrap's JavaScript requires jQuery. jQuery must be included >before Bootstrap's JavaScript.
at bootstrap.min.js:6解決方法
解決方法として、
<script src="./node_modules/jquery/dist/jquery.slim.min.js"></script>のコードを
<head>
タグ内に移動させるだけの単純な作業で解決。ソースコード
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <title>test</title> <meta http-equiv="Content-Security-Policy" content="script-src 'unsafe-inline' 'self';default-src 'self'; style-src 'self' 'unsafe-inline';" /> <link rel="stylesheet" href="./node_modules/bootstrap/dist/css/bootstrap.min.css"> <script src="./node_modules/jquery/dist/jquery.slim.min.js"></script> </head> <body> <script src="./node_modules/popper.js/dist/umd/popper.min.js"></script> <script src="./node_modules/bootstrap/dist/js/bootstrap.min.js"></script> </body> </html>終わりに
エラーメッセージの内容をしっかりと確認していなかったのが、悩ませる大きな原因でした。
- 投稿日:2020-02-14T18:48:13+09:00
【Electron】Bootstrap4を使用する際、jQueryの読み込み位置のミスの解決方法-備忘録
目的
Electronでデスクトップアプリを作成する際に、Bootstrap4を使用して綺麗なデスクトップアプリを作成する。
準備
BootStrapのインストール
npm install bootstrap@4.0.0-betajQueryのインストール
npm install jqueryPopper.jsのインストール
npm install popper.js陥ったミス
ソースコード
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <title>test</title> <meta http-equiv="Content-Security-Policy" content="script-src 'unsafe-inline' 'self';default-src 'self'; style-src 'self' 'unsafe-inline';" /> <link rel="stylesheet" href="./node_modules/bootstrap/dist/css/bootstrap.min.css"> </head> <body> <script src="./node_modules/jquery/dist/jquery.slim.min.js"></script> <script src="./node_modules/popper.js/dist/umd/popper.min.js"></script> <script src="./node_modules/bootstrap/dist/js/bootstrap.min.js"></script> </body> </html>エラーメッセージ
デベロッパーツールに下記のメッセージが表示された。
下記のメッセージを日本語訳にすると、どうやら、jQueryを上部に持ってこないといけないらしい。Uncaught Error: Bootstrap's JavaScript requires jQuery. jQuery must be included >before Bootstrap's JavaScript.
at bootstrap.min.js:6解決方法
解決方法として、
<script src="./node_modules/jquery/dist/jquery.slim.min.js"></script>のコードを
<head>
タグ内に移動させるだけの単純な作業で解決。ソースコード
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <title>test</title> <meta http-equiv="Content-Security-Policy" content="script-src 'unsafe-inline' 'self';default-src 'self'; style-src 'self' 'unsafe-inline';" /> <link rel="stylesheet" href="./node_modules/bootstrap/dist/css/bootstrap.min.css"> <script src="./node_modules/jquery/dist/jquery.slim.min.js"></script> </head> <body> <script src="./node_modules/popper.js/dist/umd/popper.min.js"></script> <script src="./node_modules/bootstrap/dist/js/bootstrap.min.js"></script> </body> </html>終わりに
エラーメッセージの内容をしっかりと確認していなかったのが、悩ませる大きな原因でした。
- 投稿日:2020-02-14T12:38:04+09:00
メッセージベースのMicroServiceをNode.js上で簡単につくれるSenecaを試してみた
背景
関わっているプロジェクトで触る機会があったので備忘録的にメモ
Senecaとは
Node.js環境でメッセージベースのMicrorServiceを簡単に構築出来るパッケージ。メッセージはJSON形式です。
Senecaの3つの重要な機能
- Pattern matching: Instead of fragile service discovery, you just let the world know what sort of messages you care about.
- Transport independence: You can send messages between services in many ways, all hidden from your business logic.
- Componentisation: Functionality is expressed as a set of plugins which can be composed together as microservices.
パターンマッチング、独立した転送、コンポーネント化ということで、ソースコードに触れながらこれらの恩恵を感じていきます(笑)
Senecaの基本的な使い方
var seneca = require('seneca')() seneca.add('role:math,cmd:sum', (msg, reply) => { reply(null, {answer: (msg.left + msg.right)}) }) seneca.act({role: 'math', cmd: 'sum', left: 1, right: 2}, function (err, result) { if (err) return console.error(err) console.log(result) })参考:http://senecajs.org/getting-started/
サンプルが凄くシンプルで解りやすかった。
seneca.addがアクションの登録、seneca.actがメッセージの送信。seneca.add
seneca.add('role:math,cmd:sum', (msg, reply) => { reply(null, {answer: (msg.left + msg.right)}) })1つ目のパラメータが処理対処とするメッセージ(JSON形式)のパターン
2つ目のパラメータが実際に処理対象のメッセージが来た時に実行するFunction(アクション)アクションはmsgとreplyという2つのパラメータを持っていてmsgはメッセージのPlain Object、replyはコールバックでerrorとresponsdのシグネチャを持っています。
seneca.act
seneca.act({role: 'math', cmd: 'sum', left: 1, right: 2}, function (err, result) { if (err) return console.error(err) console.log(result) })1つ目のパラメータがメッセージ
2つ目のパラメータがコールバックこの例だとseneca.addの
reply(null, {answer: (msg.left + msg.right)})
で指定された情報がfunctin(errr, rersult)
に入ってくる。その他
seneca.prior
var seneca = require('seneca')() seneca .add('role:math,cmd:sum',function (msg, respond) { var sum = msg.left + msg.right respond(null, { answer: sum }) }) .add('role:math,cmd:sum',function (msg, respond) { // make an error if msg.left or msg.right is infinite value if (!Number.isFinite(msg.left) || !Number.isFinite(msg.right)) { return respond(new Error("Expected left and right to be numbers.")) } this.prior({ role: 'math', cmd: 'sum', left: msg.left, right: msg.right, }, function (err, result) { if (err) return respond(err) result.info = msg.left+'+'+msg.right respond(null, result) }) }) .act('role:math,cmd:sum,left:1.5,right:2.5', console.log // prints { answer: 4, info: '1.5+2.5' } )priorを利用することで、メッセージに対するアクションの前に特定の処理を実行することができる。
1つ目のパラメータは事前処理を追加したいメッセージ
2つ目のパラメータは事前処理の内容また、サンプルコードの中では1つ目のaddで追加したアクションに対して2つ目のaddでアクションのオーバーライドを行なっている。
seneca.use
require('seneca')() .use(plugin, options)useを利用することで、パッケージ化したロジックを利用することが出来る。
1つ目のパラメータは定義した関数名かプラグイン名
2つ目のパラメータは関数やプラグインに渡すオブジェクトindex.jsfunction math(options) { this.add('role:math,cmd:sum', function (msg, respond) { respond(null, { answer: msg.left + msg.right }) }) this.add('role:math,cmd:product', function (msg, respond) { respond(null, { answer: msg.left * msg.right }) }) } require('seneca')() .use(math) .act('role:math,cmd:sum,left:1,right:2', console.log)こちらが、関数名を指定したケース。
useで指定されるパッケージの場合はthisでsenecaのインスタンスにアクセス出来る。math.jsmodule.exports = function math(options) { this.add('role:math,cmd:sum', function sum(msg, respond) { respond(null, { answer: msg.left + msg.right }) }) this.add('role:math,cmd:product', function product(msg, respond) { respond(null, { answer: msg.left * msg.right }) }) }index.js// ①ファイルパスを指定するケース require('seneca')() .use(require('./math.js')) .act('role:math,cmd:sum,left:1,right:2', console.log) // ②パッケージ名を指定するケース require('seneca')() .use('math') // finds ./math.js in local folder .act('role:math,cmd:sum,left:1,right:2', console.log)こちらが、パッケージ名を指定したケース。
seneca.wrap
module.exports = function math(options) { this.add('role:math,cmd:sum', function sum(msg, respond) { respond(null, { answer: msg.left + msg.right }) }) this.add('role:math,cmd:product', function product(msg, respond) { respond(null, { answer: msg.left * msg.right }) }) this.wrap('role:math', function (msg, respond) { msg.left = Number(msg.left).valueOf() msg.right = Number(msg.right).valueOf() this.prior(msg, respond) }) }wrap を利用すると、特定のパターンにマッチしたメッセージのアクションをオーバーライドすることができる。上記ケースの場合はaddされた2つのアクションの事前処理としてmsg.left、msg.rghtを数値に変換している。
1つ目のパラメータは対象とするメッセージのパターン
2つ目のパラメータはオーバーライドする処理内容For MicroService
math.jsmodule.exports = function math(options) { this.add('role:math,cmd:sum', function sum(msg, respond) { respond(null, { answer: msg.left + msg.right }) }) this.add('role:math,cmd:product', function product(msg, respond) { respond(null, { answer: msg.left * msg.right }) }) this.wrap('role:math', function (msg, respond) { msg.left = Number(msg.left).valueOf() msg.right = Number(msg.right).valueOf() this.prior(msg, respond) }) }service.jsrequire('seneca')() .use('math') .listen({ type: 'tcp', pin: 'role:math' })client.jsrequire('seneca')() .client({ type: 'tcp', pin: 'role:math' }) .act('role:math,cmd:sum,left:1,right:2',console.log)listen を利用することで、特定のパターンのメッセージをリッスンすることが出来る。便利!typeにはtcpやamqpなどパッケージをインストールすることで様々なタイプのメッセージを指定出来る。
client を利用することで、特定のパターンのメッセージを指定したタイプにメッセージを発信出来る。
ここからは環境に依存するものが多いので、パラメーターの紹介は割愛。
参考:http://senecajs.org/getting-started/