@katlux/providers 0.1.1-alpha.126 → 0.1.1-alpha.128

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.
package/dist/index.cjs CHANGED
@@ -18,6 +18,8 @@ function _interopNamespaceCompat(e) {
18
18
 
19
19
  const crypto__namespace = /*#__PURE__*/_interopNamespaceCompat(crypto$1);
20
20
 
21
+ const DECRYPT_ERROR_SENTINEL = Symbol.for("katlux.decrypt_error");
22
+
21
23
  var EDataFilterOperator = /* @__PURE__ */ ((EDataFilterOperator2) => {
22
24
  EDataFilterOperator2["Equal"] = "==";
23
25
  EDataFilterOperator2["NotEqual"] = "!=";
@@ -672,878 +674,1725 @@ function isAuthOrDecryptionError(err) {
672
674
  const msg = String(err.message || "").toLowerCase();
673
675
  return msg.includes("operationerror") || msg.includes("decrypt") || msg.includes("tag") || msg.includes("aad") || msg.includes("authenticated additional data") || msg.includes("authenticate data") || msg.includes("mac");
674
676
  }
675
-
676
- class AEncryptionProvider {
677
- defaultKey;
678
- constructor(key) {
679
- if (key !== void 0) {
680
- this.defaultKey = key;
677
+ function canonicalizeJson(value, seen = /* @__PURE__ */ new Set()) {
678
+ if (value === void 0) {
679
+ return "";
680
+ }
681
+ if (value === null || typeof value === "boolean") {
682
+ return JSON.stringify(value);
683
+ }
684
+ if (typeof value === "number") {
685
+ if (!Number.isFinite(value)) return "null";
686
+ if (Object.is(value, -0)) return "0";
687
+ return JSON.stringify(value);
688
+ }
689
+ if (typeof value === "string") {
690
+ return JSON.stringify(value);
691
+ }
692
+ if (typeof value === "bigint") {
693
+ throw new TypeError("Do not know how to serialize a BigInt");
694
+ }
695
+ if (typeof value === "symbol" || typeof value === "function") {
696
+ return "";
697
+ }
698
+ if (typeof value === "object") {
699
+ if (typeof value.toJSON === "function") {
700
+ return canonicalizeJson(value.toJSON(), seen);
701
+ }
702
+ if (seen.has(value)) {
703
+ throw new TypeError("Converting circular structure to JSON");
704
+ }
705
+ seen.add(value);
706
+ try {
707
+ if (Array.isArray(value)) {
708
+ const items = value.map((item) => {
709
+ if (item === void 0 || typeof item === "function" || typeof item === "symbol") {
710
+ return "null";
711
+ }
712
+ const res = canonicalizeJson(item, seen);
713
+ return res === "" ? "null" : res;
714
+ });
715
+ return `[${items.join(",")}]`;
716
+ }
717
+ const keys = Object.keys(value).sort();
718
+ const entries = [];
719
+ for (const key of keys) {
720
+ const val = value[key];
721
+ if (val === void 0 || typeof val === "function" || typeof val === "symbol") {
722
+ continue;
723
+ }
724
+ const serialized = canonicalizeJson(val, seen);
725
+ if (serialized !== "") {
726
+ entries.push(`${JSON.stringify(key)}:${serialized}`);
727
+ }
728
+ }
729
+ return `{${entries.join(",")}}`;
730
+ } finally {
731
+ seen.delete(value);
681
732
  }
682
733
  }
734
+ return JSON.stringify(value);
735
+ }
736
+
737
+ class CKeyManager {
738
+ // ── Binary & Encoding Utilities ──────────────────────────────────────────
683
739
  /**
684
- * Sets or updates the default secret key, CryptoKey, or async key provider function.
740
+ * Generates a cryptographically secure random byte buffer of the specified length.
685
741
  */
686
- setDefaultKey(key) {
687
- this.defaultKey = key || void 0;
742
+ static randomBytes(length) {
743
+ const bytes = new Uint8Array(length);
744
+ return getRandomValues(bytes);
688
745
  }
689
746
  /**
690
- * Retrieves the currently configured default key or key provider callback.
747
+ * Converts an ArrayBuffer or Uint8Array into a Base64-encoded string.
691
748
  */
692
- getDefaultKey() {
693
- return this.defaultKey;
749
+ static toBase64(buffer) {
750
+ return toBase64(buffer);
694
751
  }
695
752
  /**
696
- * Pre-derives and caches the native CryptoKey for subsequent cryptographic operations.
697
- * Subclasses should override this with a dedicated, non-side-effecting implementation.
753
+ * Converts a Base64 or Base64URL-encoded string back into a Uint8Array.
754
+ * Normalizes URL-safe characters and padding, strictly validating against the Base64 alphabet.
698
755
  */
699
- async warmupKey(_options) {
756
+ static fromBase64(base64) {
757
+ if (typeof base64 !== "string") {
758
+ throw new Error("[CKeyManager] Invalid base64 input: expected string");
759
+ }
760
+ const cleaned = base64.replace(/\s+/g, "");
761
+ if (!cleaned) return new Uint8Array(0);
762
+ const normalized = cleaned.replace(/-/g, "+").replace(/_/g, "/");
763
+ const padded = normalized.padEnd(normalized.length + (4 - normalized.length % 4) % 4, "=");
764
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(padded)) {
765
+ throw new Error("[CKeyManager] Invalid base64 string");
766
+ }
767
+ if (typeof Buffer !== "undefined") {
768
+ const buf = Buffer.from(padded, "base64");
769
+ const arrayBuf = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
770
+ return new Uint8Array(arrayBuf);
771
+ }
772
+ const binary = atob(padded);
773
+ const bytes = new Uint8Array(binary.length);
774
+ for (let i = 0; i < binary.length; i++) {
775
+ bytes[i] = binary.charCodeAt(i);
776
+ }
777
+ return bytes;
700
778
  }
701
779
  /**
702
- * Resolves raw key from keyProvider callback or defaultKey.
780
+ * Deterministically resolves a salt from Uint8Array, ArrayBuffer, Base64/Base64URL string, or raw text string.
781
+ * Guarantees 100% identical salt byte resolution between Node.js and Browser environments.
703
782
  */
704
- async resolveRawKey(keyProvider) {
705
- const target = keyProvider || this.defaultKey;
706
- if (!target) {
707
- throw new Error(
708
- `[${this.constructor.name}] No encryption key or keyProvider was supplied. Key cannot be empty or undefined. A valid key must be supplied from your authentication or secret management system.`
709
- );
783
+ static resolveSalt(salt) {
784
+ if (!salt) return CKeyManager.randomBytes(16);
785
+ if (ArrayBuffer.isView(salt)) {
786
+ return salt.byteLength > 0 ? new Uint8Array(salt.buffer.slice(salt.byteOffset, salt.byteOffset + salt.byteLength)) : CKeyManager.randomBytes(16);
710
787
  }
711
- const rawKey = typeof target === "function" ? await target() : target;
712
- if (!rawKey || typeof rawKey === "string" && rawKey.trim().length === 0 || typeof rawKey === "object" && "byteLength" in rawKey && rawKey.byteLength === 0) {
713
- throw new Error(
714
- `[${this.constructor.name}] No encryption key or keyProvider was supplied. Key provider resolved to empty or undefined key.`
715
- );
788
+ if (salt instanceof ArrayBuffer) {
789
+ return salt.byteLength > 0 ? new Uint8Array(salt.slice(0)) : CKeyManager.randomBytes(16);
716
790
  }
717
- return rawKey;
791
+ if (typeof salt === "string") {
792
+ const trimmed = salt.trim();
793
+ if (!trimmed) return CKeyManager.randomBytes(16);
794
+ if (trimmed.startsWith("base64:")) {
795
+ return CKeyManager.fromBase64(trimmed.slice(7));
796
+ }
797
+ if (trimmed.endsWith("=") && /^[A-Za-z0-9+/]+={1,2}$/.test(trimmed)) {
798
+ try {
799
+ const decoded = CKeyManager.fromBase64(trimmed);
800
+ if (decoded.byteLength > 0) return decoded;
801
+ } catch {
802
+ }
803
+ }
804
+ const encoded = textEncoder$1.encode(trimmed);
805
+ return encoded.byteLength > 0 ? encoded : CKeyManager.randomBytes(16);
806
+ }
807
+ return CKeyManager.randomBytes(16);
718
808
  }
809
+ // ── Symmetric Key Management ─────────────────────────────────────────────
719
810
  /**
720
- * Checks whether the given payload is an encrypted envelope.
811
+ * Generates a random AES CryptoKey suitable for AES-GCM or AES-CBC.
812
+ *
813
+ * E2EE Threat Model Note: Focuses on protecting GDPR/tenant data against database and
814
+ * server-side leaks. Keys remain extractable by default to enable role-based key sharing,
815
+ * wrapping, and transfer between authorized client sessions in multi-user SaaS apps.
816
+ * Client-side malware/trojan protection is out of scope.
721
817
  */
722
- static isEncrypted(payload) {
723
- if (!payload || typeof payload !== "object") return false;
724
- return payload.__encrypted === true && typeof payload.ciphertext === "string" && typeof payload.iv === "string";
818
+ static async generateAESKey(length = 256, algorithm = "AES-GCM", extractable = true) {
819
+ const subtle = getSubtleCrypto();
820
+ const usages = algorithm === "AES-KW" ? ["wrapKey", "unwrapKey"] : ["encrypt", "decrypt"];
821
+ return await subtle.generateKey(
822
+ { name: algorithm, length },
823
+ extractable,
824
+ usages
825
+ );
725
826
  }
726
827
  /**
727
- * Checks whether the given payload is an encrypted envelope.
828
+ * Exports an AES CryptoKey as a raw base64 string.
728
829
  */
729
- isEncrypted(payload) {
730
- return AEncryptionProvider.isEncrypted(payload);
830
+ static async exportAESKey(key) {
831
+ const subtle = getSubtleCrypto();
832
+ const raw = await subtle.exportKey("raw", key);
833
+ try {
834
+ return CKeyManager.toBase64(raw);
835
+ } finally {
836
+ new Uint8Array(raw).fill(0);
837
+ }
731
838
  }
732
839
  /**
733
- * Clears configured keys and cached cryptographic material from memory.
840
+ * Imports an AES CryptoKey from a raw base64 string.
734
841
  */
735
- clearKeys() {
736
- this.defaultKey = void 0;
842
+ static async importAESKey(base64, algorithm = "AES-GCM", extractable = false) {
843
+ const subtle = getSubtleCrypto();
844
+ const rawBytes = CKeyManager.fromBase64(base64);
845
+ if (rawBytes.length !== 16 && rawBytes.length !== 24 && rawBytes.length !== 32) {
846
+ rawBytes.fill(0);
847
+ throw new Error(`[CKeyManager] Invalid AES key length: expected 16, 24, or 32 bytes (128, 192, or 256 bits), got ${rawBytes.length} bytes.`);
848
+ }
849
+ const usages = algorithm === "AES-KW" ? ["wrapKey", "unwrapKey"] : ["encrypt", "decrypt"];
850
+ try {
851
+ return await subtle.importKey(
852
+ "raw",
853
+ rawBytes,
854
+ { name: algorithm },
855
+ extractable,
856
+ usages
857
+ );
858
+ } finally {
859
+ rawBytes.fill(0);
860
+ }
737
861
  }
862
+ // ── Asymmetric Key Pair Management ───────────────────────────────────────
738
863
  /**
739
- * Alias for clearKeys() to conform with standard disposal patterns.
864
+ * Generates an RSA-OAEP public/private key pair (2048 or 4096 bits).
740
865
  */
741
- dispose() {
742
- this.clearKeys();
866
+ static async generateRSAKeyPair(bits = 4096, hash = "SHA-256", extractable = true) {
867
+ const subtle = getSubtleCrypto();
868
+ const pair = await subtle.generateKey(
869
+ {
870
+ name: "RSA-OAEP",
871
+ modulusLength: bits,
872
+ publicExponent: new Uint8Array([1, 0, 1]),
873
+ hash
874
+ },
875
+ extractable,
876
+ ["encrypt", "decrypt", "wrapKey", "unwrapKey"]
877
+ );
878
+ return {
879
+ publicKey: pair.publicKey,
880
+ privateKey: pair.privateKey
881
+ };
743
882
  }
744
883
  /**
745
- * ECMAScript Explicit Resource Management protocol (using / await using declarations).
884
+ * Generates an ECDH public/private key pair for elliptic curve Diffie-Hellman.
746
885
  */
747
- [Symbol.dispose]() {
748
- this.dispose();
749
- }
750
- async [Symbol.asyncDispose]() {
751
- this.dispose();
886
+ static async generateECDHKeyPair(curve = "P-256", extractable = true) {
887
+ const subtle = getSubtleCrypto();
888
+ const pair = await subtle.generateKey(
889
+ {
890
+ name: "ECDH",
891
+ namedCurve: curve
892
+ },
893
+ extractable,
894
+ ["deriveKey", "deriveBits"]
895
+ );
896
+ return {
897
+ publicKey: pair.publicKey,
898
+ privateKey: pair.privateKey
899
+ };
752
900
  }
753
901
  /**
754
- * Resolves effective Authenticated Additional Data (AAD) for the operation:
755
- * 1. If options.bindRequestContextAad is explicitly false, returns options.aad directly without context binding.
756
- * 2. When encrypting (isDecrypt=false): binds request context `req:${method}:${url}` only if options.bindRequestContextAad === true.
757
- * 3. When decrypting (isDecrypt=true): derives request context `req:${method}:${url}` if url or method is provided.
758
- * 4. If explicit options.aad is provided: combines with request context if bound, or returns options.aad directly.
759
- * 5. Otherwise returns undefined.
902
+ * Exports a public CryptoKey to a portable JsonWebKey (JWK).
760
903
  */
761
- _resolveEffectiveAad(options, isDecrypt = false) {
762
- if (!options) return void 0;
763
- if (options.bindRequestContextAad === false) return options.aad;
764
- const hasReqContext = Boolean(options.url || options.method);
765
- if (options.aad) {
766
- if (options.bindRequestContextAad === true && hasReqContext) {
767
- const m = (options.method || "POST").toUpperCase();
768
- const u = options.url || "";
769
- const baseAad = typeof options.aad === "string" ? options.aad : options.aad instanceof Uint8Array || options.aad instanceof ArrayBuffer ? textDecoder.decode(options.aad) : String(options.aad);
770
- return `${baseAad}|req:${m}:${u}`;
771
- }
772
- return options.aad;
773
- }
774
- const shouldBindReq = isDecrypt ? hasReqContext : options.bindRequestContextAad === true && hasReqContext;
775
- if (shouldBindReq) {
776
- const m = (options.method || "POST").toUpperCase();
777
- const u = options.url || "";
778
- return `req:${m}:${u}`;
779
- }
780
- return void 0;
904
+ static async exportPublicKey(key) {
905
+ const subtle = getSubtleCrypto();
906
+ return await subtle.exportKey("jwk", key);
781
907
  }
782
908
  /**
783
- * Helper to deterministically bind record ID and field name into the AAD
784
- * to prevent ciphertext swapping between fields or records.
909
+ * Exports a private CryptoKey to a portable JsonWebKey (JWK).
785
910
  */
786
- _bindFieldAad(data, field, options) {
787
- let baseAad = "";
788
- if (options?.aad) {
789
- if (typeof options.aad === "string") baseAad = options.aad;
790
- else if (options.aad instanceof Uint8Array || options.aad instanceof ArrayBuffer) {
791
- baseAad = textDecoder.decode(options.aad);
792
- } else {
793
- baseAad = String(options.aad);
794
- }
795
- }
796
- const recordId = data && typeof data === "object" && (data.id !== void 0 || data._id !== void 0) ? String(data.id ?? data._id) : "";
797
- const recordPart = recordId ? `|record:${recordId}` : "";
798
- const reqPart = options?.bindRequestContextAad === true && (options?.url || options?.method) ? `|req:${(options.method || "POST").toUpperCase()}:${options.url || ""}` : "";
799
- return {
800
- ...options,
801
- aad: `aad:${baseAad}${recordPart}${reqPart}|field:${field}`
802
- };
911
+ static async exportPrivateKey(key) {
912
+ const subtle = getSubtleCrypto();
913
+ return await subtle.exportKey("jwk", key);
803
914
  }
804
915
  /**
805
- * Top-level polymorphic encryption method.
806
- * Automatically performs value-level encryption:
807
- * - If data is an array of objects: recursively encrypts each row.
808
- * - If data is an object: encrypts specified fields (or all values if valueEncryption is true),
809
- * preserving JSON object keys so backend APIs and ORMs can parse keys and perform upserts.
810
- * - If data is a primitive or whole-payload envelope: encrypts as a single value.
916
+ * Imports a public key from a JsonWebKey (JWK).
811
917
  */
812
- async encrypt(data, options) {
813
- if (data === void 0 || data === null) return data;
814
- if (this.isEncrypted(data)) return data;
815
- if (options?.valueEncryption === false && (!options.fields || options.fields.length === 0)) {
816
- return this.encryptValue(data, options);
817
- }
818
- if (Array.isArray(data)) {
819
- if (data.length === 0) return data;
820
- if (options?.fields && options.fields.length > 0) {
821
- const fields = options.fields;
822
- let hasUnencrypted = false;
823
- for (let i = 0; i < data.length; i++) {
824
- const item = data[i];
825
- if (item && typeof item === "object") {
826
- for (let f = 0; f < fields.length; f++) {
827
- const val = item[fields[f]];
828
- if (val !== void 0 && val !== null && !this.isEncrypted(val)) {
829
- hasUnencrypted = true;
830
- break;
831
- }
832
- }
833
- if (hasUnencrypted) break;
834
- }
918
+ static async importPublicKey(jwk, algorithm) {
919
+ const subtle = getSubtleCrypto();
920
+ let importAlgorithm = algorithm;
921
+ let usages;
922
+ const isEC = jwk.kty === "EC" || typeof algorithm === "object" && algorithm?.name === "ECDH" || algorithm === "ECDH";
923
+ if (isEC) {
924
+ usages = [];
925
+ if (!importAlgorithm || importAlgorithm === "ECDH") {
926
+ importAlgorithm = { name: "ECDH", namedCurve: jwk.crv || "P-256" };
927
+ }
928
+ } else {
929
+ usages = ["encrypt", "wrapKey"];
930
+ if (!importAlgorithm || importAlgorithm === "RSA-OAEP") {
931
+ if (jwk.kty === "RSA") {
932
+ const hash = jwk.alg === "RSA-OAEP-512" ? "SHA-512" : "SHA-256";
933
+ importAlgorithm = { name: "RSA-OAEP", hash };
934
+ } else {
935
+ throw new Error(`[CKeyManager] Unsupported JWK kty: ${jwk.kty}`);
835
936
  }
836
- if (!hasUnencrypted) {
837
- return data;
937
+ }
938
+ }
939
+ return await subtle.importKey("jwk", jwk, importAlgorithm, true, usages);
940
+ }
941
+ /**
942
+ * Imports a private key from a JsonWebKey (JWK).
943
+ */
944
+ static async importPrivateKey(jwk, algorithm) {
945
+ const subtle = getSubtleCrypto();
946
+ let importAlgorithm = algorithm;
947
+ let usages;
948
+ const isEC = jwk.kty === "EC" || typeof algorithm === "object" && algorithm?.name === "ECDH" || algorithm === "ECDH";
949
+ if (isEC) {
950
+ usages = ["deriveKey", "deriveBits"];
951
+ if (!importAlgorithm || importAlgorithm === "ECDH") {
952
+ importAlgorithm = { name: "ECDH", namedCurve: jwk.crv || "P-256" };
953
+ }
954
+ } else {
955
+ usages = ["decrypt", "unwrapKey"];
956
+ if (!importAlgorithm || importAlgorithm === "RSA-OAEP") {
957
+ if (jwk.kty === "RSA") {
958
+ const hash = jwk.alg === "RSA-OAEP-512" ? "SHA-512" : "SHA-256";
959
+ importAlgorithm = { name: "RSA-OAEP", hash };
960
+ } else {
961
+ throw new Error(`[CKeyManager] Unsupported JWK kty: ${jwk.kty}`);
838
962
  }
839
- await this.warmupKey(options);
840
- const itemOptions = { ...options, _warmed: true };
841
- return Promise.all(data.map((item) => this.encrypt(item, itemOptions)));
842
963
  }
843
- if (options?.valueEncryption === true) {
844
- await this.warmupKey(options);
845
- const itemOptions = { ...options, _warmed: true };
846
- return Promise.all(data.map((item) => this.encrypt(item, itemOptions)));
964
+ }
965
+ return await subtle.importKey("jwk", jwk, importAlgorithm, true, usages);
966
+ }
967
+ /**
968
+ * Imports a public key from a PEM (SPKI) or base64 SPKI string.
969
+ */
970
+ static async importPublicKeyFromPEM(pem, algorithm = "RSA-OAEP", curve = "P-256", hash = "SHA-256") {
971
+ const subtle = getSubtleCrypto();
972
+ const cleaned = pem.replace(/-----BEGIN[ A-Z0-9_-]+-----/g, "").replace(/-----END[ A-Z0-9_-]+-----/g, "").replace(/\s+/g, "");
973
+ const spkiBytes = CKeyManager.fromBase64(cleaned);
974
+ let importAlgorithm;
975
+ let usages;
976
+ if (algorithm === "ECDH") {
977
+ importAlgorithm = { name: "ECDH", namedCurve: curve };
978
+ usages = [];
979
+ } else if (algorithm === "ECDSA") {
980
+ importAlgorithm = { name: "ECDSA", namedCurve: curve };
981
+ usages = ["verify"];
982
+ } else {
983
+ importAlgorithm = { name: "RSA-OAEP", hash };
984
+ usages = ["encrypt", "wrapKey"];
985
+ }
986
+ try {
987
+ return await subtle.importKey("spki", spkiBytes, importAlgorithm, true, usages);
988
+ } finally {
989
+ spkiBytes.fill(0);
990
+ }
991
+ }
992
+ /**
993
+ * Imports an ECDSA public key from PEM (SPKI) or base64 SPKI string for signature verification.
994
+ */
995
+ static async importECDSAPublicKeyFromPEM(pem, curve = "P-256") {
996
+ return CKeyManager.importPublicKeyFromPEM(pem, "ECDSA", curve);
997
+ }
998
+ /**
999
+ * Imports a private key from a PEM (PKCS#8) or base64 PKCS#8 string.
1000
+ */
1001
+ static async importPrivateKeyFromPEM(pem, algorithm = "RSA-OAEP", curve = "P-256", hash = "SHA-256") {
1002
+ if (/BEGIN\s+(RSA|EC)\s+PRIVATE\s+KEY/i.test(pem)) {
1003
+ throw new Error(
1004
+ `[CKeyManager] Failed to import private key. The provided PEM appears to be in PKCS#1 or SEC1 format. Web Crypto requires PKCS#8 format. Convert your key using: openssl pkcs8 -topk8 -nocrypt -in private.pem -out private_pkcs8.pem.`
1005
+ );
1006
+ }
1007
+ const subtle = getSubtleCrypto();
1008
+ const cleaned = pem.replace(/-----BEGIN[ A-Z0-9_-]+-----/g, "").replace(/-----END[ A-Z0-9_-]+-----/g, "").replace(/\s+/g, "");
1009
+ const pkcs8Bytes = CKeyManager.fromBase64(cleaned);
1010
+ let importAlgorithm;
1011
+ let usages;
1012
+ if (algorithm === "ECDH") {
1013
+ importAlgorithm = { name: "ECDH", namedCurve: curve };
1014
+ usages = ["deriveKey", "deriveBits"];
1015
+ } else if (algorithm === "ECDSA") {
1016
+ importAlgorithm = { name: "ECDSA", namedCurve: curve };
1017
+ usages = ["sign"];
1018
+ } else {
1019
+ importAlgorithm = { name: "RSA-OAEP", hash };
1020
+ usages = ["decrypt", "unwrapKey"];
1021
+ }
1022
+ try {
1023
+ return await subtle.importKey("pkcs8", pkcs8Bytes, importAlgorithm, true, usages);
1024
+ } finally {
1025
+ pkcs8Bytes.fill(0);
1026
+ }
1027
+ }
1028
+ /**
1029
+ * Imports an ECDSA private key from PEM (PKCS#8) or base64 PKCS#8 string for signing.
1030
+ */
1031
+ static async importECDSAPrivateKeyFromPEM(pem, curve = "P-256") {
1032
+ return CKeyManager.importPrivateKeyFromPEM(pem, "ECDSA", curve);
1033
+ }
1034
+ /**
1035
+ * Exports a public CryptoKey to a standard PEM (SPKI) string.
1036
+ */
1037
+ static async exportPublicKeyToPEM(key) {
1038
+ const subtle = getSubtleCrypto();
1039
+ const spki = await subtle.exportKey("spki", key);
1040
+ let b64;
1041
+ try {
1042
+ b64 = CKeyManager.toBase64(spki);
1043
+ } finally {
1044
+ new Uint8Array(spki).fill(0);
1045
+ }
1046
+ const lines = b64.match(/.{1,64}/g) || [b64];
1047
+ return `-----BEGIN PUBLIC KEY-----
1048
+ ${lines.join("\n")}
1049
+ -----END PUBLIC KEY-----`;
1050
+ }
1051
+ /**
1052
+ * Exports a private CryptoKey to a standard PEM (PKCS#8) string.
1053
+ */
1054
+ static async exportPrivateKeyToPEM(key) {
1055
+ const subtle = getSubtleCrypto();
1056
+ const pkcs8 = await subtle.exportKey("pkcs8", key);
1057
+ let b64;
1058
+ try {
1059
+ b64 = CKeyManager.toBase64(pkcs8);
1060
+ } finally {
1061
+ new Uint8Array(pkcs8).fill(0);
1062
+ }
1063
+ const lines = b64.match(/.{1,64}/g) || [b64];
1064
+ return `-----BEGIN PRIVATE KEY-----
1065
+ ${lines.join("\n")}
1066
+ -----END PRIVATE KEY-----`;
1067
+ }
1068
+ // ── Key Wrapping & Unwrapping ────────────────────────────────────────────
1069
+ /**
1070
+ * Wraps a target CryptoKey (e.g. an Account Symmetric Key) using a wrapping key
1071
+ * (AES-KW or RSA-OAEP).
1072
+ */
1073
+ static async wrapKey(keyToWrap, wrappingKey, format = "raw") {
1074
+ const subtle = getSubtleCrypto();
1075
+ let wrapAlgorithm;
1076
+ if (wrappingKey.algorithm.name === "RSA-OAEP") {
1077
+ wrapAlgorithm = { name: "RSA-OAEP" };
1078
+ } else if (wrappingKey.algorithm.name === "AES-KW") {
1079
+ wrapAlgorithm = "AES-KW";
1080
+ } else {
1081
+ wrapAlgorithm = "AES-KW";
1082
+ }
1083
+ const wrappedBuffer = await subtle.wrapKey(format, keyToWrap, wrappingKey, wrapAlgorithm);
1084
+ return CKeyManager.toBase64(wrappedBuffer);
1085
+ }
1086
+ /**
1087
+ * Unwraps a base64-encoded wrapped key using an unwrapping key (AES-KW or RSA-OAEP).
1088
+ */
1089
+ static async unwrapKey(wrappedBase64, unwrappingKey, unwrappedKeyAlgorithm = { name: "AES-GCM", length: 256 }, extractable = true, keyUsages = ["encrypt", "decrypt"], format = "raw") {
1090
+ const subtle = getSubtleCrypto();
1091
+ const wrappedBytes = CKeyManager.fromBase64(wrappedBase64);
1092
+ let unwrapAlgorithm;
1093
+ if (unwrappingKey.algorithm.name === "RSA-OAEP") {
1094
+ unwrapAlgorithm = { name: "RSA-OAEP" };
1095
+ } else {
1096
+ unwrapAlgorithm = "AES-KW";
1097
+ }
1098
+ try {
1099
+ return await subtle.unwrapKey(
1100
+ format,
1101
+ wrappedBytes,
1102
+ unwrappingKey,
1103
+ unwrapAlgorithm,
1104
+ unwrappedKeyAlgorithm,
1105
+ extractable,
1106
+ keyUsages
1107
+ );
1108
+ } finally {
1109
+ wrappedBytes.fill(0);
1110
+ }
1111
+ }
1112
+ /**
1113
+ * Generates a high-entropy symmetric AES-GCM Account Data Encryption Key (ADEK).
1114
+ * Designed for multi-user table and field-level encryption.
1115
+ */
1116
+ static async generateAccountDataKey(length = 256, extractable = false) {
1117
+ const subtle = getSubtleCrypto();
1118
+ return await subtle.generateKey(
1119
+ { name: "AES-GCM", length },
1120
+ extractable,
1121
+ ["encrypt", "decrypt"]
1122
+ );
1123
+ }
1124
+ /**
1125
+ * Wraps an Account Data Encryption Key (ADEK) for a specific user using the user's public key (RSA-OAEP).
1126
+ * Returns a Base64-encoded wrapped key string suitable for storage in role-map / access tables.
1127
+ */
1128
+ static async wrapAccountKeyForUser(adek, userPublicKey) {
1129
+ const subtle = getSubtleCrypto();
1130
+ if (userPublicKey.algorithm.name === "RSA-OAEP") {
1131
+ const rawKey = await subtle.exportKey("raw", adek);
1132
+ try {
1133
+ const wrappedBuffer = await subtle.encrypt(
1134
+ { name: "RSA-OAEP" },
1135
+ userPublicKey,
1136
+ rawKey
1137
+ );
1138
+ return CKeyManager.toBase64(wrappedBuffer);
1139
+ } finally {
1140
+ new Uint8Array(rawKey).fill(0);
847
1141
  }
848
- return this.encryptValue(data, options);
849
1142
  }
850
- if (typeof data === "object" && !ArrayBuffer.isView(data) && !(data instanceof ArrayBuffer)) {
851
- if (options?.fields && options.fields.length > 0) {
852
- const fieldsToEncrypt = options.fields.filter(
853
- (field) => Object.prototype.hasOwnProperty.call(data, field) && data[field] !== void 0 && data[field] !== null && !this.isEncrypted(data[field])
1143
+ throw new Error(
1144
+ `[CKeyManager] Unsupported user key algorithm for wrapAccountKeyForUser: '${userPublicKey.algorithm.name}'. Expected 'RSA-OAEP'.`
1145
+ );
1146
+ }
1147
+ /**
1148
+ * Unwraps a Base64-encoded Account Data Encryption Key (ADEK) using the user's private key (RSA-OAEP).
1149
+ * Returns the restored AES-GCM CryptoKey ready for use in CWebCryptoEncryptionProvider.
1150
+ */
1151
+ static async unwrapAccountKeyForUser(wrappedKeyBase64, userPrivateKey, unwrappedKeyAlgorithm = { name: "AES-GCM", length: 256 }, extractable = false) {
1152
+ const subtle = getSubtleCrypto();
1153
+ if (userPrivateKey.algorithm.name === "RSA-OAEP") {
1154
+ const wrappedBytes = CKeyManager.fromBase64(wrappedKeyBase64);
1155
+ let unwrappedRawKey;
1156
+ try {
1157
+ unwrappedRawKey = await subtle.decrypt(
1158
+ { name: "RSA-OAEP" },
1159
+ userPrivateKey,
1160
+ wrappedBytes
854
1161
  );
855
- if (fieldsToEncrypt.length === 0) {
856
- return data;
857
- }
858
- if (!options?._warmed) {
859
- await this.warmupKey(options);
860
- }
861
- const result = { ...data };
862
- const entries = await Promise.all(
863
- fieldsToEncrypt.map(async (field) => {
864
- const fieldOptions = this._bindFieldAad(data, field, options);
865
- return [field, await this.encryptValue(result[field], fieldOptions)];
866
- })
1162
+ } finally {
1163
+ wrappedBytes.fill(0);
1164
+ }
1165
+ try {
1166
+ return await subtle.importKey(
1167
+ "raw",
1168
+ unwrappedRawKey,
1169
+ unwrappedKeyAlgorithm,
1170
+ extractable,
1171
+ ["encrypt", "decrypt"]
867
1172
  );
868
- for (const [field, encryptedVal] of entries) {
869
- safeSetProperty(result, field, encryptedVal);
870
- }
871
- return result;
1173
+ } finally {
1174
+ new Uint8Array(unwrappedRawKey).fill(0);
1175
+ }
1176
+ }
1177
+ throw new Error(
1178
+ `[CKeyManager] Unsupported user key algorithm for unwrapAccountKeyForUser: '${userPrivateKey.algorithm.name}'. Expected 'RSA-OAEP'.`
1179
+ );
1180
+ }
1181
+ /**
1182
+ * Wraps an HMAC blind indexing key for a specific recipient user using their RSA-OAEP public key.
1183
+ * Returns a Base64-encoded wrapped key string suitable for storage in role-map / access tables.
1184
+ */
1185
+ static async wrapHMACKeyForUser(hmacKey, userPublicKey) {
1186
+ const subtle = getSubtleCrypto();
1187
+ if (userPublicKey.algorithm.name === "RSA-OAEP") {
1188
+ const rawKey = await subtle.exportKey("raw", hmacKey);
1189
+ try {
1190
+ const wrappedBuffer = await subtle.encrypt(
1191
+ { name: "RSA-OAEP" },
1192
+ userPublicKey,
1193
+ rawKey
1194
+ );
1195
+ return CKeyManager.toBase64(wrappedBuffer);
1196
+ } finally {
1197
+ new Uint8Array(rawKey).fill(0);
1198
+ }
1199
+ }
1200
+ throw new Error(
1201
+ `[CKeyManager] Unsupported user key algorithm for wrapHMACKeyForUser: '${userPublicKey.algorithm.name}'. Expected 'RSA-OAEP'.`
1202
+ );
1203
+ }
1204
+ /**
1205
+ * Unwraps a Base64-encoded HMAC key using a recipient's RSA-OAEP private key.
1206
+ * Restores the CryptoKey configured for HMAC signing and verification in search indexing.
1207
+ */
1208
+ static async unwrapHMACKeyForUser(wrappedKeyBase64, userPrivateKey, hash = "SHA-256", extractable = false) {
1209
+ const subtle = getSubtleCrypto();
1210
+ if (userPrivateKey.algorithm.name === "RSA-OAEP") {
1211
+ const wrappedBytes = CKeyManager.fromBase64(wrappedKeyBase64);
1212
+ let unwrappedRawKey;
1213
+ try {
1214
+ unwrappedRawKey = await subtle.decrypt(
1215
+ { name: "RSA-OAEP" },
1216
+ userPrivateKey,
1217
+ wrappedBytes
1218
+ );
1219
+ } finally {
1220
+ wrappedBytes.fill(0);
1221
+ }
1222
+ try {
1223
+ return await subtle.importKey(
1224
+ "raw",
1225
+ unwrappedRawKey,
1226
+ { name: "HMAC", hash: { name: hash } },
1227
+ extractable,
1228
+ ["sign", "verify"]
1229
+ );
1230
+ } finally {
1231
+ new Uint8Array(unwrappedRawKey).fill(0);
1232
+ }
1233
+ }
1234
+ throw new Error(
1235
+ `[CKeyManager] Unsupported user key algorithm for unwrapHMACKeyForUser: '${userPrivateKey.algorithm.name}'. Expected 'RSA-OAEP'.`
1236
+ );
1237
+ }
1238
+ /**
1239
+ * Atomically re-wraps an Account Data Encryption Key (ADEK) for a new recipient
1240
+ * without exposing the raw extractable symmetric key to caller application code.
1241
+ *
1242
+ * @param wrappedKeyBase64 - The existing ADEK envelope wrapped with current user's public key
1243
+ * @param myPrivateKey - Current user's RSA-OAEP private key
1244
+ * @param recipientPublicKey - New recipient's RSA-OAEP public key
1245
+ * @returns Base64-encoded wrapped key envelope for the recipient
1246
+ */
1247
+ static async rewrapAccountKeyForUser(wrappedKeyBase64, myPrivateKey, recipientPublicKey) {
1248
+ const subtle = getSubtleCrypto();
1249
+ if (!myPrivateKey || !myPrivateKey.algorithm || myPrivateKey.algorithm.name !== "RSA-OAEP" || !recipientPublicKey || !recipientPublicKey.algorithm || recipientPublicKey.algorithm.name !== "RSA-OAEP") {
1250
+ throw new Error(`[CKeyManager] rewrapAccountKeyForUser requires RSA-OAEP keys.`);
1251
+ }
1252
+ if (myPrivateKey.type === "public" || recipientPublicKey.type === "private") {
1253
+ throw new Error(
1254
+ `[CKeyManager] rewrapAccountKeyForUser requires an RSA-OAEP private key for decryption and an RSA-OAEP public key for encryption.`
1255
+ );
1256
+ }
1257
+ const wrappedBytes = CKeyManager.fromBase64(wrappedKeyBase64);
1258
+ let rawKeyBuffer;
1259
+ try {
1260
+ rawKeyBuffer = await subtle.decrypt(
1261
+ { name: "RSA-OAEP" },
1262
+ myPrivateKey,
1263
+ wrappedBytes
1264
+ );
1265
+ const rewrappedBuffer = await subtle.encrypt(
1266
+ { name: "RSA-OAEP" },
1267
+ recipientPublicKey,
1268
+ rawKeyBuffer
1269
+ );
1270
+ return CKeyManager.toBase64(rewrappedBuffer);
1271
+ } finally {
1272
+ wrappedBytes.fill(0);
1273
+ if (rawKeyBuffer) {
1274
+ new Uint8Array(rawKeyBuffer).fill(0);
1275
+ }
1276
+ }
1277
+ }
1278
+ // ── Key Derivation Functions (PBKDF2 & HKDF) ──────────────────────────────
1279
+ /**
1280
+ * Derives a symmetric AES-GCM CryptoKey from a passphrase using PBKDF2 or HKDF.
1281
+ * Returns the derived CryptoKey and the base64-encoded salt.
1282
+ */
1283
+ static async deriveKey(passphrase, options) {
1284
+ if (!passphrase || typeof passphrase === "string" && passphrase.trim().length === 0 || typeof passphrase === "object" && "byteLength" in passphrase && passphrase.byteLength === 0) {
1285
+ throw new Error("[CKeyManager] Passphrase cannot be empty or undefined for key derivation.");
1286
+ }
1287
+ const subtle = getSubtleCrypto();
1288
+ const saltBytes = CKeyManager.resolveSalt(options.salt);
1289
+ const keyLength = options.keyLength || 256;
1290
+ const hash = options.hash || "SHA-256";
1291
+ const extractable = options.extractable === true;
1292
+ const rawKeyBytes = typeof passphrase === "string" ? textEncoder$1.encode(passphrase) : passphrase instanceof Uint8Array ? passphrase : new Uint8Array(passphrase);
1293
+ try {
1294
+ if (options.kdf === "pbkdf2") {
1295
+ const baseKey = await subtle.importKey(
1296
+ "raw",
1297
+ rawKeyBytes,
1298
+ "PBKDF2",
1299
+ false,
1300
+ ["deriveKey"]
1301
+ );
1302
+ const derivedKey = await subtle.deriveKey(
1303
+ {
1304
+ name: "PBKDF2",
1305
+ salt: saltBytes,
1306
+ iterations: options.iterations || 31e4,
1307
+ hash
1308
+ },
1309
+ baseKey,
1310
+ { name: "AES-GCM", length: keyLength },
1311
+ extractable,
1312
+ ["encrypt", "decrypt"]
1313
+ );
1314
+ return {
1315
+ key: derivedKey,
1316
+ salt: CKeyManager.toBase64(saltBytes)
1317
+ };
1318
+ }
1319
+ if (options.kdf === "hkdf") {
1320
+ const baseKey = await subtle.importKey(
1321
+ "raw",
1322
+ rawKeyBytes,
1323
+ "HKDF",
1324
+ false,
1325
+ ["deriveKey"]
1326
+ );
1327
+ const infoBytes = resolveInfoBytes(options.info, "katlux-derived-aes-key");
1328
+ const derivedKey = await subtle.deriveKey(
1329
+ {
1330
+ name: "HKDF",
1331
+ salt: saltBytes,
1332
+ info: infoBytes,
1333
+ hash
1334
+ },
1335
+ baseKey,
1336
+ { name: "AES-GCM", length: keyLength },
1337
+ extractable,
1338
+ ["encrypt", "decrypt"]
1339
+ );
1340
+ return {
1341
+ key: derivedKey,
1342
+ salt: CKeyManager.toBase64(saltBytes)
1343
+ };
1344
+ }
1345
+ throw new Error(`[CKeyManager] Unsupported KDF strategy: ${options.kdf}`);
1346
+ } finally {
1347
+ if (typeof passphrase === "string") {
1348
+ rawKeyBytes.fill(0);
1349
+ }
1350
+ }
1351
+ }
1352
+ /**
1353
+ * Derives an AES-GCM CryptoKey from a high-entropy secret (e.g. an ECDH shared secret key)
1354
+ * using HKDF.
1355
+ */
1356
+ static async deriveKeyFromSecret(secretKey, options = {}) {
1357
+ const subtle = getSubtleCrypto();
1358
+ let saltBytes;
1359
+ if (options.salt !== void 0) {
1360
+ saltBytes = CKeyManager.resolveSalt(options.salt);
1361
+ } else {
1362
+ saltBytes = new Uint8Array(16);
1363
+ }
1364
+ const infoBytes = resolveInfoBytes(options.info, "katlux-ecdh-aes-gcm");
1365
+ const hash = options.hash || "SHA-256";
1366
+ const keyLength = options.keyLength || 256;
1367
+ const extractable = options.extractable === true;
1368
+ return await subtle.deriveKey(
1369
+ {
1370
+ name: "HKDF",
1371
+ salt: saltBytes,
1372
+ info: infoBytes,
1373
+ hash
1374
+ },
1375
+ secretKey,
1376
+ { name: "AES-GCM", length: keyLength },
1377
+ extractable,
1378
+ ["encrypt", "decrypt"]
1379
+ );
1380
+ }
1381
+ /**
1382
+ * Derives two domain-separated, cryptographically isolated keys from a user password and salt:
1383
+ * 1. `authKey`: For client authentication, SRP, or server challenge proofs.
1384
+ * 2. `encryptionKey`: For AES-GCM local and field-level data encryption.
1385
+ *
1386
+ * Stage 1 (Password Stretching): PBKDF2-HMAC-SHA256 stretches low-entropy password into master secret.
1387
+ * Stage 2 (Domain Expansion): HKDF-Expand derives subkeys using domain-separated context info tags.
1388
+ * Immediate memory zeroization of intermediate master secret buffer is guaranteed.
1389
+ *
1390
+ * @param password - Master user password or passphrase
1391
+ * @param salt - Salt (or omitted to generate a secure random 16-byte salt)
1392
+ * @param options - PBKDF2 and HKDF configuration options
1393
+ */
1394
+ static async deriveAuthAndEncryptionKeys(password, salt, options = {}) {
1395
+ if (!password || typeof password === "string" && password.trim().length === 0 || typeof password === "object" && "byteLength" in password && password.byteLength === 0) {
1396
+ throw new Error("[CKeyManager] Password cannot be empty or undefined for key derivation.");
1397
+ }
1398
+ const subtle = getSubtleCrypto();
1399
+ let saltBytes;
1400
+ if (salt !== void 0) {
1401
+ saltBytes = CKeyManager.resolveSalt(salt);
1402
+ } else {
1403
+ saltBytes = CKeyManager.randomBytes(16);
1404
+ }
1405
+ const saltBase64 = CKeyManager.toBase64(saltBytes);
1406
+ const iterations = options.iterations || 31e4;
1407
+ const hash = options.hash || "SHA-256";
1408
+ const authKeyLength = options.authKeyLength || 256;
1409
+ const encryptionKeyLength = options.encryptionKeyLength || 256;
1410
+ const authAlgorithm = options.authAlgorithm || "HMAC";
1411
+ const authInfo = resolveInfoBytes(options.authInfo, "katlux-auth-key-v1");
1412
+ const encryptionInfo = resolveInfoBytes(options.encryptionInfo, "katlux-encryption-key-v1");
1413
+ const extractable = options.extractable === true;
1414
+ const rawPasswordBytes = typeof password === "string" ? textEncoder$1.encode(password) : password instanceof Uint8Array ? new Uint8Array(password.buffer, password.byteOffset, password.byteLength) : new Uint8Array(password);
1415
+ let masterBits;
1416
+ try {
1417
+ const baseKey = await subtle.importKey(
1418
+ "raw",
1419
+ rawPasswordBytes,
1420
+ "PBKDF2",
1421
+ false,
1422
+ ["deriveBits"]
1423
+ );
1424
+ const bitsLength = hash === "SHA-512" ? 512 : 256;
1425
+ masterBits = await subtle.deriveBits(
1426
+ {
1427
+ name: "PBKDF2",
1428
+ salt: saltBytes,
1429
+ iterations,
1430
+ hash
1431
+ },
1432
+ baseKey,
1433
+ bitsLength
1434
+ );
1435
+ const hkdfKey = await subtle.importKey(
1436
+ "raw",
1437
+ masterBits,
1438
+ "HKDF",
1439
+ false,
1440
+ ["deriveKey"]
1441
+ );
1442
+ const authDerivedAlgorithm = authAlgorithm === "AES-GCM" ? { name: "AES-GCM", length: authKeyLength } : { name: "HMAC", hash: { name: hash }, length: authKeyLength };
1443
+ const authUsages = authAlgorithm === "AES-GCM" ? ["encrypt", "decrypt"] : ["sign", "verify"];
1444
+ const authKey = await subtle.deriveKey(
1445
+ {
1446
+ name: "HKDF",
1447
+ hash,
1448
+ salt: new Uint8Array(0),
1449
+ info: authInfo
1450
+ },
1451
+ hkdfKey,
1452
+ authDerivedAlgorithm,
1453
+ extractable,
1454
+ authUsages
1455
+ );
1456
+ const encryptionKey = await subtle.deriveKey(
1457
+ {
1458
+ name: "HKDF",
1459
+ hash,
1460
+ salt: new Uint8Array(0),
1461
+ info: encryptionInfo
1462
+ },
1463
+ hkdfKey,
1464
+ { name: "AES-GCM", length: encryptionKeyLength },
1465
+ extractable,
1466
+ ["encrypt", "decrypt"]
1467
+ );
1468
+ return {
1469
+ authKey,
1470
+ encryptionKey,
1471
+ salt: saltBase64
1472
+ };
1473
+ } finally {
1474
+ if (typeof password === "string") {
1475
+ rawPasswordBytes.fill(0);
872
1476
  }
873
- if (options?.valueEncryption === true) {
874
- const result = { ...data };
875
- const keysToEncrypt = Object.keys(result).filter(
876
- (key) => result[key] !== void 0 && result[key] !== null && !this.isEncrypted(result[key])
877
- );
878
- if (keysToEncrypt.length === 0) {
879
- return result;
880
- }
881
- if (!options?._warmed) {
882
- await this.warmupKey(options);
883
- }
884
- const entries = await Promise.all(
885
- keysToEncrypt.map(async (key) => {
886
- const fieldOptions = this._bindFieldAad(data, key, options);
887
- return [key, await this.encryptValue(result[key], fieldOptions)];
888
- })
889
- );
890
- for (const [key, encryptedVal] of entries) {
891
- safeSetProperty(result, key, encryptedVal);
892
- }
893
- return result;
1477
+ if (masterBits) {
1478
+ new Uint8Array(masterBits).fill(0);
894
1479
  }
895
1480
  }
896
- return this.encryptValue(data, options);
897
1481
  }
1482
+ // ── Digital Signatures, Fingerprints & Access Grants ─────────────────────
898
1483
  /**
899
- * Top-level polymorphic decryption method.
900
- * Automatically performs value-level decryption:
901
- * - If payload is an encrypted envelope: decrypts directly via decryptValue.
902
- * - If payload is an array of objects: decrypts each row.
903
- * - If payload is an object: decrypts any encrypted value cells across fields.
1484
+ * Generates an ECDSA P-256 signing key pair.
904
1485
  */
1486
+ static async generateSigningKeyPair(curve = "P-256", extractable = true) {
1487
+ const subtle = getSubtleCrypto();
1488
+ const pair = await subtle.generateKey(
1489
+ {
1490
+ name: "ECDSA",
1491
+ namedCurve: curve
1492
+ },
1493
+ extractable,
1494
+ ["sign", "verify"]
1495
+ );
1496
+ return {
1497
+ publicKey: pair.publicKey,
1498
+ privateKey: pair.privateKey
1499
+ };
1500
+ }
905
1501
  /**
906
- * Top-level polymorphic decryption method.
907
- * Automatically performs value-level decryption:
908
- * - If payload is an encrypted envelope: decrypts directly via decryptValue.
909
- * - If payload is an array of objects: decrypts each row (batched with local memoization).
910
- * - If payload is an object: decrypts any encrypted value cells across fields.
1502
+ * Generates an ECDSA signing key pair. Alias for generateSigningKeyPair.
911
1503
  */
912
- async decrypt(payload, options) {
913
- if (payload === void 0 || payload === null) return payload;
914
- if (this.isEncrypted(payload)) {
915
- return this.decryptValue(payload, options);
916
- }
917
- if (Array.isArray(payload)) {
918
- if (payload.length === 0) return payload;
919
- const localMemo = /* @__PURE__ */ new Map();
920
- const decryptRow = async (item) => {
921
- if (!item) return item;
922
- if (this.isEncrypted(item)) {
923
- const ct = item.ciphertext;
924
- const iv = item.iv || "";
925
- const aadPart = options?.aad ? typeof options.aad === "string" ? options.aad : "" : "";
926
- const memoKey = `${ct}:${iv}:${aadPart}`;
927
- if (!localMemo.has(memoKey)) {
928
- localMemo.set(memoKey, this.decryptValue(item, options));
929
- }
930
- return localMemo.get(memoKey);
931
- }
932
- if (typeof item === "object" && !ArrayBuffer.isView(item) && !(item instanceof ArrayBuffer)) {
933
- const targetKeys = options?.fields && options.fields.length > 0 ? options.fields.filter((f) => Object.prototype.hasOwnProperty.call(item, f)) : Object.keys(item);
934
- const keysToDecrypt = targetKeys.filter((key) => this.isEncrypted(item[key]));
935
- if (keysToDecrypt.length === 0) {
936
- return item;
937
- }
938
- const proto = Object.getPrototypeOf(item);
939
- const result = proto && proto !== Object.prototype ? Object.assign(Object.create(proto), item) : { ...item };
940
- await Promise.all(
941
- keysToDecrypt.map(async (key) => {
942
- const env = item[key];
943
- const boundOptions = this._bindFieldAad(item, key, options);
944
- const hasBoundAad = boundOptions.aad !== options?.aad;
945
- const ct = env.ciphertext;
946
- const iv = env.iv || "";
947
- const aadPart = boundOptions.aad ? typeof boundOptions.aad === "string" ? boundOptions.aad : "" : "";
948
- const memoKey = `${ct}:${iv}:${aadPart}`;
949
- if (!localMemo.has(memoKey)) {
950
- const decryptPromise = this.decryptValue(env, boundOptions).catch((err) => {
951
- if (hasBoundAad && isAuthOrDecryptionError(err)) {
952
- return this.decryptValue(env, options);
953
- }
954
- throw err;
955
- });
956
- localMemo.set(memoKey, decryptPromise);
957
- }
958
- const decryptedVal = await localMemo.get(memoKey);
959
- safeSetProperty(result, key, decryptedVal);
960
- })
961
- );
962
- return result;
963
- }
964
- return item;
965
- };
966
- if (payload.length <= 100) {
967
- return Promise.all(payload.map(decryptRow));
968
- }
969
- const batchSize = 1e3;
970
- const results = new Array(payload.length);
971
- for (let i = 0; i < payload.length; i += batchSize) {
972
- const end = Math.min(i + batchSize, payload.length);
973
- const chunk = payload.slice(i, end);
974
- const chunkRes = await Promise.all(chunk.map(decryptRow));
975
- for (let j = 0; j < chunkRes.length; j++) {
976
- results[i + j] = chunkRes[j];
977
- }
978
- if (end < payload.length && (i > 0 && i % 5e3 === 0)) {
979
- await new Promise((r) => setTimeout(r, 0));
1504
+ static async generateECDSAKeyPair(curve = "P-256", extractable = true) {
1505
+ return CKeyManager.generateSigningKeyPair(curve, extractable);
1506
+ }
1507
+ /**
1508
+ * Generates a deterministic SHA-256 fingerprint for a public key (SPKI or raw).
1509
+ * Formatted as `SHA256:<base64url>`.
1510
+ */
1511
+ static async getKeyFingerprint(publicKey) {
1512
+ const subtle = getSubtleCrypto();
1513
+ let rawBytes;
1514
+ if (typeof publicKey === "object" && publicKey !== null && "algorithm" in publicKey) {
1515
+ const cryptoKey = publicKey;
1516
+ rawBytes = await subtle.exportKey("spki", cryptoKey);
1517
+ } else if (publicKey instanceof Uint8Array) {
1518
+ rawBytes = publicKey;
1519
+ } else if (publicKey instanceof ArrayBuffer) {
1520
+ rawBytes = publicKey;
1521
+ } else if (typeof publicKey === "string") {
1522
+ const trimmed = publicKey.trim();
1523
+ if (trimmed.startsWith("-----BEGIN")) {
1524
+ const b642 = trimmed.replace(/-----[^-]+-----/g, "").replace(/\s+/g, "");
1525
+ rawBytes = CKeyManager.fromBase64(b642);
1526
+ } else {
1527
+ try {
1528
+ rawBytes = CKeyManager.fromBase64(trimmed);
1529
+ } catch {
1530
+ rawBytes = textEncoder$1.encode(trimmed);
980
1531
  }
981
1532
  }
982
- return results;
983
- }
984
- if (typeof payload === "object" && !ArrayBuffer.isView(payload) && !(payload instanceof ArrayBuffer)) {
985
- const targetKeys = options?.fields && options.fields.length > 0 ? options.fields.filter((f) => Object.prototype.hasOwnProperty.call(payload, f)) : Object.keys(payload);
986
- const keysToDecrypt = targetKeys.filter((key) => this.isEncrypted(payload[key]));
987
- if (keysToDecrypt.length === 0) {
988
- return payload;
989
- }
990
- const proto = Object.getPrototypeOf(payload);
991
- const result = proto && proto !== Object.prototype ? Object.assign(Object.create(proto), payload) : { ...payload };
992
- const entries = await Promise.all(
993
- keysToDecrypt.map(async (key) => {
994
- const boundOptions = this._bindFieldAad(payload, key, options);
995
- const hasBoundAad = boundOptions.aad !== options?.aad;
996
- try {
997
- return [key, await this.decryptValue(result[key], boundOptions)];
998
- } catch (err) {
999
- if (hasBoundAad && isAuthOrDecryptionError(err)) {
1000
- try {
1001
- return [key, await this.decryptValue(result[key], options)];
1002
- } catch (fallbackErr) {
1003
- throw fallbackErr;
1004
- }
1005
- }
1006
- throw err;
1007
- }
1008
- })
1533
+ } else {
1534
+ throw new Error(
1535
+ "[CKeyManager] Invalid key input for getKeyFingerprint: expected CryptoKey, Uint8Array, ArrayBuffer, or string."
1009
1536
  );
1010
- for (const [key, decryptedVal] of entries) {
1011
- safeSetProperty(result, key, decryptedVal);
1012
- }
1013
- return result;
1014
1537
  }
1015
- return payload;
1538
+ const hashBuffer = await subtle.digest("SHA-256", rawBytes);
1539
+ const b64 = CKeyManager.toBase64(hashBuffer);
1540
+ const b64url = b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1541
+ return `SHA256:${b64url}`;
1016
1542
  }
1017
- }
1018
-
1019
- class CKeyManager {
1020
- // ── Binary & Encoding Utilities ──────────────────────────────────────────
1021
1543
  /**
1022
- * Generates a cryptographically secure random byte buffer of the specified length.
1544
+ * Signs arbitrary data or JSON objects using an ECDSA P-256 private key.
1545
+ * Objects are automatically serialized via canonicalizeJson() before hashing.
1023
1546
  */
1024
- static randomBytes(length) {
1025
- const bytes = new Uint8Array(length);
1026
- return getRandomValues(bytes);
1547
+ static async sign(data, privateKey) {
1548
+ const subtle = getSubtleCrypto();
1549
+ if (!privateKey || !privateKey.algorithm || privateKey.algorithm.name !== "ECDSA") {
1550
+ throw new Error("[CKeyManager] sign requires an ECDSA private key.");
1551
+ }
1552
+ let dataBytes;
1553
+ if (data instanceof Uint8Array) {
1554
+ dataBytes = data;
1555
+ } else if (ArrayBuffer.isView(data)) {
1556
+ dataBytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
1557
+ } else if (data instanceof ArrayBuffer) {
1558
+ dataBytes = new Uint8Array(data);
1559
+ } else if (typeof data === "string") {
1560
+ dataBytes = textEncoder$1.encode(data);
1561
+ } else {
1562
+ const canonical = canonicalizeJson(data);
1563
+ dataBytes = textEncoder$1.encode(canonical);
1564
+ }
1565
+ const namedCurve = privateKey.algorithm.namedCurve || "P-256";
1566
+ const hashName = namedCurve === "P-384" ? "SHA-384" : namedCurve === "P-521" ? "SHA-512" : "SHA-256";
1567
+ const signatureBuffer = await subtle.sign(
1568
+ {
1569
+ name: "ECDSA",
1570
+ hash: { name: hashName }
1571
+ },
1572
+ privateKey,
1573
+ dataBytes
1574
+ );
1575
+ return CKeyManager.toBase64(signatureBuffer);
1027
1576
  }
1028
1577
  /**
1029
- * Converts an ArrayBuffer or Uint8Array into a Base64-encoded string.
1578
+ * Verifies an ECDSA P-256 signature against original data or JSON object.
1030
1579
  */
1031
- static toBase64(buffer) {
1032
- return toBase64(buffer);
1580
+ static async verify(data, signature, publicKey) {
1581
+ const subtle = getSubtleCrypto();
1582
+ if (!publicKey || !publicKey.algorithm || publicKey.algorithm.name !== "ECDSA") {
1583
+ throw new Error("[CKeyManager] verify requires an ECDSA public key.");
1584
+ }
1585
+ if (typeof signature !== "string" || !signature.trim()) {
1586
+ return false;
1587
+ }
1588
+ try {
1589
+ const signatureBytes = CKeyManager.fromBase64(signature);
1590
+ let dataBytes;
1591
+ if (data instanceof Uint8Array) {
1592
+ dataBytes = data;
1593
+ } else if (ArrayBuffer.isView(data)) {
1594
+ dataBytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
1595
+ } else if (data instanceof ArrayBuffer) {
1596
+ dataBytes = new Uint8Array(data);
1597
+ } else if (typeof data === "string") {
1598
+ dataBytes = textEncoder$1.encode(data);
1599
+ } else {
1600
+ const canonical = canonicalizeJson(data);
1601
+ dataBytes = textEncoder$1.encode(canonical);
1602
+ }
1603
+ const namedCurve = publicKey.algorithm.namedCurve || "P-256";
1604
+ const hashName = namedCurve === "P-384" ? "SHA-384" : namedCurve === "P-521" ? "SHA-512" : "SHA-256";
1605
+ return await subtle.verify(
1606
+ {
1607
+ name: "ECDSA",
1608
+ hash: { name: hashName }
1609
+ },
1610
+ publicKey,
1611
+ signatureBytes,
1612
+ dataBytes
1613
+ );
1614
+ } catch {
1615
+ return false;
1616
+ }
1033
1617
  }
1034
1618
  /**
1035
- * Converts a Base64 or Base64URL-encoded string back into a Uint8Array.
1036
- * Normalizes URL-safe characters and padding, strictly validating against the Base64 alphabet.
1619
+ * Computes a deterministic HMAC-SHA256 blind index hash for an arbitrary term using the supplied CryptoKey.
1620
+ * Performs Unicode NFKC normalization, trimming, and lowercasing before computing the signature.
1621
+ *
1622
+ * @param value - The input string, number, or object to compute the blind index for.
1623
+ * @param hmacKey - CryptoKey configured for HMAC signing.
1624
+ * @returns Lowercase hexadecimal HMAC-SHA256 blind index hash.
1037
1625
  */
1038
- static fromBase64(base64) {
1039
- if (typeof base64 !== "string") {
1040
- throw new Error("[CKeyManager] Invalid base64 input: expected string");
1041
- }
1042
- const cleaned = base64.replace(/\s+/g, "");
1043
- if (!cleaned) return new Uint8Array(0);
1044
- const normalized = cleaned.replace(/-/g, "+").replace(/_/g, "/");
1045
- const padded = normalized.padEnd(normalized.length + (4 - normalized.length % 4) % 4, "=");
1046
- if (!/^[A-Za-z0-9+/]+={0,2}$/.test(padded)) {
1047
- throw new Error("[CKeyManager] Invalid base64 string");
1626
+ static async computeBlindIndex(value, hmacKey) {
1627
+ if (value === null || value === void 0) return "";
1628
+ const normalized = String(value).normalize("NFKC").trim().toLowerCase();
1629
+ if (!normalized) return "";
1630
+ if (!hmacKey || !hmacKey.algorithm || hmacKey.algorithm.name !== "HMAC") {
1631
+ throw new Error("[CKeyManager] computeBlindIndex requires an HMAC CryptoKey.");
1048
1632
  }
1049
- if (typeof Buffer !== "undefined") {
1050
- const buf = Buffer.from(padded, "base64");
1051
- const arrayBuf = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
1052
- return new Uint8Array(arrayBuf);
1633
+ const subtle = getSubtleCrypto();
1634
+ const signature = await subtle.sign("HMAC", hmacKey, textEncoder$1.encode(normalized));
1635
+ return Array.from(new Uint8Array(signature)).map((b) => b.toString(16).padStart(2, "0")).join("");
1636
+ }
1637
+ /**
1638
+ * Signs a structured access grant, embedding signature and granter key fingerprint.
1639
+ */
1640
+ static async signGrant(grant, granterPrivateKey, granterPublicKey) {
1641
+ if (!grant || typeof grant !== "object") {
1642
+ throw new Error("[CKeyManager] signGrant requires a valid grant object.");
1053
1643
  }
1054
- const binary = atob(padded);
1055
- const bytes = new Uint8Array(binary.length);
1056
- for (let i = 0; i < binary.length; i++) {
1057
- bytes[i] = binary.charCodeAt(i);
1644
+ let fingerprint;
1645
+ if (granterPublicKey) {
1646
+ fingerprint = await CKeyManager.getKeyFingerprint(granterPublicKey);
1647
+ } else {
1648
+ const subtle = getSubtleCrypto();
1649
+ try {
1650
+ const jwk = await subtle.exportKey("jwk", granterPrivateKey);
1651
+ const pubJwk = {
1652
+ kty: jwk.kty,
1653
+ crv: jwk.crv,
1654
+ x: jwk.x,
1655
+ y: jwk.y,
1656
+ ext: true,
1657
+ key_ops: ["verify"]
1658
+ };
1659
+ const pubKey = await subtle.importKey(
1660
+ "jwk",
1661
+ pubJwk,
1662
+ { name: "ECDSA", namedCurve: jwk.crv || "P-256" },
1663
+ true,
1664
+ ["verify"]
1665
+ );
1666
+ fingerprint = await CKeyManager.getKeyFingerprint(pubKey);
1667
+ } catch {
1668
+ throw new Error(
1669
+ "[CKeyManager] signGrant requires granterPublicKey when granterPrivateKey is non-extractable."
1670
+ );
1671
+ }
1058
1672
  }
1059
- return bytes;
1673
+ const signature = await CKeyManager.sign(grant, granterPrivateKey);
1674
+ return {
1675
+ grant: { ...grant },
1676
+ signature,
1677
+ fingerprint
1678
+ };
1060
1679
  }
1061
1680
  /**
1062
- * Deterministically resolves a salt from Uint8Array, ArrayBuffer, Base64/Base64URL string, or raw text string.
1063
- * Guarantees 100% identical salt byte resolution between Node.js and Browser environments.
1681
+ * Verifies the authenticity and integrity of a signed access grant against granter public key.
1064
1682
  */
1065
- static resolveSalt(salt) {
1066
- if (!salt) return CKeyManager.randomBytes(16);
1067
- if (ArrayBuffer.isView(salt)) {
1068
- return salt.byteLength > 0 ? new Uint8Array(salt.buffer.slice(salt.byteOffset, salt.byteOffset + salt.byteLength)) : CKeyManager.randomBytes(16);
1683
+ static async verifyGrant(signedGrant, granterPublicKey, options) {
1684
+ if (!signedGrant || !signedGrant.grant || !signedGrant.signature || !signedGrant.fingerprint || !granterPublicKey) {
1685
+ return false;
1069
1686
  }
1070
- if (salt instanceof ArrayBuffer) {
1071
- return salt.byteLength > 0 ? new Uint8Array(salt.slice(0)) : CKeyManager.randomBytes(16);
1687
+ const expectedFingerprint = await CKeyManager.getKeyFingerprint(granterPublicKey);
1688
+ if (signedGrant.fingerprint !== expectedFingerprint) {
1689
+ return false;
1072
1690
  }
1073
- if (typeof salt === "string") {
1074
- const trimmed = salt.trim();
1075
- if (!trimmed) return CKeyManager.randomBytes(16);
1076
- if (trimmed.startsWith("base64:")) {
1077
- return CKeyManager.fromBase64(trimmed.slice(7));
1078
- }
1079
- if (trimmed.endsWith("=") && /^[A-Za-z0-9+/]+={1,2}$/.test(trimmed)) {
1080
- try {
1081
- const decoded = CKeyManager.fromBase64(trimmed);
1082
- if (decoded.byteLength > 0) return decoded;
1083
- } catch {
1084
- }
1691
+ if (options?.checkExpiry && signedGrant.grant.expiresAt !== void 0) {
1692
+ const now = options.now ?? Date.now();
1693
+ if (now > signedGrant.grant.expiresAt) {
1694
+ return false;
1085
1695
  }
1086
- const encoded = textEncoder$1.encode(trimmed);
1087
- return encoded.byteLength > 0 ? encoded : CKeyManager.randomBytes(16);
1088
1696
  }
1089
- return CKeyManager.randomBytes(16);
1697
+ return await CKeyManager.verify(signedGrant.grant, signedGrant.signature, granterPublicKey);
1698
+ }
1699
+ }
1700
+
1701
+ class AEncryptionProvider {
1702
+ defaultKey;
1703
+ _hmacKey;
1704
+ keys;
1705
+ activeKid;
1706
+ allowedKids;
1707
+ constructor(key, options) {
1708
+ if (key !== void 0) {
1709
+ this.defaultKey = key;
1710
+ }
1711
+ if (options?.keys) {
1712
+ this.keys = options.keys;
1713
+ }
1714
+ if (options?.activeKid) {
1715
+ this.activeKid = options.activeKid;
1716
+ }
1717
+ if (options?.allowedKids) {
1718
+ this.allowedKids = options.allowedKids;
1719
+ }
1720
+ if (options?.hmacKey) {
1721
+ this._hmacKey = options.hmacKey;
1722
+ }
1723
+ }
1724
+ /**
1725
+ * Sets or updates the default secret key, CryptoKey, or async key provider function.
1726
+ */
1727
+ setDefaultKey(key) {
1728
+ this.defaultKey = key || void 0;
1729
+ }
1730
+ /**
1731
+ * Retrieves the currently configured default key or key provider callback.
1732
+ */
1733
+ getDefaultKey() {
1734
+ return this.defaultKey;
1735
+ }
1736
+ /**
1737
+ * Configures a keyring dictionary of keys with an optional active key identifier.
1738
+ */
1739
+ setKeyring(keys, activeKid) {
1740
+ this.keys = keys;
1741
+ if (activeKid !== void 0) {
1742
+ this.activeKid = activeKid;
1743
+ }
1744
+ }
1745
+ /**
1746
+ * Retrieves the currently configured keyring dictionary.
1747
+ */
1748
+ getKeyring() {
1749
+ return this.keys;
1750
+ }
1751
+ /**
1752
+ * Sets the active Key Identifier (kid) to use for outbound encryption.
1753
+ */
1754
+ setActiveKid(kid) {
1755
+ this.activeKid = kid;
1756
+ }
1757
+ /**
1758
+ * Retrieves the active Key Identifier (kid).
1759
+ */
1760
+ getActiveKid() {
1761
+ return this.activeKid;
1762
+ }
1763
+ /**
1764
+ * Sets or updates the dedicated HMAC key or key provider callback for blind indexing.
1765
+ */
1766
+ setHmacKey(key) {
1767
+ this._hmacKey = key || void 0;
1768
+ }
1769
+ /**
1770
+ * Retrieves the currently configured HMAC key or key provider callback.
1771
+ */
1772
+ getHmacKey() {
1773
+ return this._hmacKey;
1090
1774
  }
1091
- // ── Symmetric Key Management ─────────────────────────────────────────────
1092
1775
  /**
1093
- * Generates a random AES CryptoKey suitable for AES-GCM or AES-CBC.
1094
- *
1095
- * E2EE Threat Model Note: Focuses on protecting GDPR/tenant data against database and
1096
- * server-side leaks. Keys remain extractable by default to enable role-based key sharing,
1097
- * wrapping, and transfer between authorized client sessions in multi-user SaaS apps.
1098
- * Client-side malware/trojan protection is out of scope.
1776
+ * Computes a deterministic HMAC-SHA256 blind index hash for an arbitrary term.
1777
+ * If no hmacKey is explicitly provided, uses the provider's configured hmacKey or default key.
1099
1778
  */
1100
- static async generateAESKey(length = 256, algorithm = "AES-GCM", extractable = true) {
1779
+ async computeBlindIndex(value, hmacKey) {
1780
+ if (value === null || value === void 0) return "";
1781
+ const keyMaterial = hmacKey || this._hmacKey || this.defaultKey;
1782
+ if (!keyMaterial) {
1783
+ throw new Error("[AEncryptionProvider] computeBlindIndex requires an HMAC key. Configure hmacKey or pass a key directly.");
1784
+ }
1785
+ const resolvedKey = typeof keyMaterial === "function" ? await keyMaterial() : keyMaterial;
1786
+ if (resolvedKey && typeof resolvedKey !== "string" && resolvedKey.algorithm) {
1787
+ return CKeyManager.computeBlindIndex(value, resolvedKey);
1788
+ }
1101
1789
  const subtle = getSubtleCrypto();
1102
- const usages = algorithm === "AES-KW" ? ["wrapKey", "unwrapKey"] : ["encrypt", "decrypt"];
1103
- return await subtle.generateKey(
1104
- { name: algorithm, length },
1105
- extractable,
1106
- usages
1790
+ const keyBytes = typeof resolvedKey === "string" ? new TextEncoder().encode(resolvedKey) : resolvedKey instanceof Uint8Array ? resolvedKey : new Uint8Array(resolvedKey);
1791
+ const importedHmacKey = await subtle.importKey(
1792
+ "raw",
1793
+ keyBytes,
1794
+ { name: "HMAC", hash: "SHA-256" },
1795
+ false,
1796
+ ["sign"]
1107
1797
  );
1798
+ return CKeyManager.computeBlindIndex(value, importedHmacKey);
1108
1799
  }
1109
1800
  /**
1110
- * Exports an AES CryptoKey as a raw base64 string.
1801
+ * Pre-derives and caches the native CryptoKey for subsequent cryptographic operations.
1802
+ * Subclasses should override this with a dedicated, non-side-effecting implementation.
1111
1803
  */
1112
- static async exportAESKey(key) {
1113
- const subtle = getSubtleCrypto();
1114
- const raw = await subtle.exportKey("raw", key);
1115
- try {
1116
- return CKeyManager.toBase64(raw);
1117
- } finally {
1118
- new Uint8Array(raw).fill(0);
1119
- }
1804
+ async warmupKey(_options) {
1120
1805
  }
1121
1806
  /**
1122
- * Imports an AES CryptoKey from a raw base64 string.
1807
+ * Resolves raw key from keyProvider callback or defaultKey.
1123
1808
  */
1124
- static async importAESKey(base64, algorithm = "AES-GCM", extractable = false) {
1125
- const subtle = getSubtleCrypto();
1126
- const rawBytes = CKeyManager.fromBase64(base64);
1127
- if (rawBytes.length !== 16 && rawBytes.length !== 24 && rawBytes.length !== 32) {
1128
- rawBytes.fill(0);
1129
- throw new Error(`[CKeyManager] Invalid AES key length: expected 16, 24, or 32 bytes (128, 192, or 256 bits), got ${rawBytes.length} bytes.`);
1809
+ async resolveRawKey(keyProvider) {
1810
+ const target = keyProvider || (this.keys && this.activeKid ? this.keys[this.activeKid] : void 0) || this.defaultKey;
1811
+ if (!target) {
1812
+ throw new Error(
1813
+ `[${this.constructor.name}] No encryption key or keyProvider was supplied. Key cannot be empty or undefined. A valid key must be supplied from your authentication or secret management system.`
1814
+ );
1130
1815
  }
1131
- const usages = algorithm === "AES-KW" ? ["wrapKey", "unwrapKey"] : ["encrypt", "decrypt"];
1132
- try {
1133
- return await subtle.importKey(
1134
- "raw",
1135
- rawBytes,
1136
- { name: algorithm },
1137
- extractable,
1138
- usages
1816
+ const rawKey = typeof target === "function" ? await target() : target;
1817
+ if (!rawKey || typeof rawKey === "string" && rawKey.trim().length === 0 || typeof rawKey === "object" && "byteLength" in rawKey && rawKey.byteLength === 0) {
1818
+ throw new Error(
1819
+ `[${this.constructor.name}] No encryption key or keyProvider was supplied. Key provider resolved to empty or undefined key.`
1139
1820
  );
1140
- } finally {
1141
- rawBytes.fill(0);
1142
1821
  }
1822
+ return rawKey;
1143
1823
  }
1144
- // ── Asymmetric Key Pair Management ───────────────────────────────────────
1145
1824
  /**
1146
- * Generates an RSA-OAEP public/private key pair (2048 or 4096 bits).
1825
+ * Checks whether the given payload is an encrypted envelope.
1147
1826
  */
1148
- static async generateRSAKeyPair(bits = 4096, hash = "SHA-256", extractable = true) {
1149
- const subtle = getSubtleCrypto();
1150
- const pair = await subtle.generateKey(
1151
- {
1152
- name: "RSA-OAEP",
1153
- modulusLength: bits,
1154
- publicExponent: new Uint8Array([1, 0, 1]),
1155
- hash
1156
- },
1157
- extractable,
1158
- ["encrypt", "decrypt", "wrapKey", "unwrapKey"]
1159
- );
1160
- return {
1161
- publicKey: pair.publicKey,
1162
- privateKey: pair.privateKey
1163
- };
1827
+ static isEncrypted(payload) {
1828
+ if (!payload || typeof payload !== "object") return false;
1829
+ return payload.__encrypted === true && typeof payload.ciphertext === "string" && typeof payload.iv === "string";
1164
1830
  }
1165
1831
  /**
1166
- * Generates an ECDH public/private key pair for elliptic curve Diffie-Hellman.
1832
+ * Checks whether the given payload is an encrypted envelope.
1167
1833
  */
1168
- static async generateECDHKeyPair(curve = "P-256", extractable = true) {
1169
- const subtle = getSubtleCrypto();
1170
- const pair = await subtle.generateKey(
1171
- {
1172
- name: "ECDH",
1173
- namedCurve: curve
1174
- },
1175
- extractable,
1176
- ["deriveKey", "deriveBits"]
1177
- );
1178
- return {
1179
- publicKey: pair.publicKey,
1180
- privateKey: pair.privateKey
1181
- };
1834
+ isEncrypted(payload) {
1835
+ return AEncryptionProvider.isEncrypted(payload);
1182
1836
  }
1183
1837
  /**
1184
- * Exports a public CryptoKey to a portable JsonWebKey (JWK).
1838
+ * Clears configured keys and cached cryptographic material from memory.
1185
1839
  */
1186
- static async exportPublicKey(key) {
1187
- const subtle = getSubtleCrypto();
1188
- return await subtle.exportKey("jwk", key);
1840
+ clearKeys() {
1841
+ this.defaultKey = void 0;
1842
+ this.keys = void 0;
1843
+ this.activeKid = void 0;
1844
+ this.allowedKids = void 0;
1189
1845
  }
1190
1846
  /**
1191
- * Exports a private CryptoKey to a portable JsonWebKey (JWK).
1847
+ * Alias for clearKeys() to conform with standard disposal patterns.
1192
1848
  */
1193
- static async exportPrivateKey(key) {
1194
- const subtle = getSubtleCrypto();
1195
- return await subtle.exportKey("jwk", key);
1849
+ dispose() {
1850
+ this.clearKeys();
1196
1851
  }
1197
1852
  /**
1198
- * Imports a public key from a JsonWebKey (JWK).
1853
+ * ECMAScript Explicit Resource Management protocol (using / await using declarations).
1199
1854
  */
1200
- static async importPublicKey(jwk, algorithm) {
1201
- const subtle = getSubtleCrypto();
1202
- let importAlgorithm = algorithm;
1203
- let usages;
1204
- const isEC = jwk.kty === "EC" || typeof algorithm === "object" && algorithm?.name === "ECDH" || algorithm === "ECDH";
1205
- if (isEC) {
1206
- usages = [];
1207
- if (!importAlgorithm || importAlgorithm === "ECDH") {
1208
- importAlgorithm = { name: "ECDH", namedCurve: jwk.crv || "P-256" };
1209
- }
1210
- } else {
1211
- usages = ["encrypt", "wrapKey"];
1212
- if (!importAlgorithm || importAlgorithm === "RSA-OAEP") {
1213
- if (jwk.kty === "RSA") {
1214
- const hash = jwk.alg === "RSA-OAEP-512" ? "SHA-512" : "SHA-256";
1215
- importAlgorithm = { name: "RSA-OAEP", hash };
1216
- } else {
1217
- throw new Error(`[CKeyManager] Unsupported JWK kty: ${jwk.kty}`);
1218
- }
1219
- }
1220
- }
1221
- return await subtle.importKey("jwk", jwk, importAlgorithm, true, usages);
1855
+ [Symbol.dispose]() {
1856
+ this.dispose();
1857
+ }
1858
+ async [Symbol.asyncDispose]() {
1859
+ this.dispose();
1222
1860
  }
1223
1861
  /**
1224
- * Imports a private key from a JsonWebKey (JWK).
1862
+ * Resolves effective Authenticated Additional Data (AAD) for the operation:
1863
+ * 1. If options.bindRequestContextAad is explicitly false, returns options.aad directly without context binding.
1864
+ * 2. When encrypting (isDecrypt=false): binds request context `req:${method}:${url}` only if options.bindRequestContextAad === true.
1865
+ * 3. When decrypting (isDecrypt=true): derives request context `req:${method}:${url}` if url or method is provided.
1866
+ * 4. If explicit options.aad is provided: combines with request context if bound, or returns options.aad directly.
1867
+ * 5. Otherwise returns undefined.
1225
1868
  */
1226
- static async importPrivateKey(jwk, algorithm) {
1227
- const subtle = getSubtleCrypto();
1228
- let importAlgorithm = algorithm;
1229
- let usages;
1230
- const isEC = jwk.kty === "EC" || typeof algorithm === "object" && algorithm?.name === "ECDH" || algorithm === "ECDH";
1231
- if (isEC) {
1232
- usages = ["deriveKey", "deriveBits"];
1233
- if (!importAlgorithm || importAlgorithm === "ECDH") {
1234
- importAlgorithm = { name: "ECDH", namedCurve: jwk.crv || "P-256" };
1235
- }
1236
- } else {
1237
- usages = ["decrypt", "unwrapKey"];
1238
- if (!importAlgorithm || importAlgorithm === "RSA-OAEP") {
1239
- if (jwk.kty === "RSA") {
1240
- const hash = jwk.alg === "RSA-OAEP-512" ? "SHA-512" : "SHA-256";
1241
- importAlgorithm = { name: "RSA-OAEP", hash };
1242
- } else {
1243
- throw new Error(`[CKeyManager] Unsupported JWK kty: ${jwk.kty}`);
1869
+ _resolveEffectiveAad(options, isDecrypt = false) {
1870
+ if (!options) return void 0;
1871
+ let rawAad = options.aad;
1872
+ if (typeof rawAad === "function") {
1873
+ rawAad = rawAad(void 0, "");
1874
+ }
1875
+ if (rawAad === void 0 && options.baseAad) {
1876
+ rawAad = options.baseAad;
1877
+ }
1878
+ if (options.bindRequestContextAad === false) return rawAad;
1879
+ const hasReqContext = Boolean(options.url || options.method);
1880
+ if (rawAad) {
1881
+ if (options.bindRequestContextAad === true && hasReqContext) {
1882
+ const m = (options.method || "POST").toUpperCase();
1883
+ const u = options.url || "";
1884
+ const baseAad = typeof rawAad === "string" ? rawAad : rawAad instanceof Uint8Array || rawAad instanceof ArrayBuffer ? textDecoder.decode(rawAad) : String(rawAad);
1885
+ if (baseAad.includes(`|req:${m}:${u}`)) {
1886
+ return baseAad;
1244
1887
  }
1888
+ return `${baseAad}|req:${m}:${u}`;
1245
1889
  }
1890
+ return rawAad;
1246
1891
  }
1247
- return await subtle.importKey("jwk", jwk, importAlgorithm, true, usages);
1248
- }
1249
- /**
1250
- * Imports a public key from a PEM (SPKI) or base64 SPKI string.
1251
- */
1252
- static async importPublicKeyFromPEM(pem, algorithm = "RSA-OAEP", curve = "P-256", hash = "SHA-256") {
1253
- const subtle = getSubtleCrypto();
1254
- const cleaned = pem.replace(/-----BEGIN[ A-Z0-9_-]+-----/g, "").replace(/-----END[ A-Z0-9_-]+-----/g, "").replace(/\s+/g, "");
1255
- const spkiBytes = CKeyManager.fromBase64(cleaned);
1256
- let importAlgorithm;
1257
- let usages;
1258
- if (algorithm === "ECDH") {
1259
- importAlgorithm = { name: "ECDH", namedCurve: curve };
1260
- usages = [];
1261
- } else {
1262
- importAlgorithm = { name: "RSA-OAEP", hash };
1263
- usages = ["encrypt", "wrapKey"];
1264
- }
1265
- try {
1266
- return await subtle.importKey("spki", spkiBytes, importAlgorithm, true, usages);
1267
- } finally {
1268
- spkiBytes.fill(0);
1892
+ const shouldBindReq = isDecrypt ? hasReqContext : options.bindRequestContextAad === true && hasReqContext;
1893
+ if (shouldBindReq) {
1894
+ const m = (options.method || "POST").toUpperCase();
1895
+ const u = options.url || "";
1896
+ return `req:${m}:${u}`;
1269
1897
  }
1898
+ return void 0;
1270
1899
  }
1271
1900
  /**
1272
- * Imports a private key from a PEM (PKCS#8) or base64 PKCS#8 string.
1901
+ * Helper to deterministically bind record ID, contextual sibling fields (aadFields),
1902
+ * dynamic AAD callbacks, and field name into the AAD to prevent ciphertext swapping
1903
+ * between fields, records, or parent entities.
1273
1904
  */
1274
- static async importPrivateKeyFromPEM(pem, algorithm = "RSA-OAEP", curve = "P-256", hash = "SHA-256") {
1275
- if (/BEGIN\s+(RSA|EC)\s+PRIVATE\s+KEY/i.test(pem)) {
1276
- throw new Error(
1277
- `[CKeyManager] Failed to import private key. The provided PEM appears to be in PKCS#1 or SEC1 format. Web Crypto requires PKCS#8 format. Convert your key using: openssl pkcs8 -topk8 -nocrypt -in private.pem -out private_pkcs8.pem.`
1278
- );
1279
- }
1280
- const subtle = getSubtleCrypto();
1281
- const cleaned = pem.replace(/-----BEGIN[ A-Z0-9_-]+-----/g, "").replace(/-----END[ A-Z0-9_-]+-----/g, "").replace(/\s+/g, "");
1282
- const pkcs8Bytes = CKeyManager.fromBase64(cleaned);
1283
- let importAlgorithm;
1284
- let usages;
1285
- if (algorithm === "ECDH") {
1286
- importAlgorithm = { name: "ECDH", namedCurve: curve };
1287
- usages = ["deriveKey", "deriveBits"];
1288
- } else {
1289
- importAlgorithm = { name: "RSA-OAEP", hash };
1290
- usages = ["decrypt", "unwrapKey"];
1905
+ _bindFieldAad(data, field, options) {
1906
+ let baseAad = "";
1907
+ if (options?.baseAad) {
1908
+ baseAad = options.baseAad;
1291
1909
  }
1292
- try {
1293
- return await subtle.importKey("pkcs8", pkcs8Bytes, importAlgorithm, true, usages);
1294
- } finally {
1295
- pkcs8Bytes.fill(0);
1910
+ if (options?.aad) {
1911
+ if (typeof options.aad === "function") {
1912
+ const dynamicRes = options.aad(data, field);
1913
+ if (typeof dynamicRes === "string") {
1914
+ baseAad = baseAad ? `${baseAad}|${dynamicRes}` : dynamicRes;
1915
+ } else if (dynamicRes instanceof Uint8Array || dynamicRes instanceof ArrayBuffer) {
1916
+ const decoded = textDecoder.decode(dynamicRes);
1917
+ baseAad = baseAad ? `${baseAad}|${decoded}` : decoded;
1918
+ } else if (dynamicRes !== void 0 && dynamicRes !== null) {
1919
+ const str = String(dynamicRes);
1920
+ baseAad = baseAad ? `${baseAad}|${str}` : str;
1921
+ }
1922
+ } else if (typeof options.aad === "string") {
1923
+ baseAad = baseAad ? `${baseAad}|${options.aad}` : options.aad;
1924
+ } else if (options.aad instanceof Uint8Array || options.aad instanceof ArrayBuffer) {
1925
+ const decoded = textDecoder.decode(options.aad);
1926
+ baseAad = baseAad ? `${baseAad}|${decoded}` : decoded;
1927
+ } else {
1928
+ const str = String(options.aad);
1929
+ baseAad = baseAad ? `${baseAad}|${str}` : str;
1930
+ }
1296
1931
  }
1297
- }
1298
- /**
1299
- * Exports a public CryptoKey to a standard PEM (SPKI) string.
1300
- */
1301
- static async exportPublicKeyToPEM(key) {
1302
- const subtle = getSubtleCrypto();
1303
- const spki = await subtle.exportKey("spki", key);
1304
- let b64;
1305
- try {
1306
- b64 = CKeyManager.toBase64(spki);
1307
- } finally {
1308
- new Uint8Array(spki).fill(0);
1932
+ const recordId = data && typeof data === "object" && (data.id !== void 0 || data._id !== void 0) ? String(data.id ?? data._id) : "";
1933
+ const recordPart = recordId ? `|record:${recordId}` : "";
1934
+ let contextPart = "";
1935
+ if (options?.aadFields && options.aadFields.length > 0 && data && typeof data === "object") {
1936
+ const sortedFields = [...options.aadFields].sort();
1937
+ const parts = [];
1938
+ for (const f of sortedFields) {
1939
+ const val = data[f];
1940
+ let serializedVal = "";
1941
+ if (val === null) {
1942
+ serializedVal = "null";
1943
+ } else if (val !== void 0) {
1944
+ serializedVal = typeof val === "object" ? JSON.stringify(val) : String(val);
1945
+ }
1946
+ parts.push(`${f}=${serializedVal}`);
1947
+ }
1948
+ contextPart = `|ctx:${parts.join(",")}`;
1309
1949
  }
1310
- const lines = b64.match(/.{1,64}/g) || [b64];
1311
- return `-----BEGIN PUBLIC KEY-----
1312
- ${lines.join("\n")}
1313
- -----END PUBLIC KEY-----`;
1950
+ const reqPart = options?.bindRequestContextAad === true && (options?.url || options?.method) ? `|req:${(options.method || "POST").toUpperCase()}:${options.url || ""}` : "";
1951
+ return {
1952
+ ...options,
1953
+ bindRequestContextAad: false,
1954
+ aad: `aad:${baseAad}${recordPart}${contextPart}${reqPart}|field:${field}`
1955
+ };
1314
1956
  }
1315
1957
  /**
1316
- * Exports a private CryptoKey to a standard PEM (PKCS#8) string.
1958
+ * Validates envelope metadata against algorithm whitelists and key identifiers (kid).
1959
+ * In strict mode, allowedAlgorithms defaults to ['AES-GCM'].
1317
1960
  */
1318
- static async exportPrivateKeyToPEM(key) {
1319
- const subtle = getSubtleCrypto();
1320
- const pkcs8 = await subtle.exportKey("pkcs8", key);
1321
- let b64;
1322
- try {
1323
- b64 = CKeyManager.toBase64(pkcs8);
1324
- } finally {
1325
- new Uint8Array(pkcs8).fill(0);
1961
+ _validateEnvelopeMetadata(payload, options) {
1962
+ if (!payload || typeof payload !== "object") return void 0;
1963
+ const allowedAlgorithms = options?.allowedAlgorithms ?? (options?.strict ? ["AES-GCM"] : void 0);
1964
+ if (allowedAlgorithms && allowedAlgorithms.length > 0) {
1965
+ const alg = payload.alg;
1966
+ if (!alg || !allowedAlgorithms.includes(alg)) {
1967
+ return new Error(`[AEncryptionProvider] Algorithm mismatch: algorithm '${alg || "unknown"}' is not in allowedAlgorithms.`);
1968
+ }
1326
1969
  }
1327
- const lines = b64.match(/.{1,64}/g) || [b64];
1328
- return `-----BEGIN PRIVATE KEY-----
1329
- ${lines.join("\n")}
1330
- -----END PRIVATE KEY-----`;
1970
+ const effectiveKeys = options?.keys || this.keys;
1971
+ const effectiveAllowedKids = options?.allowedKids || this.allowedKids;
1972
+ const allowedKids = effectiveAllowedKids ?? (effectiveKeys ? Object.keys(effectiveKeys) : void 0);
1973
+ if (allowedKids && allowedKids.length > 0) {
1974
+ if (payload.kid && !allowedKids.includes(payload.kid)) {
1975
+ return new Error(`[AEncryptionProvider] Unauthorized key: kid '${payload.kid}' is not in allowedKids.`);
1976
+ }
1977
+ }
1978
+ return void 0;
1331
1979
  }
1332
- // ── Key Wrapping & Unwrapping ────────────────────────────────────────────
1333
1980
  /**
1334
- * Wraps a target CryptoKey (e.g. an Account Symmetric Key) using a wrapping key
1335
- * (AES-KW or RSA-OAEP).
1981
+ * Normalizes encryption options with active keyring parameters:
1982
+ * If `keys` and `activeKid` are configured, selects `key = keys[activeKid]` and sets `kid = activeKid`.
1336
1983
  */
1337
- static async wrapKey(keyToWrap, wrappingKey, format = "raw") {
1338
- const subtle = getSubtleCrypto();
1339
- let wrapAlgorithm;
1340
- if (wrappingKey.algorithm.name === "RSA-OAEP") {
1341
- wrapAlgorithm = { name: "RSA-OAEP" };
1342
- } else if (wrappingKey.algorithm.name === "AES-KW") {
1343
- wrapAlgorithm = "AES-KW";
1344
- } else {
1345
- wrapAlgorithm = "AES-KW";
1984
+ _resolveOutboundKeyringOptions(options) {
1985
+ const effectiveKeys = options?.keys || this.keys;
1986
+ const effectiveActiveKid = options?.activeKid || (options?.kid ? options.kid : this.activeKid);
1987
+ let effectiveKey = options?.key;
1988
+ let effectiveKid = options?.kid || effectiveActiveKid;
1989
+ if (effectiveKeys && effectiveActiveKid) {
1990
+ if (effectiveKeys[effectiveActiveKid]) {
1991
+ effectiveKey = options?.key || effectiveKeys[effectiveActiveKid];
1992
+ effectiveKid = effectiveActiveKid;
1993
+ } else if (!options?.key) {
1994
+ throw new Error(`[${this.constructor.name}] Active kid '${effectiveActiveKid}' not found in keyring keys.`);
1995
+ }
1996
+ } else if (effectiveKeys && !effectiveKey) {
1997
+ const kids = Object.keys(effectiveKeys);
1998
+ if (kids.length > 0) {
1999
+ effectiveKid = kids[0];
2000
+ effectiveKey = effectiveKeys[kids[0]];
2001
+ }
2002
+ }
2003
+ if (effectiveKeys || effectiveActiveKid || effectiveKey !== options?.key || effectiveKid !== options?.kid) {
2004
+ return {
2005
+ ...options,
2006
+ ...effectiveKeys ? { keys: effectiveKeys } : {},
2007
+ ...effectiveActiveKid ? { activeKid: effectiveActiveKid } : {},
2008
+ ...effectiveKey !== void 0 ? { key: effectiveKey } : {},
2009
+ ...effectiveKid !== void 0 ? { kid: effectiveKid } : {}
2010
+ };
1346
2011
  }
1347
- const wrappedBuffer = await subtle.wrapKey(format, keyToWrap, wrappingKey, wrapAlgorithm);
1348
- return CKeyManager.toBase64(wrappedBuffer);
2012
+ return options || {};
1349
2013
  }
1350
2014
  /**
1351
- * Unwraps a base64-encoded wrapped key using an unwrapping key (AES-KW or RSA-OAEP).
2015
+ * Resolves the appropriate decryption key for a specific payload envelope based on its `kid`
2016
+ * and the configured keyring (`keys`, `activeKid`, or fallback `key`).
1352
2017
  */
1353
- static async unwrapKey(wrappedBase64, unwrappingKey, unwrappedKeyAlgorithm = { name: "AES-GCM", length: 256 }, extractable = true, keyUsages = ["encrypt", "decrypt"], format = "raw") {
1354
- const subtle = getSubtleCrypto();
1355
- const wrappedBytes = CKeyManager.fromBase64(wrappedBase64);
1356
- let unwrapAlgorithm;
1357
- if (unwrappingKey.algorithm.name === "RSA-OAEP") {
1358
- unwrapAlgorithm = { name: "RSA-OAEP" };
1359
- } else {
1360
- unwrapAlgorithm = "AES-KW";
2018
+ _resolveDecryptionKeyForEnvelope(payload, options) {
2019
+ const effectiveKeys = options?.keys || this.keys;
2020
+ const effectiveActiveKid = options?.activeKid || this.activeKid;
2021
+ if (payload && typeof payload === "object" && payload.kid && effectiveKeys) {
2022
+ if (effectiveKeys[payload.kid]) {
2023
+ return effectiveKeys[payload.kid];
2024
+ }
1361
2025
  }
1362
- try {
1363
- return await subtle.unwrapKey(
1364
- format,
1365
- wrappedBytes,
1366
- unwrappingKey,
1367
- unwrapAlgorithm,
1368
- unwrappedKeyAlgorithm,
1369
- extractable,
1370
- keyUsages
1371
- );
1372
- } finally {
1373
- wrappedBytes.fill(0);
2026
+ if (effectiveKeys && effectiveActiveKid && effectiveKeys[effectiveActiveKid]) {
2027
+ return effectiveKeys[effectiveActiveKid];
1374
2028
  }
2029
+ return options?.key || this.defaultKey;
1375
2030
  }
1376
2031
  /**
1377
- * Generates a high-entropy symmetric AES-GCM Account Data Encryption Key (ADEK).
1378
- * Designed for multi-user table and field-level encryption.
2032
+ * Handles a per-field or per-payload decryption error according to configured options:
2033
+ * - Invokes onDecryptError callback if provided
2034
+ * - Attaches error metadata (__decryptErrors) to the record object
2035
+ * - Returns fallback cell placeholder ('[⚠ Decryption Error]') or rethrows
1379
2036
  */
1380
- static async generateAccountDataKey(length = 256, extractable = false) {
1381
- const subtle = getSubtleCrypto();
1382
- return await subtle.generateKey(
1383
- { name: "AES-GCM", length },
1384
- extractable,
1385
- ["encrypt", "decrypt"]
1386
- );
2037
+ _handleDecryptError(err, record, field, value, index, options) {
2038
+ if (record && typeof record === "object" && record !== value) {
2039
+ record.__decryptErrors = {
2040
+ ...record.__decryptErrors || {},
2041
+ [field || "__payload"]: err
2042
+ };
2043
+ }
2044
+ if (options?.onDecryptError === false || options?.onDecryptError === void 0) {
2045
+ throw err;
2046
+ }
2047
+ if (typeof options.onDecryptError === "function") {
2048
+ return options.onDecryptError({
2049
+ record,
2050
+ field,
2051
+ value,
2052
+ error: err,
2053
+ index
2054
+ });
2055
+ }
2056
+ if (typeof options.onDecryptError === "string") {
2057
+ return options.onDecryptError;
2058
+ }
2059
+ if (options.onDecryptError === true) {
2060
+ return "[\u26A0 Decryption Error]";
2061
+ }
2062
+ throw err;
1387
2063
  }
1388
2064
  /**
1389
- * Wraps an Account Data Encryption Key (ADEK) for a specific user using the user's public key (RSA-OAEP).
1390
- * Returns a Base64-encoded wrapped key string suitable for storage in role-map / access tables.
2065
+ * Encrypts a single scalar value with contextual AAD binding into an IEncryptedPayload envelope.
2066
+ * Binds recordId, fieldName, and sibling aadFields deterministically to protect against ciphertext swapping.
2067
+ *
2068
+ * @param value - Scalar value (string, number, boolean, object) to encrypt.
2069
+ * @param context - Field encryption context with recordId, fieldName, and optional sibling aadFields.
1391
2070
  */
1392
- static async wrapAccountKeyForUser(adek, userPublicKey) {
1393
- const subtle = getSubtleCrypto();
1394
- if (userPublicKey.algorithm.name === "RSA-OAEP") {
1395
- const rawKey = await subtle.exportKey("raw", adek);
1396
- try {
1397
- const wrappedBuffer = await subtle.encrypt(
1398
- { name: "RSA-OAEP" },
1399
- userPublicKey,
1400
- rawKey
1401
- );
1402
- return CKeyManager.toBase64(wrappedBuffer);
1403
- } finally {
1404
- new Uint8Array(rawKey).fill(0);
1405
- }
1406
- }
1407
- throw new Error(
1408
- `[CKeyManager] Unsupported user key algorithm for wrapAccountKeyForUser: '${userPublicKey.algorithm.name}'. Expected 'RSA-OAEP'.`
1409
- );
2071
+ async encryptField(value, context) {
2072
+ if (value === void 0 || value === null) return value;
2073
+ if (this.isEncrypted(value)) return value;
2074
+ const field = context?.fieldName || "field";
2075
+ const dummyData = {};
2076
+ if (context?.recordId !== void 0) {
2077
+ dummyData.id = context.recordId;
2078
+ }
2079
+ const { aadFields, ...restContext } = context || {};
2080
+ let boundOptions = { ...restContext };
2081
+ if (aadFields) {
2082
+ if (!Array.isArray(aadFields) && typeof aadFields === "object") {
2083
+ Object.assign(dummyData, aadFields);
2084
+ boundOptions.aadFields = Object.keys(aadFields);
2085
+ } else if (Array.isArray(aadFields)) {
2086
+ boundOptions.aadFields = aadFields;
2087
+ if (context?.record) {
2088
+ Object.assign(dummyData, context.record);
2089
+ }
2090
+ }
2091
+ }
2092
+ const resolvedOptions = this._bindFieldAad(dummyData, field, boundOptions);
2093
+ return this.encryptValue(value, resolvedOptions);
1410
2094
  }
1411
2095
  /**
1412
- * Unwraps a Base64-encoded Account Data Encryption Key (ADEK) using the user's private key (RSA-OAEP).
1413
- * Returns the restored AES-GCM CryptoKey ready for use in CWebCryptoEncryptionProvider.
2096
+ * Decrypts a single IEncryptedPayload envelope back into its scalar plaintext value using contextual AAD.
2097
+ * Validates recordId, fieldName, and sibling aadFields integrity.
2098
+ *
2099
+ * @param envelope - The IEncryptedPayload envelope to decrypt.
2100
+ * @param context - Field encryption context matching the encryption parameters.
1414
2101
  */
1415
- static async unwrapAccountKeyForUser(wrappedKeyBase64, userPrivateKey, unwrappedKeyAlgorithm = { name: "AES-GCM", length: 256 }, extractable = false) {
1416
- const subtle = getSubtleCrypto();
1417
- if (userPrivateKey.algorithm.name === "RSA-OAEP") {
1418
- const wrappedBytes = CKeyManager.fromBase64(wrappedKeyBase64);
1419
- let unwrappedRawKey;
1420
- try {
1421
- unwrappedRawKey = await subtle.decrypt(
1422
- { name: "RSA-OAEP" },
1423
- userPrivateKey,
1424
- wrappedBytes
1425
- );
1426
- } finally {
1427
- wrappedBytes.fill(0);
1428
- }
1429
- try {
1430
- return await subtle.importKey(
1431
- "raw",
1432
- unwrappedRawKey,
1433
- unwrappedKeyAlgorithm,
1434
- extractable,
1435
- ["encrypt", "decrypt"]
1436
- );
1437
- } finally {
1438
- new Uint8Array(unwrappedRawKey).fill(0);
2102
+ async decryptField(envelope, context) {
2103
+ if (!this.isEncrypted(envelope)) return envelope;
2104
+ const field = context?.fieldName || "field";
2105
+ const dummyData = {};
2106
+ if (context?.recordId !== void 0) {
2107
+ dummyData.id = context.recordId;
2108
+ }
2109
+ const { aadFields, ...restContext } = context || {};
2110
+ let boundOptions = { ...restContext };
2111
+ if (aadFields) {
2112
+ if (!Array.isArray(aadFields) && typeof aadFields === "object") {
2113
+ Object.assign(dummyData, aadFields);
2114
+ boundOptions.aadFields = Object.keys(aadFields);
2115
+ } else if (Array.isArray(aadFields)) {
2116
+ boundOptions.aadFields = aadFields;
2117
+ if (context?.record) {
2118
+ Object.assign(dummyData, context.record);
2119
+ }
1439
2120
  }
1440
2121
  }
1441
- throw new Error(
1442
- `[CKeyManager] Unsupported user key algorithm for unwrapAccountKeyForUser: '${userPrivateKey.algorithm.name}'. Expected 'RSA-OAEP'.`
1443
- );
2122
+ const resolvedOptions = this._bindFieldAad(dummyData, field, boundOptions);
2123
+ return this.decryptValue(envelope, resolvedOptions);
1444
2124
  }
1445
- // ── Key Derivation Functions (PBKDF2 & HKDF) ──────────────────────────────
1446
2125
  /**
1447
- * Derives a symmetric AES-GCM CryptoKey from a passphrase using PBKDF2 or HKDF.
1448
- * Returns the derived CryptoKey and the base64-encoded salt.
2126
+ * Top-level polymorphic encryption method.
2127
+ * Automatically performs value-level encryption:
2128
+ * - If data is an array of objects: recursively encrypts each row.
2129
+ * - If data is an object: encrypts specified fields (or all values if valueEncryption is true),
2130
+ * preserving JSON object keys so backend APIs and ORMs can parse keys and perform upserts.
2131
+ * - If data is a primitive or whole-payload envelope: encrypts as a single value.
1449
2132
  */
1450
- static async deriveKey(passphrase, options) {
1451
- if (!passphrase || typeof passphrase === "string" && passphrase.trim().length === 0 || typeof passphrase === "object" && "byteLength" in passphrase && passphrase.byteLength === 0) {
1452
- throw new Error("[CKeyManager] Passphrase cannot be empty or undefined for key derivation.");
2133
+ async encrypt(data, options) {
2134
+ if (data === void 0 || data === null) return data;
2135
+ if (this.isEncrypted(data)) return data;
2136
+ options = this._resolveOutboundKeyringOptions(options);
2137
+ if (options?.valueEncryption === false && (!options.fields || options.fields.length === 0)) {
2138
+ return this.encryptValue(data, options);
1453
2139
  }
1454
- const subtle = getSubtleCrypto();
1455
- const saltBytes = CKeyManager.resolveSalt(options.salt);
1456
- const keyLength = options.keyLength || 256;
1457
- const hash = options.hash || "SHA-256";
1458
- const extractable = options.extractable === true;
1459
- const rawKeyBytes = typeof passphrase === "string" ? textEncoder$1.encode(passphrase) : passphrase instanceof Uint8Array ? passphrase : new Uint8Array(passphrase);
1460
- try {
1461
- if (options.kdf === "pbkdf2") {
1462
- const baseKey = await subtle.importKey(
1463
- "raw",
1464
- rawKeyBytes,
1465
- "PBKDF2",
1466
- false,
1467
- ["deriveKey"]
2140
+ if (Array.isArray(data)) {
2141
+ if (data.length === 0) return data;
2142
+ if (options?.fields && options.fields.length > 0) {
2143
+ const fields = options.fields;
2144
+ let hasUnencrypted = false;
2145
+ for (let i = 0; i < data.length; i++) {
2146
+ const item = data[i];
2147
+ if (item && typeof item === "object") {
2148
+ for (let f = 0; f < fields.length; f++) {
2149
+ const val = item[fields[f]];
2150
+ if (val !== void 0 && val !== null && !this.isEncrypted(val)) {
2151
+ hasUnencrypted = true;
2152
+ break;
2153
+ }
2154
+ }
2155
+ if (hasUnencrypted) break;
2156
+ }
2157
+ }
2158
+ if (!hasUnencrypted) {
2159
+ return data;
2160
+ }
2161
+ await this.warmupKey(options);
2162
+ const itemOptions = { ...options, _warmed: true };
2163
+ return Promise.all(data.map((item) => this.encrypt(item, itemOptions)));
2164
+ }
2165
+ if (options?.valueEncryption === true) {
2166
+ await this.warmupKey(options);
2167
+ const itemOptions = { ...options, _warmed: true };
2168
+ return Promise.all(data.map((item) => this.encrypt(item, itemOptions)));
2169
+ }
2170
+ return this.encryptValue(data, options);
2171
+ }
2172
+ if (typeof data === "object" && !ArrayBuffer.isView(data) && !(data instanceof ArrayBuffer)) {
2173
+ if (options?.fields && options.fields.length > 0) {
2174
+ const fieldsToEncrypt = options.fields.filter(
2175
+ (field) => Object.prototype.hasOwnProperty.call(data, field) && data[field] !== void 0 && data[field] !== null && !this.isEncrypted(data[field])
1468
2176
  );
1469
- const derivedKey = await subtle.deriveKey(
1470
- {
1471
- name: "PBKDF2",
1472
- salt: saltBytes,
1473
- iterations: options.iterations || 31e4,
1474
- hash
1475
- },
1476
- baseKey,
1477
- { name: "AES-GCM", length: keyLength },
1478
- extractable,
1479
- ["encrypt", "decrypt"]
2177
+ if (fieldsToEncrypt.length === 0) {
2178
+ return data;
2179
+ }
2180
+ if (!options?._warmed) {
2181
+ await this.warmupKey(options);
2182
+ }
2183
+ const result = { ...data };
2184
+ const entries = await Promise.all(
2185
+ fieldsToEncrypt.map(async (field) => {
2186
+ const fieldOptions = this._bindFieldAad(data, field, options);
2187
+ return [field, await this.encryptValue(result[field], fieldOptions)];
2188
+ })
1480
2189
  );
1481
- return {
1482
- key: derivedKey,
1483
- salt: CKeyManager.toBase64(saltBytes)
1484
- };
2190
+ for (const [field, encryptedVal] of entries) {
2191
+ safeSetProperty(result, field, encryptedVal);
2192
+ }
2193
+ return result;
1485
2194
  }
1486
- if (options.kdf === "hkdf") {
1487
- const baseKey = await subtle.importKey(
1488
- "raw",
1489
- rawKeyBytes,
1490
- "HKDF",
1491
- false,
1492
- ["deriveKey"]
2195
+ if (options?.valueEncryption === true) {
2196
+ const result = { ...data };
2197
+ const keysToEncrypt = Object.keys(result).filter(
2198
+ (key) => result[key] !== void 0 && result[key] !== null && !this.isEncrypted(result[key])
1493
2199
  );
1494
- const infoBytes = resolveInfoBytes(options.info, "katlux-derived-aes-key");
1495
- const derivedKey = await subtle.deriveKey(
1496
- {
1497
- name: "HKDF",
1498
- salt: saltBytes,
1499
- info: infoBytes,
1500
- hash
1501
- },
1502
- baseKey,
1503
- { name: "AES-GCM", length: keyLength },
1504
- extractable,
1505
- ["encrypt", "decrypt"]
2200
+ if (keysToEncrypt.length === 0) {
2201
+ return result;
2202
+ }
2203
+ if (!options?._warmed) {
2204
+ await this.warmupKey(options);
2205
+ }
2206
+ const entries = await Promise.all(
2207
+ keysToEncrypt.map(async (key) => {
2208
+ const fieldOptions = this._bindFieldAad(data, key, options);
2209
+ return [key, await this.encryptValue(result[key], fieldOptions)];
2210
+ })
1506
2211
  );
1507
- return {
1508
- key: derivedKey,
1509
- salt: CKeyManager.toBase64(saltBytes)
1510
- };
1511
- }
1512
- throw new Error(`[CKeyManager] Unsupported KDF strategy: ${options.kdf}`);
1513
- } finally {
1514
- if (typeof passphrase === "string") {
1515
- rawKeyBytes.fill(0);
2212
+ for (const [key, encryptedVal] of entries) {
2213
+ safeSetProperty(result, key, encryptedVal);
2214
+ }
2215
+ return result;
1516
2216
  }
1517
2217
  }
2218
+ return this.encryptValue(data, options);
1518
2219
  }
1519
2220
  /**
1520
- * Derives an AES-GCM CryptoKey from a high-entropy secret (e.g. an ECDH shared secret key)
1521
- * using HKDF.
2221
+ * Top-level polymorphic decryption method.
2222
+ * Automatically performs value-level decryption:
2223
+ * - If payload is an encrypted envelope: validates metadata and decrypts directly via decryptValue.
2224
+ * - If payload is an array of objects: decrypts each row (batched with local memoization).
2225
+ * - If payload is an object: decrypts any encrypted value cells across fields.
2226
+ * In strict mode:
2227
+ * - Unencrypted plaintext in configured `fields` triggers a decryption error.
2228
+ * - Envelopes are validated against `allowedAlgorithms` and `allowedKids`.
2229
+ * - Errors are routed through `onDecryptError` or populated into `__decryptErrors`.
1522
2230
  */
1523
- static async deriveKeyFromSecret(secretKey, options = {}) {
1524
- const subtle = getSubtleCrypto();
1525
- let saltBytes;
1526
- if (options.salt !== void 0) {
1527
- saltBytes = CKeyManager.resolveSalt(options.salt);
1528
- } else {
1529
- saltBytes = new Uint8Array(16);
2231
+ async decrypt(payload, options) {
2232
+ if (payload === void 0 || payload === null) return payload;
2233
+ if (this.isEncrypted(payload)) {
2234
+ const envelopeKey = this._resolveDecryptionKeyForEnvelope(payload, options);
2235
+ const envelopeOptions = envelopeKey !== options?.key ? { ...options, key: envelopeKey } : options;
2236
+ const metaErr = this._validateEnvelopeMetadata(payload, envelopeOptions);
2237
+ if (metaErr) {
2238
+ return this._handleDecryptError(metaErr, payload, "", payload, void 0, envelopeOptions);
2239
+ }
2240
+ try {
2241
+ return await this.decryptValue(payload, envelopeOptions);
2242
+ } catch (err) {
2243
+ return this._handleDecryptError(err, payload, "", payload, void 0, envelopeOptions);
2244
+ }
1530
2245
  }
1531
- const infoBytes = resolveInfoBytes(options.info, "katlux-ecdh-aes-gcm");
1532
- const hash = options.hash || "SHA-256";
1533
- const keyLength = options.keyLength || 256;
1534
- const extractable = options.extractable === true;
1535
- return await subtle.deriveKey(
1536
- {
1537
- name: "HKDF",
1538
- salt: saltBytes,
1539
- info: infoBytes,
1540
- hash
1541
- },
1542
- secretKey,
1543
- { name: "AES-GCM", length: keyLength },
1544
- extractable,
1545
- ["encrypt", "decrypt"]
1546
- );
2246
+ if (Array.isArray(payload)) {
2247
+ if (payload.length === 0) return payload;
2248
+ const localMemo = /* @__PURE__ */ new Map();
2249
+ const decryptRow = async (item, rowIndex) => {
2250
+ if (!item) return item;
2251
+ if (this.isEncrypted(item)) {
2252
+ const envelopeKey = this._resolveDecryptionKeyForEnvelope(item, options);
2253
+ const envelopeOptions = envelopeKey !== options?.key ? { ...options, key: envelopeKey } : options;
2254
+ const metaErr = this._validateEnvelopeMetadata(item, envelopeOptions);
2255
+ if (metaErr) {
2256
+ return this._handleDecryptError(metaErr, item, "", item, rowIndex, envelopeOptions);
2257
+ }
2258
+ const ct = item.ciphertext;
2259
+ const iv = item.iv || "";
2260
+ const aadPart = options?.aad ? typeof options.aad === "string" ? options.aad : "" : "";
2261
+ const memoKey = `${ct}:${iv}:${aadPart}:${item.kid || ""}`;
2262
+ try {
2263
+ if (!localMemo.has(memoKey)) {
2264
+ localMemo.set(memoKey, this.decryptValue(item, envelopeOptions));
2265
+ }
2266
+ return await localMemo.get(memoKey);
2267
+ } catch (err) {
2268
+ return this._handleDecryptError(err, item, "", item, rowIndex, envelopeOptions);
2269
+ }
2270
+ }
2271
+ if (typeof item === "object" && !ArrayBuffer.isView(item) && !(item instanceof ArrayBuffer)) {
2272
+ const targetKeys = options?.fields && options.fields.length > 0 ? options.fields.filter((f) => Object.prototype.hasOwnProperty.call(item, f)) : Object.keys(item);
2273
+ const keysToDecrypt = targetKeys.filter((key) => this.isEncrypted(item[key]));
2274
+ const strictViolationKeys = options?.strict === true && options?.fields && options.fields.length > 0 ? targetKeys.filter((key) => item[key] !== void 0 && item[key] !== null && !this.isEncrypted(item[key])) : [];
2275
+ if (keysToDecrypt.length === 0 && strictViolationKeys.length === 0) {
2276
+ return item;
2277
+ }
2278
+ const proto = Object.getPrototypeOf(item);
2279
+ const result = proto && proto !== Object.prototype ? Object.assign(Object.create(proto), item) : { ...item };
2280
+ for (const key of strictViolationKeys) {
2281
+ const err = new Error(`[AEncryptionProvider] Strict decryption violation: field '${key}' contains unencrypted plaintext.`);
2282
+ const fallbackVal = this._handleDecryptError(err, result, key, item[key], rowIndex, options);
2283
+ safeSetProperty(result, key, fallbackVal);
2284
+ }
2285
+ await Promise.all(
2286
+ keysToDecrypt.map(async (key) => {
2287
+ const env = item[key];
2288
+ const envelopeKey = this._resolveDecryptionKeyForEnvelope(env, options);
2289
+ const baseOptions = envelopeKey !== options?.key ? { ...options, key: envelopeKey } : options;
2290
+ const boundOptions = this._bindFieldAad(item, key, baseOptions);
2291
+ const hasBoundAad = boundOptions.aad !== baseOptions?.aad;
2292
+ const metaErr = this._validateEnvelopeMetadata(env, boundOptions);
2293
+ if (metaErr) {
2294
+ const fallbackVal = this._handleDecryptError(metaErr, result, key, env, rowIndex, boundOptions);
2295
+ safeSetProperty(result, key, fallbackVal);
2296
+ return;
2297
+ }
2298
+ const ct = env.ciphertext;
2299
+ const iv = env.iv || "";
2300
+ const aadPart = boundOptions.aad ? typeof boundOptions.aad === "string" ? boundOptions.aad : "" : "";
2301
+ const memoKey = `${ct}:${iv}:${aadPart}:${env.kid || ""}`;
2302
+ try {
2303
+ if (!localMemo.has(memoKey)) {
2304
+ const decryptPromise = (async () => {
2305
+ try {
2306
+ return await this.decryptValue(env, boundOptions);
2307
+ } catch (err) {
2308
+ const canFallback = hasBoundAad && (!options?.aadFields || options.aadFields.length === 0) && typeof options?.aad !== "function";
2309
+ if (canFallback && isAuthOrDecryptionError(err)) {
2310
+ return await this.decryptValue(env, baseOptions);
2311
+ }
2312
+ throw err;
2313
+ }
2314
+ })();
2315
+ localMemo.set(memoKey, decryptPromise);
2316
+ }
2317
+ const decryptedVal = await localMemo.get(memoKey);
2318
+ safeSetProperty(result, key, decryptedVal);
2319
+ } catch (err) {
2320
+ const fallbackVal = this._handleDecryptError(err, result, key, env, rowIndex, boundOptions);
2321
+ safeSetProperty(result, key, fallbackVal);
2322
+ }
2323
+ })
2324
+ );
2325
+ return result;
2326
+ }
2327
+ return item;
2328
+ };
2329
+ if (payload.length <= 100) {
2330
+ return Promise.all(payload.map((item, index) => decryptRow(item, index)));
2331
+ }
2332
+ const batchSize = 1e3;
2333
+ const results = new Array(payload.length);
2334
+ for (let i = 0; i < payload.length; i += batchSize) {
2335
+ const end = Math.min(i + batchSize, payload.length);
2336
+ const chunk = payload.slice(i, end);
2337
+ const chunkRes = await Promise.all(chunk.map((item, localIdx) => decryptRow(item, i + localIdx)));
2338
+ for (let j = 0; j < chunkRes.length; j++) {
2339
+ results[i + j] = chunkRes[j];
2340
+ }
2341
+ if (end < payload.length && (i > 0 && i % 5e3 === 0)) {
2342
+ await new Promise((r) => setTimeout(r, 0));
2343
+ }
2344
+ }
2345
+ return results;
2346
+ }
2347
+ if (typeof payload === "object" && !ArrayBuffer.isView(payload) && !(payload instanceof ArrayBuffer)) {
2348
+ const targetKeys = options?.fields && options.fields.length > 0 ? options.fields.filter((f) => Object.prototype.hasOwnProperty.call(payload, f)) : Object.keys(payload);
2349
+ const keysToDecrypt = targetKeys.filter((key) => this.isEncrypted(payload[key]));
2350
+ const strictViolationKeys = options?.strict === true && options?.fields && options.fields.length > 0 ? targetKeys.filter((key) => payload[key] !== void 0 && payload[key] !== null && !this.isEncrypted(payload[key])) : [];
2351
+ if (keysToDecrypt.length === 0 && strictViolationKeys.length === 0) {
2352
+ return payload;
2353
+ }
2354
+ const proto = Object.getPrototypeOf(payload);
2355
+ const result = proto && proto !== Object.prototype ? Object.assign(Object.create(proto), payload) : { ...payload };
2356
+ for (const key of strictViolationKeys) {
2357
+ const err = new Error(`[AEncryptionProvider] Strict decryption violation: field '${key}' contains unencrypted plaintext.`);
2358
+ const fallbackVal = this._handleDecryptError(err, result, key, payload[key], void 0, options);
2359
+ safeSetProperty(result, key, fallbackVal);
2360
+ }
2361
+ await Promise.all(
2362
+ keysToDecrypt.map(async (key) => {
2363
+ const env = payload[key];
2364
+ const envelopeKey = this._resolveDecryptionKeyForEnvelope(env, options);
2365
+ const baseOptions = envelopeKey !== options?.key ? { ...options, key: envelopeKey } : options;
2366
+ const boundOptions = this._bindFieldAad(payload, key, baseOptions);
2367
+ const hasBoundAad = boundOptions.aad !== baseOptions?.aad;
2368
+ const metaErr = this._validateEnvelopeMetadata(env, boundOptions);
2369
+ if (metaErr) {
2370
+ const fallbackVal = this._handleDecryptError(metaErr, result, key, env, void 0, boundOptions);
2371
+ safeSetProperty(result, key, fallbackVal);
2372
+ return;
2373
+ }
2374
+ try {
2375
+ let decryptedVal;
2376
+ try {
2377
+ decryptedVal = await this.decryptValue(env, boundOptions);
2378
+ } catch (err) {
2379
+ const canFallback = hasBoundAad && (!options?.aadFields || options.aadFields.length === 0) && typeof options?.aad !== "function";
2380
+ if (canFallback && isAuthOrDecryptionError(err)) {
2381
+ decryptedVal = await this.decryptValue(env, baseOptions);
2382
+ } else {
2383
+ throw err;
2384
+ }
2385
+ }
2386
+ safeSetProperty(result, key, decryptedVal);
2387
+ } catch (err) {
2388
+ const fallbackVal = this._handleDecryptError(err, result, key, env, void 0, boundOptions);
2389
+ safeSetProperty(result, key, fallbackVal);
2390
+ }
2391
+ })
2392
+ );
2393
+ return result;
2394
+ }
2395
+ return payload;
1547
2396
  }
1548
2397
  }
1549
2398
 
@@ -1558,6 +2407,7 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
1558
2407
  _cachedKeyId;
1559
2408
  _configuredKid;
1560
2409
  _decryptSaltCache = /* @__PURE__ */ new Map();
2410
+ _keyringCache = /* @__PURE__ */ new Map();
1561
2411
  setDefaultKey(key) {
1562
2412
  super.setDefaultKey(key);
1563
2413
  this._cachedKey = void 0;
@@ -1567,6 +2417,7 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
1567
2417
  this._cachedHash = void 0;
1568
2418
  this._cachedKeyId = void 0;
1569
2419
  this._decryptSaltCache.clear();
2420
+ this._keyringCache.clear();
1570
2421
  }
1571
2422
  clearKeys() {
1572
2423
  super.clearKeys();
@@ -1577,9 +2428,10 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
1577
2428
  this._cachedHash = void 0;
1578
2429
  this._cachedKeyId = void 0;
1579
2430
  this._decryptSaltCache.clear();
2431
+ this._keyringCache.clear();
1580
2432
  }
1581
2433
  constructor(key, options) {
1582
- super(key);
2434
+ super(key, options);
1583
2435
  if (options) {
1584
2436
  if (options.kid || options.keyId) {
1585
2437
  this._configuredKid = options.kid || options.keyId;
@@ -1629,14 +2481,21 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
1629
2481
  }
1630
2482
  async _resolveCryptoKeyInternal(keyProvider, kdfOverride, saltOverride, options, iterationsOverride) {
1631
2483
  const rawKey = await this.resolveRawKey(keyProvider);
2484
+ const currentKeyId = getSafeKeyIdentifier(rawKey);
1632
2485
  if (typeof rawKey !== "string" && rawKey.algorithm) {
1633
- if (!keyProvider) {
2486
+ const res2 = { cryptoKey: rawKey, kdf: "direct" };
2487
+ this._keyringCache.set(currentKeyId, res2);
2488
+ if (!keyProvider || keyProvider === this.defaultKey) {
1634
2489
  this._cachedKey = rawKey;
1635
2490
  this._cachedKdf = "direct";
1636
2491
  }
1637
- return { cryptoKey: rawKey, kdf: "direct" };
2492
+ return res2;
1638
2493
  }
1639
2494
  if (rawKey instanceof Uint8Array || rawKey instanceof ArrayBuffer) {
2495
+ const cached = this._keyringCache.get(currentKeyId);
2496
+ if (cached) {
2497
+ return cached;
2498
+ }
1640
2499
  if (!keyProvider && this._cachedKey && this._cachedKdf === "raw") {
1641
2500
  return { cryptoKey: this._cachedKey, kdf: "raw" };
1642
2501
  }
@@ -1653,11 +2512,13 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
1653
2512
  false,
1654
2513
  ["encrypt", "decrypt"]
1655
2514
  );
1656
- if (!keyProvider) {
2515
+ const res2 = { cryptoKey: cryptoKey2, kdf: "raw" };
2516
+ this._keyringCache.set(currentKeyId, res2);
2517
+ if (!keyProvider || keyProvider === this.defaultKey) {
1657
2518
  this._cachedKey = cryptoKey2;
1658
2519
  this._cachedKdf = "raw";
1659
2520
  }
1660
- return { cryptoKey: cryptoKey2, kdf: "raw" };
2521
+ return res2;
1661
2522
  }
1662
2523
  const passphrase = rawKey;
1663
2524
  const kdf = (kdfOverride || options?.kdf || this._defaultKdf || "pbkdf2").toLowerCase();
@@ -1667,12 +2528,21 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
1667
2528
  const matchesHash = !targetHash || this._cachedHash !== void 0 && targetHash.toLowerCase() === this._cachedHash.toLowerCase();
1668
2529
  const matchesKdf = this._cachedKdf !== void 0 && kdf === this._cachedKdf.toLowerCase();
1669
2530
  const matchesSalt = !saltOverride || this._cachedSalt !== void 0 && saltOverride === this._cachedSalt;
1670
- const currentKeyId = getSafeKeyIdentifier(rawKey);
1671
2531
  if (this._cachedKey && matchesSalt && !options?.kdfSalt && matchesKdf && matchesIter && matchesHash) {
1672
2532
  if (this._cachedKeyId === currentKeyId) {
1673
2533
  return { cryptoKey: this._cachedKey, salt: this._cachedSalt, kdf: this._cachedKdf, iter: this._cachedIter };
1674
2534
  }
1675
2535
  }
2536
+ if (!saltOverride && !options?.kdfSalt) {
2537
+ const cached = this._keyringCache.get(currentKeyId);
2538
+ if (cached) {
2539
+ const cMatchesKdf = !cached.kdf || cached.kdf.toLowerCase() === kdf;
2540
+ const cMatchesIter = !targetIter || cached.iter === targetIter;
2541
+ if (cMatchesKdf && cMatchesIter) {
2542
+ return cached;
2543
+ }
2544
+ }
2545
+ }
1676
2546
  if (kdf === "pbkdf2") {
1677
2547
  const salt = saltOverride || options?.kdfSalt || this._kdfOptions?.salt;
1678
2548
  const rawIterations = iterationsOverride || options?.kdfIterations || this._kdfOptions?.iterations || 31e4;
@@ -1684,15 +2554,19 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
1684
2554
  iterations,
1685
2555
  hash
1686
2556
  });
2557
+ const res2 = { cryptoKey: derived.key, salt: derived.salt, kdf: "PBKDF2", iter: iterations };
1687
2558
  if (!saltOverride && !options?.kdfSalt) {
1688
- this._cachedKey = derived.key;
1689
- this._cachedSalt = derived.salt;
1690
- this._cachedKdf = "PBKDF2";
1691
- this._cachedIter = iterations;
1692
- this._cachedHash = hash;
1693
- this._cachedKeyId = currentKeyId;
2559
+ this._keyringCache.set(currentKeyId, res2);
2560
+ if (!keyProvider || keyProvider === this.defaultKey) {
2561
+ this._cachedKey = derived.key;
2562
+ this._cachedSalt = derived.salt;
2563
+ this._cachedKdf = "PBKDF2";
2564
+ this._cachedIter = iterations;
2565
+ this._cachedHash = hash;
2566
+ this._cachedKeyId = currentKeyId;
2567
+ }
1694
2568
  }
1695
- return { cryptoKey: derived.key, salt: derived.salt, kdf: "PBKDF2", iter: iterations };
2569
+ return res2;
1696
2570
  }
1697
2571
  if (kdf === "hkdf") {
1698
2572
  if (typeof rawKey === "string") {
@@ -1709,14 +2583,18 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
1709
2583
  hash,
1710
2584
  info
1711
2585
  });
2586
+ const res2 = { cryptoKey: derived.key, salt: derived.salt, kdf: "HKDF" };
1712
2587
  if (!saltOverride && !options?.kdfSalt) {
1713
- this._cachedKey = derived.key;
1714
- this._cachedSalt = derived.salt;
1715
- this._cachedKdf = "HKDF";
1716
- this._cachedHash = hash;
1717
- this._cachedKeyId = currentKeyId;
2588
+ this._keyringCache.set(currentKeyId, res2);
2589
+ if (!keyProvider || keyProvider === this.defaultKey) {
2590
+ this._cachedKey = derived.key;
2591
+ this._cachedSalt = derived.salt;
2592
+ this._cachedKdf = "HKDF";
2593
+ this._cachedHash = hash;
2594
+ this._cachedKeyId = currentKeyId;
2595
+ }
1718
2596
  }
1719
- return { cryptoKey: derived.key, salt: derived.salt, kdf: "HKDF" };
2597
+ return res2;
1720
2598
  }
1721
2599
  if (kdf === "none") {
1722
2600
  if (saltOverride) {
@@ -1748,11 +2626,13 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
1748
2626
  } finally {
1749
2627
  new Uint8Array(keyHash).fill(0);
1750
2628
  }
1751
- if (!keyProvider) {
2629
+ const res = { cryptoKey, kdf: "none" };
2630
+ this._keyringCache.set(currentKeyId, res);
2631
+ if (!keyProvider || keyProvider === this.defaultKey) {
1752
2632
  this._cachedKey = cryptoKey;
1753
2633
  this._cachedKdf = "none";
1754
2634
  }
1755
- return { cryptoKey, kdf: "none" };
2635
+ return res;
1756
2636
  }
1757
2637
  static isEncrypted(payload) {
1758
2638
  if (!payload || typeof payload !== "object") return false;
@@ -1762,6 +2642,29 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
1762
2642
  return CWebCryptoEncryptionProvider.isEncrypted(payload);
1763
2643
  }
1764
2644
  async warmupKey(options) {
2645
+ const effectiveKeys = options?.keys || this.keys;
2646
+ if (effectiveKeys && Object.keys(effectiveKeys).length > 0) {
2647
+ const promises = Object.entries(effectiveKeys).map(async ([kid, keyProv]) => {
2648
+ const res2 = await this.resolveCryptoKey(keyProv, void 0, void 0, { ...options, key: keyProv, kid });
2649
+ const rawKey = await this.resolveRawKey(keyProv);
2650
+ const keyId = getSafeKeyIdentifier(rawKey);
2651
+ this._keyringCache.set(keyId, res2);
2652
+ this._keyringCache.set(`kid:${kid}`, res2);
2653
+ return { kid, res: res2 };
2654
+ });
2655
+ const results = await Promise.all(promises);
2656
+ const effectiveActiveKid = options?.activeKid || this.activeKid;
2657
+ const primary = effectiveActiveKid && results.find((r) => r.kid === effectiveActiveKid)?.res || results[0]?.res;
2658
+ if (primary) {
2659
+ this._cachedKey = primary.cryptoKey;
2660
+ this._cachedSalt = primary.salt;
2661
+ this._cachedKdf = primary.kdf;
2662
+ this._cachedIter = primary.iter;
2663
+ this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
2664
+ this._cachedKeyId = getKeyIdentifier(options?.key, this.defaultKey);
2665
+ }
2666
+ return;
2667
+ }
1765
2668
  const res = await this.resolveCryptoKey(options?.key, void 0, void 0, options);
1766
2669
  this._cachedKey = res.cryptoKey;
1767
2670
  this._cachedSalt = res.salt;
@@ -1804,7 +2707,7 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
1804
2707
  cryptoKey,
1805
2708
  textEncoder$1.encode(jsonString)
1806
2709
  );
1807
- const kid = options?.kid || options?.keyId || this._configuredKid;
2710
+ const kid = options?.kid || options?.keyId || this.activeKid || this._configuredKid;
1808
2711
  return {
1809
2712
  __encrypted: true,
1810
2713
  ciphertext: CKeyManager.toBase64(ciphertextBuffer),
@@ -3127,12 +4030,26 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3127
4030
  _cachedHash;
3128
4031
  _cachedKeyId;
3129
4032
  _configuredKid;
4033
+ _keyringCache = /* @__PURE__ */ new Map();
4034
+ isKeyRetained(key) {
4035
+ if (key === this._cachedKey) return true;
4036
+ for (const entry of this._keyringCache.values()) {
4037
+ if (entry.key === key) return true;
4038
+ }
4039
+ return false;
4040
+ }
3130
4041
  setDefaultKey(key) {
3131
4042
  super.setDefaultKey(key);
3132
4043
  if (this._cachedKey) {
3133
4044
  this._cachedKey.fill(0);
3134
4045
  this._cachedKey = void 0;
3135
4046
  }
4047
+ for (const entry of this._keyringCache.values()) {
4048
+ if (entry.key !== this._cachedKey) {
4049
+ entry.key.fill(0);
4050
+ }
4051
+ }
4052
+ this._keyringCache.clear();
3136
4053
  this._cachedSalt = void 0;
3137
4054
  this._cachedKdf = void 0;
3138
4055
  this._cachedIter = void 0;
@@ -3145,6 +4062,12 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3145
4062
  this._cachedKey.fill(0);
3146
4063
  this._cachedKey = void 0;
3147
4064
  }
4065
+ for (const entry of this._keyringCache.values()) {
4066
+ if (entry.key !== this._cachedKey) {
4067
+ entry.key.fill(0);
4068
+ }
4069
+ }
4070
+ this._keyringCache.clear();
3148
4071
  this._cachedSalt = void 0;
3149
4072
  this._cachedKdf = void 0;
3150
4073
  this._cachedIter = void 0;
@@ -3152,6 +4075,29 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3152
4075
  this._cachedKeyId = void 0;
3153
4076
  }
3154
4077
  async warmupKey(options) {
4078
+ const effectiveKeys = options?.keys || this.keys;
4079
+ if (effectiveKeys && Object.keys(effectiveKeys).length > 0) {
4080
+ const promises = Object.entries(effectiveKeys).map(async ([kid, keyProv]) => {
4081
+ const res2 = await this.deriveKeyAsync(keyProv, void 0, void 0, { ...options, key: keyProv, kid });
4082
+ const raw = await this.resolveRawKeyAsync(keyProv);
4083
+ const keyId = getSafeKeyIdentifier(raw);
4084
+ this._keyringCache.set(keyId, res2);
4085
+ this._keyringCache.set(`kid:${kid}`, res2);
4086
+ return { kid, res: res2 };
4087
+ });
4088
+ const results = await Promise.all(promises);
4089
+ const effectiveActiveKid = options?.activeKid || this.activeKid;
4090
+ const primary = effectiveActiveKid && results.find((r) => r.kid === effectiveActiveKid)?.res || results[0]?.res;
4091
+ if (primary) {
4092
+ this._cachedKey = primary.key;
4093
+ this._cachedSalt = primary.salt;
4094
+ this._cachedKdf = primary.kdf;
4095
+ this._cachedIter = primary.iter;
4096
+ this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
4097
+ this._cachedKeyId = getSafeKeyIdentifier(await this.resolveRawKeyAsync(options?.key));
4098
+ }
4099
+ return;
4100
+ }
3155
4101
  const res = await this.deriveKeyAsync(options?.key, void 0, void 0, options);
3156
4102
  if (!options?.key) {
3157
4103
  this._cachedKey = res.key;
@@ -3166,6 +4112,30 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3166
4112
  * Synchronously pre-derives and caches the encryption key for subsequent synchronous operations.
3167
4113
  */
3168
4114
  warmupKeySync(options) {
4115
+ const effectiveKeys = options?.keys || this.keys;
4116
+ if (effectiveKeys && Object.keys(effectiveKeys).length > 0) {
4117
+ const entries = Object.entries(effectiveKeys);
4118
+ const results = [];
4119
+ for (const [kid, keyProv] of entries) {
4120
+ const res2 = this.deriveKeySync(keyProv, void 0, void 0, { ...options, key: keyProv, kid });
4121
+ const raw = this.resolveRawKeySync(keyProv);
4122
+ const keyId = getSafeKeyIdentifier(raw);
4123
+ this._keyringCache.set(keyId, res2);
4124
+ this._keyringCache.set(`kid:${kid}`, res2);
4125
+ results.push({ kid, res: res2 });
4126
+ }
4127
+ const effectiveActiveKid = options?.activeKid || this.activeKid;
4128
+ const primary = effectiveActiveKid && results.find((r) => r.kid === effectiveActiveKid)?.res || results[0]?.res;
4129
+ if (primary) {
4130
+ this._cachedKey = primary.key;
4131
+ this._cachedSalt = primary.salt;
4132
+ this._cachedKdf = primary.kdf;
4133
+ this._cachedIter = primary.iter;
4134
+ this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
4135
+ this._cachedKeyId = getSafeKeyIdentifier(this.resolveRawKeySync(options?.key));
4136
+ }
4137
+ return;
4138
+ }
3169
4139
  const res = this.deriveKeySync(options?.key, void 0, void 0, options);
3170
4140
  if (!options?.key) {
3171
4141
  this._cachedKey = res.key;
@@ -3184,7 +4154,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3184
4154
  return Buffer.from(infoRaw);
3185
4155
  }
3186
4156
  constructor(key, options) {
3187
- super(key);
4157
+ super(key, options);
3188
4158
  if (options) {
3189
4159
  if (options.kid || options.keyId) {
3190
4160
  this._configuredKid = options.kid || options.keyId;
@@ -3202,7 +4172,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3202
4172
  }
3203
4173
  }
3204
4174
  resolveRawKeySync(keyProvider) {
3205
- const target = keyProvider || this.defaultKey;
4175
+ const target = keyProvider || (this.activeKid && this.keys ? this.keys[this.activeKid] : void 0) || this.defaultKey;
3206
4176
  if (!target) {
3207
4177
  throw new Error("[CNodeCryptoEncryptionProvider] No encryption key or keyProvider was supplied.");
3208
4178
  }
@@ -3232,7 +4202,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3232
4202
  return rawKey;
3233
4203
  }
3234
4204
  async resolveRawKeyAsync(keyProvider) {
3235
- const target = keyProvider || this.defaultKey;
4205
+ const target = keyProvider || (this.activeKid && this.keys ? this.keys[this.activeKid] : void 0) || this.defaultKey;
3236
4206
  if (!target) {
3237
4207
  throw new Error("[CNodeCryptoEncryptionProvider] No encryption key or keyProvider was supplied.");
3238
4208
  }
@@ -3263,15 +4233,16 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3263
4233
  const currentKeyId = getSafeKeyIdentifier(raw);
3264
4234
  if (Buffer.isBuffer(raw)) {
3265
4235
  if (raw.length === 32) {
3266
- if (!keyProvider) {
3267
- if (!this._cachedKey || this._cachedKeyId !== currentKeyId) {
3268
- this._cachedKey = Buffer.from(raw);
3269
- this._cachedKdf = "raw";
3270
- this._cachedKeyId = currentKeyId;
3271
- }
3272
- return { key: this._cachedKey, kdf: "raw" };
4236
+ const cached = this._keyringCache.get(currentKeyId);
4237
+ if (cached) return cached;
4238
+ const res2 = { key: Buffer.from(raw), kdf: "raw" };
4239
+ this._keyringCache.set(currentKeyId, res2);
4240
+ if (!keyProvider || keyProvider === this.defaultKey) {
4241
+ this._cachedKey = res2.key;
4242
+ this._cachedKdf = "raw";
4243
+ this._cachedKeyId = currentKeyId;
3273
4244
  }
3274
- return { key: Buffer.from(raw), kdf: "raw" };
4245
+ return res2;
3275
4246
  }
3276
4247
  throw new Error(
3277
4248
  `[CNodeCryptoEncryptionProvider] AES-GCM-256 requires a 32-byte key. Received a Buffer of ${raw.length} bytes. Provide exactly 32 bytes or a string passphrase.`
@@ -3290,6 +4261,16 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3290
4261
  return { key: this._cachedKey, salt: this._cachedSalt, kdf: this._cachedKdf, iter: this._cachedIter };
3291
4262
  }
3292
4263
  }
4264
+ if (!saltOverride && !options?.kdfSalt) {
4265
+ const cached = this._keyringCache.get(currentKeyId);
4266
+ if (cached) {
4267
+ const cMatchesKdf = !cached.kdf || cached.kdf.toLowerCase() === kdf;
4268
+ const cMatchesIter = !targetIter || cached.iter === targetIter;
4269
+ if (cMatchesKdf && cMatchesIter) {
4270
+ return cached;
4271
+ }
4272
+ }
4273
+ }
3293
4274
  if (kdf === "pbkdf2") {
3294
4275
  const s = saltOverride || options?.kdfSalt || this._kdfOptions?.salt;
3295
4276
  const saltBytes = CKeyManager.resolveSalt(s);
@@ -3298,20 +4279,24 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3298
4279
  const iterations = Math.min(Math.max(rawIter, 1e3), 6e5);
3299
4280
  const hash = (options?.kdfHash || this._kdfOptions?.hash || "SHA-256").toLowerCase().replace("-", "");
3300
4281
  const key2 = crypto__namespace.pbkdf2Sync(passphrase, saltBuf, iterations, 32, hash);
3301
- if (!keyProvider && !saltOverride && !options?.kdfSalt) {
3302
- this._cachedKey = key2;
3303
- this._cachedSalt = saltBuf.toString("base64");
3304
- this._cachedKdf = "PBKDF2";
3305
- this._cachedIter = iterations;
3306
- this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
3307
- this._cachedKeyId = currentKeyId;
3308
- }
3309
- return {
4282
+ const res2 = {
3310
4283
  key: key2,
3311
4284
  salt: saltBuf.toString("base64"),
3312
4285
  kdf: "PBKDF2",
3313
4286
  iter: iterations
3314
4287
  };
4288
+ if (!saltOverride && !options?.kdfSalt) {
4289
+ this._keyringCache.set(currentKeyId, res2);
4290
+ if (!keyProvider || keyProvider === this.defaultKey) {
4291
+ this._cachedKey = key2;
4292
+ this._cachedSalt = saltBuf.toString("base64");
4293
+ this._cachedKdf = "PBKDF2";
4294
+ this._cachedIter = iterations;
4295
+ this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
4296
+ this._cachedKeyId = currentKeyId;
4297
+ }
4298
+ }
4299
+ return res2;
3315
4300
  }
3316
4301
  if (kdf === "hkdf") {
3317
4302
  if (typeof raw === "string") {
@@ -3325,49 +4310,56 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3325
4310
  const hash = (options?.kdfHash || this._kdfOptions?.hash || "SHA-256").toLowerCase().replace("-", "");
3326
4311
  const info = this.resolveInfoBuffer(options?.info || this._kdfOptions?.info);
3327
4312
  const key2 = Buffer.from(crypto__namespace.hkdfSync(hash, passphrase, saltBuf, info, 32));
3328
- if (!keyProvider && !saltOverride && !options?.kdfSalt) {
3329
- this._cachedKey = key2;
3330
- this._cachedSalt = saltBuf.toString("base64");
3331
- this._cachedKdf = "HKDF";
3332
- this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
3333
- this._cachedKeyId = currentKeyId;
3334
- }
3335
- return {
4313
+ const res2 = {
3336
4314
  key: key2,
3337
4315
  salt: saltBuf.toString("base64"),
3338
4316
  kdf: "HKDF"
3339
4317
  };
4318
+ if (!saltOverride && !options?.kdfSalt) {
4319
+ this._keyringCache.set(currentKeyId, res2);
4320
+ if (!keyProvider || keyProvider === this.defaultKey) {
4321
+ this._cachedKey = key2;
4322
+ this._cachedSalt = saltBuf.toString("base64");
4323
+ this._cachedKdf = "HKDF";
4324
+ this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
4325
+ this._cachedKeyId = currentKeyId;
4326
+ }
4327
+ }
4328
+ return res2;
3340
4329
  }
3341
4330
  if (kdf === "none") {
3342
4331
  console.warn(
3343
4332
  "[CNodeCryptoEncryptionProvider] WARNING: Decrypting a legacy envelope with no KDF (raw SHA-256 key derivation). This is cryptographically weak. Migrate to pbkdf2 or hkdf."
3344
4333
  );
3345
4334
  }
3346
- if (!keyProvider && this._cachedKey && this._cachedKdf === "none" && this._cachedKeyId === currentKeyId) {
4335
+ if ((!keyProvider || keyProvider === this.defaultKey) && this._cachedKey && this._cachedKdf === "none" && this._cachedKeyId === currentKeyId) {
3347
4336
  return { key: this._cachedKey, kdf: "none" };
3348
4337
  }
3349
4338
  const key = crypto__namespace.createHash("sha256").update(passphrase, "utf8").digest();
3350
- if (!keyProvider) {
4339
+ const res = { key, kdf: "none" };
4340
+ this._keyringCache.set(currentKeyId, res);
4341
+ if (!keyProvider || keyProvider === this.defaultKey) {
3351
4342
  this._cachedKey = key;
3352
4343
  this._cachedKdf = "none";
3353
4344
  this._cachedKeyId = currentKeyId;
3354
4345
  }
3355
- return { key, kdf: "none" };
4346
+ return res;
3356
4347
  }
3357
4348
  async deriveKeyAsync(keyProvider, kdfOverride, saltOverride, options, iterationsOverride) {
3358
4349
  const raw = await this.resolveRawKeyAsync(keyProvider);
3359
4350
  const currentKeyId = getSafeKeyIdentifier(raw);
3360
4351
  if (Buffer.isBuffer(raw)) {
3361
4352
  if (raw.length === 32) {
3362
- if (!keyProvider) {
3363
- if (!this._cachedKey || this._cachedKeyId !== currentKeyId) {
3364
- this._cachedKey = Buffer.from(raw);
3365
- this._cachedKdf = "raw";
3366
- this._cachedKeyId = currentKeyId;
3367
- }
3368
- return { key: this._cachedKey, kdf: "raw" };
4353
+ const cached = this._keyringCache.get(currentKeyId);
4354
+ if (cached) return cached;
4355
+ const res2 = { key: Buffer.from(raw), kdf: "raw" };
4356
+ this._keyringCache.set(currentKeyId, res2);
4357
+ if (!keyProvider || keyProvider === this.defaultKey) {
4358
+ this._cachedKey = res2.key;
4359
+ this._cachedKdf = "raw";
4360
+ this._cachedKeyId = currentKeyId;
3369
4361
  }
3370
- return { key: Buffer.from(raw), kdf: "raw" };
4362
+ return res2;
3371
4363
  }
3372
4364
  throw new Error(
3373
4365
  `[CNodeCryptoEncryptionProvider] AES-GCM-256 requires a 32-byte key. Received a Buffer of ${raw.length} bytes. Provide exactly 32 bytes or a string passphrase.`
@@ -3386,6 +4378,16 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3386
4378
  return { key: this._cachedKey, salt: this._cachedSalt, kdf: this._cachedKdf, iter: this._cachedIter };
3387
4379
  }
3388
4380
  }
4381
+ if (!saltOverride && !options?.kdfSalt) {
4382
+ const cached = this._keyringCache.get(currentKeyId);
4383
+ if (cached) {
4384
+ const cMatchesKdf = !cached.kdf || cached.kdf.toLowerCase() === kdf;
4385
+ const cMatchesIter = !targetIter || cached.iter === targetIter;
4386
+ if (cMatchesKdf && cMatchesIter) {
4387
+ return cached;
4388
+ }
4389
+ }
4390
+ }
3389
4391
  if (kdf === "pbkdf2") {
3390
4392
  const s = saltOverride || options?.kdfSalt || this._kdfOptions?.salt;
3391
4393
  const saltBytes = CKeyManager.resolveSalt(s);
@@ -3399,20 +4401,24 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3399
4401
  else resolve(derived);
3400
4402
  });
3401
4403
  });
3402
- if (!keyProvider && !saltOverride && !options?.kdfSalt) {
3403
- this._cachedKey = key2;
3404
- this._cachedSalt = saltBuf.toString("base64");
3405
- this._cachedKdf = "PBKDF2";
3406
- this._cachedIter = iterations;
3407
- this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
3408
- this._cachedKeyId = currentKeyId;
3409
- }
3410
- return {
4404
+ const res2 = {
3411
4405
  key: key2,
3412
4406
  salt: saltBuf.toString("base64"),
3413
4407
  kdf: "PBKDF2",
3414
4408
  iter: iterations
3415
4409
  };
4410
+ if (!saltOverride && !options?.kdfSalt) {
4411
+ this._keyringCache.set(currentKeyId, res2);
4412
+ if (!keyProvider || keyProvider === this.defaultKey) {
4413
+ this._cachedKey = key2;
4414
+ this._cachedSalt = saltBuf.toString("base64");
4415
+ this._cachedKdf = "PBKDF2";
4416
+ this._cachedIter = iterations;
4417
+ this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
4418
+ this._cachedKeyId = currentKeyId;
4419
+ }
4420
+ }
4421
+ return res2;
3416
4422
  }
3417
4423
  if (kdf === "hkdf") {
3418
4424
  if (typeof raw === "string") {
@@ -3431,34 +4437,40 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3431
4437
  else resolve(Buffer.from(derived));
3432
4438
  });
3433
4439
  });
3434
- if (!keyProvider && !saltOverride && !options?.kdfSalt) {
3435
- this._cachedKey = key2;
3436
- this._cachedSalt = saltBuf.toString("base64");
3437
- this._cachedKdf = "HKDF";
3438
- this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
3439
- this._cachedKeyId = currentKeyId;
3440
- }
3441
- return {
4440
+ const res2 = {
3442
4441
  key: key2,
3443
4442
  salt: saltBuf.toString("base64"),
3444
4443
  kdf: "HKDF"
3445
4444
  };
4445
+ if (!saltOverride && !options?.kdfSalt) {
4446
+ this._keyringCache.set(currentKeyId, res2);
4447
+ if (!keyProvider || keyProvider === this.defaultKey) {
4448
+ this._cachedKey = key2;
4449
+ this._cachedSalt = saltBuf.toString("base64");
4450
+ this._cachedKdf = "HKDF";
4451
+ this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
4452
+ this._cachedKeyId = currentKeyId;
4453
+ }
4454
+ }
4455
+ return res2;
3446
4456
  }
3447
4457
  if (kdf === "none") {
3448
4458
  console.warn(
3449
4459
  "[CNodeCryptoEncryptionProvider] WARNING: Decrypting a legacy envelope with no KDF (raw SHA-256 key derivation). This is cryptographically weak. Migrate to pbkdf2 or hkdf."
3450
4460
  );
3451
4461
  }
3452
- if (!keyProvider && this._cachedKey && this._cachedKdf === "none" && this._cachedKeyId === currentKeyId) {
4462
+ if ((!keyProvider || keyProvider === this.defaultKey) && this._cachedKey && this._cachedKdf === "none" && this._cachedKeyId === currentKeyId) {
3453
4463
  return { key: this._cachedKey, kdf: "none" };
3454
4464
  }
3455
4465
  const key = crypto__namespace.createHash("sha256").update(passphrase, "utf8").digest();
3456
- if (!keyProvider) {
4466
+ const res = { key, kdf: "none" };
4467
+ this._keyringCache.set(currentKeyId, res);
4468
+ if (!keyProvider || keyProvider === this.defaultKey) {
3457
4469
  this._cachedKey = key;
3458
4470
  this._cachedKdf = "none";
3459
4471
  this._cachedKeyId = currentKeyId;
3460
4472
  }
3461
- return { key, kdf: "none" };
4473
+ return res;
3462
4474
  }
3463
4475
  static isEncrypted(payload) {
3464
4476
  if (!payload || typeof payload !== "object") return false;
@@ -3498,7 +4510,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3498
4510
  ]);
3499
4511
  const authTag = cipher.getAuthTag();
3500
4512
  const combinedCiphertext = Buffer.concat([encryptedBuffer, authTag]);
3501
- const kid = options?.kid || options?.keyId || this._configuredKid;
4513
+ const kid = options?.kid || options?.keyId || this.activeKid || this._configuredKid;
3502
4514
  return {
3503
4515
  __encrypted: true,
3504
4516
  ciphertext: combinedCiphertext.toString("base64"),
@@ -3511,7 +4523,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3511
4523
  ...hasAad ? { aad: true } : {}
3512
4524
  };
3513
4525
  } finally {
3514
- if (key !== this._cachedKey) {
4526
+ if (!this.isKeyRetained(key)) {
3515
4527
  key.fill(0);
3516
4528
  }
3517
4529
  }
@@ -3600,7 +4612,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3600
4612
  if (err instanceof SyntaxError) throw err;
3601
4613
  throw new Error(`[CNodeCryptoEncryptionProvider] Decryption failed: ${err?.message || "Authentication tag mismatch or corrupted ciphertext"}`);
3602
4614
  } finally {
3603
- if (key !== this._cachedKey) {
4615
+ if (!this.isKeyRetained(key)) {
3604
4616
  key.fill(0);
3605
4617
  }
3606
4618
  }
@@ -3609,6 +4621,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3609
4621
  encryptSync(data, options) {
3610
4622
  if (data === void 0 || data === null) return data;
3611
4623
  if (this.isEncrypted(data)) return data;
4624
+ options = this._resolveOutboundKeyringOptions(options);
3612
4625
  if (Array.isArray(data)) {
3613
4626
  if (options?.fields && options.fields.length > 0 || options?.valueEncryption === true) {
3614
4627
  return data.map((item) => this.encryptSync(item, options));
@@ -3644,7 +4657,12 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3644
4657
  decryptSync(payload, options) {
3645
4658
  if (payload === void 0 || payload === null) return payload;
3646
4659
  if (this.isEncrypted(payload)) {
3647
- return this.decryptValueSync(payload, options);
4660
+ const metaErr = this._validateEnvelopeMetadata(payload, options);
4661
+ if (metaErr) {
4662
+ return this._handleDecryptError(metaErr, payload, "", payload, void 0, options);
4663
+ }
4664
+ const envelopeKey = this._resolveDecryptionKeyForEnvelope(payload, options);
4665
+ return this.decryptValueSync(payload, { ...options, key: envelopeKey });
3648
4666
  }
3649
4667
  if (Array.isArray(payload)) {
3650
4668
  return payload.map((item) => this.decryptSync(item, options));
@@ -3658,28 +4676,35 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3658
4676
  for (const key of targetKeys) {
3659
4677
  if (this.isEncrypted(result[key])) {
3660
4678
  const fieldPayload = result[key];
4679
+ const metaErr = this._validateEnvelopeMetadata(fieldPayload, options);
4680
+ if (metaErr) {
4681
+ return this._handleDecryptError(metaErr, result, key, fieldPayload, void 0, options);
4682
+ }
4683
+ const envelopeKey = this._resolveDecryptionKeyForEnvelope(fieldPayload, options);
3661
4684
  const salt = fieldPayload.salt || "__no_salt__";
3662
- let derivedKey = keyCache.get(salt);
4685
+ const memoKey = `${salt}:${fieldPayload.kid || ""}`;
4686
+ let derivedKey = keyCache.get(memoKey);
3663
4687
  if (!derivedKey) {
3664
4688
  const kdfFromEnvelope = fieldPayload.kdf ? fieldPayload.kdf.toLowerCase() : "none";
3665
4689
  const derived = this.deriveKeySync(
3666
- options?.key,
4690
+ envelopeKey,
3667
4691
  kdfFromEnvelope,
3668
4692
  fieldPayload.salt,
3669
4693
  options,
3670
4694
  fieldPayload.iter
3671
4695
  );
3672
4696
  derivedKey = derived.key;
3673
- keyCache.set(salt, derivedKey);
4697
+ keyCache.set(memoKey, derivedKey);
3674
4698
  }
3675
- const boundOptions = this._bindFieldAad(payload, key, options);
4699
+ const boundOptions = this._bindFieldAad(payload, key, { ...options, key: envelopeKey });
3676
4700
  const hasBoundAad = boundOptions.aad !== options?.aad;
3677
4701
  try {
3678
4702
  safeSetProperty(result, key, this.decryptCiphertextWithKey(fieldPayload, derivedKey, boundOptions));
3679
4703
  } catch (err) {
3680
- if (hasBoundAad && isAuthOrDecryptionError(err)) {
4704
+ const canFallback = hasBoundAad && (!options?.aadFields || options.aadFields.length === 0) && typeof options?.aad !== "function";
4705
+ if (canFallback && isAuthOrDecryptionError(err)) {
3681
4706
  try {
3682
- safeSetProperty(result, key, this.decryptCiphertextWithKey(fieldPayload, derivedKey, options));
4707
+ safeSetProperty(result, key, this.decryptCiphertextWithKey(fieldPayload, derivedKey, { ...options, key: envelopeKey }));
3683
4708
  } catch (fallbackErr) {
3684
4709
  throw fallbackErr;
3685
4710
  }
@@ -3693,7 +4718,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3693
4718
  return mutated ? result : payload;
3694
4719
  } finally {
3695
4720
  for (const k of keyCache.values()) {
3696
- if (k !== this._cachedKey) {
4721
+ if (!this.isKeyRetained(k)) {
3697
4722
  k.fill(0);
3698
4723
  }
3699
4724
  }
@@ -3741,7 +4766,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3741
4766
  ]);
3742
4767
  const authTag = cipher.getAuthTag();
3743
4768
  const combined = Buffer.concat([ciphertext, authTag]);
3744
- const kid = options?.kid || options?.keyId || this._configuredKid;
4769
+ const kid = options?.kid || options?.keyId || this.activeKid || this._configuredKid;
3745
4770
  return {
3746
4771
  __encrypted: true,
3747
4772
  ciphertext: combined.toString("base64"),
@@ -3754,7 +4779,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3754
4779
  ...hasAad ? { aad: true } : {}
3755
4780
  };
3756
4781
  } finally {
3757
- if (key !== this._cachedKey) {
4782
+ if (!this.isKeyRetained(key)) {
3758
4783
  key.fill(0);
3759
4784
  }
3760
4785
  }
@@ -3778,7 +4803,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3778
4803
  if (err instanceof SyntaxError) throw err;
3779
4804
  throw new Error(`[CNodeCryptoEncryptionProvider] Decryption failed: ${err?.message || "Authentication tag mismatch or corrupted ciphertext"}`);
3780
4805
  } finally {
3781
- if (key !== this._cachedKey) {
4806
+ if (!this.isKeyRetained(key)) {
3782
4807
  key.fill(0);
3783
4808
  }
3784
4809
  }
@@ -3786,7 +4811,12 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3786
4811
  async decrypt(payload, options) {
3787
4812
  if (payload === void 0 || payload === null) return payload;
3788
4813
  if (this.isEncrypted(payload)) {
3789
- return this.decryptValue(payload, options);
4814
+ const metaErr = this._validateEnvelopeMetadata(payload, options);
4815
+ if (metaErr) {
4816
+ return this._handleDecryptError(metaErr, payload, "", payload, void 0, options);
4817
+ }
4818
+ const envelopeKey = this._resolveDecryptionKeyForEnvelope(payload, options);
4819
+ return this.decryptValue(payload, { ...options, key: envelopeKey });
3790
4820
  }
3791
4821
  if (Array.isArray(payload)) {
3792
4822
  return Promise.all(payload.map((item) => this.decrypt(item, options)));
@@ -3800,28 +4830,35 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3800
4830
  for (const key of targetKeys) {
3801
4831
  if (this.isEncrypted(result[key])) {
3802
4832
  const fieldPayload = result[key];
4833
+ const metaErr = this._validateEnvelopeMetadata(fieldPayload, options);
4834
+ if (metaErr) {
4835
+ return this._handleDecryptError(metaErr, result, key, fieldPayload, void 0, options);
4836
+ }
4837
+ const envelopeKey = this._resolveDecryptionKeyForEnvelope(fieldPayload, options);
3803
4838
  const salt = fieldPayload.salt || "__no_salt__";
3804
- let derivedKey = keyCache.get(salt);
4839
+ const memoKey = `${salt}:${fieldPayload.kid || ""}`;
4840
+ let derivedKey = keyCache.get(memoKey);
3805
4841
  if (!derivedKey) {
3806
4842
  const kdfFromEnvelope = fieldPayload.kdf ? fieldPayload.kdf.toLowerCase() : "none";
3807
4843
  const derived = await this.deriveKeyAsync(
3808
- options?.key,
4844
+ envelopeKey,
3809
4845
  kdfFromEnvelope,
3810
4846
  fieldPayload.salt ? CKeyManager.fromBase64(fieldPayload.salt) : void 0,
3811
4847
  options,
3812
4848
  fieldPayload.iter
3813
4849
  );
3814
4850
  derivedKey = derived.key;
3815
- keyCache.set(salt, derivedKey);
4851
+ keyCache.set(memoKey, derivedKey);
3816
4852
  }
3817
- const boundOptions = this._bindFieldAad(payload, key, options);
4853
+ const boundOptions = this._bindFieldAad(payload, key, { ...options, key: envelopeKey });
3818
4854
  const hasBoundAad = boundOptions.aad !== options?.aad;
3819
4855
  try {
3820
4856
  safeSetProperty(result, key, this.decryptCiphertextWithKey(fieldPayload, derivedKey, boundOptions));
3821
4857
  } catch (err) {
3822
- if (hasBoundAad && isAuthOrDecryptionError(err)) {
4858
+ const canFallback = hasBoundAad && (!options?.aadFields || options.aadFields.length === 0) && typeof options?.aad !== "function";
4859
+ if (canFallback && isAuthOrDecryptionError(err)) {
3823
4860
  try {
3824
- safeSetProperty(result, key, this.decryptCiphertextWithKey(fieldPayload, derivedKey, options));
4861
+ safeSetProperty(result, key, this.decryptCiphertextWithKey(fieldPayload, derivedKey, { ...options, key: envelopeKey }));
3825
4862
  } catch (fallbackErr) {
3826
4863
  throw fallbackErr;
3827
4864
  }
@@ -3835,7 +4872,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3835
4872
  return mutated ? result : payload;
3836
4873
  } finally {
3837
4874
  for (const k of keyCache.values()) {
3838
- if (k !== this._cachedKey) {
4875
+ if (!this.isKeyRetained(k)) {
3839
4876
  k.fill(0);
3840
4877
  }
3841
4878
  }
@@ -3866,7 +4903,11 @@ function createEncryptionProvider(options = {}) {
3866
4903
  kdfIterations: options.kdfIterations,
3867
4904
  kdfHash: options.kdfHash,
3868
4905
  kdfSalt: options.kdfSalt,
3869
- info: options.info
4906
+ info: options.info,
4907
+ kid: options.kid || options.keyId,
4908
+ keys: options.keys,
4909
+ activeKid: options.activeKid,
4910
+ allowedKids: options.allowedKids
3870
4911
  });
3871
4912
  }
3872
4913
  if (alg === "AES-CBC") {
@@ -3903,7 +4944,11 @@ function createEncryptionProvider(options = {}) {
3903
4944
  kdfIterations: options.kdfIterations,
3904
4945
  kdfHash: options.kdfHash,
3905
4946
  kdfSalt: options.kdfSalt,
3906
- info: options.info
4947
+ info: options.info,
4948
+ kid: options.kid || options.keyId,
4949
+ keys: options.keys,
4950
+ activeKid: options.activeKid,
4951
+ allowedKids: options.allowedKids
3907
4952
  });
3908
4953
  }
3909
4954
  throw new Error(
@@ -3966,7 +5011,19 @@ function isDataEncrypted(provider, data) {
3966
5011
  function buildEncryptionContext(encryption, url, _defaultIdKey) {
3967
5012
  return {
3968
5013
  url,
5014
+ aad: encryption?.aad,
5015
+ aadFields: encryption?.aadFields,
5016
+ baseAad: encryption?.baseAad,
5017
+ bindRequestContextAad: encryption?.bindRequestContextAad,
5018
+ strict: encryption?.strict,
5019
+ allowedAlgorithms: encryption?.allowedAlgorithms,
5020
+ allowedKids: encryption?.allowedKids,
5021
+ onDecryptError: encryption?.onDecryptError,
3969
5022
  key: encryption?.key,
5023
+ kid: encryption?.kid,
5024
+ keyId: encryption?.keyId,
5025
+ keys: encryption?.keys,
5026
+ activeKid: encryption?.activeKid,
3970
5027
  valueEncryption: encryption?.valueEncryption,
3971
5028
  fields: encryption?.fields,
3972
5029
  publicKey: encryption?.publicKey,
@@ -3997,7 +5054,7 @@ async function decryptPageRows(rows, encryption, apiUrl, idKey) {
3997
5054
  }
3998
5055
  return isDataEncrypted(cipherProvider, row);
3999
5056
  });
4000
- if (!hasEncryptedRows) {
5057
+ if (!hasEncryptedRows && !encryption?.strict) {
4001
5058
  return { rows, decrypted: false };
4002
5059
  }
4003
5060
  const ctx = buildEncryptionContext(encryption, apiUrl);
@@ -4160,6 +5217,13 @@ class CSearchIndex {
4160
5217
  const signature = await subtle.sign("HMAC", key, textEncoder.encode(normalized));
4161
5218
  return Array.from(new Uint8Array(signature)).map((b) => b.toString(16).padStart(2, "0")).join("");
4162
5219
  }
5220
+ /**
5221
+ * Computes a deterministic HMAC-SHA256 blind index hash for a term using the configured HMAC key.
5222
+ * Performs Unicode NFKC normalization, trimming, and lowercasing.
5223
+ */
5224
+ async computeBlindIndex(term) {
5225
+ return this.computeToken(term);
5226
+ }
4163
5227
  /**
4164
5228
  * Computes HMAC-SHA256 tokens for all trigrams of a term.
4165
5229
  */
@@ -4189,9 +5253,21 @@ class CSearchIndex {
4189
5253
  const token = await this.computeToken(rawVal);
4190
5254
  const trigrams = await this.computeTrigramTokens(rawVal);
4191
5255
  fieldMap.set(field, { token, trigrams });
5256
+ } else if (rawVal && typeof rawVal === "object" && rawVal.__encrypted === true) {
5257
+ const precomputedToken = row[`${field}_bidx`] || row[`${field}_token`];
5258
+ if (typeof precomputedToken === "string" && precomputedToken) {
5259
+ fieldMap.set(field, { token: precomputedToken, trigrams: /* @__PURE__ */ new Set() });
5260
+ }
4192
5261
  }
4193
5262
  }
4194
5263
  }
5264
+ /**
5265
+ * Indexes a row using pre-decrypted plaintext field values.
5266
+ * Allows indexing rows that contain pre-encrypted ciphertext envelopes in allData.
5267
+ */
5268
+ async indexRowDecrypted(row, decryptedPlaintext) {
5269
+ return this.indexRow(row, decryptedPlaintext);
5270
+ }
4195
5271
  /**
4196
5272
  * Indexes an array of rows.
4197
5273
  */
@@ -4240,7 +5316,7 @@ class CSearchIndex {
4240
5316
  }
4241
5317
  return true;
4242
5318
  }
4243
- if (operator === EDataFilterOperator.NotIn || operator === "notIn") {
5319
+ if (operator === EDataFilterOperator.NotIn || operator === "notIn" || operator === "nin") {
4244
5320
  const queryTrigramTokens = await this.computeTrigramTokens(filterValue);
4245
5321
  if (queryTrigramTokens.size === 0) return false;
4246
5322
  for (const tok of queryTrigramTokens) {
@@ -4311,7 +5387,7 @@ class CSearchIndex {
4311
5387
  }
4312
5388
  return true;
4313
5389
  });
4314
- } else if (op === EDataFilterOperator.NotIn || op === "notIn") {
5390
+ } else if (op === EDataFilterOperator.NotIn || op === "notIn" || op === "nin") {
4315
5391
  const queryTrigrams = await this.computeTrigramTokens(filterValue);
4316
5392
  checks.push((row) => {
4317
5393
  const entry = this.getEntry(row, key);
@@ -4430,6 +5506,36 @@ class AEncryptedClientProvider extends ADataProvider {
4430
5506
  } : void 0;
4431
5507
  return await encryptRowsChunked(rows, this.encryption, options, void 0, this.idKey, indexChunk);
4432
5508
  }
5509
+ /**
5510
+ * Indexes pre-encrypted rows by performing a transient in-memory decryption pass
5511
+ * on search fields to populate CSearchIndex without mutating underlying encrypted records in allData.
5512
+ */
5513
+ async indexPreEncryptedRows(rows) {
5514
+ if (!this.isEncryptionEnabled || !this._searchIndex || !rows || rows.length === 0) {
5515
+ return;
5516
+ }
5517
+ const cipher = getCipherProvider(this.encryption);
5518
+ if (!cipher) return;
5519
+ const ctx = buildEncryptionContext(this.encryption, this.apiUrl, this.idKey);
5520
+ let decryptedRows = [];
5521
+ try {
5522
+ const decResult = await cipher.decrypt(rows, ctx);
5523
+ decryptedRows = Array.isArray(decResult) ? decResult : [decResult];
5524
+ } catch (err) {
5525
+ console.warn("[Katlux Providers] indexPreEncryptedRows: Decryption failed during search index build", err);
5526
+ return;
5527
+ }
5528
+ await Promise.all(rows.map((row, idx) => {
5529
+ const decRow = decryptedRows[idx];
5530
+ const plainFields = {};
5531
+ for (const f of this.encryption?.fields || Object.keys(decRow || {})) {
5532
+ if (decRow && decRow[f] !== void 0) {
5533
+ plainFields[f] = decRow[f];
5534
+ }
5535
+ }
5536
+ return this._searchIndex.indexRow(row, plainFields);
5537
+ }));
5538
+ }
4433
5539
  async decryptRows(rows) {
4434
5540
  if (!this.isEncryptionEnabled || !rows || rows.length === 0) {
4435
5541
  return { rows, decrypted: false };
@@ -4738,6 +5844,15 @@ class CFlatClientDataProvider extends AEncryptedClientProvider {
4738
5844
  this.loading = true;
4739
5845
  try {
4740
5846
  const rows = Array.isArray(data) ? data : [];
5847
+ const isPreEnc = this.encryption?.preEncrypted ?? isDataEncrypted(void 0, rows);
5848
+ if (isPreEnc) {
5849
+ this.allData = rows;
5850
+ if (this.searchIndex) {
5851
+ await this.indexPreEncryptedRows(rows);
5852
+ }
5853
+ await this.loadPageData();
5854
+ return;
5855
+ }
4741
5856
  const encryptedRows = await this.encryptRows(rows);
4742
5857
  this.allData = encryptedRows;
4743
5858
  await this.loadPageData();
@@ -4758,6 +5873,16 @@ class CFlatClientDataProvider extends AEncryptedClientProvider {
4758
5873
  this.loading = true;
4759
5874
  try {
4760
5875
  const rows = Array.isArray(data) ? data : [];
5876
+ const isPreEnc = this.encryption?.preEncrypted ?? isDataEncrypted(void 0, rows);
5877
+ if (isPreEnc) {
5878
+ this.allData = rows;
5879
+ if (this.searchIndex) {
5880
+ await this.indexPreEncryptedRows(rows);
5881
+ }
5882
+ options?.onProgress?.({ processed: rows.length, total: rows.length, percent: 100 });
5883
+ await this.loadPageData();
5884
+ return;
5885
+ }
4761
5886
  const encryptedRows = await this.encryptRowsChunked(rows, options);
4762
5887
  this.allData = encryptedRows;
4763
5888
  await this.loadPageData();
@@ -6704,8 +7829,18 @@ class RequestProvider {
6704
7829
  method,
6705
7830
  isServer,
6706
7831
  aad: encOptions.aad,
7832
+ aadFields: encOptions.aadFields,
7833
+ baseAad: encOptions.baseAad,
6707
7834
  bindRequestContextAad: encOptions.bindRequestContextAad,
7835
+ strict: encOptions.strict,
7836
+ allowedAlgorithms: encOptions.allowedAlgorithms,
7837
+ allowedKids: encOptions.allowedKids,
7838
+ onDecryptError: encOptions.onDecryptError,
6708
7839
  key: encOptions.key,
7840
+ kid: encOptions.kid,
7841
+ keyId: encOptions.keyId,
7842
+ keys: encOptions.keys,
7843
+ activeKid: encOptions.activeKid,
6709
7844
  valueEncryption: encOptions.valueEncryption,
6710
7845
  fields: encOptions.fields,
6711
7846
  publicKey: encOptions.publicKey,
@@ -6894,17 +8029,26 @@ class CAPIFlatClientDataProvider extends CFlatClientDataProvider {
6894
8029
  if (data) {
6895
8030
  const normalized = normalizeDataResult(data);
6896
8031
  const rows = normalized ? normalized.rows : data.rows || data;
8032
+ const incomingRows = Array.isArray(rows) ? rows : [];
6897
8033
  if (this._isAccumulating) {
6898
- this.allData = [...this.allData, ...rows];
8034
+ this.allData = [...this.allData, ...incomingRows];
6899
8035
  } else {
6900
- this.allData = Array.isArray(rows) ? rows : [];
8036
+ this.allData = incomingRows;
6901
8037
  }
6902
8038
  this._filterDirty = true;
6903
8039
  this._sortDirty = true;
6904
8040
  if (normalized?.rowCount !== void 0) {
6905
8041
  this.rowCount = normalized.rowCount;
6906
8042
  }
6907
- this._lastDataReceivedPromise = this.loadPageData();
8043
+ this._lastDataReceivedPromise = (async () => {
8044
+ if (this.isEncryptionEnabled && this.searchIndex && incomingRows.length > 0) {
8045
+ const isPreEnc = this.encryption?.preEncrypted ?? isDataEncrypted(void 0, incomingRows);
8046
+ if (isPreEnc) {
8047
+ await this.indexPreEncryptedRows(incomingRows);
8048
+ }
8049
+ }
8050
+ await this.loadPageData();
8051
+ })();
6908
8052
  }
6909
8053
  });
6910
8054
  }
@@ -6990,6 +8134,12 @@ class CAPIFlatClientDataProvider extends CFlatClientDataProvider {
6990
8134
  this.allData = normalized.rows;
6991
8135
  this._filterDirty = true;
6992
8136
  this._sortDirty = true;
8137
+ if (this.isEncryptionEnabled && this.searchIndex && this.allData.length > 0) {
8138
+ const isPreEnc = this.encryption?.preEncrypted ?? isDataEncrypted(void 0, this.allData);
8139
+ if (isPreEnc) {
8140
+ await this.indexPreEncryptedRows(this.allData);
8141
+ }
8142
+ }
6993
8143
  await this.loadPageData();
6994
8144
  this.initialLoad = false;
6995
8145
  this.loading = false;
@@ -10244,6 +11394,7 @@ exports.CTreeServerDataProvider = CTreeServerDataProvider;
10244
11394
  exports.CWebCryptoAESCBCProvider = CWebCryptoAESCBCProvider;
10245
11395
  exports.CWebCryptoEncryptionProvider = CWebCryptoEncryptionProvider;
10246
11396
  exports.CacheProviderFactory = CacheProviderFactory;
11397
+ exports.DECRYPT_ERROR_SENTINEL = DECRYPT_ERROR_SENTINEL;
10247
11398
  exports.DefaultHydrationAdapter = DefaultHydrationAdapter;
10248
11399
  exports.ECacheStrategy = ECacheStrategy;
10249
11400
  exports.EDataFilterOperator = EDataFilterOperator;
@@ -10256,6 +11407,7 @@ exports.RequestQueue = RequestQueue;
10256
11407
  exports.SENSITIVE_HEADER_KEYS = SENSITIVE_HEADER_KEYS;
10257
11408
  exports.applyOperator = applyOperator;
10258
11409
  exports.buildEncryptionContext = buildEncryptionContext;
11410
+ exports.canonicalizeJson = canonicalizeJson;
10259
11411
  exports.clearCachedCipherProvider = clearCachedCipherProvider;
10260
11412
  exports.clearSSRData = clearSSRData;
10261
11413
  exports.compareAnd = compareAnd;