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

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

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

* PBKDF2
    https://en.wikipedia.org/wiki/PBKDF2

* Web Crypto API
    https://developer.mozilla.org/ja/docs/Web/API/Web_Crypto_API
    https://www.w3.org/TR/WebCryptoAPI/

    http://caniuse.com/#feat=cryptography

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

stringとUint8Arrayを変換するメソッドをまず用意しておくことにします。
TextEncoder使えって話もありますが、~~もう新しいこと調べたくないし~~[対応状況](http://caniuse.com/#feat=textencoder)が怪しそうだったので今回は使わないことにしましょう。そうしましょう。

```js
	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`ってのを使うそうです。  

```js
	var bytes2base64 = function(bytes) {
		return window.btoa(String.fromCharCode.apply(String, bytes));
	};
```
参考：https://coolaj86.com/articles/typedarray-buffer-to-base64-in-javascript/

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

```js
	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生成を実装しましょう。

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

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

```js
	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);
		});
	};
```

## 動かしてみる
呼び出しはこのような形

```js
	//動かしてみる
	var salt = gensalt();
	hashpw('password', salt).then(function(hash) {
		console.log(hash);
	});
```

```text:コンソール
kggDYDDnVH9bSHLp4SXxqfVYmmz/ObedPqCkFzGJvVY=
```

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

## テスト

```js
	//テスト
	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);
	});
```


```text:コンソール
salt1でのハッシュ              XHNJWY+6AI+JQOrwPsZkL6BhYEA1ZGwfjCwgaaANNHY=
もう一度salt1でのハッシュ       XHNJWY+6AI+JQOrwPsZkL6BhYEA1ZGwfjCwgaaANNHY=
salt2でのハッシュ              xkxyVfMsM+Z9G+hlN0E44Fb64hVpvzIWR0FMLKmXlHM=
salt1でのハッシュパスワード違い  W872Ee1s6JajEUTkYvw9YwQA1vHF/L2DR8N2q6Ptcvc=
```

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

# ブラウザサポート
手元の、ChromeとFirefoxは動きそうです。
IEは動きませんでした。Edgeは確認してません。

---

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