UNPKG

passlib

Version:

Your friendly password storage and verification library

118 lines (89 loc) 2.98 kB
"use strict"; const crypto = require("crypto"); const {ensureBuffer, pbkdf2, isSameConstantTime} = require("./util"); const VERSION = 1; const VERSION_LEN = 2; const ITER_MIN = 20000; let iter = ITER_MIN; const ITER_LEN = 4; const SALT_LEN = 32; const CHECK = "sha224"; const CHECK_LEN = 28; const KDF = "sha512"; const KEY_LEN = 64; const MAC = "sha256"; const MAC_LEN = 32; const MAX_ITER_FACTOR = 8; const TARGET_MS = 100; // Lower bound time target in ms const TO_ITER_LEN = VERSION_LEN + ITER_LEN; const TO_SALT_LEN = TO_ITER_LEN + SALT_LEN; const TO_CHECK_LEN = TO_SALT_LEN + CHECK_LEN; const FULL_LEN = TO_CHECK_LEN + MAC_LEN; const INIT_ITERS = 3; async function timeOneCreate() { // one megabyte of a-s! const pw = "a".repeat(1 << 20); const start = Date.now(); await create(pw); return Date.now() - start; } async function init() { for (let i = 0; i < INIT_ITERS; ++i) { const dur = await timeOneCreate(); iter = Math.max(iter, Math.floor(iter * (TARGET_MS / dur) / 1000) * 1000); } } async function create(password) { const rv = Buffer.alloc(FULL_LEN); rv.writeUInt16BE(VERSION, 0); rv.writeUInt32BE(iter, VERSION_LEN); crypto.randomFillSync(rv, TO_ITER_LEN, SALT_LEN); const salt = rv.slice(TO_ITER_LEN, TO_SALT_LEN); const check = crypto.createHash(CHECK); check.update(rv.slice(0, TO_SALT_LEN)); check.digest().copy(rv, TO_SALT_LEN); const key = await pbkdf2(password, salt, iter, KEY_LEN, KDF); const hmac = crypto.createHmac(MAC, key); hmac.update(rv.slice(0, TO_CHECK_LEN)); hmac.digest().copy(rv, TO_CHECK_LEN); return rv; } async function verify(buffer, password) { ensureBuffer(buffer); if (buffer.length !== FULL_LEN) { throw new Error("Truncated"); } const ver = buffer.readUInt16BE(0); if (ver !== VERSION) { throw new Error("Version mismatch"); } const iterations = buffer.readUInt32BE(VERSION_LEN); if (iterations >= iter * MAX_ITER_FACTOR) { throw new Error("Refusing to compute long running KDF"); } if (iterations < ITER_MIN) { throw new Error("Refusing to compute with few iterations"); } const salt = buffer.slice(TO_ITER_LEN, TO_SALT_LEN); const expectedCheck = buffer.slice(TO_SALT_LEN, TO_CHECK_LEN); const check = crypto.createHash(CHECK); check.update(buffer.slice(0, TO_SALT_LEN)); const actualCheck = check.digest(); // no need to prevent timing side channels // the correct check is public knowledge already if (Buffer.compare(expectedCheck, actualCheck)) { return false; } const key = await pbkdf2(password, salt, iterations, KEY_LEN, KDF); const expectedMAC = buffer.slice(TO_CHECK_LEN, FULL_LEN); const hmac = crypto.createHmac(MAC, key); hmac.update(buffer.slice(0, TO_CHECK_LEN)); const actualMAC = hmac.digest(); // potential timing side channel since we compare mac, so use same() return isSameConstantTime(expectedMAC, actualMAC); } module.exports = { init, create, verify };