Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Uses the SubtleCrypto interface of the Web Cryptography API to hash a password using PBKDF2, and validate a stored password hash against a subsequently supplied password. Note that both bcrypt and scrypt offer better defence against ASIC/GPU attacks, but are not available within WebCrypto.
/**
* Returns PBKDF2 derived key from supplied password.
*
* Stored key can subsequently be used to verify that a password matches the original password used
* to derive the key, using pbkdf2Verify().
*
* @param {String} password - Password to be hashed using key derivation function.
* @param {Number} [iterations=1e6] - Number of iterations of HMAC function to apply.
* @returns {String} Derived key as base64 string.
*
* @example
* const key = await pbkdf2('pāşšŵōřđ'); // eg 'djAxBRKXWNWPyXgpKWHld8SWJA9CQFmLyMbNet7Rle5RLKJAkBCllLfM6tPFa7bAis0lSTiB'
*/
async function pbkdf2(password, iterations=1e6) {
const pwUtf8 = new TextEncoder().encode(password); // encode pw as UTF-8
const pwKey = await crypto.subtle.importKey('raw', pwUtf8, 'PBKDF2', false, ['deriveBits']); // create pw key
const saltUint8 = crypto.getRandomValues(new Uint8Array(16)); // get random salt
const params = { name: 'PBKDF2', hash: 'SHA-256', salt: saltUint8, iterations: iterations }; // pbkdf2 params
const keyBuffer = await crypto.subtle.deriveBits(params, pwKey, 256); // derive key
const keyArray = Array.from(new Uint8Array(keyBuffer)); // key as byte array
const saltArray = Array.from(new Uint8Array(saltUint8)); // salt as byte array
const iterHex = ('000000'+iterations.toString(16)).slice(-6); // iter’n count as hex
const iterArray = iterHex.match(/.{2}/g).map(byte => parseInt(byte, 16)); // iter’ns as byte array
const compositeArray = [].concat(saltArray, iterArray, keyArray); // combined array
const compositeStr = compositeArray.map(byte => String.fromCharCode(byte)).join(''); // combined as string
const compositeBase64 = btoa('v01'+compositeStr); // encode as base64
return compositeBase64; // return composite key
}
/**
* Verifies whether the supplied password matches the password previously used to generate the key.
*
* @param {String} key - Key previously generated with pbkdf2().
* @param {String} password - Password to be matched against previously derived key.
* @returns {boolean} Whether password matches key.
*
* @example
* const match = await pbkdf2Verify(key, 'pāşšŵōřđ'); // true
*/
async function pbkdf2Verify(key, password) {
let compositeStr = null; // composite key is salt, iteration count, and derived key
try { compositeStr = atob(key); } catch (e) { throw new Error ('Invalid key'); } // decode from base64
const version = compositeStr.slice(0, 3); // 3 bytes
const saltStr = compositeStr.slice(3, 19); // 16 bytes (128 bits)
const iterStr = compositeStr.slice(19, 22); // 3 bytes
const keyStr = compositeStr.slice(22, 54); // 32 bytes (256 bits)
if (version != 'v01') throw new Error('Invalid key');
// -- recover salt & iterations from stored (composite) key
const saltUint8 = new Uint8Array(saltStr.match(/./g).map(ch => ch.charCodeAt(0))); // salt as Uint8Array
// note: cannot use TextEncoder().encode(saltStr) as it generates UTF-8
const iterHex = iterStr.match(/./g).map(ch => ch.charCodeAt(0).toString(16)).join(''); // iter’n count as hex
const iterations = parseInt(iterHex, 16); // iter’ns
// -- generate new key from stored salt & iterations and supplied password
const pwUtf8 = new TextEncoder().encode(password); // encode pw as UTF-8
const pwKey = await crypto.subtle.importKey('raw', pwUtf8, 'PBKDF2', false, ['deriveBits']); // create pw key
const params = { name: 'PBKDF2', hash: 'SHA-256', salt: saltUint8, iterations: iterations }; // pbkdf params
const keyBuffer = await crypto.subtle.deriveBits(params, pwKey, 256); // derive key
const keyArray = Array.from(new Uint8Array(keyBuffer)); // key as byte array
const keyStrNew = keyArray.map(byte => String.fromCharCode(byte)).join(''); // key as string
return keyStrNew == keyStr; // test if newly generated key matches stored key
}
@chrisveness

This comment has been minimized.

Copy link
Owner Author

commented Feb 17, 2017

Cryptography is indeed subtle. If I have made any errors, let me know and I will attempt to correct.

Thx to Tim Taubert @ Mozilla.

@lauriro

This comment has been minimized.

Copy link

commented Oct 11, 2018

new Uint8Array(saltStr.match(/./g).map(ch => ch.charCodeAt(0))) does not work as expected

"äõ".match(/./g).map(ch` => ch.charCodeAt(0))
// [228, 245]
new TextEncoder().encode("äo")
// [195, 164, 111]
@chrisveness

This comment has been minimized.

Copy link
Owner Author

commented Oct 15, 2018

@lauriro I think Uint8Array() is correct for saltUint8, as it needs to be byte-for-byte version of the salt, not a UTF-8-encoded version – I've updated my note about why TextEncoder().encode() wouldn't work.

@Zemnmez

This comment has been minimized.

Copy link

commented Feb 20, 2019

@chrisveness this implementation is dangerously broken under some conditions, because the number of iterations used to verify the digest is specified in the digest itself without possible verification. An attacker can send a digest with only one or zero iterations of HMAC.

-- additionally this implementation uses a non-fixed time string comparison, which allows an attacker to glean the digest (fwiw) if they are local to the system (e.g. in the same datacentre)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.