200 lines · 7.7 KB
const encoder = new TextEncoder();

const ID_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz";
const ID_LENGTH = 20;
/** Workers' WebCrypto refuses PBKDF2 above 100,000 iterations, so this is the highest cost the runtime allows. */
const PBKDF2_ITERATIONS = 100_000;
const PBKDF2_MAX_ITERATIONS = 100_000;
const SALT_BYTES = 16;
const HASH_BITS = 256;
const AES_IV_BYTES = 12;
const AES_KEY_BITS = 256;
/** Ciphertexts under an HKDF-derived per-purpose key with AAD carry this prefix; bare base64 is legacy. */
const SEALED_V2_PREFIX = "v2:";

export function randomId(length = ID_LENGTH): string {
  const bytes = crypto.getRandomValues(new Uint8Array(length));
  let out = "";
  for (const byte of bytes) {
    out += ID_ALPHABET[byte % ID_ALPHABET.length];
  }
  return out;
}

export function toHex(bytes: ArrayBuffer | Uint8Array): string {
  const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
  let out = "";
  for (const byte of view) {
    out += byte.toString(16).padStart(2, "0");
  }
  return out;
}

export function fromHex(hex: string): Uint8Array<ArrayBuffer> {
  const out = new Uint8Array(hex.length / 2);
  for (let index = 0; index < out.length; index++) {
    out[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16);
  }
  return out;
}