@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 +1031 -164
- package/dist/index.d.cts +242 -17
- package/dist/index.d.mts +242 -17
- package/dist/index.d.ts +242 -17
- package/dist/index.mjs +1030 -165
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
784
|
-
* to prevent ciphertext swapping
|
|
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 === "
|
|
790
|
-
|
|
791
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
928
|
-
localMemo.
|
|
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
|
-
|
|
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
|
|
944
|
-
const
|
|
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
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
995
|
-
const
|
|
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
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
1689
|
-
this.
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
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
|
|
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.
|
|
1714
|
-
this.
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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 (
|
|
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;
|