@fgv/ts-extras 5.1.0-40 → 5.1.0-41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/packlets/crypto-utils/hpkeProvider.js +34 -12
  2. package/dist/packlets/crypto-utils/hpkeProvider.js.map +1 -1
  3. package/dist/packlets/crypto-utils/index.browser.js +1 -1
  4. package/dist/packlets/crypto-utils/index.browser.js.map +1 -1
  5. package/dist/packlets/crypto-utils/index.js +1 -1
  6. package/dist/packlets/crypto-utils/index.js.map +1 -1
  7. package/dist/packlets/crypto-utils/keystore/converters.js +3 -4
  8. package/dist/packlets/crypto-utils/keystore/converters.js.map +1 -1
  9. package/dist/packlets/crypto-utils/keystore/keyStore.js +155 -16
  10. package/dist/packlets/crypto-utils/keystore/keyStore.js.map +1 -1
  11. package/dist/packlets/crypto-utils/keystore/model.js +8 -3
  12. package/dist/packlets/crypto-utils/keystore/model.js.map +1 -1
  13. package/dist/packlets/crypto-utils/model.js.map +1 -1
  14. package/dist/packlets/crypto-utils/nodeCryptoProvider.js +66 -0
  15. package/dist/packlets/crypto-utils/nodeCryptoProvider.js.map +1 -1
  16. package/dist/packlets/crypto-utils/spkiHelpers.js +31 -0
  17. package/dist/packlets/crypto-utils/spkiHelpers.js.map +1 -1
  18. package/dist/ts-extras.d.ts +289 -7
  19. package/lib/packlets/crypto-utils/hpkeProvider.d.ts +11 -3
  20. package/lib/packlets/crypto-utils/hpkeProvider.d.ts.map +1 -1
  21. package/lib/packlets/crypto-utils/hpkeProvider.js +34 -12
  22. package/lib/packlets/crypto-utils/hpkeProvider.js.map +1 -1
  23. package/lib/packlets/crypto-utils/index.browser.d.ts +1 -1
  24. package/lib/packlets/crypto-utils/index.browser.d.ts.map +1 -1
  25. package/lib/packlets/crypto-utils/index.browser.js +2 -1
  26. package/lib/packlets/crypto-utils/index.browser.js.map +1 -1
  27. package/lib/packlets/crypto-utils/index.d.ts +1 -1
  28. package/lib/packlets/crypto-utils/index.d.ts.map +1 -1
  29. package/lib/packlets/crypto-utils/index.js +2 -1
  30. package/lib/packlets/crypto-utils/index.js.map +1 -1
  31. package/lib/packlets/crypto-utils/keystore/converters.d.ts.map +1 -1
  32. package/lib/packlets/crypto-utils/keystore/converters.js +2 -3
  33. package/lib/packlets/crypto-utils/keystore/converters.js.map +1 -1
  34. package/lib/packlets/crypto-utils/keystore/keyStore.d.ts +68 -3
  35. package/lib/packlets/crypto-utils/keystore/keyStore.d.ts.map +1 -1
  36. package/lib/packlets/crypto-utils/keystore/keyStore.js +154 -15
  37. package/lib/packlets/crypto-utils/keystore/keyStore.js.map +1 -1
  38. package/lib/packlets/crypto-utils/keystore/model.d.ts +75 -2
  39. package/lib/packlets/crypto-utils/keystore/model.d.ts.map +1 -1
  40. package/lib/packlets/crypto-utils/keystore/model.js +9 -4
  41. package/lib/packlets/crypto-utils/keystore/model.js.map +1 -1
  42. package/lib/packlets/crypto-utils/model.d.ts +89 -0
  43. package/lib/packlets/crypto-utils/model.d.ts.map +1 -1
  44. package/lib/packlets/crypto-utils/model.js.map +1 -1
  45. package/lib/packlets/crypto-utils/nodeCryptoProvider.d.ts +25 -1
  46. package/lib/packlets/crypto-utils/nodeCryptoProvider.d.ts.map +1 -1
  47. package/lib/packlets/crypto-utils/nodeCryptoProvider.js +66 -0
  48. package/lib/packlets/crypto-utils/nodeCryptoProvider.js.map +1 -1
  49. package/lib/packlets/crypto-utils/spkiHelpers.d.ts +15 -0
  50. package/lib/packlets/crypto-utils/spkiHelpers.d.ts.map +1 -1
  51. package/lib/packlets/crypto-utils/spkiHelpers.js +32 -0
  52. package/lib/packlets/crypto-utils/spkiHelpers.js.map +1 -1
  53. package/package.json +7 -7
