Skip to content
LogoLogo

@portable/password

Functions

Password.hash

Hashes a password with PBKDF2-HMAC-SHA256 and a random 16-byte salt, and returns a 32-byte hash in PHC string format. A string password is normalized to NFC and encoded as UTF-8, and bytes are hashed as they are.

async function hash(
  password: string | Uint8Array,
  options: hash.Options = {},
): Promise<string>;
OptionTypeDefaultDescription
iterationsnumber600000PBKDF2 iterations. Cloudflare Workers rejects more than 100,000.
maxBytesnumber4096Longest password accepted, in UTF-8 bytes after NFC normalization.
signalAbortSignalRejects the returned promise with signal.reason when aborted. WebCrypto can't stop a derivation that has started, so the derivation finishes in the background and its result is dropped.

Throws

  • MaxBytesError When the password is longer than maxBytes.
import * as Password from '@portable/password';
 
await Password.hash('correct horse battery staple');
// '$pbkdf2-sha256$i=600000$ZmFrZXNhbHRmYWtlc2FsdA$...'
await Password.hash(password, { iterations: 100_000 }); // the most Cloudflare Workers accepts

Password.needsRehash

Returns true when a stored hash is weaker than what hash writes with the same iterations: fewer iterations, a digest other than SHA-256, a salt shorter than 16 bytes, or a hash shorter than 32 bytes. Call it after a successful verify and store a new hash of the password when it returns true.

function needsRehash(
  stored: string,
  options: needsRehash.Options = {},
): boolean;
OptionTypeDefaultDescription
iterationsnumber600000Iterations a current hash has at least.

Throws

  • InvalidInputError When stored is not a well-formed PBKDF2 hash.
  • UnsupportedError When stored is another algorithm's hash.
import * as Password from '@portable/password';
 
if (await Password.verify(password, user.hash)) {
  if (Password.needsRehash(user.hash)) {
    await saveHash(user, await Password.hash(password));
  }
}

Password.verify

Checks a password against a stored PBKDF2 hash and compares the result in constant time. Reads the PHC format that hash writes ($pbkdf2-sha256$i=600000$...) and passlib's pbkdf2_sha1, pbkdf2_sha256 and pbkdf2_sha512 format ($pbkdf2-sha256$29000$...).

async function verify(
  password: string | Uint8Array,
  stored: string,
  options: verify.Options = {},
): Promise<boolean>;
OptionTypeDefaultDescription
maxBytesnumber4096Longest password accepted, in UTF-8 bytes after NFC normalization.
signalAbortSignalRejects the returned promise with signal.reason when aborted. WebCrypto can't stop a derivation that has started, so the derivation finishes in the background and its result is dropped.

Throws

  • InvalidInputError When stored is not a well-formed PBKDF2 hash.
  • UnsupportedError When stored is another algorithm's hash, such as $argon2id$....
  • MaxBytesError When the password is longer than maxBytes.
import * as Password from '@portable/password';
 
const stored = await Password.hash('hunter2');
await Password.verify('hunter2', stored); // true
await Password.verify('hunter3', stored); // false

Errors

Password.InvalidInputError

Thrown by verify and needsRehash for a stored hash that is malformed: a PBKDF2 hash in PHC or passlib format with a missing field or a bad value, or a string without a $<algorithm>$ prefix.

class InvalidInputError extends Errors.InvalidInputError {
  readonly kind: string;
}
import * as Password from '@portable/password';
 
try {
  await Password.verify(password, '$pbkdf2-sha256$i=0$...');
} catch (error) {
  if (error instanceof Password.InvalidInputError) {
    console.log('malformed hash');
  }
}

Password.MaxBytesError

Thrown by hash and verify when the password is longer than maxBytes in UTF-8.

class MaxBytesError extends Errors.MaxBytesError {
  constructor(options: Errors.MaxBytesError.Options);
  readonly kind: string;
}
import * as Password from '@portable/password';
 
try {
  await Password.hash('x'.repeat(5000));
} catch (error) {
  if (error instanceof Password.MaxBytesError) {
    console.log(`limit is ${error.maxBytes} bytes`);
  }
}

Password.UnsupportedError

Thrown by verify and needsRehash for a hash from another algorithm, such as bcrypt ($2b$...), scrypt or Argon2 ($argon2id$...). WebCrypto can't compute those, so verify them with another library while you migrate.

class UnsupportedError extends Errors.UnsupportedError {
  readonly kind: string;
}
import * as Password from '@portable/password';
 
try {
  return await Password.verify(password, stored);
} catch (error) {
  if (error instanceof Password.UnsupportedError) {
    return verifyLegacy(password, stored);
  }
  throw error;
}