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