ちょっと話題の記事

Node.jsの暗号化について調べてみた。

2014.07.28

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

node.jsは暗号化する際に特別なパッケージ等は必要なく、 require('crypto')と指定するだけで基本的な暗号化は実装できます。

AES-256で暗号化しようととした場合、以下の方法で実装が可能です。

'use strict'; 
var crypto = require('crypto');
var cipher = crypto.createCipher('aes-256-cbc', 'cryptKey'); 
var crypted = cipher.update(str, 'utf-8', 'hex'); 
crypted += cipher.final('hex');

しかし、本来AES-256-CBCで暗号化しようとした場合、
本来256bit長の鍵と初期ベクトルが必要なのですが、上記の実装では、それらの影も形もありません。

というわけで、鍵と初期ベクトルはどこに行ったのかを調べてみました。

Node.jsのソースコードを追う(JavaScript編)

まずは、 GitHubのNode.jsのソースで挙動を確認します。
用いている関数名createCipherで検索してみます。

それらしいファイルと該当箇所が見つかりました。

lib/crypto.js

exports.createCipher = exports.Cipher = Cipher;
function Cipher(cipher, password, options) {
  if (!(this instanceof Cipher))
    return new Cipher(cipher, password, options);
  this._handle = new binding.CipherBase(true);

  this._handle.init(cipher, toBuf(password));
  this._decoder = null;

  LazyTransform.call(this, options);
}

initいうところが怪しいのはわかったのですが、さすがに一般的な言葉すぎて検索には向きません

もう少し下を見てみると、鍵と初期ベクトルを用いた暗号化の実装がありました。

exports.createCipheriv = exports.Cipheriv = Cipheriv;
function Cipheriv(cipher, key, iv, options) {
  if (!(this instanceof Cipheriv))
    return new Cipheriv(cipher, key, iv, options);
  this._handle = new binding.CipherBase(true);
  this._handle.initiv(cipher, toBuf(key), toBuf(iv));
  this._decoder = null;

  LazyTransform.call(this, options);
}

initivという、それらしくかつ検索で引っかかりやすそうな関数があることがわかったので、
initivで検索をしてみます。

Node.jsのソースコードを追う(C++編)

initiv自体はC++側に実装が記載されているようです。

src / node_crypto.cc

NODE_SET_PROTOTYPE_METHOD(t, "init", Init);
NODE_SET_PROTOTYPE_METHOD(t, "initiv", InitIv);

実装箇所としては問題なさそうなので、実装方法を探ります。

initとinitivを比較すると、当該箇所が怪しそうです。

int key_len = EVP_BytesToKey(cipher_,
                               EVP_md5(),
                               NULL,
                               reinterpret_cast<const unsigned char*>(key_buf),
                               key_buf_len,
                               1,
                               key,
                               iv);

EVP_BytesToKeyについてnode.jsのソースコードを探しても見つからないので、
Google先生に聞いたところOpenSSLの関数のようです。

OpenSSL: Documents, EVP_BytesToKey(3)

実際に関数の説明を読むと、与えられたパスから鍵と初期ベクトルを生成すると記載があるため、
当該関数で実際の処理を行っていることは間違いなさそうです。

OpenSSLでの鍵と初期化ベクトルの取り出し

OpenSSLでNode.jsで暗号化したのと同じ条件で暗号化することで鍵と初期化ベクトルを調べます。

OpenSSLが導入してある環境で以下のコマンドを実行することでパスフレーズを用いた暗号化を実施できます。

$ openssl enc -aes-256-cbc -e -in /dev/null -out /dev/null  -p -pass pass:password -nosalt
key=5F4DCC3B5AA765D61D8327DEB882CF992B95990A9151374ABD8FF8C5A7A0FE08
iv =B7B4372CDFBCB3D16A2631B59B509E94

256bitの鍵と32bitの初期化ベクトルが出力されました。

結果の確認

抽出された鍵と初期化ベクトルを用いた場合とPassフレーズを用いた場合で、
OpenSSLでのコマンドの結果が同一のことを確認します。

cryptoutil.js

'use strict';
var crypto = require('crypto');

exports.cryptoAES256 = function(str) {
    var cipher = crypto.createCipher('aes-256-cbc', 'password');
    var crypted = cipher.update(str, 'utf-8', 'hex');
    crypted += cipher.final('hex');
    return crypted;
};

exports.cryptoAES256iv = function(str) {
    var key = new Buffer('5F4DCC3B5AA765D61D8327DEB882CF992B95990A9151374ABD8FF8C5A7A0FE08', 'hex');
    var iv = new Buffer('B7B4372CDFBCB3D16A2631B59B509E94', 'hex');
    var cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
    var crypted = cipher.update(str, 'utf-8', 'hex');
    crypted += cipher.final('hex');
    return crypted;
};

cryptoutiltest.js

'use strict';
var assert = require('assert');
var crypto = require('./cryptoutil');

it('ivと非ivの挙動確認', function() {
    var c = crypto.cryptoAES256('classmethod');
    var civ = crypto.cryptoAES256iv('classmethod');
    console.log(c);
    assert.equal(c, civ);
});

mochaの導入 & テスト実施

$ npm install -g mochamocha ./cryptoutiltest.js
$ mocha ./cryptoutiltest.js

92a76268018dab38ae55f3194460e63b
  ✓ ivと非ivの挙動確認

  1 passing (8ms)

パスフレーズから生成した場合と、鍵と初期化ベクトルで暗号化した場合と同一の結果になったことがわかります。

最後に暗号化された結果がOpenSSLで複合化できることを確認します。

上記で暗号化された結果である、92a76268018dab38ae55f3194460e63bを
バイナリエディタでCrypto.txtとして保存します。

パスフレーズを与えて複合化

$ openssl enc  -in ./Crypto.txt -aes-256-cbc -d -p -nosalt -pass pass:password
key=5F4DCC3B5AA765D61D8327DEB882CF992B95990A9151374ABD8FF8C5A7A0FE08
iv =B7B4372CDFBCB3D16A2631B59B509E94
classmethod

鍵と初期化ベクトルを与えて複合化

$  openssl enc  -in ./Crypto.txt -aes-256-cbc -d -p -nosalt -K "5F4DCC3B5AA765D61D8327DEB882CF992B95990A9151374ABD8FF8C5A7A0FE08" -iv "B7B4372CDFBCB3D16A2631B59B509E94"
key=5F4DCC3B5AA765D61D8327DEB882CF992B95990A9151374ABD8FF8C5A7A0FE08
iv =B7B4372CDFBCB3D16A2631B59B509E94
classmethod

まとめ

Node.jsを用いた際暗号化・複合化する際は、実装としてはOpenSSLを用いていることが確認できました。

また、暗号化・複合化に用いる鍵や初期化ベクトルを確認することができたので、 それらを用いて他の言語での実装でも問題なく実装できるかと思われます。