@@ -1 +1 @@
1
- {"version":3,"file":"spkiHelpers.js","sourceRoot":"","sources":["../../../src/packlets/crypto-utils/spkiHelpers.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright (c) 2026 Erik Fortune\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\nimport { Result, fail, succeed } from '@fgv/ts-utils';\nimport { ICryptoProvider, KeyPairAlgorithm, MultibaseSpkiPublicKey } from './model';\n\n/**\n * Shared shape check for a base64url (no-padding) body: only base64url alphabet\n * characters (`A-Z`, `a-z`, `0-9`, `-`, `_`) and a length that is never `% 4 === 1`\n * (an impossible base64 remainder). Factored so the decoder and the\n * {@link isValidMultibaseSpkiPublicKey} guard agree on exactly one rule.\n */\nfunction isBase64UrlNoPadBody(body: string): boolean {\n return /^[A-Za-z0-9_-]*$/.test(body) && body.length % 4 !== 1;\n}\n\n/**\n * Encodes a `Uint8Array` as a base64url (no-padding) string (RFC 4648 §5).\n *\n * The body uses the base64url alphabet (`+` → `-`, `/` → `_`) and trailing `=`\n * padding is stripped. This is the bare primitive with no multibase prefix; use\n * {@link CryptoUtils.multibaseBase64UrlEncode} when a multibase-`'m'`-prefixed value is required.\n *\n * @param data - The binary data to encode.\n * @returns The base64url-no-pad string.\n * @public\n */\nexport function base64UrlNoPadEncode(data: Uint8Array): string {\n let base64: string;\n if (typeof Buffer !== 'undefined') {\n base64 = Buffer.from(data).toString('base64');\n /* c8 ignore start - browser-only: btoa path not available in Node tests */\n } else {\n let binary = '';\n for (let i = 0; i < data.length; i++) {\n binary += String.fromCharCode(data[i]);\n }\n base64 = btoa(binary);\n }\n /* c8 ignore stop */\n // Convert to base64url: + → -, / → _, strip = padding\n return base64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\n/**\n * Decodes a base64url (no-padding) string (RFC 4648 §5) back to a `Uint8Array`.\n *\n * This is the bare primitive with no multibase prefix; use\n * {@link CryptoUtils.multibaseBase64UrlDecode} to decode a multibase-`'m'`-prefixed value.\n *\n * @param encoded - The base64url-no-pad body to decode.\n * @returns `Success` with the decoded bytes, or `Failure` with error context.\n * @public\n */\nexport function base64UrlNoPadDecode(encoded: string): Result<Uint8Array> {\n if (!isBase64UrlNoPadBody(encoded)) {\n return fail(`base64UrlNoPadDecode: malformed base64url body`);\n }\n // Convert base64url back to standard base64 and restore padding\n const base64 = encoded.replace(/-/g, '+').replace(/_/g, '/');\n const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4);\n try {\n let bytes: Uint8Array;\n if (typeof Buffer !== 'undefined') {\n bytes = new Uint8Array(Buffer.from(padded, 'base64'));\n /* c8 ignore start - browser-only: atob path not available in Node tests */\n } else {\n const binary = atob(padded);\n bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n }\n /* c8 ignore stop */\n return succeed(bytes);\n /* c8 ignore next 3 - defensive: shape check above prevents invalid chars from reaching here */\n } catch {\n return fail(`base64UrlNoPadDecode: malformed base64url body`);\n }\n}\n\n/**\n * The structural *shape* of a {@link CryptoUtils.MultibaseSpkiPublicKey}: the\n * multibase `'m'` prefix followed by a non-empty base64url-no-pad body\n * (`A-Z`, `a-z`, `0-9`, `-`, `_`).\n *\n * This is a shape prefilter, not full validation: it does not decode the DER\n * SPKI or verify the key material or algorithm (that happens in\n * {@link CryptoUtils.importPublicKeyFromMultibaseSpki}). The authoritative guard\n * is {@link CryptoUtils.isValidMultibaseSpkiPublicKey}, which enforces this\n * pattern **and** additionally rejects a body whose length is an impossible\n * base64 remainder. Exposed for callers that need the pattern directly (e.g. a\n * JSON-schema `pattern` field); prefer the guard/converter for validation.\n *\n * @public\n */\nexport const MultibaseSpkiPublicKeyRegExp: RegExp = /^m[A-Za-z0-9_-]+$/;\n\n/**\n * Type guard for {@link CryptoUtils.MultibaseSpkiPublicKey}: a string matching\n * {@link CryptoUtils.MultibaseSpkiPublicKeyRegExp} (multibase `'m'` prefix + a\n * non-empty base64url-no-pad body) whose body also satisfies the base64url-no-pad\n * length rule shared with {@link CryptoUtils.base64UrlNoPadDecode}. This is a\n * structural *shape* check, not full validation — it does not decode the DER SPKI\n * or verify the key material/algorithm (a malformed-but-well-shaped string fails\n * later, with clear context, in {@link CryptoUtils.importPublicKeyFromMultibaseSpki}).\n *\n * @param value - The value to test.\n * @returns `true` if `value` is a well-formed multibase SPKI public key string.\n * @public\n */\nexport function isValidMultibaseSpkiPublicKey(value: unknown): value is MultibaseSpkiPublicKey {\n return (\n typeof value === 'string' &&\n MultibaseSpkiPublicKeyRegExp.test(value) &&\n isBase64UrlNoPadBody(value.slice(1))\n );\n}\n\n/**\n * Encodes a `Uint8Array` as a multibase base64url (no-padding) string.\n *\n * The multibase prefix `'m'` identifies the encoding as RFC 4648 base64url\n * without padding. The body uses base64url alphabet: `+` → `-`, `/` → `_`,\n * and trailing `=` padding is stripped.\n *\n * @param data - The binary data to encode.\n * @returns A multibase-prefixed base64url string (`'m' + base64url-no-pad`).\n * @public\n */\nexport function multibaseBase64UrlEncode(data: Uint8Array): string {\n return 'm' + base64UrlNoPadEncode(data);\n}\n\n/**\n * Decodes a multibase base64url (no-padding) string back to a `Uint8Array`.\n *\n * Validates that the first character is `'m'` (the multibase prefix for\n * RFC 4648 base64url without padding), then decodes the remaining body.\n *\n * @param encoded - A multibase-prefixed base64url string.\n * @returns `Success` with the decoded bytes, or `Failure` with error context.\n * @public\n */\nexport function multibaseBase64UrlDecode(encoded: string): Result<Uint8Array> {\n if (!encoded.startsWith('m')) {\n return fail(\n `multibaseBase64UrlDecode: invalid multibase prefix '${\n encoded[0] ?? '(empty)'\n }' — expected 'm' (base64url)`\n );\n }\n // Intentionally pin the exact original message (the delegate has a single failure\n // mode) to keep this established public function byte-for-byte behavior-preserving\n // after the extract-and-delegate refactor — do not \"fix\" this into composing the\n // delegate's message without updating the delegation-equivalence tests.\n return base64UrlNoPadDecode(encoded.slice(1)).withErrorFormat(\n () => `multibaseBase64UrlDecode: malformed base64url body`\n );\n}\n\n/**\n * Exports a public `CryptoKey` as a multibase base64url-encoded SPKI blob.\n *\n * The SPKI (SubjectPublicKeyInfo) format is the standard DER-encoded structure\n * for public keys defined in RFC 5280, RFC 5480, and RFC 8410. It is\n * algorithm-agnostic and suitable for storage and transmission.\n *\n * @param key - The `CryptoKey` to export. Must have `key.type === 'public'`.\n * @param provider - The {@link CryptoUtils.ICryptoProvider} to use for the export operation.\n * @returns `Success` with the multibase SPKI string, or `Failure` with error context.\n * @public\n */\nexport async function exportPublicKeyAsMultibaseSpki(\n key: CryptoKey,\n provider: ICryptoProvider\n): Promise<Result<MultibaseSpkiPublicKey>> {\n return (await provider.exportPublicKeySpki(key))\n .withErrorFormat((e) => `exportPublicKeyAsMultibaseSpki: ${e}`)\n .onSuccess((buf) =>\n // The output is a freshly-built valid multibase SPKI string (`'m'` prefix +\n // base64url-no-pad body), so brand it at the construction site — no re-validation needed.\n succeed(multibaseBase64UrlEncode(buf) as MultibaseSpkiPublicKey)\n );\n}\n\n/**\n * Imports a public key from a multibase base64url-encoded SPKI blob.\n *\n * Decodes the multibase prefix, decodes the base64url body, then uses\n * the provider to import the key with the algorithm parameters from\n * {@link CryptoUtils.keyPairAlgorithmParams}.\n *\n * Accepts a plain `string` (not only a branded {@link CryptoUtils.MultibaseSpkiPublicKey}),\n * so callers holding an unbranded value read from storage or the wire can import\n * it directly; a malformed value fails with error context rather than throwing.\n *\n * @param encoded - A multibase SPKI string produced by {@link CryptoUtils.exportPublicKeyAsMultibaseSpki}.\n * @param algorithm - The {@link CryptoUtils.KeyPairAlgorithm} the key was generated for.\n * @param provider - The {@link CryptoUtils.ICryptoProvider} to use for the import operation.\n * @returns `Success` with the imported public `CryptoKey`, or `Failure` with error context.\n * @public\n */\nexport async function importPublicKeyFromMultibaseSpki(\n encoded: string,\n algorithm: KeyPairAlgorithm,\n provider: ICryptoProvider\n): Promise<Result<CryptoKey>> {\n const decodeResult = multibaseBase64UrlDecode(encoded);\n if (decodeResult.isFailure()) {\n return fail(`importPublicKeyFromMultibaseSpki: ${decodeResult.message}`);\n }\n return (await provider.importPublicKeySpki(decodeResult.value, algorithm)).withErrorFormat(\n (e) => `importPublicKeyFromMultibaseSpki: ${e}`\n );\n}\n"]}
1
+ {"version":3,"file":"spkiHelpers.js","sourceRoot":"","sources":["../../../src/packlets/crypto-utils/spkiHelpers.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright (c) 2026 Erik Fortune\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\nimport { Result, fail, succeed } from '@fgv/ts-utils';\nimport { ICryptoProvider, KeyPairAlgorithm, MultibaseSpkiPublicKey } from './model';\n\n/**\n * Shared shape check for a base64url (no-padding) body: only base64url alphabet\n * characters (`A-Z`, `a-z`, `0-9`, `-`, `_`) and a length that is never `% 4 === 1`\n * (an impossible base64 remainder). Factored so the decoder and the\n * {@link isValidMultibaseSpkiPublicKey} guard agree on exactly one rule.\n */\nfunction isBase64UrlNoPadBody(body: string): boolean {\n return /^[A-Za-z0-9_-]*$/.test(body) && body.length % 4 !== 1;\n}\n\n/**\n * Encodes a `Uint8Array` as a base64url (no-padding) string (RFC 4648 §5).\n *\n * The body uses the base64url alphabet (`+` → `-`, `/` → `_`) and trailing `=`\n * padding is stripped. This is the bare primitive with no multibase prefix; use\n * {@link CryptoUtils.multibaseBase64UrlEncode} when a multibase-`'m'`-prefixed value is required.\n *\n * @param data - The binary data to encode.\n * @returns The base64url-no-pad string.\n * @public\n */\nexport function base64UrlNoPadEncode(data: Uint8Array): string {\n let base64: string;\n if (typeof Buffer !== 'undefined') {\n base64 = Buffer.from(data).toString('base64');\n /* c8 ignore start - browser-only: btoa path not available in Node tests */\n } else {\n let binary = '';\n for (let i = 0; i < data.length; i++) {\n binary += String.fromCharCode(data[i]);\n }\n base64 = btoa(binary);\n }\n /* c8 ignore stop */\n // Convert to base64url: + → -, / → _, strip = padding\n return base64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\n/**\n * Decodes a base64url (no-padding) string (RFC 4648 §5) back to a `Uint8Array`.\n *\n * This is the bare primitive with no multibase prefix; use\n * {@link CryptoUtils.multibaseBase64UrlDecode} to decode a multibase-`'m'`-prefixed value.\n *\n * @param encoded - The base64url-no-pad body to decode.\n * @returns `Success` with the decoded bytes, or `Failure` with error context.\n * @public\n */\nexport function base64UrlNoPadDecode(encoded: string): Result<Uint8Array> {\n if (!isBase64UrlNoPadBody(encoded)) {\n return fail(`base64UrlNoPadDecode: malformed base64url body`);\n }\n // Convert base64url back to standard base64 and restore padding\n const base64 = encoded.replace(/-/g, '+').replace(/_/g, '/');\n const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4);\n try {\n let bytes: Uint8Array;\n if (typeof Buffer !== 'undefined') {\n bytes = new Uint8Array(Buffer.from(padded, 'base64'));\n /* c8 ignore start - browser-only: atob path not available in Node tests */\n } else {\n const binary = atob(padded);\n bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n }\n /* c8 ignore stop */\n return succeed(bytes);\n /* c8 ignore next 3 - defensive: shape check above prevents invalid chars from reaching here */\n } catch {\n return fail(`base64UrlNoPadDecode: malformed base64url body`);\n }\n}\n\n/**\n * The structural *shape* of a {@link CryptoUtils.MultibaseSpkiPublicKey}: the\n * multibase `'m'` prefix followed by a non-empty base64url-no-pad body\n * (`A-Z`, `a-z`, `0-9`, `-`, `_`).\n *\n * This is a shape prefilter, not full validation: it does not decode the DER\n * SPKI or verify the key material or algorithm (that happens in\n * {@link CryptoUtils.importPublicKeyFromMultibaseSpki}). The authoritative guard\n * is {@link CryptoUtils.isValidMultibaseSpkiPublicKey}, which enforces this\n * pattern **and** additionally rejects a body whose length is an impossible\n * base64 remainder. Exposed for callers that need the pattern directly (e.g. a\n * JSON-schema `pattern` field); prefer the guard/converter for validation.\n *\n * @public\n */\nexport const MultibaseSpkiPublicKeyRegExp: RegExp = /^m[A-Za-z0-9_-]+$/;\n\n/**\n * Type guard for {@link CryptoUtils.MultibaseSpkiPublicKey}: a string matching\n * {@link CryptoUtils.MultibaseSpkiPublicKeyRegExp} (multibase `'m'` prefix + a\n * non-empty base64url-no-pad body) whose body also satisfies the base64url-no-pad\n * length rule shared with {@link CryptoUtils.base64UrlNoPadDecode}. This is a\n * structural *shape* check, not full validation — it does not decode the DER SPKI\n * or verify the key material/algorithm (a malformed-but-well-shaped string fails\n * later, with clear context, in {@link CryptoUtils.importPublicKeyFromMultibaseSpki}).\n *\n * @param value - The value to test.\n * @returns `true` if `value` is a well-formed multibase SPKI public key string.\n * @public\n */\nexport function isValidMultibaseSpkiPublicKey(value: unknown): value is MultibaseSpkiPublicKey {\n return (\n typeof value === 'string' &&\n MultibaseSpkiPublicKeyRegExp.test(value) &&\n isBase64UrlNoPadBody(value.slice(1))\n );\n}\n\n/**\n * Encodes a `Uint8Array` as a multibase base64url (no-padding) string.\n *\n * The multibase prefix `'m'` identifies the encoding as RFC 4648 base64url\n * without padding. The body uses base64url alphabet: `+` → `-`, `/` → `_`,\n * and trailing `=` padding is stripped.\n *\n * @param data - The binary data to encode.\n * @returns A multibase-prefixed base64url string (`'m' + base64url-no-pad`).\n * @public\n */\nexport function multibaseBase64UrlEncode(data: Uint8Array): string {\n return 'm' + base64UrlNoPadEncode(data);\n}\n\n/**\n * Decodes a multibase base64url (no-padding) string back to a `Uint8Array`.\n *\n * Validates that the first character is `'m'` (the multibase prefix for\n * RFC 4648 base64url without padding), then decodes the remaining body.\n *\n * @param encoded - A multibase-prefixed base64url string.\n * @returns `Success` with the decoded bytes, or `Failure` with error context.\n * @public\n */\nexport function multibaseBase64UrlDecode(encoded: string): Result<Uint8Array> {\n if (!encoded.startsWith('m')) {\n return fail(\n `multibaseBase64UrlDecode: invalid multibase prefix '${\n encoded[0] ?? '(empty)'\n }' — expected 'm' (base64url)`\n );\n }\n // Intentionally pin the exact original message (the delegate has a single failure\n // mode) to keep this established public function byte-for-byte behavior-preserving\n // after the extract-and-delegate refactor — do not \"fix\" this into composing the\n // delegate's message without updating the delegation-equivalence tests.\n return base64UrlNoPadDecode(encoded.slice(1)).withErrorFormat(\n () => `multibaseBase64UrlDecode: malformed base64url body`\n );\n}\n\n/**\n * Exports a public `CryptoKey` as a multibase base64url-encoded SPKI blob.\n *\n * The SPKI (SubjectPublicKeyInfo) format is the standard DER-encoded structure\n * for public keys defined in RFC 5280, RFC 5480, and RFC 8410. It is\n * algorithm-agnostic and suitable for storage and transmission.\n *\n * @param key - The `CryptoKey` to export. Must have `key.type === 'public'`.\n * @param provider - The {@link CryptoUtils.ICryptoProvider} to use for the export operation.\n * @returns `Success` with the multibase SPKI string, or `Failure` with error context.\n * @public\n */\nexport async function exportPublicKeyAsMultibaseSpki(\n key: CryptoKey,\n provider: ICryptoProvider\n): Promise<Result<MultibaseSpkiPublicKey>> {\n return (await provider.exportPublicKeySpki(key))\n .withErrorFormat((e) => `exportPublicKeyAsMultibaseSpki: ${e}`)\n .onSuccess((buf) =>\n // The output is a freshly-built valid multibase SPKI string (`'m'` prefix +\n // base64url-no-pad body), so brand it at the construction site — no re-validation needed.\n succeed(multibaseBase64UrlEncode(buf) as MultibaseSpkiPublicKey)\n );\n}\n\n/**\n * Imports a public key from a multibase base64url-encoded SPKI blob.\n *\n * Decodes the multibase prefix, decodes the base64url body, then uses\n * the provider to import the key with the algorithm parameters from\n * {@link CryptoUtils.keyPairAlgorithmParams}.\n *\n * Accepts a plain `string` (not only a branded {@link CryptoUtils.MultibaseSpkiPublicKey}),\n * so callers holding an unbranded value read from storage or the wire can import\n * it directly; a malformed value fails with error context rather than throwing.\n *\n * @param encoded - A multibase SPKI string produced by {@link CryptoUtils.exportPublicKeyAsMultibaseSpki}.\n * @param algorithm - The {@link CryptoUtils.KeyPairAlgorithm} the key was generated for.\n * @param provider - The {@link CryptoUtils.ICryptoProvider} to use for the import operation.\n * @returns `Success` with the imported public `CryptoKey`, or `Failure` with error context.\n * @public\n */\nexport async function importPublicKeyFromMultibaseSpki(\n encoded: string,\n algorithm: KeyPairAlgorithm,\n provider: ICryptoProvider\n): Promise<Result<CryptoKey>> {\n const decodeResult = multibaseBase64UrlDecode(encoded);\n if (decodeResult.isFailure()) {\n return fail(`importPublicKeyFromMultibaseSpki: ${decodeResult.message}`);\n }\n return (await provider.importPublicKeySpki(decodeResult.value, algorithm)).withErrorFormat(\n (e) => `importPublicKeyFromMultibaseSpki: ${e}`\n );\n}\n\n// DER prefix for an X25519 SubjectPublicKeyInfo: SEQUENCE { SEQUENCE { OID id-X25519 }, BIT STRING }.\n// SEQUENCE(42) / SEQUENCE(5) / OID 1.3.101.110 (id-X25519) / BIT STRING(33, 0 unused bits).\nconst _X25519_SPKI_PREFIX: ReadonlyArray<number> = [\n 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x6e, 0x03, 0x21, 0x00\n];\nconst _X25519_SPKI_LENGTH: number = _X25519_SPKI_PREFIX.length + 32;\n\n/**\n * Strips the fixed DER prefix from an X25519 SubjectPublicKeyInfo blob, returning the raw\n * 32-byte public key.\n *\n * An X25519 SPKI is always exactly 44 bytes: a fixed 12-byte prefix (SEQUENCE / AlgorithmIdentifier\n * with OID 1.3.101.110 / BIT STRING) followed by the 32-byte raw key. Use this to convert a\n * SPKI-held recipient public key into the raw form accepted by {@link HpkeProvider.openBase}'s\n * `recipientPublicKey` parameter.\n *\n * @param spki - The DER-encoded X25519 SubjectPublicKeyInfo bytes.\n * @returns `Success` with the raw 32-byte public key, or `Failure` if `spki` is not a\n * well-formed 44-byte X25519 SPKI blob.\n * @public\n */\nexport function spkiToRawX25519(spki: Uint8Array): Result<Uint8Array> {\n if (spki.length !== _X25519_SPKI_LENGTH) {\n return fail(`spkiToRawX25519: expected ${_X25519_SPKI_LENGTH} bytes, got ${spki.length}`);\n }\n for (let i = 0; i < _X25519_SPKI_PREFIX.length; i++) {\n if (spki[i] !== _X25519_SPKI_PREFIX[i]) {\n return fail(`spkiToRawX25519: byte ${i} does not match the expected X25519 SPKI prefix`);\n }\n }\n return succeed(spki.slice(_X25519_SPKI_PREFIX.length));\n}\n"]}
@@ -381,6 +381,12 @@ declare const allKeyPairAlgorithms: ReadonlyArray<KeyPairAlgorithm>;
381
381
  */
