UNPKG

shelving

Version:

Toolkit for using data in JavaScript.

81 lines (80 loc) 3.38 kB
import { ValueError } from "../error/ValueError.js"; import { decodeBase64URLBytes, encodeBase64URL } from "./base64.js"; import { requireBytes } from "./bytes.js"; // Constants. const ALGORITHM = { name: "PBKDF2", hash: "SHA-512" }; const ITERATIONS = 500000; const SALT_LENGTH = 16; // 16 bytes = 128 bits const HASH_LENGTH = 64; // 64 bytes = 512 bits const PASSWORD_LENGTH = 6; /** Import a key for PBKDF2 operations (deriveBits) using SHA-512. */ function _getKey(password) { return crypto.subtle.importKey("raw", requireBytes(password), ALGORITHM, false, ["deriveBits"]); } /** Get random bytes from the crypto API. */ export function getRandomBytes(length) { const bytes = new Uint8Array(length); crypto.getRandomValues(bytes); return bytes; } /** * Hash a password using PBKDF2, generating a new salt, and return the combined salt$iterations$hash string. * * @param password The password to hash. * @param iterations The number of iterations. * * @returns Hash in the format `salt$iterations$hash`, where `salt` and `hash` are base64-encoded. * - Returned hash tring will be about 128 characters long (16 byte salt + iteration count + 64 byte hash + 2 separators = 116 characters once base64 encoded). */ export async function hashPassword(password, iterations = ITERATIONS) { // Checks. if (password.length < PASSWORD_LENGTH) throw new ValueError(`Password must be at least ${PASSWORD_LENGTH} characters long`, { received: password.length, caller: hashPassword, }); if (iterations < 1) throw new ValueError("Iterations must be number greater than 0", { received: iterations, caller: hashPassword }); // Hash the password. const key = await _getKey(password); const salt = getRandomBytes(SALT_LENGTH); const bits = HASH_LENGTH * 8; const hash = await crypto.subtle.deriveBits({ ...ALGORITHM, salt, iterations }, key, bits); // Return the combined string return `${encodeBase64URL(salt)}$${iterations}$${encodeBase64URL(hash)}`; } /** * Verify a password against a stored salt$iterations$hash string using PBKDF2. * * @param password The password to verify. * @param hash String in the format `salt$iterations$hash`, where `salt` and `hash` are base64-encoded. * * @returns True if the password matches the hash, false otherwise. */ export async function verifyPassword(password, hash) { // Check salthash. const [s, i, h] = hash.split("$"); if (!s || !i || !h) return false; const hashBytes = decodeBase64URLBytes(h); // Check iterations. const iterations = Number.parseInt(i, 10); if (!Number.isFinite(iterations) || iterations < 1) return false; // Derive the hash. const key = await _getKey(password); const salt = decodeBase64URLBytes(s); const bits = hashBytes.length * 8; const derivedBytes = new Uint8Array(await crypto.subtle.deriveBits({ ...ALGORITHM, salt, iterations }, key, bits)); // Compare the derived hash with the stored hash. return _compareBytes(derivedBytes, hashBytes); } /** Compare two sets of bytes using constant time comparison. */ function _compareBytes(left, right) { if (left.length !== right.length) return false; let result = 0; for (let i = 0; i < left.length; ++i) result |= left[i] ^ right[i]; return result === 0; }