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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -734,742 +734,764 @@ function canonicalizeJson(value, seen = /* @__PURE__ */ new Set()) {
734
734
  return JSON.stringify(value);
735
735
  }
736
736
 
737
- class AEncryptionProvider {
738
- defaultKey;
739
- keys;
740
- activeKid;
741
- allowedKids;
742
- constructor(key, options) {
743
- if (key !== void 0) {
744
- this.defaultKey = key;
745
- }
746
- if (options?.keys) {
747
- this.keys = options.keys;
748
- }
749
- if (options?.activeKid) {
750
- this.activeKid = options.activeKid;
751
- }
752
- if (options?.allowedKids) {
753
- this.allowedKids = options.allowedKids;
754
- }
755
- }
737
+ class CKeyManager {
738
+ // ── Binary & Encoding Utilities ──────────────────────────────────────────
756
739
  /**
757
- * Sets or updates the default secret key, CryptoKey, or async key provider function.
740
+ * Generates a cryptographically secure random byte buffer of the specified length.
758
741
  */
759
- setDefaultKey(key) {
760
- this.defaultKey = key || void 0;
742
+ static randomBytes(length) {
743
+ const bytes = new Uint8Array(length);
744
+ return getRandomValues(bytes);
761
745
  }
762
746
  /**
763
- * Retrieves the currently configured default key or key provider callback.
747
+ * Converts an ArrayBuffer or Uint8Array into a Base64-encoded string.
764
748
  */
765
- getDefaultKey() {
766
- return this.defaultKey;
749
+ static toBase64(buffer) {
750
+ return toBase64(buffer);
767
751
  }
768
752
  /**
769
- * Configures a keyring dictionary of keys with an optional active key identifier.
753
+ * Converts a Base64 or Base64URL-encoded string back into a Uint8Array.
754
+ * Normalizes URL-safe characters and padding, strictly validating against the Base64 alphabet.
770
755
  */
771
- setKeyring(keys, activeKid) {
772
- this.keys = keys;
773
- if (activeKid !== void 0) {
774
- this.activeKid = activeKid;
756
+ static fromBase64(base64) {
757
+ if (typeof base64 !== "string") {
758
+ throw new Error("[CKeyManager] Invalid base64 input: expected string");
775
759
  }
760
+ const cleaned = base64.replace(/\s+/g, "");
761
+ if (!cleaned) return new Uint8Array(0);
762
+ const normalized = cleaned.replace(/-/g, "+").replace(/_/g, "/");
763
+ const padded = normalized.padEnd(normalized.length + (4 - normalized.length % 4) % 4, "=");
764
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(padded)) {
765
+ throw new Error("[CKeyManager] Invalid base64 string");
766
+ }
767
+ if (typeof Buffer !== "undefined") {
768
+ const buf = Buffer.from(padded, "base64");
769
+ const arrayBuf = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
770
+ return new Uint8Array(arrayBuf);
771
+ }
772
+ const binary = atob(padded);
773
+ const bytes = new Uint8Array(binary.length);
774
+ for (let i = 0; i < binary.length; i++) {
775
+ bytes[i] = binary.charCodeAt(i);
776
+ }
777
+ return bytes;
776
778
  }
777
779
  /**
778
- * Retrieves the currently configured keyring dictionary.
779
- */
780
- getKeyring() {
781
- return this.keys;
782
- }
783
- /**
784
- * Sets the active Key Identifier (kid) to use for outbound encryption.
780
+ * Deterministically resolves a salt from Uint8Array, ArrayBuffer, Base64/Base64URL string, or raw text string.
781
+ * Guarantees 100% identical salt byte resolution between Node.js and Browser environments.
785
782
  */
786
- setActiveKid(kid) {
787
- this.activeKid = kid;
783
+ static resolveSalt(salt) {
784
+ if (!salt) return CKeyManager.randomBytes(16);
785
+ if (ArrayBuffer.isView(salt)) {
786
+ return salt.byteLength > 0 ? new Uint8Array(salt.buffer.slice(salt.byteOffset, salt.byteOffset + salt.byteLength)) : CKeyManager.randomBytes(16);
787
+ }
788
+ if (salt instanceof ArrayBuffer) {
789
+ return salt.byteLength > 0 ? new Uint8Array(salt.slice(0)) : CKeyManager.randomBytes(16);
790
+ }
791
+ if (typeof salt === "string") {
792
+ const trimmed = salt.trim();
793
+ if (!trimmed) return CKeyManager.randomBytes(16);
794
+ if (trimmed.startsWith("base64:")) {
795
+ return CKeyManager.fromBase64(trimmed.slice(7));
796
+ }
797
+ if (trimmed.endsWith("=") && /^[A-Za-z0-9+/]+={1,2}$/.test(trimmed)) {
798
+ try {
799
+ const decoded = CKeyManager.fromBase64(trimmed);
800
+ if (decoded.byteLength > 0) return decoded;
801
+ } catch {
802
+ }
803
+ }
804
+ const encoded = textEncoder$1.encode(trimmed);
805
+ return encoded.byteLength > 0 ? encoded : CKeyManager.randomBytes(16);
806
+ }
807
+ return CKeyManager.randomBytes(16);
788
808
  }
809
+ // ── Symmetric Key Management ─────────────────────────────────────────────
789
810
  /**
790
- * Retrieves the active Key Identifier (kid).
811
+ * Generates a random AES CryptoKey suitable for AES-GCM or AES-CBC.
812
+ *
813
+ * E2EE Threat Model Note: Focuses on protecting GDPR/tenant data against database and
814
+ * server-side leaks. Keys remain extractable by default to enable role-based key sharing,
815
+ * wrapping, and transfer between authorized client sessions in multi-user SaaS apps.
816
+ * Client-side malware/trojan protection is out of scope.
791
817
  */
792
- getActiveKid() {
793
- return this.activeKid;
818
+ static async generateAESKey(length = 256, algorithm = "AES-GCM", extractable = true) {
819
+ const subtle = getSubtleCrypto();
820
+ const usages = algorithm === "AES-KW" ? ["wrapKey", "unwrapKey"] : ["encrypt", "decrypt"];
821
+ return await subtle.generateKey(
822
+ { name: algorithm, length },
823
+ extractable,
824
+ usages
825
+ );
794
826
  }
795
827
  /**
796
- * Pre-derives and caches the native CryptoKey for subsequent cryptographic operations.
797
- * Subclasses should override this with a dedicated, non-side-effecting implementation.
828
+ * Exports an AES CryptoKey as a raw base64 string.
798
829
  */
799
- async warmupKey(_options) {
830
+ static async exportAESKey(key) {
831
+ const subtle = getSubtleCrypto();
832
+ const raw = await subtle.exportKey("raw", key);
833
+ try {
834
+ return CKeyManager.toBase64(raw);
835
+ } finally {
836
+ new Uint8Array(raw).fill(0);
837
+ }
800
838
  }
801
839
  /**
802
- * Resolves raw key from keyProvider callback or defaultKey.
840
+ * Imports an AES CryptoKey from a raw base64 string.
803
841
  */
804
- async resolveRawKey(keyProvider) {
805
- const target = keyProvider || (this.keys && this.activeKid ? this.keys[this.activeKid] : void 0) || this.defaultKey;
806
- if (!target) {
807
- throw new Error(
808
- `[${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.`
809
- );
842
+ static async importAESKey(base64, algorithm = "AES-GCM", extractable = false) {
843
+ const subtle = getSubtleCrypto();
844
+ const rawBytes = CKeyManager.fromBase64(base64);
845
+ if (rawBytes.length !== 16 && rawBytes.length !== 24 && rawBytes.length !== 32) {
846
+ rawBytes.fill(0);
847
+ throw new Error(`[CKeyManager] Invalid AES key length: expected 16, 24, or 32 bytes (128, 192, or 256 bits), got ${rawBytes.length} bytes.`);
810
848
  }
811
- const rawKey = typeof target === "function" ? await target() : target;
812
- if (!rawKey || typeof rawKey === "string" && rawKey.trim().length === 0 || typeof rawKey === "object" && "byteLength" in rawKey && rawKey.byteLength === 0) {
813
- throw new Error(
814
- `[${this.constructor.name}] No encryption key or keyProvider was supplied. Key provider resolved to empty or undefined key.`
849
+ const usages = algorithm === "AES-KW" ? ["wrapKey", "unwrapKey"] : ["encrypt", "decrypt"];
850
+ try {
851
+ return await subtle.importKey(
852
+ "raw",
853
+ rawBytes,
854
+ { name: algorithm },
855
+ extractable,
856
+ usages
815
857
  );
858
+ } finally {
859
+ rawBytes.fill(0);
816
860
  }
817
- return rawKey;
818
861
  }
862
+ // ── Asymmetric Key Pair Management ───────────────────────────────────────
819
863
  /**
820
- * Checks whether the given payload is an encrypted envelope.
864
+ * Generates an RSA-OAEP public/private key pair (2048 or 4096 bits).
821
865
  */
822
- static isEncrypted(payload) {
823
- if (!payload || typeof payload !== "object") return false;
824
- return payload.__encrypted === true && typeof payload.ciphertext === "string" && typeof payload.iv === "string";
866
+ static async generateRSAKeyPair(bits = 4096, hash = "SHA-256", extractable = true) {
867
+ const subtle = getSubtleCrypto();
868
+ const pair = await subtle.generateKey(
869
+ {
870
+ name: "RSA-OAEP",
871
+ modulusLength: bits,
872
+ publicExponent: new Uint8Array([1, 0, 1]),
873
+ hash
874
+ },
875
+ extractable,
876
+ ["encrypt", "decrypt", "wrapKey", "unwrapKey"]
877
+ );
878
+ return {
879
+ publicKey: pair.publicKey,
880
+ privateKey: pair.privateKey
881
+ };
825
882
  }
826
883
  /**
827
- * Checks whether the given payload is an encrypted envelope.
884
+ * Generates an ECDH public/private key pair for elliptic curve Diffie-Hellman.
828
885
  */
829
- isEncrypted(payload) {
830
- return AEncryptionProvider.isEncrypted(payload);
886
+ static async generateECDHKeyPair(curve = "P-256", extractable = true) {
887
+ const subtle = getSubtleCrypto();
888
+ const pair = await subtle.generateKey(
889
+ {
890
+ name: "ECDH",
891
+ namedCurve: curve
892
+ },
893
+ extractable,
894
+ ["deriveKey", "deriveBits"]
895
+ );
896
+ return {
897
+ publicKey: pair.publicKey,
898
+ privateKey: pair.privateKey
899
+ };
831
900
  }
832
901
  /**
833
- * Clears configured keys and cached cryptographic material from memory.
902
+ * Exports a public CryptoKey to a portable JsonWebKey (JWK).
834
903
  */
835
- clearKeys() {
836
- this.defaultKey = void 0;
837
- this.keys = void 0;
838
- this.activeKid = void 0;
839
- this.allowedKids = void 0;
904
+ static async exportPublicKey(key) {
905
+ const subtle = getSubtleCrypto();
906
+ return await subtle.exportKey("jwk", key);
840
907
  }
841
908
  /**
842
- * Alias for clearKeys() to conform with standard disposal patterns.
909
+ * Exports a private CryptoKey to a portable JsonWebKey (JWK).
843
910
  */
844
- dispose() {
845
- this.clearKeys();
911
+ static async exportPrivateKey(key) {
912
+ const subtle = getSubtleCrypto();
913
+ return await subtle.exportKey("jwk", key);
846
914
  }
847
915
  /**
848
- * ECMAScript Explicit Resource Management protocol (using / await using declarations).
916
+ * Imports a public key from a JsonWebKey (JWK).
849
917
  */
850
- [Symbol.dispose]() {
851
- this.dispose();
852
- }
853
- async [Symbol.asyncDispose]() {
854
- this.dispose();
918
+ static async importPublicKey(jwk, algorithm) {
919
+ const subtle = getSubtleCrypto();
920
+ let importAlgorithm = algorithm;
921
+ let usages;
922
+ const isEC = jwk.kty === "EC" || typeof algorithm === "object" && algorithm?.name === "ECDH" || algorithm === "ECDH";
923
+ if (isEC) {
924
+ usages = [];
925
+ if (!importAlgorithm || importAlgorithm === "ECDH") {
926
+ importAlgorithm = { name: "ECDH", namedCurve: jwk.crv || "P-256" };
927
+ }
928
+ } else {
929
+ usages = ["encrypt", "wrapKey"];
930
+ if (!importAlgorithm || importAlgorithm === "RSA-OAEP") {
931
+ if (jwk.kty === "RSA") {
932
+ const hash = jwk.alg === "RSA-OAEP-512" ? "SHA-512" : "SHA-256";
933
+ importAlgorithm = { name: "RSA-OAEP", hash };
934
+ } else {
935
+ throw new Error(`[CKeyManager] Unsupported JWK kty: ${jwk.kty}`);
936
+ }
937
+ }
938
+ }
939
+ return await subtle.importKey("jwk", jwk, importAlgorithm, true, usages);
855
940
  }
856
941
  /**
857
- * Resolves effective Authenticated Additional Data (AAD) for the operation:
858
- * 1. If options.bindRequestContextAad is explicitly false, returns options.aad directly without context binding.
859
- * 2. When encrypting (isDecrypt=false): binds request context `req:${method}:${url}` only if options.bindRequestContextAad === true.
860
- * 3. When decrypting (isDecrypt=true): derives request context `req:${method}:${url}` if url or method is provided.
861
- * 4. If explicit options.aad is provided: combines with request context if bound, or returns options.aad directly.
862
- * 5. Otherwise returns undefined.
942
+ * Imports a private key from a JsonWebKey (JWK).
863
943
  */
864
- _resolveEffectiveAad(options, isDecrypt = false) {
865
- if (!options) return void 0;
866
- let rawAad = options.aad;
867
- if (typeof rawAad === "function") {
868
- rawAad = rawAad(void 0, "");
869
- }
870
- if (rawAad === void 0 && options.baseAad) {
871
- rawAad = options.baseAad;
872
- }
873
- if (options.bindRequestContextAad === false) return rawAad;
874
- const hasReqContext = Boolean(options.url || options.method);
875
- if (rawAad) {
876
- if (options.bindRequestContextAad === true && hasReqContext) {
877
- const m = (options.method || "POST").toUpperCase();
878
- const u = options.url || "";
879
- const baseAad = typeof rawAad === "string" ? rawAad : rawAad instanceof Uint8Array || rawAad instanceof ArrayBuffer ? textDecoder.decode(rawAad) : String(rawAad);
880
- if (baseAad.includes(`|req:${m}:${u}`)) {
881
- return baseAad;
944
+ static async importPrivateKey(jwk, algorithm) {
945
+ const subtle = getSubtleCrypto();
946
+ let importAlgorithm = algorithm;
947
+ let usages;
948
+ const isEC = jwk.kty === "EC" || typeof algorithm === "object" && algorithm?.name === "ECDH" || algorithm === "ECDH";
949
+ if (isEC) {
950
+ usages = ["deriveKey", "deriveBits"];
951
+ if (!importAlgorithm || importAlgorithm === "ECDH") {
952
+ importAlgorithm = { name: "ECDH", namedCurve: jwk.crv || "P-256" };
953
+ }
954
+ } else {
955
+ usages = ["decrypt", "unwrapKey"];
956
+ if (!importAlgorithm || importAlgorithm === "RSA-OAEP") {
957
+ if (jwk.kty === "RSA") {
958
+ const hash = jwk.alg === "RSA-OAEP-512" ? "SHA-512" : "SHA-256";
959
+ importAlgorithm = { name: "RSA-OAEP", hash };
960
+ } else {
961
+ throw new Error(`[CKeyManager] Unsupported JWK kty: ${jwk.kty}`);
882
962
  }
883
- return `${baseAad}|req:${m}:${u}`;
884
963
  }
885
- return rawAad;
886
964
  }
887
- const shouldBindReq = isDecrypt ? hasReqContext : options.bindRequestContextAad === true && hasReqContext;
888
- if (shouldBindReq) {
889
- const m = (options.method || "POST").toUpperCase();
890
- const u = options.url || "";
891
- return `req:${m}:${u}`;
892
- }
893
- return void 0;
965
+ return await subtle.importKey("jwk", jwk, importAlgorithm, true, usages);
894
966
  }
895
967
  /**
896
- * Helper to deterministically bind record ID, contextual sibling fields (aadFields),
897
- * dynamic AAD callbacks, and field name into the AAD to prevent ciphertext swapping
898
- * between fields, records, or parent entities.
968
+ * Imports a public key from a PEM (SPKI) or base64 SPKI string.
899
969
  */
900
- _bindFieldAad(data, field, options) {
901
- let baseAad = "";
902
- if (options?.baseAad) {
903
- baseAad = options.baseAad;
904
- }
905
- if (options?.aad) {
906
- if (typeof options.aad === "function") {
907
- const dynamicRes = options.aad(data, field);
908
- if (typeof dynamicRes === "string") {
909
- baseAad = baseAad ? `${baseAad}|${dynamicRes}` : dynamicRes;
910
- } else if (dynamicRes instanceof Uint8Array || dynamicRes instanceof ArrayBuffer) {
911
- const decoded = textDecoder.decode(dynamicRes);
912
- baseAad = baseAad ? `${baseAad}|${decoded}` : decoded;
913
- } else if (dynamicRes !== void 0 && dynamicRes !== null) {
914
- const str = String(dynamicRes);
915
- baseAad = baseAad ? `${baseAad}|${str}` : str;
916
- }
917
- } else if (typeof options.aad === "string") {
918
- baseAad = baseAad ? `${baseAad}|${options.aad}` : options.aad;
919
- } else if (options.aad instanceof Uint8Array || options.aad instanceof ArrayBuffer) {
920
- const decoded = textDecoder.decode(options.aad);
921
- baseAad = baseAad ? `${baseAad}|${decoded}` : decoded;
922
- } else {
923
- const str = String(options.aad);
924
- baseAad = baseAad ? `${baseAad}|${str}` : str;
925
- }
970
+ static async importPublicKeyFromPEM(pem, algorithm = "RSA-OAEP", curve = "P-256", hash = "SHA-256") {
971
+ const subtle = getSubtleCrypto();
972
+ const cleaned = pem.replace(/-----BEGIN[ A-Z0-9_-]+-----/g, "").replace(/-----END[ A-Z0-9_-]+-----/g, "").replace(/\s+/g, "");
973
+ const spkiBytes = CKeyManager.fromBase64(cleaned);
974
+ let importAlgorithm;
975
+ let usages;
976
+ if (algorithm === "ECDH") {
977
+ importAlgorithm = { name: "ECDH", namedCurve: curve };
978
+ usages = [];
979
+ } else if (algorithm === "ECDSA") {
980
+ importAlgorithm = { name: "ECDSA", namedCurve: curve };
981
+ usages = ["verify"];
982
+ } else {
983
+ importAlgorithm = { name: "RSA-OAEP", hash };
984
+ usages = ["encrypt", "wrapKey"];
926
985
  }
927
- const recordId = data && typeof data === "object" && (data.id !== void 0 || data._id !== void 0) ? String(data.id ?? data._id) : "";
928
- const recordPart = recordId ? `|record:${recordId}` : "";
929
- let contextPart = "";
930
- if (options?.aadFields && options.aadFields.length > 0 && data && typeof data === "object") {
931
- const sortedFields = [...options.aadFields].sort();
932
- const parts = [];
933
- for (const f of sortedFields) {
934
- const val = data[f];
935
- let serializedVal = "";
936
- if (val === null) {
937
- serializedVal = "null";
938
- } else if (val !== void 0) {
939
- serializedVal = typeof val === "object" ? JSON.stringify(val) : String(val);
940
- }
941
- parts.push(`${f}=${serializedVal}`);
942
- }
943
- contextPart = `|ctx:${parts.join(",")}`;
986
+ try {
987
+ return await subtle.importKey("spki", spkiBytes, importAlgorithm, true, usages);
988
+ } finally {
989
+ spkiBytes.fill(0);
944
990
  }
945
- const reqPart = options?.bindRequestContextAad === true && (options?.url || options?.method) ? `|req:${(options.method || "POST").toUpperCase()}:${options.url || ""}` : "";
946
- return {
947
- ...options,
948
- bindRequestContextAad: false,
949
- aad: `aad:${baseAad}${recordPart}${contextPart}${reqPart}|field:${field}`
950
- };
951
991
  }
952
992
  /**
953
- * Validates envelope metadata against algorithm whitelists and key identifiers (kid).
954
- * In strict mode, allowedAlgorithms defaults to ['AES-GCM'].
993
+ * Imports an ECDSA public key from PEM (SPKI) or base64 SPKI string for signature verification.
955
994
  */
956
- _validateEnvelopeMetadata(payload, options) {
957
- if (!payload || typeof payload !== "object") return void 0;
958
- const allowedAlgorithms = options?.allowedAlgorithms ?? (options?.strict ? ["AES-GCM"] : void 0);
959
- if (allowedAlgorithms && allowedAlgorithms.length > 0) {
960
- const alg = payload.alg;
961
- if (!alg || !allowedAlgorithms.includes(alg)) {
962
- return new Error(`[AEncryptionProvider] Algorithm mismatch: algorithm '${alg || "unknown"}' is not in allowedAlgorithms.`);
963
- }
964
- }
965
- const effectiveKeys = options?.keys || this.keys;
966
- const effectiveAllowedKids = options?.allowedKids || this.allowedKids;
967
- const allowedKids = effectiveAllowedKids ?? (effectiveKeys ? Object.keys(effectiveKeys) : void 0);
968
- if (allowedKids && allowedKids.length > 0) {
969
- if (payload.kid && !allowedKids.includes(payload.kid)) {
970
- return new Error(`[AEncryptionProvider] Unauthorized key: kid '${payload.kid}' is not in allowedKids.`);
971
- }
972
- }
973
- return void 0;
995
+ static async importECDSAPublicKeyFromPEM(pem, curve = "P-256") {
996
+ return CKeyManager.importPublicKeyFromPEM(pem, "ECDSA", curve);
974
997
  }
975
998
  /**
976
- * Normalizes encryption options with active keyring parameters:
977
- * If `keys` and `activeKid` are configured, selects `key = keys[activeKid]` and sets `kid = activeKid`.
999
+ * Imports a private key from a PEM (PKCS#8) or base64 PKCS#8 string.
978
1000
  */
979
- _resolveOutboundKeyringOptions(options) {
980
- const effectiveKeys = options?.keys || this.keys;
981
- const effectiveActiveKid = options?.activeKid || (options?.kid ? options.kid : this.activeKid);
982
- let effectiveKey = options?.key;
983
- let effectiveKid = options?.kid || effectiveActiveKid;
984
- if (effectiveKeys && effectiveActiveKid) {
985
- if (effectiveKeys[effectiveActiveKid]) {
986
- effectiveKey = options?.key || effectiveKeys[effectiveActiveKid];
987
- effectiveKid = effectiveActiveKid;
988
- } else if (!options?.key) {
989
- throw new Error(`[${this.constructor.name}] Active kid '${effectiveActiveKid}' not found in keyring keys.`);
990
- }
991
- } else if (effectiveKeys && !effectiveKey) {
992
- const kids = Object.keys(effectiveKeys);
993
- if (kids.length > 0) {
994
- effectiveKid = kids[0];
995
- effectiveKey = effectiveKeys[kids[0]];
996
- }
1001
+ static async importPrivateKeyFromPEM(pem, algorithm = "RSA-OAEP", curve = "P-256", hash = "SHA-256") {
1002
+ if (/BEGIN\s+(RSA|EC)\s+PRIVATE\s+KEY/i.test(pem)) {
1003
+ throw new Error(
1004
+ `[CKeyManager] Failed to import private key. The provided PEM appears to be in PKCS#1 or SEC1 format. Web Crypto requires PKCS#8 format. Convert your key using: openssl pkcs8 -topk8 -nocrypt -in private.pem -out private_pkcs8.pem.`
1005
+ );
997
1006
  }
998
- if (effectiveKeys || effectiveActiveKid || effectiveKey !== options?.key || effectiveKid !== options?.kid) {
999
- return {
1000
- ...options,
1001
- ...effectiveKeys ? { keys: effectiveKeys } : {},
1002
- ...effectiveActiveKid ? { activeKid: effectiveActiveKid } : {},
1003
- ...effectiveKey !== void 0 ? { key: effectiveKey } : {},
1004
- ...effectiveKid !== void 0 ? { kid: effectiveKid } : {}
1005
- };
1007
+ const subtle = getSubtleCrypto();
1008
+ const cleaned = pem.replace(/-----BEGIN[ A-Z0-9_-]+-----/g, "").replace(/-----END[ A-Z0-9_-]+-----/g, "").replace(/\s+/g, "");
1009
+ const pkcs8Bytes = CKeyManager.fromBase64(cleaned);
1010
+ let importAlgorithm;
1011
+ let usages;
1012
+ if (algorithm === "ECDH") {
1013
+ importAlgorithm = { name: "ECDH", namedCurve: curve };
1014
+ usages = ["deriveKey", "deriveBits"];
1015
+ } else if (algorithm === "ECDSA") {
1016
+ importAlgorithm = { name: "ECDSA", namedCurve: curve };
1017
+ usages = ["sign"];
1018
+ } else {
1019
+ importAlgorithm = { name: "RSA-OAEP", hash };
1020
+ usages = ["decrypt", "unwrapKey"];
1021
+ }
1022
+ try {
1023
+ return await subtle.importKey("pkcs8", pkcs8Bytes, importAlgorithm, true, usages);
1024
+ } finally {
1025
+ pkcs8Bytes.fill(0);
1006
1026
  }
1007
- return options || {};
1008
1027
  }
1009
1028
  /**
1010
- * Resolves the appropriate decryption key for a specific payload envelope based on its `kid`
1011
- * and the configured keyring (`keys`, `activeKid`, or fallback `key`).
1029
+ * Imports an ECDSA private key from PEM (PKCS#8) or base64 PKCS#8 string for signing.
1012
1030
  */
1013
- _resolveDecryptionKeyForEnvelope(payload, options) {
1014
- const effectiveKeys = options?.keys || this.keys;
1015
- const effectiveActiveKid = options?.activeKid || this.activeKid;
1016
- if (payload && typeof payload === "object" && payload.kid && effectiveKeys) {
1017
- if (effectiveKeys[payload.kid]) {
1018
- return effectiveKeys[payload.kid];
1019
- }
1020
- }
1021
- if (effectiveKeys && effectiveActiveKid && effectiveKeys[effectiveActiveKid]) {
1022
- return effectiveKeys[effectiveActiveKid];
1023
- }
1024
- return options?.key || this.defaultKey;
1031
+ static async importECDSAPrivateKeyFromPEM(pem, curve = "P-256") {
1032
+ return CKeyManager.importPrivateKeyFromPEM(pem, "ECDSA", curve);
1025
1033
  }
1026
1034
  /**
1027
- * Handles a per-field or per-payload decryption error according to configured options:
1028
- * - Invokes onDecryptError callback if provided
1029
- * - Attaches error metadata (__decryptErrors) to the record object
1030
- * - Returns fallback cell placeholder ('[⚠ Decryption Error]') or rethrows
1035
+ * Exports a public CryptoKey to a standard PEM (SPKI) string.
1031
1036
  */
1032
- _handleDecryptError(err, record, field, value, index, options) {
1033
- if (record && typeof record === "object" && record !== value) {
1034
- record.__decryptErrors = {
1035
- ...record.__decryptErrors || {},
1036
- [field || "__payload"]: err
1037
- };
1038
- }
1039
- if (options?.onDecryptError === false || options?.onDecryptError === void 0) {
1040
- throw err;
1041
- }
1042
- if (typeof options.onDecryptError === "function") {
1043
- return options.onDecryptError({
1044
- record,
1045
- field,
1046
- value,
1047
- error: err,
1048
- index
1049
- });
1050
- }
1051
- if (typeof options.onDecryptError === "string") {
1052
- return options.onDecryptError;
1053
- }
1054
- if (options.onDecryptError === true) {
1055
- return "[\u26A0 Decryption Error]";
1037
+ static async exportPublicKeyToPEM(key) {
1038
+ const subtle = getSubtleCrypto();
1039
+ const spki = await subtle.exportKey("spki", key);
1040
+ let b64;
1041
+ try {
1042
+ b64 = CKeyManager.toBase64(spki);
1043
+ } finally {
1044
+ new Uint8Array(spki).fill(0);
1056
1045
  }
1057
- throw err;
1046
+ const lines = b64.match(/.{1,64}/g) || [b64];
1047
+ return `-----BEGIN PUBLIC KEY-----
1048
+ ${lines.join("\n")}
1049
+ -----END PUBLIC KEY-----`;
1058
1050
  }
1059
1051
  /**
1060
- * Top-level polymorphic encryption method.
1061
- * Automatically performs value-level encryption:
1062
- * - If data is an array of objects: recursively encrypts each row.
1063
- * - If data is an object: encrypts specified fields (or all values if valueEncryption is true),
1064
- * preserving JSON object keys so backend APIs and ORMs can parse keys and perform upserts.
1065
- * - If data is a primitive or whole-payload envelope: encrypts as a single value.
1052
+ * Exports a private CryptoKey to a standard PEM (PKCS#8) string.
1066
1053
  */
1067
- async encrypt(data, options) {
1068
- if (data === void 0 || data === null) return data;
1069
- if (this.isEncrypted(data)) return data;
1070
- options = this._resolveOutboundKeyringOptions(options);
1071
- if (options?.valueEncryption === false && (!options.fields || options.fields.length === 0)) {
1072
- return this.encryptValue(data, options);
1073
- }
1074
- if (Array.isArray(data)) {
1075
- if (data.length === 0) return data;
1076
- if (options?.fields && options.fields.length > 0) {
1077
- const fields = options.fields;
1078
- let hasUnencrypted = false;
1079
- for (let i = 0; i < data.length; i++) {
1080
- const item = data[i];
1081
- if (item && typeof item === "object") {
1082
- for (let f = 0; f < fields.length; f++) {
1083
- const val = item[fields[f]];
1084
- if (val !== void 0 && val !== null && !this.isEncrypted(val)) {
1085
- hasUnencrypted = true;
1086
- break;
1087
- }
1088
- }
1089
- if (hasUnencrypted) break;
1090
- }
1091
- }
1092
- if (!hasUnencrypted) {
1093
- return data;
1094
- }
1095
- await this.warmupKey(options);
1096
- const itemOptions = { ...options, _warmed: true };
1097
- return Promise.all(data.map((item) => this.encrypt(item, itemOptions)));
1098
- }
1099
- if (options?.valueEncryption === true) {
1100
- await this.warmupKey(options);
1101
- const itemOptions = { ...options, _warmed: true };
1102
- return Promise.all(data.map((item) => this.encrypt(item, itemOptions)));
1103
- }
1104
- return this.encryptValue(data, options);
1054
+ static async exportPrivateKeyToPEM(key) {
1055
+ const subtle = getSubtleCrypto();
1056
+ const pkcs8 = await subtle.exportKey("pkcs8", key);
1057
+ let b64;
1058
+ try {
1059
+ b64 = CKeyManager.toBase64(pkcs8);
1060
+ } finally {
1061
+ new Uint8Array(pkcs8).fill(0);
1105
1062
  }
1106
- if (typeof data === "object" && !ArrayBuffer.isView(data) && !(data instanceof ArrayBuffer)) {
1107
- if (options?.fields && options.fields.length > 0) {
1108
- const fieldsToEncrypt = options.fields.filter(
1109
- (field) => Object.prototype.hasOwnProperty.call(data, field) && data[field] !== void 0 && data[field] !== null && !this.isEncrypted(data[field])
1063
+ const lines = b64.match(/.{1,64}/g) || [b64];
1064
+ return `-----BEGIN PRIVATE KEY-----
1065
+ ${lines.join("\n")}
1066
+ -----END PRIVATE KEY-----`;
1067
+ }
1068
+ // ── Key Wrapping & Unwrapping ────────────────────────────────────────────
1069
+ /**
1070
+ * Wraps a target CryptoKey (e.g. an Account Symmetric Key) using a wrapping key
1071
+ * (AES-KW or RSA-OAEP).
1072
+ */
1073
+ static async wrapKey(keyToWrap, wrappingKey, format = "raw") {
1074
+ const subtle = getSubtleCrypto();
1075
+ let wrapAlgorithm;
1076
+ if (wrappingKey.algorithm.name === "RSA-OAEP") {
1077
+ wrapAlgorithm = { name: "RSA-OAEP" };
1078
+ } else if (wrappingKey.algorithm.name === "AES-KW") {
1079
+ wrapAlgorithm = "AES-KW";
1080
+ } else {
1081
+ wrapAlgorithm = "AES-KW";
1082
+ }
1083
+ const wrappedBuffer = await subtle.wrapKey(format, keyToWrap, wrappingKey, wrapAlgorithm);
1084
+ return CKeyManager.toBase64(wrappedBuffer);
1085
+ }
1086
+ /**
1087
+ * Unwraps a base64-encoded wrapped key using an unwrapping key (AES-KW or RSA-OAEP).
1088
+ */
1089
+ static async unwrapKey(wrappedBase64, unwrappingKey, unwrappedKeyAlgorithm = { name: "AES-GCM", length: 256 }, extractable = true, keyUsages = ["encrypt", "decrypt"], format = "raw") {
1090
+ const subtle = getSubtleCrypto();
1091
+ const wrappedBytes = CKeyManager.fromBase64(wrappedBase64);
1092
+ let unwrapAlgorithm;
1093
+ if (unwrappingKey.algorithm.name === "RSA-OAEP") {
1094
+ unwrapAlgorithm = { name: "RSA-OAEP" };
1095
+ } else {
1096
+ unwrapAlgorithm = "AES-KW";
1097
+ }
1098
+ try {
1099
+ return await subtle.unwrapKey(
1100
+ format,
1101
+ wrappedBytes,
1102
+ unwrappingKey,
1103
+ unwrapAlgorithm,
1104
+ unwrappedKeyAlgorithm,
1105
+ extractable,
1106
+ keyUsages
1107
+ );
1108
+ } finally {
1109
+ wrappedBytes.fill(0);
1110
+ }
1111
+ }
1112
+ /**
1113
+ * Generates a high-entropy symmetric AES-GCM Account Data Encryption Key (ADEK).
1114
+ * Designed for multi-user table and field-level encryption.
1115
+ */
1116
+ static async generateAccountDataKey(length = 256, extractable = false) {
1117
+ const subtle = getSubtleCrypto();
1118
+ return await subtle.generateKey(
1119
+ { name: "AES-GCM", length },
1120
+ extractable,
1121
+ ["encrypt", "decrypt"]
1122
+ );
1123
+ }
1124
+ /**
1125
+ * Wraps an Account Data Encryption Key (ADEK) for a specific user using the user's public key (RSA-OAEP).
1126
+ * Returns a Base64-encoded wrapped key string suitable for storage in role-map / access tables.
1127
+ */
1128
+ static async wrapAccountKeyForUser(adek, userPublicKey) {
1129
+ const subtle = getSubtleCrypto();
1130
+ if (userPublicKey.algorithm.name === "RSA-OAEP") {
1131
+ const rawKey = await subtle.exportKey("raw", adek);
1132
+ try {
1133
+ const wrappedBuffer = await subtle.encrypt(
1134
+ { name: "RSA-OAEP" },
1135
+ userPublicKey,
1136
+ rawKey
1110
1137
  );
1111
- if (fieldsToEncrypt.length === 0) {
1112
- return data;
1113
- }
1114
- if (!options?._warmed) {
1115
- await this.warmupKey(options);
1116
- }
1117
- const result = { ...data };
1118
- const entries = await Promise.all(
1119
- fieldsToEncrypt.map(async (field) => {
1120
- const fieldOptions = this._bindFieldAad(data, field, options);
1121
- return [field, await this.encryptValue(result[field], fieldOptions)];
1122
- })
1138
+ return CKeyManager.toBase64(wrappedBuffer);
1139
+ } finally {
1140
+ new Uint8Array(rawKey).fill(0);
1141
+ }
1142
+ }
1143
+ throw new Error(
1144
+ `[CKeyManager] Unsupported user key algorithm for wrapAccountKeyForUser: '${userPublicKey.algorithm.name}'. Expected 'RSA-OAEP'.`
1145
+ );
1146
+ }
1147
+ /**
1148
+ * Unwraps a Base64-encoded Account Data Encryption Key (ADEK) using the user's private key (RSA-OAEP).
1149
+ * Returns the restored AES-GCM CryptoKey ready for use in CWebCryptoEncryptionProvider.
1150
+ */
1151
+ static async unwrapAccountKeyForUser(wrappedKeyBase64, userPrivateKey, unwrappedKeyAlgorithm = { name: "AES-GCM", length: 256 }, extractable = false) {
1152
+ const subtle = getSubtleCrypto();
1153
+ if (userPrivateKey.algorithm.name === "RSA-OAEP") {
1154
+ const wrappedBytes = CKeyManager.fromBase64(wrappedKeyBase64);
1155
+ let unwrappedRawKey;
1156
+ try {
1157
+ unwrappedRawKey = await subtle.decrypt(
1158
+ { name: "RSA-OAEP" },
1159
+ userPrivateKey,
1160
+ wrappedBytes
1123
1161
  );
1124
- for (const [field, encryptedVal] of entries) {
1125
- safeSetProperty(result, field, encryptedVal);
1126
- }
1127
- return result;
1162
+ } finally {
1163
+ wrappedBytes.fill(0);
1128
1164
  }
1129
- if (options?.valueEncryption === true) {
1130
- const result = { ...data };
1131
- const keysToEncrypt = Object.keys(result).filter(
1132
- (key) => result[key] !== void 0 && result[key] !== null && !this.isEncrypted(result[key])
1165
+ try {
1166
+ return await subtle.importKey(
1167
+ "raw",
1168
+ unwrappedRawKey,
1169
+ unwrappedKeyAlgorithm,
1170
+ extractable,
1171
+ ["encrypt", "decrypt"]
1133
1172
  );
1134
- if (keysToEncrypt.length === 0) {
1135
- return result;
1136
- }
1137
- if (!options?._warmed) {
1138
- await this.warmupKey(options);
1139
- }
1140
- const entries = await Promise.all(
1141
- keysToEncrypt.map(async (key) => {
1142
- const fieldOptions = this._bindFieldAad(data, key, options);
1143
- return [key, await this.encryptValue(result[key], fieldOptions)];
1144
- })
1173
+ } finally {
1174
+ new Uint8Array(unwrappedRawKey).fill(0);
1175
+ }
1176
+ }
1177
+ throw new Error(
1178
+ `[CKeyManager] Unsupported user key algorithm for unwrapAccountKeyForUser: '${userPrivateKey.algorithm.name}'. Expected 'RSA-OAEP'.`
1179
+ );
1180
+ }
1181
+ /**
1182
+ * Wraps an HMAC blind indexing key for a specific recipient user using their RSA-OAEP public key.
1183
+ * Returns a Base64-encoded wrapped key string suitable for storage in role-map / access tables.
1184
+ */
1185
+ static async wrapHMACKeyForUser(hmacKey, userPublicKey) {
1186
+ const subtle = getSubtleCrypto();
1187
+ if (userPublicKey.algorithm.name === "RSA-OAEP") {
1188
+ const rawKey = await subtle.exportKey("raw", hmacKey);
1189
+ try {
1190
+ const wrappedBuffer = await subtle.encrypt(
1191
+ { name: "RSA-OAEP" },
1192
+ userPublicKey,
1193
+ rawKey
1145
1194
  );
1146
- for (const [key, encryptedVal] of entries) {
1147
- safeSetProperty(result, key, encryptedVal);
1148
- }
1149
- return result;
1195
+ return CKeyManager.toBase64(wrappedBuffer);
1196
+ } finally {
1197
+ new Uint8Array(rawKey).fill(0);
1150
1198
  }
1151
1199
  }
1152
- return this.encryptValue(data, options);
1200
+ throw new Error(
1201
+ `[CKeyManager] Unsupported user key algorithm for wrapHMACKeyForUser: '${userPublicKey.algorithm.name}'. Expected 'RSA-OAEP'.`
1202
+ );
1153
1203
  }
1154
1204
  /**
1155
- * Top-level polymorphic decryption method.
1156
- * Automatically performs value-level decryption:
1157
- * - If payload is an encrypted envelope: validates metadata and decrypts directly via decryptValue.
1158
- * - If payload is an array of objects: decrypts each row (batched with local memoization).
1159
- * - If payload is an object: decrypts any encrypted value cells across fields.
1160
- * In strict mode:
1161
- * - Unencrypted plaintext in configured `fields` triggers a decryption error.
1162
- * - Envelopes are validated against `allowedAlgorithms` and `allowedKids`.
1163
- * - Errors are routed through `onDecryptError` or populated into `__decryptErrors`.
1205
+ * Unwraps a Base64-encoded HMAC key using a recipient's RSA-OAEP private key.
1206
+ * Restores the CryptoKey configured for HMAC signing and verification in search indexing.
1164
1207
  */
1165
- async decrypt(payload, options) {
1166
- if (payload === void 0 || payload === null) return payload;
1167
- if (this.isEncrypted(payload)) {
1168
- const envelopeKey = this._resolveDecryptionKeyForEnvelope(payload, options);
1169
- const envelopeOptions = envelopeKey !== options?.key ? { ...options, key: envelopeKey } : options;
1170
- const metaErr = this._validateEnvelopeMetadata(payload, envelopeOptions);
1171
- if (metaErr) {
1172
- return this._handleDecryptError(metaErr, payload, "", payload, void 0, envelopeOptions);
1208
+ static async unwrapHMACKeyForUser(wrappedKeyBase64, userPrivateKey, hash = "SHA-256", extractable = false) {
1209
+ const subtle = getSubtleCrypto();
1210
+ if (userPrivateKey.algorithm.name === "RSA-OAEP") {
1211
+ const wrappedBytes = CKeyManager.fromBase64(wrappedKeyBase64);
1212
+ let unwrappedRawKey;
1213
+ try {
1214
+ unwrappedRawKey = await subtle.decrypt(
1215
+ { name: "RSA-OAEP" },
1216
+ userPrivateKey,
1217
+ wrappedBytes
1218
+ );
1219
+ } finally {
1220
+ wrappedBytes.fill(0);
1173
1221
  }
1174
1222
  try {
1175
- return await this.decryptValue(payload, envelopeOptions);
1176
- } catch (err) {
1177
- return this._handleDecryptError(err, payload, "", payload, void 0, envelopeOptions);
1223
+ return await subtle.importKey(
1224
+ "raw",
1225
+ unwrappedRawKey,
1226
+ { name: "HMAC", hash: { name: hash } },
1227
+ extractable,
1228
+ ["sign", "verify"]
1229
+ );
1230
+ } finally {
1231
+ new Uint8Array(unwrappedRawKey).fill(0);
1178
1232
  }
1179
1233
  }
1180
- if (Array.isArray(payload)) {
1181
- if (payload.length === 0) return payload;
1182
- const localMemo = /* @__PURE__ */ new Map();
1183
- const decryptRow = async (item, rowIndex) => {
1184
- if (!item) return item;
1185
- if (this.isEncrypted(item)) {
1186
- const envelopeKey = this._resolveDecryptionKeyForEnvelope(item, options);
1187
- const envelopeOptions = envelopeKey !== options?.key ? { ...options, key: envelopeKey } : options;
1188
- const metaErr = this._validateEnvelopeMetadata(item, envelopeOptions);
1189
- if (metaErr) {
1190
- return this._handleDecryptError(metaErr, item, "", item, rowIndex, envelopeOptions);
1191
- }
1192
- const ct = item.ciphertext;
1193
- const iv = item.iv || "";
1194
- const aadPart = options?.aad ? typeof options.aad === "string" ? options.aad : "" : "";
1195
- const memoKey = `${ct}:${iv}:${aadPart}:${item.kid || ""}`;
1196
- try {
1197
- if (!localMemo.has(memoKey)) {
1198
- localMemo.set(memoKey, this.decryptValue(item, envelopeOptions));
1199
- }
1200
- return await localMemo.get(memoKey);
1201
- } catch (err) {
1202
- return this._handleDecryptError(err, item, "", item, rowIndex, envelopeOptions);
1203
- }
1204
- }
1205
- if (typeof item === "object" && !ArrayBuffer.isView(item) && !(item instanceof ArrayBuffer)) {
1206
- const targetKeys = options?.fields && options.fields.length > 0 ? options.fields.filter((f) => Object.prototype.hasOwnProperty.call(item, f)) : Object.keys(item);
1207
- const keysToDecrypt = targetKeys.filter((key) => this.isEncrypted(item[key]));
1208
- 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])) : [];
1209
- if (keysToDecrypt.length === 0 && strictViolationKeys.length === 0) {
1210
- return item;
1211
- }
1212
- const proto = Object.getPrototypeOf(item);
1213
- const result = proto && proto !== Object.prototype ? Object.assign(Object.create(proto), item) : { ...item };
1214
- for (const key of strictViolationKeys) {
1215
- const err = new Error(`[AEncryptionProvider] Strict decryption violation: field '${key}' contains unencrypted plaintext.`);
1216
- const fallbackVal = this._handleDecryptError(err, result, key, item[key], rowIndex, options);
1217
- safeSetProperty(result, key, fallbackVal);
1218
- }
1219
- await Promise.all(
1220
- keysToDecrypt.map(async (key) => {
1221
- const env = item[key];
1222
- const envelopeKey = this._resolveDecryptionKeyForEnvelope(env, options);
1223
- const baseOptions = envelopeKey !== options?.key ? { ...options, key: envelopeKey } : options;
1224
- const boundOptions = this._bindFieldAad(item, key, baseOptions);
1225
- const hasBoundAad = boundOptions.aad !== baseOptions?.aad;
1226
- const metaErr = this._validateEnvelopeMetadata(env, boundOptions);
1227
- if (metaErr) {
1228
- const fallbackVal = this._handleDecryptError(metaErr, result, key, env, rowIndex, boundOptions);
1229
- safeSetProperty(result, key, fallbackVal);
1230
- return;
1231
- }
1232
- const ct = env.ciphertext;
1233
- const iv = env.iv || "";
1234
- const aadPart = boundOptions.aad ? typeof boundOptions.aad === "string" ? boundOptions.aad : "" : "";
1235
- const memoKey = `${ct}:${iv}:${aadPart}:${env.kid || ""}`;
1236
- try {
1237
- if (!localMemo.has(memoKey)) {
1238
- const decryptPromise = (async () => {
1239
- try {
1240
- return await this.decryptValue(env, boundOptions);
1241
- } catch (err) {
1242
- const canFallback = hasBoundAad && (!options?.aadFields || options.aadFields.length === 0) && typeof options?.aad !== "function";
1243
- if (canFallback && isAuthOrDecryptionError(err)) {
1244
- return await this.decryptValue(env, baseOptions);
1245
- }
1246
- throw err;
1247
- }
1248
- })();
1249
- localMemo.set(memoKey, decryptPromise);
1250
- }
1251
- const decryptedVal = await localMemo.get(memoKey);
1252
- safeSetProperty(result, key, decryptedVal);
1253
- } catch (err) {
1254
- const fallbackVal = this._handleDecryptError(err, result, key, env, rowIndex, boundOptions);
1255
- safeSetProperty(result, key, fallbackVal);
1256
- }
1257
- })
1258
- );
1259
- return result;
1260
- }
1261
- return item;
1262
- };
1263
- if (payload.length <= 100) {
1264
- return Promise.all(payload.map((item, index) => decryptRow(item, index)));
1265
- }
1266
- const batchSize = 1e3;
1267
- const results = new Array(payload.length);
1268
- for (let i = 0; i < payload.length; i += batchSize) {
1269
- const end = Math.min(i + batchSize, payload.length);
1270
- const chunk = payload.slice(i, end);
1271
- const chunkRes = await Promise.all(chunk.map((item, localIdx) => decryptRow(item, i + localIdx)));
1272
- for (let j = 0; j < chunkRes.length; j++) {
1273
- results[i + j] = chunkRes[j];
1274
- }
1275
- if (end < payload.length && (i > 0 && i % 5e3 === 0)) {
1276
- await new Promise((r) => setTimeout(r, 0));
1277
- }
1278
- }
1279
- return results;
1280
- }
1281
- if (typeof payload === "object" && !ArrayBuffer.isView(payload) && !(payload instanceof ArrayBuffer)) {
1282
- const targetKeys = options?.fields && options.fields.length > 0 ? options.fields.filter((f) => Object.prototype.hasOwnProperty.call(payload, f)) : Object.keys(payload);
1283
- const keysToDecrypt = targetKeys.filter((key) => this.isEncrypted(payload[key]));
1284
- 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])) : [];
1285
- if (keysToDecrypt.length === 0 && strictViolationKeys.length === 0) {
1286
- return payload;
1287
- }
1288
- const proto = Object.getPrototypeOf(payload);
1289
- const result = proto && proto !== Object.prototype ? Object.assign(Object.create(proto), payload) : { ...payload };
1290
- for (const key of strictViolationKeys) {
1291
- const err = new Error(`[AEncryptionProvider] Strict decryption violation: field '${key}' contains unencrypted plaintext.`);
1292
- const fallbackVal = this._handleDecryptError(err, result, key, payload[key], void 0, options);
1293
- safeSetProperty(result, key, fallbackVal);
1294
- }
1295
- await Promise.all(
1296
- keysToDecrypt.map(async (key) => {
1297
- const env = payload[key];
1298
- const envelopeKey = this._resolveDecryptionKeyForEnvelope(env, options);
1299
- const baseOptions = envelopeKey !== options?.key ? { ...options, key: envelopeKey } : options;
1300
- const boundOptions = this._bindFieldAad(payload, key, baseOptions);
1301
- const hasBoundAad = boundOptions.aad !== baseOptions?.aad;
1302
- const metaErr = this._validateEnvelopeMetadata(env, boundOptions);
1303
- if (metaErr) {
1304
- const fallbackVal = this._handleDecryptError(metaErr, result, key, env, void 0, boundOptions);
1305
- safeSetProperty(result, key, fallbackVal);
1306
- return;
1307
- }
1308
- try {
1309
- let decryptedVal;
1310
- try {
1311
- decryptedVal = await this.decryptValue(env, boundOptions);
1312
- } catch (err) {
1313
- const canFallback = hasBoundAad && (!options?.aadFields || options.aadFields.length === 0) && typeof options?.aad !== "function";
1314
- if (canFallback && isAuthOrDecryptionError(err)) {
1315
- decryptedVal = await this.decryptValue(env, baseOptions);
1316
- } else {
1317
- throw err;
1318
- }
1319
- }
1320
- safeSetProperty(result, key, decryptedVal);
1321
- } catch (err) {
1322
- const fallbackVal = this._handleDecryptError(err, result, key, env, void 0, boundOptions);
1323
- safeSetProperty(result, key, fallbackVal);
1324
- }
1325
- })
1326
- );
1327
- return result;
1328
- }
1329
- return payload;
1330
- }
1331
- }
1332
-
1333
- class CKeyManager {
1334
- // ── Binary & Encoding Utilities ──────────────────────────────────────────
1335
- /**
1336
- * Generates a cryptographically secure random byte buffer of the specified length.
1337
- */
1338
- static randomBytes(length) {
1339
- const bytes = new Uint8Array(length);
1340
- return getRandomValues(bytes);
1341
- }
1342
- /**
1343
- * Converts an ArrayBuffer or Uint8Array into a Base64-encoded string.
1344
- */
1345
- static toBase64(buffer) {
1346
- return toBase64(buffer);
1234
+ throw new Error(
1235
+ `[CKeyManager] Unsupported user key algorithm for unwrapHMACKeyForUser: '${userPrivateKey.algorithm.name}'. Expected 'RSA-OAEP'.`
1236
+ );
1347
1237
  }
1348
1238
  /**
1349
- * Converts a Base64 or Base64URL-encoded string back into a Uint8Array.
1350
- * Normalizes URL-safe characters and padding, strictly validating against the Base64 alphabet.
1239
+ * Atomically re-wraps an Account Data Encryption Key (ADEK) for a new recipient
1240
+ * without exposing the raw extractable symmetric key to caller application code.
1241
+ *
1242
+ * @param wrappedKeyBase64 - The existing ADEK envelope wrapped with current user's public key
1243
+ * @param myPrivateKey - Current user's RSA-OAEP private key
1244
+ * @param recipientPublicKey - New recipient's RSA-OAEP public key
1245
+ * @returns Base64-encoded wrapped key envelope for the recipient
1351
1246
  */
1352
- static fromBase64(base64) {
1353
- if (typeof base64 !== "string") {
1354
- throw new Error("[CKeyManager] Invalid base64 input: expected string");
1355
- }
1356
- const cleaned = base64.replace(/\s+/g, "");
1357
- if (!cleaned) return new Uint8Array(0);
1358
- const normalized = cleaned.replace(/-/g, "+").replace(/_/g, "/");
1359
- const padded = normalized.padEnd(normalized.length + (4 - normalized.length % 4) % 4, "=");
1360
- if (!/^[A-Za-z0-9+/]+={0,2}$/.test(padded)) {
1361
- throw new Error("[CKeyManager] Invalid base64 string");
1247
+ static async rewrapAccountKeyForUser(wrappedKeyBase64, myPrivateKey, recipientPublicKey) {
1248
+ const subtle = getSubtleCrypto();
1249
+ if (!myPrivateKey || !myPrivateKey.algorithm || myPrivateKey.algorithm.name !== "RSA-OAEP" || !recipientPublicKey || !recipientPublicKey.algorithm || recipientPublicKey.algorithm.name !== "RSA-OAEP") {
1250
+ throw new Error(`[CKeyManager] rewrapAccountKeyForUser requires RSA-OAEP keys.`);
1362
1251
  }
1363
- if (typeof Buffer !== "undefined") {
1364
- const buf = Buffer.from(padded, "base64");
1365
- const arrayBuf = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
1366
- return new Uint8Array(arrayBuf);
1252
+ if (myPrivateKey.type === "public" || recipientPublicKey.type === "private") {
1253
+ throw new Error(
1254
+ `[CKeyManager] rewrapAccountKeyForUser requires an RSA-OAEP private key for decryption and an RSA-OAEP public key for encryption.`
1255
+ );
1367
1256
  }
1368
- const binary = atob(padded);
1369
- const bytes = new Uint8Array(binary.length);
1370
- for (let i = 0; i < binary.length; i++) {
1371
- bytes[i] = binary.charCodeAt(i);
1257
+ const wrappedBytes = CKeyManager.fromBase64(wrappedKeyBase64);
1258
+ let rawKeyBuffer;
1259
+ try {
1260
+ rawKeyBuffer = await subtle.decrypt(
1261
+ { name: "RSA-OAEP" },
1262
+ myPrivateKey,
1263
+ wrappedBytes
1264
+ );
1265
+ const rewrappedBuffer = await subtle.encrypt(
1266
+ { name: "RSA-OAEP" },
1267
+ recipientPublicKey,
1268
+ rawKeyBuffer
1269
+ );
1270
+ return CKeyManager.toBase64(rewrappedBuffer);
1271
+ } finally {
1272
+ wrappedBytes.fill(0);
1273
+ if (rawKeyBuffer) {
1274
+ new Uint8Array(rawKeyBuffer).fill(0);
1275
+ }
1372
1276
  }
1373
- return bytes;
1374
1277
  }
1278
+ // ── Key Derivation Functions (PBKDF2 & HKDF) ──────────────────────────────
1375
1279
  /**
1376
- * Deterministically resolves a salt from Uint8Array, ArrayBuffer, Base64/Base64URL string, or raw text string.
1377
- * Guarantees 100% identical salt byte resolution between Node.js and Browser environments.
1280
+ * Derives a symmetric AES-GCM CryptoKey from a passphrase using PBKDF2 or HKDF.
1281
+ * Returns the derived CryptoKey and the base64-encoded salt.
1378
1282
  */
1379
- static resolveSalt(salt) {
1380
- if (!salt) return CKeyManager.randomBytes(16);
1381
- if (ArrayBuffer.isView(salt)) {
1382
- return salt.byteLength > 0 ? new Uint8Array(salt.buffer.slice(salt.byteOffset, salt.byteOffset + salt.byteLength)) : CKeyManager.randomBytes(16);
1383
- }
1384
- if (salt instanceof ArrayBuffer) {
1385
- return salt.byteLength > 0 ? new Uint8Array(salt.slice(0)) : CKeyManager.randomBytes(16);
1283
+ static async deriveKey(passphrase, options) {
1284
+ if (!passphrase || typeof passphrase === "string" && passphrase.trim().length === 0 || typeof passphrase === "object" && "byteLength" in passphrase && passphrase.byteLength === 0) {
1285
+ throw new Error("[CKeyManager] Passphrase cannot be empty or undefined for key derivation.");
1386
1286
  }
1387
- if (typeof salt === "string") {
1388
- const trimmed = salt.trim();
1389
- if (!trimmed) return CKeyManager.randomBytes(16);
1390
- if (trimmed.startsWith("base64:")) {
1391
- return CKeyManager.fromBase64(trimmed.slice(7));
1287
+ const subtle = getSubtleCrypto();
1288
+ const saltBytes = CKeyManager.resolveSalt(options.salt);
1289
+ const keyLength = options.keyLength || 256;
1290
+ const hash = options.hash || "SHA-256";
1291
+ const extractable = options.extractable === true;
1292
+ const rawKeyBytes = typeof passphrase === "string" ? textEncoder$1.encode(passphrase) : passphrase instanceof Uint8Array ? passphrase : new Uint8Array(passphrase);
1293
+ try {
1294
+ if (options.kdf === "pbkdf2") {
1295
+ const baseKey = await subtle.importKey(
1296
+ "raw",
1297
+ rawKeyBytes,
1298
+ "PBKDF2",
1299
+ false,
1300
+ ["deriveKey"]
1301
+ );
1302
+ const derivedKey = await subtle.deriveKey(
1303
+ {
1304
+ name: "PBKDF2",
1305
+ salt: saltBytes,
1306
+ iterations: options.iterations || 31e4,
1307
+ hash
1308
+ },
1309
+ baseKey,
1310
+ { name: "AES-GCM", length: keyLength },
1311
+ extractable,
1312
+ ["encrypt", "decrypt"]
1313
+ );
1314
+ return {
1315
+ key: derivedKey,
1316
+ salt: CKeyManager.toBase64(saltBytes)
1317
+ };
1318
+ }
1319
+ if (options.kdf === "hkdf") {
1320
+ const baseKey = await subtle.importKey(
1321
+ "raw",
1322
+ rawKeyBytes,
1323
+ "HKDF",
1324
+ false,
1325
+ ["deriveKey"]
1326
+ );
1327
+ const infoBytes = resolveInfoBytes(options.info, "katlux-derived-aes-key");
1328
+ const derivedKey = await subtle.deriveKey(
1329
+ {
1330
+ name: "HKDF",
1331
+ salt: saltBytes,
1332
+ info: infoBytes,
1333
+ hash
1334
+ },
1335
+ baseKey,
1336
+ { name: "AES-GCM", length: keyLength },
1337
+ extractable,
1338
+ ["encrypt", "decrypt"]
1339
+ );
1340
+ return {
1341
+ key: derivedKey,
1342
+ salt: CKeyManager.toBase64(saltBytes)
1343
+ };
1392
1344
  }
1393
- if (trimmed.endsWith("=") && /^[A-Za-z0-9+/]+={1,2}$/.test(trimmed)) {
1394
- try {
1395
- const decoded = CKeyManager.fromBase64(trimmed);
1396
- if (decoded.byteLength > 0) return decoded;
1397
- } catch {
1398
- }
1345
+ throw new Error(`[CKeyManager] Unsupported KDF strategy: ${options.kdf}`);
1346
+ } finally {
1347
+ if (typeof passphrase === "string") {
1348
+ rawKeyBytes.fill(0);
1399
1349
  }
1400
- const encoded = textEncoder$1.encode(trimmed);
1401
- return encoded.byteLength > 0 ? encoded : CKeyManager.randomBytes(16);
1402
1350
  }
1403
- return CKeyManager.randomBytes(16);
1404
1351
  }
1405
- // ── Symmetric Key Management ─────────────────────────────────────────────
1406
1352
  /**
1407
- * Generates a random AES CryptoKey suitable for AES-GCM or AES-CBC.
1408
- *
1409
- * E2EE Threat Model Note: Focuses on protecting GDPR/tenant data against database and
1410
- * server-side leaks. Keys remain extractable by default to enable role-based key sharing,
1411
- * wrapping, and transfer between authorized client sessions in multi-user SaaS apps.
1412
- * Client-side malware/trojan protection is out of scope.
1353
+ * Derives an AES-GCM CryptoKey from a high-entropy secret (e.g. an ECDH shared secret key)
1354
+ * using HKDF.
1413
1355
  */
1414
- static async generateAESKey(length = 256, algorithm = "AES-GCM", extractable = true) {
1356
+ static async deriveKeyFromSecret(secretKey, options = {}) {
1415
1357
  const subtle = getSubtleCrypto();
1416
- const usages = algorithm === "AES-KW" ? ["wrapKey", "unwrapKey"] : ["encrypt", "decrypt"];
1417
- return await subtle.generateKey(
1418
- { name: algorithm, length },
1358
+ let saltBytes;
1359
+ if (options.salt !== void 0) {
1360
+ saltBytes = CKeyManager.resolveSalt(options.salt);
1361
+ } else {
1362
+ saltBytes = new Uint8Array(16);
1363
+ }
1364
+ const infoBytes = resolveInfoBytes(options.info, "katlux-ecdh-aes-gcm");
1365
+ const hash = options.hash || "SHA-256";
1366
+ const keyLength = options.keyLength || 256;
1367
+ const extractable = options.extractable === true;
1368
+ return await subtle.deriveKey(
1369
+ {
1370
+ name: "HKDF",
1371
+ salt: saltBytes,
1372
+ info: infoBytes,
1373
+ hash
1374
+ },
1375
+ secretKey,
1376
+ { name: "AES-GCM", length: keyLength },
1419
1377
  extractable,
1420
- usages
1378
+ ["encrypt", "decrypt"]
1421
1379
  );
1422
1380
  }
1423
1381
  /**
1424
- * Exports an AES CryptoKey as a raw base64 string.
1382
+ * Derives two domain-separated, cryptographically isolated keys from a user password and salt:
1383
+ * 1. `authKey`: For client authentication, SRP, or server challenge proofs.
1384
+ * 2. `encryptionKey`: For AES-GCM local and field-level data encryption.
1385
+ *
1386
+ * Stage 1 (Password Stretching): PBKDF2-HMAC-SHA256 stretches low-entropy password into master secret.
1387
+ * Stage 2 (Domain Expansion): HKDF-Expand derives subkeys using domain-separated context info tags.
1388
+ * Immediate memory zeroization of intermediate master secret buffer is guaranteed.
1389
+ *
1390
+ * @param password - Master user password or passphrase
1391
+ * @param salt - Salt (or omitted to generate a secure random 16-byte salt)
1392
+ * @param options - PBKDF2 and HKDF configuration options
1425
1393
  */
1426
- static async exportAESKey(key) {
1427
- const subtle = getSubtleCrypto();
1428
- const raw = await subtle.exportKey("raw", key);
1429
- try {
1430
- return CKeyManager.toBase64(raw);
1431
- } finally {
1432
- new Uint8Array(raw).fill(0);
1394
+ static async deriveAuthAndEncryptionKeys(password, salt, options = {}) {
1395
+ if (!password || typeof password === "string" && password.trim().length === 0 || typeof password === "object" && "byteLength" in password && password.byteLength === 0) {
1396
+ throw new Error("[CKeyManager] Password cannot be empty or undefined for key derivation.");
1433
1397
  }
1434
- }
1435
- /**
1436
- * Imports an AES CryptoKey from a raw base64 string.
1437
- */
1438
- static async importAESKey(base64, algorithm = "AES-GCM", extractable = false) {
1439
1398
  const subtle = getSubtleCrypto();
1440
- const rawBytes = CKeyManager.fromBase64(base64);
1441
- if (rawBytes.length !== 16 && rawBytes.length !== 24 && rawBytes.length !== 32) {
1442
- rawBytes.fill(0);
1443
- throw new Error(`[CKeyManager] Invalid AES key length: expected 16, 24, or 32 bytes (128, 192, or 256 bits), got ${rawBytes.length} bytes.`);
1399
+ let saltBytes;
1400
+ if (salt !== void 0) {
1401
+ saltBytes = CKeyManager.resolveSalt(salt);
1402
+ } else {
1403
+ saltBytes = CKeyManager.randomBytes(16);
1444
1404
  }
1445
- const usages = algorithm === "AES-KW" ? ["wrapKey", "unwrapKey"] : ["encrypt", "decrypt"];
1405
+ const saltBase64 = CKeyManager.toBase64(saltBytes);
1406
+ const iterations = options.iterations || 31e4;
1407
+ const hash = options.hash || "SHA-256";
1408
+ const authKeyLength = options.authKeyLength || 256;
1409
+ const encryptionKeyLength = options.encryptionKeyLength || 256;
1410
+ const authAlgorithm = options.authAlgorithm || "HMAC";
1411
+ const authInfo = resolveInfoBytes(options.authInfo, "katlux-auth-key-v1");
1412
+ const encryptionInfo = resolveInfoBytes(options.encryptionInfo, "katlux-encryption-key-v1");
1413
+ const extractable = options.extractable === true;
1414
+ const rawPasswordBytes = typeof password === "string" ? textEncoder$1.encode(password) : password instanceof Uint8Array ? new Uint8Array(password.buffer, password.byteOffset, password.byteLength) : new Uint8Array(password);
1415
+ let masterBits;
1446
1416
  try {
1447
- return await subtle.importKey(
1417
+ const baseKey = await subtle.importKey(
1448
1418
  "raw",
1449
- rawBytes,
1450
- { name: algorithm },
1419
+ rawPasswordBytes,
1420
+ "PBKDF2",
1421
+ false,
1422
+ ["deriveBits"]
1423
+ );
1424
+ const bitsLength = hash === "SHA-512" ? 512 : 256;
1425
+ masterBits = await subtle.deriveBits(
1426
+ {
1427
+ name: "PBKDF2",
1428
+ salt: saltBytes,
1429
+ iterations,
1430
+ hash
1431
+ },
1432
+ baseKey,
1433
+ bitsLength
1434
+ );
1435
+ const hkdfKey = await subtle.importKey(
1436
+ "raw",
1437
+ masterBits,
1438
+ "HKDF",
1439
+ false,
1440
+ ["deriveKey"]
1441
+ );
1442
+ const authDerivedAlgorithm = authAlgorithm === "AES-GCM" ? { name: "AES-GCM", length: authKeyLength } : { name: "HMAC", hash: { name: hash }, length: authKeyLength };
1443
+ const authUsages = authAlgorithm === "AES-GCM" ? ["encrypt", "decrypt"] : ["sign", "verify"];
1444
+ const authKey = await subtle.deriveKey(
1445
+ {
1446
+ name: "HKDF",
1447
+ hash,
1448
+ salt: new Uint8Array(0),
1449
+ info: authInfo
1450
+ },
1451
+ hkdfKey,
1452
+ authDerivedAlgorithm,
1451
1453
  extractable,
1452
- usages
1454
+ authUsages
1455
+ );
1456
+ const encryptionKey = await subtle.deriveKey(
1457
+ {
1458
+ name: "HKDF",
1459
+ hash,
1460
+ salt: new Uint8Array(0),
1461
+ info: encryptionInfo
1462
+ },
1463
+ hkdfKey,
1464
+ { name: "AES-GCM", length: encryptionKeyLength },
1465
+ extractable,
1466
+ ["encrypt", "decrypt"]
1453
1467
  );
1468
+ return {
1469
+ authKey,
1470
+ encryptionKey,
1471
+ salt: saltBase64
1472
+ };
1454
1473
  } finally {
1455
- rawBytes.fill(0);
1474
+ if (typeof password === "string") {
1475
+ rawPasswordBytes.fill(0);
1476
+ }
1477
+ if (masterBits) {
1478
+ new Uint8Array(masterBits).fill(0);
1479
+ }
1456
1480
  }
1457
1481
  }
1458
- // ── Asymmetric Key Pair Management ───────────────────────────────────────
1482
+ // ── Digital Signatures, Fingerprints & Access Grants ─────────────────────
1459
1483
  /**
1460
- * Generates an RSA-OAEP public/private key pair (2048 or 4096 bits).
1484
+ * Generates an ECDSA P-256 signing key pair.
1461
1485
  */
1462
- static async generateRSAKeyPair(bits = 4096, hash = "SHA-256", extractable = true) {
1486
+ static async generateSigningKeyPair(curve = "P-256", extractable = true) {
1463
1487
  const subtle = getSubtleCrypto();
1464
1488
  const pair = await subtle.generateKey(
1465
1489
  {
1466
- name: "RSA-OAEP",
1467
- modulusLength: bits,
1468
- publicExponent: new Uint8Array([1, 0, 1]),
1469
- hash
1490
+ name: "ECDSA",
1491
+ namedCurve: curve
1470
1492
  },
1471
1493
  extractable,
1472
- ["encrypt", "decrypt", "wrapKey", "unwrapKey"]
1494
+ ["sign", "verify"]
1473
1495
  );
1474
1496
  return {
1475
1497
  publicKey: pair.publicKey,
@@ -1477,720 +1499,900 @@ class CKeyManager {
1477
1499
  };
1478
1500
  }
1479
1501
  /**
1480
- * Generates an ECDH public/private key pair for elliptic curve Diffie-Hellman.
1502
+ * Generates an ECDSA signing key pair. Alias for generateSigningKeyPair.
1481
1503
  */
1482
- static async generateECDHKeyPair(curve = "P-256", extractable = true) {
1504
+ static async generateECDSAKeyPair(curve = "P-256", extractable = true) {
1505
+ return CKeyManager.generateSigningKeyPair(curve, extractable);
1506
+ }
1507
+ /**
1508
+ * Generates a deterministic SHA-256 fingerprint for a public key (SPKI or raw).
1509
+ * Formatted as `SHA256:<base64url>`.
1510
+ */
1511
+ static async getKeyFingerprint(publicKey) {
1483
1512
  const subtle = getSubtleCrypto();
1484
- const pair = await subtle.generateKey(
1513
+ let rawBytes;
1514
+ if (typeof publicKey === "object" && publicKey !== null && "algorithm" in publicKey) {
1515
+ const cryptoKey = publicKey;
1516
+ rawBytes = await subtle.exportKey("spki", cryptoKey);
1517
+ } else if (publicKey instanceof Uint8Array) {
1518
+ rawBytes = publicKey;
1519
+ } else if (publicKey instanceof ArrayBuffer) {
1520
+ rawBytes = publicKey;
1521
+ } else if (typeof publicKey === "string") {
1522
+ const trimmed = publicKey.trim();
1523
+ if (trimmed.startsWith("-----BEGIN")) {
1524
+ const b642 = trimmed.replace(/-----[^-]+-----/g, "").replace(/\s+/g, "");
1525
+ rawBytes = CKeyManager.fromBase64(b642);
1526
+ } else {
1527
+ try {
1528
+ rawBytes = CKeyManager.fromBase64(trimmed);
1529
+ } catch {
1530
+ rawBytes = textEncoder$1.encode(trimmed);
1531
+ }
1532
+ }
1533
+ } else {
1534
+ throw new Error(
1535
+ "[CKeyManager] Invalid key input for getKeyFingerprint: expected CryptoKey, Uint8Array, ArrayBuffer, or string."
1536
+ );
1537
+ }
1538
+ const hashBuffer = await subtle.digest("SHA-256", rawBytes);
1539
+ const b64 = CKeyManager.toBase64(hashBuffer);
1540
+ const b64url = b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1541
+ return `SHA256:${b64url}`;
1542
+ }
1543
+ /**
1544
+ * Signs arbitrary data or JSON objects using an ECDSA P-256 private key.
1545
+ * Objects are automatically serialized via canonicalizeJson() before hashing.
1546
+ */
1547
+ static async sign(data, privateKey) {
1548
+ const subtle = getSubtleCrypto();
1549
+ if (!privateKey || !privateKey.algorithm || privateKey.algorithm.name !== "ECDSA") {
1550
+ throw new Error("[CKeyManager] sign requires an ECDSA private key.");
1551
+ }
1552
+ let dataBytes;
1553
+ if (data instanceof Uint8Array) {
1554
+ dataBytes = data;
1555
+ } else if (ArrayBuffer.isView(data)) {
1556
+ dataBytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
1557
+ } else if (data instanceof ArrayBuffer) {
1558
+ dataBytes = new Uint8Array(data);
1559
+ } else if (typeof data === "string") {
1560
+ dataBytes = textEncoder$1.encode(data);
1561
+ } else {
1562
+ const canonical = canonicalizeJson(data);
1563
+ dataBytes = textEncoder$1.encode(canonical);
1564
+ }
1565
+ const namedCurve = privateKey.algorithm.namedCurve || "P-256";
1566
+ const hashName = namedCurve === "P-384" ? "SHA-384" : namedCurve === "P-521" ? "SHA-512" : "SHA-256";
1567
+ const signatureBuffer = await subtle.sign(
1485
1568
  {
1486
- name: "ECDH",
1487
- namedCurve: curve
1569
+ name: "ECDSA",
1570
+ hash: { name: hashName }
1488
1571
  },
1489
- extractable,
1490
- ["deriveKey", "deriveBits"]
1572
+ privateKey,
1573
+ dataBytes
1491
1574
  );
1492
- return {
1493
- publicKey: pair.publicKey,
1494
- privateKey: pair.privateKey
1495
- };
1575
+ return CKeyManager.toBase64(signatureBuffer);
1496
1576
  }
1497
1577
  /**
1498
- * Exports a public CryptoKey to a portable JsonWebKey (JWK).
1578
+ * Verifies an ECDSA P-256 signature against original data or JSON object.
1499
1579
  */
1500
- static async exportPublicKey(key) {
1580
+ static async verify(data, signature, publicKey) {
1501
1581
  const subtle = getSubtleCrypto();
1502
- return await subtle.exportKey("jwk", key);
1582
+ if (!publicKey || !publicKey.algorithm || publicKey.algorithm.name !== "ECDSA") {
1583
+ throw new Error("[CKeyManager] verify requires an ECDSA public key.");
1584
+ }
1585
+ if (typeof signature !== "string" || !signature.trim()) {
1586
+ return false;
1587
+ }
1588
+ try {
1589
+ const signatureBytes = CKeyManager.fromBase64(signature);
1590
+ let dataBytes;
1591
+ if (data instanceof Uint8Array) {
1592
+ dataBytes = data;
1593
+ } else if (ArrayBuffer.isView(data)) {
1594
+ dataBytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
1595
+ } else if (data instanceof ArrayBuffer) {
1596
+ dataBytes = new Uint8Array(data);
1597
+ } else if (typeof data === "string") {
1598
+ dataBytes = textEncoder$1.encode(data);
1599
+ } else {
1600
+ const canonical = canonicalizeJson(data);
1601
+ dataBytes = textEncoder$1.encode(canonical);
1602
+ }
1603
+ const namedCurve = publicKey.algorithm.namedCurve || "P-256";
1604
+ const hashName = namedCurve === "P-384" ? "SHA-384" : namedCurve === "P-521" ? "SHA-512" : "SHA-256";
1605
+ return await subtle.verify(
1606
+ {
1607
+ name: "ECDSA",
1608
+ hash: { name: hashName }
1609
+ },
1610
+ publicKey,
1611
+ signatureBytes,
1612
+ dataBytes
1613
+ );
1614
+ } catch {
1615
+ return false;
1616
+ }
1503
1617
  }
1504
1618
  /**
1505
- * Exports a private CryptoKey to a portable JsonWebKey (JWK).
1619
+ * Computes a deterministic HMAC-SHA256 blind index hash for an arbitrary term using the supplied CryptoKey.
1620
+ * Performs Unicode NFKC normalization, trimming, and lowercasing before computing the signature.
1621
+ *
1622
+ * @param value - The input string, number, or object to compute the blind index for.
1623
+ * @param hmacKey - CryptoKey configured for HMAC signing.
1624
+ * @returns Lowercase hexadecimal HMAC-SHA256 blind index hash.
1506
1625
  */
1507
- static async exportPrivateKey(key) {
1626
+ static async computeBlindIndex(value, hmacKey) {
1627
+ if (value === null || value === void 0) return "";
1628
+ const normalized = String(value).normalize("NFKC").trim().toLowerCase();
1629
+ if (!normalized) return "";
1630
+ if (!hmacKey || !hmacKey.algorithm || hmacKey.algorithm.name !== "HMAC") {
1631
+ throw new Error("[CKeyManager] computeBlindIndex requires an HMAC CryptoKey.");
1632
+ }
1508
1633
  const subtle = getSubtleCrypto();
1509
- return await subtle.exportKey("jwk", key);
1634
+ const signature = await subtle.sign("HMAC", hmacKey, textEncoder$1.encode(normalized));
1635
+ return Array.from(new Uint8Array(signature)).map((b) => b.toString(16).padStart(2, "0")).join("");
1510
1636
  }
1511
1637
  /**
1512
- * Imports a public key from a JsonWebKey (JWK).
1638
+ * Signs a structured access grant, embedding signature and granter key fingerprint.
1513
1639
  */
1514
- static async importPublicKey(jwk, algorithm) {
1515
- const subtle = getSubtleCrypto();
1516
- let importAlgorithm = algorithm;
1517
- let usages;
1518
- const isEC = jwk.kty === "EC" || typeof algorithm === "object" && algorithm?.name === "ECDH" || algorithm === "ECDH";
1519
- if (isEC) {
1520
- usages = [];
1521
- if (!importAlgorithm || importAlgorithm === "ECDH") {
1522
- importAlgorithm = { name: "ECDH", namedCurve: jwk.crv || "P-256" };
1523
- }
1640
+ static async signGrant(grant, granterPrivateKey, granterPublicKey) {
1641
+ if (!grant || typeof grant !== "object") {
1642
+ throw new Error("[CKeyManager] signGrant requires a valid grant object.");
1643
+ }
1644
+ let fingerprint;
1645
+ if (granterPublicKey) {
1646
+ fingerprint = await CKeyManager.getKeyFingerprint(granterPublicKey);
1524
1647
  } else {
1525
- usages = ["encrypt", "wrapKey"];
1526
- if (!importAlgorithm || importAlgorithm === "RSA-OAEP") {
1527
- if (jwk.kty === "RSA") {
1528
- const hash = jwk.alg === "RSA-OAEP-512" ? "SHA-512" : "SHA-256";
1529
- importAlgorithm = { name: "RSA-OAEP", hash };
1530
- } else {
1531
- throw new Error(`[CKeyManager] Unsupported JWK kty: ${jwk.kty}`);
1532
- }
1648
+ const subtle = getSubtleCrypto();
1649
+ try {
1650
+ const jwk = await subtle.exportKey("jwk", granterPrivateKey);
1651
+ const pubJwk = {
1652
+ kty: jwk.kty,
1653
+ crv: jwk.crv,
1654
+ x: jwk.x,
1655
+ y: jwk.y,
1656
+ ext: true,
1657
+ key_ops: ["verify"]
1658
+ };
1659
+ const pubKey = await subtle.importKey(
1660
+ "jwk",
1661
+ pubJwk,
1662
+ { name: "ECDSA", namedCurve: jwk.crv || "P-256" },
1663
+ true,
1664
+ ["verify"]
1665
+ );
1666
+ fingerprint = await CKeyManager.getKeyFingerprint(pubKey);
1667
+ } catch {
1668
+ throw new Error(
1669
+ "[CKeyManager] signGrant requires granterPublicKey when granterPrivateKey is non-extractable."
1670
+ );
1533
1671
  }
1534
1672
  }
1535
- return await subtle.importKey("jwk", jwk, importAlgorithm, true, usages);
1673
+ const signature = await CKeyManager.sign(grant, granterPrivateKey);
1674
+ return {
1675
+ grant: { ...grant },
1676
+ signature,
1677
+ fingerprint
1678
+ };
1536
1679
  }
1537
1680
  /**
1538
- * Imports a private key from a JsonWebKey (JWK).
1681
+ * Verifies the authenticity and integrity of a signed access grant against granter public key.
1539
1682
  */
1540
- static async importPrivateKey(jwk, algorithm) {
1541
- const subtle = getSubtleCrypto();
1542
- let importAlgorithm = algorithm;
1543
- let usages;
1544
- const isEC = jwk.kty === "EC" || typeof algorithm === "object" && algorithm?.name === "ECDH" || algorithm === "ECDH";
1545
- if (isEC) {
1546
- usages = ["deriveKey", "deriveBits"];
1547
- if (!importAlgorithm || importAlgorithm === "ECDH") {
1548
- importAlgorithm = { name: "ECDH", namedCurve: jwk.crv || "P-256" };
1549
- }
1550
- } else {
1551
- usages = ["decrypt", "unwrapKey"];
1552
- if (!importAlgorithm || importAlgorithm === "RSA-OAEP") {
1553
- if (jwk.kty === "RSA") {
1554
- const hash = jwk.alg === "RSA-OAEP-512" ? "SHA-512" : "SHA-256";
1555
- importAlgorithm = { name: "RSA-OAEP", hash };
1556
- } else {
1557
- throw new Error(`[CKeyManager] Unsupported JWK kty: ${jwk.kty}`);
1558
- }
1683
+ static async verifyGrant(signedGrant, granterPublicKey, options) {
1684
+ if (!signedGrant || !signedGrant.grant || !signedGrant.signature || !signedGrant.fingerprint || !granterPublicKey) {
1685
+ return false;
1686
+ }
1687
+ const expectedFingerprint = await CKeyManager.getKeyFingerprint(granterPublicKey);
1688
+ if (signedGrant.fingerprint !== expectedFingerprint) {
1689
+ return false;
1690
+ }
1691
+ if (options?.checkExpiry && signedGrant.grant.expiresAt !== void 0) {
1692
+ const now = options.now ?? Date.now();
1693
+ if (now > signedGrant.grant.expiresAt) {
1694
+ return false;
1559
1695
  }
1560
1696
  }
1561
- return await subtle.importKey("jwk", jwk, importAlgorithm, true, usages);
1697
+ return await CKeyManager.verify(signedGrant.grant, signedGrant.signature, granterPublicKey);
1562
1698
  }
1563
- /**
1564
- * Imports a public key from a PEM (SPKI) or base64 SPKI string.
1565
- */
1566
- static async importPublicKeyFromPEM(pem, algorithm = "RSA-OAEP", curve = "P-256", hash = "SHA-256") {
1567
- const subtle = getSubtleCrypto();
1568
- const cleaned = pem.replace(/-----BEGIN[ A-Z0-9_-]+-----/g, "").replace(/-----END[ A-Z0-9_-]+-----/g, "").replace(/\s+/g, "");
1569
- const spkiBytes = CKeyManager.fromBase64(cleaned);
1570
- let importAlgorithm;
1571
- let usages;
1572
- if (algorithm === "ECDH") {
1573
- importAlgorithm = { name: "ECDH", namedCurve: curve };
1574
- usages = [];
1575
- } else {
1576
- importAlgorithm = { name: "RSA-OAEP", hash };
1577
- usages = ["encrypt", "wrapKey"];
1699
+ }
1700
+
1701
+ class AEncryptionProvider {
1702
+ defaultKey;
1703
+ _hmacKey;
1704
+ keys;
1705
+ activeKid;
1706
+ allowedKids;
1707
+ constructor(key, options) {
1708
+ if (key !== void 0) {
1709
+ this.defaultKey = key;
1578
1710
  }
1579
- try {
1580
- return await subtle.importKey("spki", spkiBytes, importAlgorithm, true, usages);
1581
- } finally {
1582
- spkiBytes.fill(0);
1711
+ if (options?.keys) {
1712
+ this.keys = options.keys;
1583
1713
  }
1584
- }
1585
- /**
1586
- * Imports a private key from a PEM (PKCS#8) or base64 PKCS#8 string.
1587
- */
1588
- static async importPrivateKeyFromPEM(pem, algorithm = "RSA-OAEP", curve = "P-256", hash = "SHA-256") {
1589
- if (/BEGIN\s+(RSA|EC)\s+PRIVATE\s+KEY/i.test(pem)) {
1590
- throw new Error(
1591
- `[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.`
1592
- );
1714
+ if (options?.activeKid) {
1715
+ this.activeKid = options.activeKid;
1593
1716
  }
1594
- const subtle = getSubtleCrypto();
1595
- const cleaned = pem.replace(/-----BEGIN[ A-Z0-9_-]+-----/g, "").replace(/-----END[ A-Z0-9_-]+-----/g, "").replace(/\s+/g, "");
1596
- const pkcs8Bytes = CKeyManager.fromBase64(cleaned);
1597
- let importAlgorithm;
1598
- let usages;
1599
- if (algorithm === "ECDH") {
1600
- importAlgorithm = { name: "ECDH", namedCurve: curve };
1601
- usages = ["deriveKey", "deriveBits"];
1602
- } else {
1603
- importAlgorithm = { name: "RSA-OAEP", hash };
1604
- usages = ["decrypt", "unwrapKey"];
1717
+ if (options?.allowedKids) {
1718
+ this.allowedKids = options.allowedKids;
1605
1719
  }
1606
- try {
1607
- return await subtle.importKey("pkcs8", pkcs8Bytes, importAlgorithm, true, usages);
1608
- } finally {
1609
- pkcs8Bytes.fill(0);
1720
+ if (options?.hmacKey) {
1721
+ this._hmacKey = options.hmacKey;
1610
1722
  }
1611
1723
  }
1612
1724
  /**
1613
- * Exports a public CryptoKey to a standard PEM (SPKI) string.
1725
+ * Sets or updates the default secret key, CryptoKey, or async key provider function.
1614
1726
  */
1615
- static async exportPublicKeyToPEM(key) {
1616
- const subtle = getSubtleCrypto();
1617
- const spki = await subtle.exportKey("spki", key);
1618
- let b64;
1619
- try {
1620
- b64 = CKeyManager.toBase64(spki);
1621
- } finally {
1622
- new Uint8Array(spki).fill(0);
1623
- }
1624
- const lines = b64.match(/.{1,64}/g) || [b64];
1625
- return `-----BEGIN PUBLIC KEY-----
1626
- ${lines.join("\n")}
1627
- -----END PUBLIC KEY-----`;
1727
+ setDefaultKey(key) {
1728
+ this.defaultKey = key || void 0;
1628
1729
  }
1629
1730
  /**
1630
- * Exports a private CryptoKey to a standard PEM (PKCS#8) string.
1731
+ * Retrieves the currently configured default key or key provider callback.
1631
1732
  */
1632
- static async exportPrivateKeyToPEM(key) {
1633
- const subtle = getSubtleCrypto();
1634
- const pkcs8 = await subtle.exportKey("pkcs8", key);
1635
- let b64;
1636
- try {
1637
- b64 = CKeyManager.toBase64(pkcs8);
1638
- } finally {
1639
- new Uint8Array(pkcs8).fill(0);
1640
- }
1641
- const lines = b64.match(/.{1,64}/g) || [b64];
1642
- return `-----BEGIN PRIVATE KEY-----
1643
- ${lines.join("\n")}
1644
- -----END PRIVATE KEY-----`;
1733
+ getDefaultKey() {
1734
+ return this.defaultKey;
1735
+ }
1736
+ /**
1737
+ * Configures a keyring dictionary of keys with an optional active key identifier.
1738
+ */
1739
+ setKeyring(keys, activeKid) {
1740
+ this.keys = keys;
1741
+ if (activeKid !== void 0) {
1742
+ this.activeKid = activeKid;
1743
+ }
1645
1744
  }
1646
- // ── Key Wrapping & Unwrapping ────────────────────────────────────────────
1647
1745
  /**
1648
- * Wraps a target CryptoKey (e.g. an Account Symmetric Key) using a wrapping key
1649
- * (AES-KW or RSA-OAEP).
1746
+ * Retrieves the currently configured keyring dictionary.
1650
1747
  */
1651
- static async wrapKey(keyToWrap, wrappingKey, format = "raw") {
1652
- const subtle = getSubtleCrypto();
1653
- let wrapAlgorithm;
1654
- if (wrappingKey.algorithm.name === "RSA-OAEP") {
1655
- wrapAlgorithm = { name: "RSA-OAEP" };
1656
- } else if (wrappingKey.algorithm.name === "AES-KW") {
1657
- wrapAlgorithm = "AES-KW";
1658
- } else {
1659
- wrapAlgorithm = "AES-KW";
1660
- }
1661
- const wrappedBuffer = await subtle.wrapKey(format, keyToWrap, wrappingKey, wrapAlgorithm);
1662
- return CKeyManager.toBase64(wrappedBuffer);
1748
+ getKeyring() {
1749
+ return this.keys;
1663
1750
  }
1664
1751
  /**
1665
- * Unwraps a base64-encoded wrapped key using an unwrapping key (AES-KW or RSA-OAEP).
1752
+ * Sets the active Key Identifier (kid) to use for outbound encryption.
1666
1753
  */
1667
- static async unwrapKey(wrappedBase64, unwrappingKey, unwrappedKeyAlgorithm = { name: "AES-GCM", length: 256 }, extractable = true, keyUsages = ["encrypt", "decrypt"], format = "raw") {
1668
- const subtle = getSubtleCrypto();
1669
- const wrappedBytes = CKeyManager.fromBase64(wrappedBase64);
1670
- let unwrapAlgorithm;
1671
- if (unwrappingKey.algorithm.name === "RSA-OAEP") {
1672
- unwrapAlgorithm = { name: "RSA-OAEP" };
1673
- } else {
1674
- unwrapAlgorithm = "AES-KW";
1675
- }
1676
- try {
1677
- return await subtle.unwrapKey(
1678
- format,
1679
- wrappedBytes,
1680
- unwrappingKey,
1681
- unwrapAlgorithm,
1682
- unwrappedKeyAlgorithm,
1683
- extractable,
1684
- keyUsages
1685
- );
1686
- } finally {
1687
- wrappedBytes.fill(0);
1688
- }
1754
+ setActiveKid(kid) {
1755
+ this.activeKid = kid;
1689
1756
  }
1690
1757
  /**
1691
- * Generates a high-entropy symmetric AES-GCM Account Data Encryption Key (ADEK).
1692
- * Designed for multi-user table and field-level encryption.
1758
+ * Retrieves the active Key Identifier (kid).
1693
1759
  */
1694
- static async generateAccountDataKey(length = 256, extractable = false) {
1695
- const subtle = getSubtleCrypto();
1696
- return await subtle.generateKey(
1697
- { name: "AES-GCM", length },
1698
- extractable,
1699
- ["encrypt", "decrypt"]
1700
- );
1760
+ getActiveKid() {
1761
+ return this.activeKid;
1701
1762
  }
1702
1763
  /**
1703
- * Wraps an Account Data Encryption Key (ADEK) for a specific user using the user's public key (RSA-OAEP).
1704
- * Returns a Base64-encoded wrapped key string suitable for storage in role-map / access tables.
1764
+ * Sets or updates the dedicated HMAC key or key provider callback for blind indexing.
1705
1765
  */
1706
- static async wrapAccountKeyForUser(adek, userPublicKey) {
1707
- const subtle = getSubtleCrypto();
1708
- if (userPublicKey.algorithm.name === "RSA-OAEP") {
1709
- const rawKey = await subtle.exportKey("raw", adek);
1710
- try {
1711
- const wrappedBuffer = await subtle.encrypt(
1712
- { name: "RSA-OAEP" },
1713
- userPublicKey,
1714
- rawKey
1715
- );
1716
- return CKeyManager.toBase64(wrappedBuffer);
1717
- } finally {
1718
- new Uint8Array(rawKey).fill(0);
1719
- }
1720
- }
1721
- throw new Error(
1722
- `[CKeyManager] Unsupported user key algorithm for wrapAccountKeyForUser: '${userPublicKey.algorithm.name}'. Expected 'RSA-OAEP'.`
1723
- );
1766
+ setHmacKey(key) {
1767
+ this._hmacKey = key || void 0;
1724
1768
  }
1725
1769
  /**
1726
- * Unwraps a Base64-encoded Account Data Encryption Key (ADEK) using the user's private key (RSA-OAEP).
1727
- * Returns the restored AES-GCM CryptoKey ready for use in CWebCryptoEncryptionProvider.
1770
+ * Retrieves the currently configured HMAC key or key provider callback.
1728
1771
  */
1729
- static async unwrapAccountKeyForUser(wrappedKeyBase64, userPrivateKey, unwrappedKeyAlgorithm = { name: "AES-GCM", length: 256 }, extractable = false) {
1730
- const subtle = getSubtleCrypto();
1731
- if (userPrivateKey.algorithm.name === "RSA-OAEP") {
1732
- const wrappedBytes = CKeyManager.fromBase64(wrappedKeyBase64);
1733
- let unwrappedRawKey;
1734
- try {
1735
- unwrappedRawKey = await subtle.decrypt(
1736
- { name: "RSA-OAEP" },
1737
- userPrivateKey,
1738
- wrappedBytes
1739
- );
1740
- } finally {
1741
- wrappedBytes.fill(0);
1742
- }
1743
- try {
1744
- return await subtle.importKey(
1745
- "raw",
1746
- unwrappedRawKey,
1747
- unwrappedKeyAlgorithm,
1748
- extractable,
1749
- ["encrypt", "decrypt"]
1750
- );
1751
- } finally {
1752
- new Uint8Array(unwrappedRawKey).fill(0);
1753
- }
1772
+ getHmacKey() {
1773
+ return this._hmacKey;
1774
+ }
1775
+ /**
1776
+ * Computes a deterministic HMAC-SHA256 blind index hash for an arbitrary term.
1777
+ * If no hmacKey is explicitly provided, uses the provider's configured hmacKey or default key.
1778
+ */
1779
+ async computeBlindIndex(value, hmacKey) {
1780
+ if (value === null || value === void 0) return "";
1781
+ const keyMaterial = hmacKey || this._hmacKey || this.defaultKey;
1782
+ if (!keyMaterial) {
1783
+ throw new Error("[AEncryptionProvider] computeBlindIndex requires an HMAC key. Configure hmacKey or pass a key directly.");
1754
1784
  }
1755
- throw new Error(
1756
- `[CKeyManager] Unsupported user key algorithm for unwrapAccountKeyForUser: '${userPrivateKey.algorithm.name}'. Expected 'RSA-OAEP'.`
1785
+ const resolvedKey = typeof keyMaterial === "function" ? await keyMaterial() : keyMaterial;
1786
+ if (resolvedKey && typeof resolvedKey !== "string" && resolvedKey.algorithm) {
1787
+ return CKeyManager.computeBlindIndex(value, resolvedKey);
1788
+ }
1789
+ const subtle = getSubtleCrypto();
1790
+ const keyBytes = typeof resolvedKey === "string" ? new TextEncoder().encode(resolvedKey) : resolvedKey instanceof Uint8Array ? resolvedKey : new Uint8Array(resolvedKey);
1791
+ const importedHmacKey = await subtle.importKey(
1792
+ "raw",
1793
+ keyBytes,
1794
+ { name: "HMAC", hash: "SHA-256" },
1795
+ false,
1796
+ ["sign"]
1757
1797
  );
1798
+ return CKeyManager.computeBlindIndex(value, importedHmacKey);
1758
1799
  }
1759
1800
  /**
1760
- * Atomically re-wraps an Account Data Encryption Key (ADEK) for a new recipient
1761
- * without exposing the raw extractable symmetric key to caller application code.
1762
- *
1763
- * @param wrappedKeyBase64 - The existing ADEK envelope wrapped with current user's public key
1764
- * @param myPrivateKey - Current user's RSA-OAEP private key
1765
- * @param recipientPublicKey - New recipient's RSA-OAEP public key
1766
- * @returns Base64-encoded wrapped key envelope for the recipient
1801
+ * Pre-derives and caches the native CryptoKey for subsequent cryptographic operations.
1802
+ * Subclasses should override this with a dedicated, non-side-effecting implementation.
1767
1803
  */
1768
- static async rewrapAccountKeyForUser(wrappedKeyBase64, myPrivateKey, recipientPublicKey) {
1769
- const subtle = getSubtleCrypto();
1770
- if (!myPrivateKey || !myPrivateKey.algorithm || myPrivateKey.algorithm.name !== "RSA-OAEP" || !recipientPublicKey || !recipientPublicKey.algorithm || recipientPublicKey.algorithm.name !== "RSA-OAEP") {
1771
- throw new Error(`[CKeyManager] rewrapAccountKeyForUser requires RSA-OAEP keys.`);
1772
- }
1773
- if (myPrivateKey.type === "public" || recipientPublicKey.type === "private") {
1804
+ async warmupKey(_options) {
1805
+ }
1806
+ /**
1807
+ * Resolves raw key from keyProvider callback or defaultKey.
1808
+ */
1809
+ async resolveRawKey(keyProvider) {
1810
+ const target = keyProvider || (this.keys && this.activeKid ? this.keys[this.activeKid] : void 0) || this.defaultKey;
1811
+ if (!target) {
1774
1812
  throw new Error(
1775
- `[CKeyManager] rewrapAccountKeyForUser requires an RSA-OAEP private key for decryption and an RSA-OAEP public key for encryption.`
1813
+ `[${this.constructor.name}] No encryption key or keyProvider was supplied. Key cannot be empty or undefined. A valid key must be supplied from your authentication or secret management system.`
1776
1814
  );
1777
1815
  }
1778
- const wrappedBytes = CKeyManager.fromBase64(wrappedKeyBase64);
1779
- let rawKeyBuffer;
1780
- try {
1781
- rawKeyBuffer = await subtle.decrypt(
1782
- { name: "RSA-OAEP" },
1783
- myPrivateKey,
1784
- wrappedBytes
1785
- );
1786
- const rewrappedBuffer = await subtle.encrypt(
1787
- { name: "RSA-OAEP" },
1788
- recipientPublicKey,
1789
- rawKeyBuffer
1816
+ const rawKey = typeof target === "function" ? await target() : target;
1817
+ if (!rawKey || typeof rawKey === "string" && rawKey.trim().length === 0 || typeof rawKey === "object" && "byteLength" in rawKey && rawKey.byteLength === 0) {
1818
+ throw new Error(
1819
+ `[${this.constructor.name}] No encryption key or keyProvider was supplied. Key provider resolved to empty or undefined key.`
1790
1820
  );
1791
- return CKeyManager.toBase64(rewrappedBuffer);
1792
- } finally {
1793
- wrappedBytes.fill(0);
1794
- if (rawKeyBuffer) {
1795
- new Uint8Array(rawKeyBuffer).fill(0);
1796
- }
1797
1821
  }
1822
+ return rawKey;
1798
1823
  }
1799
- // ── Key Derivation Functions (PBKDF2 & HKDF) ──────────────────────────────
1800
1824
  /**
1801
- * Derives a symmetric AES-GCM CryptoKey from a passphrase using PBKDF2 or HKDF.
1802
- * Returns the derived CryptoKey and the base64-encoded salt.
1825
+ * Checks whether the given payload is an encrypted envelope.
1803
1826
  */
1804
- static async deriveKey(passphrase, options) {
1805
- if (!passphrase || typeof passphrase === "string" && passphrase.trim().length === 0 || typeof passphrase === "object" && "byteLength" in passphrase && passphrase.byteLength === 0) {
1806
- throw new Error("[CKeyManager] Passphrase cannot be empty or undefined for key derivation.");
1807
- }
1808
- const subtle = getSubtleCrypto();
1809
- const saltBytes = CKeyManager.resolveSalt(options.salt);
1810
- const keyLength = options.keyLength || 256;
1811
- const hash = options.hash || "SHA-256";
1812
- const extractable = options.extractable === true;
1813
- const rawKeyBytes = typeof passphrase === "string" ? textEncoder$1.encode(passphrase) : passphrase instanceof Uint8Array ? passphrase : new Uint8Array(passphrase);
1814
- try {
1815
- if (options.kdf === "pbkdf2") {
1816
- const baseKey = await subtle.importKey(
1817
- "raw",
1818
- rawKeyBytes,
1819
- "PBKDF2",
1820
- false,
1821
- ["deriveKey"]
1822
- );
1823
- const derivedKey = await subtle.deriveKey(
1824
- {
1825
- name: "PBKDF2",
1826
- salt: saltBytes,
1827
- iterations: options.iterations || 31e4,
1828
- hash
1829
- },
1830
- baseKey,
1831
- { name: "AES-GCM", length: keyLength },
1832
- extractable,
1833
- ["encrypt", "decrypt"]
1834
- );
1835
- return {
1836
- key: derivedKey,
1837
- salt: CKeyManager.toBase64(saltBytes)
1838
- };
1839
- }
1840
- if (options.kdf === "hkdf") {
1841
- const baseKey = await subtle.importKey(
1842
- "raw",
1843
- rawKeyBytes,
1844
- "HKDF",
1845
- false,
1846
- ["deriveKey"]
1847
- );
1848
- const infoBytes = resolveInfoBytes(options.info, "katlux-derived-aes-key");
1849
- const derivedKey = await subtle.deriveKey(
1850
- {
1851
- name: "HKDF",
1852
- salt: saltBytes,
1853
- info: infoBytes,
1854
- hash
1855
- },
1856
- baseKey,
1857
- { name: "AES-GCM", length: keyLength },
1858
- extractable,
1859
- ["encrypt", "decrypt"]
1860
- );
1861
- return {
1862
- key: derivedKey,
1863
- salt: CKeyManager.toBase64(saltBytes)
1864
- };
1865
- }
1866
- throw new Error(`[CKeyManager] Unsupported KDF strategy: ${options.kdf}`);
1867
- } finally {
1868
- if (typeof passphrase === "string") {
1869
- rawKeyBytes.fill(0);
1827
+ static isEncrypted(payload) {
1828
+ if (!payload || typeof payload !== "object") return false;
1829
+ return payload.__encrypted === true && typeof payload.ciphertext === "string" && typeof payload.iv === "string";
1830
+ }
1831
+ /**
1832
+ * Checks whether the given payload is an encrypted envelope.
1833
+ */
1834
+ isEncrypted(payload) {
1835
+ return AEncryptionProvider.isEncrypted(payload);
1836
+ }
1837
+ /**
1838
+ * Clears configured keys and cached cryptographic material from memory.
1839
+ */
1840
+ clearKeys() {
1841
+ this.defaultKey = void 0;
1842
+ this.keys = void 0;
1843
+ this.activeKid = void 0;
1844
+ this.allowedKids = void 0;
1845
+ }
1846
+ /**
1847
+ * Alias for clearKeys() to conform with standard disposal patterns.
1848
+ */
1849
+ dispose() {
1850
+ this.clearKeys();
1851
+ }
1852
+ /**
1853
+ * ECMAScript Explicit Resource Management protocol (using / await using declarations).
1854
+ */
1855
+ [Symbol.dispose]() {
1856
+ this.dispose();
1857
+ }
1858
+ async [Symbol.asyncDispose]() {
1859
+ this.dispose();
1860
+ }
1861
+ /**
1862
+ * Resolves effective Authenticated Additional Data (AAD) for the operation:
1863
+ * 1. If options.bindRequestContextAad is explicitly false, returns options.aad directly without context binding.
1864
+ * 2. When encrypting (isDecrypt=false): binds request context `req:${method}:${url}` only if options.bindRequestContextAad === true.
1865
+ * 3. When decrypting (isDecrypt=true): derives request context `req:${method}:${url}` if url or method is provided.
1866
+ * 4. If explicit options.aad is provided: combines with request context if bound, or returns options.aad directly.
1867
+ * 5. Otherwise returns undefined.
1868
+ */
1869
+ _resolveEffectiveAad(options, isDecrypt = false) {
1870
+ if (!options) return void 0;
1871
+ let rawAad = options.aad;
1872
+ if (typeof rawAad === "function") {
1873
+ rawAad = rawAad(void 0, "");
1874
+ }
1875
+ if (rawAad === void 0 && options.baseAad) {
1876
+ rawAad = options.baseAad;
1877
+ }
1878
+ if (options.bindRequestContextAad === false) return rawAad;
1879
+ const hasReqContext = Boolean(options.url || options.method);
1880
+ if (rawAad) {
1881
+ if (options.bindRequestContextAad === true && hasReqContext) {
1882
+ const m = (options.method || "POST").toUpperCase();
1883
+ const u = options.url || "";
1884
+ const baseAad = typeof rawAad === "string" ? rawAad : rawAad instanceof Uint8Array || rawAad instanceof ArrayBuffer ? textDecoder.decode(rawAad) : String(rawAad);
1885
+ if (baseAad.includes(`|req:${m}:${u}`)) {
1886
+ return baseAad;
1887
+ }
1888
+ return `${baseAad}|req:${m}:${u}`;
1870
1889
  }
1890
+ return rawAad;
1891
+ }
1892
+ const shouldBindReq = isDecrypt ? hasReqContext : options.bindRequestContextAad === true && hasReqContext;
1893
+ if (shouldBindReq) {
1894
+ const m = (options.method || "POST").toUpperCase();
1895
+ const u = options.url || "";
1896
+ return `req:${m}:${u}`;
1871
1897
  }
1898
+ return void 0;
1872
1899
  }
1873
1900
  /**
1874
- * Derives an AES-GCM CryptoKey from a high-entropy secret (e.g. an ECDH shared secret key)
1875
- * using HKDF.
1901
+ * Helper to deterministically bind record ID, contextual sibling fields (aadFields),
1902
+ * dynamic AAD callbacks, and field name into the AAD to prevent ciphertext swapping
1903
+ * between fields, records, or parent entities.
1876
1904
  */
1877
- static async deriveKeyFromSecret(secretKey, options = {}) {
1878
- const subtle = getSubtleCrypto();
1879
- let saltBytes;
1880
- if (options.salt !== void 0) {
1881
- saltBytes = CKeyManager.resolveSalt(options.salt);
1882
- } else {
1883
- saltBytes = new Uint8Array(16);
1905
+ _bindFieldAad(data, field, options) {
1906
+ let baseAad = "";
1907
+ if (options?.baseAad) {
1908
+ baseAad = options.baseAad;
1884
1909
  }
1885
- const infoBytes = resolveInfoBytes(options.info, "katlux-ecdh-aes-gcm");
1886
- const hash = options.hash || "SHA-256";
1887
- const keyLength = options.keyLength || 256;
1888
- const extractable = options.extractable === true;
1889
- return await subtle.deriveKey(
1890
- {
1891
- name: "HKDF",
1892
- salt: saltBytes,
1893
- info: infoBytes,
1894
- hash
1895
- },
1896
- secretKey,
1897
- { name: "AES-GCM", length: keyLength },
1898
- extractable,
1899
- ["encrypt", "decrypt"]
1900
- );
1910
+ if (options?.aad) {
1911
+ if (typeof options.aad === "function") {
1912
+ const dynamicRes = options.aad(data, field);
1913
+ if (typeof dynamicRes === "string") {
1914
+ baseAad = baseAad ? `${baseAad}|${dynamicRes}` : dynamicRes;
1915
+ } else if (dynamicRes instanceof Uint8Array || dynamicRes instanceof ArrayBuffer) {
1916
+ const decoded = textDecoder.decode(dynamicRes);
1917
+ baseAad = baseAad ? `${baseAad}|${decoded}` : decoded;
1918
+ } else if (dynamicRes !== void 0 && dynamicRes !== null) {
1919
+ const str = String(dynamicRes);
1920
+ baseAad = baseAad ? `${baseAad}|${str}` : str;
1921
+ }
1922
+ } else if (typeof options.aad === "string") {
1923
+ baseAad = baseAad ? `${baseAad}|${options.aad}` : options.aad;
1924
+ } else if (options.aad instanceof Uint8Array || options.aad instanceof ArrayBuffer) {
1925
+ const decoded = textDecoder.decode(options.aad);
1926
+ baseAad = baseAad ? `${baseAad}|${decoded}` : decoded;
1927
+ } else {
1928
+ const str = String(options.aad);
1929
+ baseAad = baseAad ? `${baseAad}|${str}` : str;
1930
+ }
1931
+ }
1932
+ const recordId = data && typeof data === "object" && (data.id !== void 0 || data._id !== void 0) ? String(data.id ?? data._id) : "";
1933
+ const recordPart = recordId ? `|record:${recordId}` : "";
1934
+ let contextPart = "";
1935
+ if (options?.aadFields && options.aadFields.length > 0 && data && typeof data === "object") {
1936
+ const sortedFields = [...options.aadFields].sort();
1937
+ const parts = [];
1938
+ for (const f of sortedFields) {
1939
+ const val = data[f];
1940
+ let serializedVal = "";
1941
+ if (val === null) {
1942
+ serializedVal = "null";
1943
+ } else if (val !== void 0) {
1944
+ serializedVal = typeof val === "object" ? JSON.stringify(val) : String(val);
1945
+ }
1946
+ parts.push(`${f}=${serializedVal}`);
1947
+ }
1948
+ contextPart = `|ctx:${parts.join(",")}`;
1949
+ }
1950
+ const reqPart = options?.bindRequestContextAad === true && (options?.url || options?.method) ? `|req:${(options.method || "POST").toUpperCase()}:${options.url || ""}` : "";
1951
+ return {
1952
+ ...options,
1953
+ bindRequestContextAad: false,
1954
+ aad: `aad:${baseAad}${recordPart}${contextPart}${reqPart}|field:${field}`
1955
+ };
1901
1956
  }
1902
1957
  /**
1903
- * Derives two domain-separated, cryptographically isolated keys from a user password and salt:
1904
- * 1. `authKey`: For client authentication, SRP, or server challenge proofs.
1905
- * 2. `encryptionKey`: For AES-GCM local and field-level data encryption.
1906
- *
1907
- * Stage 1 (Password Stretching): PBKDF2-HMAC-SHA256 stretches low-entropy password into master secret.
1908
- * Stage 2 (Domain Expansion): HKDF-Expand derives subkeys using domain-separated context info tags.
1909
- * Immediate memory zeroization of intermediate master secret buffer is guaranteed.
1910
- *
1911
- * @param password - Master user password or passphrase
1912
- * @param salt - Salt (or omitted to generate a secure random 16-byte salt)
1913
- * @param options - PBKDF2 and HKDF configuration options
1958
+ * Validates envelope metadata against algorithm whitelists and key identifiers (kid).
1959
+ * In strict mode, allowedAlgorithms defaults to ['AES-GCM'].
1914
1960
  */
1915
- static async deriveAuthAndEncryptionKeys(password, salt, options = {}) {
1916
- if (!password || typeof password === "string" && password.trim().length === 0 || typeof password === "object" && "byteLength" in password && password.byteLength === 0) {
1917
- throw new Error("[CKeyManager] Password cannot be empty or undefined for key derivation.");
1961
+ _validateEnvelopeMetadata(payload, options) {
1962
+ if (!payload || typeof payload !== "object") return void 0;
1963
+ const allowedAlgorithms = options?.allowedAlgorithms ?? (options?.strict ? ["AES-GCM"] : void 0);
1964
+ if (allowedAlgorithms && allowedAlgorithms.length > 0) {
1965
+ const alg = payload.alg;
1966
+ if (!alg || !allowedAlgorithms.includes(alg)) {
1967
+ return new Error(`[AEncryptionProvider] Algorithm mismatch: algorithm '${alg || "unknown"}' is not in allowedAlgorithms.`);
1968
+ }
1918
1969
  }
1919
- const subtle = getSubtleCrypto();
1920
- let saltBytes;
1921
- if (salt !== void 0) {
1922
- saltBytes = CKeyManager.resolveSalt(salt);
1923
- } else {
1924
- saltBytes = CKeyManager.randomBytes(16);
1970
+ const effectiveKeys = options?.keys || this.keys;
1971
+ const effectiveAllowedKids = options?.allowedKids || this.allowedKids;
1972
+ const allowedKids = effectiveAllowedKids ?? (effectiveKeys ? Object.keys(effectiveKeys) : void 0);
1973
+ if (allowedKids && allowedKids.length > 0) {
1974
+ if (payload.kid && !allowedKids.includes(payload.kid)) {
1975
+ return new Error(`[AEncryptionProvider] Unauthorized key: kid '${payload.kid}' is not in allowedKids.`);
1976
+ }
1925
1977
  }
1926
- const saltBase64 = CKeyManager.toBase64(saltBytes);
1927
- const iterations = options.iterations || 31e4;
1928
- const hash = options.hash || "SHA-256";
1929
- const authKeyLength = options.authKeyLength || 256;
1930
- const encryptionKeyLength = options.encryptionKeyLength || 256;
1931
- const authAlgorithm = options.authAlgorithm || "HMAC";
1932
- const authInfo = resolveInfoBytes(options.authInfo, "katlux-auth-key-v1");
1933
- const encryptionInfo = resolveInfoBytes(options.encryptionInfo, "katlux-encryption-key-v1");
1934
- const extractable = options.extractable === true;
1935
- const rawPasswordBytes = typeof password === "string" ? textEncoder$1.encode(password) : password instanceof Uint8Array ? new Uint8Array(password.buffer, password.byteOffset, password.byteLength) : new Uint8Array(password);
1936
- let masterBits;
1937
- try {
1938
- const baseKey = await subtle.importKey(
1939
- "raw",
1940
- rawPasswordBytes,
1941
- "PBKDF2",
1942
- false,
1943
- ["deriveBits"]
1944
- );
1945
- const bitsLength = hash === "SHA-512" ? 512 : 256;
1946
- masterBits = await subtle.deriveBits(
1947
- {
1948
- name: "PBKDF2",
1949
- salt: saltBytes,
1950
- iterations,
1951
- hash
1952
- },
1953
- baseKey,
1954
- bitsLength
1955
- );
1956
- const hkdfKey = await subtle.importKey(
1957
- "raw",
1958
- masterBits,
1959
- "HKDF",
1960
- false,
1961
- ["deriveKey"]
1962
- );
1963
- const authDerivedAlgorithm = authAlgorithm === "AES-GCM" ? { name: "AES-GCM", length: authKeyLength } : { name: "HMAC", hash: { name: hash }, length: authKeyLength };
1964
- const authUsages = authAlgorithm === "AES-GCM" ? ["encrypt", "decrypt"] : ["sign", "verify"];
1965
- const authKey = await subtle.deriveKey(
1966
- {
1967
- name: "HKDF",
1968
- hash,
1969
- salt: new Uint8Array(0),
1970
- info: authInfo
1971
- },
1972
- hkdfKey,
1973
- authDerivedAlgorithm,
1974
- extractable,
1975
- authUsages
1976
- );
1977
- const encryptionKey = await subtle.deriveKey(
1978
- {
1979
- name: "HKDF",
1980
- hash,
1981
- salt: new Uint8Array(0),
1982
- info: encryptionInfo
1983
- },
1984
- hkdfKey,
1985
- { name: "AES-GCM", length: encryptionKeyLength },
1986
- extractable,
1987
- ["encrypt", "decrypt"]
1988
- );
1989
- return {
1990
- authKey,
1991
- encryptionKey,
1992
- salt: saltBase64
1993
- };
1994
- } finally {
1995
- if (typeof password === "string") {
1996
- rawPasswordBytes.fill(0);
1978
+ return void 0;
1979
+ }
1980
+ /**
1981
+ * Normalizes encryption options with active keyring parameters:
1982
+ * If `keys` and `activeKid` are configured, selects `key = keys[activeKid]` and sets `kid = activeKid`.
1983
+ */
1984
+ _resolveOutboundKeyringOptions(options) {
1985
+ const effectiveKeys = options?.keys || this.keys;
1986
+ const effectiveActiveKid = options?.activeKid || (options?.kid ? options.kid : this.activeKid);
1987
+ let effectiveKey = options?.key;
1988
+ let effectiveKid = options?.kid || effectiveActiveKid;
1989
+ if (effectiveKeys && effectiveActiveKid) {
1990
+ if (effectiveKeys[effectiveActiveKid]) {
1991
+ effectiveKey = options?.key || effectiveKeys[effectiveActiveKid];
1992
+ effectiveKid = effectiveActiveKid;
1993
+ } else if (!options?.key) {
1994
+ throw new Error(`[${this.constructor.name}] Active kid '${effectiveActiveKid}' not found in keyring keys.`);
1997
1995
  }
1998
- if (masterBits) {
1999
- new Uint8Array(masterBits).fill(0);
1996
+ } else if (effectiveKeys && !effectiveKey) {
1997
+ const kids = Object.keys(effectiveKeys);
1998
+ if (kids.length > 0) {
1999
+ effectiveKid = kids[0];
2000
+ effectiveKey = effectiveKeys[kids[0]];
2000
2001
  }
2001
2002
  }
2002
- }
2003
- // ── Digital Signatures, Fingerprints & Access Grants ─────────────────────
2004
- /**
2005
- * Generates an ECDSA P-256 signing key pair.
2006
- */
2007
- static async generateSigningKeyPair(curve = "P-256", extractable = true) {
2008
- const subtle = getSubtleCrypto();
2009
- const pair = await subtle.generateKey(
2010
- {
2011
- name: "ECDSA",
2012
- namedCurve: curve
2013
- },
2014
- extractable,
2015
- ["sign", "verify"]
2016
- );
2017
- return {
2018
- publicKey: pair.publicKey,
2019
- privateKey: pair.privateKey
2020
- };
2003
+ if (effectiveKeys || effectiveActiveKid || effectiveKey !== options?.key || effectiveKid !== options?.kid) {
2004
+ return {
2005
+ ...options,
2006
+ ...effectiveKeys ? { keys: effectiveKeys } : {},
2007
+ ...effectiveActiveKid ? { activeKid: effectiveActiveKid } : {},
2008
+ ...effectiveKey !== void 0 ? { key: effectiveKey } : {},
2009
+ ...effectiveKid !== void 0 ? { kid: effectiveKid } : {}
2010
+ };
2011
+ }
2012
+ return options || {};
2021
2013
  }
2022
2014
  /**
2023
- * Generates a deterministic SHA-256 fingerprint for a public key (SPKI or raw).
2024
- * Formatted as `SHA256:<base64url>`.
2015
+ * Resolves the appropriate decryption key for a specific payload envelope based on its `kid`
2016
+ * and the configured keyring (`keys`, `activeKid`, or fallback `key`).
2025
2017
  */
2026
- static async getKeyFingerprint(publicKey) {
2027
- const subtle = getSubtleCrypto();
2028
- let rawBytes;
2029
- if (typeof publicKey === "object" && publicKey !== null && "algorithm" in publicKey) {
2030
- const cryptoKey = publicKey;
2031
- rawBytes = await subtle.exportKey("spki", cryptoKey);
2032
- } else if (publicKey instanceof Uint8Array) {
2033
- rawBytes = publicKey;
2034
- } else if (publicKey instanceof ArrayBuffer) {
2035
- rawBytes = publicKey;
2036
- } else if (typeof publicKey === "string") {
2037
- const trimmed = publicKey.trim();
2038
- if (trimmed.startsWith("-----BEGIN")) {
2039
- const b642 = trimmed.replace(/-----[^-]+-----/g, "").replace(/\s+/g, "");
2040
- rawBytes = CKeyManager.fromBase64(b642);
2041
- } else {
2042
- try {
2043
- rawBytes = CKeyManager.fromBase64(trimmed);
2044
- } catch {
2045
- rawBytes = textEncoder$1.encode(trimmed);
2046
- }
2018
+ _resolveDecryptionKeyForEnvelope(payload, options) {
2019
+ const effectiveKeys = options?.keys || this.keys;
2020
+ const effectiveActiveKid = options?.activeKid || this.activeKid;
2021
+ if (payload && typeof payload === "object" && payload.kid && effectiveKeys) {
2022
+ if (effectiveKeys[payload.kid]) {
2023
+ return effectiveKeys[payload.kid];
2047
2024
  }
2048
- } else {
2049
- throw new Error(
2050
- "[CKeyManager] Invalid key input for getKeyFingerprint: expected CryptoKey, Uint8Array, ArrayBuffer, or string."
2051
- );
2052
2025
  }
2053
- const hashBuffer = await subtle.digest("SHA-256", rawBytes);
2054
- const b64 = CKeyManager.toBase64(hashBuffer);
2055
- const b64url = b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
2056
- return `SHA256:${b64url}`;
2026
+ if (effectiveKeys && effectiveActiveKid && effectiveKeys[effectiveActiveKid]) {
2027
+ return effectiveKeys[effectiveActiveKid];
2028
+ }
2029
+ return options?.key || this.defaultKey;
2057
2030
  }
2058
2031
  /**
2059
- * Signs arbitrary data or JSON objects using an ECDSA P-256 private key.
2060
- * Objects are automatically serialized via canonicalizeJson() before hashing.
2032
+ * Handles a per-field or per-payload decryption error according to configured options:
2033
+ * - Invokes onDecryptError callback if provided
2034
+ * - Attaches error metadata (__decryptErrors) to the record object
2035
+ * - Returns fallback cell placeholder ('[⚠ Decryption Error]') or rethrows
2061
2036
  */
2062
- static async sign(data, privateKey) {
2063
- const subtle = getSubtleCrypto();
2064
- if (!privateKey || !privateKey.algorithm || privateKey.algorithm.name !== "ECDSA") {
2065
- throw new Error("[CKeyManager] sign requires an ECDSA private key.");
2037
+ _handleDecryptError(err, record, field, value, index, options) {
2038
+ if (record && typeof record === "object" && record !== value) {
2039
+ record.__decryptErrors = {
2040
+ ...record.__decryptErrors || {},
2041
+ [field || "__payload"]: err
2042
+ };
2066
2043
  }
2067
- let dataBytes;
2068
- if (data instanceof Uint8Array) {
2069
- dataBytes = data;
2070
- } else if (ArrayBuffer.isView(data)) {
2071
- dataBytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
2072
- } else if (data instanceof ArrayBuffer) {
2073
- dataBytes = new Uint8Array(data);
2074
- } else if (typeof data === "string") {
2075
- dataBytes = textEncoder$1.encode(data);
2076
- } else {
2077
- const canonical = canonicalizeJson(data);
2078
- dataBytes = textEncoder$1.encode(canonical);
2044
+ if (options?.onDecryptError === false || options?.onDecryptError === void 0) {
2045
+ throw err;
2079
2046
  }
2080
- const namedCurve = privateKey.algorithm.namedCurve || "P-256";
2081
- const hashName = namedCurve === "P-384" ? "SHA-384" : namedCurve === "P-521" ? "SHA-512" : "SHA-256";
2082
- const signatureBuffer = await subtle.sign(
2083
- {
2084
- name: "ECDSA",
2085
- hash: { name: hashName }
2086
- },
2087
- privateKey,
2088
- dataBytes
2089
- );
2090
- return CKeyManager.toBase64(signatureBuffer);
2047
+ if (typeof options.onDecryptError === "function") {
2048
+ return options.onDecryptError({
2049
+ record,
2050
+ field,
2051
+ value,
2052
+ error: err,
2053
+ index
2054
+ });
2055
+ }
2056
+ if (typeof options.onDecryptError === "string") {
2057
+ return options.onDecryptError;
2058
+ }
2059
+ if (options.onDecryptError === true) {
2060
+ return "[\u26A0 Decryption Error]";
2061
+ }
2062
+ throw err;
2091
2063
  }
2092
2064
  /**
2093
- * Verifies an ECDSA P-256 signature against original data or JSON object.
2065
+ * Encrypts a single scalar value with contextual AAD binding into an IEncryptedPayload envelope.
2066
+ * Binds recordId, fieldName, and sibling aadFields deterministically to protect against ciphertext swapping.
2067
+ *
2068
+ * @param value - Scalar value (string, number, boolean, object) to encrypt.
2069
+ * @param context - Field encryption context with recordId, fieldName, and optional sibling aadFields.
2094
2070
  */
2095
- static async verify(data, signature, publicKey) {
2096
- const subtle = getSubtleCrypto();
2097
- if (!publicKey || !publicKey.algorithm || publicKey.algorithm.name !== "ECDSA") {
2098
- throw new Error("[CKeyManager] verify requires an ECDSA public key.");
2099
- }
2100
- if (typeof signature !== "string" || !signature.trim()) {
2101
- return false;
2071
+ async encryptField(value, context) {
2072
+ if (value === void 0 || value === null) return value;
2073
+ if (this.isEncrypted(value)) return value;
2074
+ const field = context?.fieldName || "field";
2075
+ const dummyData = {};
2076
+ if (context?.recordId !== void 0) {
2077
+ dummyData.id = context.recordId;
2078
+ }
2079
+ const { aadFields, ...restContext } = context || {};
2080
+ let boundOptions = { ...restContext };
2081
+ if (aadFields) {
2082
+ if (!Array.isArray(aadFields) && typeof aadFields === "object") {
2083
+ Object.assign(dummyData, aadFields);
2084
+ boundOptions.aadFields = Object.keys(aadFields);
2085
+ } else if (Array.isArray(aadFields)) {
2086
+ boundOptions.aadFields = aadFields;
2087
+ if (context?.record) {
2088
+ Object.assign(dummyData, context.record);
2089
+ }
2090
+ }
2091
+ }
2092
+ const resolvedOptions = this._bindFieldAad(dummyData, field, boundOptions);
2093
+ return this.encryptValue(value, resolvedOptions);
2094
+ }
2095
+ /**
2096
+ * Decrypts a single IEncryptedPayload envelope back into its scalar plaintext value using contextual AAD.
2097
+ * Validates recordId, fieldName, and sibling aadFields integrity.
2098
+ *
2099
+ * @param envelope - The IEncryptedPayload envelope to decrypt.
2100
+ * @param context - Field encryption context matching the encryption parameters.
2101
+ */
2102
+ async decryptField(envelope, context) {
2103
+ if (!this.isEncrypted(envelope)) return envelope;
2104
+ const field = context?.fieldName || "field";
2105
+ const dummyData = {};
2106
+ if (context?.recordId !== void 0) {
2107
+ dummyData.id = context.recordId;
2102
2108
  }
2103
- try {
2104
- const signatureBytes = CKeyManager.fromBase64(signature);
2105
- let dataBytes;
2106
- if (data instanceof Uint8Array) {
2107
- dataBytes = data;
2108
- } else if (ArrayBuffer.isView(data)) {
2109
- dataBytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
2110
- } else if (data instanceof ArrayBuffer) {
2111
- dataBytes = new Uint8Array(data);
2112
- } else if (typeof data === "string") {
2113
- dataBytes = textEncoder$1.encode(data);
2114
- } else {
2115
- const canonical = canonicalizeJson(data);
2116
- dataBytes = textEncoder$1.encode(canonical);
2109
+ const { aadFields, ...restContext } = context || {};
2110
+ let boundOptions = { ...restContext };
2111
+ if (aadFields) {
2112
+ if (!Array.isArray(aadFields) && typeof aadFields === "object") {
2113
+ Object.assign(dummyData, aadFields);
2114
+ boundOptions.aadFields = Object.keys(aadFields);
2115
+ } else if (Array.isArray(aadFields)) {
2116
+ boundOptions.aadFields = aadFields;
2117
+ if (context?.record) {
2118
+ Object.assign(dummyData, context.record);
2119
+ }
2117
2120
  }
2118
- const namedCurve = publicKey.algorithm.namedCurve || "P-256";
2119
- const hashName = namedCurve === "P-384" ? "SHA-384" : namedCurve === "P-521" ? "SHA-512" : "SHA-256";
2120
- return await subtle.verify(
2121
- {
2122
- name: "ECDSA",
2123
- hash: { name: hashName }
2124
- },
2125
- publicKey,
2126
- signatureBytes,
2127
- dataBytes
2128
- );
2129
- } catch {
2130
- return false;
2131
2121
  }
2122
+ const resolvedOptions = this._bindFieldAad(dummyData, field, boundOptions);
2123
+ return this.decryptValue(envelope, resolvedOptions);
2132
2124
  }
2133
2125
  /**
2134
- * Signs a structured access grant, embedding signature and granter key fingerprint.
2126
+ * Top-level polymorphic encryption method.
2127
+ * Automatically performs value-level encryption:
2128
+ * - If data is an array of objects: recursively encrypts each row.
2129
+ * - If data is an object: encrypts specified fields (or all values if valueEncryption is true),
2130
+ * preserving JSON object keys so backend APIs and ORMs can parse keys and perform upserts.
2131
+ * - If data is a primitive or whole-payload envelope: encrypts as a single value.
2135
2132
  */
2136
- static async signGrant(grant, granterPrivateKey, granterPublicKey) {
2137
- if (!grant || typeof grant !== "object") {
2138
- throw new Error("[CKeyManager] signGrant requires a valid grant object.");
2133
+ async encrypt(data, options) {
2134
+ if (data === void 0 || data === null) return data;
2135
+ if (this.isEncrypted(data)) return data;
2136
+ options = this._resolveOutboundKeyringOptions(options);
2137
+ if (options?.valueEncryption === false && (!options.fields || options.fields.length === 0)) {
2138
+ return this.encryptValue(data, options);
2139
2139
  }
2140
- let fingerprint;
2141
- if (granterPublicKey) {
2142
- fingerprint = await CKeyManager.getKeyFingerprint(granterPublicKey);
2143
- } else {
2144
- const subtle = getSubtleCrypto();
2145
- try {
2146
- const jwk = await subtle.exportKey("jwk", granterPrivateKey);
2147
- const pubJwk = {
2148
- kty: jwk.kty,
2149
- crv: jwk.crv,
2150
- x: jwk.x,
2151
- y: jwk.y,
2152
- ext: true,
2153
- key_ops: ["verify"]
2154
- };
2155
- const pubKey = await subtle.importKey(
2156
- "jwk",
2157
- pubJwk,
2158
- { name: "ECDSA", namedCurve: jwk.crv || "P-256" },
2159
- true,
2160
- ["verify"]
2140
+ if (Array.isArray(data)) {
2141
+ if (data.length === 0) return data;
2142
+ if (options?.fields && options.fields.length > 0) {
2143
+ const fields = options.fields;
2144
+ let hasUnencrypted = false;
2145
+ for (let i = 0; i < data.length; i++) {
2146
+ const item = data[i];
2147
+ if (item && typeof item === "object") {
2148
+ for (let f = 0; f < fields.length; f++) {
2149
+ const val = item[fields[f]];
2150
+ if (val !== void 0 && val !== null && !this.isEncrypted(val)) {
2151
+ hasUnencrypted = true;
2152
+ break;
2153
+ }
2154
+ }
2155
+ if (hasUnencrypted) break;
2156
+ }
2157
+ }
2158
+ if (!hasUnencrypted) {
2159
+ return data;
2160
+ }
2161
+ await this.warmupKey(options);
2162
+ const itemOptions = { ...options, _warmed: true };
2163
+ return Promise.all(data.map((item) => this.encrypt(item, itemOptions)));
2164
+ }
2165
+ if (options?.valueEncryption === true) {
2166
+ await this.warmupKey(options);
2167
+ const itemOptions = { ...options, _warmed: true };
2168
+ return Promise.all(data.map((item) => this.encrypt(item, itemOptions)));
2169
+ }
2170
+ return this.encryptValue(data, options);
2171
+ }
2172
+ if (typeof data === "object" && !ArrayBuffer.isView(data) && !(data instanceof ArrayBuffer)) {
2173
+ if (options?.fields && options.fields.length > 0) {
2174
+ const fieldsToEncrypt = options.fields.filter(
2175
+ (field) => Object.prototype.hasOwnProperty.call(data, field) && data[field] !== void 0 && data[field] !== null && !this.isEncrypted(data[field])
2176
+ );
2177
+ if (fieldsToEncrypt.length === 0) {
2178
+ return data;
2179
+ }
2180
+ if (!options?._warmed) {
2181
+ await this.warmupKey(options);
2182
+ }
2183
+ const result = { ...data };
2184
+ const entries = await Promise.all(
2185
+ fieldsToEncrypt.map(async (field) => {
2186
+ const fieldOptions = this._bindFieldAad(data, field, options);
2187
+ return [field, await this.encryptValue(result[field], fieldOptions)];
2188
+ })
2161
2189
  );
2162
- fingerprint = await CKeyManager.getKeyFingerprint(pubKey);
2163
- } catch {
2164
- throw new Error(
2165
- "[CKeyManager] signGrant requires granterPublicKey when granterPrivateKey is non-extractable."
2190
+ for (const [field, encryptedVal] of entries) {
2191
+ safeSetProperty(result, field, encryptedVal);
2192
+ }
2193
+ return result;
2194
+ }
2195
+ if (options?.valueEncryption === true) {
2196
+ const result = { ...data };
2197
+ const keysToEncrypt = Object.keys(result).filter(
2198
+ (key) => result[key] !== void 0 && result[key] !== null && !this.isEncrypted(result[key])
2199
+ );
2200
+ if (keysToEncrypt.length === 0) {
2201
+ return result;
2202
+ }
2203
+ if (!options?._warmed) {
2204
+ await this.warmupKey(options);
2205
+ }
2206
+ const entries = await Promise.all(
2207
+ keysToEncrypt.map(async (key) => {
2208
+ const fieldOptions = this._bindFieldAad(data, key, options);
2209
+ return [key, await this.encryptValue(result[key], fieldOptions)];
2210
+ })
2166
2211
  );
2212
+ for (const [key, encryptedVal] of entries) {
2213
+ safeSetProperty(result, key, encryptedVal);
2214
+ }
2215
+ return result;
2167
2216
  }
2168
2217
  }
2169
- const signature = await CKeyManager.sign(grant, granterPrivateKey);
2170
- return {
2171
- grant: { ...grant },
2172
- signature,
2173
- fingerprint
2174
- };
2218
+ return this.encryptValue(data, options);
2175
2219
  }
2176
2220
  /**
2177
- * Verifies the authenticity and integrity of a signed access grant against granter public key.
2221
+ * Top-level polymorphic decryption method.
2222
+ * Automatically performs value-level decryption:
2223
+ * - If payload is an encrypted envelope: validates metadata and decrypts directly via decryptValue.
2224
+ * - If payload is an array of objects: decrypts each row (batched with local memoization).
2225
+ * - If payload is an object: decrypts any encrypted value cells across fields.
2226
+ * In strict mode:
2227
+ * - Unencrypted plaintext in configured `fields` triggers a decryption error.
2228
+ * - Envelopes are validated against `allowedAlgorithms` and `allowedKids`.
2229
+ * - Errors are routed through `onDecryptError` or populated into `__decryptErrors`.
2178
2230
  */
2179
- static async verifyGrant(signedGrant, granterPublicKey, options) {
2180
- if (!signedGrant || !signedGrant.grant || !signedGrant.signature || !signedGrant.fingerprint || !granterPublicKey) {
2181
- return false;
2231
+ async decrypt(payload, options) {
2232
+ if (payload === void 0 || payload === null) return payload;
2233
+ if (this.isEncrypted(payload)) {
2234
+ const envelopeKey = this._resolveDecryptionKeyForEnvelope(payload, options);
2235
+ const envelopeOptions = envelopeKey !== options?.key ? { ...options, key: envelopeKey } : options;
2236
+ const metaErr = this._validateEnvelopeMetadata(payload, envelopeOptions);
2237
+ if (metaErr) {
2238
+ return this._handleDecryptError(metaErr, payload, "", payload, void 0, envelopeOptions);
2239
+ }
2240
+ try {
2241
+ return await this.decryptValue(payload, envelopeOptions);
2242
+ } catch (err) {
2243
+ return this._handleDecryptError(err, payload, "", payload, void 0, envelopeOptions);
2244
+ }
2182
2245
  }
2183
- const expectedFingerprint = await CKeyManager.getKeyFingerprint(granterPublicKey);
2184
- if (signedGrant.fingerprint !== expectedFingerprint) {
2185
- return false;
2246
+ if (Array.isArray(payload)) {
2247
+ if (payload.length === 0) return payload;
2248
+ const localMemo = /* @__PURE__ */ new Map();
2249
+ const decryptRow = async (item, rowIndex) => {
2250
+ if (!item) return item;
2251
+ if (this.isEncrypted(item)) {
2252
+ const envelopeKey = this._resolveDecryptionKeyForEnvelope(item, options);
2253
+ const envelopeOptions = envelopeKey !== options?.key ? { ...options, key: envelopeKey } : options;
2254
+ const metaErr = this._validateEnvelopeMetadata(item, envelopeOptions);
2255
+ if (metaErr) {
2256
+ return this._handleDecryptError(metaErr, item, "", item, rowIndex, envelopeOptions);
2257
+ }
2258
+ const ct = item.ciphertext;
2259
+ const iv = item.iv || "";
2260
+ const aadPart = options?.aad ? typeof options.aad === "string" ? options.aad : "" : "";
2261
+ const memoKey = `${ct}:${iv}:${aadPart}:${item.kid || ""}`;
2262
+ try {
2263
+ if (!localMemo.has(memoKey)) {
2264
+ localMemo.set(memoKey, this.decryptValue(item, envelopeOptions));
2265
+ }
2266
+ return await localMemo.get(memoKey);
2267
+ } catch (err) {
2268
+ return this._handleDecryptError(err, item, "", item, rowIndex, envelopeOptions);
2269
+ }
2270
+ }
2271
+ if (typeof item === "object" && !ArrayBuffer.isView(item) && !(item instanceof ArrayBuffer)) {
2272
+ const targetKeys = options?.fields && options.fields.length > 0 ? options.fields.filter((f) => Object.prototype.hasOwnProperty.call(item, f)) : Object.keys(item);
2273
+ const keysToDecrypt = targetKeys.filter((key) => this.isEncrypted(item[key]));
2274
+ const strictViolationKeys = options?.strict === true && options?.fields && options.fields.length > 0 ? targetKeys.filter((key) => item[key] !== void 0 && item[key] !== null && !this.isEncrypted(item[key])) : [];
2275
+ if (keysToDecrypt.length === 0 && strictViolationKeys.length === 0) {
2276
+ return item;
2277
+ }
2278
+ const proto = Object.getPrototypeOf(item);
2279
+ const result = proto && proto !== Object.prototype ? Object.assign(Object.create(proto), item) : { ...item };
2280
+ for (const key of strictViolationKeys) {
2281
+ const err = new Error(`[AEncryptionProvider] Strict decryption violation: field '${key}' contains unencrypted plaintext.`);
2282
+ const fallbackVal = this._handleDecryptError(err, result, key, item[key], rowIndex, options);
2283
+ safeSetProperty(result, key, fallbackVal);
2284
+ }
2285
+ await Promise.all(
2286
+ keysToDecrypt.map(async (key) => {
2287
+ const env = item[key];
2288
+ const envelopeKey = this._resolveDecryptionKeyForEnvelope(env, options);
2289
+ const baseOptions = envelopeKey !== options?.key ? { ...options, key: envelopeKey } : options;
2290
+ const boundOptions = this._bindFieldAad(item, key, baseOptions);
2291
+ const hasBoundAad = boundOptions.aad !== baseOptions?.aad;
2292
+ const metaErr = this._validateEnvelopeMetadata(env, boundOptions);
2293
+ if (metaErr) {
2294
+ const fallbackVal = this._handleDecryptError(metaErr, result, key, env, rowIndex, boundOptions);
2295
+ safeSetProperty(result, key, fallbackVal);
2296
+ return;
2297
+ }
2298
+ const ct = env.ciphertext;
2299
+ const iv = env.iv || "";
2300
+ const aadPart = boundOptions.aad ? typeof boundOptions.aad === "string" ? boundOptions.aad : "" : "";
2301
+ const memoKey = `${ct}:${iv}:${aadPart}:${env.kid || ""}`;
2302
+ try {
2303
+ if (!localMemo.has(memoKey)) {
2304
+ const decryptPromise = (async () => {
2305
+ try {
2306
+ return await this.decryptValue(env, boundOptions);
2307
+ } catch (err) {
2308
+ const canFallback = hasBoundAad && (!options?.aadFields || options.aadFields.length === 0) && typeof options?.aad !== "function";
2309
+ if (canFallback && isAuthOrDecryptionError(err)) {
2310
+ return await this.decryptValue(env, baseOptions);
2311
+ }
2312
+ throw err;
2313
+ }
2314
+ })();
2315
+ localMemo.set(memoKey, decryptPromise);
2316
+ }
2317
+ const decryptedVal = await localMemo.get(memoKey);
2318
+ safeSetProperty(result, key, decryptedVal);
2319
+ } catch (err) {
2320
+ const fallbackVal = this._handleDecryptError(err, result, key, env, rowIndex, boundOptions);
2321
+ safeSetProperty(result, key, fallbackVal);
2322
+ }
2323
+ })
2324
+ );
2325
+ return result;
2326
+ }
2327
+ return item;
2328
+ };
2329
+ if (payload.length <= 100) {
2330
+ return Promise.all(payload.map((item, index) => decryptRow(item, index)));
2331
+ }
2332
+ const batchSize = 1e3;
2333
+ const results = new Array(payload.length);
2334
+ for (let i = 0; i < payload.length; i += batchSize) {
2335
+ const end = Math.min(i + batchSize, payload.length);
2336
+ const chunk = payload.slice(i, end);
2337
+ const chunkRes = await Promise.all(chunk.map((item, localIdx) => decryptRow(item, i + localIdx)));
2338
+ for (let j = 0; j < chunkRes.length; j++) {
2339
+ results[i + j] = chunkRes[j];
2340
+ }
2341
+ if (end < payload.length && (i > 0 && i % 5e3 === 0)) {
2342
+ await new Promise((r) => setTimeout(r, 0));
2343
+ }
2344
+ }
2345
+ return results;
2186
2346
  }
2187
- if (options?.checkExpiry && signedGrant.grant.expiresAt !== void 0) {
2188
- const now = options.now ?? Date.now();
2189
- if (now > signedGrant.grant.expiresAt) {
2190
- return false;
2347
+ if (typeof payload === "object" && !ArrayBuffer.isView(payload) && !(payload instanceof ArrayBuffer)) {
2348
+ const targetKeys = options?.fields && options.fields.length > 0 ? options.fields.filter((f) => Object.prototype.hasOwnProperty.call(payload, f)) : Object.keys(payload);
2349
+ const keysToDecrypt = targetKeys.filter((key) => this.isEncrypted(payload[key]));
2350
+ const strictViolationKeys = options?.strict === true && options?.fields && options.fields.length > 0 ? targetKeys.filter((key) => payload[key] !== void 0 && payload[key] !== null && !this.isEncrypted(payload[key])) : [];
2351
+ if (keysToDecrypt.length === 0 && strictViolationKeys.length === 0) {
2352
+ return payload;
2353
+ }
2354
+ const proto = Object.getPrototypeOf(payload);
2355
+ const result = proto && proto !== Object.prototype ? Object.assign(Object.create(proto), payload) : { ...payload };
2356
+ for (const key of strictViolationKeys) {
2357
+ const err = new Error(`[AEncryptionProvider] Strict decryption violation: field '${key}' contains unencrypted plaintext.`);
2358
+ const fallbackVal = this._handleDecryptError(err, result, key, payload[key], void 0, options);
2359
+ safeSetProperty(result, key, fallbackVal);
2191
2360
  }
2361
+ await Promise.all(
2362
+ keysToDecrypt.map(async (key) => {
2363
+ const env = payload[key];
2364
+ const envelopeKey = this._resolveDecryptionKeyForEnvelope(env, options);
2365
+ const baseOptions = envelopeKey !== options?.key ? { ...options, key: envelopeKey } : options;
2366
+ const boundOptions = this._bindFieldAad(payload, key, baseOptions);
2367
+ const hasBoundAad = boundOptions.aad !== baseOptions?.aad;
2368
+ const metaErr = this._validateEnvelopeMetadata(env, boundOptions);
2369
+ if (metaErr) {
2370
+ const fallbackVal = this._handleDecryptError(metaErr, result, key, env, void 0, boundOptions);
2371
+ safeSetProperty(result, key, fallbackVal);
2372
+ return;
2373
+ }
2374
+ try {
2375
+ let decryptedVal;
2376
+ try {
2377
+ decryptedVal = await this.decryptValue(env, boundOptions);
2378
+ } catch (err) {
2379
+ const canFallback = hasBoundAad && (!options?.aadFields || options.aadFields.length === 0) && typeof options?.aad !== "function";
2380
+ if (canFallback && isAuthOrDecryptionError(err)) {
2381
+ decryptedVal = await this.decryptValue(env, baseOptions);
2382
+ } else {
2383
+ throw err;
2384
+ }
2385
+ }
2386
+ safeSetProperty(result, key, decryptedVal);
2387
+ } catch (err) {
2388
+ const fallbackVal = this._handleDecryptError(err, result, key, env, void 0, boundOptions);
2389
+ safeSetProperty(result, key, fallbackVal);
2390
+ }
2391
+ })
2392
+ );
2393
+ return result;
2192
2394
  }
2193
- return await CKeyManager.verify(signedGrant.grant, signedGrant.signature, granterPublicKey);
2395
+ return payload;
2194
2396
  }
2195
2397
  }
2196
2398
 
@@ -5015,6 +5217,13 @@ class CSearchIndex {
5015
5217
  const signature = await subtle.sign("HMAC", key, textEncoder.encode(normalized));
5016
5218
  return Array.from(new Uint8Array(signature)).map((b) => b.toString(16).padStart(2, "0")).join("");
5017
5219
  }
5220
+ /**
5221
+ * Computes a deterministic HMAC-SHA256 blind index hash for a term using the configured HMAC key.
5222
+ * Performs Unicode NFKC normalization, trimming, and lowercasing.
5223
+ */
5224
+ async computeBlindIndex(term) {
5225
+ return this.computeToken(term);
5226
+ }
5018
5227
  /**
5019
5228
  * Computes HMAC-SHA256 tokens for all trigrams of a term.
5020
5229
  */
@@ -5044,9 +5253,21 @@ class CSearchIndex {
5044
5253
  const token = await this.computeToken(rawVal);
5045
5254
  const trigrams = await this.computeTrigramTokens(rawVal);
5046
5255
  fieldMap.set(field, { token, trigrams });
5256
+ } else if (rawVal && typeof rawVal === "object" && rawVal.__encrypted === true) {
5257
+ const precomputedToken = row[`${field}_bidx`] || row[`${field}_token`];
5258
+ if (typeof precomputedToken === "string" && precomputedToken) {
5259
+ fieldMap.set(field, { token: precomputedToken, trigrams: /* @__PURE__ */ new Set() });
5260
+ }
5047
5261
  }
5048
5262
  }
5049
5263
  }
5264
+ /**
5265
+ * Indexes a row using pre-decrypted plaintext field values.
5266
+ * Allows indexing rows that contain pre-encrypted ciphertext envelopes in allData.
5267
+ */
5268
+ async indexRowDecrypted(row, decryptedPlaintext) {
5269
+ return this.indexRow(row, decryptedPlaintext);
5270
+ }
5050
5271
  /**
5051
5272
  * Indexes an array of rows.
5052
5273
  */
@@ -5095,7 +5316,7 @@ class CSearchIndex {
5095
5316
  }
5096
5317
  return true;
5097
5318
  }
5098
- if (operator === EDataFilterOperator.NotIn || operator === "notIn") {
5319
+ if (operator === EDataFilterOperator.NotIn || operator === "notIn" || operator === "nin") {
5099
5320
  const queryTrigramTokens = await this.computeTrigramTokens(filterValue);
5100
5321
  if (queryTrigramTokens.size === 0) return false;
5101
5322
  for (const tok of queryTrigramTokens) {
@@ -5166,7 +5387,7 @@ class CSearchIndex {
5166
5387
  }
5167
5388
  return true;
5168
5389
  });
5169
- } else if (op === EDataFilterOperator.NotIn || op === "notIn") {
5390
+ } else if (op === EDataFilterOperator.NotIn || op === "notIn" || op === "nin") {
5170
5391
  const queryTrigrams = await this.computeTrigramTokens(filterValue);
5171
5392
  checks.push((row) => {
5172
5393
  const entry = this.getEntry(row, key);
@@ -5285,6 +5506,36 @@ class AEncryptedClientProvider extends ADataProvider {
5285
5506
  } : void 0;
5286
5507
  return await encryptRowsChunked(rows, this.encryption, options, void 0, this.idKey, indexChunk);
5287
5508
  }
5509
+ /**
5510
+ * Indexes pre-encrypted rows by performing a transient in-memory decryption pass
5511
+ * on search fields to populate CSearchIndex without mutating underlying encrypted records in allData.
5512
+ */
5513
+ async indexPreEncryptedRows(rows) {
5514
+ if (!this.isEncryptionEnabled || !this._searchIndex || !rows || rows.length === 0) {
5515
+ return;
5516
+ }
5517
+ const cipher = getCipherProvider(this.encryption);
5518
+ if (!cipher) return;
5519
+ const ctx = buildEncryptionContext(this.encryption, this.apiUrl, this.idKey);
5520
+ let decryptedRows = [];
5521
+ try {
5522
+ const decResult = await cipher.decrypt(rows, ctx);
5523
+ decryptedRows = Array.isArray(decResult) ? decResult : [decResult];
5524
+ } catch (err) {
5525
+ console.warn("[Katlux Providers] indexPreEncryptedRows: Decryption failed during search index build", err);
5526
+ return;
5527
+ }
5528
+ await Promise.all(rows.map((row, idx) => {
5529
+ const decRow = decryptedRows[idx];
5530
+ const plainFields = {};
5531
+ for (const f of this.encryption?.fields || Object.keys(decRow || {})) {
5532
+ if (decRow && decRow[f] !== void 0) {
5533
+ plainFields[f] = decRow[f];
5534
+ }
5535
+ }
5536
+ return this._searchIndex.indexRow(row, plainFields);
5537
+ }));
5538
+ }
5288
5539
  async decryptRows(rows) {
5289
5540
  if (!this.isEncryptionEnabled || !rows || rows.length === 0) {
5290
5541
  return { rows, decrypted: false };
@@ -5593,6 +5844,15 @@ class CFlatClientDataProvider extends AEncryptedClientProvider {
5593
5844
  this.loading = true;
5594
5845
  try {
5595
5846
  const rows = Array.isArray(data) ? data : [];
5847
+ const isPreEnc = this.encryption?.preEncrypted ?? isDataEncrypted(void 0, rows);
5848
+ if (isPreEnc) {
5849
+ this.allData = rows;
5850
+ if (this.searchIndex) {
5851
+ await this.indexPreEncryptedRows(rows);
5852
+ }
5853
+ await this.loadPageData();
5854
+ return;
5855
+ }
5596
5856
  const encryptedRows = await this.encryptRows(rows);
5597
5857
  this.allData = encryptedRows;
5598
5858
  await this.loadPageData();
@@ -5613,6 +5873,16 @@ class CFlatClientDataProvider extends AEncryptedClientProvider {
5613
5873
  this.loading = true;
5614
5874
  try {
5615
5875
  const rows = Array.isArray(data) ? data : [];
5876
+ const isPreEnc = this.encryption?.preEncrypted ?? isDataEncrypted(void 0, rows);
5877
+ if (isPreEnc) {
5878
+ this.allData = rows;
5879
+ if (this.searchIndex) {
5880
+ await this.indexPreEncryptedRows(rows);
5881
+ }
5882
+ options?.onProgress?.({ processed: rows.length, total: rows.length, percent: 100 });
5883
+ await this.loadPageData();
5884
+ return;
5885
+ }
5616
5886
  const encryptedRows = await this.encryptRowsChunked(rows, options);
5617
5887
  this.allData = encryptedRows;
5618
5888
  await this.loadPageData();
@@ -7759,17 +8029,26 @@ class CAPIFlatClientDataProvider extends CFlatClientDataProvider {
7759
8029
  if (data) {
7760
8030
  const normalized = normalizeDataResult(data);
7761
8031
  const rows = normalized ? normalized.rows : data.rows || data;
8032
+ const incomingRows = Array.isArray(rows) ? rows : [];
7762
8033
  if (this._isAccumulating) {
7763
- this.allData = [...this.allData, ...rows];
8034
+ this.allData = [...this.allData, ...incomingRows];
7764
8035
  } else {
7765
- this.allData = Array.isArray(rows) ? rows : [];
8036
+ this.allData = incomingRows;
7766
8037
  }
7767
8038
  this._filterDirty = true;
7768
8039
  this._sortDirty = true;
7769
8040
  if (normalized?.rowCount !== void 0) {
7770
8041
  this.rowCount = normalized.rowCount;
7771
8042
  }
7772
- this._lastDataReceivedPromise = this.loadPageData();
8043
+ this._lastDataReceivedPromise = (async () => {
8044
+ if (this.isEncryptionEnabled && this.searchIndex && incomingRows.length > 0) {
8045
+ const isPreEnc = this.encryption?.preEncrypted ?? isDataEncrypted(void 0, incomingRows);
8046
+ if (isPreEnc) {
8047
+ await this.indexPreEncryptedRows(incomingRows);
8048
+ }
8049
+ }
8050
+ await this.loadPageData();
8051
+ })();
7773
8052
  }
7774
8053
  });
7775
8054
  }
@@ -7855,6 +8134,12 @@ class CAPIFlatClientDataProvider extends CFlatClientDataProvider {
7855
8134
  this.allData = normalized.rows;
7856
8135
  this._filterDirty = true;
7857
8136
  this._sortDirty = true;
8137
+ if (this.isEncryptionEnabled && this.searchIndex && this.allData.length > 0) {
8138
+ const isPreEnc = this.encryption?.preEncrypted ?? isDataEncrypted(void 0, this.allData);
8139
+ if (isPreEnc) {
8140
+ await this.indexPreEncryptedRows(this.allData);
8141
+ }
8142
+ }
7858
8143
  await this.loadPageData();
7859
8144
  this.initialLoad = false;
7860
8145
  this.loading = false;