382
382
  declare const allKeyStoreAsymmetricSecretTypes: ReadonlyArray<KeyStoreAsymmetricSecretType>;
383
383
 
384
+ /**
385
+ * All recognized key store format versions (readable by the current library).
386
+ * @public
387
+ */
388
+ declare const allKeyStoreFormats: ReadonlyArray<KeyStoreFormat>;
389
+
384
390
  /**
385
391
  * All valid key store secret types.
386
392
  * @public
@@ -730,6 +736,7 @@ declare namespace CryptoUtils {
730
736
  MultibaseSpkiPublicKeyRegExp,
731
737
  multibaseBase64UrlDecode,
732
738
  multibaseBase64UrlEncode,
739
+ spkiToRawX25519,
733
740
  HpkeProvider,
734
741
  IHpkeSealResult,
735
742
  isEncryptedFile,
@@ -738,6 +745,7 @@ declare namespace CryptoUtils {
738
745
  EncryptedFileFormat,
739
746
  INamedSecret,
740
747
  IEncryptionResult,
748
+ IEncryptBytesResult,
741
749
  KeyPairAlgorithm,
742
750
  IWrapBytesOptions,
743
751
  IWrappedBytes,
@@ -1435,15 +1443,23 @@ declare class HpkeProvider {
1435
1443
  *
1436
1444
  * @param recipientPrivateKey - Recipient's X25519 private `CryptoKey`
1437
1445
  * (`algorithm.name === 'X25519'`, `type === 'private'`, `usages` includes `'deriveBits'`).
1438
- * **Must be extractable** (`extractable: true`) the recipient's public key bytes
1439
- * are recovered from the JWK `x` field during Decap.
1446
+ * **Must be extractable** (`extractable: true`) only when `recipientPublicKey` is not
1447
+ * supplied — the recipient's public key bytes are then recovered from the JWK `x` field
1448
+ * during Decap. When `recipientPublicKey` is supplied, `recipientPrivateKey` may be
1449
+ * non-extractable.
1440
1450
  * @param info - Context-binding bytes. Must exactly match `info` from `sealBase`.
1441
1451
  * @param aad - Must exactly match `aad` from `sealBase`.
1442
1452
  * @param enc - The encapsulated key from `sealBase` — exactly 32 bytes.
1443
1453
  * @param ciphertext - The ciphertext from `sealBase` — `plaintext.length + 16` bytes.
1454
+ * @param recipientPublicKey - Optional raw 32-byte X25519 public key matching
1455
+ * `recipientPrivateKey` (`pkRm` in RFC 9180 §4.1). Public material — supplying it lets
1456
+ * Decap build `kem_context` without exporting `recipientPrivateKey` to JWK, so
1457
+ * `recipientPrivateKey` no longer needs to be extractable. A mismatched value only breaks
1458
+ * the caller's own decryption (AEAD authentication fails) — it cannot be used to attack
1459
+ * another party's ciphertext.
1444
1460
  * @returns `Success` with decrypted plaintext bytes, or `Failure` with error context.
1445
1461
  */
