{"version":3,"file":"encryption.mjs","names":[],"sources":["../../../../../../encryption/src/encryption.ts"],"sourcesContent":["import CryptoJS from \"crypto-js\";\nimport { assertIterations, getEncryptionConfig } from \"./configurations\";\nimport {\n  bytesToUtf8,\n  getSubtle,\n  randomBytes,\n  utf8ToBytes,\n} from \"./crypto-runtime\";\nimport {\n  AUTH_TAG_LENGTH_BITS,\n  buildHeader,\n  encodeEnvelope,\n  IV_LENGTH,\n  parseEnvelope,\n  SALT_LENGTH,\n} from \"./envelope\";\nimport {\n  DecryptionError,\n  EncryptionError,\n  MissingEncryptionKeyError,\n} from \"./errors\";\nimport { deriveKey } from \"./key-derivation\";\nimport { isLegacyCipher, isLegacyDriver, legacyDecrypt } from \"./legacy\";\nimport { DecryptOptions, EncryptOptions, LegacyCipherDriver } from \"./types\";\n\n/**\n * Return md5 hashed string\n *\n * @deprecated MD5 is collision-broken. Never use it for passwords, signatures\n * or integrity checks — it is exported for legacy interop (cache keys,\n * gravatar-style identifiers) only.\n *\n * @param {string} text\n * @returns {string}\n */\nexport function md5(text: string): string {\n  return CryptoJS.MD5(text).toString();\n}\n\n/**\n * Return sha1 hashed string\n *\n * @deprecated SHA-1 is collision-broken. Never use it for passwords,\n * signatures or integrity checks — it is exported for legacy interop only.\n *\n * @param {string} text\n * @returns {string}\n */\nexport function sha1(text: string): string {\n  return CryptoJS.SHA1(text).toString();\n}\n\n/**\n * Return sha256 hashed string\n *\n * @param {string} text\n * @returns {string}\n */\nexport function sha256(text: string): string {\n  return CryptoJS.SHA256(text).toString();\n}\n\n/**\n * Return sha512 hashed string\n *\n * @param {string} text\n * @returns {string}\n */\nexport function sha512(text: string): string {\n  return CryptoJS.SHA512(text).toString();\n}\n\nfunction assertKey(key: string): asserts key is string {\n  if (!key) {\n    throw new MissingEncryptionKeyError();\n  }\n\n  if (typeof key !== \"string\") {\n    throw new EncryptionError(\n      `The encryption key must be a string, ${typeof key} given.`\n    );\n  }\n}\n\n/**\n * Encrypt the given value with AES-256-GCM.\n *\n * The key is stretched with PBKDF2-HMAC-SHA256 over a fresh random salt, and\n * the payload is sealed under a fresh random 96-bit nonce; both are stored in\n * the returned envelope. Two calls with the same value and key therefore never\n * produce the same string, and any edit to the returned string makes\n * {@link decrypt} reject rather than return altered data.\n *\n * @breaking-change v2 — this is `async`. v1.x returned the ciphertext directly.\n *\n * @param {any} value any JSON-encodable value\n * @param {string} key\n * @param {EncryptOptions} options\n * @returns {Promise<string>} base64 envelope\n */\nexport async function encrypt(\n  value: any,\n  key: string = getEncryptionConfig(\"key\"),\n  options: EncryptOptions = {}\n): Promise<string> {\n  assertKey(key);\n\n  if (isLegacyDriver(options)) {\n    throw new EncryptionError(\n      \"encrypt() no longer takes a cipher driver: v2 always encrypts with AES-256-GCM. Drop the third argument; pass a legacy driver to decrypt() instead if you still need to read v1.x ciphertexts.\"\n    );\n  }\n\n  const iterations: number =\n    options?.iterations ?? getEncryptionConfig(\"iterations\");\n\n  assertIterations(iterations);\n\n  // Serialised before any randomness is drawn, so a circular value throws the\n  // same synchronous-shaped TypeError v1.x threw (surfaced as a rejection).\n  const data = utf8ToBytes(\n    JSON.stringify({\n      data: value,\n    })\n  );\n\n  const subtle = getSubtle();\n  const salt = randomBytes(SALT_LENGTH);\n  const iv = randomBytes(IV_LENGTH);\n  const header = buildHeader(iterations, salt, iv);\n\n  const cryptoKey = await deriveKey(key, salt, iterations);\n\n  const payload = await subtle.encrypt(\n    {\n      name: \"AES-GCM\",\n      iv,\n      // Binds version, suite, iteration count, salt and nonce to the tag —\n      // none of them can be edited in transit without the tag check failing.\n      additionalData: header,\n      tagLength: AUTH_TAG_LENGTH_BITS,\n    },\n    cryptoKey,\n    data\n  );\n\n  return encodeEnvelope(header, new Uint8Array(payload));\n}\n\n/**\n * Decrypt the given ciphertext and return its original value.\n *\n * @breaking-change v2 — this is `async`, and it **throws** a\n * {@link DecryptionError} on a wrong key, a tampered envelope or a malformed\n * input, where v1.x returned `null`. Failing loudly is the entire point of\n * moving to an authenticated cipher: a returned `null` cannot be distinguished\n * from a legitimately encrypted `null`. Use {@link tryDecrypt} for the old\n * null-on-failure shape.\n *\n * @param {string} cypher\n * @param {string} key\n * @param {DecryptOptions|LegacyCipherDriver} options a v1.x cipher driver is\n *        accepted here and read as `{ legacyDriver, legacyDecryption: true }`.\n * @returns {Promise<any>}\n */\nexport async function decrypt(\n  cypher: string,\n  key: string = getEncryptionConfig(\"key\"),\n  options: DecryptOptions | LegacyCipherDriver = {}\n): Promise<any> {\n  assertKey(key);\n\n  const { legacyDecryption, legacyDriver } = normalizeDecryptOptions(options);\n\n  if (typeof cypher !== \"string\" || cypher.length === 0) {\n    throw new DecryptionError(\n      \"Unable to decrypt: the given ciphertext is empty or not a string.\"\n    );\n  }\n\n  const envelope = parseEnvelope(cypher);\n\n  if (!envelope) {\n    return decryptLegacy(cypher, key, legacyDecryption, legacyDriver);\n  }\n\n  const cryptoKey = await deriveKey(key, envelope.salt, envelope.iterations);\n\n  let plainBytes: ArrayBuffer;\n\n  try {\n    plainBytes = await getSubtle().decrypt(\n      {\n        name: \"AES-GCM\",\n        iv: envelope.iv,\n        additionalData: envelope.header,\n        tagLength: AUTH_TAG_LENGTH_BITS,\n      },\n      cryptoKey,\n      envelope.payload\n    );\n  } catch {\n    // Deliberately one message for both \"wrong key\" and \"tampered\": the two\n    // are indistinguishable to GCM, and keeping them indistinguishable here\n    // denies an attacker an oracle.\n    throw new DecryptionError(\n      \"Authentication failed: the ciphertext was modified, or the key is wrong.\"\n    );\n  }\n\n  try {\n    return JSON.parse(bytesToUtf8(new Uint8Array(plainBytes))).data;\n  } catch {\n    // Authenticated, so this is our own data — just not a payload this\n    // version knows how to read.\n    throw new DecryptionError(\n      \"Decryption succeeded but the payload is not a @mongez/encryption envelope body.\"\n    );\n  }\n}\n\n/**\n * Decrypt, returning `null` instead of throwing when the ciphertext cannot be\n * authenticated.\n *\n * This is the v1.x failure shape, for callers that genuinely do not care why a\n * value failed to decrypt. Note the ambiguity it carries: `encrypt(null)`\n * round-trips to `null` too, so `null` here means \"no usable value\", not\n * \"failure\". Prefer {@link decrypt}.\n *\n * @param {string} cypher\n * @param {string} key\n * @param {DecryptOptions|LegacyCipherDriver} options\n * @returns {Promise<any|null>}\n */\nexport async function tryDecrypt(\n  cypher: string,\n  key: string = getEncryptionConfig(\"key\"),\n  options: DecryptOptions | LegacyCipherDriver = {}\n): Promise<any> {\n  try {\n    return await decrypt(cypher, key, options);\n  } catch (error) {\n    // A missing key or an unusable runtime is a programming/deployment fault,\n    // not a bad ciphertext — those still throw.\n    if (error instanceof DecryptionError) return null;\n    throw error;\n  }\n}\n\nfunction normalizeDecryptOptions(options: DecryptOptions | LegacyCipherDriver) {\n  if (isLegacyDriver(options)) {\n    return { legacyDecryption: true, legacyDriver: options };\n  }\n\n  const { legacyDecryption, legacyDriver } = (options ??\n    {}) as DecryptOptions;\n\n  return {\n    legacyDecryption: legacyDecryption ?? !!legacyDriver,\n    legacyDriver,\n  };\n}\n\nfunction decryptLegacy(\n  cypher: string,\n  key: string,\n  legacyDecryption: boolean,\n  legacyDriver?: LegacyCipherDriver\n): any {\n  if (!isLegacyCipher(cypher)) {\n    throw new DecryptionError(\n      \"Unrecognised ciphertext: not an AES-GCM envelope, and not a legacy (v1.x) ciphertext either.\"\n    );\n  }\n\n  const enabled =\n    legacyDecryption || getEncryptionConfig(\"legacyDecryption\") === true;\n\n  if (!enabled) {\n    throw new DecryptionError(\n      \"This is a legacy (v1.x) ciphertext, which is unauthenticated and therefore rejected by default. Enable it while migrating with setEncryptionConfigurations({ legacyDecryption: true }), then re-encrypt the value.\"\n    );\n  }\n\n  return legacyDecrypt(\n    cypher,\n    key,\n    legacyDriver ?? getEncryptionConfig(\"legacyDriver\")\n  );\n}\n"],"mappings":"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"}