~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
99 lines · 3.4 KB Raw History
// @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 };
}