1446
- openBase(recipientPrivateKey: CryptoKey, info: Uint8Array, aad: Uint8Array, enc: Uint8Array, ciphertext: Uint8Array): Promise<Result<Uint8Array>>;
1462
+ openBase(recipientPrivateKey: CryptoKey, info: Uint8Array, aad: Uint8Array, enc: Uint8Array, ciphertext: Uint8Array, recipientPublicKey?: Uint8Array): Promise<Result<Uint8Array>>;
1447
1463
  /**
1448
1464
  * HKDF-SHA256 key derivation (RFC 5869). Extract-then-Expand using SHA-256.
1449
1465
  *
@@ -1508,6 +1524,28 @@ declare interface IAddKeyPairOptions {
1508
1524
  * downgrading.
1509
1525
  */
1510
1526
  readonly extractable?: boolean;
1527
+ /**
1528
+ * Opt in to private-key escrow. When `true`, an encrypted copy of the
1529
+ * private key (as a JWK) is carried inside the vault entry
1530
+ * (`escrowedPrivateKeyJwk`), enabling cross-device recovery from the vault
1531
+ * file plus the master password alone via
1532
+ * `getKeyPair(name, { rehydrate: true })`.
1533
+ *
1534
+ * Orthogonal to `extractable`: the escrow copy is always captured (the
1535
+ * transient keypair is generated extractable so it can be exported to JWK),
1536
+ * while the LIVE stored key's extractability still follows the normal rule
1537
+ * (`extractable` override, else the backend default). So escrow does not
1538
+ * change how extractable the day-to-day key is — only whether a recovery
1539
+ * copy is retained in the vault.
1540
+ *
1541
+ * @defaultValue false — no escrow copy is written (today's behavior).
1542
+ *
1543
+ * SECURITY: escrow makes the master password the sole gate protecting a
1544
+ * private key that would otherwise be unrecoverable from the vault. Use a
1545
+ * strong KDF (Argon2id-derived or high-iteration PBKDF2) for escrow-bearing
1546
+ * stores. See the {@link CryptoUtils.KeyStore.KeyStore} class docs.
1547
+ */
1548
+ readonly escrow?: boolean;
1511
1549
  }
1512
1550
 
1513
1551
  /**
@@ -2681,6 +2719,68 @@ declare interface ICryptoProvider {
2681
2719
  * @returns Success with decrypted UTF-8 string, or Failure with error
2682
2720
  */
2683
2721
  decrypt(encryptedData: Uint8Array, key: Uint8Array, iv: Uint8Array, authTag: Uint8Array): Promise<Result<string>>;
