272 lines · 9.7 KB
import type { SqlExecutor } from "./storage.ts";
import type { AddressRow, OwnerRef } from "./types.ts";

const ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
const LOCAL_PART_LENGTH = 10;
const MAX_CREATE_ATTEMPTS = 5;

// Addresses must not be predictable: users can mint addresses and see the
// results, and Math.random()'s PRNG state is recoverable from observed
// output, which would let one user guess another's addresses. Uses rejection
// sampling: bytes at or above the largest multiple of the alphabet length
// are discarded rather than folded in with %, which would bias the low
// characters of the alphabet.
const REJECTION_LIMIT = 256 - (256 % ALPHABET.length);

// Exported since other receivers (mail.tm) need their own random strings
// (a local part, an account password) with the same guarantee.
export function randomAlphanumeric(length: number): string {
  let out = "";
  while (out.length < length) {
    const bytes = new Uint8Array(length);
    crypto.getRandomValues(bytes);
    for (const byte of bytes) {
      if (byte >= REJECTION_LIMIT) {
        continue;
      }
      out += ALPHABET[byte % ALPHABET.length];
      if (out.length === length) {
        break;
      }
    }
  }
  return out;
}

export async function createAddress(
  db: SqlExecutor,
  owner: OwnerRef,
  domain: string,
  ttlSeconds: number,