20200214のNode.jsに関する記事は6件です。

[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

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

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
}

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

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

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

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

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

npm start

純粋なNode.APIでの確認

terminal

node -r esm ./src/cli.js

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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

画像をスライダー形式にする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の公式サイトに行くと、以下のようなデモが見れて、ソースコードも見れますので、かなり便利です!
Image from Gyazo

Image from Gyazo

yarnのinstall

まず、homebrewでyarnをPCにインストールします。

$ brew lsでyarnが既に入っているか確認できます。
Image from Gyazo

画面では一番最後のところに「yarn」の記載があるので、この場合はインストールされていますね。
記載がない場合はインストールされていないので、以下コマンでインストールします。

$ brew install yarn

yarnでパッケージを導入

package.jsonを作成するために以下のコマンドを打ちます。(gemで言う所の$ bundle initに似てますね。)もともとファイルがあればやる必要はないです。

$ yarn init

次に、swiperを導入します。

$ yarn add swiper

こうすると、package.jsonにswiperの記載がなされるはずです!

インストールします。

$ yarn install

必要なファイルがnodeディレクトリ配下に作成されます!

Image from Gyazo

導入したファイルの読み込み設定

マニフェストファイルに導入したファイルの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.rb
Rails.application.config.assets.paths << Rails.root.join('node_modules')

これで準備は完了です。

あとはview側の実装や、jsファイルの作成などは、swiperの公式ドキュメントを見れば、コピペで実装できます!

https://swiperjs.com/demos/

yarnでswiperを導入するところまでの解説でした!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Electron】ElectronでBootstrap4を使用する際のミスの備忘録

目的

Electronでデスクトップアプリを作成する際に、Bootstrap4を使用して綺麗なデスクトップアプリを作成する。

準備

BootStrapのインストール

npm install bootstrap@4.0.0-beta

jQueryのインストール

npm install jquery

Popper.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>

終わりに

 エラーメッセージの内容をしっかりと確認していなかったのが、悩ませる大きな原因でした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Electron】Bootstrap4を使用する際、jQueryの読み込み位置のミスの解決方法-備忘録

目的

Electronでデスクトップアプリを作成する際に、Bootstrap4を使用して綺麗なデスクトップアプリを作成する。

準備

BootStrapのインストール

npm install bootstrap@4.0.0-beta

jQueryのインストール

npm install jquery

Popper.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>

終わりに

 エラーメッセージの内容をしっかりと確認していなかったのが、悩ませる大きな原因でした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

メッセージベースの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.js
function 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で指定されるパッケージの場合はthissenecaのインスタンスにアクセス出来る。

math.js
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 })
  })

}
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.js
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)
  })

}
service.js
require('seneca')()
  .use('math')
  .listen({ type: 'tcp', pin: 'role:math' })
client.js
require('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/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

webpackとは何なのか?

概要

執筆中

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む