{"version":3,"file":"envelope.mjs","names":[],"sources":["../../../../../../encryption/src/envelope.ts"],"sourcesContent":["import { concatBytes, toBase64, tryFromBase64 } from \"./crypto-runtime\";\nimport { DecryptionError } from \"./errors\";\n\n/**\n * Envelope version 1 — the only format this package writes.\n *\n * A leading version byte makes the format self-describing: future algorithm\n * changes bump the version (or the suite) and `decrypt` can still recognise,\n * and refuse or handle, every generation of ciphertext it is handed.\n */\nexport const ENVELOPE_VERSION_1 = 0x01;\n\n/**\n * Cipher suite 1 — PBKDF2-HMAC-SHA256 → AES-256-GCM with a 128-bit tag.\n */\nexport const SUITE_PBKDF2_SHA256_AES_256_GCM = 0x01;\n\n/** Random per-message PBKDF2 salt, in bytes. */\nexport const SALT_LENGTH = 16;\n\n/** Random per-message GCM nonce, in bytes. 96 bits is the GCM-native size. */\nexport const IV_LENGTH = 12;\n\n/** GCM authentication tag, in bytes (128 bits). */\nexport const AUTH_TAG_LENGTH = 16;\n\n/** GCM authentication tag, in bits — what WebCrypto's `tagLength` wants. */\nexport const AUTH_TAG_LENGTH_BITS = AUTH_TAG_LENGTH * 8;\n\n/**\n * version(1) + suite(1) + iterations(4, uint32 BE) + salt(16) + iv(12).\n *\n * The whole header is fed to AES-GCM as additional authenticated data, so the\n * declared iteration count, salt and nonce are covered by the auth tag and\n * cannot be edited without the tag check failing.\n */\nexport const HEADER_LENGTH = 2 + 4 + SALT_LENGTH + IV_LENGTH;\n\n/** OWASP-aligned default work factor for PBKDF2-HMAC-SHA256. */\nexport const DEFAULT_ITERATIONS = 210_000;\n\n/** Floor enforced when encrypting. Below this the KDF is not worth its name. */\nexport const MIN_ITERATIONS = 100_000;\n\n/**\n * Ceiling accepted when decrypting.\n *\n * The iteration count is read out of attacker-reachable ciphertext, so an\n * unbounded value would be a trivial CPU-exhaustion vector: a forged envelope\n * claiming 4 billion iterations would pin a core for minutes before the tag\n * check ever ran. Anything above this is rejected before key derivation.\n */\nexport const MAX_ITERATIONS = 5_000_000;\n\nexport type EncryptionEnvelope = {\n  version: number;\n  suite: number;\n  iterations: number;\n  salt: Uint8Array;\n  iv: Uint8Array;\n  /** The raw header bytes, reused verbatim as GCM additional authenticated data. */\n  header: Uint8Array;\n  /** Ciphertext with the GCM tag appended, exactly as WebCrypto returns it. */\n  payload: Uint8Array;\n};\n\n/**\n * Build the 34-byte version-1 header.\n */\nexport function buildHeader(\n  iterations: number,\n  salt: Uint8Array,\n  iv: Uint8Array\n): Uint8Array {\n  const header = new Uint8Array(HEADER_LENGTH);\n\n  header[0] = ENVELOPE_VERSION_1;\n  header[1] = SUITE_PBKDF2_SHA256_AES_256_GCM;\n\n  new DataView(header.buffer).setUint32(2, iterations, false);\n\n  header.set(salt, 6);\n  header.set(iv, 6 + SALT_LENGTH);\n\n  return header;\n}\n\n/**\n * Serialise header + payload into the base64 string handed back to callers.\n */\nexport function encodeEnvelope(\n  header: Uint8Array,\n  payload: Uint8Array\n): string {\n  return toBase64(concatBytes(header, payload));\n}\n\n/**\n * Parse a ciphertext string as a version-1 envelope.\n *\n * Returns `null` when the input is not a version-1 envelope at all (bad\n * base64, or a different leading version byte) — that is the signal to try the\n * legacy path. Throws when the input *claims* to be a version-1 envelope but\n * is truncated, uses an unknown suite, or declares an implausible work factor;\n * those are corrupt/hostile inputs, not old data.\n */\nexport function parseEnvelope(cipher: string): EncryptionEnvelope | null {\n  const bytes = tryFromBase64(cipher);\n\n  if (!bytes || bytes.length === 0) return null;\n  if (bytes[0] !== ENVELOPE_VERSION_1) return null;\n\n  if (bytes.length < HEADER_LENGTH + AUTH_TAG_LENGTH) {\n    throw new DecryptionError(\n      \"Malformed ciphertext: the envelope is shorter than its own header and authentication tag.\"\n    );\n  }\n\n  const suite = bytes[1];\n\n  if (suite !== SUITE_PBKDF2_SHA256_AES_256_GCM) {\n    throw new DecryptionError(\n      `Unsupported cipher suite 0x${suite\n        .toString(16)\n        .padStart(2, \"0\")} in a version 1 envelope; this ciphertext was produced by a newer version of @mongez/encryption.`\n    );\n  }\n\n  const iterations = new DataView(\n    bytes.buffer,\n    bytes.byteOffset,\n    bytes.byteLength\n  ).getUint32(2, false);\n\n  if (iterations < 1 || iterations > MAX_ITERATIONS) {\n    throw new DecryptionError(\n      `Refusing to decrypt: the envelope declares ${iterations} PBKDF2 iterations, outside the accepted range of 1..${MAX_ITERATIONS}.`\n    );\n  }\n\n  return {\n    version: ENVELOPE_VERSION_1,\n    suite,\n    iterations,\n    salt: bytes.subarray(6, 6 + SALT_LENGTH),\n    iv: bytes.subarray(6 + SALT_LENGTH, HEADER_LENGTH),\n    header: bytes.subarray(0, HEADER_LENGTH),\n    payload: bytes.subarray(HEADER_LENGTH),\n  };\n}\n\n/**\n * Whether the given string looks like a version-1 AES-GCM envelope.\n *\n * Useful for migration scripts that walk a store and re-encrypt whatever is\n * still in the legacy format.\n */\nexport function isEncryptionEnvelope(cipher: string): boolean {\n  const bytes = tryFromBase64(cipher);\n\n  return (\n    !!bytes &&\n    bytes.length >= HEADER_LENGTH + AUTH_TAG_LENGTH &&\n    bytes[0] === ENVELOPE_VERSION_1 &&\n    bytes[1] === SUITE_PBKDF2_SHA256_AES_256_GCM\n  );\n}\n"],"mappings":";;;;;;;;;;;AAUA,MAAa,qBAAqB;;;;AAKlC,MAAa,kCAAkC;;AAG/C,MAAa,cAAc;;AAG3B,MAAa,YAAY;;AAGzB,MAAa,kBAAkB;;AAG/B,MAAa,4BAAyC;;;;;;;;AAStD,MAAa,gBAAgB;;AAG7B,MAAa,qBAAqB;;AAGlC,MAAa,iBAAiB;;;;;;;;;AAU9B,MAAa,iBAAiB;;;;AAiB9B,SAAgB,YACd,YACA,MACA,IACY;CACZ,MAAM,SAAS,IAAI,aAAwB;CAE3C,OAAO;CACP,OAAO;CAEP,IAAI,SAAS,OAAO,MAAM,CAAC,CAAC,UAAU,GAAG,YAAY,KAAK;CAE1D,OAAO,IAAI,MAAM,CAAC;CAClB,OAAO,IAAI,IAAI,EAAe;CAE9B,OAAO;AACT;;;;AAKA,SAAgB,eACd,QACA,SACQ;CACR,OAAO,SAAS,YAAY,QAAQ,OAAO,CAAC;AAC9C;;;;;;;;;;AAWA,SAAgB,cAAc,QAA2C;CACvE,MAAM,QAAQ,cAAc,MAAM;CAElC,IAAI,CAAC,SAAS,MAAM,WAAW,GAAG,OAAO;CACzC,IAAI,MAAM,UAA2B,OAAO;CAE5C,IAAI,MAAM,SAAS,IACjB,MAAM,IAAI,gBACR,2FACF;CAGF,MAAM,QAAQ,MAAM;CAEpB,IAAI,aACF,MAAM,IAAI,gBACR,8BAA8B,MAC3B,SAAS,EAAE,CAAC,CACZ,SAAS,GAAG,GAAG,EAAE,iGACtB;CAGF,MAAM,aAAa,IAAI,SACrB,MAAM,QACN,MAAM,YACN,MAAM,UACR,CAAC,CAAC,UAAU,GAAG,KAAK;CAEpB,IAAI,aAAa,KAAK,kBACpB,MAAM,IAAI,gBACR,8CAA8C,WAAW,uDAAuD,eAAe,EACjI;CAGF,OAAO;EACL;EACA;EACA;EACA,MAAM,MAAM,SAAS,GAAG,EAAe;EACvC,IAAI,MAAM,SAAS,MAA8B;EACjD,QAAQ,MAAM,SAAS,KAAgB;EACvC,SAAS,MAAM,WAAsB;CACvC;AACF;;;;;;;AAQA,SAAgB,qBAAqB,QAAyB;CAC5D,MAAM,QAAQ,cAAc,MAAM;CAElC,OACE,CAAC,CAAC,SACF,MAAM,UAAU,MAChB,MAAM,YACN,MAAM;AAEV"}