2722
+ /**
2723
+ * Encrypts raw bytes using AES-256-GCM with a **caller-supplied nonce** and
2724
+ * optional additional authenticated data (AAD).
2725
+ *
2726
+ * This is the raw-byte sibling of {@link CryptoUtils.ICryptoProvider.encrypt | encrypt}.
2727
+ * Unlike `encrypt`, it takes and returns `Uint8Array` (no UTF-8 coding), the
2728
+ * caller owns the nonce (rather than the provider generating one), and it
2729
+ * binds optional `aad` into the GCM authentication. Use it when you need to
2730
+ * bind context (e.g. an actor id, key version, or row kind) into the
2731
+ * authentication so a wrapped secret cannot be replayed across
2732
+ * users/versions/kinds, or when you manage nonces yourself.
2733
+ *
2734
+ * @remarks
2735
+ * **⚠️ NONCE UNIQUENESS IS THE CALLER'S RESPONSIBILITY AND IS CRITICAL.**
2736
+ * Because the caller supplies the nonce, this primitive cannot guarantee
2737
+ * uniqueness. Reusing a `(key, nonce)` pair for two different messages is
2738
+ * **catastrophic** for AES-GCM: it breaks confidentiality (the XOR of the two
2739
+ * plaintexts leaks) AND authentication (the GCM authentication key can be
2740
+ * recovered, letting an attacker forge tags for arbitrary messages under that
2741
+ * key). The caller MUST use a unique nonce for every message encrypted under a
2742
+ * given key — draw it from {@link CryptoUtils.ICryptoProvider.generateRandomBytes | generateRandomBytes(12)}
2743
+ * (12 random bytes has negligible collision probability well within a single
2744
+ * key's message budget) or from a strictly-increasing counter. Never hardcode
2745
+ * a nonce and never reuse one.
2746
+ *
2747
+ * @param key - 32-byte AES-256 key. Wrong lengths fail with error context.
2748
+ * @param nonce - 12-byte (96-bit) GCM nonce. MUST be unique per message under
2749
+ * `key` (see the nonce-uniqueness warning above). Wrong lengths fail with
2750
+ * error context.
2751
+ * @param plaintext - The bytes to encrypt. Empty plaintext is permitted and
2752
+ * round-trips (GCM produces a valid tag over zero-length plaintext).
2753
+ * @param aad - Optional additional authenticated data bound into the GCM tag
2754
+ * but NOT encrypted. If provided at encrypt time, the identical bytes must be
2755
+ * supplied to `decryptBytes` or decryption fails authentication. Absent means
2756
+ * no AAD.
2757
+ * @returns `Success` with the {@link CryptoUtils.IEncryptBytesResult | ciphertext and 16-byte auth tag},
2758
+ * or `Failure` with error context.
2759
+ */
2760
+ encryptBytes(key: Uint8Array, nonce: Uint8Array, plaintext: Uint8Array, aad?: Uint8Array): Promise<Result<IEncryptBytesResult>>;
2761
+ /**
2762
+ * Decrypts raw bytes produced by
2763
+ * {@link CryptoUtils.ICryptoProvider.encryptBytes | encryptBytes} using
2764
+ * AES-256-GCM. The inverse of `encryptBytes`: the `ciphertext` and `authTag`
2765
+ * from an `encryptBytes` result, together with the same `key`, `nonce`, and
2766
+ * `aad`, recover the original plaintext.
2767
+ *
2768
+ * Fails (never throws) on any authentication failure: a tampered ciphertext
2769
+ * or tag, the wrong key or nonce, or an `aad` that differs from the one used
2770
+ * at encrypt time. AES-GCM authentication is fail-closed — a mismatched `aad`
2771
+ * fails exactly as a tampered ciphertext does.
2772
+ *
2773
+ * @param key - 32-byte AES-256 key (the same key used to encrypt).
2774
+ * @param nonce - 12-byte (96-bit) GCM nonce (the same nonce used to encrypt).
2775
+ * @param ciphertext - The ciphertext from the `encryptBytes` result.
2776
+ * @param authTag - The 16-byte (128-bit) GCM authentication tag from the
2777
+ * `encryptBytes` result.
2778
+ * @param aad - The identical additional authenticated data supplied at encrypt
2779
+ * time (or absent if none was supplied). A mismatch fails authentication.
2780
+ * @returns `Success` with the decrypted plaintext bytes, or `Failure` with
2781
+ * error context (including all authentication failures).
2782
+ */
2783
+ decryptBytes(key: Uint8Array, nonce: Uint8Array, ciphertext: Uint8Array, authTag: Uint8Array, aad?: Uint8Array): Promise<Result<Uint8Array>>;
2684
2784
  /**
2685
2785
  * Generates a random 32-byte key suitable for AES-256.
2686
2786
  * @returns Success with generated key, or Failure with error
@@ -2905,6 +3005,34 @@ declare interface IDirectEncryptionProviderParams {
2905
3005
  readonly boundSecretName?: string;
2906
3006
  }
2907
3007
 
3008
+ /**
3009
+ * Result of a raw-byte AES-256-GCM encryption via
3010
+ * {@link CryptoUtils.ICryptoProvider.encryptBytes | encryptBytes}. The
3011
+ * authentication tag is returned separately from the ciphertext, mirroring the
3012
+ * separated-tag convention of {@link CryptoUtils.IEncryptionResult}. The exact
3013
+ * byte layout is:
3014
+ * - `ciphertext`: the AES-GCM ciphertext, byte-for-byte the same length as the
3015
+ * input plaintext (GCM is a stream cipher — no padding). Empty plaintext
3016
+ * yields an empty `ciphertext`.
3017
+ * - `authTag`: the 16-byte (128-bit) GCM authentication tag, computed over the
3018
+ * ciphertext, the nonce, and any `aad`.
3019
+ *
3020
+ * The caller persists both fields alongside the (caller-owned) nonce and feeds
3021
+ * them back to {@link CryptoUtils.ICryptoProvider.decryptBytes | decryptBytes}.
3022
+ * @public
3023
+ */
3024
+ declare interface IEncryptBytesResult {
3025
+ /**
3026
+ * The AES-256-GCM ciphertext. Same length as the input plaintext (no tag
3027
+ * appended — the tag is carried separately in the `authTag` field).
3028
+ */
3029
+ readonly ciphertext: Uint8Array;
3030
+ /**
3031
+ * The 16-byte (128-bit) GCM authentication tag.
3032
+ */
3033
+ readonly authTag: Uint8Array;
3034
+ }
3035
+
2908
3036
  /**
2909
3037
  * Generic encrypted file format.
2910
3038
  * This is the JSON structure stored in encrypted files.
@@ -3278,6 +3406,26 @@ declare interface IGenerateJsonCompletionResult<T> {
3278
3406
  readonly response: IAiCompletionResponse;
3279
3407
  }
3280
3408
 
3409
+ /**
3410
+ * Options for retrieving an asymmetric keypair via {@link CryptoUtils.KeyStore.KeyStore.getKeyPair}.
3411
+ * @public
3412
+ */
3413
+ declare interface IGetKeyPairOptions {
3414
+ /**
3415
+ * Opt in to escrow rehydration. When `true` AND no private-key blob exists
3416
+ * under the entry's storage `id` AND the entry carries an
3417
+ * `escrowedPrivateKeyJwk`, the escrowed JWK is imported (non-extractable
3418
+ * where the backend supports it) and stored under the entry's `id` before
3419
+ * being returned — filling a gap on a fresh device from the recovered vault.
3420
+ *
3421
+ * Fill-a-gap only: an existing storage blob is never overwritten. When a
3422
+ * blob is present it is loaded as usual and the escrow copy is ignored.
3423
+ *
3424
+ * @defaultValue false — today's behavior: load from storage or fail.
3425
+ */
3426
+ readonly rehydrate?: boolean;
3427
+ }
3428
+
3281
3429
  /**
3282
3430
  * Provider-specific config for gpt-image-1.
3283
3431
  * @public
@@ -3453,6 +3601,21 @@ declare interface IKeyStoreAsymmetricEntry {
3453
3601
  * The public key as a JSON Web Key.
3454
3602
  */
3455
3603
  readonly publicKeyJwk: JsonWebKey;
3604
+ /**
3605
+ * Optional escrowed copy of the private key, as a JSON Web Key. Present only
3606
+ * when the entry was created with `addKeyPair(name, { escrow: true })`.
3607
+ *
3608
+ * This is an opt-in cross-device recovery affordance: the private key is
3609
+ * carried inside the vault's AES-GCM ciphertext (same custody class as
3610
+ * `publicKeyJwk`), so a recovered vault plus the master password can
3611
+ * reconstitute the signing/decryption identity on a fresh device via
3612
+ * `getKeyPair(name, { rehydrate: true })`.
3613
+ *
3614
+ * SECURITY: when present, the master password becomes the sole gate
3615
+ * protecting this private key. Use a strong KDF for escrow-bearing stores.
3616
+ * See the {@link CryptoUtils.KeyStore.KeyStore} class docs.
3617
+ */
3618
+ readonly escrowedPrivateKeyJwk?: JsonWebKey;
3456
3619
  /**
3457
3620
  * Optional description for this entry.
3458
3621
  */
