読者です 読者をやめる 読者になる 読者になる

レガシーコード生産ガイド

私に教えられることなら

JavaScriptで暗号化通信

JavaScript Node.js 備忘録

特に漏れてもダメージは無いデータを扱うWebサービスを作っていて、こちらで発行したパスワードをトークンでの暗号化でやりとりする方式で認証していた。しかし平文が流れるのは流石に…と気になったのと、興味があったので、GET/POSTを公開鍵暗号方式で暗号化してみることにした。

node.jsとAngular.jsでのSPAなので、JavaScriptのみを使う。

ついでに今年の目標など - レガシーコード生産ガイドで述べたように、図を描く習慣と能力を育てようと思うので、やりとりを紙にスケッチして開発、その後Inkscapeをインストールして画像にしてみた。

f:id:phaendal:20150105172749p:plain

暗号化・復号にはCrypticoというライブラリを使った。

wwwtyro/cryptico · GitHub

ブラウザ側ではcryptico.min.jsを読み込み、グローバル変数にcrypticoが追加されるのでそれを使う。 node.jsではcryptico-nodeというモジュールを使う。

phpmycoder/cryptico-node · GitHub

npm install --save-dev crypticoで入った。

上記の図の通り、クライアント・サーバーそれぞれに共通するのは

  • 自分の公開鍵を送信する
  • 自分の、又は受け取った鍵による暗号化
  • 自分の秘密鍵による復号

という動作なので、次のような関数を作った。

    var Crypter = function (seed) {

        var passphrase = パスフレーズ作成関数(seed),
            bits = 1024, // 2048だとブラウザがちょっとキツい雰囲気
            rsakey = cryptico.generateRSAKey(passphrase, bits),
            pubkey = cryptico.publicKeyString(rsakey)
        ;

        function encrypt (source, key) {
            var pkey = key || pubkey,
                encrypted = cryptico.encrypt(source, pkey).cipher ;

            return encrypted;
        }

        function decrypt (source) {
            var decrypted = cryptico.decrypt(source, rsakey).plaintext;
            return decrypted;
        }

        return {
            encrypt: encrypt,
            decrypt: decrypt,
            getPubkey: function () { return pubkey; }
        };

    };

次の様に使う。

var crypter = Crypter("fuuaa"),
    source_str  = "hyoooaa",

    encrypted = crypter.encrypt(source_str),    // 暗号化
    // encrypted = crypter.encrypt(post_data_str, given_pubkey), // 公開鍵をもらう場合 

    decrypted = crypter.decrypt(encrypted), // 復号

    pubkey    = crypter.getPubkey() // 公開鍵取り出し
;

if (source_str === decrypted) { console.log("ok"); }

クライアントサイドでは、Angular.jsの$httpをラップしたサービスを使っているので、それに暗号化版のメソッドを追加しておいた。

サーバーサイド(express)では、サーバーの公開鍵を取得するurlと、次のようなミドルウェアを追加した。

// ↑のCrypterをモジュールにした
var Crypter = require('./Crypter.js')("hyoo");

app.post('/*', function (req, res, next) {
    // 暗号化されていない
    if (!req.body || !req.body.encrypted) { next(); return; }

    var resjson = res.json,
        pubkey  = req.body.pubkey,
        body
    ;

    // 送られてきたデータがあれば復号
    if (req.body.data) {
        body = JSON.parse(Crypter.decrypt(req.body.data));
        req.body = body;
    }

    // 返信を暗号化でラッピング
    function newResJSON (json) {
        var source  = JSON.stringify(json),
            encrypted = Crypter.encrypt(source, pubkey)
        ;

        // thisに気をつける
        resjson.call(res, encrypted);
    }
    res.json = newResJSON;

    next();
});

暗号化を行う場合、クライアント側では

{ encrypted: true, pubkey: 公開鍵 (, data: 暗号化されたJSONデータ) }

という形のJSONを生成しPOSTする。

その場合、サーバー側は送られてきたデータがあれば復号し、res.json()を送られてきた公開鍵で暗号化するものでラッピングする。

感想

  • 図を描いたらスムーズに開発が進んだ。
  • ルートの証明はできないので、最終的にはSSLを使う必要がある。らしい。
  • 久々に自作ではないオブジェクトのメソッド置き換えをしたのでthisでハマった。いつもクロージャ使ってるからなあ。
広告を非表示にする