javascriptでPBKDF2パスワードハッシュ(Web Crypto API利用)


jsでパスワードハッシュってどうやるんだろうと思って調べたので書きます。

(本記事はWeb Crypto APIを使ったブラウザでの動作が前提のものです。Node.jsでは使えません。)

Uint8Array

Web Crypto APIを扱うにはUint8Arrayとか扱わないといけないみたいです。
また普段使わない奴出てきてつらいですが、javaのbyte配列みたいなものを扱う奴ってことでしょうか?
http://qiita.com/yaegaki/items/909587a2dae20467c74a

stringとUint8Arrayを変換するメソッドをまず用意しておくことにします。
TextEncoder使えって話もありますが、もう新しいこと調べたくないし対応状況が怪しそうだったので今回は使わないことにしましょう。そうしましょう。

    var str2bytes = function(str) {
        return (new Uint8Array(Array.prototype.map.call(str, function(c) {
            return c.charCodeAt(0);
        })));
    };

参考:https://gist.github.com/kawanet/352a2ed1d1656816b2bc

あと、ついでなのでbase64変換も用意しておきましょう。
btoaってのを使うそうです。

    var bytes2base64 = function(bytes) {
        return window.btoa(String.fromCharCode.apply(String, bytes));
    };

参考:https://coolaj86.com/articles/typedarray-buffer-to-base64-in-javascript/

今回は使いませんが、逆はatobってのを使うそうです。

    var base642bytes = function(str) {
        var binary = window.atob(str);
        return str2bytes(binary);
    };

PBKDF2

やっと本題です。PBKDF2によるパスワードハッシュします。

以下参考にさせていただきました。ありがとうございます。
https://github.com/diafygi/webcrypto-examples#pbkdf2
https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/deriveKey
https://timtaubert.de/blog/2015/05/implementing-a-pbkdf2-based-password-storage-scheme-for-firefox-os/

salt生成

まずはsalt生成を実装しましょう。

    var gensalt = function(saltLength) {
        return window.crypto.getRandomValues(new Uint8Array(saltLength || 8));
    };

パスワードハッシュ化

importKeyとかderiveBitsとか英語読めないので詳しい仕様はよくわかりませんが、
先人たちの実装を見ると以下のように書けば行ける様子です。

    var importKey = function(password) {
        return window.crypto.subtle.importKey(
            'raw',
            str2bytes(password),
            {
                name: 'PBKDF2',
            },
            false,
            ['deriveBits']
        );
    };
    var getHashOutputLength = function(hashAlgorithm) {
        switch (hashAlgorithm) {
        case 'SHA-1': return 160;
        case 'SHA-256': return 256;
        case 'SHA-384': return 384;
        case 'SHA-512': return 512;
        default:
            break;
        }

        throw new Error('Unsupported hash algorithm');
    };
    var deriveBits = function(key, salt, hashAlgorithm, iterations) {
        hashAlgorithm = hashAlgorithm || 'SHA-256';
        return window.crypto.subtle.deriveBits(
            {
                name: 'PBKDF2',
                salt: salt,
                iterations: iterations || 1000,
                hash: {name: hashAlgorithm}, //"SHA-1", "SHA-256", "SHA-384", or "SHA-512"
            },
            key,
            getHashOutputLength(hashAlgorithm)
        );
    };

    /**
     * パスワードハッシュ化
     */
    var hashpw = function(password, salt) {
        return importKey(password).then(function(key) {
            return deriveBits(key, salt, 'SHA-256', 1000);
        }).then(function(buffer) {
            var bytes = new Uint8Array(buffer);
            return bytes2base64(bytes);
        });
    };

動かしてみる

呼び出しはこのような形

    //動かしてみる
    var salt = gensalt();
    hashpw('password', salt).then(function(hash) {
        console.log(hash);
    });
コンソール
kggDYDDnVH9bSHLp4SXxqfVYmmz/ObedPqCkFzGJvVY=

これだとうまくいったかわかりませんね。少しテストをします。

テスト

    //テスト
    var salt1 = gensalt();
    var salt2 = gensalt();
    hashpw('password', salt1).then(function(hash) {
        console.log('salt1でのハッシュ\t\t\t\t', hash);
    });
    hashpw('password', salt1).then(function(hash) {
        console.log('もう一度salt1でのハッシュ\t\t\t', hash);
    });
    hashpw('password', salt2).then(function(hash) {
        console.log('salt2でのハッシュ\t\t\t\t', hash);
    });
    hashpw('pppppppp', salt1).then(function(hash) {
        console.log('salt1でのハッシュパスワード違い\t', hash);
    });
コンソール
salt1でのハッシュ              XHNJWY+6AI+JQOrwPsZkL6BhYEA1ZGwfjCwgaaANNHY=
もう一度salt1でのハッシュ       XHNJWY+6AI+JQOrwPsZkL6BhYEA1ZGwfjCwgaaANNHY=
salt2でのハッシュ              xkxyVfMsM+Z9G+hlN0E44Fb64hVpvzIWR0FMLKmXlHM=
salt1でのハッシュパスワード違い  W872Ee1s6JajEUTkYvw9YwQA1vHF/L2DR8N2q6Ptcvc=

これを見たところでPDKDF2になってるかどうかはわかりませんが、
パスワードハッシュとしては大丈夫な雰囲気が出ています。

ブラウザサポート

手元の、ChromeとFirefoxは動きそうです。
IEは動きませんでした。Edgeは確認してません。


これで何か作っても、ブラウザの開発者ツール使われたらハッシュ化ロジックはバレバレですね。ハッシュは不可逆だからある程度は平気な気がしますが、ガチで守りたいパスワードで使うのはよしたほうが良いのでしょうか?どうなのでしょうか?