chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
// @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<string|null>} 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<boolean>}
*/
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<boolean>}
*/
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<boolean>} False when the storage cannot hold it
*/
async function change(next) {
const did = await getDid();
if (!did || !changeable()) return false;
await /** @type {NonNullable<typeof actorStorage.setAccountPassword>} */ (
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 };
}