@@ -3491,6 +3654,12 @@ declare interface IKeyStoreAsymmetricEntryJson {
3491
3654
  * The public key as a JSON Web Key.
3492
3655
  */
3493
3656
  readonly publicKeyJwk: JsonWebKey;
3657
+ /**
3658
+ * Optional escrowed copy of the private key, as a JSON Web Key. Present only
3659
+ * on `'keystore-v2'` vaults whose entry was created with escrow enabled. A
3660
+ * v1 vault omits the field; a v2 reader treats its absence as "no escrow".
3661
+ */
3662
+ readonly escrowedPrivateKeyJwk?: JsonWebKey;
3494
3663
  /**
3495
3664
  * Optional description.
3496
3665
  */
@@ -4604,6 +4773,7 @@ declare namespace KeyStore {
4604
4773
  allKeyPairAlgorithms,
4605
4774
  KeyPairAlgorithm,
4606
4775
  KeyStoreFormat,
4776
+ allKeyStoreFormats,
4607
4777
  KEYSTORE_FORMAT,
4608
4778
  DEFAULT_KEYSTORE_ITERATIONS,
4609
4779
  MIN_SALT_LENGTH,
@@ -4635,6 +4805,7 @@ declare namespace KeyStore {
4635
4805
  IAddSecretFromPasswordResult,
4636
4806
  IAddSecretFromPasswordArgon2idOptions,
4637
4807
  IAddKeyPairOptions,
4808
+ IGetKeyPairOptions,
4638
4809
  IAddKeyPairResult,
4639
4810
  IRemoveSecretResult,
4640
4811
  IPrivateKeyStorage
@@ -4670,6 +4841,20 @@ declare namespace KeyStore {
4670
4841
  * const encryptionConfig = keystore2.getEncryptionConfig().orThrow();
4671
4842
  * ```
4672
4843
  *
4844
+ * @remarks
4845
+ * SECURITY — private-key escrow. By default an asymmetric keypair's private
4846
+ * key lives only in the per-device {@link CryptoUtils.KeyStore.IPrivateKeyStorage}
4847
+ * backend; the vault carries only the public JWK, so a vault recovered on a new
4848
+ * device reconstitutes with no private keys (a lost device is a lost identity).
4849
+ * `addKeyPair(name, { escrow: true })` opts into carrying an encrypted private-key
4850
+ * copy inside the vault ciphertext, so the vault file plus the master password can
4851
+ * recover the identity on a fresh device via `getKeyPair(name, { rehydrate: true })`.
4852
+ *
4853
+ * When escrow is used, **the master password becomes the sole gate** protecting a
4854
+ * private signing/decryption key that was previously unrecoverable from the vault.
4855
+ * Configure escrow-bearing stores with a strong KDF — an Argon2id-derived master
4856
+ * key or a high-iteration PBKDF2 count — and treat the vault file accordingly.
4857
+ *
4673
4858
  * @public
4674
4859
  */
4675
4860
  declare class KeyStore_2 implements IEncryptionProvider {
@@ -4776,6 +4961,12 @@ declare class KeyStore_2 implements IEncryptionProvider {
4776
4961
  * Gets a secret by name. Returns the {@link CryptoUtils.KeyStore.IKeyStoreEntry | discriminated union}
4777
4962
  * — callers must check `entry.type` before accessing `key`/`id` since asymmetric
4778
4963
  * entries carry no raw key material.
4964
+ *
4965
+ * SECURITY: for an escrow-enabled asymmetric-keypair entry the returned object
4966
+ * carries `escrowedPrivateKeyJwk` in cleartext (same custody class as
4967
+ * `publicKeyJwk`, gated by the same unlock) — do not log or serialize a
4968
+ * `getSecret()` result casually.
4969
+ *
4779
4970
  * @param name - Name of the secret
4780
4971
  * @returns Success with secret entry, Failure if not found or locked
4781
4972
  * @public
@@ -4962,12 +5153,23 @@ declare class KeyStore_2 implements IEncryptionProvider {
4962
5153
  * the keystore never caches private `CryptoKey` references between calls.
4963
5154
  * The public key is re-imported from the vault's JWK so callers always
4964
5155
  * receive a `CryptoKey` rather than the JWK form.
5156
+ *
5157
+ * With `options.rehydrate: true`, if the storage backend holds no blob for
5158
+ * the entry's `id` and the entry carries an `escrowedPrivateKeyJwk` (see
5159
+ * `addKeyPair(name, { escrow: true })`), the escrowed JWK is imported and
5160
+ * stored under the entry's `id` before being returned — recovering the
5161
+ * private key on a fresh device from the vault plus master password. This is
5162
+ * fill-a-gap only: an existing storage blob is never overwritten.
5163
+ *
4965
5164
  * @param name - Name of the entry
5165
+ * @param options - Optional {@link CryptoUtils.KeyStore.IGetKeyPairOptions}
5166
+ * (currently the `rehydrate` escrow-recovery flag).
4966
5167
  * @returns Success with `{ publicKey, privateKey }`, Failure if not found,
4967
- * locked, wrong type, no provider, or storage load failed.
5168
+ * locked, wrong type, no provider, or storage load (and any escrow
5169
+ * rehydration) failed.
4968
5170
  * @public
4969
5171
  */
4970
- getKeyPair(name: string): Promise<Result<{
5172
+ getKeyPair(name: string, options?: IGetKeyPairOptions): Promise<Result<{
4971
5173
  publicKey: CryptoKey;
4972
5174
  privateKey: CryptoKey;
4973
5175
  }>>;
@@ -5053,6 +5255,40 @@ declare class KeyStore_2 implements IEncryptionProvider {
5053
5255
  * @returns A warning string if storage cleanup failed, otherwise undefined.
5054
5256
  */
5055
5257
  private _releaseEntryResources;
5258
+ /**
5259
+ * Exports the transient extractable private `CryptoKey` to a JWK for escrow,
5260
+ * via WebCrypto's `exportKey('jwk', ...)` (cross-runtime through
5261
+ * `globalThis.crypto.subtle`). The result is carried in the vault entry — the
5262
+ * same custody class as the public JWK — and is never returned from a public
5263
+ * method nor logged.
5264
+ */
5265
+ private _exportPrivateKeyJwk;
5266
+ /**
5267
+ * Re-imports an escrowed private-key JWK as a `CryptoKey` for `algorithm`
5268
+ * with the requested extractability. The WebCrypto JWK-import descriptor is
5269
+ * shared between the public and private halves for every supported algorithm,
5270
+ * so `IKeyPairAlgorithmParams.importPublicKey` is reused; the private/public
5271
+ * distinction is carried by the requested usages (see
5272
+ * {@link KeyStore._privateKeyUsagesFor}). Cross-runtime through
5273
+ * `globalThis.crypto.subtle`.
5274
+ */
5275
+ private _importEscrowedPrivateKey;
5276
+ /**
5277
+ * Loads the private key for `entry` from storage, or — when
5278
+ * `options.rehydrate` is set, storage holds no blob, and the entry carries an
5279
+ * escrowed JWK — imports the escrowed key, persists it under the entry's `id`
5280
+ * (fill-a-gap; never overwrites an existing blob), and returns it.
5281
+ */
5282
+ private _loadOrRehydratePrivateKey;
5283
+ /**
5284
+ * Computes the key usages to request when importing an escrowed private JWK.
5285
+ * Mirrors `EncryptedFilePrivateKeyStorage`: intersect the algorithm's private
5286
+ * usages (its keypair usages minus the public-only ones) with the JWK's
5287
+ * recorded `key_ops` so we request exactly the operations the stored key
5288
+ * supports; fall back to the algorithm's private usages when `key_ops` is
5289
+ * absent.
5290
+ */
5291
+ private static _privateKeyUsagesFor;
5056
5292
  /**
5057
5293
  * Constant-time byte comparison. Returns false immediately for length
5058
5294
  * mismatch (length is not secret); for equal-length inputs, walks the full
@@ -5069,7 +5305,7 @@ declare class KeyStore_2 implements IEncryptionProvider {
5069
5305
  }
5070
5306
 
5071
5307
  /**
5072
- * Current format version constant.
5308
+ * Current format version constant. New vaults are written as `'keystore-v2'`.
5073
5309
  * @public
5074
5310
  */
5075
5311
  declare const KEYSTORE_FORMAT: KeyStoreFormat;
@@ -5107,9 +5343,15 @@ declare const keystoreFile: Converter<IKeyStoreFile>;
5107
5343
 
5108
5344
  /**
5109
5345
  * Format version for key store files.
5346
+ *
5347
+ * - `'keystore-v1'`: the original format (no private-key escrow).
5348
+ * - `'keystore-v2'`: adds the optional `escrowedPrivateKeyJwk` field on
5349
+ * asymmetric-keypair entries. A strict superset of v1 — the field is
5350
+ * optional, so a v2 reader opens a v1 vault with no special-casing, and a
5351
+ * v1 vault opened and re-saved is silently upgraded to v2.
5110
5352
  * @public
5111
5353
  */
5112
- declare type KeyStoreFormat = 'keystore-v1';
5354
+ declare type KeyStoreFormat = 'keystore-v1' | 'keystore-v2';
5113
5355
 
5114
5356
  /**
5115
5357
  * Converter for {@link CryptoUtils.KeyStore.KeyStoreFormat | key store format} version.
@@ -5447,6 +5689,30 @@ declare class NodeCryptoProvider implements ICryptoProvider {
5447
5689
  * @returns `Success` with decrypted UTF-8 string, or `Failure` with an error.
5448
5690
  */
5449
5691
  decrypt(encryptedData: Uint8Array, key: Uint8Array, iv: Uint8Array, authTag: Uint8Array): Promise<Result<string>>;
5692
+ /**
5693
+ * Encrypts raw bytes using AES-256-GCM with a caller-supplied nonce and
5694
+ * optional AAD. See {@link CryptoUtils.ICryptoProvider.encryptBytes | ICryptoProvider.encryptBytes}
5695
+ * — in particular the caller's responsibility to use a unique `nonce` per
5696
+ * message under a given `key`.
5697
+ * @param key - 32-byte AES-256 key.
5698
+ * @param nonce - 12-byte GCM nonce (must be unique per message under `key`).
5699
+ * @param plaintext - The bytes to encrypt (empty permitted).
5700
+ * @param aad - Optional additional authenticated data bound into the tag.
5701
+ * @returns `Success` with the ciphertext and 16-byte auth tag, or `Failure` with an error.
5702
+ */
5703
+ encryptBytes(key: Uint8Array, nonce: Uint8Array, plaintext: Uint8Array, aad?: Uint8Array): Promise<Result<IEncryptBytesResult>>;
5704
+ /**
5705
+ * Decrypts raw bytes produced by {@link NodeCryptoProvider.encryptBytes} using
5706
+ * AES-256-GCM. See {@link CryptoUtils.ICryptoProvider.decryptBytes | ICryptoProvider.decryptBytes}.
5707
+ * Fails (never throws) on any authentication failure, including a mismatched `aad`.
5708
+ * @param key - 32-byte AES-256 key.
5709
+ * @param nonce - 12-byte GCM nonce (the same one used to encrypt).
5710
+ * @param ciphertext - The ciphertext from the `encryptBytes` result.
5711
+ * @param authTag - The 16-byte GCM auth tag from the `encryptBytes` result.
5712
+ * @param aad - The identical AAD supplied at encrypt time (or absent).
5713
+ * @returns `Success` with the decrypted plaintext bytes, or `Failure` with an error.
5714
+ */
5715
+ decryptBytes(key: Uint8Array, nonce: Uint8Array, ciphertext: Uint8Array, authTag: Uint8Array, aad?: Uint8Array): Promise<Result<Uint8Array>>;
5450
5716
  /**
5451
5717
  * Generates a random 32-byte key suitable for AES-256.
5452
5718
  * @returns `Success` with generated key, or `Failure` with an error.
@@ -5956,6 +6222,22 @@ declare type SecretProvider = (secretName: string) => Promise<Result<Uint8Array>
5956
6222
  */
5957
6223
  declare const SMART_JSON_PROMPT_HINT: string;
5958
6224
 
6225
+ /**
6226
+ * Strips the fixed DER prefix from an X25519 SubjectPublicKeyInfo blob, returning the raw
6227
+ * 32-byte public key.
6228
+ *
6229
+ * An X25519 SPKI is always exactly 44 bytes: a fixed 12-byte prefix (SEQUENCE / AlgorithmIdentifier
6230
+ * with OID 1.3.101.110 / BIT STRING) followed by the 32-byte raw key. Use this to convert a
6231
+ * SPKI-held recipient public key into the raw form accepted by {@link HpkeProvider.openBase}'s
6232
+ * `recipientPublicKey` parameter.
6233
+ *
6234
+ * @param spki - The DER-encoded X25519 SubjectPublicKeyInfo bytes.
6235
+ * @returns `Success` with the raw 32-byte public key, or `Failure` if `spki` is not a
6236
+ * well-formed 44-byte X25519 SPKI blob.
6237
+ * @public
6238
+ */
6239
+ declare function spkiToRawX25519(spki: Uint8Array): Result<Uint8Array>;
6240
+
5959
6241
  /**
5960
6242
  * Whether a provider declares any embedding capability at all.
5961
6243
  *
@@ -93,15 +93,23 @@ export declare class HpkeProvider {
93
93
  *
94
94
  * @param recipientPrivateKey - Recipient's X25519 private `CryptoKey`
95
95
  * (`algorithm.name === 'X25519'`, `type === 'private'`, `usages` includes `'deriveBits'`).
96
- * **Must be extractable** (`extractable: true`) the recipient's public key bytes
97
- * are recovered from the JWK `x` field during Decap.
96
+ * **Must be extractable** (`extractable: true`) only when `recipientPublicKey` is not
97
+ * supplied — the recipient's public key bytes are then recovered from the JWK `x` field
98
+ * during Decap. When `recipientPublicKey` is supplied, `recipientPrivateKey` may be
99
+ * non-extractable.
98
100
  * @param info - Context-binding bytes. Must exactly match `info` from `sealBase`.
99
101
  * @param aad - Must exactly match `aad` from `sealBase`.
100
102
  * @param enc - The encapsulated key from `sealBase` — exactly 32 bytes.
101
103
  * @param ciphertext - The ciphertext from `sealBase` — `plaintext.length + 16` bytes.
104
+ * @param recipientPublicKey - Optional raw 32-byte X25519 public key matching
105
+ * `recipientPrivateKey` (`pkRm` in RFC 9180 §4.1). Public material — supplying it lets
106
+ * Decap build `kem_context` without exporting `recipientPrivateKey` to JWK, so
107
+ * `recipientPrivateKey` no longer needs to be extractable. A mismatched value only breaks
108
+ * the caller's own decryption (AEAD authentication fails) — it cannot be used to attack
109
+ * another party's ciphertext.
102
110
  * @returns `Success` with decrypted plaintext bytes, or `Failure` with error context.
103
111
  */
104
- openBase(recipientPrivateKey: CryptoKey, info: Uint8Array, aad: Uint8Array, enc: Uint8Array, ciphertext: Uint8Array): Promise<Result<Uint8Array>>;
112
+ openBase(recipientPrivateKey: CryptoKey, info: Uint8Array, aad: Uint8Array, enc: Uint8Array, ciphertext: Uint8Array, recipientPublicKey?: Uint8Array): Promise<Result<Uint8Array>>;
105
113
  /**
106
114
  * HKDF-SHA256 key derivation (RFC 5869). Extract-then-Expand using SHA-256.
107
115
  *
@@ -1 +1 @@
1
- {"version":3,"file":"hpkeProvider.d.ts","sourceRoot":"","sources":["../../../src/packlets/crypto-utils/hpkeProvider.ts"],"names":[],"mappings":"AAoBA,OAAO,EAAE,MAAM,EAAoD,MAAM,eAAe,CAAC;AAoOzF;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC;IAEzB;;;OAGG;IACH,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;CACjC;AAID;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAe;IAEvC,OAAO;IAIP;;;;;;;OAOG;WACW,MAAM,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAAC,YAAY,CAAC;IAIhE;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACU,QAAQ,CACnB,kBAAkB,EAAE,SAAS,EAC7B,IAAI,EAAE,UAAU,EAChB,GAAG,EAAE,UAAU,EACf,SAAS,EAAE,UAAU,GACpB,OAAO,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;IAenC;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACU,QAAQ,CACnB,mBAAmB,EAAE,SAAS,EAC9B,IAAI,EAAE,UAAU,EAChB,GAAG,EAAE,UAAU,EACf,GAAG,EAAE,UAAU,EACf,UAAU,EAAE,UAAU,GACrB,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IAuB9B;;;;;;;;;;;;;OAaG;IACU,IAAI,CACf,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,UAAU,EAChB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IAQ9B;;;;;;;;OAQG;WACW,cAAc,CAAC,MAAM,EAAE,eAAe,GAAG,UAAU;IAIjE;;;;;;;;;OASG;WACW,cAAc,CAAC,QAAQ,EAAE,UAAU,GAAG,MAAM,CAAC,eAAe,CAAC;CAY5E"}
1
+ {"version":3,"file":"hpkeProvider.d.ts","sourceRoot":"","sources":["../../../src/packlets/crypto-utils/hpkeProvider.ts"],"names":[],"mappings":"AAoBA,OAAO,EAAE,MAAM,EAAoD,MAAM,eAAe,CAAC;AA+OzF;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC;IAEzB;;;OAGG;IACH,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;CACjC;AAID;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAe;IAEvC,OAAO;IAIP;;;;;;;OAOG;WACW,MAAM,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAAC,YAAY,CAAC;IAIhE;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACU,QAAQ,CACnB,kBAAkB,EAAE,SAAS,EAC7B,IAAI,EAAE,UAAU,EAChB,GAAG,EAAE,UAAU,EACf,SAAS,EAAE,UAAU,GACpB,OAAO,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;IAenC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAgCG;IACU,QAAQ,CACnB,mBAAmB,EAAE,SAAS,EAC9B,IAAI,EAAE,UAAU,EAChB,GAAG,EAAE,UAAU,EACf,GAAG,EAAE,UAAU,EACf,UAAU,EAAE,UAAU,EACtB,kBAAkB,CAAC,EAAE,UAAU,GAC9B,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IA4B9B;;;;;;;;;;;;;OAaG;IACU,IAAI,CACf,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,UAAU,EAChB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IAQ9B;;;;;;;;OAQG;WACW,cAAc,CAAC,MAAM,EAAE,eAAe,GAAG,UAAU;IAIjE;;;;;;;;;OASG;WACW,cAAc,CAAC,QAAQ,EAAE,UAAU,GAAG,MAAM,CAAC,eAAe,CAAC;CAY5E"}
@@ -141,17 +141,28 @@ async function _kemEncap(subtle, recipientPublicKey) {
141
141
  }
142
142
  // RFC 9180 §4.1 DHKEM Decap — deserializes enc, DH with recipient privkey,
143
143
  // derives same shared_secret via ExtractAndExpand.
144
- // Requires recipientPrivateKey to be extractable (JWK export needed for pkRm).
145
- async function _kemDecap(subtle, enc, recipientPrivateKey) {
144
+ // The DH step (deriveBits) works on a non-extractable recipientPrivateKey. Recovering the
145
+ // recipient's own public key bytes (pkRm) for kem_context normally requires a JWK export, which
146
+ // in turn requires recipientPrivateKey to be extractable — UNLESS the caller supplies pkRm
147
+ // directly (raw 32-byte X25519 public key), in which case no JWK export happens and
148
+ // recipientPrivateKey may be non-extractable.
149
+ async function _kemDecap(subtle, enc, recipientPrivateKey, recipientPublicKey) {
146
150
  const pkE = await subtle.importKey('raw', _toBufferView(enc), { name: 'X25519' }, true, []);
147
151
  const dh = new Uint8Array(await subtle.deriveBits({ name: 'X25519', public: pkE }, recipientPrivateKey, 256));
148
- // Recover recipient's own public key from JWK x field (base64url-encoded raw X25519 public key).
149
- const jwk = (await subtle.exportKey('jwk', recipientPrivateKey));
150
- /* c8 ignore next 3 - defensive: X25519 JWK always has an x field; unreachable via public API */
151
- if (!jwk.x) {
152
- throw new Error('HPKE Decap: failed to extract public key bytes from recipient private key JWK');
152
+ let pkRm;
153
+ if (recipientPublicKey !== undefined) {
154
+ // Caller-supplied; length already validated in openBase.
155
+ pkRm = recipientPublicKey;
156
+ }
157
+ else {
158
+ // Recover recipient's own public key from JWK x field (base64url-encoded raw X25519 public key).
159
+ const jwk = (await subtle.exportKey('jwk', recipientPrivateKey));
160
+ /* c8 ignore next 3 - defensive: X25519 JWK always has an x field; unreachable via public API */
161
+ if (!jwk.x) {
162
+ throw new Error('HPKE Decap: failed to extract public key bytes from recipient private key JWK');
163
+ }
164
+ pkRm = _base64UrlDecode(jwk.x);
153
165
  }
154
- const pkRm = _base64UrlDecode(jwk.x);
155
166
  const kemContext = _concat(enc, pkRm);
156
167
  const eaePrk = await _labeledExtract(subtle, _KEM_SUITE_ID, new Uint8Array(0), 'eae_prk', dh);
157
168
  return _labeledExpand(subtle, _KEM_SUITE_ID, eaePrk, 'shared_secret', kemContext, _N_SECRET);
@@ -255,23 +266,34 @@ class HpkeProvider {
255
266
  *
256
267
  * @param recipientPrivateKey - Recipient's X25519 private `CryptoKey`
257
268
  * (`algorithm.name === 'X25519'`, `type === 'private'`, `usages` includes `'deriveBits'`).
258
- * **Must be extractable** (`extractable: true`) the recipient's public key bytes
259
- * are recovered from the JWK `x` field during Decap.
269
+ * **Must be extractable** (`extractable: true`) only when `recipientPublicKey` is not
270
+ * supplied — the recipient's public key bytes are then recovered from the JWK `x` field
271
+ * during Decap. When `recipientPublicKey` is supplied, `recipientPrivateKey` may be
272
+ * non-extractable.
260
273
  * @param info - Context-binding bytes. Must exactly match `info` from `sealBase`.
261
274
  * @param aad - Must exactly match `aad` from `sealBase`.
262
275
  * @param enc - The encapsulated key from `sealBase` — exactly 32 bytes.
263
276
  * @param ciphertext - The ciphertext from `sealBase` — `plaintext.length + 16` bytes.
277
+ * @param recipientPublicKey - Optional raw 32-byte X25519 public key matching
278
+ * `recipientPrivateKey` (`pkRm` in RFC 9180 §4.1). Public material — supplying it lets
279
+ * Decap build `kem_context` without exporting `recipientPrivateKey` to JWK, so
280
+ * `recipientPrivateKey` no longer needs to be extractable. A mismatched value only breaks
281
+ * the caller's own decryption (AEAD authentication fails) — it cannot be used to attack
282
+ * another party's ciphertext.
264
283
  * @returns `Success` with decrypted plaintext bytes, or `Failure` with error context.
265
284
  */
266
- async openBase(recipientPrivateKey, info, aad, enc, ciphertext) {
285
+ async openBase(recipientPrivateKey, info, aad, enc, ciphertext, recipientPublicKey) {
267
286
  if (enc.length !== _N_PK) {
268
287
  return (0, ts_utils_1.fail)(`HPKE openBase: enc must be ${_N_PK} bytes, got ${enc.length}`);
269
288
  }
270
289
  if (ciphertext.length < _N_T) {
271
290
  return (0, ts_utils_1.fail)(`HPKE openBase: ciphertext too short (minimum ${_N_T} bytes for auth tag, got ${ciphertext.length})`);
272
291
  }
292
+ if (recipientPublicKey !== undefined && recipientPublicKey.length !== _N_PK) {
293
+ return (0, ts_utils_1.fail)(`HPKE openBase: recipientPublicKey must be ${_N_PK} bytes, got ${recipientPublicKey.length}`);
294
+ }
273
295
  const result = await (0, ts_utils_1.captureAsyncResult)(async () => {
274
- const sharedSecret = await _kemDecap(this._subtle, enc, recipientPrivateKey);
296
+ const sharedSecret = await _kemDecap(this._subtle, enc, recipientPrivateKey, recipientPublicKey);
275
297
  const { key, baseNonce } = await _keyScheduleBase(this._subtle, sharedSecret, info);
276
298
  const aesKey = await this._subtle.importKey('raw', key, { name: 'AES-GCM' }, false, ['decrypt']);
277
299
  const pt = await this._subtle.decrypt({ name: 'AES-GCM', iv: baseNonce, additionalData: _toBufferView(aad) }, aesKey, _toBufferView(ciphertext));