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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -18,6 +18,8 @@ function _interopNamespaceCompat(e) {
18
18
 
19
19
  const crypto__namespace = /*#__PURE__*/_interopNamespaceCompat(crypto$1);
20
20
 
21
+ const DECRYPT_ERROR_SENTINEL = Symbol.for("katlux.decrypt_error");
22
+
21
23
  var EDataFilterOperator = /* @__PURE__ */ ((EDataFilterOperator2) => {
22
24
  EDataFilterOperator2["Equal"] = "==";
23
25
  EDataFilterOperator2["NotEqual"] = "!=";
@@ -672,13 +674,84 @@ function isAuthOrDecryptionError(err) {
672
674
  const msg = String(err.message || "").toLowerCase();
673
675
  return msg.includes("operationerror") || msg.includes("decrypt") || msg.includes("tag") || msg.includes("aad") || msg.includes("authenticated additional data") || msg.includes("authenticate data") || msg.includes("mac");
674
676
  }
677
+ function canonicalizeJson(value, seen = /* @__PURE__ */ new Set()) {
678
+ if (value === void 0) {
679
+ return "";
680
+ }
681
+ if (value === null || typeof value === "boolean") {
682
+ return JSON.stringify(value);
683
+ }
684
+ if (typeof value === "number") {
685
+ if (!Number.isFinite(value)) return "null";
686
+ if (Object.is(value, -0)) return "0";
687
+ return JSON.stringify(value);
688
+ }
689
+ if (typeof value === "string") {
690
+ return JSON.stringify(value);
691
+ }
692
+ if (typeof value === "bigint") {
693
+ throw new TypeError("Do not know how to serialize a BigInt");
694
+ }
695
+ if (typeof value === "symbol" || typeof value === "function") {
696
+ return "";
697
+ }
698
+ if (typeof value === "object") {
699
+ if (typeof value.toJSON === "function") {
700
+ return canonicalizeJson(value.toJSON(), seen);
701
+ }
702
+ if (seen.has(value)) {
703
+ throw new TypeError("Converting circular structure to JSON");
704
+ }
705
+ seen.add(value);
706
+ try {
707
+ if (Array.isArray(value)) {
708
+ const items = value.map((item) => {
709
+ if (item === void 0 || typeof item === "function" || typeof item === "symbol") {
710
+ return "null";
711
+ }
712
+ const res = canonicalizeJson(item, seen);
713
+ return res === "" ? "null" : res;
714
+ });
715
+ return `[${items.join(",")}]`;
716
+ }
717
+ const keys = Object.keys(value).sort();
718
+ const entries = [];
719
+ for (const key of keys) {
720
+ const val = value[key];
721
+ if (val === void 0 || typeof val === "function" || typeof val === "symbol") {
722
+ continue;
723
+ }
724
+ const serialized = canonicalizeJson(val, seen);
725
+ if (serialized !== "") {
726
+ entries.push(`${JSON.stringify(key)}:${serialized}`);
727
+ }
728
+ }
729
+ return `{${entries.join(",")}}`;
730
+ } finally {
731
+ seen.delete(value);
732
+ }
733
+ }
734
+ return JSON.stringify(value);
735
+ }
675
736
 
676
737
  class AEncryptionProvider {
677
738
  defaultKey;
678
- constructor(key) {
739
+ keys;
740
+ activeKid;
741
+ allowedKids;
742
+ constructor(key, options) {
679
743
  if (key !== void 0) {
680
744
  this.defaultKey = key;
681
745
  }
746
+ if (options?.keys) {
747
+ this.keys = options.keys;
748
+ }
749
+ if (options?.activeKid) {
750
+ this.activeKid = options.activeKid;
751
+ }
752
+ if (options?.allowedKids) {
753
+ this.allowedKids = options.allowedKids;
754
+ }
682
755
  }
683
756
  /**
684
757
  * Sets or updates the default secret key, CryptoKey, or async key provider function.
@@ -692,6 +765,33 @@ class AEncryptionProvider {
692
765
  getDefaultKey() {
693
766
  return this.defaultKey;
694
767
  }
768
+ /**
769
+ * Configures a keyring dictionary of keys with an optional active key identifier.
770
+ */
771
+ setKeyring(keys, activeKid) {
772
+ this.keys = keys;
773
+ if (activeKid !== void 0) {
774
+ this.activeKid = activeKid;
775
+ }
776
+ }
777
+ /**
778
+ * Retrieves the currently configured keyring dictionary.
779
+ */
780
+ getKeyring() {
781
+ return this.keys;
782
+ }
783
+ /**
784
+ * Sets the active Key Identifier (kid) to use for outbound encryption.
785
+ */
786
+ setActiveKid(kid) {
787
+ this.activeKid = kid;
788
+ }
789
+ /**
790
+ * Retrieves the active Key Identifier (kid).
791
+ */
792
+ getActiveKid() {
793
+ return this.activeKid;
794
+ }
695
795
  /**
696
796
  * Pre-derives and caches the native CryptoKey for subsequent cryptographic operations.
697
797
  * Subclasses should override this with a dedicated, non-side-effecting implementation.
@@ -702,7 +802,7 @@ class AEncryptionProvider {
702
802
  * Resolves raw key from keyProvider callback or defaultKey.
703
803
  */
704
804
  async resolveRawKey(keyProvider) {
705
- const target = keyProvider || this.defaultKey;
805
+ const target = keyProvider || (this.keys && this.activeKid ? this.keys[this.activeKid] : void 0) || this.defaultKey;
706
806
  if (!target) {
707
807
  throw new Error(
708
808
  `[${this.constructor.name}] No encryption key or keyProvider was supplied. Key cannot be empty or undefined. A valid key must be supplied from your authentication or secret management system.`
@@ -734,6 +834,9 @@ class AEncryptionProvider {
734
834
  */
735
835
  clearKeys() {
736
836
  this.defaultKey = void 0;
837
+ this.keys = void 0;
838
+ this.activeKid = void 0;
839
+ this.allowedKids = void 0;
737
840
  }
738
841
  /**
739
842
  * Alias for clearKeys() to conform with standard disposal patterns.
@@ -760,16 +863,26 @@ class AEncryptionProvider {
760
863
  */
761
864
  _resolveEffectiveAad(options, isDecrypt = false) {
762
865
  if (!options) return void 0;
763
- if (options.bindRequestContextAad === false) return options.aad;
866
+ let rawAad = options.aad;
867
+ if (typeof rawAad === "function") {
868
+ rawAad = rawAad(void 0, "");
869
+ }
870
+ if (rawAad === void 0 && options.baseAad) {
871
+ rawAad = options.baseAad;
872
+ }
873
+ if (options.bindRequestContextAad === false) return rawAad;
764
874
  const hasReqContext = Boolean(options.url || options.method);
765
- if (options.aad) {
875
+ if (rawAad) {
766
876
  if (options.bindRequestContextAad === true && hasReqContext) {
767
877
  const m = (options.method || "POST").toUpperCase();
768
878
  const u = options.url || "";
769
- const baseAad = typeof options.aad === "string" ? options.aad : options.aad instanceof Uint8Array || options.aad instanceof ArrayBuffer ? textDecoder.decode(options.aad) : String(options.aad);
879
+ const baseAad = typeof rawAad === "string" ? rawAad : rawAad instanceof Uint8Array || rawAad instanceof ArrayBuffer ? textDecoder.decode(rawAad) : String(rawAad);
880
+ if (baseAad.includes(`|req:${m}:${u}`)) {
881
+ return baseAad;
882
+ }
770
883
  return `${baseAad}|req:${m}:${u}`;
771
884
  }
772
- return options.aad;
885
+ return rawAad;
773
886
  }
774
887
  const shouldBindReq = isDecrypt ? hasReqContext : options.bindRequestContextAad === true && hasReqContext;
775
888
  if (shouldBindReq) {
@@ -780,27 +893,169 @@ class AEncryptionProvider {
780
893
  return void 0;
781
894
  }
782
895
  /**
783
- * Helper to deterministically bind record ID and field name into the AAD
784
- * to prevent ciphertext swapping between fields or records.
896
+ * Helper to deterministically bind record ID, contextual sibling fields (aadFields),
897
+ * dynamic AAD callbacks, and field name into the AAD to prevent ciphertext swapping
898
+ * between fields, records, or parent entities.
785
899
  */
786
900
  _bindFieldAad(data, field, options) {
787
901
  let baseAad = "";
902
+ if (options?.baseAad) {
903
+ baseAad = options.baseAad;
904
+ }
788
905
  if (options?.aad) {
789
- if (typeof options.aad === "string") baseAad = options.aad;
790
- else if (options.aad instanceof Uint8Array || options.aad instanceof ArrayBuffer) {
791
- baseAad = textDecoder.decode(options.aad);
906
+ if (typeof options.aad === "function") {
907
+ const dynamicRes = options.aad(data, field);
908
+ if (typeof dynamicRes === "string") {
909
+ baseAad = baseAad ? `${baseAad}|${dynamicRes}` : dynamicRes;
910
+ } else if (dynamicRes instanceof Uint8Array || dynamicRes instanceof ArrayBuffer) {
911
+ const decoded = textDecoder.decode(dynamicRes);
912
+ baseAad = baseAad ? `${baseAad}|${decoded}` : decoded;
913
+ } else if (dynamicRes !== void 0 && dynamicRes !== null) {
914
+ const str = String(dynamicRes);
915
+ baseAad = baseAad ? `${baseAad}|${str}` : str;
916
+ }
917
+ } else if (typeof options.aad === "string") {
918
+ baseAad = baseAad ? `${baseAad}|${options.aad}` : options.aad;
919
+ } else if (options.aad instanceof Uint8Array || options.aad instanceof ArrayBuffer) {
920
+ const decoded = textDecoder.decode(options.aad);
921
+ baseAad = baseAad ? `${baseAad}|${decoded}` : decoded;
792
922
  } else {
793
- baseAad = String(options.aad);
923
+ const str = String(options.aad);
924
+ baseAad = baseAad ? `${baseAad}|${str}` : str;
794
925
  }
795
926
  }
796
927
  const recordId = data && typeof data === "object" && (data.id !== void 0 || data._id !== void 0) ? String(data.id ?? data._id) : "";
797
928
  const recordPart = recordId ? `|record:${recordId}` : "";
929
+ let contextPart = "";
930
+ if (options?.aadFields && options.aadFields.length > 0 && data && typeof data === "object") {
931
+ const sortedFields = [...options.aadFields].sort();
932
+ const parts = [];
933
+ for (const f of sortedFields) {
934
+ const val = data[f];
935
+ let serializedVal = "";
936
+ if (val === null) {
937
+ serializedVal = "null";
938
+ } else if (val !== void 0) {
939
+ serializedVal = typeof val === "object" ? JSON.stringify(val) : String(val);
940
+ }
941
+ parts.push(`${f}=${serializedVal}`);
942
+ }
943
+ contextPart = `|ctx:${parts.join(",")}`;
944
+ }
798
945
  const reqPart = options?.bindRequestContextAad === true && (options?.url || options?.method) ? `|req:${(options.method || "POST").toUpperCase()}:${options.url || ""}` : "";
799
946
  return {
800
947
  ...options,
801
- aad: `aad:${baseAad}${recordPart}${reqPart}|field:${field}`
948
+ bindRequestContextAad: false,
949
+ aad: `aad:${baseAad}${recordPart}${contextPart}${reqPart}|field:${field}`
802
950
  };
803
951
  }
952
+ /**
953
+ * Validates envelope metadata against algorithm whitelists and key identifiers (kid).
954
+ * In strict mode, allowedAlgorithms defaults to ['AES-GCM'].
955
+ */
956
+ _validateEnvelopeMetadata(payload, options) {
957
+ if (!payload || typeof payload !== "object") return void 0;
958
+ const allowedAlgorithms = options?.allowedAlgorithms ?? (options?.strict ? ["AES-GCM"] : void 0);
959
+ if (allowedAlgorithms && allowedAlgorithms.length > 0) {
960
+ const alg = payload.alg;
961
+ if (!alg || !allowedAlgorithms.includes(alg)) {
962
+ return new Error(`[AEncryptionProvider] Algorithm mismatch: algorithm '${alg || "unknown"}' is not in allowedAlgorithms.`);
963
+ }
964
+ }
965
+ const effectiveKeys = options?.keys || this.keys;
966
+ const effectiveAllowedKids = options?.allowedKids || this.allowedKids;
967
+ const allowedKids = effectiveAllowedKids ?? (effectiveKeys ? Object.keys(effectiveKeys) : void 0);
968
+ if (allowedKids && allowedKids.length > 0) {
969
+ if (payload.kid && !allowedKids.includes(payload.kid)) {
970
+ return new Error(`[AEncryptionProvider] Unauthorized key: kid '${payload.kid}' is not in allowedKids.`);
971
+ }
972
+ }
973
+ return void 0;
974
+ }
975
+ /**
976
+ * Normalizes encryption options with active keyring parameters:
977
+ * If `keys` and `activeKid` are configured, selects `key = keys[activeKid]` and sets `kid = activeKid`.
978
+ */
979
+ _resolveOutboundKeyringOptions(options) {
980
+ const effectiveKeys = options?.keys || this.keys;
981
+ const effectiveActiveKid = options?.activeKid || (options?.kid ? options.kid : this.activeKid);
982
+ let effectiveKey = options?.key;
983
+ let effectiveKid = options?.kid || effectiveActiveKid;
984
+ if (effectiveKeys && effectiveActiveKid) {
985
+ if (effectiveKeys[effectiveActiveKid]) {
986
+ effectiveKey = options?.key || effectiveKeys[effectiveActiveKid];
987
+ effectiveKid = effectiveActiveKid;
988
+ } else if (!options?.key) {
989
+ throw new Error(`[${this.constructor.name}] Active kid '${effectiveActiveKid}' not found in keyring keys.`);
990
+ }
991
+ } else if (effectiveKeys && !effectiveKey) {
992
+ const kids = Object.keys(effectiveKeys);
993
+ if (kids.length > 0) {
994
+ effectiveKid = kids[0];
995
+ effectiveKey = effectiveKeys[kids[0]];
996
+ }
997
+ }
998
+ if (effectiveKeys || effectiveActiveKid || effectiveKey !== options?.key || effectiveKid !== options?.kid) {
999
+ return {
1000
+ ...options,
1001
+ ...effectiveKeys ? { keys: effectiveKeys } : {},
1002
+ ...effectiveActiveKid ? { activeKid: effectiveActiveKid } : {},
1003
+ ...effectiveKey !== void 0 ? { key: effectiveKey } : {},
1004
+ ...effectiveKid !== void 0 ? { kid: effectiveKid } : {}
1005
+ };
1006
+ }
1007
+ return options || {};
1008
+ }
1009
+ /**
1010
+ * Resolves the appropriate decryption key for a specific payload envelope based on its `kid`
1011
+ * and the configured keyring (`keys`, `activeKid`, or fallback `key`).
1012
+ */
1013
+ _resolveDecryptionKeyForEnvelope(payload, options) {
1014
+ const effectiveKeys = options?.keys || this.keys;
1015
+ const effectiveActiveKid = options?.activeKid || this.activeKid;
1016
+ if (payload && typeof payload === "object" && payload.kid && effectiveKeys) {
1017
+ if (effectiveKeys[payload.kid]) {
1018
+ return effectiveKeys[payload.kid];
1019
+ }
1020
+ }
1021
+ if (effectiveKeys && effectiveActiveKid && effectiveKeys[effectiveActiveKid]) {
1022
+ return effectiveKeys[effectiveActiveKid];
1023
+ }
1024
+ return options?.key || this.defaultKey;
1025
+ }
1026
+ /**
1027
+ * Handles a per-field or per-payload decryption error according to configured options:
1028
+ * - Invokes onDecryptError callback if provided
1029
+ * - Attaches error metadata (__decryptErrors) to the record object
1030
+ * - Returns fallback cell placeholder ('[⚠ Decryption Error]') or rethrows
1031
+ */
1032
+ _handleDecryptError(err, record, field, value, index, options) {
1033
+ if (record && typeof record === "object" && record !== value) {
1034
+ record.__decryptErrors = {
1035
+ ...record.__decryptErrors || {},
1036
+ [field || "__payload"]: err
1037
+ };
1038
+ }
1039
+ if (options?.onDecryptError === false || options?.onDecryptError === void 0) {
1040
+ throw err;
1041
+ }
1042
+ if (typeof options.onDecryptError === "function") {
1043
+ return options.onDecryptError({
1044
+ record,
1045
+ field,
1046
+ value,
1047
+ error: err,
1048
+ index
1049
+ });
1050
+ }
1051
+ if (typeof options.onDecryptError === "string") {
1052
+ return options.onDecryptError;
1053
+ }
1054
+ if (options.onDecryptError === true) {
1055
+ return "[\u26A0 Decryption Error]";
1056
+ }
1057
+ throw err;
1058
+ }
804
1059
  /**
805
1060
  * Top-level polymorphic encryption method.
806
1061
  * Automatically performs value-level encryption:
@@ -812,6 +1067,7 @@ class AEncryptionProvider {
812
1067
  async encrypt(data, options) {
813
1068
  if (data === void 0 || data === null) return data;
814
1069
  if (this.isEncrypted(data)) return data;
1070
+ options = this._resolveOutboundKeyringOptions(options);
815
1071
  if (options?.valueEncryption === false && (!options.fields || options.fields.length === 0)) {
816
1072
  return this.encryptValue(data, options);
817
1073
  }
@@ -898,65 +1154,106 @@ class AEncryptionProvider {
898
1154
  /**
899
1155
  * Top-level polymorphic decryption method.
900
1156
  * Automatically performs value-level decryption:
901
- * - If payload is an encrypted envelope: decrypts directly via decryptValue.
902
- * - If payload is an array of objects: decrypts each row.
903
- * - If payload is an object: decrypts any encrypted value cells across fields.
904
- */
905
- /**
906
- * Top-level polymorphic decryption method.
907
- * Automatically performs value-level decryption:
908
- * - If payload is an encrypted envelope: decrypts directly via decryptValue.
1157
+ * - If payload is an encrypted envelope: validates metadata and decrypts directly via decryptValue.
909
1158
  * - If payload is an array of objects: decrypts each row (batched with local memoization).
910
1159
  * - If payload is an object: decrypts any encrypted value cells across fields.
1160
+ * In strict mode:
1161
+ * - Unencrypted plaintext in configured `fields` triggers a decryption error.
1162
+ * - Envelopes are validated against `allowedAlgorithms` and `allowedKids`.
1163
+ * - Errors are routed through `onDecryptError` or populated into `__decryptErrors`.
911
1164
  */
912
1165
  async decrypt(payload, options) {
913
1166
  if (payload === void 0 || payload === null) return payload;
914
1167
  if (this.isEncrypted(payload)) {
915
- return this.decryptValue(payload, options);
1168
+ const envelopeKey = this._resolveDecryptionKeyForEnvelope(payload, options);
1169
+ const envelopeOptions = envelopeKey !== options?.key ? { ...options, key: envelopeKey } : options;
1170
+ const metaErr = this._validateEnvelopeMetadata(payload, envelopeOptions);
1171
+ if (metaErr) {
1172
+ return this._handleDecryptError(metaErr, payload, "", payload, void 0, envelopeOptions);
1173
+ }
1174
+ try {
1175
+ return await this.decryptValue(payload, envelopeOptions);
1176
+ } catch (err) {
1177
+ return this._handleDecryptError(err, payload, "", payload, void 0, envelopeOptions);
1178
+ }
916
1179
  }
917
1180
  if (Array.isArray(payload)) {
918
1181
  if (payload.length === 0) return payload;
919
1182
  const localMemo = /* @__PURE__ */ new Map();
920
- const decryptRow = async (item) => {
1183
+ const decryptRow = async (item, rowIndex) => {
921
1184
  if (!item) return item;
922
1185
  if (this.isEncrypted(item)) {
1186
+ const envelopeKey = this._resolveDecryptionKeyForEnvelope(item, options);
1187
+ const envelopeOptions = envelopeKey !== options?.key ? { ...options, key: envelopeKey } : options;
1188
+ const metaErr = this._validateEnvelopeMetadata(item, envelopeOptions);
1189
+ if (metaErr) {
1190
+ return this._handleDecryptError(metaErr, item, "", item, rowIndex, envelopeOptions);
1191
+ }
923
1192
  const ct = item.ciphertext;
924
1193
  const iv = item.iv || "";
925
1194
  const aadPart = options?.aad ? typeof options.aad === "string" ? options.aad : "" : "";
926
- const memoKey = `${ct}:${iv}:${aadPart}`;
927
- if (!localMemo.has(memoKey)) {
928
- localMemo.set(memoKey, this.decryptValue(item, options));
1195
+ const memoKey = `${ct}:${iv}:${aadPart}:${item.kid || ""}`;
1196
+ try {
1197
+ if (!localMemo.has(memoKey)) {
1198
+ localMemo.set(memoKey, this.decryptValue(item, envelopeOptions));
1199
+ }
1200
+ return await localMemo.get(memoKey);
1201
+ } catch (err) {
1202
+ return this._handleDecryptError(err, item, "", item, rowIndex, envelopeOptions);
929
1203
  }
930
- return localMemo.get(memoKey);
931
1204
  }
932
1205
  if (typeof item === "object" && !ArrayBuffer.isView(item) && !(item instanceof ArrayBuffer)) {
933
1206
  const targetKeys = options?.fields && options.fields.length > 0 ? options.fields.filter((f) => Object.prototype.hasOwnProperty.call(item, f)) : Object.keys(item);
934
1207
  const keysToDecrypt = targetKeys.filter((key) => this.isEncrypted(item[key]));
935
- if (keysToDecrypt.length === 0) {
1208
+ const strictViolationKeys = options?.strict === true && options?.fields && options.fields.length > 0 ? targetKeys.filter((key) => item[key] !== void 0 && item[key] !== null && !this.isEncrypted(item[key])) : [];
1209
+ if (keysToDecrypt.length === 0 && strictViolationKeys.length === 0) {
936
1210
  return item;
937
1211
  }
938
1212
  const proto = Object.getPrototypeOf(item);
939
1213
  const result = proto && proto !== Object.prototype ? Object.assign(Object.create(proto), item) : { ...item };
1214
+ for (const key of strictViolationKeys) {
1215
+ const err = new Error(`[AEncryptionProvider] Strict decryption violation: field '${key}' contains unencrypted plaintext.`);
1216
+ const fallbackVal = this._handleDecryptError(err, result, key, item[key], rowIndex, options);
1217
+ safeSetProperty(result, key, fallbackVal);
1218
+ }
940
1219
  await Promise.all(
941
1220
  keysToDecrypt.map(async (key) => {
942
1221
  const env = item[key];
943
- const boundOptions = this._bindFieldAad(item, key, options);
944
- const hasBoundAad = boundOptions.aad !== options?.aad;
1222
+ const envelopeKey = this._resolveDecryptionKeyForEnvelope(env, options);
1223
+ const baseOptions = envelopeKey !== options?.key ? { ...options, key: envelopeKey } : options;
1224
+ const boundOptions = this._bindFieldAad(item, key, baseOptions);
1225
+ const hasBoundAad = boundOptions.aad !== baseOptions?.aad;
1226
+ const metaErr = this._validateEnvelopeMetadata(env, boundOptions);
1227
+ if (metaErr) {
1228
+ const fallbackVal = this._handleDecryptError(metaErr, result, key, env, rowIndex, boundOptions);
1229
+ safeSetProperty(result, key, fallbackVal);
1230
+ return;
1231
+ }
945
1232
  const ct = env.ciphertext;
946
1233
  const iv = env.iv || "";
947
1234
  const aadPart = boundOptions.aad ? typeof boundOptions.aad === "string" ? boundOptions.aad : "" : "";
948
- const memoKey = `${ct}:${iv}:${aadPart}`;
949
- if (!localMemo.has(memoKey)) {
950
- const decryptPromise = this.decryptValue(env, boundOptions).catch((err) => {
951
- if (hasBoundAad && isAuthOrDecryptionError(err)) {
952
- return this.decryptValue(env, options);
953
- }
954
- throw err;
955
- });
956
- localMemo.set(memoKey, decryptPromise);
1235
+ const memoKey = `${ct}:${iv}:${aadPart}:${env.kid || ""}`;
1236
+ try {
1237
+ if (!localMemo.has(memoKey)) {
1238
+ const decryptPromise = (async () => {
1239
+ try {
1240
+ return await this.decryptValue(env, boundOptions);
1241
+ } catch (err) {
1242
+ const canFallback = hasBoundAad && (!options?.aadFields || options.aadFields.length === 0) && typeof options?.aad !== "function";
1243
+ if (canFallback && isAuthOrDecryptionError(err)) {
1244
+ return await this.decryptValue(env, baseOptions);
1245
+ }
1246
+ throw err;
1247
+ }
1248
+ })();
1249
+ localMemo.set(memoKey, decryptPromise);
1250
+ }
1251
+ const decryptedVal = await localMemo.get(memoKey);
1252
+ safeSetProperty(result, key, decryptedVal);
1253
+ } catch (err) {
1254
+ const fallbackVal = this._handleDecryptError(err, result, key, env, rowIndex, boundOptions);
1255
+ safeSetProperty(result, key, fallbackVal);
957
1256
  }
958
- const decryptedVal = await localMemo.get(memoKey);
959
- safeSetProperty(result, key, decryptedVal);
960
1257
  })
961
1258
  );
962
1259
  return result;
@@ -964,14 +1261,14 @@ class AEncryptionProvider {
964
1261
  return item;
965
1262
  };
966
1263
  if (payload.length <= 100) {
967
- return Promise.all(payload.map(decryptRow));
1264
+ return Promise.all(payload.map((item, index) => decryptRow(item, index)));
968
1265
  }
969
1266
  const batchSize = 1e3;
970
1267
  const results = new Array(payload.length);
971
1268
  for (let i = 0; i < payload.length; i += batchSize) {
972
1269
  const end = Math.min(i + batchSize, payload.length);
973
1270
  const chunk = payload.slice(i, end);
974
- const chunkRes = await Promise.all(chunk.map(decryptRow));
1271
+ const chunkRes = await Promise.all(chunk.map((item, localIdx) => decryptRow(item, i + localIdx)));
975
1272
  for (let j = 0; j < chunkRes.length; j++) {
976
1273
  results[i + j] = chunkRes[j];
977
1274
  }
@@ -984,32 +1281,49 @@ class AEncryptionProvider {
984
1281
  if (typeof payload === "object" && !ArrayBuffer.isView(payload) && !(payload instanceof ArrayBuffer)) {
985
1282
  const targetKeys = options?.fields && options.fields.length > 0 ? options.fields.filter((f) => Object.prototype.hasOwnProperty.call(payload, f)) : Object.keys(payload);
986
1283
  const keysToDecrypt = targetKeys.filter((key) => this.isEncrypted(payload[key]));
987
- if (keysToDecrypt.length === 0) {
1284
+ const strictViolationKeys = options?.strict === true && options?.fields && options.fields.length > 0 ? targetKeys.filter((key) => payload[key] !== void 0 && payload[key] !== null && !this.isEncrypted(payload[key])) : [];
1285
+ if (keysToDecrypt.length === 0 && strictViolationKeys.length === 0) {
988
1286
  return payload;
989
1287
  }
990
1288
  const proto = Object.getPrototypeOf(payload);
991
1289
  const result = proto && proto !== Object.prototype ? Object.assign(Object.create(proto), payload) : { ...payload };
992
- const entries = await Promise.all(
1290
+ for (const key of strictViolationKeys) {
1291
+ const err = new Error(`[AEncryptionProvider] Strict decryption violation: field '${key}' contains unencrypted plaintext.`);
1292
+ const fallbackVal = this._handleDecryptError(err, result, key, payload[key], void 0, options);
1293
+ safeSetProperty(result, key, fallbackVal);
1294
+ }
1295
+ await Promise.all(
993
1296
  keysToDecrypt.map(async (key) => {
994
- const boundOptions = this._bindFieldAad(payload, key, options);
995
- const hasBoundAad = boundOptions.aad !== options?.aad;
1297
+ const env = payload[key];
1298
+ const envelopeKey = this._resolveDecryptionKeyForEnvelope(env, options);
1299
+ const baseOptions = envelopeKey !== options?.key ? { ...options, key: envelopeKey } : options;
1300
+ const boundOptions = this._bindFieldAad(payload, key, baseOptions);
1301
+ const hasBoundAad = boundOptions.aad !== baseOptions?.aad;
1302
+ const metaErr = this._validateEnvelopeMetadata(env, boundOptions);
1303
+ if (metaErr) {
1304
+ const fallbackVal = this._handleDecryptError(metaErr, result, key, env, void 0, boundOptions);
1305
+ safeSetProperty(result, key, fallbackVal);
1306
+ return;
1307
+ }
996
1308
  try {
997
- return [key, await this.decryptValue(result[key], boundOptions)];
998
- } catch (err) {
999
- if (hasBoundAad && isAuthOrDecryptionError(err)) {
1000
- try {
1001
- return [key, await this.decryptValue(result[key], options)];
1002
- } catch (fallbackErr) {
1003
- throw fallbackErr;
1309
+ let decryptedVal;
1310
+ try {
1311
+ decryptedVal = await this.decryptValue(env, boundOptions);
1312
+ } catch (err) {
1313
+ const canFallback = hasBoundAad && (!options?.aadFields || options.aadFields.length === 0) && typeof options?.aad !== "function";
1314
+ if (canFallback && isAuthOrDecryptionError(err)) {
1315
+ decryptedVal = await this.decryptValue(env, baseOptions);
1316
+ } else {
1317
+ throw err;
1004
1318
  }
1005
1319
  }
1006
- throw err;
1320
+ safeSetProperty(result, key, decryptedVal);
1321
+ } catch (err) {
1322
+ const fallbackVal = this._handleDecryptError(err, result, key, env, void 0, boundOptions);
1323
+ safeSetProperty(result, key, fallbackVal);
1007
1324
  }
1008
1325
  })
1009
1326
  );
1010
- for (const [key, decryptedVal] of entries) {
1011
- safeSetProperty(result, key, decryptedVal);
1012
- }
1013
1327
  return result;
1014
1328
  }
1015
1329
  return payload;
@@ -1442,6 +1756,46 @@ ${lines.join("\n")}
1442
1756
  `[CKeyManager] Unsupported user key algorithm for unwrapAccountKeyForUser: '${userPrivateKey.algorithm.name}'. Expected 'RSA-OAEP'.`
1443
1757
  );
1444
1758
  }
1759
+ /**
1760
+ * Atomically re-wraps an Account Data Encryption Key (ADEK) for a new recipient
1761
+ * without exposing the raw extractable symmetric key to caller application code.
1762
+ *
1763
+ * @param wrappedKeyBase64 - The existing ADEK envelope wrapped with current user's public key
1764
+ * @param myPrivateKey - Current user's RSA-OAEP private key
1765
+ * @param recipientPublicKey - New recipient's RSA-OAEP public key
1766
+ * @returns Base64-encoded wrapped key envelope for the recipient
1767
+ */
1768
+ static async rewrapAccountKeyForUser(wrappedKeyBase64, myPrivateKey, recipientPublicKey) {
1769
+ const subtle = getSubtleCrypto();
1770
+ if (!myPrivateKey || !myPrivateKey.algorithm || myPrivateKey.algorithm.name !== "RSA-OAEP" || !recipientPublicKey || !recipientPublicKey.algorithm || recipientPublicKey.algorithm.name !== "RSA-OAEP") {
1771
+ throw new Error(`[CKeyManager] rewrapAccountKeyForUser requires RSA-OAEP keys.`);
1772
+ }
1773
+ if (myPrivateKey.type === "public" || recipientPublicKey.type === "private") {
1774
+ throw new Error(
1775
+ `[CKeyManager] rewrapAccountKeyForUser requires an RSA-OAEP private key for decryption and an RSA-OAEP public key for encryption.`
1776
+ );
1777
+ }
1778
+ const wrappedBytes = CKeyManager.fromBase64(wrappedKeyBase64);
1779
+ let rawKeyBuffer;
1780
+ try {
1781
+ rawKeyBuffer = await subtle.decrypt(
1782
+ { name: "RSA-OAEP" },
1783
+ myPrivateKey,
1784
+ wrappedBytes
1785
+ );
1786
+ const rewrappedBuffer = await subtle.encrypt(
1787
+ { name: "RSA-OAEP" },
1788
+ recipientPublicKey,
1789
+ rawKeyBuffer
1790
+ );
1791
+ return CKeyManager.toBase64(rewrappedBuffer);
1792
+ } finally {
1793
+ wrappedBytes.fill(0);
1794
+ if (rawKeyBuffer) {
1795
+ new Uint8Array(rawKeyBuffer).fill(0);
1796
+ }
1797
+ }
1798
+ }
1445
1799
  // ── Key Derivation Functions (PBKDF2 & HKDF) ──────────────────────────────
1446
1800
  /**
1447
1801
  * Derives a symmetric AES-GCM CryptoKey from a passphrase using PBKDF2 or HKDF.
@@ -1545,6 +1899,299 @@ ${lines.join("\n")}
1545
1899
  ["encrypt", "decrypt"]
1546
1900
  );
1547
1901
  }
1902
+ /**
1903
+ * Derives two domain-separated, cryptographically isolated keys from a user password and salt:
1904
+ * 1. `authKey`: For client authentication, SRP, or server challenge proofs.
1905
+ * 2. `encryptionKey`: For AES-GCM local and field-level data encryption.
1906
+ *
1907
+ * Stage 1 (Password Stretching): PBKDF2-HMAC-SHA256 stretches low-entropy password into master secret.
1908
+ * Stage 2 (Domain Expansion): HKDF-Expand derives subkeys using domain-separated context info tags.
1909
+ * Immediate memory zeroization of intermediate master secret buffer is guaranteed.
1910
+ *
1911
+ * @param password - Master user password or passphrase
1912
+ * @param salt - Salt (or omitted to generate a secure random 16-byte salt)
1913
+ * @param options - PBKDF2 and HKDF configuration options
1914
+ */
1915
+ static async deriveAuthAndEncryptionKeys(password, salt, options = {}) {
1916
+ if (!password || typeof password === "string" && password.trim().length === 0 || typeof password === "object" && "byteLength" in password && password.byteLength === 0) {
1917
+ throw new Error("[CKeyManager] Password cannot be empty or undefined for key derivation.");
1918
+ }
1919
+ const subtle = getSubtleCrypto();
1920
+ let saltBytes;
1921
+ if (salt !== void 0) {
1922
+ saltBytes = CKeyManager.resolveSalt(salt);
1923
+ } else {
1924
+ saltBytes = CKeyManager.randomBytes(16);
1925
+ }
1926
+ const saltBase64 = CKeyManager.toBase64(saltBytes);
1927
+ const iterations = options.iterations || 31e4;
1928
+ const hash = options.hash || "SHA-256";
1929
+ const authKeyLength = options.authKeyLength || 256;
1930
+ const encryptionKeyLength = options.encryptionKeyLength || 256;
1931
+ const authAlgorithm = options.authAlgorithm || "HMAC";
1932
+ const authInfo = resolveInfoBytes(options.authInfo, "katlux-auth-key-v1");
1933
+ const encryptionInfo = resolveInfoBytes(options.encryptionInfo, "katlux-encryption-key-v1");
1934
+ const extractable = options.extractable === true;
1935
+ const rawPasswordBytes = typeof password === "string" ? textEncoder$1.encode(password) : password instanceof Uint8Array ? new Uint8Array(password.buffer, password.byteOffset, password.byteLength) : new Uint8Array(password);
1936
+ let masterBits;
1937
+ try {
1938
+ const baseKey = await subtle.importKey(
1939
+ "raw",
1940
+ rawPasswordBytes,
1941
+ "PBKDF2",
1942
+ false,
1943
+ ["deriveBits"]
1944
+ );
1945
+ const bitsLength = hash === "SHA-512" ? 512 : 256;
1946
+ masterBits = await subtle.deriveBits(
1947
+ {
1948
+ name: "PBKDF2",
1949
+ salt: saltBytes,
1950
+ iterations,
1951
+ hash
1952
+ },
1953
+ baseKey,
1954
+ bitsLength
1955
+ );
1956
+ const hkdfKey = await subtle.importKey(
1957
+ "raw",
1958
+ masterBits,
1959
+ "HKDF",
1960
+ false,
1961
+ ["deriveKey"]
1962
+ );
1963
+ const authDerivedAlgorithm = authAlgorithm === "AES-GCM" ? { name: "AES-GCM", length: authKeyLength } : { name: "HMAC", hash: { name: hash }, length: authKeyLength };
1964
+ const authUsages = authAlgorithm === "AES-GCM" ? ["encrypt", "decrypt"] : ["sign", "verify"];
1965
+ const authKey = await subtle.deriveKey(
1966
+ {
1967
+ name: "HKDF",
1968
+ hash,
1969
+ salt: new Uint8Array(0),
1970
+ info: authInfo
1971
+ },
1972
+ hkdfKey,
1973
+ authDerivedAlgorithm,
1974
+ extractable,
1975
+ authUsages
1976
+ );
1977
+ const encryptionKey = await subtle.deriveKey(
1978
+ {
1979
+ name: "HKDF",
1980
+ hash,
1981
+ salt: new Uint8Array(0),
1982
+ info: encryptionInfo
1983
+ },
1984
+ hkdfKey,
1985
+ { name: "AES-GCM", length: encryptionKeyLength },
1986
+ extractable,
1987
+ ["encrypt", "decrypt"]
1988
+ );
1989
+ return {
1990
+ authKey,
1991
+ encryptionKey,
1992
+ salt: saltBase64
1993
+ };
1994
+ } finally {
1995
+ if (typeof password === "string") {
1996
+ rawPasswordBytes.fill(0);
1997
+ }
1998
+ if (masterBits) {
1999
+ new Uint8Array(masterBits).fill(0);
2000
+ }
2001
+ }
2002
+ }
2003
+ // ── Digital Signatures, Fingerprints & Access Grants ─────────────────────
2004
+ /**
2005
+ * Generates an ECDSA P-256 signing key pair.
2006
+ */
2007
+ static async generateSigningKeyPair(curve = "P-256", extractable = true) {
2008
+ const subtle = getSubtleCrypto();
2009
+ const pair = await subtle.generateKey(
2010
+ {
2011
+ name: "ECDSA",
2012
+ namedCurve: curve
2013
+ },
2014
+ extractable,
2015
+ ["sign", "verify"]
2016
+ );
2017
+ return {
2018
+ publicKey: pair.publicKey,
2019
+ privateKey: pair.privateKey
2020
+ };
2021
+ }
2022
+ /**
2023
+ * Generates a deterministic SHA-256 fingerprint for a public key (SPKI or raw).
2024
+ * Formatted as `SHA256:<base64url>`.
2025
+ */
2026
+ static async getKeyFingerprint(publicKey) {
2027
+ const subtle = getSubtleCrypto();
2028
+ let rawBytes;
2029
+ if (typeof publicKey === "object" && publicKey !== null && "algorithm" in publicKey) {
2030
+ const cryptoKey = publicKey;
2031
+ rawBytes = await subtle.exportKey("spki", cryptoKey);
2032
+ } else if (publicKey instanceof Uint8Array) {
2033
+ rawBytes = publicKey;
2034
+ } else if (publicKey instanceof ArrayBuffer) {
2035
+ rawBytes = publicKey;
2036
+ } else if (typeof publicKey === "string") {
2037
+ const trimmed = publicKey.trim();
2038
+ if (trimmed.startsWith("-----BEGIN")) {
2039
+ const b642 = trimmed.replace(/-----[^-]+-----/g, "").replace(/\s+/g, "");
2040
+ rawBytes = CKeyManager.fromBase64(b642);
2041
+ } else {
2042
+ try {
2043
+ rawBytes = CKeyManager.fromBase64(trimmed);
2044
+ } catch {
2045
+ rawBytes = textEncoder$1.encode(trimmed);
2046
+ }
2047
+ }
2048
+ } else {
2049
+ throw new Error(
2050
+ "[CKeyManager] Invalid key input for getKeyFingerprint: expected CryptoKey, Uint8Array, ArrayBuffer, or string."
2051
+ );
2052
+ }
2053
+ const hashBuffer = await subtle.digest("SHA-256", rawBytes);
2054
+ const b64 = CKeyManager.toBase64(hashBuffer);
2055
+ const b64url = b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
2056
+ return `SHA256:${b64url}`;
2057
+ }
2058
+ /**
2059
+ * Signs arbitrary data or JSON objects using an ECDSA P-256 private key.
2060
+ * Objects are automatically serialized via canonicalizeJson() before hashing.
2061
+ */
2062
+ static async sign(data, privateKey) {
2063
+ const subtle = getSubtleCrypto();
2064
+ if (!privateKey || !privateKey.algorithm || privateKey.algorithm.name !== "ECDSA") {
2065
+ throw new Error("[CKeyManager] sign requires an ECDSA private key.");
2066
+ }
2067
+ let dataBytes;
2068
+ if (data instanceof Uint8Array) {
2069
+ dataBytes = data;
2070
+ } else if (ArrayBuffer.isView(data)) {
2071
+ dataBytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
2072
+ } else if (data instanceof ArrayBuffer) {
2073
+ dataBytes = new Uint8Array(data);
2074
+ } else if (typeof data === "string") {
2075
+ dataBytes = textEncoder$1.encode(data);
2076
+ } else {
2077
+ const canonical = canonicalizeJson(data);
2078
+ dataBytes = textEncoder$1.encode(canonical);
2079
+ }
2080
+ const namedCurve = privateKey.algorithm.namedCurve || "P-256";
2081
+ const hashName = namedCurve === "P-384" ? "SHA-384" : namedCurve === "P-521" ? "SHA-512" : "SHA-256";
2082
+ const signatureBuffer = await subtle.sign(
2083
+ {
2084
+ name: "ECDSA",
2085
+ hash: { name: hashName }
2086
+ },
2087
+ privateKey,
2088
+ dataBytes
2089
+ );
2090
+ return CKeyManager.toBase64(signatureBuffer);
2091
+ }
2092
+ /**
2093
+ * Verifies an ECDSA P-256 signature against original data or JSON object.
2094
+ */
2095
+ static async verify(data, signature, publicKey) {
2096
+ const subtle = getSubtleCrypto();
2097
+ if (!publicKey || !publicKey.algorithm || publicKey.algorithm.name !== "ECDSA") {
2098
+ throw new Error("[CKeyManager] verify requires an ECDSA public key.");
2099
+ }
2100
+ if (typeof signature !== "string" || !signature.trim()) {
2101
+ return false;
2102
+ }
2103
+ try {
2104
+ const signatureBytes = CKeyManager.fromBase64(signature);
2105
+ let dataBytes;
2106
+ if (data instanceof Uint8Array) {
2107
+ dataBytes = data;
2108
+ } else if (ArrayBuffer.isView(data)) {
2109
+ dataBytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
2110
+ } else if (data instanceof ArrayBuffer) {
2111
+ dataBytes = new Uint8Array(data);
2112
+ } else if (typeof data === "string") {
2113
+ dataBytes = textEncoder$1.encode(data);
2114
+ } else {
2115
+ const canonical = canonicalizeJson(data);
2116
+ dataBytes = textEncoder$1.encode(canonical);
2117
+ }
2118
+ const namedCurve = publicKey.algorithm.namedCurve || "P-256";
2119
+ const hashName = namedCurve === "P-384" ? "SHA-384" : namedCurve === "P-521" ? "SHA-512" : "SHA-256";
2120
+ return await subtle.verify(
2121
+ {
2122
+ name: "ECDSA",
2123
+ hash: { name: hashName }
2124
+ },
2125
+ publicKey,
2126
+ signatureBytes,
2127
+ dataBytes
2128
+ );
2129
+ } catch {
2130
+ return false;
2131
+ }
2132
+ }
2133
+ /**
2134
+ * Signs a structured access grant, embedding signature and granter key fingerprint.
2135
+ */
2136
+ static async signGrant(grant, granterPrivateKey, granterPublicKey) {
2137
+ if (!grant || typeof grant !== "object") {
2138
+ throw new Error("[CKeyManager] signGrant requires a valid grant object.");
2139
+ }
2140
+ let fingerprint;
2141
+ if (granterPublicKey) {
2142
+ fingerprint = await CKeyManager.getKeyFingerprint(granterPublicKey);
2143
+ } else {
2144
+ const subtle = getSubtleCrypto();
2145
+ try {
2146
+ const jwk = await subtle.exportKey("jwk", granterPrivateKey);
2147
+ const pubJwk = {
2148
+ kty: jwk.kty,
2149
+ crv: jwk.crv,
2150
+ x: jwk.x,
2151
+ y: jwk.y,
2152
+ ext: true,
2153
+ key_ops: ["verify"]
2154
+ };
2155
+ const pubKey = await subtle.importKey(
2156
+ "jwk",
2157
+ pubJwk,
2158
+ { name: "ECDSA", namedCurve: jwk.crv || "P-256" },
2159
+ true,
2160
+ ["verify"]
2161
+ );
2162
+ fingerprint = await CKeyManager.getKeyFingerprint(pubKey);
2163
+ } catch {
2164
+ throw new Error(
2165
+ "[CKeyManager] signGrant requires granterPublicKey when granterPrivateKey is non-extractable."
2166
+ );
2167
+ }
2168
+ }
2169
+ const signature = await CKeyManager.sign(grant, granterPrivateKey);
2170
+ return {
2171
+ grant: { ...grant },
2172
+ signature,
2173
+ fingerprint
2174
+ };
2175
+ }
2176
+ /**
2177
+ * Verifies the authenticity and integrity of a signed access grant against granter public key.
2178
+ */
2179
+ static async verifyGrant(signedGrant, granterPublicKey, options) {
2180
+ if (!signedGrant || !signedGrant.grant || !signedGrant.signature || !signedGrant.fingerprint || !granterPublicKey) {
2181
+ return false;
2182
+ }
2183
+ const expectedFingerprint = await CKeyManager.getKeyFingerprint(granterPublicKey);
2184
+ if (signedGrant.fingerprint !== expectedFingerprint) {
2185
+ return false;
2186
+ }
2187
+ if (options?.checkExpiry && signedGrant.grant.expiresAt !== void 0) {
2188
+ const now = options.now ?? Date.now();
2189
+ if (now > signedGrant.grant.expiresAt) {
2190
+ return false;
2191
+ }
2192
+ }
2193
+ return await CKeyManager.verify(signedGrant.grant, signedGrant.signature, granterPublicKey);
2194
+ }
1548
2195
  }
1549
2196
 
1550
2197
  class CWebCryptoEncryptionProvider extends AEncryptionProvider {
@@ -1558,6 +2205,7 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
1558
2205
  _cachedKeyId;
1559
2206
  _configuredKid;
1560
2207
  _decryptSaltCache = /* @__PURE__ */ new Map();
2208
+ _keyringCache = /* @__PURE__ */ new Map();
1561
2209
  setDefaultKey(key) {
1562
2210
  super.setDefaultKey(key);
1563
2211
  this._cachedKey = void 0;
@@ -1567,6 +2215,7 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
1567
2215
  this._cachedHash = void 0;
1568
2216
  this._cachedKeyId = void 0;
1569
2217
  this._decryptSaltCache.clear();
2218
+ this._keyringCache.clear();
1570
2219
  }
1571
2220
  clearKeys() {
1572
2221
  super.clearKeys();
@@ -1577,9 +2226,10 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
1577
2226
  this._cachedHash = void 0;
1578
2227
  this._cachedKeyId = void 0;
1579
2228
  this._decryptSaltCache.clear();
2229
+ this._keyringCache.clear();
1580
2230
  }
1581
2231
  constructor(key, options) {
1582
- super(key);
2232
+ super(key, options);
1583
2233
  if (options) {
1584
2234
  if (options.kid || options.keyId) {
1585
2235
  this._configuredKid = options.kid || options.keyId;
@@ -1629,14 +2279,21 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
1629
2279
  }
1630
2280
  async _resolveCryptoKeyInternal(keyProvider, kdfOverride, saltOverride, options, iterationsOverride) {
1631
2281
  const rawKey = await this.resolveRawKey(keyProvider);
2282
+ const currentKeyId = getSafeKeyIdentifier(rawKey);
1632
2283
  if (typeof rawKey !== "string" && rawKey.algorithm) {
1633
- if (!keyProvider) {
2284
+ const res2 = { cryptoKey: rawKey, kdf: "direct" };
2285
+ this._keyringCache.set(currentKeyId, res2);
2286
+ if (!keyProvider || keyProvider === this.defaultKey) {
1634
2287
  this._cachedKey = rawKey;
1635
2288
  this._cachedKdf = "direct";
1636
2289
  }
1637
- return { cryptoKey: rawKey, kdf: "direct" };
2290
+ return res2;
1638
2291
  }
1639
2292
  if (rawKey instanceof Uint8Array || rawKey instanceof ArrayBuffer) {
2293
+ const cached = this._keyringCache.get(currentKeyId);
2294
+ if (cached) {
2295
+ return cached;
2296
+ }
1640
2297
  if (!keyProvider && this._cachedKey && this._cachedKdf === "raw") {
1641
2298
  return { cryptoKey: this._cachedKey, kdf: "raw" };
1642
2299
  }
@@ -1653,11 +2310,13 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
1653
2310
  false,
1654
2311
  ["encrypt", "decrypt"]
1655
2312
  );
1656
- if (!keyProvider) {
2313
+ const res2 = { cryptoKey: cryptoKey2, kdf: "raw" };
2314
+ this._keyringCache.set(currentKeyId, res2);
2315
+ if (!keyProvider || keyProvider === this.defaultKey) {
1657
2316
  this._cachedKey = cryptoKey2;
1658
2317
  this._cachedKdf = "raw";
1659
2318
  }
1660
- return { cryptoKey: cryptoKey2, kdf: "raw" };
2319
+ return res2;
1661
2320
  }
1662
2321
  const passphrase = rawKey;
1663
2322
  const kdf = (kdfOverride || options?.kdf || this._defaultKdf || "pbkdf2").toLowerCase();
@@ -1667,12 +2326,21 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
1667
2326
  const matchesHash = !targetHash || this._cachedHash !== void 0 && targetHash.toLowerCase() === this._cachedHash.toLowerCase();
1668
2327
  const matchesKdf = this._cachedKdf !== void 0 && kdf === this._cachedKdf.toLowerCase();
1669
2328
  const matchesSalt = !saltOverride || this._cachedSalt !== void 0 && saltOverride === this._cachedSalt;
1670
- const currentKeyId = getSafeKeyIdentifier(rawKey);
1671
2329
  if (this._cachedKey && matchesSalt && !options?.kdfSalt && matchesKdf && matchesIter && matchesHash) {
1672
2330
  if (this._cachedKeyId === currentKeyId) {
1673
2331
  return { cryptoKey: this._cachedKey, salt: this._cachedSalt, kdf: this._cachedKdf, iter: this._cachedIter };
1674
2332
  }
1675
2333
  }
2334
+ if (!saltOverride && !options?.kdfSalt) {
2335
+ const cached = this._keyringCache.get(currentKeyId);
2336
+ if (cached) {
2337
+ const cMatchesKdf = !cached.kdf || cached.kdf.toLowerCase() === kdf;
2338
+ const cMatchesIter = !targetIter || cached.iter === targetIter;
2339
+ if (cMatchesKdf && cMatchesIter) {
2340
+ return cached;
2341
+ }
2342
+ }
2343
+ }
1676
2344
  if (kdf === "pbkdf2") {
1677
2345
  const salt = saltOverride || options?.kdfSalt || this._kdfOptions?.salt;
1678
2346
  const rawIterations = iterationsOverride || options?.kdfIterations || this._kdfOptions?.iterations || 31e4;
@@ -1684,15 +2352,19 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
1684
2352
  iterations,
1685
2353
  hash
1686
2354
  });
2355
+ const res2 = { cryptoKey: derived.key, salt: derived.salt, kdf: "PBKDF2", iter: iterations };
1687
2356
  if (!saltOverride && !options?.kdfSalt) {
1688
- this._cachedKey = derived.key;
1689
- this._cachedSalt = derived.salt;
1690
- this._cachedKdf = "PBKDF2";
1691
- this._cachedIter = iterations;
1692
- this._cachedHash = hash;
1693
- this._cachedKeyId = currentKeyId;
2357
+ this._keyringCache.set(currentKeyId, res2);
2358
+ if (!keyProvider || keyProvider === this.defaultKey) {
2359
+ this._cachedKey = derived.key;
2360
+ this._cachedSalt = derived.salt;
2361
+ this._cachedKdf = "PBKDF2";
2362
+ this._cachedIter = iterations;
2363
+ this._cachedHash = hash;
2364
+ this._cachedKeyId = currentKeyId;
2365
+ }
1694
2366
  }
1695
- return { cryptoKey: derived.key, salt: derived.salt, kdf: "PBKDF2", iter: iterations };
2367
+ return res2;
1696
2368
  }
1697
2369
  if (kdf === "hkdf") {
1698
2370
  if (typeof rawKey === "string") {
@@ -1709,14 +2381,18 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
1709
2381
  hash,
1710
2382
  info
1711
2383
  });
2384
+ const res2 = { cryptoKey: derived.key, salt: derived.salt, kdf: "HKDF" };
1712
2385
  if (!saltOverride && !options?.kdfSalt) {
1713
- this._cachedKey = derived.key;
1714
- this._cachedSalt = derived.salt;
1715
- this._cachedKdf = "HKDF";
1716
- this._cachedHash = hash;
1717
- this._cachedKeyId = currentKeyId;
2386
+ this._keyringCache.set(currentKeyId, res2);
2387
+ if (!keyProvider || keyProvider === this.defaultKey) {
2388
+ this._cachedKey = derived.key;
2389
+ this._cachedSalt = derived.salt;
2390
+ this._cachedKdf = "HKDF";
2391
+ this._cachedHash = hash;
2392
+ this._cachedKeyId = currentKeyId;
2393
+ }
1718
2394
  }
1719
- return { cryptoKey: derived.key, salt: derived.salt, kdf: "HKDF" };
2395
+ return res2;
1720
2396
  }
1721
2397
  if (kdf === "none") {
1722
2398
  if (saltOverride) {
@@ -1748,11 +2424,13 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
1748
2424
  } finally {
1749
2425
  new Uint8Array(keyHash).fill(0);
1750
2426
  }
1751
- if (!keyProvider) {
2427
+ const res = { cryptoKey, kdf: "none" };
2428
+ this._keyringCache.set(currentKeyId, res);
2429
+ if (!keyProvider || keyProvider === this.defaultKey) {
1752
2430
  this._cachedKey = cryptoKey;
1753
2431
  this._cachedKdf = "none";
1754
2432
  }
1755
- return { cryptoKey, kdf: "none" };
2433
+ return res;
1756
2434
  }
1757
2435
  static isEncrypted(payload) {
1758
2436
  if (!payload || typeof payload !== "object") return false;
@@ -1762,6 +2440,29 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
1762
2440
  return CWebCryptoEncryptionProvider.isEncrypted(payload);
1763
2441
  }
1764
2442
  async warmupKey(options) {
2443
+ const effectiveKeys = options?.keys || this.keys;
2444
+ if (effectiveKeys && Object.keys(effectiveKeys).length > 0) {
2445
+ const promises = Object.entries(effectiveKeys).map(async ([kid, keyProv]) => {
2446
+ const res2 = await this.resolveCryptoKey(keyProv, void 0, void 0, { ...options, key: keyProv, kid });
2447
+ const rawKey = await this.resolveRawKey(keyProv);
2448
+ const keyId = getSafeKeyIdentifier(rawKey);
2449
+ this._keyringCache.set(keyId, res2);
2450
+ this._keyringCache.set(`kid:${kid}`, res2);
2451
+ return { kid, res: res2 };
2452
+ });
2453
+ const results = await Promise.all(promises);
2454
+ const effectiveActiveKid = options?.activeKid || this.activeKid;
2455
+ const primary = effectiveActiveKid && results.find((r) => r.kid === effectiveActiveKid)?.res || results[0]?.res;
2456
+ if (primary) {
2457
+ this._cachedKey = primary.cryptoKey;
2458
+ this._cachedSalt = primary.salt;
2459
+ this._cachedKdf = primary.kdf;
2460
+ this._cachedIter = primary.iter;
2461
+ this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
2462
+ this._cachedKeyId = getKeyIdentifier(options?.key, this.defaultKey);
2463
+ }
2464
+ return;
2465
+ }
1765
2466
  const res = await this.resolveCryptoKey(options?.key, void 0, void 0, options);
1766
2467
  this._cachedKey = res.cryptoKey;
1767
2468
  this._cachedSalt = res.salt;
@@ -1804,7 +2505,7 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
1804
2505
  cryptoKey,
1805
2506
  textEncoder$1.encode(jsonString)
1806
2507
  );
1807
- const kid = options?.kid || options?.keyId || this._configuredKid;
2508
+ const kid = options?.kid || options?.keyId || this.activeKid || this._configuredKid;
1808
2509
  return {
1809
2510
  __encrypted: true,
1810
2511
  ciphertext: CKeyManager.toBase64(ciphertextBuffer),
@@ -3127,12 +3828,26 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3127
3828
  _cachedHash;
3128
3829
  _cachedKeyId;
3129
3830
  _configuredKid;
3831
+ _keyringCache = /* @__PURE__ */ new Map();
3832
+ isKeyRetained(key) {
3833
+ if (key === this._cachedKey) return true;
3834
+ for (const entry of this._keyringCache.values()) {
3835
+ if (entry.key === key) return true;
3836
+ }
3837
+ return false;
3838
+ }
3130
3839
  setDefaultKey(key) {
3131
3840
  super.setDefaultKey(key);
3132
3841
  if (this._cachedKey) {
3133
3842
  this._cachedKey.fill(0);
3134
3843
  this._cachedKey = void 0;
3135
3844
  }
3845
+ for (const entry of this._keyringCache.values()) {
3846
+ if (entry.key !== this._cachedKey) {
3847
+ entry.key.fill(0);
3848
+ }
3849
+ }
3850
+ this._keyringCache.clear();
3136
3851
  this._cachedSalt = void 0;
3137
3852
  this._cachedKdf = void 0;
3138
3853
  this._cachedIter = void 0;
@@ -3145,6 +3860,12 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3145
3860
  this._cachedKey.fill(0);
3146
3861
  this._cachedKey = void 0;
3147
3862
  }
3863
+ for (const entry of this._keyringCache.values()) {
3864
+ if (entry.key !== this._cachedKey) {
3865
+ entry.key.fill(0);
3866
+ }
3867
+ }
3868
+ this._keyringCache.clear();
3148
3869
  this._cachedSalt = void 0;
3149
3870
  this._cachedKdf = void 0;
3150
3871
  this._cachedIter = void 0;
@@ -3152,6 +3873,29 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3152
3873
  this._cachedKeyId = void 0;
3153
3874
  }
3154
3875
  async warmupKey(options) {
3876
+ const effectiveKeys = options?.keys || this.keys;
3877
+ if (effectiveKeys && Object.keys(effectiveKeys).length > 0) {
3878
+ const promises = Object.entries(effectiveKeys).map(async ([kid, keyProv]) => {
3879
+ const res2 = await this.deriveKeyAsync(keyProv, void 0, void 0, { ...options, key: keyProv, kid });
3880
+ const raw = await this.resolveRawKeyAsync(keyProv);
3881
+ const keyId = getSafeKeyIdentifier(raw);
3882
+ this._keyringCache.set(keyId, res2);
3883
+ this._keyringCache.set(`kid:${kid}`, res2);
3884
+ return { kid, res: res2 };
3885
+ });
3886
+ const results = await Promise.all(promises);
3887
+ const effectiveActiveKid = options?.activeKid || this.activeKid;
3888
+ const primary = effectiveActiveKid && results.find((r) => r.kid === effectiveActiveKid)?.res || results[0]?.res;
3889
+ if (primary) {
3890
+ this._cachedKey = primary.key;
3891
+ this._cachedSalt = primary.salt;
3892
+ this._cachedKdf = primary.kdf;
3893
+ this._cachedIter = primary.iter;
3894
+ this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
3895
+ this._cachedKeyId = getSafeKeyIdentifier(await this.resolveRawKeyAsync(options?.key));
3896
+ }
3897
+ return;
3898
+ }
3155
3899
  const res = await this.deriveKeyAsync(options?.key, void 0, void 0, options);
3156
3900
  if (!options?.key) {
3157
3901
  this._cachedKey = res.key;
@@ -3166,6 +3910,30 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3166
3910
  * Synchronously pre-derives and caches the encryption key for subsequent synchronous operations.
3167
3911
  */
3168
3912
  warmupKeySync(options) {
3913
+ const effectiveKeys = options?.keys || this.keys;
3914
+ if (effectiveKeys && Object.keys(effectiveKeys).length > 0) {
3915
+ const entries = Object.entries(effectiveKeys);
3916
+ const results = [];
3917
+ for (const [kid, keyProv] of entries) {
3918
+ const res2 = this.deriveKeySync(keyProv, void 0, void 0, { ...options, key: keyProv, kid });
3919
+ const raw = this.resolveRawKeySync(keyProv);
3920
+ const keyId = getSafeKeyIdentifier(raw);
3921
+ this._keyringCache.set(keyId, res2);
3922
+ this._keyringCache.set(`kid:${kid}`, res2);
3923
+ results.push({ kid, res: res2 });
3924
+ }
3925
+ const effectiveActiveKid = options?.activeKid || this.activeKid;
3926
+ const primary = effectiveActiveKid && results.find((r) => r.kid === effectiveActiveKid)?.res || results[0]?.res;
3927
+ if (primary) {
3928
+ this._cachedKey = primary.key;
3929
+ this._cachedSalt = primary.salt;
3930
+ this._cachedKdf = primary.kdf;
3931
+ this._cachedIter = primary.iter;
3932
+ this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
3933
+ this._cachedKeyId = getSafeKeyIdentifier(this.resolveRawKeySync(options?.key));
3934
+ }
3935
+ return;
3936
+ }
3169
3937
  const res = this.deriveKeySync(options?.key, void 0, void 0, options);
3170
3938
  if (!options?.key) {
3171
3939
  this._cachedKey = res.key;
@@ -3184,7 +3952,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3184
3952
  return Buffer.from(infoRaw);
3185
3953
  }
3186
3954
  constructor(key, options) {
3187
- super(key);
3955
+ super(key, options);
3188
3956
  if (options) {
3189
3957
  if (options.kid || options.keyId) {
3190
3958
  this._configuredKid = options.kid || options.keyId;
@@ -3202,7 +3970,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3202
3970
  }
3203
3971
  }
3204
3972
  resolveRawKeySync(keyProvider) {
3205
- const target = keyProvider || this.defaultKey;
3973
+ const target = keyProvider || (this.activeKid && this.keys ? this.keys[this.activeKid] : void 0) || this.defaultKey;
3206
3974
  if (!target) {
3207
3975
  throw new Error("[CNodeCryptoEncryptionProvider] No encryption key or keyProvider was supplied.");
3208
3976
  }
@@ -3232,7 +4000,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3232
4000
  return rawKey;
3233
4001
  }
3234
4002
  async resolveRawKeyAsync(keyProvider) {
3235
- const target = keyProvider || this.defaultKey;
4003
+ const target = keyProvider || (this.activeKid && this.keys ? this.keys[this.activeKid] : void 0) || this.defaultKey;
3236
4004
  if (!target) {
3237
4005
  throw new Error("[CNodeCryptoEncryptionProvider] No encryption key or keyProvider was supplied.");
3238
4006
  }
@@ -3263,15 +4031,16 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3263
4031
  const currentKeyId = getSafeKeyIdentifier(raw);
3264
4032
  if (Buffer.isBuffer(raw)) {
3265
4033
  if (raw.length === 32) {
3266
- if (!keyProvider) {
3267
- if (!this._cachedKey || this._cachedKeyId !== currentKeyId) {
3268
- this._cachedKey = Buffer.from(raw);
3269
- this._cachedKdf = "raw";
3270
- this._cachedKeyId = currentKeyId;
3271
- }
3272
- return { key: this._cachedKey, kdf: "raw" };
4034
+ const cached = this._keyringCache.get(currentKeyId);
4035
+ if (cached) return cached;
4036
+ const res2 = { key: Buffer.from(raw), kdf: "raw" };
4037
+ this._keyringCache.set(currentKeyId, res2);
4038
+ if (!keyProvider || keyProvider === this.defaultKey) {
4039
+ this._cachedKey = res2.key;
4040
+ this._cachedKdf = "raw";
4041
+ this._cachedKeyId = currentKeyId;
3273
4042
  }
3274
- return { key: Buffer.from(raw), kdf: "raw" };
4043
+ return res2;
3275
4044
  }
3276
4045
  throw new Error(
3277
4046
  `[CNodeCryptoEncryptionProvider] AES-GCM-256 requires a 32-byte key. Received a Buffer of ${raw.length} bytes. Provide exactly 32 bytes or a string passphrase.`
@@ -3290,6 +4059,16 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3290
4059
  return { key: this._cachedKey, salt: this._cachedSalt, kdf: this._cachedKdf, iter: this._cachedIter };
3291
4060
  }
3292
4061
  }
4062
+ if (!saltOverride && !options?.kdfSalt) {
4063
+ const cached = this._keyringCache.get(currentKeyId);
4064
+ if (cached) {
4065
+ const cMatchesKdf = !cached.kdf || cached.kdf.toLowerCase() === kdf;
4066
+ const cMatchesIter = !targetIter || cached.iter === targetIter;
4067
+ if (cMatchesKdf && cMatchesIter) {
4068
+ return cached;
4069
+ }
4070
+ }
4071
+ }
3293
4072
  if (kdf === "pbkdf2") {
3294
4073
  const s = saltOverride || options?.kdfSalt || this._kdfOptions?.salt;
3295
4074
  const saltBytes = CKeyManager.resolveSalt(s);
@@ -3298,20 +4077,24 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3298
4077
  const iterations = Math.min(Math.max(rawIter, 1e3), 6e5);
3299
4078
  const hash = (options?.kdfHash || this._kdfOptions?.hash || "SHA-256").toLowerCase().replace("-", "");
3300
4079
  const key2 = crypto__namespace.pbkdf2Sync(passphrase, saltBuf, iterations, 32, hash);
3301
- if (!keyProvider && !saltOverride && !options?.kdfSalt) {
3302
- this._cachedKey = key2;
3303
- this._cachedSalt = saltBuf.toString("base64");
3304
- this._cachedKdf = "PBKDF2";
3305
- this._cachedIter = iterations;
3306
- this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
3307
- this._cachedKeyId = currentKeyId;
3308
- }
3309
- return {
4080
+ const res2 = {
3310
4081
  key: key2,
3311
4082
  salt: saltBuf.toString("base64"),
3312
4083
  kdf: "PBKDF2",
3313
4084
  iter: iterations
3314
4085
  };
4086
+ if (!saltOverride && !options?.kdfSalt) {
4087
+ this._keyringCache.set(currentKeyId, res2);
4088
+ if (!keyProvider || keyProvider === this.defaultKey) {
4089
+ this._cachedKey = key2;
4090
+ this._cachedSalt = saltBuf.toString("base64");
4091
+ this._cachedKdf = "PBKDF2";
4092
+ this._cachedIter = iterations;
4093
+ this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
4094
+ this._cachedKeyId = currentKeyId;
4095
+ }
4096
+ }
4097
+ return res2;
3315
4098
  }
3316
4099
  if (kdf === "hkdf") {
3317
4100
  if (typeof raw === "string") {
@@ -3325,49 +4108,56 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3325
4108
  const hash = (options?.kdfHash || this._kdfOptions?.hash || "SHA-256").toLowerCase().replace("-", "");
3326
4109
  const info = this.resolveInfoBuffer(options?.info || this._kdfOptions?.info);
3327
4110
  const key2 = Buffer.from(crypto__namespace.hkdfSync(hash, passphrase, saltBuf, info, 32));
3328
- if (!keyProvider && !saltOverride && !options?.kdfSalt) {
3329
- this._cachedKey = key2;
3330
- this._cachedSalt = saltBuf.toString("base64");
3331
- this._cachedKdf = "HKDF";
3332
- this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
3333
- this._cachedKeyId = currentKeyId;
3334
- }
3335
- return {
4111
+ const res2 = {
3336
4112
  key: key2,
3337
4113
  salt: saltBuf.toString("base64"),
3338
4114
  kdf: "HKDF"
3339
4115
  };
4116
+ if (!saltOverride && !options?.kdfSalt) {
4117
+ this._keyringCache.set(currentKeyId, res2);
4118
+ if (!keyProvider || keyProvider === this.defaultKey) {
4119
+ this._cachedKey = key2;
4120
+ this._cachedSalt = saltBuf.toString("base64");
4121
+ this._cachedKdf = "HKDF";
4122
+ this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
4123
+ this._cachedKeyId = currentKeyId;
4124
+ }
4125
+ }
4126
+ return res2;
3340
4127
  }
3341
4128
  if (kdf === "none") {
3342
4129
  console.warn(
3343
4130
  "[CNodeCryptoEncryptionProvider] WARNING: Decrypting a legacy envelope with no KDF (raw SHA-256 key derivation). This is cryptographically weak. Migrate to pbkdf2 or hkdf."
3344
4131
  );
3345
4132
  }
3346
- if (!keyProvider && this._cachedKey && this._cachedKdf === "none" && this._cachedKeyId === currentKeyId) {
4133
+ if ((!keyProvider || keyProvider === this.defaultKey) && this._cachedKey && this._cachedKdf === "none" && this._cachedKeyId === currentKeyId) {
3347
4134
  return { key: this._cachedKey, kdf: "none" };
3348
4135
  }
3349
4136
  const key = crypto__namespace.createHash("sha256").update(passphrase, "utf8").digest();
3350
- if (!keyProvider) {
4137
+ const res = { key, kdf: "none" };
4138
+ this._keyringCache.set(currentKeyId, res);
4139
+ if (!keyProvider || keyProvider === this.defaultKey) {
3351
4140
  this._cachedKey = key;
3352
4141
  this._cachedKdf = "none";
3353
4142
  this._cachedKeyId = currentKeyId;
3354
4143
  }
3355
- return { key, kdf: "none" };
4144
+ return res;
3356
4145
  }
3357
4146
  async deriveKeyAsync(keyProvider, kdfOverride, saltOverride, options, iterationsOverride) {
3358
4147
  const raw = await this.resolveRawKeyAsync(keyProvider);
3359
4148
  const currentKeyId = getSafeKeyIdentifier(raw);
3360
4149
  if (Buffer.isBuffer(raw)) {
3361
4150
  if (raw.length === 32) {
3362
- if (!keyProvider) {
3363
- if (!this._cachedKey || this._cachedKeyId !== currentKeyId) {
3364
- this._cachedKey = Buffer.from(raw);
3365
- this._cachedKdf = "raw";
3366
- this._cachedKeyId = currentKeyId;
3367
- }
3368
- return { key: this._cachedKey, kdf: "raw" };
4151
+ const cached = this._keyringCache.get(currentKeyId);
4152
+ if (cached) return cached;
4153
+ const res2 = { key: Buffer.from(raw), kdf: "raw" };
4154
+ this._keyringCache.set(currentKeyId, res2);
4155
+ if (!keyProvider || keyProvider === this.defaultKey) {
4156
+ this._cachedKey = res2.key;
4157
+ this._cachedKdf = "raw";
4158
+ this._cachedKeyId = currentKeyId;
3369
4159
  }
3370
- return { key: Buffer.from(raw), kdf: "raw" };
4160
+ return res2;
3371
4161
  }
3372
4162
  throw new Error(
3373
4163
  `[CNodeCryptoEncryptionProvider] AES-GCM-256 requires a 32-byte key. Received a Buffer of ${raw.length} bytes. Provide exactly 32 bytes or a string passphrase.`
@@ -3386,6 +4176,16 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3386
4176
  return { key: this._cachedKey, salt: this._cachedSalt, kdf: this._cachedKdf, iter: this._cachedIter };
3387
4177
  }
3388
4178
  }
4179
+ if (!saltOverride && !options?.kdfSalt) {
4180
+ const cached = this._keyringCache.get(currentKeyId);
4181
+ if (cached) {
4182
+ const cMatchesKdf = !cached.kdf || cached.kdf.toLowerCase() === kdf;
4183
+ const cMatchesIter = !targetIter || cached.iter === targetIter;
4184
+ if (cMatchesKdf && cMatchesIter) {
4185
+ return cached;
4186
+ }
4187
+ }
4188
+ }
3389
4189
  if (kdf === "pbkdf2") {
3390
4190
  const s = saltOverride || options?.kdfSalt || this._kdfOptions?.salt;
3391
4191
  const saltBytes = CKeyManager.resolveSalt(s);
@@ -3399,20 +4199,24 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3399
4199
  else resolve(derived);
3400
4200
  });
3401
4201
  });
3402
- if (!keyProvider && !saltOverride && !options?.kdfSalt) {
3403
- this._cachedKey = key2;
3404
- this._cachedSalt = saltBuf.toString("base64");
3405
- this._cachedKdf = "PBKDF2";
3406
- this._cachedIter = iterations;
3407
- this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
3408
- this._cachedKeyId = currentKeyId;
3409
- }
3410
- return {
4202
+ const res2 = {
3411
4203
  key: key2,
3412
4204
  salt: saltBuf.toString("base64"),
3413
4205
  kdf: "PBKDF2",
3414
4206
  iter: iterations
3415
4207
  };
4208
+ if (!saltOverride && !options?.kdfSalt) {
4209
+ this._keyringCache.set(currentKeyId, res2);
4210
+ if (!keyProvider || keyProvider === this.defaultKey) {
4211
+ this._cachedKey = key2;
4212
+ this._cachedSalt = saltBuf.toString("base64");
4213
+ this._cachedKdf = "PBKDF2";
4214
+ this._cachedIter = iterations;
4215
+ this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
4216
+ this._cachedKeyId = currentKeyId;
4217
+ }
4218
+ }
4219
+ return res2;
3416
4220
  }
3417
4221
  if (kdf === "hkdf") {
3418
4222
  if (typeof raw === "string") {
@@ -3431,34 +4235,40 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3431
4235
  else resolve(Buffer.from(derived));
3432
4236
  });
3433
4237
  });
3434
- if (!keyProvider && !saltOverride && !options?.kdfSalt) {
3435
- this._cachedKey = key2;
3436
- this._cachedSalt = saltBuf.toString("base64");
3437
- this._cachedKdf = "HKDF";
3438
- this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
3439
- this._cachedKeyId = currentKeyId;
3440
- }
3441
- return {
4238
+ const res2 = {
3442
4239
  key: key2,
3443
4240
  salt: saltBuf.toString("base64"),
3444
4241
  kdf: "HKDF"
3445
4242
  };
4243
+ if (!saltOverride && !options?.kdfSalt) {
4244
+ this._keyringCache.set(currentKeyId, res2);
4245
+ if (!keyProvider || keyProvider === this.defaultKey) {
4246
+ this._cachedKey = key2;
4247
+ this._cachedSalt = saltBuf.toString("base64");
4248
+ this._cachedKdf = "HKDF";
4249
+ this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
4250
+ this._cachedKeyId = currentKeyId;
4251
+ }
4252
+ }
4253
+ return res2;
3446
4254
  }
3447
4255
  if (kdf === "none") {
3448
4256
  console.warn(
3449
4257
  "[CNodeCryptoEncryptionProvider] WARNING: Decrypting a legacy envelope with no KDF (raw SHA-256 key derivation). This is cryptographically weak. Migrate to pbkdf2 or hkdf."
3450
4258
  );
3451
4259
  }
3452
- if (!keyProvider && this._cachedKey && this._cachedKdf === "none" && this._cachedKeyId === currentKeyId) {
4260
+ if ((!keyProvider || keyProvider === this.defaultKey) && this._cachedKey && this._cachedKdf === "none" && this._cachedKeyId === currentKeyId) {
3453
4261
  return { key: this._cachedKey, kdf: "none" };
3454
4262
  }
3455
4263
  const key = crypto__namespace.createHash("sha256").update(passphrase, "utf8").digest();
3456
- if (!keyProvider) {
4264
+ const res = { key, kdf: "none" };
4265
+ this._keyringCache.set(currentKeyId, res);
4266
+ if (!keyProvider || keyProvider === this.defaultKey) {
3457
4267
  this._cachedKey = key;
3458
4268
  this._cachedKdf = "none";
3459
4269
  this._cachedKeyId = currentKeyId;
3460
4270
  }
3461
- return { key, kdf: "none" };
4271
+ return res;
3462
4272
  }
3463
4273
  static isEncrypted(payload) {
3464
4274
  if (!payload || typeof payload !== "object") return false;
@@ -3498,7 +4308,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3498
4308
  ]);
3499
4309
  const authTag = cipher.getAuthTag();
3500
4310
  const combinedCiphertext = Buffer.concat([encryptedBuffer, authTag]);
3501
- const kid = options?.kid || options?.keyId || this._configuredKid;
4311
+ const kid = options?.kid || options?.keyId || this.activeKid || this._configuredKid;
3502
4312
  return {
3503
4313
  __encrypted: true,
3504
4314
  ciphertext: combinedCiphertext.toString("base64"),
@@ -3511,7 +4321,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3511
4321
  ...hasAad ? { aad: true } : {}
3512
4322
  };
3513
4323
  } finally {
3514
- if (key !== this._cachedKey) {
4324
+ if (!this.isKeyRetained(key)) {
3515
4325
  key.fill(0);
3516
4326
  }
3517
4327
  }
@@ -3600,7 +4410,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3600
4410
  if (err instanceof SyntaxError) throw err;
3601
4411
  throw new Error(`[CNodeCryptoEncryptionProvider] Decryption failed: ${err?.message || "Authentication tag mismatch or corrupted ciphertext"}`);
3602
4412
  } finally {
3603
- if (key !== this._cachedKey) {
4413
+ if (!this.isKeyRetained(key)) {
3604
4414
  key.fill(0);
3605
4415
  }
3606
4416
  }
@@ -3609,6 +4419,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3609
4419
  encryptSync(data, options) {
3610
4420
  if (data === void 0 || data === null) return data;
3611
4421
  if (this.isEncrypted(data)) return data;
4422
+ options = this._resolveOutboundKeyringOptions(options);
3612
4423
  if (Array.isArray(data)) {
3613
4424
  if (options?.fields && options.fields.length > 0 || options?.valueEncryption === true) {
3614
4425
  return data.map((item) => this.encryptSync(item, options));
@@ -3644,7 +4455,12 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3644
4455
  decryptSync(payload, options) {
3645
4456
  if (payload === void 0 || payload === null) return payload;
3646
4457
  if (this.isEncrypted(payload)) {
3647
- return this.decryptValueSync(payload, options);
4458
+ const metaErr = this._validateEnvelopeMetadata(payload, options);
4459
+ if (metaErr) {
4460
+ return this._handleDecryptError(metaErr, payload, "", payload, void 0, options);
4461
+ }
4462
+ const envelopeKey = this._resolveDecryptionKeyForEnvelope(payload, options);
4463
+ return this.decryptValueSync(payload, { ...options, key: envelopeKey });
3648
4464
  }
3649
4465
  if (Array.isArray(payload)) {
3650
4466
  return payload.map((item) => this.decryptSync(item, options));
@@ -3658,28 +4474,35 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3658
4474
  for (const key of targetKeys) {
3659
4475
  if (this.isEncrypted(result[key])) {
3660
4476
  const fieldPayload = result[key];
4477
+ const metaErr = this._validateEnvelopeMetadata(fieldPayload, options);
4478
+ if (metaErr) {
4479
+ return this._handleDecryptError(metaErr, result, key, fieldPayload, void 0, options);
4480
+ }
4481
+ const envelopeKey = this._resolveDecryptionKeyForEnvelope(fieldPayload, options);
3661
4482
  const salt = fieldPayload.salt || "__no_salt__";
3662
- let derivedKey = keyCache.get(salt);
4483
+ const memoKey = `${salt}:${fieldPayload.kid || ""}`;
4484
+ let derivedKey = keyCache.get(memoKey);
3663
4485
  if (!derivedKey) {
3664
4486
  const kdfFromEnvelope = fieldPayload.kdf ? fieldPayload.kdf.toLowerCase() : "none";
3665
4487
  const derived = this.deriveKeySync(
3666
- options?.key,
4488
+ envelopeKey,
3667
4489
  kdfFromEnvelope,
3668
4490
  fieldPayload.salt,
3669
4491
  options,
3670
4492
  fieldPayload.iter
3671
4493
  );
3672
4494
  derivedKey = derived.key;
3673
- keyCache.set(salt, derivedKey);
4495
+ keyCache.set(memoKey, derivedKey);
3674
4496
  }
3675
- const boundOptions = this._bindFieldAad(payload, key, options);
4497
+ const boundOptions = this._bindFieldAad(payload, key, { ...options, key: envelopeKey });
3676
4498
  const hasBoundAad = boundOptions.aad !== options?.aad;
3677
4499
  try {
3678
4500
  safeSetProperty(result, key, this.decryptCiphertextWithKey(fieldPayload, derivedKey, boundOptions));
3679
4501
  } catch (err) {
3680
- if (hasBoundAad && isAuthOrDecryptionError(err)) {
4502
+ const canFallback = hasBoundAad && (!options?.aadFields || options.aadFields.length === 0) && typeof options?.aad !== "function";
4503
+ if (canFallback && isAuthOrDecryptionError(err)) {
3681
4504
  try {
3682
- safeSetProperty(result, key, this.decryptCiphertextWithKey(fieldPayload, derivedKey, options));
4505
+ safeSetProperty(result, key, this.decryptCiphertextWithKey(fieldPayload, derivedKey, { ...options, key: envelopeKey }));
3683
4506
  } catch (fallbackErr) {
3684
4507
  throw fallbackErr;
3685
4508
  }
@@ -3693,7 +4516,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3693
4516
  return mutated ? result : payload;
3694
4517
  } finally {
3695
4518
  for (const k of keyCache.values()) {
3696
- if (k !== this._cachedKey) {
4519
+ if (!this.isKeyRetained(k)) {
3697
4520
  k.fill(0);
3698
4521
  }
3699
4522
  }
@@ -3741,7 +4564,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3741
4564
  ]);
3742
4565
  const authTag = cipher.getAuthTag();
3743
4566
  const combined = Buffer.concat([ciphertext, authTag]);
3744
- const kid = options?.kid || options?.keyId || this._configuredKid;
4567
+ const kid = options?.kid || options?.keyId || this.activeKid || this._configuredKid;
3745
4568
  return {
3746
4569
  __encrypted: true,
3747
4570
  ciphertext: combined.toString("base64"),
@@ -3754,7 +4577,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3754
4577
  ...hasAad ? { aad: true } : {}
3755
4578
  };
3756
4579
  } finally {
3757
- if (key !== this._cachedKey) {
4580
+ if (!this.isKeyRetained(key)) {
3758
4581
  key.fill(0);
3759
4582
  }
3760
4583
  }
@@ -3778,7 +4601,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3778
4601
  if (err instanceof SyntaxError) throw err;
3779
4602
  throw new Error(`[CNodeCryptoEncryptionProvider] Decryption failed: ${err?.message || "Authentication tag mismatch or corrupted ciphertext"}`);
3780
4603
  } finally {
3781
- if (key !== this._cachedKey) {
4604
+ if (!this.isKeyRetained(key)) {
3782
4605
  key.fill(0);
3783
4606
  }
3784
4607
  }
@@ -3786,7 +4609,12 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3786
4609
  async decrypt(payload, options) {
3787
4610
  if (payload === void 0 || payload === null) return payload;
3788
4611
  if (this.isEncrypted(payload)) {
3789
- return this.decryptValue(payload, options);
4612
+ const metaErr = this._validateEnvelopeMetadata(payload, options);
4613
+ if (metaErr) {
4614
+ return this._handleDecryptError(metaErr, payload, "", payload, void 0, options);
4615
+ }
4616
+ const envelopeKey = this._resolveDecryptionKeyForEnvelope(payload, options);
4617
+ return this.decryptValue(payload, { ...options, key: envelopeKey });
3790
4618
  }
3791
4619
  if (Array.isArray(payload)) {
3792
4620
  return Promise.all(payload.map((item) => this.decrypt(item, options)));
@@ -3800,28 +4628,35 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3800
4628
  for (const key of targetKeys) {
3801
4629
  if (this.isEncrypted(result[key])) {
3802
4630
  const fieldPayload = result[key];
4631
+ const metaErr = this._validateEnvelopeMetadata(fieldPayload, options);
4632
+ if (metaErr) {
4633
+ return this._handleDecryptError(metaErr, result, key, fieldPayload, void 0, options);
4634
+ }
4635
+ const envelopeKey = this._resolveDecryptionKeyForEnvelope(fieldPayload, options);
3803
4636
  const salt = fieldPayload.salt || "__no_salt__";
3804
- let derivedKey = keyCache.get(salt);
4637
+ const memoKey = `${salt}:${fieldPayload.kid || ""}`;
4638
+ let derivedKey = keyCache.get(memoKey);
3805
4639
  if (!derivedKey) {
3806
4640
  const kdfFromEnvelope = fieldPayload.kdf ? fieldPayload.kdf.toLowerCase() : "none";
3807
4641
  const derived = await this.deriveKeyAsync(
3808
- options?.key,
4642
+ envelopeKey,
3809
4643
  kdfFromEnvelope,
3810
4644
  fieldPayload.salt ? CKeyManager.fromBase64(fieldPayload.salt) : void 0,
3811
4645
  options,
3812
4646
  fieldPayload.iter
3813
4647
  );
3814
4648
  derivedKey = derived.key;
3815
- keyCache.set(salt, derivedKey);
4649
+ keyCache.set(memoKey, derivedKey);
3816
4650
  }
3817
- const boundOptions = this._bindFieldAad(payload, key, options);
4651
+ const boundOptions = this._bindFieldAad(payload, key, { ...options, key: envelopeKey });
3818
4652
  const hasBoundAad = boundOptions.aad !== options?.aad;
3819
4653
  try {
3820
4654
  safeSetProperty(result, key, this.decryptCiphertextWithKey(fieldPayload, derivedKey, boundOptions));
3821
4655
  } catch (err) {
3822
- if (hasBoundAad && isAuthOrDecryptionError(err)) {
4656
+ const canFallback = hasBoundAad && (!options?.aadFields || options.aadFields.length === 0) && typeof options?.aad !== "function";
4657
+ if (canFallback && isAuthOrDecryptionError(err)) {
3823
4658
  try {
3824
- safeSetProperty(result, key, this.decryptCiphertextWithKey(fieldPayload, derivedKey, options));
4659
+ safeSetProperty(result, key, this.decryptCiphertextWithKey(fieldPayload, derivedKey, { ...options, key: envelopeKey }));
3825
4660
  } catch (fallbackErr) {
3826
4661
  throw fallbackErr;
3827
4662
  }
@@ -3835,7 +4670,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
3835
4670
  return mutated ? result : payload;
3836
4671
  } finally {
3837
4672
  for (const k of keyCache.values()) {
3838
- if (k !== this._cachedKey) {
4673
+ if (!this.isKeyRetained(k)) {
3839
4674
  k.fill(0);
3840
4675
  }
3841
4676
  }
@@ -3866,7 +4701,11 @@ function createEncryptionProvider(options = {}) {
3866
4701
  kdfIterations: options.kdfIterations,
3867
4702
  kdfHash: options.kdfHash,
3868
4703
  kdfSalt: options.kdfSalt,
3869
- info: options.info
4704
+ info: options.info,
4705
+ kid: options.kid || options.keyId,
4706
+ keys: options.keys,
4707
+ activeKid: options.activeKid,
4708
+ allowedKids: options.allowedKids
3870
4709
  });
3871
4710
  }
3872
4711
  if (alg === "AES-CBC") {
@@ -3903,7 +4742,11 @@ function createEncryptionProvider(options = {}) {
3903
4742
  kdfIterations: options.kdfIterations,
3904
4743
  kdfHash: options.kdfHash,
3905
4744
  kdfSalt: options.kdfSalt,
3906
- info: options.info
4745
+ info: options.info,
4746
+ kid: options.kid || options.keyId,
4747
+ keys: options.keys,
4748
+ activeKid: options.activeKid,
4749
+ allowedKids: options.allowedKids
3907
4750
  });
3908
4751
  }
3909
4752
  throw new Error(
@@ -3966,7 +4809,19 @@ function isDataEncrypted(provider, data) {
3966
4809
  function buildEncryptionContext(encryption, url, _defaultIdKey) {
3967
4810
  return {
3968
4811
  url,
4812
+ aad: encryption?.aad,
4813
+ aadFields: encryption?.aadFields,
4814
+ baseAad: encryption?.baseAad,
4815
+ bindRequestContextAad: encryption?.bindRequestContextAad,
4816
+ strict: encryption?.strict,
4817
+ allowedAlgorithms: encryption?.allowedAlgorithms,
4818
+ allowedKids: encryption?.allowedKids,
4819
+ onDecryptError: encryption?.onDecryptError,
3969
4820
  key: encryption?.key,
4821
+ kid: encryption?.kid,
4822
+ keyId: encryption?.keyId,
4823
+ keys: encryption?.keys,
4824
+ activeKid: encryption?.activeKid,
3970
4825
  valueEncryption: encryption?.valueEncryption,
3971
4826
  fields: encryption?.fields,
3972
4827
  publicKey: encryption?.publicKey,
@@ -3997,7 +4852,7 @@ async function decryptPageRows(rows, encryption, apiUrl, idKey) {
3997
4852
  }
3998
4853
  return isDataEncrypted(cipherProvider, row);
3999
4854
  });
4000
- if (!hasEncryptedRows) {
4855
+ if (!hasEncryptedRows && !encryption?.strict) {
4001
4856
  return { rows, decrypted: false };
4002
4857
  }
4003
4858
  const ctx = buildEncryptionContext(encryption, apiUrl);
@@ -6704,8 +7559,18 @@ class RequestProvider {
6704
7559
  method,
6705
7560
  isServer,
6706
7561
  aad: encOptions.aad,
7562
+ aadFields: encOptions.aadFields,
7563
+ baseAad: encOptions.baseAad,
6707
7564
  bindRequestContextAad: encOptions.bindRequestContextAad,
7565
+ strict: encOptions.strict,
7566
+ allowedAlgorithms: encOptions.allowedAlgorithms,
7567
+ allowedKids: encOptions.allowedKids,
7568
+ onDecryptError: encOptions.onDecryptError,
6708
7569
  key: encOptions.key,
7570
+ kid: encOptions.kid,
7571
+ keyId: encOptions.keyId,
7572
+ keys: encOptions.keys,
7573
+ activeKid: encOptions.activeKid,
6709
7574
  valueEncryption: encOptions.valueEncryption,
6710
7575
  fields: encOptions.fields,
6711
7576
  publicKey: encOptions.publicKey,
@@ -10244,6 +11109,7 @@ exports.CTreeServerDataProvider = CTreeServerDataProvider;
10244
11109
  exports.CWebCryptoAESCBCProvider = CWebCryptoAESCBCProvider;
10245
11110
  exports.CWebCryptoEncryptionProvider = CWebCryptoEncryptionProvider;
10246
11111
  exports.CacheProviderFactory = CacheProviderFactory;
11112
+ exports.DECRYPT_ERROR_SENTINEL = DECRYPT_ERROR_SENTINEL;
10247
11113
  exports.DefaultHydrationAdapter = DefaultHydrationAdapter;
10248
11114
  exports.ECacheStrategy = ECacheStrategy;
10249
11115
  exports.EDataFilterOperator = EDataFilterOperator;
@@ -10256,6 +11122,7 @@ exports.RequestQueue = RequestQueue;
10256
11122
  exports.SENSITIVE_HEADER_KEYS = SENSITIVE_HEADER_KEYS;
10257
11123
  exports.applyOperator = applyOperator;
10258
11124
  exports.buildEncryptionContext = buildEncryptionContext;
11125
+ exports.canonicalizeJson = canonicalizeJson;
10259
11126
  exports.clearCachedCipherProvider = clearCachedCipherProvider;
10260
11127
  exports.clearSSRData = clearSSRData;
10261
11128
  exports.compareAnd = compareAnd;