// @pdsjs/core/account-password - the account password as a stored credential. // // The configured password (PDS_PASSWORD) seeds a fresh server. Once the user // changes it through the account pages, a hash lives in actor storage and // wins. Setting a different configured password afterwards is the operator's // reset: it wins over the stored hash and clears it, which is the recovery // path when the password is lost. Removing the configured password is not a // reset; the stored hash keeps working. import { hashAppPassword } from './app-password.js'; import { timingSafeEqual } from './crypto.js'; /** * @typedef {Object} AccountPasswordContext * @property {import('./ports.js').ActorStoragePort} actorStorage * @property {() => Promise} getDid * @property {string} [configuredPassword] */ /** * @param {AccountPasswordContext} ctx */ export function createAccountPasswordStore(ctx) { const { actorStorage, getDid, configuredPassword } = ctx; async function stored() { return typeof actorStorage.getAccountPassword === 'function' ? await actorStorage.getAccountPassword() : null; } /** * Whether `candidate` is the account password. * @param {unknown} candidate * @returns {Promise} */ async function verify(candidate) { if (typeof candidate !== 'string' || candidate === '') return false; const did = await getDid(); const record = did ? await stored() : null; if (record && did) { if (configuredPassword) { const configuredHash = await hashAppPassword(did, configuredPassword); if (!(await timingSafeEqual(configuredHash, record.seedHash ?? ''))) { // The configured password moved since the change: an operator // reset. It becomes the password again and the stored hash goes. if (typeof actorStorage.setAccountPassword === 'function') { await actorStorage.setAccountPassword(null); } return timingSafeEqual(candidate, configuredPassword); } } const candidateHash = await hashAppPassword(did, candidate); return timingSafeEqual(candidateHash, record.hash); } if (!configuredPassword) return false; return timingSafeEqual(candidate, configuredPassword); } /** * Whether any password can sign in: a configured one, or a stored one. * @returns {Promise} */ async function configured() { if (configuredPassword) return true; return !!(await getDid()) && !!(await stored()); } /** * Whether the storage can hold a changed password. * @returns {boolean} */ function changeable() { return typeof actorStorage.setAccountPassword === 'function'; } /** * Store `next` as the account password. The caller verifies the current * password first. * @param {string} next * @returns {Promise} False when the storage cannot hold it */ async function change(next) { const did = await getDid(); if (!did || !changeable()) return false; await /** @type {NonNullable} */ ( actorStorage.setAccountPassword )({ hash: await hashAppPassword(did, next), seedHash: configuredPassword ? await hashAppPassword(did, configuredPassword) : null, changedAt: new Date().toISOString(), }); return true; } return { verify, configured, changeable, change }; }