@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.mjs CHANGED
@@ -1,6 +1,8 @@
1
1
  import * as crypto$1 from 'node:crypto';
2
2
  import { $fetch } from 'ofetch';
3
3
 
4
+ const DECRYPT_ERROR_SENTINEL = Symbol.for("katlux.decrypt_error");
5
+
4
6
  var EDataFilterOperator = /* @__PURE__ */ ((EDataFilterOperator2) => {
5
7
  EDataFilterOperator2["Equal"] = "==";
6
8
  EDataFilterOperator2["NotEqual"] = "!=";
@@ -655,878 +657,1725 @@ function isAuthOrDecryptionError(err) {
655
657
  const msg = String(err.message || "").toLowerCase();
656
658
  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");
657
659
  }
658
-
659
- class AEncryptionProvider {
660
- defaultKey;
661
- constructor(key) {
662
- if (key !== void 0) {
663
- this.defaultKey = key;
660
+ function canonicalizeJson(value, seen = /* @__PURE__ */ new Set()) {
661
+ if (value === void 0) {
662
+ return "";
663
+ }
664
+ if (value === null || typeof value === "boolean") {
665
+ return JSON.stringify(value);
666
+ }
667
+ if (typeof value === "number") {
668
+ if (!Number.isFinite(value)) return "null";
669
+ if (Object.is(value, -0)) return "0";
670
+ return JSON.stringify(value);
671
+ }
672
+ if (typeof value === "string") {
673
+ return JSON.stringify(value);
674
+ }
675
+ if (typeof value === "bigint") {
676
+ throw new TypeError("Do not know how to serialize a BigInt");
677
+ }
678
+ if (typeof value === "symbol" || typeof value === "function") {
679
+ return "";
680
+ }
681
+ if (typeof value === "object") {
682
+ if (typeof value.toJSON === "function") {
683
+ return canonicalizeJson(value.toJSON(), seen);
684
+ }
685
+ if (seen.has(value)) {
686
+ throw new TypeError("Converting circular structure to JSON");
687
+ }
688
+ seen.add(value);
689
+ try {
690
+ if (Array.isArray(value)) {
691
+ const items = value.map((item) => {
692
+ if (item === void 0 || typeof item === "function" || typeof item === "symbol") {
693
+ return "null";
694
+ }
695
+ const res = canonicalizeJson(item, seen);
696
+ return res === "" ? "null" : res;
697
+ });
698
+ return `[${items.join(",")}]`;
699
+ }
700
+ const keys = Object.keys(value).sort();
701
+ const entries = [];
702
+ for (const key of keys) {
703
+ const val = value[key];
704
+ if (val === void 0 || typeof val === "function" || typeof val === "symbol") {
705
+ continue;
706
+ }
707
+ const serialized = canonicalizeJson(val, seen);
708
+ if (serialized !== "") {
709
+ entries.push(`${JSON.stringify(key)}:${serialized}`);
710
+ }
711
+ }
712
+ return `{${entries.join(",")}}`;
713
+ } finally {
714
+ seen.delete(value);
664
715
  }
665
716
  }
717
+ return JSON.stringify(value);
718
+ }
719
+
720
+ class CKeyManager {
721
+ // ── Binary & Encoding Utilities ──────────────────────────────────────────
666
722
  /**
667
- * Sets or updates the default secret key, CryptoKey, or async key provider function.
723
+ * Generates a cryptographically secure random byte buffer of the specified length.
668
724
  */
669
- setDefaultKey(key) {
670
- this.defaultKey = key || void 0;
725
+ static randomBytes(length) {
726
+ const bytes = new Uint8Array(length);
727
+ return getRandomValues(bytes);
671
728
  }
672
729
  /**
673
- * Retrieves the currently configured default key or key provider callback.
730
+ * Converts an ArrayBuffer or Uint8Array into a Base64-encoded string.
674
731
  */
675
- getDefaultKey() {
676
- return this.defaultKey;
732
+ static toBase64(buffer) {
733
+ return toBase64(buffer);
677
734
  }
678
735
  /**
679
- * Pre-derives and caches the native CryptoKey for subsequent cryptographic operations.
680
- * Subclasses should override this with a dedicated, non-side-effecting implementation.
736
+ * Converts a Base64 or Base64URL-encoded string back into a Uint8Array.
737
+ * Normalizes URL-safe characters and padding, strictly validating against the Base64 alphabet.
681
738
  */
682
- async warmupKey(_options) {
739
+ static fromBase64(base64) {
740
+ if (typeof base64 !== "string") {
741
+ throw new Error("[CKeyManager] Invalid base64 input: expected string");
742
+ }
743
+ const cleaned = base64.replace(/\s+/g, "");
744
+ if (!cleaned) return new Uint8Array(0);
745
+ const normalized = cleaned.replace(/-/g, "+").replace(/_/g, "/");
746
+ const padded = normalized.padEnd(normalized.length + (4 - normalized.length % 4) % 4, "=");
747
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(padded)) {
748
+ throw new Error("[CKeyManager] Invalid base64 string");
749
+ }
750
+ if (typeof Buffer !== "undefined") {
751
+ const buf = Buffer.from(padded, "base64");
752
+ const arrayBuf = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
753
+ return new Uint8Array(arrayBuf);
754
+ }
755
+ const binary = atob(padded);
756
+ const bytes = new Uint8Array(binary.length);
757
+ for (let i = 0; i < binary.length; i++) {
758
+ bytes[i] = binary.charCodeAt(i);
759
+ }
760
+ return bytes;
683
761
  }
684
762
  /**
685
- * Resolves raw key from keyProvider callback or defaultKey.
763
+ * Deterministically resolves a salt from Uint8Array, ArrayBuffer, Base64/Base64URL string, or raw text string.
764
+ * Guarantees 100% identical salt byte resolution between Node.js and Browser environments.
686
765
  */
687
- async resolveRawKey(keyProvider) {
688
- const target = keyProvider || this.defaultKey;
689
- if (!target) {
690
- throw new Error(
691
- `[${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.`
692
- );
766
+ static resolveSalt(salt) {
767
+ if (!salt) return CKeyManager.randomBytes(16);
768
+ if (ArrayBuffer.isView(salt)) {
769
+ return salt.byteLength > 0 ? new Uint8Array(salt.buffer.slice(salt.byteOffset, salt.byteOffset + salt.byteLength)) : CKeyManager.randomBytes(16);
693
770
  }
694
- const rawKey = typeof target === "function" ? await target() : target;
695
- if (!rawKey || typeof rawKey === "string" && rawKey.trim().length === 0 || typeof rawKey === "object" && "byteLength" in rawKey && rawKey.byteLength === 0) {
696
- throw new Error(
697
- `[${this.constructor.name}] No encryption key or keyProvider was supplied. Key provider resolved to empty or undefined key.`
698
- );
771
+ if (salt instanceof ArrayBuffer) {
772
+ return salt.byteLength > 0 ? new Uint8Array(salt.slice(0)) : CKeyManager.randomBytes(16);
699
773
  }
700
- return rawKey;
774
+ if (typeof salt === "string") {
775
+ const trimmed = salt.trim();
776
+ if (!trimmed) return CKeyManager.randomBytes(16);
777
+ if (trimmed.startsWith("base64:")) {
778
+ return CKeyManager.fromBase64(trimmed.slice(7));
779
+ }
780
+ if (trimmed.endsWith("=") && /^[A-Za-z0-9+/]+={1,2}$/.test(trimmed)) {
781
+ try {
782
+ const decoded = CKeyManager.fromBase64(trimmed);
783
+ if (decoded.byteLength > 0) return decoded;
784
+ } catch {
785
+ }
786
+ }
787
+ const encoded = textEncoder$1.encode(trimmed);
788
+ return encoded.byteLength > 0 ? encoded : CKeyManager.randomBytes(16);
789
+ }
790
+ return CKeyManager.randomBytes(16);
701
791
  }
792
+ // ── Symmetric Key Management ─────────────────────────────────────────────
702
793
  /**
703
- * Checks whether the given payload is an encrypted envelope.
794
+ * Generates a random AES CryptoKey suitable for AES-GCM or AES-CBC.
795
+ *
796
+ * E2EE Threat Model Note: Focuses on protecting GDPR/tenant data against database and
797
+ * server-side leaks. Keys remain extractable by default to enable role-based key sharing,
798
+ * wrapping, and transfer between authorized client sessions in multi-user SaaS apps.
799
+ * Client-side malware/trojan protection is out of scope.
704
800
  */
705
- static isEncrypted(payload) {
706
- if (!payload || typeof payload !== "object") return false;
707
- return payload.__encrypted === true && typeof payload.ciphertext === "string" && typeof payload.iv === "string";
801
+ static async generateAESKey(length = 256, algorithm = "AES-GCM", extractable = true) {
802
+ const subtle = getSubtleCrypto();
803
+ const usages = algorithm === "AES-KW" ? ["wrapKey", "unwrapKey"] : ["encrypt", "decrypt"];
804
+ return await subtle.generateKey(
805
+ { name: algorithm, length },
806
+ extractable,
807
+ usages
808
+ );
708
809
  }
709
810
  /**
710
- * Checks whether the given payload is an encrypted envelope.
811
+ * Exports an AES CryptoKey as a raw base64 string.
711
812
  */
712
- isEncrypted(payload) {
713
- return AEncryptionProvider.isEncrypted(payload);
813
+ static async exportAESKey(key) {
814
+ const subtle = getSubtleCrypto();
815
+ const raw = await subtle.exportKey("raw", key);
816
+ try {
817
+ return CKeyManager.toBase64(raw);
818
+ } finally {
819
+ new Uint8Array(raw).fill(0);
820
+ }
714
821
  }
715
822
  /**
716
- * Clears configured keys and cached cryptographic material from memory.
823
+ * Imports an AES CryptoKey from a raw base64 string.
717
824
  */
718
- clearKeys() {
719
- this.defaultKey = void 0;
825
+ static async importAESKey(base64, algorithm = "AES-GCM", extractable = false) {
826
+ const subtle = getSubtleCrypto();
827
+ const rawBytes = CKeyManager.fromBase64(base64);
828
+ if (rawBytes.length !== 16 && rawBytes.length !== 24 && rawBytes.length !== 32) {
829
+ rawBytes.fill(0);
830
+ throw new Error(`[CKeyManager] Invalid AES key length: expected 16, 24, or 32 bytes (128, 192, or 256 bits), got ${rawBytes.length} bytes.`);
831
+ }
832
+ const usages = algorithm === "AES-KW" ? ["wrapKey", "unwrapKey"] : ["encrypt", "decrypt"];
833
+ try {
834
+ return await subtle.importKey(
835
+ "raw",
836
+ rawBytes,
837
+ { name: algorithm },
838
+ extractable,
839
+ usages
840
+ );
841
+ } finally {
842
+ rawBytes.fill(0);
843
+ }
720
844
  }
845
+ // ── Asymmetric Key Pair Management ───────────────────────────────────────
721
846
  /**
722
- * Alias for clearKeys() to conform with standard disposal patterns.
847
+ * Generates an RSA-OAEP public/private key pair (2048 or 4096 bits).
723
848
  */
724
- dispose() {
725
- this.clearKeys();
849
+ static async generateRSAKeyPair(bits = 4096, hash = "SHA-256", extractable = true) {
850
+ const subtle = getSubtleCrypto();
851
+ const pair = await subtle.generateKey(
852
+ {
853
+ name: "RSA-OAEP",
854
+ modulusLength: bits,
855
+ publicExponent: new Uint8Array([1, 0, 1]),
856
+ hash
857
+ },
858
+ extractable,
859
+ ["encrypt", "decrypt", "wrapKey", "unwrapKey"]
860
+ );
861
+ return {
862
+ publicKey: pair.publicKey,
863
+ privateKey: pair.privateKey
864
+ };
726
865
  }
727
866
  /**
728
- * ECMAScript Explicit Resource Management protocol (using / await using declarations).
867
+ * Generates an ECDH public/private key pair for elliptic curve Diffie-Hellman.
729
868
  */
730
- [Symbol.dispose]() {
731
- this.dispose();
732
- }
733
- async [Symbol.asyncDispose]() {
734
- this.dispose();
869
+ static async generateECDHKeyPair(curve = "P-256", extractable = true) {
870
+ const subtle = getSubtleCrypto();
871
+ const pair = await subtle.generateKey(
872
+ {
873
+ name: "ECDH",
874
+ namedCurve: curve
875
+ },
876
+ extractable,
877
+ ["deriveKey", "deriveBits"]
878
+ );
879
+ return {
880
+ publicKey: pair.publicKey,
881
+ privateKey: pair.privateKey
882
+ };
735
883
  }
736
884
  /**
737
- * Resolves effective Authenticated Additional Data (AAD) for the operation:
738
- * 1. If options.bindRequestContextAad is explicitly false, returns options.aad directly without context binding.
739
- * 2. When encrypting (isDecrypt=false): binds request context `req:${method}:${url}` only if options.bindRequestContextAad === true.
740
- * 3. When decrypting (isDecrypt=true): derives request context `req:${method}:${url}` if url or method is provided.
741
- * 4. If explicit options.aad is provided: combines with request context if bound, or returns options.aad directly.
742
- * 5. Otherwise returns undefined.
885
+ * Exports a public CryptoKey to a portable JsonWebKey (JWK).
743
886
  */
744
- _resolveEffectiveAad(options, isDecrypt = false) {
745
- if (!options) return void 0;
746
- if (options.bindRequestContextAad === false) return options.aad;
747
- const hasReqContext = Boolean(options.url || options.method);
748
- if (options.aad) {
749
- if (options.bindRequestContextAad === true && hasReqContext) {
750
- const m = (options.method || "POST").toUpperCase();
751
- const u = options.url || "";
752
- const baseAad = typeof options.aad === "string" ? options.aad : options.aad instanceof Uint8Array || options.aad instanceof ArrayBuffer ? textDecoder.decode(options.aad) : String(options.aad);
753
- return `${baseAad}|req:${m}:${u}`;
754
- }
755
- return options.aad;
756
- }
757
- const shouldBindReq = isDecrypt ? hasReqContext : options.bindRequestContextAad === true && hasReqContext;
758
- if (shouldBindReq) {
759
- const m = (options.method || "POST").toUpperCase();
760
- const u = options.url || "";
761
- return `req:${m}:${u}`;
762
- }
763
- return void 0;
887
+ static async exportPublicKey(key) {
888
+ const subtle = getSubtleCrypto();
889
+ return await subtle.exportKey("jwk", key);
764
890
  }
765
891
  /**
766
- * Helper to deterministically bind record ID and field name into the AAD
767
- * to prevent ciphertext swapping between fields or records.
892
+ * Exports a private CryptoKey to a portable JsonWebKey (JWK).
768
893
  */
769
- _bindFieldAad(data, field, options) {
770
- let baseAad = "";
771
- if (options?.aad) {
772
- if (typeof options.aad === "string") baseAad = options.aad;
773
- else if (options.aad instanceof Uint8Array || options.aad instanceof ArrayBuffer) {
774
- baseAad = textDecoder.decode(options.aad);
775
- } else {
776
- baseAad = String(options.aad);
777
- }
778
- }
779
- const recordId = data && typeof data === "object" && (data.id !== void 0 || data._id !== void 0) ? String(data.id ?? data._id) : "";
780
- const recordPart = recordId ? `|record:${recordId}` : "";
781
- const reqPart = options?.bindRequestContextAad === true && (options?.url || options?.method) ? `|req:${(options.method || "POST").toUpperCase()}:${options.url || ""}` : "";
782
- return {
783
- ...options,
784
- aad: `aad:${baseAad}${recordPart}${reqPart}|field:${field}`
785
- };
894
+ static async exportPrivateKey(key) {
895
+ const subtle = getSubtleCrypto();
896
+ return await subtle.exportKey("jwk", key);
786
897
  }
787
898
  /**
788
- * Top-level polymorphic encryption method.
789
- * Automatically performs value-level encryption:
790
- * - If data is an array of objects: recursively encrypts each row.
791
- * - If data is an object: encrypts specified fields (or all values if valueEncryption is true),
792
- * preserving JSON object keys so backend APIs and ORMs can parse keys and perform upserts.
793
- * - If data is a primitive or whole-payload envelope: encrypts as a single value.
899
+ * Imports a public key from a JsonWebKey (JWK).
794
900
  */
795
- async encrypt(data, options) {
796
- if (data === void 0 || data === null) return data;
797
- if (this.isEncrypted(data)) return data;
798
- if (options?.valueEncryption === false && (!options.fields || options.fields.length === 0)) {
799
- return this.encryptValue(data, options);
800
- }
801
- if (Array.isArray(data)) {
802
- if (data.length === 0) return data;
803
- if (options?.fields && options.fields.length > 0) {
804
- const fields = options.fields;
805
- let hasUnencrypted = false;
806
- for (let i = 0; i < data.length; i++) {
807
- const item = data[i];
808
- if (item && typeof item === "object") {
809
- for (let f = 0; f < fields.length; f++) {
810
- const val = item[fields[f]];
811
- if (val !== void 0 && val !== null && !this.isEncrypted(val)) {
812
- hasUnencrypted = true;
813
- break;
814
- }
815
- }
816
- if (hasUnencrypted) break;
817
- }
901
+ static async importPublicKey(jwk, algorithm) {
902
+ const subtle = getSubtleCrypto();
903
+ let importAlgorithm = algorithm;
904
+ let usages;
905
+ const isEC = jwk.kty === "EC" || typeof algorithm === "object" && algorithm?.name === "ECDH" || algorithm === "ECDH";
906
+ if (isEC) {
907
+ usages = [];
908
+ if (!importAlgorithm || importAlgorithm === "ECDH") {
909
+ importAlgorithm = { name: "ECDH", namedCurve: jwk.crv || "P-256" };
910
+ }
911
+ } else {
912
+ usages = ["encrypt", "wrapKey"];
913
+ if (!importAlgorithm || importAlgorithm === "RSA-OAEP") {
914
+ if (jwk.kty === "RSA") {
915
+ const hash = jwk.alg === "RSA-OAEP-512" ? "SHA-512" : "SHA-256";
916
+ importAlgorithm = { name: "RSA-OAEP", hash };
917
+ } else {
918
+ throw new Error(`[CKeyManager] Unsupported JWK kty: ${jwk.kty}`);
818
919
  }
819
- if (!hasUnencrypted) {
820
- return data;
920
+ }
921
+ }
922
+ return await subtle.importKey("jwk", jwk, importAlgorithm, true, usages);
923
+ }
924
+ /**
925
+ * Imports a private key from a JsonWebKey (JWK).
926
+ */
927
+ static async importPrivateKey(jwk, algorithm) {
928
+ const subtle = getSubtleCrypto();
929
+ let importAlgorithm = algorithm;
930
+ let usages;
931
+ const isEC = jwk.kty === "EC" || typeof algorithm === "object" && algorithm?.name === "ECDH" || algorithm === "ECDH";
932
+ if (isEC) {
933
+ usages = ["deriveKey", "deriveBits"];
934
+ if (!importAlgorithm || importAlgorithm === "ECDH") {
935
+ importAlgorithm = { name: "ECDH", namedCurve: jwk.crv || "P-256" };
936
+ }
937
+ } else {
938
+ usages = ["decrypt", "unwrapKey"];
939
+ if (!importAlgorithm || importAlgorithm === "RSA-OAEP") {
940
+ if (jwk.kty === "RSA") {
941
+ const hash = jwk.alg === "RSA-OAEP-512" ? "SHA-512" : "SHA-256";
942
+ importAlgorithm = { name: "RSA-OAEP", hash };
943
+ } else {
944
+ throw new Error(`[CKeyManager] Unsupported JWK kty: ${jwk.kty}`);
821
945
  }
822
- await this.warmupKey(options);
823
- const itemOptions = { ...options, _warmed: true };
824
- return Promise.all(data.map((item) => this.encrypt(item, itemOptions)));
825
946
  }
826
- if (options?.valueEncryption === true) {
827
- await this.warmupKey(options);
828
- const itemOptions = { ...options, _warmed: true };
829
- return Promise.all(data.map((item) => this.encrypt(item, itemOptions)));
947
+ }
948
+ return await subtle.importKey("jwk", jwk, importAlgorithm, true, usages);
949
+ }
950
+ /**
951
+ * Imports a public key from a PEM (SPKI) or base64 SPKI string.
952
+ */
953
+ static async importPublicKeyFromPEM(pem, algorithm = "RSA-OAEP", curve = "P-256", hash = "SHA-256") {
954
+ const subtle = getSubtleCrypto();
955
+ const cleaned = pem.replace(/-----BEGIN[ A-Z0-9_-]+-----/g, "").replace(/-----END[ A-Z0-9_-]+-----/g, "").replace(/\s+/g, "");
956
+ const spkiBytes = CKeyManager.fromBase64(cleaned);
957
+ let importAlgorithm;
958
+ let usages;
959
+ if (algorithm === "ECDH") {
960
+ importAlgorithm = { name: "ECDH", namedCurve: curve };
961
+ usages = [];
962
+ } else if (algorithm === "ECDSA") {
963
+ importAlgorithm = { name: "ECDSA", namedCurve: curve };
964
+ usages = ["verify"];
965
+ } else {
966
+ importAlgorithm = { name: "RSA-OAEP", hash };
967
+ usages = ["encrypt", "wrapKey"];
968
+ }
969
+ try {
970
+ return await subtle.importKey("spki", spkiBytes, importAlgorithm, true, usages);
971
+ } finally {
972
+ spkiBytes.fill(0);
973
+ }
974
+ }
975
+ /**
976
+ * Imports an ECDSA public key from PEM (SPKI) or base64 SPKI string for signature verification.
977
+ */
978
+ static async importECDSAPublicKeyFromPEM(pem, curve = "P-256") {
979
+ return CKeyManager.importPublicKeyFromPEM(pem, "ECDSA", curve);
980
+ }
981
+ /**
982
+ * Imports a private key from a PEM (PKCS#8) or base64 PKCS#8 string.
983
+ */
984
+ static async importPrivateKeyFromPEM(pem, algorithm = "RSA-OAEP", curve = "P-256", hash = "SHA-256") {
985
+ if (/BEGIN\s+(RSA|EC)\s+PRIVATE\s+KEY/i.test(pem)) {
986
+ throw new Error(
987
+ `[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.`
988
+ );
989
+ }
990
+ const subtle = getSubtleCrypto();
991
+ const cleaned = pem.replace(/-----BEGIN[ A-Z0-9_-]+-----/g, "").replace(/-----END[ A-Z0-9_-]+-----/g, "").replace(/\s+/g, "");
992
+ const pkcs8Bytes = CKeyManager.fromBase64(cleaned);
993
+ let importAlgorithm;
994
+ let usages;
995
+ if (algorithm === "ECDH") {
996
+ importAlgorithm = { name: "ECDH", namedCurve: curve };
997
+ usages = ["deriveKey", "deriveBits"];
998
+ } else if (algorithm === "ECDSA") {
999
+ importAlgorithm = { name: "ECDSA", namedCurve: curve };
1000
+ usages = ["sign"];
1001
+ } else {
1002
+ importAlgorithm = { name: "RSA-OAEP", hash };
1003
+ usages = ["decrypt", "unwrapKey"];
1004
+ }
1005
+ try {
1006
+ return await subtle.importKey("pkcs8", pkcs8Bytes, importAlgorithm, true, usages);
1007
+ } finally {
1008
+ pkcs8Bytes.fill(0);
1009
+ }
1010
+ }
1011
+ /**
1012
+ * Imports an ECDSA private key from PEM (PKCS#8) or base64 PKCS#8 string for signing.
1013
+ */
1014
+ static async importECDSAPrivateKeyFromPEM(pem, curve = "P-256") {
1015
+ return CKeyManager.importPrivateKeyFromPEM(pem, "ECDSA", curve);
1016
+ }
1017
+ /**
1018
+ * Exports a public CryptoKey to a standard PEM (SPKI) string.
1019
+ */
1020
+ static async exportPublicKeyToPEM(key) {
1021
+ const subtle = getSubtleCrypto();
1022
+ const spki = await subtle.exportKey("spki", key);
1023
+ let b64;
1024
+ try {
1025
+ b64 = CKeyManager.toBase64(spki);
1026
+ } finally {
1027
+ new Uint8Array(spki).fill(0);
1028
+ }
1029
+ const lines = b64.match(/.{1,64}/g) || [b64];
1030
+ return `-----BEGIN PUBLIC KEY-----
1031
+ ${lines.join("\n")}
1032
+ -----END PUBLIC KEY-----`;
1033
+ }
1034
+ /**
1035
+ * Exports a private CryptoKey to a standard PEM (PKCS#8) string.
1036
+ */
1037
+ static async exportPrivateKeyToPEM(key) {
1038
+ const subtle = getSubtleCrypto();
1039
+ const pkcs8 = await subtle.exportKey("pkcs8", key);
1040
+ let b64;
1041
+ try {
1042
+ b64 = CKeyManager.toBase64(pkcs8);
1043
+ } finally {
1044
+ new Uint8Array(pkcs8).fill(0);
1045
+ }
1046
+ const lines = b64.match(/.{1,64}/g) || [b64];
1047
+ return `-----BEGIN PRIVATE KEY-----
1048
+ ${lines.join("\n")}
1049
+ -----END PRIVATE KEY-----`;
1050
+ }
1051
+ // ── Key Wrapping & Unwrapping ────────────────────────────────────────────
1052
+ /**
1053
+ * Wraps a target CryptoKey (e.g. an Account Symmetric Key) using a wrapping key
1054
+ * (AES-KW or RSA-OAEP).
1055
+ */
1056
+ static async wrapKey(keyToWrap, wrappingKey, format = "raw") {
1057
+ const subtle = getSubtleCrypto();
1058
+ let wrapAlgorithm;
1059
+ if (wrappingKey.algorithm.name === "RSA-OAEP") {
1060
+ wrapAlgorithm = { name: "RSA-OAEP" };
1061
+ } else if (wrappingKey.algorithm.name === "AES-KW") {
1062
+ wrapAlgorithm = "AES-KW";
1063
+ } else {
1064
+ wrapAlgorithm = "AES-KW";
1065
+ }
1066
+ const wrappedBuffer = await subtle.wrapKey(format, keyToWrap, wrappingKey, wrapAlgorithm);
1067
+ return CKeyManager.toBase64(wrappedBuffer);
1068
+ }
1069
+ /**
1070
+ * Unwraps a base64-encoded wrapped key using an unwrapping key (AES-KW or RSA-OAEP).
1071
+ */
1072
+ static async unwrapKey(wrappedBase64, unwrappingKey, unwrappedKeyAlgorithm = { name: "AES-GCM", length: 256 }, extractable = true, keyUsages = ["encrypt", "decrypt"], format = "raw") {
1073
+ const subtle = getSubtleCrypto();
1074
+ const wrappedBytes = CKeyManager.fromBase64(wrappedBase64);
1075
+ let unwrapAlgorithm;
1076
+ if (unwrappingKey.algorithm.name === "RSA-OAEP") {
1077
+ unwrapAlgorithm = { name: "RSA-OAEP" };
1078
+ } else {
1079
+ unwrapAlgorithm = "AES-KW";
1080
+ }
1081
+ try {
1082
+ return await subtle.unwrapKey(
1083
+ format,
1084
+ wrappedBytes,
1085
+ unwrappingKey,
1086
+ unwrapAlgorithm,
1087
+ unwrappedKeyAlgorithm,
1088
+ extractable,
1089
+ keyUsages
1090
+ );
1091
+ } finally {
1092
+ wrappedBytes.fill(0);
1093
+ }
1094
+ }
1095
+ /**
1096
+ * Generates a high-entropy symmetric AES-GCM Account Data Encryption Key (ADEK).
1097
+ * Designed for multi-user table and field-level encryption.
1098
+ */
1099
+ static async generateAccountDataKey(length = 256, extractable = false) {
1100
+ const subtle = getSubtleCrypto();
1101
+ return await subtle.generateKey(
1102
+ { name: "AES-GCM", length },
1103
+ extractable,
1104
+ ["encrypt", "decrypt"]
1105
+ );
1106
+ }
1107
+ /**
1108
+ * Wraps an Account Data Encryption Key (ADEK) for a specific user using the user's public key (RSA-OAEP).
1109
+ * Returns a Base64-encoded wrapped key string suitable for storage in role-map / access tables.
1110
+ */
1111
+ static async wrapAccountKeyForUser(adek, userPublicKey) {
1112
+ const subtle = getSubtleCrypto();
1113
+ if (userPublicKey.algorithm.name === "RSA-OAEP") {
1114
+ const rawKey = await subtle.exportKey("raw", adek);
1115
+ try {
1116
+ const wrappedBuffer = await subtle.encrypt(
1117
+ { name: "RSA-OAEP" },
1118
+ userPublicKey,
1119
+ rawKey
1120
+ );
1121
+ return CKeyManager.toBase64(wrappedBuffer);
1122
+ } finally {
1123
+ new Uint8Array(rawKey).fill(0);
830
1124
  }
831
- return this.encryptValue(data, options);
832
1125
  }
833
- if (typeof data === "object" && !ArrayBuffer.isView(data) && !(data instanceof ArrayBuffer)) {
834
- if (options?.fields && options.fields.length > 0) {
835
- const fieldsToEncrypt = options.fields.filter(
836
- (field) => Object.prototype.hasOwnProperty.call(data, field) && data[field] !== void 0 && data[field] !== null && !this.isEncrypted(data[field])
1126
+ throw new Error(
1127
+ `[CKeyManager] Unsupported user key algorithm for wrapAccountKeyForUser: '${userPublicKey.algorithm.name}'. Expected 'RSA-OAEP'.`
1128
+ );
1129
+ }
1130
+ /**
1131
+ * Unwraps a Base64-encoded Account Data Encryption Key (ADEK) using the user's private key (RSA-OAEP).
1132
+ * Returns the restored AES-GCM CryptoKey ready for use in CWebCryptoEncryptionProvider.
1133
+ */
1134
+ static async unwrapAccountKeyForUser(wrappedKeyBase64, userPrivateKey, unwrappedKeyAlgorithm = { name: "AES-GCM", length: 256 }, extractable = false) {
1135
+ const subtle = getSubtleCrypto();
1136
+ if (userPrivateKey.algorithm.name === "RSA-OAEP") {
1137
+ const wrappedBytes = CKeyManager.fromBase64(wrappedKeyBase64);
1138
+ let unwrappedRawKey;
1139
+ try {
1140
+ unwrappedRawKey = await subtle.decrypt(
1141
+ { name: "RSA-OAEP" },
1142
+ userPrivateKey,
1143
+ wrappedBytes
837
1144
  );
838
- if (fieldsToEncrypt.length === 0) {
839
- return data;
840
- }
841
- if (!options?._warmed) {
842
- await this.warmupKey(options);
843
- }
844
- const result = { ...data };
845
- const entries = await Promise.all(
846
- fieldsToEncrypt.map(async (field) => {
847
- const fieldOptions = this._bindFieldAad(data, field, options);
848
- return [field, await this.encryptValue(result[field], fieldOptions)];
849
- })
1145
+ } finally {
1146
+ wrappedBytes.fill(0);
1147
+ }
1148
+ try {
1149
+ return await subtle.importKey(
1150
+ "raw",
1151
+ unwrappedRawKey,
1152
+ unwrappedKeyAlgorithm,
1153
+ extractable,
1154
+ ["encrypt", "decrypt"]
850
1155
  );
851
- for (const [field, encryptedVal] of entries) {
852
- safeSetProperty(result, field, encryptedVal);
853
- }
854
- return result;
1156
+ } finally {
1157
+ new Uint8Array(unwrappedRawKey).fill(0);
1158
+ }
1159
+ }
1160
+ throw new Error(
1161
+ `[CKeyManager] Unsupported user key algorithm for unwrapAccountKeyForUser: '${userPrivateKey.algorithm.name}'. Expected 'RSA-OAEP'.`
1162
+ );
1163
+ }
1164
+ /**
1165
+ * Wraps an HMAC blind indexing key for a specific recipient user using their RSA-OAEP public key.
1166
+ * Returns a Base64-encoded wrapped key string suitable for storage in role-map / access tables.
1167
+ */
1168
+ static async wrapHMACKeyForUser(hmacKey, userPublicKey) {
1169
+ const subtle = getSubtleCrypto();
1170
+ if (userPublicKey.algorithm.name === "RSA-OAEP") {
1171
+ const rawKey = await subtle.exportKey("raw", hmacKey);
1172
+ try {
1173
+ const wrappedBuffer = await subtle.encrypt(
1174
+ { name: "RSA-OAEP" },
1175
+ userPublicKey,
1176
+ rawKey
1177
+ );
1178
+ return CKeyManager.toBase64(wrappedBuffer);
1179
+ } finally {
1180
+ new Uint8Array(rawKey).fill(0);
1181
+ }
1182
+ }
1183
+ throw new Error(
1184
+ `[CKeyManager] Unsupported user key algorithm for wrapHMACKeyForUser: '${userPublicKey.algorithm.name}'. Expected 'RSA-OAEP'.`
1185
+ );
1186
+ }
1187
+ /**
1188
+ * Unwraps a Base64-encoded HMAC key using a recipient's RSA-OAEP private key.
1189
+ * Restores the CryptoKey configured for HMAC signing and verification in search indexing.
1190
+ */
1191
+ static async unwrapHMACKeyForUser(wrappedKeyBase64, userPrivateKey, hash = "SHA-256", extractable = false) {
1192
+ const subtle = getSubtleCrypto();
1193
+ if (userPrivateKey.algorithm.name === "RSA-OAEP") {
1194
+ const wrappedBytes = CKeyManager.fromBase64(wrappedKeyBase64);
1195
+ let unwrappedRawKey;
1196
+ try {
1197
+ unwrappedRawKey = await subtle.decrypt(
1198
+ { name: "RSA-OAEP" },
1199
+ userPrivateKey,
1200
+ wrappedBytes
1201
+ );
1202
+ } finally {
1203
+ wrappedBytes.fill(0);
1204
+ }
1205
+ try {
1206
+ return await subtle.importKey(
1207
+ "raw",
1208
+ unwrappedRawKey,
1209
+ { name: "HMAC", hash: { name: hash } },
1210
+ extractable,
1211
+ ["sign", "verify"]
1212
+ );
1213
+ } finally {
1214
+ new Uint8Array(unwrappedRawKey).fill(0);
1215
+ }
1216
+ }
1217
+ throw new Error(
1218
+ `[CKeyManager] Unsupported user key algorithm for unwrapHMACKeyForUser: '${userPrivateKey.algorithm.name}'. Expected 'RSA-OAEP'.`
1219
+ );
1220
+ }
1221
+ /**
1222
+ * Atomically re-wraps an Account Data Encryption Key (ADEK) for a new recipient
1223
+ * without exposing the raw extractable symmetric key to caller application code.
1224
+ *
1225
+ * @param wrappedKeyBase64 - The existing ADEK envelope wrapped with current user's public key
1226
+ * @param myPrivateKey - Current user's RSA-OAEP private key
1227
+ * @param recipientPublicKey - New recipient's RSA-OAEP public key
1228
+ * @returns Base64-encoded wrapped key envelope for the recipient
1229
+ */
1230
+ static async rewrapAccountKeyForUser(wrappedKeyBase64, myPrivateKey, recipientPublicKey) {
1231
+ const subtle = getSubtleCrypto();
1232
+ if (!myPrivateKey || !myPrivateKey.algorithm || myPrivateKey.algorithm.name !== "RSA-OAEP" || !recipientPublicKey || !recipientPublicKey.algorithm || recipientPublicKey.algorithm.name !== "RSA-OAEP") {
1233
+ throw new Error(`[CKeyManager] rewrapAccountKeyForUser requires RSA-OAEP keys.`);
1234
+ }
1235
+ if (myPrivateKey.type === "public" || recipientPublicKey.type === "private") {
1236
+ throw new Error(
1237
+ `[CKeyManager] rewrapAccountKeyForUser requires an RSA-OAEP private key for decryption and an RSA-OAEP public key for encryption.`
1238
+ );
1239
+ }
1240
+ const wrappedBytes = CKeyManager.fromBase64(wrappedKeyBase64);
1241
+ let rawKeyBuffer;
1242
+ try {
1243
+ rawKeyBuffer = await subtle.decrypt(
1244
+ { name: "RSA-OAEP" },
1245
+ myPrivateKey,
1246
+ wrappedBytes
1247
+ );
1248
+ const rewrappedBuffer = await subtle.encrypt(
1249
+ { name: "RSA-OAEP" },
1250
+ recipientPublicKey,
1251
+ rawKeyBuffer
1252
+ );
1253
+ return CKeyManager.toBase64(rewrappedBuffer);
1254
+ } finally {
1255
+ wrappedBytes.fill(0);
1256
+ if (rawKeyBuffer) {
1257
+ new Uint8Array(rawKeyBuffer).fill(0);
1258
+ }
1259
+ }
1260
+ }
1261
+ // ── Key Derivation Functions (PBKDF2 & HKDF) ──────────────────────────────
1262
+ /**
1263
+ * Derives a symmetric AES-GCM CryptoKey from a passphrase using PBKDF2 or HKDF.
1264
+ * Returns the derived CryptoKey and the base64-encoded salt.
1265
+ */
1266
+ static async deriveKey(passphrase, options) {
1267
+ if (!passphrase || typeof passphrase === "string" && passphrase.trim().length === 0 || typeof passphrase === "object" && "byteLength" in passphrase && passphrase.byteLength === 0) {
1268
+ throw new Error("[CKeyManager] Passphrase cannot be empty or undefined for key derivation.");
1269
+ }
1270
+ const subtle = getSubtleCrypto();
1271
+ const saltBytes = CKeyManager.resolveSalt(options.salt);
1272
+ const keyLength = options.keyLength || 256;
1273
+ const hash = options.hash || "SHA-256";
1274
+ const extractable = options.extractable === true;
1275
+ const rawKeyBytes = typeof passphrase === "string" ? textEncoder$1.encode(passphrase) : passphrase instanceof Uint8Array ? passphrase : new Uint8Array(passphrase);
1276
+ try {
1277
+ if (options.kdf === "pbkdf2") {
1278
+ const baseKey = await subtle.importKey(
1279
+ "raw",
1280
+ rawKeyBytes,
1281
+ "PBKDF2",
1282
+ false,
1283
+ ["deriveKey"]
1284
+ );
1285
+ const derivedKey = await subtle.deriveKey(
1286
+ {
1287
+ name: "PBKDF2",
1288
+ salt: saltBytes,
1289
+ iterations: options.iterations || 31e4,
1290
+ hash
1291
+ },
1292
+ baseKey,
1293
+ { name: "AES-GCM", length: keyLength },
1294
+ extractable,
1295
+ ["encrypt", "decrypt"]
1296
+ );
1297
+ return {
1298
+ key: derivedKey,
1299
+ salt: CKeyManager.toBase64(saltBytes)
1300
+ };
1301
+ }
1302
+ if (options.kdf === "hkdf") {
1303
+ const baseKey = await subtle.importKey(
1304
+ "raw",
1305
+ rawKeyBytes,
1306
+ "HKDF",
1307
+ false,
1308
+ ["deriveKey"]
1309
+ );
1310
+ const infoBytes = resolveInfoBytes(options.info, "katlux-derived-aes-key");
1311
+ const derivedKey = await subtle.deriveKey(
1312
+ {
1313
+ name: "HKDF",
1314
+ salt: saltBytes,
1315
+ info: infoBytes,
1316
+ hash
1317
+ },
1318
+ baseKey,
1319
+ { name: "AES-GCM", length: keyLength },
1320
+ extractable,
1321
+ ["encrypt", "decrypt"]
1322
+ );
1323
+ return {
1324
+ key: derivedKey,
1325
+ salt: CKeyManager.toBase64(saltBytes)
1326
+ };
1327
+ }
1328
+ throw new Error(`[CKeyManager] Unsupported KDF strategy: ${options.kdf}`);
1329
+ } finally {
1330
+ if (typeof passphrase === "string") {
1331
+ rawKeyBytes.fill(0);
1332
+ }
1333
+ }
1334
+ }
1335
+ /**
1336
+ * Derives an AES-GCM CryptoKey from a high-entropy secret (e.g. an ECDH shared secret key)
1337
+ * using HKDF.
1338
+ */
1339
+ static async deriveKeyFromSecret(secretKey, options = {}) {
1340
+ const subtle = getSubtleCrypto();
1341
+ let saltBytes;
1342
+ if (options.salt !== void 0) {
1343
+ saltBytes = CKeyManager.resolveSalt(options.salt);
1344
+ } else {
1345
+ saltBytes = new Uint8Array(16);
1346
+ }
1347
+ const infoBytes = resolveInfoBytes(options.info, "katlux-ecdh-aes-gcm");
1348
+ const hash = options.hash || "SHA-256";
1349
+ const keyLength = options.keyLength || 256;
1350
+ const extractable = options.extractable === true;
1351
+ return await subtle.deriveKey(
1352
+ {
1353
+ name: "HKDF",
1354
+ salt: saltBytes,
1355
+ info: infoBytes,
1356
+ hash
1357
+ },
1358
+ secretKey,
1359
+ { name: "AES-GCM", length: keyLength },
1360
+ extractable,
1361
+ ["encrypt", "decrypt"]
1362
+ );
1363
+ }
1364
+ /**
1365
+ * Derives two domain-separated, cryptographically isolated keys from a user password and salt:
1366
+ * 1. `authKey`: For client authentication, SRP, or server challenge proofs.
1367
+ * 2. `encryptionKey`: For AES-GCM local and field-level data encryption.
1368
+ *
1369
+ * Stage 1 (Password Stretching): PBKDF2-HMAC-SHA256 stretches low-entropy password into master secret.
1370
+ * Stage 2 (Domain Expansion): HKDF-Expand derives subkeys using domain-separated context info tags.
1371
+ * Immediate memory zeroization of intermediate master secret buffer is guaranteed.
1372
+ *
1373
+ * @param password - Master user password or passphrase
1374
+ * @param salt - Salt (or omitted to generate a secure random 16-byte salt)
1375
+ * @param options - PBKDF2 and HKDF configuration options
1376
+ */
1377
+ static async deriveAuthAndEncryptionKeys(password, salt, options = {}) {
1378
+ if (!password || typeof password === "string" && password.trim().length === 0 || typeof password === "object" && "byteLength" in password && password.byteLength === 0) {
1379
+ throw new Error("[CKeyManager] Password cannot be empty or undefined for key derivation.");
1380
+ }
1381
+ const subtle = getSubtleCrypto();
1382
+ let saltBytes;
1383
+ if (salt !== void 0) {
1384
+ saltBytes = CKeyManager.resolveSalt(salt);
1385
+ } else {
1386
+ saltBytes = CKeyManager.randomBytes(16);
1387
+ }
1388
+ const saltBase64 = CKeyManager.toBase64(saltBytes);
1389
+ const iterations = options.iterations || 31e4;
1390
+ const hash = options.hash || "SHA-256";
1391
+ const authKeyLength = options.authKeyLength || 256;
1392
+ const encryptionKeyLength = options.encryptionKeyLength || 256;
1393
+ const authAlgorithm = options.authAlgorithm || "HMAC";
1394
+ const authInfo = resolveInfoBytes(options.authInfo, "katlux-auth-key-v1");
1395
+ const encryptionInfo = resolveInfoBytes(options.encryptionInfo, "katlux-encryption-key-v1");
1396
+ const extractable = options.extractable === true;
1397
+ const rawPasswordBytes = typeof password === "string" ? textEncoder$1.encode(password) : password instanceof Uint8Array ? new Uint8Array(password.buffer, password.byteOffset, password.byteLength) : new Uint8Array(password);
1398
+ let masterBits;
1399
+ try {
1400
+ const baseKey = await subtle.importKey(
1401
+ "raw",
1402
+ rawPasswordBytes,
1403
+ "PBKDF2",
1404
+ false,
1405
+ ["deriveBits"]
1406
+ );
1407
+ const bitsLength = hash === "SHA-512" ? 512 : 256;
1408
+ masterBits = await subtle.deriveBits(
1409
+ {
1410
+ name: "PBKDF2",
1411
+ salt: saltBytes,
1412
+ iterations,
1413
+ hash
1414
+ },
1415
+ baseKey,
1416
+ bitsLength
1417
+ );
1418
+ const hkdfKey = await subtle.importKey(
1419
+ "raw",
1420
+ masterBits,
1421
+ "HKDF",
1422
+ false,
1423
+ ["deriveKey"]
1424
+ );
1425
+ const authDerivedAlgorithm = authAlgorithm === "AES-GCM" ? { name: "AES-GCM", length: authKeyLength } : { name: "HMAC", hash: { name: hash }, length: authKeyLength };
1426
+ const authUsages = authAlgorithm === "AES-GCM" ? ["encrypt", "decrypt"] : ["sign", "verify"];
1427
+ const authKey = await subtle.deriveKey(
1428
+ {
1429
+ name: "HKDF",
1430
+ hash,
1431
+ salt: new Uint8Array(0),
1432
+ info: authInfo
1433
+ },
1434
+ hkdfKey,
1435
+ authDerivedAlgorithm,
1436
+ extractable,
1437
+ authUsages
1438
+ );
1439
+ const encryptionKey = await subtle.deriveKey(
1440
+ {
1441
+ name: "HKDF",
1442
+ hash,
1443
+ salt: new Uint8Array(0),
1444
+ info: encryptionInfo
1445
+ },
1446
+ hkdfKey,
1447
+ { name: "AES-GCM", length: encryptionKeyLength },
1448
+ extractable,
1449
+ ["encrypt", "decrypt"]
1450
+ );
1451
+ return {
1452
+ authKey,
1453
+ encryptionKey,
1454
+ salt: saltBase64
1455
+ };
1456
+ } finally {
1457
+ if (typeof password === "string") {
1458
+ rawPasswordBytes.fill(0);
855
1459
  }
856
- if (options?.valueEncryption === true) {
857
- const result = { ...data };
858
- const keysToEncrypt = Object.keys(result).filter(
859
- (key) => result[key] !== void 0 && result[key] !== null && !this.isEncrypted(result[key])
860
- );
861
- if (keysToEncrypt.length === 0) {
862
- return result;
863
- }
864
- if (!options?._warmed) {
865
- await this.warmupKey(options);
866
- }
867
- const entries = await Promise.all(
868
- keysToEncrypt.map(async (key) => {
869
- const fieldOptions = this._bindFieldAad(data, key, options);
870
- return [key, await this.encryptValue(result[key], fieldOptions)];
871
- })
872
- );
873
- for (const [key, encryptedVal] of entries) {
874
- safeSetProperty(result, key, encryptedVal);
875
- }
876
- return result;
1460
+ if (masterBits) {
1461
+ new Uint8Array(masterBits).fill(0);
877
1462
  }
878
1463
  }
879
- return this.encryptValue(data, options);
880
1464
  }
1465
+ // ── Digital Signatures, Fingerprints & Access Grants ─────────────────────
881
1466
  /**
882
- * Top-level polymorphic decryption method.
883
- * Automatically performs value-level decryption:
884
- * - If payload is an encrypted envelope: decrypts directly via decryptValue.
885
- * - If payload is an array of objects: decrypts each row.
886
- * - If payload is an object: decrypts any encrypted value cells across fields.
1467
+ * Generates an ECDSA P-256 signing key pair.
887
1468
  */
1469
+ static async generateSigningKeyPair(curve = "P-256", extractable = true) {
1470
+ const subtle = getSubtleCrypto();
1471
+ const pair = await subtle.generateKey(
1472
+ {
1473
+ name: "ECDSA",
1474
+ namedCurve: curve
1475
+ },
1476
+ extractable,
1477
+ ["sign", "verify"]
1478
+ );
1479
+ return {
1480
+ publicKey: pair.publicKey,
1481
+ privateKey: pair.privateKey
1482
+ };
1483
+ }
888
1484
  /**
889
- * Top-level polymorphic decryption method.
890
- * Automatically performs value-level decryption:
891
- * - If payload is an encrypted envelope: decrypts directly via decryptValue.
892
- * - If payload is an array of objects: decrypts each row (batched with local memoization).
893
- * - If payload is an object: decrypts any encrypted value cells across fields.
1485
+ * Generates an ECDSA signing key pair. Alias for generateSigningKeyPair.
894
1486
  */
895
- async decrypt(payload, options) {
896
- if (payload === void 0 || payload === null) return payload;
897
- if (this.isEncrypted(payload)) {
898
- return this.decryptValue(payload, options);
899
- }
900
- if (Array.isArray(payload)) {
901
- if (payload.length === 0) return payload;
902
- const localMemo = /* @__PURE__ */ new Map();
903
- const decryptRow = async (item) => {
904
- if (!item) return item;
905
- if (this.isEncrypted(item)) {
906
- const ct = item.ciphertext;
907
- const iv = item.iv || "";
908
- const aadPart = options?.aad ? typeof options.aad === "string" ? options.aad : "" : "";
909
- const memoKey = `${ct}:${iv}:${aadPart}`;
910
- if (!localMemo.has(memoKey)) {
911
- localMemo.set(memoKey, this.decryptValue(item, options));
912
- }
913
- return localMemo.get(memoKey);
914
- }
915
- if (typeof item === "object" && !ArrayBuffer.isView(item) && !(item instanceof ArrayBuffer)) {
916
- const targetKeys = options?.fields && options.fields.length > 0 ? options.fields.filter((f) => Object.prototype.hasOwnProperty.call(item, f)) : Object.keys(item);
917
- const keysToDecrypt = targetKeys.filter((key) => this.isEncrypted(item[key]));
918
- if (keysToDecrypt.length === 0) {
919
- return item;
920
- }
921
- const proto = Object.getPrototypeOf(item);
922
- const result = proto && proto !== Object.prototype ? Object.assign(Object.create(proto), item) : { ...item };
923
- await Promise.all(
924
- keysToDecrypt.map(async (key) => {
925
- const env = item[key];
926
- const boundOptions = this._bindFieldAad(item, key, options);
927
- const hasBoundAad = boundOptions.aad !== options?.aad;
928
- const ct = env.ciphertext;
929
- const iv = env.iv || "";
930
- const aadPart = boundOptions.aad ? typeof boundOptions.aad === "string" ? boundOptions.aad : "" : "";
931
- const memoKey = `${ct}:${iv}:${aadPart}`;
932
- if (!localMemo.has(memoKey)) {
933
- const decryptPromise = this.decryptValue(env, boundOptions).catch((err) => {
934
- if (hasBoundAad && isAuthOrDecryptionError(err)) {
935
- return this.decryptValue(env, options);
936
- }
937
- throw err;
938
- });
939
- localMemo.set(memoKey, decryptPromise);
940
- }
941
- const decryptedVal = await localMemo.get(memoKey);
942
- safeSetProperty(result, key, decryptedVal);
943
- })
944
- );
945
- return result;
946
- }
947
- return item;
948
- };
949
- if (payload.length <= 100) {
950
- return Promise.all(payload.map(decryptRow));
951
- }
952
- const batchSize = 1e3;
953
- const results = new Array(payload.length);
954
- for (let i = 0; i < payload.length; i += batchSize) {
955
- const end = Math.min(i + batchSize, payload.length);
956
- const chunk = payload.slice(i, end);
957
- const chunkRes = await Promise.all(chunk.map(decryptRow));
958
- for (let j = 0; j < chunkRes.length; j++) {
959
- results[i + j] = chunkRes[j];
960
- }
961
- if (end < payload.length && (i > 0 && i % 5e3 === 0)) {
962
- await new Promise((r) => setTimeout(r, 0));
1487
+ static async generateECDSAKeyPair(curve = "P-256", extractable = true) {
1488
+ return CKeyManager.generateSigningKeyPair(curve, extractable);
1489
+ }
1490
+ /**
1491
+ * Generates a deterministic SHA-256 fingerprint for a public key (SPKI or raw).
1492
+ * Formatted as `SHA256:<base64url>`.
1493
+ */
1494
+ static async getKeyFingerprint(publicKey) {
1495
+ const subtle = getSubtleCrypto();
1496
+ let rawBytes;
1497
+ if (typeof publicKey === "object" && publicKey !== null && "algorithm" in publicKey) {
1498
+ const cryptoKey = publicKey;
1499
+ rawBytes = await subtle.exportKey("spki", cryptoKey);
1500
+ } else if (publicKey instanceof Uint8Array) {
1501
+ rawBytes = publicKey;
1502
+ } else if (publicKey instanceof ArrayBuffer) {
1503
+ rawBytes = publicKey;
1504
+ } else if (typeof publicKey === "string") {
1505
+ const trimmed = publicKey.trim();
1506
+ if (trimmed.startsWith("-----BEGIN")) {
1507
+ const b642 = trimmed.replace(/-----[^-]+-----/g, "").replace(/\s+/g, "");
1508
+ rawBytes = CKeyManager.fromBase64(b642);
1509
+ } else {
1510
+ try {
1511
+ rawBytes = CKeyManager.fromBase64(trimmed);
1512
+ } catch {
1513
+ rawBytes = textEncoder$1.encode(trimmed);
963
1514
  }
964
1515
  }
965
- return results;
966
- }
967
- if (typeof payload === "object" && !ArrayBuffer.isView(payload) && !(payload instanceof ArrayBuffer)) {
968
- const targetKeys = options?.fields && options.fields.length > 0 ? options.fields.filter((f) => Object.prototype.hasOwnProperty.call(payload, f)) : Object.keys(payload);
969
- const keysToDecrypt = targetKeys.filter((key) => this.isEncrypted(payload[key]));
970
- if (keysToDecrypt.length === 0) {
971
- return payload;
972
- }
973
- const proto = Object.getPrototypeOf(payload);
974
- const result = proto && proto !== Object.prototype ? Object.assign(Object.create(proto), payload) : { ...payload };
975
- const entries = await Promise.all(
976
- keysToDecrypt.map(async (key) => {
977
- const boundOptions = this._bindFieldAad(payload, key, options);
978
- const hasBoundAad = boundOptions.aad !== options?.aad;
979
- try {
980
- return [key, await this.decryptValue(result[key], boundOptions)];
981
- } catch (err) {
982
- if (hasBoundAad && isAuthOrDecryptionError(err)) {
983
- try {
984
- return [key, await this.decryptValue(result[key], options)];
985
- } catch (fallbackErr) {
986
- throw fallbackErr;
987
- }
988
- }
989
- throw err;
990
- }
991
- })
1516
+ } else {
1517
+ throw new Error(
1518
+ "[CKeyManager] Invalid key input for getKeyFingerprint: expected CryptoKey, Uint8Array, ArrayBuffer, or string."
992
1519
  );
993
- for (const [key, decryptedVal] of entries) {
994
- safeSetProperty(result, key, decryptedVal);
995
- }
996
- return result;
997
1520
  }
998
- return payload;
1521
+ const hashBuffer = await subtle.digest("SHA-256", rawBytes);
1522
+ const b64 = CKeyManager.toBase64(hashBuffer);
1523
+ const b64url = b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1524
+ return `SHA256:${b64url}`;
999
1525
  }
1000
- }
1001
-
1002
- class CKeyManager {
1003
- // ── Binary & Encoding Utilities ──────────────────────────────────────────
1004
1526
  /**
1005
- * Generates a cryptographically secure random byte buffer of the specified length.
1527
+ * Signs arbitrary data or JSON objects using an ECDSA P-256 private key.
1528
+ * Objects are automatically serialized via canonicalizeJson() before hashing.
1006
1529
  */
1007
- static randomBytes(length) {
1008
- const bytes = new Uint8Array(length);
1009
- return getRandomValues(bytes);
1530
+ static async sign(data, privateKey) {
1531
+ const subtle = getSubtleCrypto();
1532
+ if (!privateKey || !privateKey.algorithm || privateKey.algorithm.name !== "ECDSA") {
1533
+ throw new Error("[CKeyManager] sign requires an ECDSA private key.");
1534
+ }
1535
+ let dataBytes;
1536
+ if (data instanceof Uint8Array) {
1537
+ dataBytes = data;
1538
+ } else if (ArrayBuffer.isView(data)) {
1539
+ dataBytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
1540
+ } else if (data instanceof ArrayBuffer) {
1541
+ dataBytes = new Uint8Array(data);
1542
+ } else if (typeof data === "string") {
1543
+ dataBytes = textEncoder$1.encode(data);
1544
+ } else {
1545
+ const canonical = canonicalizeJson(data);
1546
+ dataBytes = textEncoder$1.encode(canonical);
1547
+ }
1548
+ const namedCurve = privateKey.algorithm.namedCurve || "P-256";
1549
+ const hashName = namedCurve === "P-384" ? "SHA-384" : namedCurve === "P-521" ? "SHA-512" : "SHA-256";
1550
+ const signatureBuffer = await subtle.sign(
1551
+ {
1552
+ name: "ECDSA",
1553
+ hash: { name: hashName }
1554
+ },
1555
+ privateKey,
1556
+ dataBytes
1557
+ );
1558
+ return CKeyManager.toBase64(signatureBuffer);
1010
1559
  }
1011
1560
  /**
1012
- * Converts an ArrayBuffer or Uint8Array into a Base64-encoded string.
1561
+ * Verifies an ECDSA P-256 signature against original data or JSON object.
1013
1562
  */
1014
- static toBase64(buffer) {
1015
- return toBase64(buffer);
1563
+ static async verify(data, signature, publicKey) {
1564
+ const subtle = getSubtleCrypto();
1565
+ if (!publicKey || !publicKey.algorithm || publicKey.algorithm.name !== "ECDSA") {
1566
+ throw new Error("[CKeyManager] verify requires an ECDSA public key.");
1567
+ }
1568
+ if (typeof signature !== "string" || !signature.trim()) {
1569
+ return false;
1570
+ }
1571
+ try {
1572
+ const signatureBytes = CKeyManager.fromBase64(signature);
1573
+ let dataBytes;
1574
+ if (data instanceof Uint8Array) {
1575
+ dataBytes = data;
1576
+ } else if (ArrayBuffer.isView(data)) {
1577
+ dataBytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
1578
+ } else if (data instanceof ArrayBuffer) {
1579
+ dataBytes = new Uint8Array(data);
1580
+ } else if (typeof data === "string") {
1581
+ dataBytes = textEncoder$1.encode(data);
1582
+ } else {
1583
+ const canonical = canonicalizeJson(data);
1584
+ dataBytes = textEncoder$1.encode(canonical);
1585
+ }
1586
+ const namedCurve = publicKey.algorithm.namedCurve || "P-256";
1587
+ const hashName = namedCurve === "P-384" ? "SHA-384" : namedCurve === "P-521" ? "SHA-512" : "SHA-256";
1588
+ return await subtle.verify(
1589
+ {
1590
+ name: "ECDSA",
1591
+ hash: { name: hashName }
1592
+ },
1593
+ publicKey,
1594
+ signatureBytes,
1595
+ dataBytes
1596
+ );
1597
+ } catch {
1598
+ return false;
1599
+ }
1016
1600
  }
1017
1601
  /**
1018
- * Converts a Base64 or Base64URL-encoded string back into a Uint8Array.
1019
- * Normalizes URL-safe characters and padding, strictly validating against the Base64 alphabet.
1602
+ * Computes a deterministic HMAC-SHA256 blind index hash for an arbitrary term using the supplied CryptoKey.
1603
+ * Performs Unicode NFKC normalization, trimming, and lowercasing before computing the signature.
1604
+ *
1605
+ * @param value - The input string, number, or object to compute the blind index for.
1606
+ * @param hmacKey - CryptoKey configured for HMAC signing.
1607
+ * @returns Lowercase hexadecimal HMAC-SHA256 blind index hash.
1020
1608
  */
1021
- static fromBase64(base64) {
1022
- if (typeof base64 !== "string") {
1023
- throw new Error("[CKeyManager] Invalid base64 input: expected string");
1024
- }
1025
- const cleaned = base64.replace(/\s+/g, "");
1026
- if (!cleaned) return new Uint8Array(0);
1027
- const normalized = cleaned.replace(/-/g, "+").replace(/_/g, "/");
1028
- const padded = normalized.padEnd(normalized.length + (4 - normalized.length % 4) % 4, "=");
1029
- if (!/^[A-Za-z0-9+/]+={0,2}$/.test(padded)) {
1030
- throw new Error("[CKeyManager] Invalid base64 string");
1609
+ static async computeBlindIndex(value, hmacKey) {
1610
+ if (value === null || value === void 0) return "";
1611
+ const normalized = String(value).normalize("NFKC").trim().toLowerCase();
1612
+ if (!normalized) return "";
1613
+ if (!hmacKey || !hmacKey.algorithm || hmacKey.algorithm.name !== "HMAC") {
1614
+ throw new Error("[CKeyManager] computeBlindIndex requires an HMAC CryptoKey.");
1031
1615
  }
1032
- if (typeof Buffer !== "undefined") {
1033
- const buf = Buffer.from(padded, "base64");
1034
- const arrayBuf = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
1035
- return new Uint8Array(arrayBuf);
1616
+ const subtle = getSubtleCrypto();
1617
+ const signature = await subtle.sign("HMAC", hmacKey, textEncoder$1.encode(normalized));
1618
+ return Array.from(new Uint8Array(signature)).map((b) => b.toString(16).padStart(2, "0")).join("");
1619
+ }
1620
+ /**
1621
+ * Signs a structured access grant, embedding signature and granter key fingerprint.
1622
+ */
1623
+ static async signGrant(grant, granterPrivateKey, granterPublicKey) {
1624
+ if (!grant || typeof grant !== "object") {
1625
+ throw new Error("[CKeyManager] signGrant requires a valid grant object.");
1036
1626
  }
1037
- const binary = atob(padded);
1038
- const bytes = new Uint8Array(binary.length);
1039
- for (let i = 0; i < binary.length; i++) {
1040
- bytes[i] = binary.charCodeAt(i);
1627
+ let fingerprint;
1628
+ if (granterPublicKey) {
1629
+ fingerprint = await CKeyManager.getKeyFingerprint(granterPublicKey);
1630
+ } else {
1631
+ const subtle = getSubtleCrypto();
1632
+ try {
1633
+ const jwk = await subtle.exportKey("jwk", granterPrivateKey);
1634
+ const pubJwk = {
1635
+ kty: jwk.kty,
1636
+ crv: jwk.crv,
1637
+ x: jwk.x,
1638
+ y: jwk.y,
1639
+ ext: true,
1640
+ key_ops: ["verify"]
1641
+ };
1642
+ const pubKey = await subtle.importKey(
1643
+ "jwk",
1644
+ pubJwk,
1645
+ { name: "ECDSA", namedCurve: jwk.crv || "P-256" },
1646
+ true,
1647
+ ["verify"]
1648
+ );
1649
+ fingerprint = await CKeyManager.getKeyFingerprint(pubKey);
1650
+ } catch {
1651
+ throw new Error(
1652
+ "[CKeyManager] signGrant requires granterPublicKey when granterPrivateKey is non-extractable."
1653
+ );
1654
+ }
1041
1655
  }
1042
- return bytes;
1656
+ const signature = await CKeyManager.sign(grant, granterPrivateKey);
1657
+ return {
1658
+ grant: { ...grant },
1659
+ signature,
1660
+ fingerprint
1661
+ };
1043
1662
  }
1044
1663
  /**
1045
- * Deterministically resolves a salt from Uint8Array, ArrayBuffer, Base64/Base64URL string, or raw text string.
1046
- * Guarantees 100% identical salt byte resolution between Node.js and Browser environments.
1664
+ * Verifies the authenticity and integrity of a signed access grant against granter public key.
1047
1665
  */
1048
- static resolveSalt(salt) {
1049
- if (!salt) return CKeyManager.randomBytes(16);
1050
- if (ArrayBuffer.isView(salt)) {
1051
- return salt.byteLength > 0 ? new Uint8Array(salt.buffer.slice(salt.byteOffset, salt.byteOffset + salt.byteLength)) : CKeyManager.randomBytes(16);
1666
+ static async verifyGrant(signedGrant, granterPublicKey, options) {
1667
+ if (!signedGrant || !signedGrant.grant || !signedGrant.signature || !signedGrant.fingerprint || !granterPublicKey) {
1668
+ return false;
1052
1669
  }
1053
- if (salt instanceof ArrayBuffer) {
1054
- return salt.byteLength > 0 ? new Uint8Array(salt.slice(0)) : CKeyManager.randomBytes(16);
1670
+ const expectedFingerprint = await CKeyManager.getKeyFingerprint(granterPublicKey);
1671
+ if (signedGrant.fingerprint !== expectedFingerprint) {
1672
+ return false;
1055
1673
  }
1056
- if (typeof salt === "string") {
1057
- const trimmed = salt.trim();
1058
- if (!trimmed) return CKeyManager.randomBytes(16);
1059
- if (trimmed.startsWith("base64:")) {
1060
- return CKeyManager.fromBase64(trimmed.slice(7));
1061
- }
1062
- if (trimmed.endsWith("=") && /^[A-Za-z0-9+/]+={1,2}$/.test(trimmed)) {
1063
- try {
1064
- const decoded = CKeyManager.fromBase64(trimmed);
1065
- if (decoded.byteLength > 0) return decoded;
1066
- } catch {
1067
- }
1674
+ if (options?.checkExpiry && signedGrant.grant.expiresAt !== void 0) {
1675
+ const now = options.now ?? Date.now();
1676
+ if (now > signedGrant.grant.expiresAt) {
1677
+ return false;
1068
1678
  }
1069
- const encoded = textEncoder$1.encode(trimmed);
1070
- return encoded.byteLength > 0 ? encoded : CKeyManager.randomBytes(16);
1071
1679
  }
1072
- return CKeyManager.randomBytes(16);
1680
+ return await CKeyManager.verify(signedGrant.grant, signedGrant.signature, granterPublicKey);
1681
+ }
1682
+ }
1683
+
1684
+ class AEncryptionProvider {
1685
+ defaultKey;
1686
+ _hmacKey;
1687
+ keys;
1688
+ activeKid;
1689
+ allowedKids;
1690
+ constructor(key, options) {
1691
+ if (key !== void 0) {
1692
+ this.defaultKey = key;
1693
+ }
1694
+ if (options?.keys) {
1695
+ this.keys = options.keys;
1696
+ }
1697
+ if (options?.activeKid) {
1698
+ this.activeKid = options.activeKid;
1699
+ }
1700
+ if (options?.allowedKids) {
1701
+ this.allowedKids = options.allowedKids;
1702
+ }
1703
+ if (options?.hmacKey) {
1704
+ this._hmacKey = options.hmacKey;
1705
+ }
1706
+ }
1707
+ /**
1708
+ * Sets or updates the default secret key, CryptoKey, or async key provider function.
1709
+ */
1710
+ setDefaultKey(key) {
1711
+ this.defaultKey = key || void 0;
1712
+ }
1713
+ /**
1714
+ * Retrieves the currently configured default key or key provider callback.
1715
+ */
1716
+ getDefaultKey() {
1717
+ return this.defaultKey;
1718
+ }
1719
+ /**
1720
+ * Configures a keyring dictionary of keys with an optional active key identifier.
1721
+ */
1722
+ setKeyring(keys, activeKid) {
1723
+ this.keys = keys;
1724
+ if (activeKid !== void 0) {
1725
+ this.activeKid = activeKid;
1726
+ }
1727
+ }
1728
+ /**
1729
+ * Retrieves the currently configured keyring dictionary.
1730
+ */
1731
+ getKeyring() {
1732
+ return this.keys;
1733
+ }
1734
+ /**
1735
+ * Sets the active Key Identifier (kid) to use for outbound encryption.
1736
+ */
1737
+ setActiveKid(kid) {
1738
+ this.activeKid = kid;
1739
+ }
1740
+ /**
1741
+ * Retrieves the active Key Identifier (kid).
1742
+ */
1743
+ getActiveKid() {
1744
+ return this.activeKid;
1745
+ }
1746
+ /**
1747
+ * Sets or updates the dedicated HMAC key or key provider callback for blind indexing.
1748
+ */
1749
+ setHmacKey(key) {
1750
+ this._hmacKey = key || void 0;
1751
+ }
1752
+ /**
1753
+ * Retrieves the currently configured HMAC key or key provider callback.
1754
+ */
1755
+ getHmacKey() {
1756
+ return this._hmacKey;
1073
1757
  }
1074
- // ── Symmetric Key Management ─────────────────────────────────────────────
1075
1758
  /**
1076
- * Generates a random AES CryptoKey suitable for AES-GCM or AES-CBC.
1077
- *
1078
- * E2EE Threat Model Note: Focuses on protecting GDPR/tenant data against database and
1079
- * server-side leaks. Keys remain extractable by default to enable role-based key sharing,
1080
- * wrapping, and transfer between authorized client sessions in multi-user SaaS apps.
1081
- * Client-side malware/trojan protection is out of scope.
1759
+ * Computes a deterministic HMAC-SHA256 blind index hash for an arbitrary term.
1760
+ * If no hmacKey is explicitly provided, uses the provider's configured hmacKey or default key.
1082
1761
  */
1083
- static async generateAESKey(length = 256, algorithm = "AES-GCM", extractable = true) {
1762
+ async computeBlindIndex(value, hmacKey) {
1763
+ if (value === null || value === void 0) return "";
1764
+ const keyMaterial = hmacKey || this._hmacKey || this.defaultKey;
1765
+ if (!keyMaterial) {
1766
+ throw new Error("[AEncryptionProvider] computeBlindIndex requires an HMAC key. Configure hmacKey or pass a key directly.");
1767
+ }
1768
+ const resolvedKey = typeof keyMaterial === "function" ? await keyMaterial() : keyMaterial;
1769
+ if (resolvedKey && typeof resolvedKey !== "string" && resolvedKey.algorithm) {
1770
+ return CKeyManager.computeBlindIndex(value, resolvedKey);
1771
+ }
1084
1772
  const subtle = getSubtleCrypto();
1085
- const usages = algorithm === "AES-KW" ? ["wrapKey", "unwrapKey"] : ["encrypt", "decrypt"];
1086
- return await subtle.generateKey(
1087
- { name: algorithm, length },
1088
- extractable,
1089
- usages
1773
+ const keyBytes = typeof resolvedKey === "string" ? new TextEncoder().encode(resolvedKey) : resolvedKey instanceof Uint8Array ? resolvedKey : new Uint8Array(resolvedKey);
1774
+ const importedHmacKey = await subtle.importKey(
1775
+ "raw",
1776
+ keyBytes,
1777
+ { name: "HMAC", hash: "SHA-256" },
1778
+ false,
1779
+ ["sign"]
1090
1780
  );
1781
+ return CKeyManager.computeBlindIndex(value, importedHmacKey);
1091
1782
  }
1092
1783
  /**
1093
- * Exports an AES CryptoKey as a raw base64 string.
1784
+ * Pre-derives and caches the native CryptoKey for subsequent cryptographic operations.
1785
+ * Subclasses should override this with a dedicated, non-side-effecting implementation.
1094
1786
  */
1095
- static async exportAESKey(key) {
1096
- const subtle = getSubtleCrypto();
1097
- const raw = await subtle.exportKey("raw", key);
1098
- try {
1099
- return CKeyManager.toBase64(raw);
1100
- } finally {
1101
- new Uint8Array(raw).fill(0);
1102
- }
1787
+ async warmupKey(_options) {
1103
1788
  }
1104
1789
  /**
1105
- * Imports an AES CryptoKey from a raw base64 string.
1790
+ * Resolves raw key from keyProvider callback or defaultKey.
1106
1791
  */
1107
- static async importAESKey(base64, algorithm = "AES-GCM", extractable = false) {
1108
- const subtle = getSubtleCrypto();
1109
- const rawBytes = CKeyManager.fromBase64(base64);
1110
- if (rawBytes.length !== 16 && rawBytes.length !== 24 && rawBytes.length !== 32) {
1111
- rawBytes.fill(0);
1112
- throw new Error(`[CKeyManager] Invalid AES key length: expected 16, 24, or 32 bytes (128, 192, or 256 bits), got ${rawBytes.length} bytes.`);
1792
+ async resolveRawKey(keyProvider) {
1793
+ const target = keyProvider || (this.keys && this.activeKid ? this.keys[this.activeKid] : void 0) || this.defaultKey;
1794
+ if (!target) {
1795
+ throw new Error(
1796
+ `[${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.`
1797
+ );
1113
1798
  }
1114
- const usages = algorithm === "AES-KW" ? ["wrapKey", "unwrapKey"] : ["encrypt", "decrypt"];
1115
- try {
1116
- return await subtle.importKey(
1117
- "raw",
1118
- rawBytes,
1119
- { name: algorithm },
1120
- extractable,
1121
- usages
1799
+ const rawKey = typeof target === "function" ? await target() : target;
1800
+ if (!rawKey || typeof rawKey === "string" && rawKey.trim().length === 0 || typeof rawKey === "object" && "byteLength" in rawKey && rawKey.byteLength === 0) {
1801
+ throw new Error(
1802
+ `[${this.constructor.name}] No encryption key or keyProvider was supplied. Key provider resolved to empty or undefined key.`
1122
1803
  );
1123
- } finally {
1124
- rawBytes.fill(0);
1125
1804
  }
1805
+ return rawKey;
1126
1806
  }
1127
- // ── Asymmetric Key Pair Management ───────────────────────────────────────
1128
1807
  /**
1129
- * Generates an RSA-OAEP public/private key pair (2048 or 4096 bits).
1808
+ * Checks whether the given payload is an encrypted envelope.
1130
1809
  */
1131
- static async generateRSAKeyPair(bits = 4096, hash = "SHA-256", extractable = true) {
1132
- const subtle = getSubtleCrypto();
1133
- const pair = await subtle.generateKey(
1134
- {
1135
- name: "RSA-OAEP",
1136
- modulusLength: bits,
1137
- publicExponent: new Uint8Array([1, 0, 1]),
1138
- hash
1139
- },
1140
- extractable,
1141
- ["encrypt", "decrypt", "wrapKey", "unwrapKey"]
1142
- );
1143
- return {
1144
- publicKey: pair.publicKey,
1145
- privateKey: pair.privateKey
1146
- };
1810
+ static isEncrypted(payload) {
1811
+ if (!payload || typeof payload !== "object") return false;
1812
+ return payload.__encrypted === true && typeof payload.ciphertext === "string" && typeof payload.iv === "string";
1147
1813
  }
1148
1814
  /**
1149
- * Generates an ECDH public/private key pair for elliptic curve Diffie-Hellman.
1815
+ * Checks whether the given payload is an encrypted envelope.
1150
1816
  */
1151
- static async generateECDHKeyPair(curve = "P-256", extractable = true) {
1152
- const subtle = getSubtleCrypto();
1153
- const pair = await subtle.generateKey(
1154
- {
1155
- name: "ECDH",
1156
- namedCurve: curve
1157
- },
1158
- extractable,
1159
- ["deriveKey", "deriveBits"]
1160
- );
1161
- return {
1162
- publicKey: pair.publicKey,
1163
- privateKey: pair.privateKey
1164
- };
1817
+ isEncrypted(payload) {
1818
+ return AEncryptionProvider.isEncrypted(payload);
1165
1819
  }
1166
1820
  /**
1167
- * Exports a public CryptoKey to a portable JsonWebKey (JWK).
1821
+ * Clears configured keys and cached cryptographic material from memory.
1168
1822
  */
1169
- static async exportPublicKey(key) {
1170
- const subtle = getSubtleCrypto();
1171
- return await subtle.exportKey("jwk", key);
1823
+ clearKeys() {
1824
+ this.defaultKey = void 0;
1825
+ this.keys = void 0;
1826
+ this.activeKid = void 0;
1827
+ this.allowedKids = void 0;
1172
1828
  }
1173
1829
  /**
1174
- * Exports a private CryptoKey to a portable JsonWebKey (JWK).
1830
+ * Alias for clearKeys() to conform with standard disposal patterns.
1175
1831
  */
1176
- static async exportPrivateKey(key) {
1177
- const subtle = getSubtleCrypto();
1178
- return await subtle.exportKey("jwk", key);
1832
+ dispose() {
1833
+ this.clearKeys();
1179
1834
  }
1180
1835
  /**
1181
- * Imports a public key from a JsonWebKey (JWK).
1836
+ * ECMAScript Explicit Resource Management protocol (using / await using declarations).
1182
1837
  */
1183
- static async importPublicKey(jwk, algorithm) {
1184
- const subtle = getSubtleCrypto();
1185
- let importAlgorithm = algorithm;
1186
- let usages;
1187
- const isEC = jwk.kty === "EC" || typeof algorithm === "object" && algorithm?.name === "ECDH" || algorithm === "ECDH";
1188
- if (isEC) {
1189
- usages = [];
1190
- if (!importAlgorithm || importAlgorithm === "ECDH") {
1191
- importAlgorithm = { name: "ECDH", namedCurve: jwk.crv || "P-256" };
1192
- }
1193
- } else {
1194
- usages = ["encrypt", "wrapKey"];
1195
- if (!importAlgorithm || importAlgorithm === "RSA-OAEP") {
1196
- if (jwk.kty === "RSA") {
1197
- const hash = jwk.alg === "RSA-OAEP-512" ? "SHA-512" : "SHA-256";
1198
- importAlgorithm = { name: "RSA-OAEP", hash };
1199
- } else {
1200
- throw new Error(`[CKeyManager] Unsupported JWK kty: ${jwk.kty}`);
1201
- }
1202
- }
1203
- }
1204
- return await subtle.importKey("jwk", jwk, importAlgorithm, true, usages);
1838
+ [Symbol.dispose]() {
1839
+ this.dispose();
1840
+ }
1841
+ async [Symbol.asyncDispose]() {
1842
+ this.dispose();
1205
1843
  }
1206
1844
  /**
1207
- * Imports a private key from a JsonWebKey (JWK).
1845
+ * Resolves effective Authenticated Additional Data (AAD) for the operation:
1846
+ * 1. If options.bindRequestContextAad is explicitly false, returns options.aad directly without context binding.
1847
+ * 2. When encrypting (isDecrypt=false): binds request context `req:${method}:${url}` only if options.bindRequestContextAad === true.
1848
+ * 3. When decrypting (isDecrypt=true): derives request context `req:${method}:${url}` if url or method is provided.
1849
+ * 4. If explicit options.aad is provided: combines with request context if bound, or returns options.aad directly.
1850
+ * 5. Otherwise returns undefined.
1208
1851
  */
1209
- static async importPrivateKey(jwk, algorithm) {
1210
- const subtle = getSubtleCrypto();
1211
- let importAlgorithm = algorithm;
1212
- let usages;
1213
- const isEC = jwk.kty === "EC" || typeof algorithm === "object" && algorithm?.name === "ECDH" || algorithm === "ECDH";
1214
- if (isEC) {
1215
- usages = ["deriveKey", "deriveBits"];
1216
- if (!importAlgorithm || importAlgorithm === "ECDH") {
1217
- importAlgorithm = { name: "ECDH", namedCurve: jwk.crv || "P-256" };
1218
- }
1219
- } else {
1220
- usages = ["decrypt", "unwrapKey"];
1221
- if (!importAlgorithm || importAlgorithm === "RSA-OAEP") {
1222
- if (jwk.kty === "RSA") {
1223
- const hash = jwk.alg === "RSA-OAEP-512" ? "SHA-512" : "SHA-256";
1224
- importAlgorithm = { name: "RSA-OAEP", hash };
1225
- } else {
1226
- throw new Error(`[CKeyManager] Unsupported JWK kty: ${jwk.kty}`);
1852
+ _resolveEffectiveAad(options, isDecrypt = false) {
1853
+ if (!options) return void 0;
1854
+ let rawAad = options.aad;
1855
+ if (typeof rawAad === "function") {
1856
+ rawAad = rawAad(void 0, "");
1857
+ }
1858
+ if (rawAad === void 0 && options.baseAad) {
1859
+ rawAad = options.baseAad;
1860
+ }
1861
+ if (options.bindRequestContextAad === false) return rawAad;
1862
+ const hasReqContext = Boolean(options.url || options.method);
1863
+ if (rawAad) {
1864
+ if (options.bindRequestContextAad === true && hasReqContext) {
1865
+ const m = (options.method || "POST").toUpperCase();
1866
+ const u = options.url || "";
1867
+ const baseAad = typeof rawAad === "string" ? rawAad : rawAad instanceof Uint8Array || rawAad instanceof ArrayBuffer ? textDecoder.decode(rawAad) : String(rawAad);
1868
+ if (baseAad.includes(`|req:${m}:${u}`)) {
1869
+ return baseAad;
1227
1870
  }
1871
+ return `${baseAad}|req:${m}:${u}`;
1228
1872
  }
1873
+ return rawAad;
1229
1874
  }
1230
- return await subtle.importKey("jwk", jwk, importAlgorithm, true, usages);
1231
- }
1232
- /**
1233
- * Imports a public key from a PEM (SPKI) or base64 SPKI string.
1234
- */
1235
- static async importPublicKeyFromPEM(pem, algorithm = "RSA-OAEP", curve = "P-256", hash = "SHA-256") {
1236
- const subtle = getSubtleCrypto();
1237
- const cleaned = pem.replace(/-----BEGIN[ A-Z0-9_-]+-----/g, "").replace(/-----END[ A-Z0-9_-]+-----/g, "").replace(/\s+/g, "");
1238
- const spkiBytes = CKeyManager.fromBase64(cleaned);
1239
- let importAlgorithm;
1240
- let usages;
1241
- if (algorithm === "ECDH") {
1242
- importAlgorithm = { name: "ECDH", namedCurve: curve };
1243
- usages = [];
1244
- } else {
1245
- importAlgorithm = { name: "RSA-OAEP", hash };
1246
- usages = ["encrypt", "wrapKey"];
1247
- }
1248
- try {
1249
- return await subtle.importKey("spki", spkiBytes, importAlgorithm, true, usages);
1250
- } finally {
1251
- spkiBytes.fill(0);
1875
+ const shouldBindReq = isDecrypt ? hasReqContext : options.bindRequestContextAad === true && hasReqContext;
1876
+ if (shouldBindReq) {
1877
+ const m = (options.method || "POST").toUpperCase();
1878
+ const u = options.url || "";
1879
+ return `req:${m}:${u}`;
1252
1880
  }
1881
+ return void 0;
1253
1882
  }
1254
1883
  /**
1255
- * Imports a private key from a PEM (PKCS#8) or base64 PKCS#8 string.
1884
+ * Helper to deterministically bind record ID, contextual sibling fields (aadFields),
1885
+ * dynamic AAD callbacks, and field name into the AAD to prevent ciphertext swapping
1886
+ * between fields, records, or parent entities.
1256
1887
  */
1257
- static async importPrivateKeyFromPEM(pem, algorithm = "RSA-OAEP", curve = "P-256", hash = "SHA-256") {
1258
- if (/BEGIN\s+(RSA|EC)\s+PRIVATE\s+KEY/i.test(pem)) {
1259
- throw new Error(
1260
- `[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.`
1261
- );
1262
- }
1263
- const subtle = getSubtleCrypto();
1264
- const cleaned = pem.replace(/-----BEGIN[ A-Z0-9_-]+-----/g, "").replace(/-----END[ A-Z0-9_-]+-----/g, "").replace(/\s+/g, "");
1265
- const pkcs8Bytes = CKeyManager.fromBase64(cleaned);
1266
- let importAlgorithm;
1267
- let usages;
1268
- if (algorithm === "ECDH") {
1269
- importAlgorithm = { name: "ECDH", namedCurve: curve };
1270
- usages = ["deriveKey", "deriveBits"];
1271
- } else {
1272
- importAlgorithm = { name: "RSA-OAEP", hash };
1273
- usages = ["decrypt", "unwrapKey"];
1888
+ _bindFieldAad(data, field, options) {
1889
+ let baseAad = "";
1890
+ if (options?.baseAad) {
1891
+ baseAad = options.baseAad;
1274
1892
  }
1275
- try {
1276
- return await subtle.importKey("pkcs8", pkcs8Bytes, importAlgorithm, true, usages);
1277
- } finally {
1278
- pkcs8Bytes.fill(0);
1893
+ if (options?.aad) {
1894
+ if (typeof options.aad === "function") {
1895
+ const dynamicRes = options.aad(data, field);
1896
+ if (typeof dynamicRes === "string") {
1897
+ baseAad = baseAad ? `${baseAad}|${dynamicRes}` : dynamicRes;
1898
+ } else if (dynamicRes instanceof Uint8Array || dynamicRes instanceof ArrayBuffer) {
1899
+ const decoded = textDecoder.decode(dynamicRes);
1900
+ baseAad = baseAad ? `${baseAad}|${decoded}` : decoded;
1901
+ } else if (dynamicRes !== void 0 && dynamicRes !== null) {
1902
+ const str = String(dynamicRes);
1903
+ baseAad = baseAad ? `${baseAad}|${str}` : str;
1904
+ }
1905
+ } else if (typeof options.aad === "string") {
1906
+ baseAad = baseAad ? `${baseAad}|${options.aad}` : options.aad;
1907
+ } else if (options.aad instanceof Uint8Array || options.aad instanceof ArrayBuffer) {
1908
+ const decoded = textDecoder.decode(options.aad);
1909
+ baseAad = baseAad ? `${baseAad}|${decoded}` : decoded;
1910
+ } else {
1911
+ const str = String(options.aad);
1912
+ baseAad = baseAad ? `${baseAad}|${str}` : str;
1913
+ }
1279
1914
  }
1280
- }
1281
- /**
1282
- * Exports a public CryptoKey to a standard PEM (SPKI) string.
1283
- */
1284
- static async exportPublicKeyToPEM(key) {
1285
- const subtle = getSubtleCrypto();
1286
- const spki = await subtle.exportKey("spki", key);
1287
- let b64;
1288
- try {
1289
- b64 = CKeyManager.toBase64(spki);
1290
- } finally {
1291
- new Uint8Array(spki).fill(0);
1915
+ const recordId = data && typeof data === "object" && (data.id !== void 0 || data._id !== void 0) ? String(data.id ?? data._id) : "";
1916
+ const recordPart = recordId ? `|record:${recordId}` : "";
1917
+ let contextPart = "";
1918
+ if (options?.aadFields && options.aadFields.length > 0 && data && typeof data === "object") {
1919
+ const sortedFields = [...options.aadFields].sort();
1920
+ const parts = [];
1921
+ for (const f of sortedFields) {
1922
+ const val = data[f];
1923
+ let serializedVal = "";
1924
+ if (val === null) {
1925
+ serializedVal = "null";
1926
+ } else if (val !== void 0) {
1927
+ serializedVal = typeof val === "object" ? JSON.stringify(val) : String(val);
1928
+ }
1929
+ parts.push(`${f}=${serializedVal}`);
1930
+ }
1931
+ contextPart = `|ctx:${parts.join(",")}`;
1292
1932
  }
1293
- const lines = b64.match(/.{1,64}/g) || [b64];
1294
- return `-----BEGIN PUBLIC KEY-----
1295
- ${lines.join("\n")}
1296
- -----END PUBLIC KEY-----`;
1933
+ const reqPart = options?.bindRequestContextAad === true && (options?.url || options?.method) ? `|req:${(options.method || "POST").toUpperCase()}:${options.url || ""}` : "";
1934
+ return {
1935
+ ...options,
1936
+ bindRequestContextAad: false,
1937
+ aad: `aad:${baseAad}${recordPart}${contextPart}${reqPart}|field:${field}`
1938
+ };
1297
1939
  }
1298
1940
  /**
1299
- * Exports a private CryptoKey to a standard PEM (PKCS#8) string.
1941
+ * Validates envelope metadata against algorithm whitelists and key identifiers (kid).
1942
+ * In strict mode, allowedAlgorithms defaults to ['AES-GCM'].
1300
1943
  */
1301
- static async exportPrivateKeyToPEM(key) {
1302
- const subtle = getSubtleCrypto();
1303
- const pkcs8 = await subtle.exportKey("pkcs8", key);
1304
- let b64;
1305
- try {
1306
- b64 = CKeyManager.toBase64(pkcs8);
1307
- } finally {
1308
- new Uint8Array(pkcs8).fill(0);
1944
+ _validateEnvelopeMetadata(payload, options) {
1945
+ if (!payload || typeof payload !== "object") return void 0;
1946
+ const allowedAlgorithms = options?.allowedAlgorithms ?? (options?.strict ? ["AES-GCM"] : void 0);
1947
+ if (allowedAlgorithms && allowedAlgorithms.length > 0) {
1948
+ const alg = payload.alg;
1949
+ if (!alg || !allowedAlgorithms.includes(alg)) {
1950
+ return new Error(`[AEncryptionProvider] Algorithm mismatch: algorithm '${alg || "unknown"}' is not in allowedAlgorithms.`);
1951
+ }
1309
1952
  }
1310
- const lines = b64.match(/.{1,64}/g) || [b64];
1311
- return `-----BEGIN PRIVATE KEY-----
1312
- ${lines.join("\n")}
1313
- -----END PRIVATE KEY-----`;
1953
+ const effectiveKeys = options?.keys || this.keys;
1954
+ const effectiveAllowedKids = options?.allowedKids || this.allowedKids;
1955
+ const allowedKids = effectiveAllowedKids ?? (effectiveKeys ? Object.keys(effectiveKeys) : void 0);
1956
+ if (allowedKids && allowedKids.length > 0) {
1957
+ if (payload.kid && !allowedKids.includes(payload.kid)) {
1958
+ return new Error(`[AEncryptionProvider] Unauthorized key: kid '${payload.kid}' is not in allowedKids.`);
1959
+ }
1960
+ }
1961
+ return void 0;
1314
1962
  }
1315
- // ── Key Wrapping & Unwrapping ────────────────────────────────────────────
1316
1963
  /**
1317
- * Wraps a target CryptoKey (e.g. an Account Symmetric Key) using a wrapping key
1318
- * (AES-KW or RSA-OAEP).
1964
+ * Normalizes encryption options with active keyring parameters:
1965
+ * If `keys` and `activeKid` are configured, selects `key = keys[activeKid]` and sets `kid = activeKid`.
1319
1966
  */
1320
- static async wrapKey(keyToWrap, wrappingKey, format = "raw") {
1321
- const subtle = getSubtleCrypto();
1322
- let wrapAlgorithm;
1323
- if (wrappingKey.algorithm.name === "RSA-OAEP") {
1324
- wrapAlgorithm = { name: "RSA-OAEP" };
1325
- } else if (wrappingKey.algorithm.name === "AES-KW") {
1326
- wrapAlgorithm = "AES-KW";
1327
- } else {
1328
- wrapAlgorithm = "AES-KW";
1967
+ _resolveOutboundKeyringOptions(options) {
1968
+ const effectiveKeys = options?.keys || this.keys;
1969
+ const effectiveActiveKid = options?.activeKid || (options?.kid ? options.kid : this.activeKid);
1970
+ let effectiveKey = options?.key;
1971
+ let effectiveKid = options?.kid || effectiveActiveKid;
1972
+ if (effectiveKeys && effectiveActiveKid) {
1973
+ if (effectiveKeys[effectiveActiveKid]) {
1974
+ effectiveKey = options?.key || effectiveKeys[effectiveActiveKid];
1975
+ effectiveKid = effectiveActiveKid;
1976
+ } else if (!options?.key) {
1977
+ throw new Error(`[${this.constructor.name}] Active kid '${effectiveActiveKid}' not found in keyring keys.`);
1978
+ }
1979
+ } else if (effectiveKeys && !effectiveKey) {
1980
+ const kids = Object.keys(effectiveKeys);
1981
+ if (kids.length > 0) {
1982
+ effectiveKid = kids[0];
1983
+ effectiveKey = effectiveKeys[kids[0]];
1984
+ }
1985
+ }
1986
+ if (effectiveKeys || effectiveActiveKid || effectiveKey !== options?.key || effectiveKid !== options?.kid) {
1987
+ return {
1988
+ ...options,
1989
+ ...effectiveKeys ? { keys: effectiveKeys } : {},
1990
+ ...effectiveActiveKid ? { activeKid: effectiveActiveKid } : {},
1991
+ ...effectiveKey !== void 0 ? { key: effectiveKey } : {},
1992
+ ...effectiveKid !== void 0 ? { kid: effectiveKid } : {}
1993
+ };
1329
1994
  }
1330
- const wrappedBuffer = await subtle.wrapKey(format, keyToWrap, wrappingKey, wrapAlgorithm);
1331
- return CKeyManager.toBase64(wrappedBuffer);
1995
+ return options || {};
1332
1996
  }
1333
1997
  /**
1334
- * Unwraps a base64-encoded wrapped key using an unwrapping key (AES-KW or RSA-OAEP).
1998
+ * Resolves the appropriate decryption key for a specific payload envelope based on its `kid`
1999
+ * and the configured keyring (`keys`, `activeKid`, or fallback `key`).
1335
2000
  */
1336
- static async unwrapKey(wrappedBase64, unwrappingKey, unwrappedKeyAlgorithm = { name: "AES-GCM", length: 256 }, extractable = true, keyUsages = ["encrypt", "decrypt"], format = "raw") {
1337
- const subtle = getSubtleCrypto();
1338
- const wrappedBytes = CKeyManager.fromBase64(wrappedBase64);
1339
- let unwrapAlgorithm;
1340
- if (unwrappingKey.algorithm.name === "RSA-OAEP") {
1341
- unwrapAlgorithm = { name: "RSA-OAEP" };
1342
- } else {
1343
- unwrapAlgorithm = "AES-KW";
2001
+ _resolveDecryptionKeyForEnvelope(payload, options) {
2002
+ const effectiveKeys = options?.keys || this.keys;
2003
+ const effectiveActiveKid = options?.activeKid || this.activeKid;
2004
+ if (payload && typeof payload === "object" && payload.kid && effectiveKeys) {
2005
+ if (effectiveKeys[payload.kid]) {
2006
+ return effectiveKeys[payload.kid];
2007
+ }
1344
2008
  }
1345
- try {
1346
- return await subtle.unwrapKey(
1347
- format,
1348
- wrappedBytes,
1349
- unwrappingKey,
1350
- unwrapAlgorithm,
1351
- unwrappedKeyAlgorithm,
1352
- extractable,
1353
- keyUsages
1354
- );
1355
- } finally {
1356
- wrappedBytes.fill(0);
2009
+ if (effectiveKeys && effectiveActiveKid && effectiveKeys[effectiveActiveKid]) {
2010
+ return effectiveKeys[effectiveActiveKid];
1357
2011
  }
2012
+ return options?.key || this.defaultKey;
1358
2013
  }
1359
2014
  /**
1360
- * Generates a high-entropy symmetric AES-GCM Account Data Encryption Key (ADEK).
1361
- * Designed for multi-user table and field-level encryption.
2015
+ * Handles a per-field or per-payload decryption error according to configured options:
2016
+ * - Invokes onDecryptError callback if provided
2017
+ * - Attaches error metadata (__decryptErrors) to the record object
2018
+ * - Returns fallback cell placeholder ('[⚠ Decryption Error]') or rethrows
1362
2019
  */
1363
- static async generateAccountDataKey(length = 256, extractable = false) {
1364
- const subtle = getSubtleCrypto();
1365
- return await subtle.generateKey(
1366
- { name: "AES-GCM", length },
1367
- extractable,
1368
- ["encrypt", "decrypt"]
1369
- );
2020
+ _handleDecryptError(err, record, field, value, index, options) {
2021
+ if (record && typeof record === "object" && record !== value) {
2022
+ record.__decryptErrors = {
2023
+ ...record.__decryptErrors || {},
2024
+ [field || "__payload"]: err
2025
+ };
2026
+ }
2027
+ if (options?.onDecryptError === false || options?.onDecryptError === void 0) {
2028
+ throw err;
2029
+ }
2030
+ if (typeof options.onDecryptError === "function") {
2031
+ return options.onDecryptError({
2032
+ record,
2033
+ field,
2034
+ value,
2035
+ error: err,
2036
+ index
2037
+ });
2038
+ }
2039
+ if (typeof options.onDecryptError === "string") {
2040
+ return options.onDecryptError;
2041
+ }
2042
+ if (options.onDecryptError === true) {
2043
+ return "[\u26A0 Decryption Error]";
2044
+ }
2045
+ throw err;
1370
2046
  }
1371
2047
  /**
1372
- * Wraps an Account Data Encryption Key (ADEK) for a specific user using the user's public key (RSA-OAEP).
1373
- * Returns a Base64-encoded wrapped key string suitable for storage in role-map / access tables.
2048
+ * Encrypts a single scalar value with contextual AAD binding into an IEncryptedPayload envelope.
2049
+ * Binds recordId, fieldName, and sibling aadFields deterministically to protect against ciphertext swapping.
2050
+ *
2051
+ * @param value - Scalar value (string, number, boolean, object) to encrypt.
2052
+ * @param context - Field encryption context with recordId, fieldName, and optional sibling aadFields.
1374
2053
  */
1375
- static async wrapAccountKeyForUser(adek, userPublicKey) {
1376
- const subtle = getSubtleCrypto();
1377
- if (userPublicKey.algorithm.name === "RSA-OAEP") {
1378
- const rawKey = await subtle.exportKey("raw", adek);
1379
- try {
1380
- const wrappedBuffer = await subtle.encrypt(
1381
- { name: "RSA-OAEP" },
1382
- userPublicKey,
1383
- rawKey
1384
- );
1385
- return CKeyManager.toBase64(wrappedBuffer);
1386
- } finally {
1387
- new Uint8Array(rawKey).fill(0);
1388
- }
1389
- }
1390
- throw new Error(
1391
- `[CKeyManager] Unsupported user key algorithm for wrapAccountKeyForUser: '${userPublicKey.algorithm.name}'. Expected 'RSA-OAEP'.`
1392
- );
2054
+ async encryptField(value, context) {
2055
+ if (value === void 0 || value === null) return value;
2056
+ if (this.isEncrypted(value)) return value;
2057
+ const field = context?.fieldName || "field";
2058
+ const dummyData = {};
2059
+ if (context?.recordId !== void 0) {
2060
+ dummyData.id = context.recordId;
2061
+ }
2062
+ const { aadFields, ...restContext } = context || {};
2063
+ let boundOptions = { ...restContext };
2064
+ if (aadFields) {
2065
+ if (!Array.isArray(aadFields) && typeof aadFields === "object") {
2066
+ Object.assign(dummyData, aadFields);
2067
+ boundOptions.aadFields = Object.keys(aadFields);
2068
+ } else if (Array.isArray(aadFields)) {
2069
+ boundOptions.aadFields = aadFields;
2070
+ if (context?.record) {
2071
+ Object.assign(dummyData, context.record);
2072
+ }
2073
+ }
2074
+ }
2075
+ const resolvedOptions = this._bindFieldAad(dummyData, field, boundOptions);
2076
+ return this.encryptValue(value, resolvedOptions);
1393
2077
  }
1394
2078
  /**
1395
- * Unwraps a Base64-encoded Account Data Encryption Key (ADEK) using the user's private key (RSA-OAEP).
1396
- * Returns the restored AES-GCM CryptoKey ready for use in CWebCryptoEncryptionProvider.
2079
+ * Decrypts a single IEncryptedPayload envelope back into its scalar plaintext value using contextual AAD.
2080
+ * Validates recordId, fieldName, and sibling aadFields integrity.
2081
+ *
2082
+ * @param envelope - The IEncryptedPayload envelope to decrypt.
2083
+ * @param context - Field encryption context matching the encryption parameters.
1397
2084
  */
1398
- static async unwrapAccountKeyForUser(wrappedKeyBase64, userPrivateKey, unwrappedKeyAlgorithm = { name: "AES-GCM", length: 256 }, extractable = false) {
1399
- const subtle = getSubtleCrypto();
1400
- if (userPrivateKey.algorithm.name === "RSA-OAEP") {
1401
- const wrappedBytes = CKeyManager.fromBase64(wrappedKeyBase64);
1402
- let unwrappedRawKey;
1403
- try {
1404
- unwrappedRawKey = await subtle.decrypt(
1405
- { name: "RSA-OAEP" },
1406
- userPrivateKey,
1407
- wrappedBytes
1408
- );
1409
- } finally {
1410
- wrappedBytes.fill(0);
1411
- }
1412
- try {
1413
- return await subtle.importKey(
1414
- "raw",
1415
- unwrappedRawKey,
1416
- unwrappedKeyAlgorithm,
1417
- extractable,
1418
- ["encrypt", "decrypt"]
1419
- );
1420
- } finally {
1421
- new Uint8Array(unwrappedRawKey).fill(0);
2085
+ async decryptField(envelope, context) {
2086
+ if (!this.isEncrypted(envelope)) return envelope;
2087
+ const field = context?.fieldName || "field";
2088
+ const dummyData = {};
2089
+ if (context?.recordId !== void 0) {
2090
+ dummyData.id = context.recordId;
2091
+ }
2092
+ const { aadFields, ...restContext } = context || {};
2093
+ let boundOptions = { ...restContext };
2094
+ if (aadFields) {
2095
+ if (!Array.isArray(aadFields) && typeof aadFields === "object") {
2096
+ Object.assign(dummyData, aadFields);
2097
+ boundOptions.aadFields = Object.keys(aadFields);
2098
+ } else if (Array.isArray(aadFields)) {
2099
+ boundOptions.aadFields = aadFields;
2100
+ if (context?.record) {
2101
+ Object.assign(dummyData, context.record);
2102
+ }
1422
2103
  }
1423
2104
  }
1424
- throw new Error(
1425
- `[CKeyManager] Unsupported user key algorithm for unwrapAccountKeyForUser: '${userPrivateKey.algorithm.name}'. Expected 'RSA-OAEP'.`
1426
- );
2105
+ const resolvedOptions = this._bindFieldAad(dummyData, field, boundOptions);
2106
+ return this.decryptValue(envelope, resolvedOptions);
1427
2107
  }
1428
- // ── Key Derivation Functions (PBKDF2 & HKDF) ──────────────────────────────
1429
2108
  /**
1430
- * Derives a symmetric AES-GCM CryptoKey from a passphrase using PBKDF2 or HKDF.
1431
- * Returns the derived CryptoKey and the base64-encoded salt.
2109
+ * Top-level polymorphic encryption method.
2110
+ * Automatically performs value-level encryption:
2111
+ * - If data is an array of objects: recursively encrypts each row.
2112
+ * - If data is an object: encrypts specified fields (or all values if valueEncryption is true),
2113
+ * preserving JSON object keys so backend APIs and ORMs can parse keys and perform upserts.
2114
+ * - If data is a primitive or whole-payload envelope: encrypts as a single value.
1432
2115
  */
1433
- static async deriveKey(passphrase, options) {
1434
- if (!passphrase || typeof passphrase === "string" && passphrase.trim().length === 0 || typeof passphrase === "object" && "byteLength" in passphrase && passphrase.byteLength === 0) {
1435
- throw new Error("[CKeyManager] Passphrase cannot be empty or undefined for key derivation.");
2116
+ async encrypt(data, options) {
2117
+ if (data === void 0 || data === null) return data;
2118
+ if (this.isEncrypted(data)) return data;
2119
+ options = this._resolveOutboundKeyringOptions(options);
2120
+ if (options?.valueEncryption === false && (!options.fields || options.fields.length === 0)) {
2121
+ return this.encryptValue(data, options);
1436
2122
  }
1437
- const subtle = getSubtleCrypto();
1438
- const saltBytes = CKeyManager.resolveSalt(options.salt);
1439
- const keyLength = options.keyLength || 256;
1440
- const hash = options.hash || "SHA-256";
1441
- const extractable = options.extractable === true;
1442
- const rawKeyBytes = typeof passphrase === "string" ? textEncoder$1.encode(passphrase) : passphrase instanceof Uint8Array ? passphrase : new Uint8Array(passphrase);
1443
- try {
1444
- if (options.kdf === "pbkdf2") {
1445
- const baseKey = await subtle.importKey(
1446
- "raw",
1447
- rawKeyBytes,
1448
- "PBKDF2",
1449
- false,
1450
- ["deriveKey"]
2123
+ if (Array.isArray(data)) {
2124
+ if (data.length === 0) return data;
2125
+ if (options?.fields && options.fields.length > 0) {
2126
+ const fields = options.fields;
2127
+ let hasUnencrypted = false;
2128
+ for (let i = 0; i < data.length; i++) {
2129
+ const item = data[i];
2130
+ if (item && typeof item === "object") {
2131
+ for (let f = 0; f < fields.length; f++) {
2132
+ const val = item[fields[f]];
2133
+ if (val !== void 0 && val !== null && !this.isEncrypted(val)) {
2134
+ hasUnencrypted = true;
2135
+ break;
2136
+ }
2137
+ }
2138
+ if (hasUnencrypted) break;
2139
+ }
2140
+ }
2141
+ if (!hasUnencrypted) {
2142
+ return data;
2143
+ }
2144
+ await this.warmupKey(options);
2145
+ const itemOptions = { ...options, _warmed: true };
2146
+ return Promise.all(data.map((item) => this.encrypt(item, itemOptions)));
2147
+ }
2148
+ if (options?.valueEncryption === true) {
2149
+ await this.warmupKey(options);
2150
+ const itemOptions = { ...options, _warmed: true };
2151
+ return Promise.all(data.map((item) => this.encrypt(item, itemOptions)));
2152
+ }
2153
+ return this.encryptValue(data, options);
2154
+ }
2155
+ if (typeof data === "object" && !ArrayBuffer.isView(data) && !(data instanceof ArrayBuffer)) {
2156
+ if (options?.fields && options.fields.length > 0) {
2157
+ const fieldsToEncrypt = options.fields.filter(
2158
+ (field) => Object.prototype.hasOwnProperty.call(data, field) && data[field] !== void 0 && data[field] !== null && !this.isEncrypted(data[field])
1451
2159
  );
1452
- const derivedKey = await subtle.deriveKey(
1453
- {
1454
- name: "PBKDF2",
1455
- salt: saltBytes,
1456
- iterations: options.iterations || 31e4,
1457
- hash
1458
- },
1459
- baseKey,
1460
- { name: "AES-GCM", length: keyLength },
1461
- extractable,
1462
- ["encrypt", "decrypt"]
2160
+ if (fieldsToEncrypt.length === 0) {
2161
+ return data;
2162
+ }
2163
+ if (!options?._warmed) {
2164
+ await this.warmupKey(options);
2165
+ }
2166
+ const result = { ...data };
2167
+ const entries = await Promise.all(
2168
+ fieldsToEncrypt.map(async (field) => {
2169
+ const fieldOptions = this._bindFieldAad(data, field, options);
2170
+ return [field, await this.encryptValue(result[field], fieldOptions)];
2171
+ })
1463
2172
  );
1464
- return {
1465
- key: derivedKey,
1466
- salt: CKeyManager.toBase64(saltBytes)
1467
- };
2173
+ for (const [field, encryptedVal] of entries) {
2174
+ safeSetProperty(result, field, encryptedVal);
2175
+ }
2176
+ return result;
1468
2177
  }
1469
- if (options.kdf === "hkdf") {
1470
- const baseKey = await subtle.importKey(
1471
- "raw",
1472
- rawKeyBytes,
1473
- "HKDF",
1474
- false,
1475
- ["deriveKey"]
2178
+ if (options?.valueEncryption === true) {
2179
+ const result = { ...data };
2180
+ const keysToEncrypt = Object.keys(result).filter(
2181
+ (key) => result[key] !== void 0 && result[key] !== null && !this.isEncrypted(result[key])
1476
2182
  );
1477
- const infoBytes = resolveInfoBytes(options.info, "katlux-derived-aes-key");
1478
- const derivedKey = await subtle.deriveKey(
1479
- {
1480
- name: "HKDF",
1481
- salt: saltBytes,
1482
- info: infoBytes,
1483
- hash
1484
- },
1485
- baseKey,
1486
- { name: "AES-GCM", length: keyLength },
1487
- extractable,
1488
- ["encrypt", "decrypt"]
2183
+ if (keysToEncrypt.length === 0) {
2184
+ return result;
2185
+ }
2186
+ if (!options?._warmed) {
2187
+ await this.warmupKey(options);
2188
+ }
2189
+ const entries = await Promise.all(
2190
+ keysToEncrypt.map(async (key) => {
2191
+ const fieldOptions = this._bindFieldAad(data, key, options);
2192
+ return [key, await this.encryptValue(result[key], fieldOptions)];
2193
+ })
1489
2194
  );
1490
- return {
1491
- key: derivedKey,
1492
- salt: CKeyManager.toBase64(saltBytes)
1493
- };
1494
- }
1495
- throw new Error(`[CKeyManager] Unsupported KDF strategy: ${options.kdf}`);
1496
- } finally {
1497
- if (typeof passphrase === "string") {
1498
- rawKeyBytes.fill(0);
2195
+ for (const [key, encryptedVal] of entries) {
2196
+ safeSetProperty(result, key, encryptedVal);
2197
+ }
2198
+ return result;
1499
2199
  }
1500
2200
  }
2201
+ return this.encryptValue(data, options);
1501
2202
  }
1502
2203
  /**
1503
- * Derives an AES-GCM CryptoKey from a high-entropy secret (e.g. an ECDH shared secret key)
1504
- * using HKDF.
2204
+ * Top-level polymorphic decryption method.
2205
+ * Automatically performs value-level decryption:
2206
+ * - If payload is an encrypted envelope: validates metadata and decrypts directly via decryptValue.
2207
+ * - If payload is an array of objects: decrypts each row (batched with local memoization).
2208
+ * - If payload is an object: decrypts any encrypted value cells across fields.
2209
+ * In strict mode:
2210
+ * - Unencrypted plaintext in configured `fields` triggers a decryption error.
2211
+ * - Envelopes are validated against `allowedAlgorithms` and `allowedKids`.
2212
+ * - Errors are routed through `onDecryptError` or populated into `__decryptErrors`.
1505
2213
  */
1506
- static async deriveKeyFromSecret(secretKey, options = {}) {
1507
- const subtle = getSubtleCrypto();
1508
- let saltBytes;
1509
- if (options.salt !== void 0) {
1510
- saltBytes = CKeyManager.resolveSalt(options.salt);
1511
- } else {
1512
- saltBytes = new Uint8Array(16);
2214
+ async decrypt(payload, options) {
2215
+ if (payload === void 0 || payload === null) return payload;
2216
+ if (this.isEncrypted(payload)) {
2217
+ const envelopeKey = this._resolveDecryptionKeyForEnvelope(payload, options);
2218
+ const envelopeOptions = envelopeKey !== options?.key ? { ...options, key: envelopeKey } : options;
2219
+ const metaErr = this._validateEnvelopeMetadata(payload, envelopeOptions);
2220
+ if (metaErr) {
2221
+ return this._handleDecryptError(metaErr, payload, "", payload, void 0, envelopeOptions);
2222
+ }
2223
+ try {
2224
+ return await this.decryptValue(payload, envelopeOptions);
2225
+ } catch (err) {
2226
+ return this._handleDecryptError(err, payload, "", payload, void 0, envelopeOptions);
2227
+ }
1513
2228
  }
1514
- const infoBytes = resolveInfoBytes(options.info, "katlux-ecdh-aes-gcm");
1515
- const hash = options.hash || "SHA-256";
1516
- const keyLength = options.keyLength || 256;
1517
- const extractable = options.extractable === true;
1518
- return await subtle.deriveKey(
1519
- {
1520
- name: "HKDF",
1521
- salt: saltBytes,
1522
- info: infoBytes,
1523
- hash
1524
- },
1525
- secretKey,
1526
- { name: "AES-GCM", length: keyLength },
1527
- extractable,
1528
- ["encrypt", "decrypt"]
1529
- );
2229
+ if (Array.isArray(payload)) {
2230
+ if (payload.length === 0) return payload;
2231
+ const localMemo = /* @__PURE__ */ new Map();
2232
+ const decryptRow = async (item, rowIndex) => {
2233
+ if (!item) return item;
2234
+ if (this.isEncrypted(item)) {
2235
+ const envelopeKey = this._resolveDecryptionKeyForEnvelope(item, options);
2236
+ const envelopeOptions = envelopeKey !== options?.key ? { ...options, key: envelopeKey } : options;
2237
+ const metaErr = this._validateEnvelopeMetadata(item, envelopeOptions);
2238
+ if (metaErr) {
2239
+ return this._handleDecryptError(metaErr, item, "", item, rowIndex, envelopeOptions);
2240
+ }
2241
+ const ct = item.ciphertext;
2242
+ const iv = item.iv || "";
2243
+ const aadPart = options?.aad ? typeof options.aad === "string" ? options.aad : "" : "";
2244
+ const memoKey = `${ct}:${iv}:${aadPart}:${item.kid || ""}`;
2245
+ try {
2246
+ if (!localMemo.has(memoKey)) {
2247
+ localMemo.set(memoKey, this.decryptValue(item, envelopeOptions));
2248
+ }
2249
+ return await localMemo.get(memoKey);
2250
+ } catch (err) {
2251
+ return this._handleDecryptError(err, item, "", item, rowIndex, envelopeOptions);
2252
+ }
2253
+ }
2254
+ if (typeof item === "object" && !ArrayBuffer.isView(item) && !(item instanceof ArrayBuffer)) {
2255
+ const targetKeys = options?.fields && options.fields.length > 0 ? options.fields.filter((f) => Object.prototype.hasOwnProperty.call(item, f)) : Object.keys(item);
2256
+ const keysToDecrypt = targetKeys.filter((key) => this.isEncrypted(item[key]));
2257
+ 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])) : [];
2258
+ if (keysToDecrypt.length === 0 && strictViolationKeys.length === 0) {
2259
+ return item;
2260
+ }
2261
+ const proto = Object.getPrototypeOf(item);
2262
+ const result = proto && proto !== Object.prototype ? Object.assign(Object.create(proto), item) : { ...item };
2263
+ for (const key of strictViolationKeys) {
2264
+ const err = new Error(`[AEncryptionProvider] Strict decryption violation: field '${key}' contains unencrypted plaintext.`);
2265
+ const fallbackVal = this._handleDecryptError(err, result, key, item[key], rowIndex, options);
2266
+ safeSetProperty(result, key, fallbackVal);
2267
+ }
2268
+ await Promise.all(
2269
+ keysToDecrypt.map(async (key) => {
2270
+ const env = item[key];
2271
+ const envelopeKey = this._resolveDecryptionKeyForEnvelope(env, options);
2272
+ const baseOptions = envelopeKey !== options?.key ? { ...options, key: envelopeKey } : options;
2273
+ const boundOptions = this._bindFieldAad(item, key, baseOptions);
2274
+ const hasBoundAad = boundOptions.aad !== baseOptions?.aad;
2275
+ const metaErr = this._validateEnvelopeMetadata(env, boundOptions);
2276
+ if (metaErr) {
2277
+ const fallbackVal = this._handleDecryptError(metaErr, result, key, env, rowIndex, boundOptions);
2278
+ safeSetProperty(result, key, fallbackVal);
2279
+ return;
2280
+ }
2281
+ const ct = env.ciphertext;
2282
+ const iv = env.iv || "";
2283
+ const aadPart = boundOptions.aad ? typeof boundOptions.aad === "string" ? boundOptions.aad : "" : "";
2284
+ const memoKey = `${ct}:${iv}:${aadPart}:${env.kid || ""}`;
2285
+ try {
2286
+ if (!localMemo.has(memoKey)) {
2287
+ const decryptPromise = (async () => {
2288
+ try {
2289
+ return await this.decryptValue(env, boundOptions);
2290
+ } catch (err) {
2291
+ const canFallback = hasBoundAad && (!options?.aadFields || options.aadFields.length === 0) && typeof options?.aad !== "function";
2292
+ if (canFallback && isAuthOrDecryptionError(err)) {
2293
+ return await this.decryptValue(env, baseOptions);
2294
+ }
2295
+ throw err;
2296
+ }
2297
+ })();
2298
+ localMemo.set(memoKey, decryptPromise);
2299
+ }
2300
+ const decryptedVal = await localMemo.get(memoKey);
2301
+ safeSetProperty(result, key, decryptedVal);
2302
+ } catch (err) {
2303
+ const fallbackVal = this._handleDecryptError(err, result, key, env, rowIndex, boundOptions);
2304
+ safeSetProperty(result, key, fallbackVal);
2305
+ }
2306
+ })
2307
+ );
2308
+ return result;
2309
+ }
2310
+ return item;
2311
+ };
2312
+ if (payload.length <= 100) {
2313
+ return Promise.all(payload.map((item, index) => decryptRow(item, index)));
2314
+ }
2315
+ const batchSize = 1e3;
2316
+ const results = new Array(payload.length);
2317
+ for (let i = 0; i < payload.length; i += batchSize) {
2318
+ const end = Math.min(i + batchSize, payload.length);
2319
+ const chunk = payload.slice(i, end);
2320
+ const chunkRes = await Promise.all(chunk.map((item, localIdx) => decryptRow(item, i + localIdx)));
2321
+ for (let j = 0; j < chunkRes.length; j++) {
2322
+ results[i + j] = chunkRes[j];
2323
+ }
2324
+ if (end < payload.length && (i > 0 && i % 5e3 === 0)) {
2325
+ await new Promise((r) => setTimeout(r, 0));
2326
+ }
2327
+ }
2328
+ return results;
2329
+ }
2330
+ if (typeof payload === "object" && !ArrayBuffer.isView(payload) && !(payload instanceof ArrayBuffer)) {
2331
+ const targetKeys = options?.fields && options.fields.length > 0 ? options.fields.filter((f) => Object.prototype.hasOwnProperty.call(payload, f)) : Object.keys(payload);
2332
+ const keysToDecrypt = targetKeys.filter((key) => this.isEncrypted(payload[key]));
2333
+ 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])) : [];
2334
+ if (keysToDecrypt.length === 0 && strictViolationKeys.length === 0) {
2335
+ return payload;
2336
+ }
2337
+ const proto = Object.getPrototypeOf(payload);
2338
+ const result = proto && proto !== Object.prototype ? Object.assign(Object.create(proto), payload) : { ...payload };
2339
+ for (const key of strictViolationKeys) {
2340
+ const err = new Error(`[AEncryptionProvider] Strict decryption violation: field '${key}' contains unencrypted plaintext.`);
2341
+ const fallbackVal = this._handleDecryptError(err, result, key, payload[key], void 0, options);
2342
+ safeSetProperty(result, key, fallbackVal);
2343
+ }
2344
+ await Promise.all(
2345
+ keysToDecrypt.map(async (key) => {
2346
+ const env = payload[key];
2347
+ const envelopeKey = this._resolveDecryptionKeyForEnvelope(env, options);
2348
+ const baseOptions = envelopeKey !== options?.key ? { ...options, key: envelopeKey } : options;
2349
+ const boundOptions = this._bindFieldAad(payload, key, baseOptions);
2350
+ const hasBoundAad = boundOptions.aad !== baseOptions?.aad;
2351
+ const metaErr = this._validateEnvelopeMetadata(env, boundOptions);
2352
+ if (metaErr) {
2353
+ const fallbackVal = this._handleDecryptError(metaErr, result, key, env, void 0, boundOptions);
2354
+ safeSetProperty(result, key, fallbackVal);
2355
+ return;
2356
+ }
2357
+ try {
2358
+ let decryptedVal;
2359
+ try {
2360
+ decryptedVal = await this.decryptValue(env, boundOptions);
2361
+ } catch (err) {
2362
+ const canFallback = hasBoundAad && (!options?.aadFields || options.aadFields.length === 0) && typeof options?.aad !== "function";
2363
+ if (canFallback && isAuthOrDecryptionError(err)) {
2364
+ decryptedVal = await this.decryptValue(env, baseOptions);
2365
+ } else {
2366
+ throw err;
2367
+ }
2368
+ }
2369
+ safeSetProperty(result, key, decryptedVal);
2370
+ } catch (err) {
2371
+ const fallbackVal = this._handleDecryptError(err, result, key, env, void 0, boundOptions);
2372
+ safeSetProperty(result, key, fallbackVal);
2373
+ }
2374
+ })
2375
+ );
2376
+ return result;
2377
+ }
2378
+ return payload;
1530
2379
  }
1531
2380
  }
1532
2381
 
@@ -1541,6 +2390,7 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
1541
2390
  _cachedKeyId;
1542
2391
  _configuredKid;
1543
2392
  _decryptSaltCache = /* @__PURE__ */ new Map();
2393
+ _keyringCache = /* @__PURE__ */ new Map();
1544
2394
  setDefaultKey(key) {
1545
2395
  super.setDefaultKey(key);
1546
2396
  this._cachedKey = void 0;
@@ -1550,6 +2400,7 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
1550
2400
  this._cachedHash = void 0;
1551
2401
  this._cachedKeyId = void 0;
1552
2402
  this._decryptSaltCache.clear();
2403
+ this._keyringCache.clear();
1553
2404
  }
1554
2405
  clearKeys() {
1555
2406
  super.clearKeys();
@@ -1560,9 +2411,10 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
1560
2411
  this._cachedHash = void 0;
1561
2412
  this._cachedKeyId = void 0;
1562
2413
  this._decryptSaltCache.clear();
2414
+ this._keyringCache.clear();
1563
2415
  }
1564
2416
  constructor(key, options) {
1565
- super(key);
2417
+ super(key, options);
1566
2418
  if (options) {
1567
2419
  if (options.kid || options.keyId) {
1568
2420
  this._configuredKid = options.kid || options.keyId;
@@ -1612,14 +2464,21 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
1612
2464
  }
1613
2465
  async _resolveCryptoKeyInternal(keyProvider, kdfOverride, saltOverride, options, iterationsOverride) {
1614
2466
  const rawKey = await this.resolveRawKey(keyProvider);
2467
+ const currentKeyId = getSafeKeyIdentifier(rawKey);
1615
2468
  if (typeof rawKey !== "string" && rawKey.algorithm) {
1616
- if (!keyProvider) {
2469
+ const res2 = { cryptoKey: rawKey, kdf: "direct" };
2470
+ this._keyringCache.set(currentKeyId, res2);
2471
+ if (!keyProvider || keyProvider === this.defaultKey) {
1617
2472
  this._cachedKey = rawKey;
1618
2473
  this._cachedKdf = "direct";
1619
2474
  }
1620
- return { cryptoKey: rawKey, kdf: "direct" };
2475
+ return res2;
1621
2476
  }
1622
2477
  if (rawKey instanceof Uint8Array || rawKey instanceof ArrayBuffer) {
2478
+ const cached = this._keyringCache.get(currentKeyId);
2479
+ if (cached) {
2480
+ return cached;
2481
+ }
1623
2482
  if (!keyProvider && this._cachedKey && this._cachedKdf === "raw") {
1624
2483
  return { cryptoKey: this._cachedKey, kdf: "raw" };
1625
2484
  }
@@ -1636,11 +2495,13 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
1636
2495
  false,
1637
2496
  ["encrypt", "decrypt"]
1638
2497
  );
1639
- if (!keyProvider) {
2498
+ const res2 = { cryptoKey: cryptoKey2, kdf: "raw" };
2499
+ this._keyringCache.set(currentKeyId, res2);
2500
+ if (!keyProvider || keyProvider === this.defaultKey) {
1640
2501
  this._cachedKey = cryptoKey2;
1641
2502
  this._cachedKdf = "raw";
1642
2503
  }
1643
- return { cryptoKey: cryptoKey2, kdf: "raw" };
2504
+ return res2;
1644
2505
  }
1645
2506
  const passphrase = rawKey;
1646
2507
  const kdf = (kdfOverride || options?.kdf || this._defaultKdf || "pbkdf2").toLowerCase();
@@ -1650,12 +2511,21 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
1650
2511
  const matchesHash = !targetHash || this._cachedHash !== void 0 && targetHash.toLowerCase() === this._cachedHash.toLowerCase();
1651
2512
  const matchesKdf = this._cachedKdf !== void 0 && kdf === this._cachedKdf.toLowerCase();
1652
2513
  const matchesSalt = !saltOverride || this._cachedSalt !== void 0 && saltOverride === this._cachedSalt;
1653
- const currentKeyId = getSafeKeyIdentifier(rawKey);
1654
2514
  if (this._cachedKey && matchesSalt && !options?.kdfSalt && matchesKdf && matchesIter && matchesHash) {
1655
2515
  if (this._cachedKeyId === currentKeyId) {
1656
2516
  return { cryptoKey: this._cachedKey, salt: this._cachedSalt, kdf: this._cachedKdf, iter: this._cachedIter };
1657
2517
  }
1658
2518
  }
2519
+ if (!saltOverride && !options?.kdfSalt) {
2520
+ const cached = this._keyringCache.get(currentKeyId);
2521
+ if (cached) {
2522
+ const cMatchesKdf = !cached.kdf || cached.kdf.toLowerCase() === kdf;
2523
+ const cMatchesIter = !targetIter || cached.iter === targetIter;
2524
+ if (cMatchesKdf && cMatchesIter) {
2525
+ return cached;
2526
+ }
2527
+ }
2528
+ }
1659
2529
  if (kdf === "pbkdf2") {
1660
2530
  const salt = saltOverride || options?.kdfSalt || this._kdfOptions?.salt;
1661
2531
  const rawIterations = iterationsOverride || options?.kdfIterations || this._kdfOptions?.iterations || 31e4;
@@ -1667,15 +2537,19 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
1667
2537
  iterations,
1668
2538
  hash
1669
2539
  });
2540
+ const res2 = { cryptoKey: derived.key, salt: derived.salt, kdf: "PBKDF2", iter: iterations };
1670
2541
  if (!saltOverride && !options?.kdfSalt) {
1671
- this._cachedKey = derived.key;
1672
- this._cachedSalt = derived.salt;
1673
- this._cachedKdf = "PBKDF2";
1674
- this._cachedIter = iterations;
1675
- this._cachedHash = hash;
1676
- this._cachedKeyId = currentKeyId;
2542
+ this._keyringCache.set(currentKeyId, res2);
2543
+ if (!keyProvider || keyProvider === this.defaultKey) {
2544
+ this._cachedKey = derived.key;
2545
+ this._cachedSalt = derived.salt;
2546
+ this._cachedKdf = "PBKDF2";
2547
+ this._cachedIter = iterations;
2548
+ this._cachedHash = hash;
2549
+ this._cachedKeyId = currentKeyId;
2550
+ }
1677
2551
  }
1678
- return { cryptoKey: derived.key, salt: derived.salt, kdf: "PBKDF2", iter: iterations };
2552
+ return res2;
1679
2553
  }
1680
2554
  if (kdf === "hkdf") {
1681
2555
  if (typeof rawKey === "string") {
@@ -1692,14 +2566,18 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
1692
2566
  hash,
1693
2567
  info
1694
2568
  });
2569
+ const res2 = { cryptoKey: derived.key, salt: derived.salt, kdf: "HKDF" };
1695
2570
  if (!saltOverride && !options?.kdfSalt) {
1696
- this._cachedKey = derived.key;
1697
- this._cachedSalt = derived.salt;
1698
- this._cachedKdf = "HKDF";
1699
- this._cachedHash = hash;
1700
- this._cachedKeyId = currentKeyId;
2571
+ this._keyringCache.set(currentKeyId, res2);
2572
+ if (!keyProvider || keyProvider === this.defaultKey) {
2573
+ this._cachedKey = derived.key;
2574
+ this._cachedSalt = derived.salt;
2575
+ this._cachedKdf = "HKDF";
2576
+ this._cachedHash = hash;
2577
+ this._cachedKeyId = currentKeyId;
2578
+ }
1701
2579
  }
1702
- return { cryptoKey: derived.key, salt: derived.salt, kdf: "HKDF" };
2580
+ return res2;
1703
2581
  }
1704
2582
  if (kdf === "none") {
1705
2583
  if (saltOverride) {
@@ -1731,11 +2609,13 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
1731
2609
  } finally {
1732
2610
  new Uint8Array(keyHash).fill(0);
1733
2611
  }
1734
- if (!keyProvider) {
2612
+ const res = { cryptoKey, kdf: "none" };
2613
+ this._keyringCache.set(currentKeyId, res);
2614
+ if (!keyProvider || keyProvider === this.defaultKey) {
1735
2615
  this._cachedKey = cryptoKey;
1736
2616
  this._cachedKdf = "none";
1737
2617
  }
1738
- return { cryptoKey, kdf: "none" };
2618
+ return res;
1739
2619
  }
1740
2620
  static isEncrypted(payload) {
1741
2621
  if (!payload || typeof payload !== "object") return false;
@@ -1745,6 +2625,29 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
1745
2625
  return CWebCryptoEncryptionProvider.isEncrypted(payload);
1746
2626
  }
1747
2627
  async warmupKey(options) {
2628
+ const effectiveKeys = options?.keys || this.keys;
2629
+ if (effectiveKeys && Object.keys(effectiveKeys).length > 0) {
2630
+ const promises = Object.entries(effectiveKeys).map(async ([kid, keyProv]) => {
2631
+ const res2 = await this.resolveCryptoKey(keyProv, void 0, void 0, { ...options, key: keyProv, kid });
2632
+ const rawKey = await this.resolveRawKey(keyProv);
2633
+ const keyId = getSafeKeyIdentifier(rawKey);
2634
+ this._keyringCache.set(keyId, res2);
2635
+ this._keyringCache.set(`kid:${kid}`, res2);
2636
+ return { kid, res: res2 };
2637
+ });
2638
+ const results = await Promise.all(promises);
2639
+ const effectiveActiveKid = options?.activeKid || this.activeKid;
2640
+ const primary = effectiveActiveKid && results.find((r) => r.kid === effectiveActiveKid)?.res || results[0]?.res;
2641
+ if (primary) {
2642
+ this._cachedKey = primary.cryptoKey;
2643
+ this._cachedSalt = primary.salt;
2644
+ this._cachedKdf = primary.kdf;
2645
+ this._cachedIter = primary.iter;
2646
+ this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
2647
+ this._cachedKeyId = getKeyIdentifier(options?.key, this.defaultKey);
2648
+ }
2649
+ return;
2650
+ }
1748
2651
  const res = await this.resolveCryptoKey(options?.key, void 0, void 0, options);
1749
2652
  this._cachedKey = res.cryptoKey;
1750
2653
  this._cachedSalt = res.salt;
@@ -1787,7 +2690,7 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
1787
2690
  cryptoKey,
1788
2691
  textEncoder$1.encode(jsonString)
1789
2692
  );
1790
- const kid = options?.kid || options?.keyId || this._configuredKid;
2693
+ const kid = options?.kid || options?.keyId || this.activeKid || this._configuredKid;
1791
2694
  return {
1792
2695
  __encrypted: true,
1793
2696
  ciphertext: CKeyManager.toBase64(ciphertextBuffer),
@@ -3110,12 +4013,26 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3110
4013
  _cachedHash;
3111
4014
  _cachedKeyId;
3112
4015
  _configuredKid;
4016
+ _keyringCache = /* @__PURE__ */ new Map();
4017
+ isKeyRetained(key) {
4018
+ if (key === this._cachedKey) return true;
4019
+ for (const entry of this._keyringCache.values()) {
4020
+ if (entry.key === key) return true;
4021
+ }
4022
+ return false;
4023
+ }
3113
4024
  setDefaultKey(key) {
3114
4025
  super.setDefaultKey(key);
3115
4026
  if (this._cachedKey) {
3116
4027
  this._cachedKey.fill(0);
3117
4028
  this._cachedKey = void 0;
3118
4029
  }
4030
+ for (const entry of this._keyringCache.values()) {
4031
+ if (entry.key !== this._cachedKey) {
4032
+ entry.key.fill(0);
4033
+ }
4034
+ }
4035
+ this._keyringCache.clear();
3119
4036
  this._cachedSalt = void 0;
3120
4037
  this._cachedKdf = void 0;
3121
4038
  this._cachedIter = void 0;
@@ -3128,6 +4045,12 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3128
4045
  this._cachedKey.fill(0);
3129
4046
  this._cachedKey = void 0;
3130
4047
  }
4048
+ for (const entry of this._keyringCache.values()) {
4049
+ if (entry.key !== this._cachedKey) {
4050
+ entry.key.fill(0);
4051
+ }
4052
+ }
4053
+ this._keyringCache.clear();
3131
4054
  this._cachedSalt = void 0;
3132
4055
  this._cachedKdf = void 0;
3133
4056
  this._cachedIter = void 0;
@@ -3135,6 +4058,29 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3135
4058
  this._cachedKeyId = void 0;
3136
4059
  }
3137
4060
  async warmupKey(options) {
4061
+ const effectiveKeys = options?.keys || this.keys;
4062
+ if (effectiveKeys && Object.keys(effectiveKeys).length > 0) {
4063
+ const promises = Object.entries(effectiveKeys).map(async ([kid, keyProv]) => {
4064
+ const res2 = await this.deriveKeyAsync(keyProv, void 0, void 0, { ...options, key: keyProv, kid });
4065
+ const raw = await this.resolveRawKeyAsync(keyProv);
4066
+ const keyId = getSafeKeyIdentifier(raw);
4067
+ this._keyringCache.set(keyId, res2);
4068
+ this._keyringCache.set(`kid:${kid}`, res2);
4069
+ return { kid, res: res2 };
4070
+ });
4071
+ const results = await Promise.all(promises);
4072
+ const effectiveActiveKid = options?.activeKid || this.activeKid;
4073
+ const primary = effectiveActiveKid && results.find((r) => r.kid === effectiveActiveKid)?.res || results[0]?.res;
4074
+ if (primary) {
4075
+ this._cachedKey = primary.key;
4076
+ this._cachedSalt = primary.salt;
4077
+ this._cachedKdf = primary.kdf;
4078
+ this._cachedIter = primary.iter;
4079
+ this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
4080
+ this._cachedKeyId = getSafeKeyIdentifier(await this.resolveRawKeyAsync(options?.key));
4081
+ }
4082
+ return;
4083
+ }
3138
4084
  const res = await this.deriveKeyAsync(options?.key, void 0, void 0, options);
3139
4085
  if (!options?.key) {
3140
4086
  this._cachedKey = res.key;
@@ -3149,6 +4095,30 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3149
4095
  * Synchronously pre-derives and caches the encryption key for subsequent synchronous operations.
3150
4096
  */
3151
4097
  warmupKeySync(options) {
4098
+ const effectiveKeys = options?.keys || this.keys;
4099
+ if (effectiveKeys && Object.keys(effectiveKeys).length > 0) {
4100
+ const entries = Object.entries(effectiveKeys);
4101
+ const results = [];
4102
+ for (const [kid, keyProv] of entries) {
4103
+ const res2 = this.deriveKeySync(keyProv, void 0, void 0, { ...options, key: keyProv, kid });
4104
+ const raw = this.resolveRawKeySync(keyProv);
4105
+ const keyId = getSafeKeyIdentifier(raw);
4106
+ this._keyringCache.set(keyId, res2);
4107
+ this._keyringCache.set(`kid:${kid}`, res2);
4108
+ results.push({ kid, res: res2 });
4109
+ }
4110
+ const effectiveActiveKid = options?.activeKid || this.activeKid;
4111
+ const primary = effectiveActiveKid && results.find((r) => r.kid === effectiveActiveKid)?.res || results[0]?.res;
4112
+ if (primary) {
4113
+ this._cachedKey = primary.key;
4114
+ this._cachedSalt = primary.salt;
4115
+ this._cachedKdf = primary.kdf;
4116
+ this._cachedIter = primary.iter;
4117
+ this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
4118
+ this._cachedKeyId = getSafeKeyIdentifier(this.resolveRawKeySync(options?.key));
4119
+ }
4120
+ return;
4121
+ }
3152
4122
  const res = this.deriveKeySync(options?.key, void 0, void 0, options);
3153
4123
  if (!options?.key) {
3154
4124
  this._cachedKey = res.key;
@@ -3167,7 +4137,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3167
4137
  return Buffer.from(infoRaw);
3168
4138
  }
3169
4139
  constructor(key, options) {
3170
- super(key);
4140
+ super(key, options);
3171
4141
  if (options) {
3172
4142
  if (options.kid || options.keyId) {
3173
4143
  this._configuredKid = options.kid || options.keyId;
@@ -3185,7 +4155,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3185
4155
  }
3186
4156
  }
3187
4157
  resolveRawKeySync(keyProvider) {
3188
- const target = keyProvider || this.defaultKey;
4158
+ const target = keyProvider || (this.activeKid && this.keys ? this.keys[this.activeKid] : void 0) || this.defaultKey;
3189
4159
  if (!target) {
3190
4160
  throw new Error("[CNodeCryptoEncryptionProvider] No encryption key or keyProvider was supplied.");
3191
4161
  }
@@ -3215,7 +4185,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3215
4185
  return rawKey;
3216
4186
  }
3217
4187
  async resolveRawKeyAsync(keyProvider) {
3218
- const target = keyProvider || this.defaultKey;
4188
+ const target = keyProvider || (this.activeKid && this.keys ? this.keys[this.activeKid] : void 0) || this.defaultKey;
3219
4189
  if (!target) {
3220
4190
  throw new Error("[CNodeCryptoEncryptionProvider] No encryption key or keyProvider was supplied.");
3221
4191
  }
@@ -3246,15 +4216,16 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3246
4216
  const currentKeyId = getSafeKeyIdentifier(raw);
3247
4217
  if (Buffer.isBuffer(raw)) {
3248
4218
  if (raw.length === 32) {
3249
- if (!keyProvider) {
3250
- if (!this._cachedKey || this._cachedKeyId !== currentKeyId) {
3251
- this._cachedKey = Buffer.from(raw);
3252
- this._cachedKdf = "raw";
3253
- this._cachedKeyId = currentKeyId;
3254
- }
3255
- return { key: this._cachedKey, kdf: "raw" };
4219
+ const cached = this._keyringCache.get(currentKeyId);
4220
+ if (cached) return cached;
4221
+ const res2 = { key: Buffer.from(raw), kdf: "raw" };
4222
+ this._keyringCache.set(currentKeyId, res2);
4223
+ if (!keyProvider || keyProvider === this.defaultKey) {
4224
+ this._cachedKey = res2.key;
4225
+ this._cachedKdf = "raw";
4226
+ this._cachedKeyId = currentKeyId;
3256
4227
  }
3257
- return { key: Buffer.from(raw), kdf: "raw" };
4228
+ return res2;
3258
4229
  }
3259
4230
  throw new Error(
3260
4231
  `[CNodeCryptoEncryptionProvider] AES-GCM-256 requires a 32-byte key. Received a Buffer of ${raw.length} bytes. Provide exactly 32 bytes or a string passphrase.`
@@ -3273,6 +4244,16 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3273
4244
  return { key: this._cachedKey, salt: this._cachedSalt, kdf: this._cachedKdf, iter: this._cachedIter };
3274
4245
  }
3275
4246
  }
4247
+ if (!saltOverride && !options?.kdfSalt) {
4248
+ const cached = this._keyringCache.get(currentKeyId);
4249
+ if (cached) {
4250
+ const cMatchesKdf = !cached.kdf || cached.kdf.toLowerCase() === kdf;
4251
+ const cMatchesIter = !targetIter || cached.iter === targetIter;
4252
+ if (cMatchesKdf && cMatchesIter) {
4253
+ return cached;
4254
+ }
4255
+ }
4256
+ }
3276
4257
  if (kdf === "pbkdf2") {
3277
4258
  const s = saltOverride || options?.kdfSalt || this._kdfOptions?.salt;
3278
4259
  const saltBytes = CKeyManager.resolveSalt(s);
@@ -3281,20 +4262,24 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3281
4262
  const iterations = Math.min(Math.max(rawIter, 1e3), 6e5);
3282
4263
  const hash = (options?.kdfHash || this._kdfOptions?.hash || "SHA-256").toLowerCase().replace("-", "");
3283
4264
  const key2 = crypto$1.pbkdf2Sync(passphrase, saltBuf, iterations, 32, hash);
3284
- if (!keyProvider && !saltOverride && !options?.kdfSalt) {
3285
- this._cachedKey = key2;
3286
- this._cachedSalt = saltBuf.toString("base64");
3287
- this._cachedKdf = "PBKDF2";
3288
- this._cachedIter = iterations;
3289
- this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
3290
- this._cachedKeyId = currentKeyId;
3291
- }
3292
- return {
4265
+ const res2 = {
3293
4266
  key: key2,
3294
4267
  salt: saltBuf.toString("base64"),
3295
4268
  kdf: "PBKDF2",
3296
4269
  iter: iterations
3297
4270
  };
4271
+ if (!saltOverride && !options?.kdfSalt) {
4272
+ this._keyringCache.set(currentKeyId, res2);
4273
+ if (!keyProvider || keyProvider === this.defaultKey) {
4274
+ this._cachedKey = key2;
4275
+ this._cachedSalt = saltBuf.toString("base64");
4276
+ this._cachedKdf = "PBKDF2";
4277
+ this._cachedIter = iterations;
4278
+ this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
4279
+ this._cachedKeyId = currentKeyId;
4280
+ }
4281
+ }
4282
+ return res2;
3298
4283
  }
3299
4284
  if (kdf === "hkdf") {
3300
4285
  if (typeof raw === "string") {
@@ -3308,49 +4293,56 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3308
4293
  const hash = (options?.kdfHash || this._kdfOptions?.hash || "SHA-256").toLowerCase().replace("-", "");
3309
4294
  const info = this.resolveInfoBuffer(options?.info || this._kdfOptions?.info);
3310
4295
  const key2 = Buffer.from(crypto$1.hkdfSync(hash, passphrase, saltBuf, info, 32));
3311
- if (!keyProvider && !saltOverride && !options?.kdfSalt) {
3312
- this._cachedKey = key2;
3313
- this._cachedSalt = saltBuf.toString("base64");
3314
- this._cachedKdf = "HKDF";
3315
- this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
3316
- this._cachedKeyId = currentKeyId;
3317
- }
3318
- return {
4296
+ const res2 = {
3319
4297
  key: key2,
3320
4298
  salt: saltBuf.toString("base64"),
3321
4299
  kdf: "HKDF"
3322
4300
  };
4301
+ if (!saltOverride && !options?.kdfSalt) {
4302
+ this._keyringCache.set(currentKeyId, res2);
4303
+ if (!keyProvider || keyProvider === this.defaultKey) {
4304
+ this._cachedKey = key2;
4305
+ this._cachedSalt = saltBuf.toString("base64");
4306
+ this._cachedKdf = "HKDF";
4307
+ this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
4308
+ this._cachedKeyId = currentKeyId;
4309
+ }
4310
+ }
4311
+ return res2;
3323
4312
  }
3324
4313
  if (kdf === "none") {
3325
4314
  console.warn(
3326
4315
  "[CNodeCryptoEncryptionProvider] WARNING: Decrypting a legacy envelope with no KDF (raw SHA-256 key derivation). This is cryptographically weak. Migrate to pbkdf2 or hkdf."
3327
4316
  );
3328
4317
  }
3329
- if (!keyProvider && this._cachedKey && this._cachedKdf === "none" && this._cachedKeyId === currentKeyId) {
4318
+ if ((!keyProvider || keyProvider === this.defaultKey) && this._cachedKey && this._cachedKdf === "none" && this._cachedKeyId === currentKeyId) {
3330
4319
  return { key: this._cachedKey, kdf: "none" };
3331
4320
  }
3332
4321
  const key = crypto$1.createHash("sha256").update(passphrase, "utf8").digest();
3333
- if (!keyProvider) {
4322
+ const res = { key, kdf: "none" };
4323
+ this._keyringCache.set(currentKeyId, res);
4324
+ if (!keyProvider || keyProvider === this.defaultKey) {
3334
4325
  this._cachedKey = key;
3335
4326
  this._cachedKdf = "none";
3336
4327
  this._cachedKeyId = currentKeyId;
3337
4328
  }
3338
- return { key, kdf: "none" };
4329
+ return res;
3339
4330
  }
3340
4331
  async deriveKeyAsync(keyProvider, kdfOverride, saltOverride, options, iterationsOverride) {
3341
4332
  const raw = await this.resolveRawKeyAsync(keyProvider);
3342
4333
  const currentKeyId = getSafeKeyIdentifier(raw);
3343
4334
  if (Buffer.isBuffer(raw)) {
3344
4335
  if (raw.length === 32) {
3345
- if (!keyProvider) {
3346
- if (!this._cachedKey || this._cachedKeyId !== currentKeyId) {
3347
- this._cachedKey = Buffer.from(raw);
3348
- this._cachedKdf = "raw";
3349
- this._cachedKeyId = currentKeyId;
3350
- }
3351
- return { key: this._cachedKey, kdf: "raw" };
4336
+ const cached = this._keyringCache.get(currentKeyId);
4337
+ if (cached) return cached;
4338
+ const res2 = { key: Buffer.from(raw), kdf: "raw" };
4339
+ this._keyringCache.set(currentKeyId, res2);
4340
+ if (!keyProvider || keyProvider === this.defaultKey) {
4341
+ this._cachedKey = res2.key;
4342
+ this._cachedKdf = "raw";
4343
+ this._cachedKeyId = currentKeyId;
3352
4344
  }
3353
- return { key: Buffer.from(raw), kdf: "raw" };
4345
+ return res2;
3354
4346
  }
3355
4347
  throw new Error(
3356
4348
  `[CNodeCryptoEncryptionProvider] AES-GCM-256 requires a 32-byte key. Received a Buffer of ${raw.length} bytes. Provide exactly 32 bytes or a string passphrase.`
@@ -3369,6 +4361,16 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3369
4361
  return { key: this._cachedKey, salt: this._cachedSalt, kdf: this._cachedKdf, iter: this._cachedIter };
3370
4362
  }
3371
4363
  }
4364
+ if (!saltOverride && !options?.kdfSalt) {
4365
+ const cached = this._keyringCache.get(currentKeyId);
4366
+ if (cached) {
4367
+ const cMatchesKdf = !cached.kdf || cached.kdf.toLowerCase() === kdf;
4368
+ const cMatchesIter = !targetIter || cached.iter === targetIter;
4369
+ if (cMatchesKdf && cMatchesIter) {
4370
+ return cached;
4371
+ }
4372
+ }
4373
+ }
3372
4374
  if (kdf === "pbkdf2") {
3373
4375
  const s = saltOverride || options?.kdfSalt || this._kdfOptions?.salt;
3374
4376
  const saltBytes = CKeyManager.resolveSalt(s);
@@ -3382,20 +4384,24 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3382
4384
  else resolve(derived);
3383
4385
  });
3384
4386
  });
3385
- if (!keyProvider && !saltOverride && !options?.kdfSalt) {
3386
- this._cachedKey = key2;
3387
- this._cachedSalt = saltBuf.toString("base64");
3388
- this._cachedKdf = "PBKDF2";
3389
- this._cachedIter = iterations;
3390
- this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
3391
- this._cachedKeyId = currentKeyId;
3392
- }
3393
- return {
4387
+ const res2 = {
3394
4388
  key: key2,
3395
4389
  salt: saltBuf.toString("base64"),
3396
4390
  kdf: "PBKDF2",
3397
4391
  iter: iterations
3398
4392
  };
4393
+ if (!saltOverride && !options?.kdfSalt) {
4394
+ this._keyringCache.set(currentKeyId, res2);
4395
+ if (!keyProvider || keyProvider === this.defaultKey) {
4396
+ this._cachedKey = key2;
4397
+ this._cachedSalt = saltBuf.toString("base64");
4398
+ this._cachedKdf = "PBKDF2";
4399
+ this._cachedIter = iterations;
4400
+ this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
4401
+ this._cachedKeyId = currentKeyId;
4402
+ }
4403
+ }
4404
+ return res2;
3399
4405
  }
3400
4406
  if (kdf === "hkdf") {
3401
4407
  if (typeof raw === "string") {
@@ -3414,34 +4420,40 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3414
4420
  else resolve(Buffer.from(derived));
3415
4421
  });
3416
4422
  });
3417
- if (!keyProvider && !saltOverride && !options?.kdfSalt) {
3418
- this._cachedKey = key2;
3419
- this._cachedSalt = saltBuf.toString("base64");
3420
- this._cachedKdf = "HKDF";
3421
- this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
3422
- this._cachedKeyId = currentKeyId;
3423
- }
3424
- return {
4423
+ const res2 = {
3425
4424
  key: key2,
3426
4425
  salt: saltBuf.toString("base64"),
3427
4426
  kdf: "HKDF"
3428
4427
  };
4428
+ if (!saltOverride && !options?.kdfSalt) {
4429
+ this._keyringCache.set(currentKeyId, res2);
4430
+ if (!keyProvider || keyProvider === this.defaultKey) {
4431
+ this._cachedKey = key2;
4432
+ this._cachedSalt = saltBuf.toString("base64");
4433
+ this._cachedKdf = "HKDF";
4434
+ this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
4435
+ this._cachedKeyId = currentKeyId;
4436
+ }
4437
+ }
4438
+ return res2;
3429
4439
  }
3430
4440
  if (kdf === "none") {
3431
4441
  console.warn(
3432
4442
  "[CNodeCryptoEncryptionProvider] WARNING: Decrypting a legacy envelope with no KDF (raw SHA-256 key derivation). This is cryptographically weak. Migrate to pbkdf2 or hkdf."
3433
4443
  );
3434
4444
  }
3435
- if (!keyProvider && this._cachedKey && this._cachedKdf === "none" && this._cachedKeyId === currentKeyId) {
4445
+ if ((!keyProvider || keyProvider === this.defaultKey) && this._cachedKey && this._cachedKdf === "none" && this._cachedKeyId === currentKeyId) {
3436
4446
  return { key: this._cachedKey, kdf: "none" };
3437
4447
  }
3438
4448
  const key = crypto$1.createHash("sha256").update(passphrase, "utf8").digest();
3439
- if (!keyProvider) {
4449
+ const res = { key, kdf: "none" };
4450
+ this._keyringCache.set(currentKeyId, res);
4451
+ if (!keyProvider || keyProvider === this.defaultKey) {
3440
4452
  this._cachedKey = key;
3441
4453
  this._cachedKdf = "none";
3442
4454
  this._cachedKeyId = currentKeyId;
3443
4455
  }
3444
- return { key, kdf: "none" };
4456
+ return res;
3445
4457
  }
3446
4458
  static isEncrypted(payload) {
3447
4459
  if (!payload || typeof payload !== "object") return false;
@@ -3481,7 +4493,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3481
4493
  ]);
3482
4494
  const authTag = cipher.getAuthTag();
3483
4495
  const combinedCiphertext = Buffer.concat([encryptedBuffer, authTag]);
3484
- const kid = options?.kid || options?.keyId || this._configuredKid;
4496
+ const kid = options?.kid || options?.keyId || this.activeKid || this._configuredKid;
3485
4497
  return {
3486
4498
  __encrypted: true,
3487
4499
  ciphertext: combinedCiphertext.toString("base64"),
@@ -3494,7 +4506,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3494
4506
  ...hasAad ? { aad: true } : {}
3495
4507
  };
3496
4508
  } finally {
3497
- if (key !== this._cachedKey) {
4509
+ if (!this.isKeyRetained(key)) {
3498
4510
  key.fill(0);
3499
4511
  }
3500
4512
  }
@@ -3583,7 +4595,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3583
4595
  if (err instanceof SyntaxError) throw err;
3584
4596
  throw new Error(`[CNodeCryptoEncryptionProvider] Decryption failed: ${err?.message || "Authentication tag mismatch or corrupted ciphertext"}`);
3585
4597
  } finally {
3586
- if (key !== this._cachedKey) {
4598
+ if (!this.isKeyRetained(key)) {
3587
4599
  key.fill(0);
3588
4600
  }
3589
4601
  }
@@ -3592,6 +4604,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3592
4604
  encryptSync(data, options) {
3593
4605
  if (data === void 0 || data === null) return data;
3594
4606
  if (this.isEncrypted(data)) return data;
4607
+ options = this._resolveOutboundKeyringOptions(options);
3595
4608
  if (Array.isArray(data)) {
3596
4609
  if (options?.fields && options.fields.length > 0 || options?.valueEncryption === true) {
3597
4610
  return data.map((item) => this.encryptSync(item, options));
@@ -3627,7 +4640,12 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3627
4640
  decryptSync(payload, options) {
3628
4641
  if (payload === void 0 || payload === null) return payload;
3629
4642
  if (this.isEncrypted(payload)) {
3630
- return this.decryptValueSync(payload, options);
4643
+ const metaErr = this._validateEnvelopeMetadata(payload, options);
4644
+ if (metaErr) {
4645
+ return this._handleDecryptError(metaErr, payload, "", payload, void 0, options);
4646
+ }
4647
+ const envelopeKey = this._resolveDecryptionKeyForEnvelope(payload, options);
4648
+ return this.decryptValueSync(payload, { ...options, key: envelopeKey });
3631
4649
  }
3632
4650
  if (Array.isArray(payload)) {
3633
4651
  return payload.map((item) => this.decryptSync(item, options));
@@ -3641,28 +4659,35 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3641
4659
  for (const key of targetKeys) {
3642
4660
  if (this.isEncrypted(result[key])) {
3643
4661
  const fieldPayload = result[key];
4662
+ const metaErr = this._validateEnvelopeMetadata(fieldPayload, options);
4663
+ if (metaErr) {
4664
+ return this._handleDecryptError(metaErr, result, key, fieldPayload, void 0, options);
4665
+ }
4666
+ const envelopeKey = this._resolveDecryptionKeyForEnvelope(fieldPayload, options);
3644
4667
  const salt = fieldPayload.salt || "__no_salt__";
3645
- let derivedKey = keyCache.get(salt);
4668
+ const memoKey = `${salt}:${fieldPayload.kid || ""}`;
4669
+ let derivedKey = keyCache.get(memoKey);
3646
4670
  if (!derivedKey) {
3647
4671
  const kdfFromEnvelope = fieldPayload.kdf ? fieldPayload.kdf.toLowerCase() : "none";
3648
4672
  const derived = this.deriveKeySync(
3649
- options?.key,
4673
+ envelopeKey,
3650
4674
  kdfFromEnvelope,
3651
4675
  fieldPayload.salt,
3652
4676
  options,
3653
4677
  fieldPayload.iter
3654
4678
  );
3655
4679
  derivedKey = derived.key;
3656
- keyCache.set(salt, derivedKey);
4680
+ keyCache.set(memoKey, derivedKey);
3657
4681
  }
3658
- const boundOptions = this._bindFieldAad(payload, key, options);
4682
+ const boundOptions = this._bindFieldAad(payload, key, { ...options, key: envelopeKey });
3659
4683
  const hasBoundAad = boundOptions.aad !== options?.aad;
3660
4684
  try {
3661
4685
  safeSetProperty(result, key, this.decryptCiphertextWithKey(fieldPayload, derivedKey, boundOptions));
3662
4686
  } catch (err) {
3663
- if (hasBoundAad && isAuthOrDecryptionError(err)) {
4687
+ const canFallback = hasBoundAad && (!options?.aadFields || options.aadFields.length === 0) && typeof options?.aad !== "function";
4688
+ if (canFallback && isAuthOrDecryptionError(err)) {
3664
4689
  try {
3665
- safeSetProperty(result, key, this.decryptCiphertextWithKey(fieldPayload, derivedKey, options));
4690
+ safeSetProperty(result, key, this.decryptCiphertextWithKey(fieldPayload, derivedKey, { ...options, key: envelopeKey }));
3666
4691
  } catch (fallbackErr) {
3667
4692
  throw fallbackErr;
3668
4693
  }
@@ -3676,7 +4701,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3676
4701
  return mutated ? result : payload;
3677
4702
  } finally {
3678
4703
  for (const k of keyCache.values()) {
3679
- if (k !== this._cachedKey) {
4704
+ if (!this.isKeyRetained(k)) {
3680
4705
  k.fill(0);
3681
4706
  }
3682
4707
  }
@@ -3724,7 +4749,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3724
4749
  ]);
3725
4750
  const authTag = cipher.getAuthTag();
3726
4751
  const combined = Buffer.concat([ciphertext, authTag]);
3727
- const kid = options?.kid || options?.keyId || this._configuredKid;
4752
+ const kid = options?.kid || options?.keyId || this.activeKid || this._configuredKid;
3728
4753
  return {
3729
4754
  __encrypted: true,
3730
4755
  ciphertext: combined.toString("base64"),
@@ -3737,7 +4762,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3737
4762
  ...hasAad ? { aad: true } : {}
3738
4763
  };
3739
4764
  } finally {
3740
- if (key !== this._cachedKey) {
4765
+ if (!this.isKeyRetained(key)) {
3741
4766
  key.fill(0);
3742
4767
  }
3743
4768
  }
@@ -3761,7 +4786,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3761
4786
  if (err instanceof SyntaxError) throw err;
3762
4787
  throw new Error(`[CNodeCryptoEncryptionProvider] Decryption failed: ${err?.message || "Authentication tag mismatch or corrupted ciphertext"}`);
3763
4788
  } finally {
3764
- if (key !== this._cachedKey) {
4789
+ if (!this.isKeyRetained(key)) {
3765
4790
  key.fill(0);
3766
4791
  }
3767
4792
  }
@@ -3769,7 +4794,12 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3769
4794
  async decrypt(payload, options) {
3770
4795
  if (payload === void 0 || payload === null) return payload;
3771
4796
  if (this.isEncrypted(payload)) {
3772
- return this.decryptValue(payload, options);
4797
+ const metaErr = this._validateEnvelopeMetadata(payload, options);
4798
+ if (metaErr) {
4799
+ return this._handleDecryptError(metaErr, payload, "", payload, void 0, options);
4800
+ }
4801
+ const envelopeKey = this._resolveDecryptionKeyForEnvelope(payload, options);
4802
+ return this.decryptValue(payload, { ...options, key: envelopeKey });
3773
4803
  }
3774
4804
  if (Array.isArray(payload)) {
3775
4805
  return Promise.all(payload.map((item) => this.decrypt(item, options)));
@@ -3783,28 +4813,35 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3783
4813
  for (const key of targetKeys) {
3784
4814
  if (this.isEncrypted(result[key])) {
3785
4815
  const fieldPayload = result[key];
4816
+ const metaErr = this._validateEnvelopeMetadata(fieldPayload, options);
4817
+ if (metaErr) {
4818
+ return this._handleDecryptError(metaErr, result, key, fieldPayload, void 0, options);
4819
+ }
4820
+ const envelopeKey = this._resolveDecryptionKeyForEnvelope(fieldPayload, options);
3786
4821
  const salt = fieldPayload.salt || "__no_salt__";
3787
- let derivedKey = keyCache.get(salt);
4822
+ const memoKey = `${salt}:${fieldPayload.kid || ""}`;
4823
+ let derivedKey = keyCache.get(memoKey);
3788
4824
  if (!derivedKey) {
3789
4825
  const kdfFromEnvelope = fieldPayload.kdf ? fieldPayload.kdf.toLowerCase() : "none";
3790
4826
  const derived = await this.deriveKeyAsync(
3791
- options?.key,
4827
+ envelopeKey,
3792
4828
  kdfFromEnvelope,
3793
4829
  fieldPayload.salt ? CKeyManager.fromBase64(fieldPayload.salt) : void 0,
3794
4830
  options,
3795
4831
  fieldPayload.iter
3796
4832
  );
3797
4833
  derivedKey = derived.key;
3798
- keyCache.set(salt, derivedKey);
4834
+ keyCache.set(memoKey, derivedKey);
3799
4835
  }
3800
- const boundOptions = this._bindFieldAad(payload, key, options);
4836
+ const boundOptions = this._bindFieldAad(payload, key, { ...options, key: envelopeKey });
3801
4837
  const hasBoundAad = boundOptions.aad !== options?.aad;
3802
4838
  try {
3803
4839
  safeSetProperty(result, key, this.decryptCiphertextWithKey(fieldPayload, derivedKey, boundOptions));
3804
4840
  } catch (err) {
3805
- if (hasBoundAad && isAuthOrDecryptionError(err)) {
4841
+ const canFallback = hasBoundAad && (!options?.aadFields || options.aadFields.length === 0) && typeof options?.aad !== "function";
4842
+ if (canFallback && isAuthOrDecryptionError(err)) {
3806
4843
  try {
3807
- safeSetProperty(result, key, this.decryptCiphertextWithKey(fieldPayload, derivedKey, options));
4844
+ safeSetProperty(result, key, this.decryptCiphertextWithKey(fieldPayload, derivedKey, { ...options, key: envelopeKey }));
3808
4845
  } catch (fallbackErr) {
3809
4846
  throw fallbackErr;
3810
4847
  }
@@ -3818,7 +4855,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3818
4855
  return mutated ? result : payload;
3819
4856
  } finally {
3820
4857
  for (const k of keyCache.values()) {
3821
- if (k !== this._cachedKey) {
4858
+ if (!this.isKeyRetained(k)) {
3822
4859
  k.fill(0);
3823
4860
  }
3824
4861
  }
@@ -3849,7 +4886,11 @@ function createEncryptionProvider(options = {}) {
3849
4886
  kdfIterations: options.kdfIterations,
3850
4887
  kdfHash: options.kdfHash,
3851
4888
  kdfSalt: options.kdfSalt,
3852
- info: options.info
4889
+ info: options.info,
4890
+ kid: options.kid || options.keyId,
4891
+ keys: options.keys,
4892
+ activeKid: options.activeKid,
4893
+ allowedKids: options.allowedKids
3853
4894
  });
3854
4895
  }
3855
4896
  if (alg === "AES-CBC") {
@@ -3886,7 +4927,11 @@ function createEncryptionProvider(options = {}) {
3886
4927
  kdfIterations: options.kdfIterations,
3887
4928
  kdfHash: options.kdfHash,
3888
4929
  kdfSalt: options.kdfSalt,
3889
- info: options.info
4930
+ info: options.info,
4931
+ kid: options.kid || options.keyId,
4932
+ keys: options.keys,
4933
+ activeKid: options.activeKid,
4934
+ allowedKids: options.allowedKids
3890
4935
  });
3891
4936
  }
3892
4937
  throw new Error(
@@ -3949,7 +4994,19 @@ function isDataEncrypted(provider, data) {
3949
4994
  function buildEncryptionContext(encryption, url, _defaultIdKey) {
3950
4995
  return {
3951
4996
  url,
4997
+ aad: encryption?.aad,
4998
+ aadFields: encryption?.aadFields,
4999
+ baseAad: encryption?.baseAad,
5000
+ bindRequestContextAad: encryption?.bindRequestContextAad,
5001
+ strict: encryption?.strict,
5002
+ allowedAlgorithms: encryption?.allowedAlgorithms,
5003
+ allowedKids: encryption?.allowedKids,
5004
+ onDecryptError: encryption?.onDecryptError,
3952
5005
  key: encryption?.key,
5006
+ kid: encryption?.kid,
5007
+ keyId: encryption?.keyId,
5008
+ keys: encryption?.keys,
5009
+ activeKid: encryption?.activeKid,
3953
5010
  valueEncryption: encryption?.valueEncryption,
3954
5011
  fields: encryption?.fields,
3955
5012
  publicKey: encryption?.publicKey,
@@ -3980,7 +5037,7 @@ async function decryptPageRows(rows, encryption, apiUrl, idKey) {
3980
5037
  }
3981
5038
  return isDataEncrypted(cipherProvider, row);
3982
5039
  });
3983
- if (!hasEncryptedRows) {
5040
+ if (!hasEncryptedRows && !encryption?.strict) {
3984
5041
  return { rows, decrypted: false };
3985
5042
  }
3986
5043
  const ctx = buildEncryptionContext(encryption, apiUrl);
@@ -4143,6 +5200,13 @@ class CSearchIndex {
4143
5200
  const signature = await subtle.sign("HMAC", key, textEncoder.encode(normalized));
4144
5201
  return Array.from(new Uint8Array(signature)).map((b) => b.toString(16).padStart(2, "0")).join("");
4145
5202
  }
5203
+ /**
5204
+ * Computes a deterministic HMAC-SHA256 blind index hash for a term using the configured HMAC key.
5205
+ * Performs Unicode NFKC normalization, trimming, and lowercasing.
5206
+ */
5207
+ async computeBlindIndex(term) {
5208
+ return this.computeToken(term);
5209
+ }
4146
5210
  /**
4147
5211
  * Computes HMAC-SHA256 tokens for all trigrams of a term.
4148
5212
  */
@@ -4172,9 +5236,21 @@ class CSearchIndex {
4172
5236
  const token = await this.computeToken(rawVal);
4173
5237
  const trigrams = await this.computeTrigramTokens(rawVal);
4174
5238
  fieldMap.set(field, { token, trigrams });
5239
+ } else if (rawVal && typeof rawVal === "object" && rawVal.__encrypted === true) {
5240
+ const precomputedToken = row[`${field}_bidx`] || row[`${field}_token`];
5241
+ if (typeof precomputedToken === "string" && precomputedToken) {
5242
+ fieldMap.set(field, { token: precomputedToken, trigrams: /* @__PURE__ */ new Set() });
5243
+ }
4175
5244
  }
4176
5245
  }
4177
5246
  }
5247
+ /**
5248
+ * Indexes a row using pre-decrypted plaintext field values.
5249
+ * Allows indexing rows that contain pre-encrypted ciphertext envelopes in allData.
5250
+ */
5251
+ async indexRowDecrypted(row, decryptedPlaintext) {
5252
+ return this.indexRow(row, decryptedPlaintext);
5253
+ }
4178
5254
  /**
4179
5255
  * Indexes an array of rows.
4180
5256
  */
@@ -4223,7 +5299,7 @@ class CSearchIndex {
4223
5299
  }
4224
5300
  return true;
4225
5301
  }
4226
- if (operator === EDataFilterOperator.NotIn || operator === "notIn") {
5302
+ if (operator === EDataFilterOperator.NotIn || operator === "notIn" || operator === "nin") {
4227
5303
  const queryTrigramTokens = await this.computeTrigramTokens(filterValue);
4228
5304
  if (queryTrigramTokens.size === 0) return false;
4229
5305
  for (const tok of queryTrigramTokens) {
@@ -4294,7 +5370,7 @@ class CSearchIndex {
4294
5370
  }
4295
5371
  return true;
4296
5372
  });
4297
- } else if (op === EDataFilterOperator.NotIn || op === "notIn") {
5373
+ } else if (op === EDataFilterOperator.NotIn || op === "notIn" || op === "nin") {
4298
5374
  const queryTrigrams = await this.computeTrigramTokens(filterValue);
4299
5375
  checks.push((row) => {
4300
5376
  const entry = this.getEntry(row, key);
@@ -4413,6 +5489,36 @@ class AEncryptedClientProvider extends ADataProvider {
4413
5489
  } : void 0;
4414
5490
  return await encryptRowsChunked(rows, this.encryption, options, void 0, this.idKey, indexChunk);
4415
5491
  }
5492
+ /**
5493
+ * Indexes pre-encrypted rows by performing a transient in-memory decryption pass
5494
+ * on search fields to populate CSearchIndex without mutating underlying encrypted records in allData.
5495
+ */
5496
+ async indexPreEncryptedRows(rows) {
5497
+ if (!this.isEncryptionEnabled || !this._searchIndex || !rows || rows.length === 0) {
5498
+ return;
5499
+ }
5500
+ const cipher = getCipherProvider(this.encryption);
5501
+ if (!cipher) return;
5502
+ const ctx = buildEncryptionContext(this.encryption, this.apiUrl, this.idKey);
5503
+ let decryptedRows = [];
5504
+ try {
5505
+ const decResult = await cipher.decrypt(rows, ctx);
5506
+ decryptedRows = Array.isArray(decResult) ? decResult : [decResult];
5507
+ } catch (err) {
5508
+ console.warn("[Katlux Providers] indexPreEncryptedRows: Decryption failed during search index build", err);
5509
+ return;
5510
+ }
5511
+ await Promise.all(rows.map((row, idx) => {
5512
+ const decRow = decryptedRows[idx];
5513
+ const plainFields = {};
5514
+ for (const f of this.encryption?.fields || Object.keys(decRow || {})) {
5515
+ if (decRow && decRow[f] !== void 0) {
5516
+ plainFields[f] = decRow[f];
5517
+ }
5518
+ }
5519
+ return this._searchIndex.indexRow(row, plainFields);
5520
+ }));
5521
+ }
4416
5522
  async decryptRows(rows) {
4417
5523
  if (!this.isEncryptionEnabled || !rows || rows.length === 0) {
4418
5524
  return { rows, decrypted: false };
@@ -4721,6 +5827,15 @@ class CFlatClientDataProvider extends AEncryptedClientProvider {
4721
5827
  this.loading = true;
4722
5828
  try {
4723
5829
  const rows = Array.isArray(data) ? data : [];
5830
+ const isPreEnc = this.encryption?.preEncrypted ?? isDataEncrypted(void 0, rows);
5831
+ if (isPreEnc) {
5832
+ this.allData = rows;
5833
+ if (this.searchIndex) {
5834
+ await this.indexPreEncryptedRows(rows);
5835
+ }
5836
+ await this.loadPageData();
5837
+ return;
5838
+ }
4724
5839
  const encryptedRows = await this.encryptRows(rows);
4725
5840
  this.allData = encryptedRows;
4726
5841
  await this.loadPageData();
@@ -4741,6 +5856,16 @@ class CFlatClientDataProvider extends AEncryptedClientProvider {
4741
5856
  this.loading = true;
4742
5857
  try {
4743
5858
  const rows = Array.isArray(data) ? data : [];
5859
+ const isPreEnc = this.encryption?.preEncrypted ?? isDataEncrypted(void 0, rows);
5860
+ if (isPreEnc) {
5861
+ this.allData = rows;
5862
+ if (this.searchIndex) {
5863
+ await this.indexPreEncryptedRows(rows);
5864
+ }
5865
+ options?.onProgress?.({ processed: rows.length, total: rows.length, percent: 100 });
5866
+ await this.loadPageData();
5867
+ return;
5868
+ }
4744
5869
  const encryptedRows = await this.encryptRowsChunked(rows, options);
4745
5870
  this.allData = encryptedRows;
4746
5871
  await this.loadPageData();
@@ -6687,8 +7812,18 @@ class RequestProvider {
6687
7812
  method,
6688
7813
  isServer,
6689
7814
  aad: encOptions.aad,
7815
+ aadFields: encOptions.aadFields,
7816
+ baseAad: encOptions.baseAad,
6690
7817
  bindRequestContextAad: encOptions.bindRequestContextAad,
7818
+ strict: encOptions.strict,
7819
+ allowedAlgorithms: encOptions.allowedAlgorithms,
7820
+ allowedKids: encOptions.allowedKids,
7821
+ onDecryptError: encOptions.onDecryptError,
6691
7822
  key: encOptions.key,
7823
+ kid: encOptions.kid,
7824
+ keyId: encOptions.keyId,
7825
+ keys: encOptions.keys,
7826
+ activeKid: encOptions.activeKid,
6692
7827
  valueEncryption: encOptions.valueEncryption,
6693
7828
  fields: encOptions.fields,
6694
7829
  publicKey: encOptions.publicKey,
@@ -6877,17 +8012,26 @@ class CAPIFlatClientDataProvider extends CFlatClientDataProvider {
6877
8012
  if (data) {
6878
8013
  const normalized = normalizeDataResult(data);
6879
8014
  const rows = normalized ? normalized.rows : data.rows || data;
8015
+ const incomingRows = Array.isArray(rows) ? rows : [];
6880
8016
  if (this._isAccumulating) {
6881
- this.allData = [...this.allData, ...rows];
8017
+ this.allData = [...this.allData, ...incomingRows];
6882
8018
  } else {
6883
- this.allData = Array.isArray(rows) ? rows : [];
8019
+ this.allData = incomingRows;
6884
8020
  }
6885
8021
  this._filterDirty = true;
6886
8022
  this._sortDirty = true;
6887
8023
  if (normalized?.rowCount !== void 0) {
6888
8024
  this.rowCount = normalized.rowCount;
6889
8025
  }
6890
- this._lastDataReceivedPromise = this.loadPageData();
8026
+ this._lastDataReceivedPromise = (async () => {
8027
+ if (this.isEncryptionEnabled && this.searchIndex && incomingRows.length > 0) {
8028
+ const isPreEnc = this.encryption?.preEncrypted ?? isDataEncrypted(void 0, incomingRows);
8029
+ if (isPreEnc) {
8030
+ await this.indexPreEncryptedRows(incomingRows);
8031
+ }
8032
+ }
8033
+ await this.loadPageData();
8034
+ })();
6891
8035
  }
6892
8036
  });
6893
8037
  }
@@ -6973,6 +8117,12 @@ class CAPIFlatClientDataProvider extends CFlatClientDataProvider {
6973
8117
  this.allData = normalized.rows;
6974
8118
  this._filterDirty = true;
6975
8119
  this._sortDirty = true;
8120
+ if (this.isEncryptionEnabled && this.searchIndex && this.allData.length > 0) {
8121
+ const isPreEnc = this.encryption?.preEncrypted ?? isDataEncrypted(void 0, this.allData);
8122
+ if (isPreEnc) {
8123
+ await this.indexPreEncryptedRows(this.allData);
8124
+ }
8125
+ }
6976
8126
  await this.loadPageData();
6977
8127
  this.initialLoad = false;
6978
8128
  this.loading = false;
@@ -10200,4 +11350,4 @@ function isEncrypted(payload) {
10200
11350
  return AEncryptionProvider.isEncrypted(payload);
10201
11351
  }
10202
11352
 
10203
- export { AAPIDataProvider, ADataProvider, AEncryptedClientProvider, AEncryptionProvider, CAPIFlatClientDataProvider, CAPIFlatServerDataProvider, CAPITreeClientDataProvider, CAPITreeServerDataProvider, CApplicationCache, CBrowserSessionCache, CCookieCache, CECDHEncryptionProvider, CFlatClientDataProvider, CFlatServerDataProvider, CIndexedDBCache, CKeyManager, CLocalStorageCache, CMemoryCache, CNodeCryptoEncryptionProvider, CRSAEncryptionProvider, CSearchIndex, CServerSessionCache, CTreeClientDataProvider, CTreeServerDataProvider, CWebCryptoAESCBCProvider, CWebCryptoEncryptionProvider, CacheProviderFactory, DefaultHydrationAdapter, ECacheStrategy, EDataFilterOperator, EQueueStorage, KatluxIndexedDB, QueueStorageFactory, RequestProvider, RequestProviderRegistry, RequestQueue, SENSITIVE_HEADER_KEYS, applyOperator, buildEncryptionContext, clearCachedCipherProvider, clearSSRData, compareAnd, compareEqual, compareIn, compareOr, compareValues, createEncryptionProvider, debounce, decryptPageRows, encryptRows, encryptRowsChunked, evaluateFilter, extractTrigrams, fastKeyHash, fetchWithSSR, filterData, filterTreeData, getAppRoute, getAppRouter, getCipherProvider, getHydrationAdapter, getKeyIdentifier, getPlatformApp, getRandomValues, getSSRData, getSSRPayload, getSafeKeyIdentifier, getSubtleCrypto, isAuthOrDecryptionError, isDataEncrypted, isEncrypted, normalizeIndexTerm, onPlatformReady, resolveInfoBytes, safeSetProperty, setHydrationAdapter, sortData, textDecoder, textEncoder$1 as textEncoder, toBase64, useDebounce, useFilterLogic, useSortLogic };
11353
+ export { AAPIDataProvider, ADataProvider, AEncryptedClientProvider, AEncryptionProvider, CAPIFlatClientDataProvider, CAPIFlatServerDataProvider, CAPITreeClientDataProvider, CAPITreeServerDataProvider, CApplicationCache, CBrowserSessionCache, CCookieCache, CECDHEncryptionProvider, CFlatClientDataProvider, CFlatServerDataProvider, CIndexedDBCache, CKeyManager, CLocalStorageCache, CMemoryCache, CNodeCryptoEncryptionProvider, CRSAEncryptionProvider, CSearchIndex, CServerSessionCache, CTreeClientDataProvider, CTreeServerDataProvider, CWebCryptoAESCBCProvider, CWebCryptoEncryptionProvider, CacheProviderFactory, DECRYPT_ERROR_SENTINEL, DefaultHydrationAdapter, ECacheStrategy, EDataFilterOperator, EQueueStorage, KatluxIndexedDB, QueueStorageFactory, RequestProvider, RequestProviderRegistry, RequestQueue, SENSITIVE_HEADER_KEYS, applyOperator, buildEncryptionContext, canonicalizeJson, clearCachedCipherProvider, clearSSRData, compareAnd, compareEqual, compareIn, compareOr, compareValues, createEncryptionProvider, debounce, decryptPageRows, encryptRows, encryptRowsChunked, evaluateFilter, extractTrigrams, fastKeyHash, fetchWithSSR, filterData, filterTreeData, getAppRoute, getAppRouter, getCipherProvider, getHydrationAdapter, getKeyIdentifier, getPlatformApp, getRandomValues, getSSRData, getSSRPayload, getSafeKeyIdentifier, getSubtleCrypto, isAuthOrDecryptionError, isDataEncrypted, isEncrypted, normalizeIndexTerm, onPlatformReady, resolveInfoBytes, safeSetProperty, setHydrationAdapter, sortData, textDecoder, textEncoder$1 as textEncoder, toBase64, useDebounce, useFilterLogic, useSortLogic };