@katlux/providers 0.1.1-alpha.126 → 0.1.1-alpha.128
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1977 -825
- package/dist/index.d.cts +347 -19
- package/dist/index.d.mts +347 -19
- package/dist/index.d.ts +347 -19
- package/dist/index.mjs +1976 -826
- 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,878 +674,1725 @@ 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
|
}
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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);
|
|
681
732
|
}
|
|
682
733
|
}
|
|
734
|
+
return JSON.stringify(value);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
class CKeyManager {
|
|
738
|
+
// ── Binary & Encoding Utilities ──────────────────────────────────────────
|
|
683
739
|
/**
|
|
684
|
-
*
|
|
740
|
+
* Generates a cryptographically secure random byte buffer of the specified length.
|
|
685
741
|
*/
|
|
686
|
-
|
|
687
|
-
|
|
742
|
+
static randomBytes(length) {
|
|
743
|
+
const bytes = new Uint8Array(length);
|
|
744
|
+
return getRandomValues(bytes);
|
|
688
745
|
}
|
|
689
746
|
/**
|
|
690
|
-
*
|
|
747
|
+
* Converts an ArrayBuffer or Uint8Array into a Base64-encoded string.
|
|
691
748
|
*/
|
|
692
|
-
|
|
693
|
-
return
|
|
749
|
+
static toBase64(buffer) {
|
|
750
|
+
return toBase64(buffer);
|
|
694
751
|
}
|
|
695
752
|
/**
|
|
696
|
-
*
|
|
697
|
-
*
|
|
753
|
+
* Converts a Base64 or Base64URL-encoded string back into a Uint8Array.
|
|
754
|
+
* Normalizes URL-safe characters and padding, strictly validating against the Base64 alphabet.
|
|
698
755
|
*/
|
|
699
|
-
|
|
756
|
+
static fromBase64(base64) {
|
|
757
|
+
if (typeof base64 !== "string") {
|
|
758
|
+
throw new Error("[CKeyManager] Invalid base64 input: expected string");
|
|
759
|
+
}
|
|
760
|
+
const cleaned = base64.replace(/\s+/g, "");
|
|
761
|
+
if (!cleaned) return new Uint8Array(0);
|
|
762
|
+
const normalized = cleaned.replace(/-/g, "+").replace(/_/g, "/");
|
|
763
|
+
const padded = normalized.padEnd(normalized.length + (4 - normalized.length % 4) % 4, "=");
|
|
764
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(padded)) {
|
|
765
|
+
throw new Error("[CKeyManager] Invalid base64 string");
|
|
766
|
+
}
|
|
767
|
+
if (typeof Buffer !== "undefined") {
|
|
768
|
+
const buf = Buffer.from(padded, "base64");
|
|
769
|
+
const arrayBuf = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
770
|
+
return new Uint8Array(arrayBuf);
|
|
771
|
+
}
|
|
772
|
+
const binary = atob(padded);
|
|
773
|
+
const bytes = new Uint8Array(binary.length);
|
|
774
|
+
for (let i = 0; i < binary.length; i++) {
|
|
775
|
+
bytes[i] = binary.charCodeAt(i);
|
|
776
|
+
}
|
|
777
|
+
return bytes;
|
|
700
778
|
}
|
|
701
779
|
/**
|
|
702
|
-
*
|
|
780
|
+
* Deterministically resolves a salt from Uint8Array, ArrayBuffer, Base64/Base64URL string, or raw text string.
|
|
781
|
+
* Guarantees 100% identical salt byte resolution between Node.js and Browser environments.
|
|
703
782
|
*/
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
if (
|
|
707
|
-
|
|
708
|
-
`[${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.`
|
|
709
|
-
);
|
|
783
|
+
static resolveSalt(salt) {
|
|
784
|
+
if (!salt) return CKeyManager.randomBytes(16);
|
|
785
|
+
if (ArrayBuffer.isView(salt)) {
|
|
786
|
+
return salt.byteLength > 0 ? new Uint8Array(salt.buffer.slice(salt.byteOffset, salt.byteOffset + salt.byteLength)) : CKeyManager.randomBytes(16);
|
|
710
787
|
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
throw new Error(
|
|
714
|
-
`[${this.constructor.name}] No encryption key or keyProvider was supplied. Key provider resolved to empty or undefined key.`
|
|
715
|
-
);
|
|
788
|
+
if (salt instanceof ArrayBuffer) {
|
|
789
|
+
return salt.byteLength > 0 ? new Uint8Array(salt.slice(0)) : CKeyManager.randomBytes(16);
|
|
716
790
|
}
|
|
717
|
-
|
|
791
|
+
if (typeof salt === "string") {
|
|
792
|
+
const trimmed = salt.trim();
|
|
793
|
+
if (!trimmed) return CKeyManager.randomBytes(16);
|
|
794
|
+
if (trimmed.startsWith("base64:")) {
|
|
795
|
+
return CKeyManager.fromBase64(trimmed.slice(7));
|
|
796
|
+
}
|
|
797
|
+
if (trimmed.endsWith("=") && /^[A-Za-z0-9+/]+={1,2}$/.test(trimmed)) {
|
|
798
|
+
try {
|
|
799
|
+
const decoded = CKeyManager.fromBase64(trimmed);
|
|
800
|
+
if (decoded.byteLength > 0) return decoded;
|
|
801
|
+
} catch {
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
const encoded = textEncoder$1.encode(trimmed);
|
|
805
|
+
return encoded.byteLength > 0 ? encoded : CKeyManager.randomBytes(16);
|
|
806
|
+
}
|
|
807
|
+
return CKeyManager.randomBytes(16);
|
|
718
808
|
}
|
|
809
|
+
// ── Symmetric Key Management ─────────────────────────────────────────────
|
|
719
810
|
/**
|
|
720
|
-
*
|
|
811
|
+
* Generates a random AES CryptoKey suitable for AES-GCM or AES-CBC.
|
|
812
|
+
*
|
|
813
|
+
* E2EE Threat Model Note: Focuses on protecting GDPR/tenant data against database and
|
|
814
|
+
* server-side leaks. Keys remain extractable by default to enable role-based key sharing,
|
|
815
|
+
* wrapping, and transfer between authorized client sessions in multi-user SaaS apps.
|
|
816
|
+
* Client-side malware/trojan protection is out of scope.
|
|
721
817
|
*/
|
|
722
|
-
static
|
|
723
|
-
|
|
724
|
-
|
|
818
|
+
static async generateAESKey(length = 256, algorithm = "AES-GCM", extractable = true) {
|
|
819
|
+
const subtle = getSubtleCrypto();
|
|
820
|
+
const usages = algorithm === "AES-KW" ? ["wrapKey", "unwrapKey"] : ["encrypt", "decrypt"];
|
|
821
|
+
return await subtle.generateKey(
|
|
822
|
+
{ name: algorithm, length },
|
|
823
|
+
extractable,
|
|
824
|
+
usages
|
|
825
|
+
);
|
|
725
826
|
}
|
|
726
827
|
/**
|
|
727
|
-
*
|
|
828
|
+
* Exports an AES CryptoKey as a raw base64 string.
|
|
728
829
|
*/
|
|
729
|
-
|
|
730
|
-
|
|
830
|
+
static async exportAESKey(key) {
|
|
831
|
+
const subtle = getSubtleCrypto();
|
|
832
|
+
const raw = await subtle.exportKey("raw", key);
|
|
833
|
+
try {
|
|
834
|
+
return CKeyManager.toBase64(raw);
|
|
835
|
+
} finally {
|
|
836
|
+
new Uint8Array(raw).fill(0);
|
|
837
|
+
}
|
|
731
838
|
}
|
|
732
839
|
/**
|
|
733
|
-
*
|
|
840
|
+
* Imports an AES CryptoKey from a raw base64 string.
|
|
734
841
|
*/
|
|
735
|
-
|
|
736
|
-
|
|
842
|
+
static async importAESKey(base64, algorithm = "AES-GCM", extractable = false) {
|
|
843
|
+
const subtle = getSubtleCrypto();
|
|
844
|
+
const rawBytes = CKeyManager.fromBase64(base64);
|
|
845
|
+
if (rawBytes.length !== 16 && rawBytes.length !== 24 && rawBytes.length !== 32) {
|
|
846
|
+
rawBytes.fill(0);
|
|
847
|
+
throw new Error(`[CKeyManager] Invalid AES key length: expected 16, 24, or 32 bytes (128, 192, or 256 bits), got ${rawBytes.length} bytes.`);
|
|
848
|
+
}
|
|
849
|
+
const usages = algorithm === "AES-KW" ? ["wrapKey", "unwrapKey"] : ["encrypt", "decrypt"];
|
|
850
|
+
try {
|
|
851
|
+
return await subtle.importKey(
|
|
852
|
+
"raw",
|
|
853
|
+
rawBytes,
|
|
854
|
+
{ name: algorithm },
|
|
855
|
+
extractable,
|
|
856
|
+
usages
|
|
857
|
+
);
|
|
858
|
+
} finally {
|
|
859
|
+
rawBytes.fill(0);
|
|
860
|
+
}
|
|
737
861
|
}
|
|
862
|
+
// ── Asymmetric Key Pair Management ───────────────────────────────────────
|
|
738
863
|
/**
|
|
739
|
-
*
|
|
864
|
+
* Generates an RSA-OAEP public/private key pair (2048 or 4096 bits).
|
|
740
865
|
*/
|
|
741
|
-
|
|
742
|
-
|
|
866
|
+
static async generateRSAKeyPair(bits = 4096, hash = "SHA-256", extractable = true) {
|
|
867
|
+
const subtle = getSubtleCrypto();
|
|
868
|
+
const pair = await subtle.generateKey(
|
|
869
|
+
{
|
|
870
|
+
name: "RSA-OAEP",
|
|
871
|
+
modulusLength: bits,
|
|
872
|
+
publicExponent: new Uint8Array([1, 0, 1]),
|
|
873
|
+
hash
|
|
874
|
+
},
|
|
875
|
+
extractable,
|
|
876
|
+
["encrypt", "decrypt", "wrapKey", "unwrapKey"]
|
|
877
|
+
);
|
|
878
|
+
return {
|
|
879
|
+
publicKey: pair.publicKey,
|
|
880
|
+
privateKey: pair.privateKey
|
|
881
|
+
};
|
|
743
882
|
}
|
|
744
883
|
/**
|
|
745
|
-
*
|
|
884
|
+
* Generates an ECDH public/private key pair for elliptic curve Diffie-Hellman.
|
|
746
885
|
*/
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
886
|
+
static async generateECDHKeyPair(curve = "P-256", extractable = true) {
|
|
887
|
+
const subtle = getSubtleCrypto();
|
|
888
|
+
const pair = await subtle.generateKey(
|
|
889
|
+
{
|
|
890
|
+
name: "ECDH",
|
|
891
|
+
namedCurve: curve
|
|
892
|
+
},
|
|
893
|
+
extractable,
|
|
894
|
+
["deriveKey", "deriveBits"]
|
|
895
|
+
);
|
|
896
|
+
return {
|
|
897
|
+
publicKey: pair.publicKey,
|
|
898
|
+
privateKey: pair.privateKey
|
|
899
|
+
};
|
|
752
900
|
}
|
|
753
901
|
/**
|
|
754
|
-
*
|
|
755
|
-
* 1. If options.bindRequestContextAad is explicitly false, returns options.aad directly without context binding.
|
|
756
|
-
* 2. When encrypting (isDecrypt=false): binds request context `req:${method}:${url}` only if options.bindRequestContextAad === true.
|
|
757
|
-
* 3. When decrypting (isDecrypt=true): derives request context `req:${method}:${url}` if url or method is provided.
|
|
758
|
-
* 4. If explicit options.aad is provided: combines with request context if bound, or returns options.aad directly.
|
|
759
|
-
* 5. Otherwise returns undefined.
|
|
902
|
+
* Exports a public CryptoKey to a portable JsonWebKey (JWK).
|
|
760
903
|
*/
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
const hasReqContext = Boolean(options.url || options.method);
|
|
765
|
-
if (options.aad) {
|
|
766
|
-
if (options.bindRequestContextAad === true && hasReqContext) {
|
|
767
|
-
const m = (options.method || "POST").toUpperCase();
|
|
768
|
-
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);
|
|
770
|
-
return `${baseAad}|req:${m}:${u}`;
|
|
771
|
-
}
|
|
772
|
-
return options.aad;
|
|
773
|
-
}
|
|
774
|
-
const shouldBindReq = isDecrypt ? hasReqContext : options.bindRequestContextAad === true && hasReqContext;
|
|
775
|
-
if (shouldBindReq) {
|
|
776
|
-
const m = (options.method || "POST").toUpperCase();
|
|
777
|
-
const u = options.url || "";
|
|
778
|
-
return `req:${m}:${u}`;
|
|
779
|
-
}
|
|
780
|
-
return void 0;
|
|
904
|
+
static async exportPublicKey(key) {
|
|
905
|
+
const subtle = getSubtleCrypto();
|
|
906
|
+
return await subtle.exportKey("jwk", key);
|
|
781
907
|
}
|
|
782
908
|
/**
|
|
783
|
-
*
|
|
784
|
-
* to prevent ciphertext swapping between fields or records.
|
|
909
|
+
* Exports a private CryptoKey to a portable JsonWebKey (JWK).
|
|
785
910
|
*/
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
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);
|
|
792
|
-
} else {
|
|
793
|
-
baseAad = String(options.aad);
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
const recordId = data && typeof data === "object" && (data.id !== void 0 || data._id !== void 0) ? String(data.id ?? data._id) : "";
|
|
797
|
-
const recordPart = recordId ? `|record:${recordId}` : "";
|
|
798
|
-
const reqPart = options?.bindRequestContextAad === true && (options?.url || options?.method) ? `|req:${(options.method || "POST").toUpperCase()}:${options.url || ""}` : "";
|
|
799
|
-
return {
|
|
800
|
-
...options,
|
|
801
|
-
aad: `aad:${baseAad}${recordPart}${reqPart}|field:${field}`
|
|
802
|
-
};
|
|
911
|
+
static async exportPrivateKey(key) {
|
|
912
|
+
const subtle = getSubtleCrypto();
|
|
913
|
+
return await subtle.exportKey("jwk", key);
|
|
803
914
|
}
|
|
804
915
|
/**
|
|
805
|
-
*
|
|
806
|
-
* Automatically performs value-level encryption:
|
|
807
|
-
* - If data is an array of objects: recursively encrypts each row.
|
|
808
|
-
* - If data is an object: encrypts specified fields (or all values if valueEncryption is true),
|
|
809
|
-
* preserving JSON object keys so backend APIs and ORMs can parse keys and perform upserts.
|
|
810
|
-
* - If data is a primitive or whole-payload envelope: encrypts as a single value.
|
|
916
|
+
* Imports a public key from a JsonWebKey (JWK).
|
|
811
917
|
*/
|
|
812
|
-
async
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
if (
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
break;
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
if (hasUnencrypted) break;
|
|
834
|
-
}
|
|
918
|
+
static async importPublicKey(jwk, algorithm) {
|
|
919
|
+
const subtle = getSubtleCrypto();
|
|
920
|
+
let importAlgorithm = algorithm;
|
|
921
|
+
let usages;
|
|
922
|
+
const isEC = jwk.kty === "EC" || typeof algorithm === "object" && algorithm?.name === "ECDH" || algorithm === "ECDH";
|
|
923
|
+
if (isEC) {
|
|
924
|
+
usages = [];
|
|
925
|
+
if (!importAlgorithm || importAlgorithm === "ECDH") {
|
|
926
|
+
importAlgorithm = { name: "ECDH", namedCurve: jwk.crv || "P-256" };
|
|
927
|
+
}
|
|
928
|
+
} else {
|
|
929
|
+
usages = ["encrypt", "wrapKey"];
|
|
930
|
+
if (!importAlgorithm || importAlgorithm === "RSA-OAEP") {
|
|
931
|
+
if (jwk.kty === "RSA") {
|
|
932
|
+
const hash = jwk.alg === "RSA-OAEP-512" ? "SHA-512" : "SHA-256";
|
|
933
|
+
importAlgorithm = { name: "RSA-OAEP", hash };
|
|
934
|
+
} else {
|
|
935
|
+
throw new Error(`[CKeyManager] Unsupported JWK kty: ${jwk.kty}`);
|
|
835
936
|
}
|
|
836
|
-
|
|
837
|
-
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
return await subtle.importKey("jwk", jwk, importAlgorithm, true, usages);
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* Imports a private key from a JsonWebKey (JWK).
|
|
943
|
+
*/
|
|
944
|
+
static async importPrivateKey(jwk, algorithm) {
|
|
945
|
+
const subtle = getSubtleCrypto();
|
|
946
|
+
let importAlgorithm = algorithm;
|
|
947
|
+
let usages;
|
|
948
|
+
const isEC = jwk.kty === "EC" || typeof algorithm === "object" && algorithm?.name === "ECDH" || algorithm === "ECDH";
|
|
949
|
+
if (isEC) {
|
|
950
|
+
usages = ["deriveKey", "deriveBits"];
|
|
951
|
+
if (!importAlgorithm || importAlgorithm === "ECDH") {
|
|
952
|
+
importAlgorithm = { name: "ECDH", namedCurve: jwk.crv || "P-256" };
|
|
953
|
+
}
|
|
954
|
+
} else {
|
|
955
|
+
usages = ["decrypt", "unwrapKey"];
|
|
956
|
+
if (!importAlgorithm || importAlgorithm === "RSA-OAEP") {
|
|
957
|
+
if (jwk.kty === "RSA") {
|
|
958
|
+
const hash = jwk.alg === "RSA-OAEP-512" ? "SHA-512" : "SHA-256";
|
|
959
|
+
importAlgorithm = { name: "RSA-OAEP", hash };
|
|
960
|
+
} else {
|
|
961
|
+
throw new Error(`[CKeyManager] Unsupported JWK kty: ${jwk.kty}`);
|
|
838
962
|
}
|
|
839
|
-
await this.warmupKey(options);
|
|
840
|
-
const itemOptions = { ...options, _warmed: true };
|
|
841
|
-
return Promise.all(data.map((item) => this.encrypt(item, itemOptions)));
|
|
842
963
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
964
|
+
}
|
|
965
|
+
return await subtle.importKey("jwk", jwk, importAlgorithm, true, usages);
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Imports a public key from a PEM (SPKI) or base64 SPKI string.
|
|
969
|
+
*/
|
|
970
|
+
static async importPublicKeyFromPEM(pem, algorithm = "RSA-OAEP", curve = "P-256", hash = "SHA-256") {
|
|
971
|
+
const subtle = getSubtleCrypto();
|
|
972
|
+
const cleaned = pem.replace(/-----BEGIN[ A-Z0-9_-]+-----/g, "").replace(/-----END[ A-Z0-9_-]+-----/g, "").replace(/\s+/g, "");
|
|
973
|
+
const spkiBytes = CKeyManager.fromBase64(cleaned);
|
|
974
|
+
let importAlgorithm;
|
|
975
|
+
let usages;
|
|
976
|
+
if (algorithm === "ECDH") {
|
|
977
|
+
importAlgorithm = { name: "ECDH", namedCurve: curve };
|
|
978
|
+
usages = [];
|
|
979
|
+
} else if (algorithm === "ECDSA") {
|
|
980
|
+
importAlgorithm = { name: "ECDSA", namedCurve: curve };
|
|
981
|
+
usages = ["verify"];
|
|
982
|
+
} else {
|
|
983
|
+
importAlgorithm = { name: "RSA-OAEP", hash };
|
|
984
|
+
usages = ["encrypt", "wrapKey"];
|
|
985
|
+
}
|
|
986
|
+
try {
|
|
987
|
+
return await subtle.importKey("spki", spkiBytes, importAlgorithm, true, usages);
|
|
988
|
+
} finally {
|
|
989
|
+
spkiBytes.fill(0);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Imports an ECDSA public key from PEM (SPKI) or base64 SPKI string for signature verification.
|
|
994
|
+
*/
|
|
995
|
+
static async importECDSAPublicKeyFromPEM(pem, curve = "P-256") {
|
|
996
|
+
return CKeyManager.importPublicKeyFromPEM(pem, "ECDSA", curve);
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Imports a private key from a PEM (PKCS#8) or base64 PKCS#8 string.
|
|
1000
|
+
*/
|
|
1001
|
+
static async importPrivateKeyFromPEM(pem, algorithm = "RSA-OAEP", curve = "P-256", hash = "SHA-256") {
|
|
1002
|
+
if (/BEGIN\s+(RSA|EC)\s+PRIVATE\s+KEY/i.test(pem)) {
|
|
1003
|
+
throw new Error(
|
|
1004
|
+
`[CKeyManager] Failed to import private key. The provided PEM appears to be in PKCS#1 or SEC1 format. Web Crypto requires PKCS#8 format. Convert your key using: openssl pkcs8 -topk8 -nocrypt -in private.pem -out private_pkcs8.pem.`
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
const subtle = getSubtleCrypto();
|
|
1008
|
+
const cleaned = pem.replace(/-----BEGIN[ A-Z0-9_-]+-----/g, "").replace(/-----END[ A-Z0-9_-]+-----/g, "").replace(/\s+/g, "");
|
|
1009
|
+
const pkcs8Bytes = CKeyManager.fromBase64(cleaned);
|
|
1010
|
+
let importAlgorithm;
|
|
1011
|
+
let usages;
|
|
1012
|
+
if (algorithm === "ECDH") {
|
|
1013
|
+
importAlgorithm = { name: "ECDH", namedCurve: curve };
|
|
1014
|
+
usages = ["deriveKey", "deriveBits"];
|
|
1015
|
+
} else if (algorithm === "ECDSA") {
|
|
1016
|
+
importAlgorithm = { name: "ECDSA", namedCurve: curve };
|
|
1017
|
+
usages = ["sign"];
|
|
1018
|
+
} else {
|
|
1019
|
+
importAlgorithm = { name: "RSA-OAEP", hash };
|
|
1020
|
+
usages = ["decrypt", "unwrapKey"];
|
|
1021
|
+
}
|
|
1022
|
+
try {
|
|
1023
|
+
return await subtle.importKey("pkcs8", pkcs8Bytes, importAlgorithm, true, usages);
|
|
1024
|
+
} finally {
|
|
1025
|
+
pkcs8Bytes.fill(0);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Imports an ECDSA private key from PEM (PKCS#8) or base64 PKCS#8 string for signing.
|
|
1030
|
+
*/
|
|
1031
|
+
static async importECDSAPrivateKeyFromPEM(pem, curve = "P-256") {
|
|
1032
|
+
return CKeyManager.importPrivateKeyFromPEM(pem, "ECDSA", curve);
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Exports a public CryptoKey to a standard PEM (SPKI) string.
|
|
1036
|
+
*/
|
|
1037
|
+
static async exportPublicKeyToPEM(key) {
|
|
1038
|
+
const subtle = getSubtleCrypto();
|
|
1039
|
+
const spki = await subtle.exportKey("spki", key);
|
|
1040
|
+
let b64;
|
|
1041
|
+
try {
|
|
1042
|
+
b64 = CKeyManager.toBase64(spki);
|
|
1043
|
+
} finally {
|
|
1044
|
+
new Uint8Array(spki).fill(0);
|
|
1045
|
+
}
|
|
1046
|
+
const lines = b64.match(/.{1,64}/g) || [b64];
|
|
1047
|
+
return `-----BEGIN PUBLIC KEY-----
|
|
1048
|
+
${lines.join("\n")}
|
|
1049
|
+
-----END PUBLIC KEY-----`;
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Exports a private CryptoKey to a standard PEM (PKCS#8) string.
|
|
1053
|
+
*/
|
|
1054
|
+
static async exportPrivateKeyToPEM(key) {
|
|
1055
|
+
const subtle = getSubtleCrypto();
|
|
1056
|
+
const pkcs8 = await subtle.exportKey("pkcs8", key);
|
|
1057
|
+
let b64;
|
|
1058
|
+
try {
|
|
1059
|
+
b64 = CKeyManager.toBase64(pkcs8);
|
|
1060
|
+
} finally {
|
|
1061
|
+
new Uint8Array(pkcs8).fill(0);
|
|
1062
|
+
}
|
|
1063
|
+
const lines = b64.match(/.{1,64}/g) || [b64];
|
|
1064
|
+
return `-----BEGIN PRIVATE KEY-----
|
|
1065
|
+
${lines.join("\n")}
|
|
1066
|
+
-----END PRIVATE KEY-----`;
|
|
1067
|
+
}
|
|
1068
|
+
// ── Key Wrapping & Unwrapping ────────────────────────────────────────────
|
|
1069
|
+
/**
|
|
1070
|
+
* Wraps a target CryptoKey (e.g. an Account Symmetric Key) using a wrapping key
|
|
1071
|
+
* (AES-KW or RSA-OAEP).
|
|
1072
|
+
*/
|
|
1073
|
+
static async wrapKey(keyToWrap, wrappingKey, format = "raw") {
|
|
1074
|
+
const subtle = getSubtleCrypto();
|
|
1075
|
+
let wrapAlgorithm;
|
|
1076
|
+
if (wrappingKey.algorithm.name === "RSA-OAEP") {
|
|
1077
|
+
wrapAlgorithm = { name: "RSA-OAEP" };
|
|
1078
|
+
} else if (wrappingKey.algorithm.name === "AES-KW") {
|
|
1079
|
+
wrapAlgorithm = "AES-KW";
|
|
1080
|
+
} else {
|
|
1081
|
+
wrapAlgorithm = "AES-KW";
|
|
1082
|
+
}
|
|
1083
|
+
const wrappedBuffer = await subtle.wrapKey(format, keyToWrap, wrappingKey, wrapAlgorithm);
|
|
1084
|
+
return CKeyManager.toBase64(wrappedBuffer);
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* Unwraps a base64-encoded wrapped key using an unwrapping key (AES-KW or RSA-OAEP).
|
|
1088
|
+
*/
|
|
1089
|
+
static async unwrapKey(wrappedBase64, unwrappingKey, unwrappedKeyAlgorithm = { name: "AES-GCM", length: 256 }, extractable = true, keyUsages = ["encrypt", "decrypt"], format = "raw") {
|
|
1090
|
+
const subtle = getSubtleCrypto();
|
|
1091
|
+
const wrappedBytes = CKeyManager.fromBase64(wrappedBase64);
|
|
1092
|
+
let unwrapAlgorithm;
|
|
1093
|
+
if (unwrappingKey.algorithm.name === "RSA-OAEP") {
|
|
1094
|
+
unwrapAlgorithm = { name: "RSA-OAEP" };
|
|
1095
|
+
} else {
|
|
1096
|
+
unwrapAlgorithm = "AES-KW";
|
|
1097
|
+
}
|
|
1098
|
+
try {
|
|
1099
|
+
return await subtle.unwrapKey(
|
|
1100
|
+
format,
|
|
1101
|
+
wrappedBytes,
|
|
1102
|
+
unwrappingKey,
|
|
1103
|
+
unwrapAlgorithm,
|
|
1104
|
+
unwrappedKeyAlgorithm,
|
|
1105
|
+
extractable,
|
|
1106
|
+
keyUsages
|
|
1107
|
+
);
|
|
1108
|
+
} finally {
|
|
1109
|
+
wrappedBytes.fill(0);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Generates a high-entropy symmetric AES-GCM Account Data Encryption Key (ADEK).
|
|
1114
|
+
* Designed for multi-user table and field-level encryption.
|
|
1115
|
+
*/
|
|
1116
|
+
static async generateAccountDataKey(length = 256, extractable = false) {
|
|
1117
|
+
const subtle = getSubtleCrypto();
|
|
1118
|
+
return await subtle.generateKey(
|
|
1119
|
+
{ name: "AES-GCM", length },
|
|
1120
|
+
extractable,
|
|
1121
|
+
["encrypt", "decrypt"]
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
/**
|
|
1125
|
+
* Wraps an Account Data Encryption Key (ADEK) for a specific user using the user's public key (RSA-OAEP).
|
|
1126
|
+
* Returns a Base64-encoded wrapped key string suitable for storage in role-map / access tables.
|
|
1127
|
+
*/
|
|
1128
|
+
static async wrapAccountKeyForUser(adek, userPublicKey) {
|
|
1129
|
+
const subtle = getSubtleCrypto();
|
|
1130
|
+
if (userPublicKey.algorithm.name === "RSA-OAEP") {
|
|
1131
|
+
const rawKey = await subtle.exportKey("raw", adek);
|
|
1132
|
+
try {
|
|
1133
|
+
const wrappedBuffer = await subtle.encrypt(
|
|
1134
|
+
{ name: "RSA-OAEP" },
|
|
1135
|
+
userPublicKey,
|
|
1136
|
+
rawKey
|
|
1137
|
+
);
|
|
1138
|
+
return CKeyManager.toBase64(wrappedBuffer);
|
|
1139
|
+
} finally {
|
|
1140
|
+
new Uint8Array(rawKey).fill(0);
|
|
847
1141
|
}
|
|
848
|
-
return this.encryptValue(data, options);
|
|
849
1142
|
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
1143
|
+
throw new Error(
|
|
1144
|
+
`[CKeyManager] Unsupported user key algorithm for wrapAccountKeyForUser: '${userPublicKey.algorithm.name}'. Expected 'RSA-OAEP'.`
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
/**
|
|
1148
|
+
* Unwraps a Base64-encoded Account Data Encryption Key (ADEK) using the user's private key (RSA-OAEP).
|
|
1149
|
+
* Returns the restored AES-GCM CryptoKey ready for use in CWebCryptoEncryptionProvider.
|
|
1150
|
+
*/
|
|
1151
|
+
static async unwrapAccountKeyForUser(wrappedKeyBase64, userPrivateKey, unwrappedKeyAlgorithm = { name: "AES-GCM", length: 256 }, extractable = false) {
|
|
1152
|
+
const subtle = getSubtleCrypto();
|
|
1153
|
+
if (userPrivateKey.algorithm.name === "RSA-OAEP") {
|
|
1154
|
+
const wrappedBytes = CKeyManager.fromBase64(wrappedKeyBase64);
|
|
1155
|
+
let unwrappedRawKey;
|
|
1156
|
+
try {
|
|
1157
|
+
unwrappedRawKey = await subtle.decrypt(
|
|
1158
|
+
{ name: "RSA-OAEP" },
|
|
1159
|
+
userPrivateKey,
|
|
1160
|
+
wrappedBytes
|
|
854
1161
|
);
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
return [field, await this.encryptValue(result[field], fieldOptions)];
|
|
866
|
-
})
|
|
1162
|
+
} finally {
|
|
1163
|
+
wrappedBytes.fill(0);
|
|
1164
|
+
}
|
|
1165
|
+
try {
|
|
1166
|
+
return await subtle.importKey(
|
|
1167
|
+
"raw",
|
|
1168
|
+
unwrappedRawKey,
|
|
1169
|
+
unwrappedKeyAlgorithm,
|
|
1170
|
+
extractable,
|
|
1171
|
+
["encrypt", "decrypt"]
|
|
867
1172
|
);
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
1173
|
+
} finally {
|
|
1174
|
+
new Uint8Array(unwrappedRawKey).fill(0);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
throw new Error(
|
|
1178
|
+
`[CKeyManager] Unsupported user key algorithm for unwrapAccountKeyForUser: '${userPrivateKey.algorithm.name}'. Expected 'RSA-OAEP'.`
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* Wraps an HMAC blind indexing key for a specific recipient user using their RSA-OAEP public key.
|
|
1183
|
+
* Returns a Base64-encoded wrapped key string suitable for storage in role-map / access tables.
|
|
1184
|
+
*/
|
|
1185
|
+
static async wrapHMACKeyForUser(hmacKey, userPublicKey) {
|
|
1186
|
+
const subtle = getSubtleCrypto();
|
|
1187
|
+
if (userPublicKey.algorithm.name === "RSA-OAEP") {
|
|
1188
|
+
const rawKey = await subtle.exportKey("raw", hmacKey);
|
|
1189
|
+
try {
|
|
1190
|
+
const wrappedBuffer = await subtle.encrypt(
|
|
1191
|
+
{ name: "RSA-OAEP" },
|
|
1192
|
+
userPublicKey,
|
|
1193
|
+
rawKey
|
|
1194
|
+
);
|
|
1195
|
+
return CKeyManager.toBase64(wrappedBuffer);
|
|
1196
|
+
} finally {
|
|
1197
|
+
new Uint8Array(rawKey).fill(0);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
throw new Error(
|
|
1201
|
+
`[CKeyManager] Unsupported user key algorithm for wrapHMACKeyForUser: '${userPublicKey.algorithm.name}'. Expected 'RSA-OAEP'.`
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
/**
|
|
1205
|
+
* Unwraps a Base64-encoded HMAC key using a recipient's RSA-OAEP private key.
|
|
1206
|
+
* Restores the CryptoKey configured for HMAC signing and verification in search indexing.
|
|
1207
|
+
*/
|
|
1208
|
+
static async unwrapHMACKeyForUser(wrappedKeyBase64, userPrivateKey, hash = "SHA-256", extractable = false) {
|
|
1209
|
+
const subtle = getSubtleCrypto();
|
|
1210
|
+
if (userPrivateKey.algorithm.name === "RSA-OAEP") {
|
|
1211
|
+
const wrappedBytes = CKeyManager.fromBase64(wrappedKeyBase64);
|
|
1212
|
+
let unwrappedRawKey;
|
|
1213
|
+
try {
|
|
1214
|
+
unwrappedRawKey = await subtle.decrypt(
|
|
1215
|
+
{ name: "RSA-OAEP" },
|
|
1216
|
+
userPrivateKey,
|
|
1217
|
+
wrappedBytes
|
|
1218
|
+
);
|
|
1219
|
+
} finally {
|
|
1220
|
+
wrappedBytes.fill(0);
|
|
1221
|
+
}
|
|
1222
|
+
try {
|
|
1223
|
+
return await subtle.importKey(
|
|
1224
|
+
"raw",
|
|
1225
|
+
unwrappedRawKey,
|
|
1226
|
+
{ name: "HMAC", hash: { name: hash } },
|
|
1227
|
+
extractable,
|
|
1228
|
+
["sign", "verify"]
|
|
1229
|
+
);
|
|
1230
|
+
} finally {
|
|
1231
|
+
new Uint8Array(unwrappedRawKey).fill(0);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
throw new Error(
|
|
1235
|
+
`[CKeyManager] Unsupported user key algorithm for unwrapHMACKeyForUser: '${userPrivateKey.algorithm.name}'. Expected 'RSA-OAEP'.`
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Atomically re-wraps an Account Data Encryption Key (ADEK) for a new recipient
|
|
1240
|
+
* without exposing the raw extractable symmetric key to caller application code.
|
|
1241
|
+
*
|
|
1242
|
+
* @param wrappedKeyBase64 - The existing ADEK envelope wrapped with current user's public key
|
|
1243
|
+
* @param myPrivateKey - Current user's RSA-OAEP private key
|
|
1244
|
+
* @param recipientPublicKey - New recipient's RSA-OAEP public key
|
|
1245
|
+
* @returns Base64-encoded wrapped key envelope for the recipient
|
|
1246
|
+
*/
|
|
1247
|
+
static async rewrapAccountKeyForUser(wrappedKeyBase64, myPrivateKey, recipientPublicKey) {
|
|
1248
|
+
const subtle = getSubtleCrypto();
|
|
1249
|
+
if (!myPrivateKey || !myPrivateKey.algorithm || myPrivateKey.algorithm.name !== "RSA-OAEP" || !recipientPublicKey || !recipientPublicKey.algorithm || recipientPublicKey.algorithm.name !== "RSA-OAEP") {
|
|
1250
|
+
throw new Error(`[CKeyManager] rewrapAccountKeyForUser requires RSA-OAEP keys.`);
|
|
1251
|
+
}
|
|
1252
|
+
if (myPrivateKey.type === "public" || recipientPublicKey.type === "private") {
|
|
1253
|
+
throw new Error(
|
|
1254
|
+
`[CKeyManager] rewrapAccountKeyForUser requires an RSA-OAEP private key for decryption and an RSA-OAEP public key for encryption.`
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
const wrappedBytes = CKeyManager.fromBase64(wrappedKeyBase64);
|
|
1258
|
+
let rawKeyBuffer;
|
|
1259
|
+
try {
|
|
1260
|
+
rawKeyBuffer = await subtle.decrypt(
|
|
1261
|
+
{ name: "RSA-OAEP" },
|
|
1262
|
+
myPrivateKey,
|
|
1263
|
+
wrappedBytes
|
|
1264
|
+
);
|
|
1265
|
+
const rewrappedBuffer = await subtle.encrypt(
|
|
1266
|
+
{ name: "RSA-OAEP" },
|
|
1267
|
+
recipientPublicKey,
|
|
1268
|
+
rawKeyBuffer
|
|
1269
|
+
);
|
|
1270
|
+
return CKeyManager.toBase64(rewrappedBuffer);
|
|
1271
|
+
} finally {
|
|
1272
|
+
wrappedBytes.fill(0);
|
|
1273
|
+
if (rawKeyBuffer) {
|
|
1274
|
+
new Uint8Array(rawKeyBuffer).fill(0);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
// ── Key Derivation Functions (PBKDF2 & HKDF) ──────────────────────────────
|
|
1279
|
+
/**
|
|
1280
|
+
* Derives a symmetric AES-GCM CryptoKey from a passphrase using PBKDF2 or HKDF.
|
|
1281
|
+
* Returns the derived CryptoKey and the base64-encoded salt.
|
|
1282
|
+
*/
|
|
1283
|
+
static async deriveKey(passphrase, options) {
|
|
1284
|
+
if (!passphrase || typeof passphrase === "string" && passphrase.trim().length === 0 || typeof passphrase === "object" && "byteLength" in passphrase && passphrase.byteLength === 0) {
|
|
1285
|
+
throw new Error("[CKeyManager] Passphrase cannot be empty or undefined for key derivation.");
|
|
1286
|
+
}
|
|
1287
|
+
const subtle = getSubtleCrypto();
|
|
1288
|
+
const saltBytes = CKeyManager.resolveSalt(options.salt);
|
|
1289
|
+
const keyLength = options.keyLength || 256;
|
|
1290
|
+
const hash = options.hash || "SHA-256";
|
|
1291
|
+
const extractable = options.extractable === true;
|
|
1292
|
+
const rawKeyBytes = typeof passphrase === "string" ? textEncoder$1.encode(passphrase) : passphrase instanceof Uint8Array ? passphrase : new Uint8Array(passphrase);
|
|
1293
|
+
try {
|
|
1294
|
+
if (options.kdf === "pbkdf2") {
|
|
1295
|
+
const baseKey = await subtle.importKey(
|
|
1296
|
+
"raw",
|
|
1297
|
+
rawKeyBytes,
|
|
1298
|
+
"PBKDF2",
|
|
1299
|
+
false,
|
|
1300
|
+
["deriveKey"]
|
|
1301
|
+
);
|
|
1302
|
+
const derivedKey = await subtle.deriveKey(
|
|
1303
|
+
{
|
|
1304
|
+
name: "PBKDF2",
|
|
1305
|
+
salt: saltBytes,
|
|
1306
|
+
iterations: options.iterations || 31e4,
|
|
1307
|
+
hash
|
|
1308
|
+
},
|
|
1309
|
+
baseKey,
|
|
1310
|
+
{ name: "AES-GCM", length: keyLength },
|
|
1311
|
+
extractable,
|
|
1312
|
+
["encrypt", "decrypt"]
|
|
1313
|
+
);
|
|
1314
|
+
return {
|
|
1315
|
+
key: derivedKey,
|
|
1316
|
+
salt: CKeyManager.toBase64(saltBytes)
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
if (options.kdf === "hkdf") {
|
|
1320
|
+
const baseKey = await subtle.importKey(
|
|
1321
|
+
"raw",
|
|
1322
|
+
rawKeyBytes,
|
|
1323
|
+
"HKDF",
|
|
1324
|
+
false,
|
|
1325
|
+
["deriveKey"]
|
|
1326
|
+
);
|
|
1327
|
+
const infoBytes = resolveInfoBytes(options.info, "katlux-derived-aes-key");
|
|
1328
|
+
const derivedKey = await subtle.deriveKey(
|
|
1329
|
+
{
|
|
1330
|
+
name: "HKDF",
|
|
1331
|
+
salt: saltBytes,
|
|
1332
|
+
info: infoBytes,
|
|
1333
|
+
hash
|
|
1334
|
+
},
|
|
1335
|
+
baseKey,
|
|
1336
|
+
{ name: "AES-GCM", length: keyLength },
|
|
1337
|
+
extractable,
|
|
1338
|
+
["encrypt", "decrypt"]
|
|
1339
|
+
);
|
|
1340
|
+
return {
|
|
1341
|
+
key: derivedKey,
|
|
1342
|
+
salt: CKeyManager.toBase64(saltBytes)
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
throw new Error(`[CKeyManager] Unsupported KDF strategy: ${options.kdf}`);
|
|
1346
|
+
} finally {
|
|
1347
|
+
if (typeof passphrase === "string") {
|
|
1348
|
+
rawKeyBytes.fill(0);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
/**
|
|
1353
|
+
* Derives an AES-GCM CryptoKey from a high-entropy secret (e.g. an ECDH shared secret key)
|
|
1354
|
+
* using HKDF.
|
|
1355
|
+
*/
|
|
1356
|
+
static async deriveKeyFromSecret(secretKey, options = {}) {
|
|
1357
|
+
const subtle = getSubtleCrypto();
|
|
1358
|
+
let saltBytes;
|
|
1359
|
+
if (options.salt !== void 0) {
|
|
1360
|
+
saltBytes = CKeyManager.resolveSalt(options.salt);
|
|
1361
|
+
} else {
|
|
1362
|
+
saltBytes = new Uint8Array(16);
|
|
1363
|
+
}
|
|
1364
|
+
const infoBytes = resolveInfoBytes(options.info, "katlux-ecdh-aes-gcm");
|
|
1365
|
+
const hash = options.hash || "SHA-256";
|
|
1366
|
+
const keyLength = options.keyLength || 256;
|
|
1367
|
+
const extractable = options.extractable === true;
|
|
1368
|
+
return await subtle.deriveKey(
|
|
1369
|
+
{
|
|
1370
|
+
name: "HKDF",
|
|
1371
|
+
salt: saltBytes,
|
|
1372
|
+
info: infoBytes,
|
|
1373
|
+
hash
|
|
1374
|
+
},
|
|
1375
|
+
secretKey,
|
|
1376
|
+
{ name: "AES-GCM", length: keyLength },
|
|
1377
|
+
extractable,
|
|
1378
|
+
["encrypt", "decrypt"]
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1382
|
+
* Derives two domain-separated, cryptographically isolated keys from a user password and salt:
|
|
1383
|
+
* 1. `authKey`: For client authentication, SRP, or server challenge proofs.
|
|
1384
|
+
* 2. `encryptionKey`: For AES-GCM local and field-level data encryption.
|
|
1385
|
+
*
|
|
1386
|
+
* Stage 1 (Password Stretching): PBKDF2-HMAC-SHA256 stretches low-entropy password into master secret.
|
|
1387
|
+
* Stage 2 (Domain Expansion): HKDF-Expand derives subkeys using domain-separated context info tags.
|
|
1388
|
+
* Immediate memory zeroization of intermediate master secret buffer is guaranteed.
|
|
1389
|
+
*
|
|
1390
|
+
* @param password - Master user password or passphrase
|
|
1391
|
+
* @param salt - Salt (or omitted to generate a secure random 16-byte salt)
|
|
1392
|
+
* @param options - PBKDF2 and HKDF configuration options
|
|
1393
|
+
*/
|
|
1394
|
+
static async deriveAuthAndEncryptionKeys(password, salt, options = {}) {
|
|
1395
|
+
if (!password || typeof password === "string" && password.trim().length === 0 || typeof password === "object" && "byteLength" in password && password.byteLength === 0) {
|
|
1396
|
+
throw new Error("[CKeyManager] Password cannot be empty or undefined for key derivation.");
|
|
1397
|
+
}
|
|
1398
|
+
const subtle = getSubtleCrypto();
|
|
1399
|
+
let saltBytes;
|
|
1400
|
+
if (salt !== void 0) {
|
|
1401
|
+
saltBytes = CKeyManager.resolveSalt(salt);
|
|
1402
|
+
} else {
|
|
1403
|
+
saltBytes = CKeyManager.randomBytes(16);
|
|
1404
|
+
}
|
|
1405
|
+
const saltBase64 = CKeyManager.toBase64(saltBytes);
|
|
1406
|
+
const iterations = options.iterations || 31e4;
|
|
1407
|
+
const hash = options.hash || "SHA-256";
|
|
1408
|
+
const authKeyLength = options.authKeyLength || 256;
|
|
1409
|
+
const encryptionKeyLength = options.encryptionKeyLength || 256;
|
|
1410
|
+
const authAlgorithm = options.authAlgorithm || "HMAC";
|
|
1411
|
+
const authInfo = resolveInfoBytes(options.authInfo, "katlux-auth-key-v1");
|
|
1412
|
+
const encryptionInfo = resolveInfoBytes(options.encryptionInfo, "katlux-encryption-key-v1");
|
|
1413
|
+
const extractable = options.extractable === true;
|
|
1414
|
+
const rawPasswordBytes = typeof password === "string" ? textEncoder$1.encode(password) : password instanceof Uint8Array ? new Uint8Array(password.buffer, password.byteOffset, password.byteLength) : new Uint8Array(password);
|
|
1415
|
+
let masterBits;
|
|
1416
|
+
try {
|
|
1417
|
+
const baseKey = await subtle.importKey(
|
|
1418
|
+
"raw",
|
|
1419
|
+
rawPasswordBytes,
|
|
1420
|
+
"PBKDF2",
|
|
1421
|
+
false,
|
|
1422
|
+
["deriveBits"]
|
|
1423
|
+
);
|
|
1424
|
+
const bitsLength = hash === "SHA-512" ? 512 : 256;
|
|
1425
|
+
masterBits = await subtle.deriveBits(
|
|
1426
|
+
{
|
|
1427
|
+
name: "PBKDF2",
|
|
1428
|
+
salt: saltBytes,
|
|
1429
|
+
iterations,
|
|
1430
|
+
hash
|
|
1431
|
+
},
|
|
1432
|
+
baseKey,
|
|
1433
|
+
bitsLength
|
|
1434
|
+
);
|
|
1435
|
+
const hkdfKey = await subtle.importKey(
|
|
1436
|
+
"raw",
|
|
1437
|
+
masterBits,
|
|
1438
|
+
"HKDF",
|
|
1439
|
+
false,
|
|
1440
|
+
["deriveKey"]
|
|
1441
|
+
);
|
|
1442
|
+
const authDerivedAlgorithm = authAlgorithm === "AES-GCM" ? { name: "AES-GCM", length: authKeyLength } : { name: "HMAC", hash: { name: hash }, length: authKeyLength };
|
|
1443
|
+
const authUsages = authAlgorithm === "AES-GCM" ? ["encrypt", "decrypt"] : ["sign", "verify"];
|
|
1444
|
+
const authKey = await subtle.deriveKey(
|
|
1445
|
+
{
|
|
1446
|
+
name: "HKDF",
|
|
1447
|
+
hash,
|
|
1448
|
+
salt: new Uint8Array(0),
|
|
1449
|
+
info: authInfo
|
|
1450
|
+
},
|
|
1451
|
+
hkdfKey,
|
|
1452
|
+
authDerivedAlgorithm,
|
|
1453
|
+
extractable,
|
|
1454
|
+
authUsages
|
|
1455
|
+
);
|
|
1456
|
+
const encryptionKey = await subtle.deriveKey(
|
|
1457
|
+
{
|
|
1458
|
+
name: "HKDF",
|
|
1459
|
+
hash,
|
|
1460
|
+
salt: new Uint8Array(0),
|
|
1461
|
+
info: encryptionInfo
|
|
1462
|
+
},
|
|
1463
|
+
hkdfKey,
|
|
1464
|
+
{ name: "AES-GCM", length: encryptionKeyLength },
|
|
1465
|
+
extractable,
|
|
1466
|
+
["encrypt", "decrypt"]
|
|
1467
|
+
);
|
|
1468
|
+
return {
|
|
1469
|
+
authKey,
|
|
1470
|
+
encryptionKey,
|
|
1471
|
+
salt: saltBase64
|
|
1472
|
+
};
|
|
1473
|
+
} finally {
|
|
1474
|
+
if (typeof password === "string") {
|
|
1475
|
+
rawPasswordBytes.fill(0);
|
|
872
1476
|
}
|
|
873
|
-
if (
|
|
874
|
-
|
|
875
|
-
const keysToEncrypt = Object.keys(result).filter(
|
|
876
|
-
(key) => result[key] !== void 0 && result[key] !== null && !this.isEncrypted(result[key])
|
|
877
|
-
);
|
|
878
|
-
if (keysToEncrypt.length === 0) {
|
|
879
|
-
return result;
|
|
880
|
-
}
|
|
881
|
-
if (!options?._warmed) {
|
|
882
|
-
await this.warmupKey(options);
|
|
883
|
-
}
|
|
884
|
-
const entries = await Promise.all(
|
|
885
|
-
keysToEncrypt.map(async (key) => {
|
|
886
|
-
const fieldOptions = this._bindFieldAad(data, key, options);
|
|
887
|
-
return [key, await this.encryptValue(result[key], fieldOptions)];
|
|
888
|
-
})
|
|
889
|
-
);
|
|
890
|
-
for (const [key, encryptedVal] of entries) {
|
|
891
|
-
safeSetProperty(result, key, encryptedVal);
|
|
892
|
-
}
|
|
893
|
-
return result;
|
|
1477
|
+
if (masterBits) {
|
|
1478
|
+
new Uint8Array(masterBits).fill(0);
|
|
894
1479
|
}
|
|
895
1480
|
}
|
|
896
|
-
return this.encryptValue(data, options);
|
|
897
1481
|
}
|
|
1482
|
+
// ── Digital Signatures, Fingerprints & Access Grants ─────────────────────
|
|
898
1483
|
/**
|
|
899
|
-
*
|
|
900
|
-
* 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.
|
|
1484
|
+
* Generates an ECDSA P-256 signing key pair.
|
|
904
1485
|
*/
|
|
1486
|
+
static async generateSigningKeyPair(curve = "P-256", extractable = true) {
|
|
1487
|
+
const subtle = getSubtleCrypto();
|
|
1488
|
+
const pair = await subtle.generateKey(
|
|
1489
|
+
{
|
|
1490
|
+
name: "ECDSA",
|
|
1491
|
+
namedCurve: curve
|
|
1492
|
+
},
|
|
1493
|
+
extractable,
|
|
1494
|
+
["sign", "verify"]
|
|
1495
|
+
);
|
|
1496
|
+
return {
|
|
1497
|
+
publicKey: pair.publicKey,
|
|
1498
|
+
privateKey: pair.privateKey
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
905
1501
|
/**
|
|
906
|
-
*
|
|
907
|
-
* Automatically performs value-level decryption:
|
|
908
|
-
* - If payload is an encrypted envelope: decrypts directly via decryptValue.
|
|
909
|
-
* - If payload is an array of objects: decrypts each row (batched with local memoization).
|
|
910
|
-
* - If payload is an object: decrypts any encrypted value cells across fields.
|
|
1502
|
+
* Generates an ECDSA signing key pair. Alias for generateSigningKeyPair.
|
|
911
1503
|
*/
|
|
912
|
-
async
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
const result = proto && proto !== Object.prototype ? Object.assign(Object.create(proto), item) : { ...item };
|
|
940
|
-
await Promise.all(
|
|
941
|
-
keysToDecrypt.map(async (key) => {
|
|
942
|
-
const env = item[key];
|
|
943
|
-
const boundOptions = this._bindFieldAad(item, key, options);
|
|
944
|
-
const hasBoundAad = boundOptions.aad !== options?.aad;
|
|
945
|
-
const ct = env.ciphertext;
|
|
946
|
-
const iv = env.iv || "";
|
|
947
|
-
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);
|
|
957
|
-
}
|
|
958
|
-
const decryptedVal = await localMemo.get(memoKey);
|
|
959
|
-
safeSetProperty(result, key, decryptedVal);
|
|
960
|
-
})
|
|
961
|
-
);
|
|
962
|
-
return result;
|
|
963
|
-
}
|
|
964
|
-
return item;
|
|
965
|
-
};
|
|
966
|
-
if (payload.length <= 100) {
|
|
967
|
-
return Promise.all(payload.map(decryptRow));
|
|
968
|
-
}
|
|
969
|
-
const batchSize = 1e3;
|
|
970
|
-
const results = new Array(payload.length);
|
|
971
|
-
for (let i = 0; i < payload.length; i += batchSize) {
|
|
972
|
-
const end = Math.min(i + batchSize, payload.length);
|
|
973
|
-
const chunk = payload.slice(i, end);
|
|
974
|
-
const chunkRes = await Promise.all(chunk.map(decryptRow));
|
|
975
|
-
for (let j = 0; j < chunkRes.length; j++) {
|
|
976
|
-
results[i + j] = chunkRes[j];
|
|
977
|
-
}
|
|
978
|
-
if (end < payload.length && (i > 0 && i % 5e3 === 0)) {
|
|
979
|
-
await new Promise((r) => setTimeout(r, 0));
|
|
1504
|
+
static async generateECDSAKeyPair(curve = "P-256", extractable = true) {
|
|
1505
|
+
return CKeyManager.generateSigningKeyPair(curve, extractable);
|
|
1506
|
+
}
|
|
1507
|
+
/**
|
|
1508
|
+
* Generates a deterministic SHA-256 fingerprint for a public key (SPKI or raw).
|
|
1509
|
+
* Formatted as `SHA256:<base64url>`.
|
|
1510
|
+
*/
|
|
1511
|
+
static async getKeyFingerprint(publicKey) {
|
|
1512
|
+
const subtle = getSubtleCrypto();
|
|
1513
|
+
let rawBytes;
|
|
1514
|
+
if (typeof publicKey === "object" && publicKey !== null && "algorithm" in publicKey) {
|
|
1515
|
+
const cryptoKey = publicKey;
|
|
1516
|
+
rawBytes = await subtle.exportKey("spki", cryptoKey);
|
|
1517
|
+
} else if (publicKey instanceof Uint8Array) {
|
|
1518
|
+
rawBytes = publicKey;
|
|
1519
|
+
} else if (publicKey instanceof ArrayBuffer) {
|
|
1520
|
+
rawBytes = publicKey;
|
|
1521
|
+
} else if (typeof publicKey === "string") {
|
|
1522
|
+
const trimmed = publicKey.trim();
|
|
1523
|
+
if (trimmed.startsWith("-----BEGIN")) {
|
|
1524
|
+
const b642 = trimmed.replace(/-----[^-]+-----/g, "").replace(/\s+/g, "");
|
|
1525
|
+
rawBytes = CKeyManager.fromBase64(b642);
|
|
1526
|
+
} else {
|
|
1527
|
+
try {
|
|
1528
|
+
rawBytes = CKeyManager.fromBase64(trimmed);
|
|
1529
|
+
} catch {
|
|
1530
|
+
rawBytes = textEncoder$1.encode(trimmed);
|
|
980
1531
|
}
|
|
981
1532
|
}
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
const targetKeys = options?.fields && options.fields.length > 0 ? options.fields.filter((f) => Object.prototype.hasOwnProperty.call(payload, f)) : Object.keys(payload);
|
|
986
|
-
const keysToDecrypt = targetKeys.filter((key) => this.isEncrypted(payload[key]));
|
|
987
|
-
if (keysToDecrypt.length === 0) {
|
|
988
|
-
return payload;
|
|
989
|
-
}
|
|
990
|
-
const proto = Object.getPrototypeOf(payload);
|
|
991
|
-
const result = proto && proto !== Object.prototype ? Object.assign(Object.create(proto), payload) : { ...payload };
|
|
992
|
-
const entries = await Promise.all(
|
|
993
|
-
keysToDecrypt.map(async (key) => {
|
|
994
|
-
const boundOptions = this._bindFieldAad(payload, key, options);
|
|
995
|
-
const hasBoundAad = boundOptions.aad !== options?.aad;
|
|
996
|
-
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;
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
throw err;
|
|
1007
|
-
}
|
|
1008
|
-
})
|
|
1533
|
+
} else {
|
|
1534
|
+
throw new Error(
|
|
1535
|
+
"[CKeyManager] Invalid key input for getKeyFingerprint: expected CryptoKey, Uint8Array, ArrayBuffer, or string."
|
|
1009
1536
|
);
|
|
1010
|
-
for (const [key, decryptedVal] of entries) {
|
|
1011
|
-
safeSetProperty(result, key, decryptedVal);
|
|
1012
|
-
}
|
|
1013
|
-
return result;
|
|
1014
1537
|
}
|
|
1015
|
-
|
|
1538
|
+
const hashBuffer = await subtle.digest("SHA-256", rawBytes);
|
|
1539
|
+
const b64 = CKeyManager.toBase64(hashBuffer);
|
|
1540
|
+
const b64url = b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1541
|
+
return `SHA256:${b64url}`;
|
|
1016
1542
|
}
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
class CKeyManager {
|
|
1020
|
-
// ── Binary & Encoding Utilities ──────────────────────────────────────────
|
|
1021
1543
|
/**
|
|
1022
|
-
*
|
|
1544
|
+
* Signs arbitrary data or JSON objects using an ECDSA P-256 private key.
|
|
1545
|
+
* Objects are automatically serialized via canonicalizeJson() before hashing.
|
|
1023
1546
|
*/
|
|
1024
|
-
static
|
|
1025
|
-
const
|
|
1026
|
-
|
|
1547
|
+
static async sign(data, privateKey) {
|
|
1548
|
+
const subtle = getSubtleCrypto();
|
|
1549
|
+
if (!privateKey || !privateKey.algorithm || privateKey.algorithm.name !== "ECDSA") {
|
|
1550
|
+
throw new Error("[CKeyManager] sign requires an ECDSA private key.");
|
|
1551
|
+
}
|
|
1552
|
+
let dataBytes;
|
|
1553
|
+
if (data instanceof Uint8Array) {
|
|
1554
|
+
dataBytes = data;
|
|
1555
|
+
} else if (ArrayBuffer.isView(data)) {
|
|
1556
|
+
dataBytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
1557
|
+
} else if (data instanceof ArrayBuffer) {
|
|
1558
|
+
dataBytes = new Uint8Array(data);
|
|
1559
|
+
} else if (typeof data === "string") {
|
|
1560
|
+
dataBytes = textEncoder$1.encode(data);
|
|
1561
|
+
} else {
|
|
1562
|
+
const canonical = canonicalizeJson(data);
|
|
1563
|
+
dataBytes = textEncoder$1.encode(canonical);
|
|
1564
|
+
}
|
|
1565
|
+
const namedCurve = privateKey.algorithm.namedCurve || "P-256";
|
|
1566
|
+
const hashName = namedCurve === "P-384" ? "SHA-384" : namedCurve === "P-521" ? "SHA-512" : "SHA-256";
|
|
1567
|
+
const signatureBuffer = await subtle.sign(
|
|
1568
|
+
{
|
|
1569
|
+
name: "ECDSA",
|
|
1570
|
+
hash: { name: hashName }
|
|
1571
|
+
},
|
|
1572
|
+
privateKey,
|
|
1573
|
+
dataBytes
|
|
1574
|
+
);
|
|
1575
|
+
return CKeyManager.toBase64(signatureBuffer);
|
|
1027
1576
|
}
|
|
1028
1577
|
/**
|
|
1029
|
-
*
|
|
1578
|
+
* Verifies an ECDSA P-256 signature against original data or JSON object.
|
|
1030
1579
|
*/
|
|
1031
|
-
static
|
|
1032
|
-
|
|
1580
|
+
static async verify(data, signature, publicKey) {
|
|
1581
|
+
const subtle = getSubtleCrypto();
|
|
1582
|
+
if (!publicKey || !publicKey.algorithm || publicKey.algorithm.name !== "ECDSA") {
|
|
1583
|
+
throw new Error("[CKeyManager] verify requires an ECDSA public key.");
|
|
1584
|
+
}
|
|
1585
|
+
if (typeof signature !== "string" || !signature.trim()) {
|
|
1586
|
+
return false;
|
|
1587
|
+
}
|
|
1588
|
+
try {
|
|
1589
|
+
const signatureBytes = CKeyManager.fromBase64(signature);
|
|
1590
|
+
let dataBytes;
|
|
1591
|
+
if (data instanceof Uint8Array) {
|
|
1592
|
+
dataBytes = data;
|
|
1593
|
+
} else if (ArrayBuffer.isView(data)) {
|
|
1594
|
+
dataBytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
1595
|
+
} else if (data instanceof ArrayBuffer) {
|
|
1596
|
+
dataBytes = new Uint8Array(data);
|
|
1597
|
+
} else if (typeof data === "string") {
|
|
1598
|
+
dataBytes = textEncoder$1.encode(data);
|
|
1599
|
+
} else {
|
|
1600
|
+
const canonical = canonicalizeJson(data);
|
|
1601
|
+
dataBytes = textEncoder$1.encode(canonical);
|
|
1602
|
+
}
|
|
1603
|
+
const namedCurve = publicKey.algorithm.namedCurve || "P-256";
|
|
1604
|
+
const hashName = namedCurve === "P-384" ? "SHA-384" : namedCurve === "P-521" ? "SHA-512" : "SHA-256";
|
|
1605
|
+
return await subtle.verify(
|
|
1606
|
+
{
|
|
1607
|
+
name: "ECDSA",
|
|
1608
|
+
hash: { name: hashName }
|
|
1609
|
+
},
|
|
1610
|
+
publicKey,
|
|
1611
|
+
signatureBytes,
|
|
1612
|
+
dataBytes
|
|
1613
|
+
);
|
|
1614
|
+
} catch {
|
|
1615
|
+
return false;
|
|
1616
|
+
}
|
|
1033
1617
|
}
|
|
1034
1618
|
/**
|
|
1035
|
-
*
|
|
1036
|
-
*
|
|
1619
|
+
* Computes a deterministic HMAC-SHA256 blind index hash for an arbitrary term using the supplied CryptoKey.
|
|
1620
|
+
* Performs Unicode NFKC normalization, trimming, and lowercasing before computing the signature.
|
|
1621
|
+
*
|
|
1622
|
+
* @param value - The input string, number, or object to compute the blind index for.
|
|
1623
|
+
* @param hmacKey - CryptoKey configured for HMAC signing.
|
|
1624
|
+
* @returns Lowercase hexadecimal HMAC-SHA256 blind index hash.
|
|
1037
1625
|
*/
|
|
1038
|
-
static
|
|
1039
|
-
if (
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
const normalized = cleaned.replace(/-/g, "+").replace(/_/g, "/");
|
|
1045
|
-
const padded = normalized.padEnd(normalized.length + (4 - normalized.length % 4) % 4, "=");
|
|
1046
|
-
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(padded)) {
|
|
1047
|
-
throw new Error("[CKeyManager] Invalid base64 string");
|
|
1626
|
+
static async computeBlindIndex(value, hmacKey) {
|
|
1627
|
+
if (value === null || value === void 0) return "";
|
|
1628
|
+
const normalized = String(value).normalize("NFKC").trim().toLowerCase();
|
|
1629
|
+
if (!normalized) return "";
|
|
1630
|
+
if (!hmacKey || !hmacKey.algorithm || hmacKey.algorithm.name !== "HMAC") {
|
|
1631
|
+
throw new Error("[CKeyManager] computeBlindIndex requires an HMAC CryptoKey.");
|
|
1048
1632
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1633
|
+
const subtle = getSubtleCrypto();
|
|
1634
|
+
const signature = await subtle.sign("HMAC", hmacKey, textEncoder$1.encode(normalized));
|
|
1635
|
+
return Array.from(new Uint8Array(signature)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* Signs a structured access grant, embedding signature and granter key fingerprint.
|
|
1639
|
+
*/
|
|
1640
|
+
static async signGrant(grant, granterPrivateKey, granterPublicKey) {
|
|
1641
|
+
if (!grant || typeof grant !== "object") {
|
|
1642
|
+
throw new Error("[CKeyManager] signGrant requires a valid grant object.");
|
|
1053
1643
|
}
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1644
|
+
let fingerprint;
|
|
1645
|
+
if (granterPublicKey) {
|
|
1646
|
+
fingerprint = await CKeyManager.getKeyFingerprint(granterPublicKey);
|
|
1647
|
+
} else {
|
|
1648
|
+
const subtle = getSubtleCrypto();
|
|
1649
|
+
try {
|
|
1650
|
+
const jwk = await subtle.exportKey("jwk", granterPrivateKey);
|
|
1651
|
+
const pubJwk = {
|
|
1652
|
+
kty: jwk.kty,
|
|
1653
|
+
crv: jwk.crv,
|
|
1654
|
+
x: jwk.x,
|
|
1655
|
+
y: jwk.y,
|
|
1656
|
+
ext: true,
|
|
1657
|
+
key_ops: ["verify"]
|
|
1658
|
+
};
|
|
1659
|
+
const pubKey = await subtle.importKey(
|
|
1660
|
+
"jwk",
|
|
1661
|
+
pubJwk,
|
|
1662
|
+
{ name: "ECDSA", namedCurve: jwk.crv || "P-256" },
|
|
1663
|
+
true,
|
|
1664
|
+
["verify"]
|
|
1665
|
+
);
|
|
1666
|
+
fingerprint = await CKeyManager.getKeyFingerprint(pubKey);
|
|
1667
|
+
} catch {
|
|
1668
|
+
throw new Error(
|
|
1669
|
+
"[CKeyManager] signGrant requires granterPublicKey when granterPrivateKey is non-extractable."
|
|
1670
|
+
);
|
|
1671
|
+
}
|
|
1058
1672
|
}
|
|
1059
|
-
|
|
1673
|
+
const signature = await CKeyManager.sign(grant, granterPrivateKey);
|
|
1674
|
+
return {
|
|
1675
|
+
grant: { ...grant },
|
|
1676
|
+
signature,
|
|
1677
|
+
fingerprint
|
|
1678
|
+
};
|
|
1060
1679
|
}
|
|
1061
1680
|
/**
|
|
1062
|
-
*
|
|
1063
|
-
* Guarantees 100% identical salt byte resolution between Node.js and Browser environments.
|
|
1681
|
+
* Verifies the authenticity and integrity of a signed access grant against granter public key.
|
|
1064
1682
|
*/
|
|
1065
|
-
static
|
|
1066
|
-
if (!
|
|
1067
|
-
|
|
1068
|
-
return salt.byteLength > 0 ? new Uint8Array(salt.buffer.slice(salt.byteOffset, salt.byteOffset + salt.byteLength)) : CKeyManager.randomBytes(16);
|
|
1683
|
+
static async verifyGrant(signedGrant, granterPublicKey, options) {
|
|
1684
|
+
if (!signedGrant || !signedGrant.grant || !signedGrant.signature || !signedGrant.fingerprint || !granterPublicKey) {
|
|
1685
|
+
return false;
|
|
1069
1686
|
}
|
|
1070
|
-
|
|
1071
|
-
|
|
1687
|
+
const expectedFingerprint = await CKeyManager.getKeyFingerprint(granterPublicKey);
|
|
1688
|
+
if (signedGrant.fingerprint !== expectedFingerprint) {
|
|
1689
|
+
return false;
|
|
1072
1690
|
}
|
|
1073
|
-
if (
|
|
1074
|
-
const
|
|
1075
|
-
if (
|
|
1076
|
-
|
|
1077
|
-
return CKeyManager.fromBase64(trimmed.slice(7));
|
|
1078
|
-
}
|
|
1079
|
-
if (trimmed.endsWith("=") && /^[A-Za-z0-9+/]+={1,2}$/.test(trimmed)) {
|
|
1080
|
-
try {
|
|
1081
|
-
const decoded = CKeyManager.fromBase64(trimmed);
|
|
1082
|
-
if (decoded.byteLength > 0) return decoded;
|
|
1083
|
-
} catch {
|
|
1084
|
-
}
|
|
1691
|
+
if (options?.checkExpiry && signedGrant.grant.expiresAt !== void 0) {
|
|
1692
|
+
const now = options.now ?? Date.now();
|
|
1693
|
+
if (now > signedGrant.grant.expiresAt) {
|
|
1694
|
+
return false;
|
|
1085
1695
|
}
|
|
1086
|
-
const encoded = textEncoder$1.encode(trimmed);
|
|
1087
|
-
return encoded.byteLength > 0 ? encoded : CKeyManager.randomBytes(16);
|
|
1088
1696
|
}
|
|
1089
|
-
return CKeyManager.
|
|
1697
|
+
return await CKeyManager.verify(signedGrant.grant, signedGrant.signature, granterPublicKey);
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
class AEncryptionProvider {
|
|
1702
|
+
defaultKey;
|
|
1703
|
+
_hmacKey;
|
|
1704
|
+
keys;
|
|
1705
|
+
activeKid;
|
|
1706
|
+
allowedKids;
|
|
1707
|
+
constructor(key, options) {
|
|
1708
|
+
if (key !== void 0) {
|
|
1709
|
+
this.defaultKey = key;
|
|
1710
|
+
}
|
|
1711
|
+
if (options?.keys) {
|
|
1712
|
+
this.keys = options.keys;
|
|
1713
|
+
}
|
|
1714
|
+
if (options?.activeKid) {
|
|
1715
|
+
this.activeKid = options.activeKid;
|
|
1716
|
+
}
|
|
1717
|
+
if (options?.allowedKids) {
|
|
1718
|
+
this.allowedKids = options.allowedKids;
|
|
1719
|
+
}
|
|
1720
|
+
if (options?.hmacKey) {
|
|
1721
|
+
this._hmacKey = options.hmacKey;
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
/**
|
|
1725
|
+
* Sets or updates the default secret key, CryptoKey, or async key provider function.
|
|
1726
|
+
*/
|
|
1727
|
+
setDefaultKey(key) {
|
|
1728
|
+
this.defaultKey = key || void 0;
|
|
1729
|
+
}
|
|
1730
|
+
/**
|
|
1731
|
+
* Retrieves the currently configured default key or key provider callback.
|
|
1732
|
+
*/
|
|
1733
|
+
getDefaultKey() {
|
|
1734
|
+
return this.defaultKey;
|
|
1735
|
+
}
|
|
1736
|
+
/**
|
|
1737
|
+
* Configures a keyring dictionary of keys with an optional active key identifier.
|
|
1738
|
+
*/
|
|
1739
|
+
setKeyring(keys, activeKid) {
|
|
1740
|
+
this.keys = keys;
|
|
1741
|
+
if (activeKid !== void 0) {
|
|
1742
|
+
this.activeKid = activeKid;
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
/**
|
|
1746
|
+
* Retrieves the currently configured keyring dictionary.
|
|
1747
|
+
*/
|
|
1748
|
+
getKeyring() {
|
|
1749
|
+
return this.keys;
|
|
1750
|
+
}
|
|
1751
|
+
/**
|
|
1752
|
+
* Sets the active Key Identifier (kid) to use for outbound encryption.
|
|
1753
|
+
*/
|
|
1754
|
+
setActiveKid(kid) {
|
|
1755
|
+
this.activeKid = kid;
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* Retrieves the active Key Identifier (kid).
|
|
1759
|
+
*/
|
|
1760
|
+
getActiveKid() {
|
|
1761
|
+
return this.activeKid;
|
|
1762
|
+
}
|
|
1763
|
+
/**
|
|
1764
|
+
* Sets or updates the dedicated HMAC key or key provider callback for blind indexing.
|
|
1765
|
+
*/
|
|
1766
|
+
setHmacKey(key) {
|
|
1767
|
+
this._hmacKey = key || void 0;
|
|
1768
|
+
}
|
|
1769
|
+
/**
|
|
1770
|
+
* Retrieves the currently configured HMAC key or key provider callback.
|
|
1771
|
+
*/
|
|
1772
|
+
getHmacKey() {
|
|
1773
|
+
return this._hmacKey;
|
|
1090
1774
|
}
|
|
1091
|
-
// ── Symmetric Key Management ─────────────────────────────────────────────
|
|
1092
1775
|
/**
|
|
1093
|
-
*
|
|
1094
|
-
*
|
|
1095
|
-
* E2EE Threat Model Note: Focuses on protecting GDPR/tenant data against database and
|
|
1096
|
-
* server-side leaks. Keys remain extractable by default to enable role-based key sharing,
|
|
1097
|
-
* wrapping, and transfer between authorized client sessions in multi-user SaaS apps.
|
|
1098
|
-
* Client-side malware/trojan protection is out of scope.
|
|
1776
|
+
* Computes a deterministic HMAC-SHA256 blind index hash for an arbitrary term.
|
|
1777
|
+
* If no hmacKey is explicitly provided, uses the provider's configured hmacKey or default key.
|
|
1099
1778
|
*/
|
|
1100
|
-
|
|
1779
|
+
async computeBlindIndex(value, hmacKey) {
|
|
1780
|
+
if (value === null || value === void 0) return "";
|
|
1781
|
+
const keyMaterial = hmacKey || this._hmacKey || this.defaultKey;
|
|
1782
|
+
if (!keyMaterial) {
|
|
1783
|
+
throw new Error("[AEncryptionProvider] computeBlindIndex requires an HMAC key. Configure hmacKey or pass a key directly.");
|
|
1784
|
+
}
|
|
1785
|
+
const resolvedKey = typeof keyMaterial === "function" ? await keyMaterial() : keyMaterial;
|
|
1786
|
+
if (resolvedKey && typeof resolvedKey !== "string" && resolvedKey.algorithm) {
|
|
1787
|
+
return CKeyManager.computeBlindIndex(value, resolvedKey);
|
|
1788
|
+
}
|
|
1101
1789
|
const subtle = getSubtleCrypto();
|
|
1102
|
-
const
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1790
|
+
const keyBytes = typeof resolvedKey === "string" ? new TextEncoder().encode(resolvedKey) : resolvedKey instanceof Uint8Array ? resolvedKey : new Uint8Array(resolvedKey);
|
|
1791
|
+
const importedHmacKey = await subtle.importKey(
|
|
1792
|
+
"raw",
|
|
1793
|
+
keyBytes,
|
|
1794
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
1795
|
+
false,
|
|
1796
|
+
["sign"]
|
|
1107
1797
|
);
|
|
1798
|
+
return CKeyManager.computeBlindIndex(value, importedHmacKey);
|
|
1108
1799
|
}
|
|
1109
1800
|
/**
|
|
1110
|
-
*
|
|
1801
|
+
* Pre-derives and caches the native CryptoKey for subsequent cryptographic operations.
|
|
1802
|
+
* Subclasses should override this with a dedicated, non-side-effecting implementation.
|
|
1111
1803
|
*/
|
|
1112
|
-
|
|
1113
|
-
const subtle = getSubtleCrypto();
|
|
1114
|
-
const raw = await subtle.exportKey("raw", key);
|
|
1115
|
-
try {
|
|
1116
|
-
return CKeyManager.toBase64(raw);
|
|
1117
|
-
} finally {
|
|
1118
|
-
new Uint8Array(raw).fill(0);
|
|
1119
|
-
}
|
|
1804
|
+
async warmupKey(_options) {
|
|
1120
1805
|
}
|
|
1121
1806
|
/**
|
|
1122
|
-
*
|
|
1807
|
+
* Resolves raw key from keyProvider callback or defaultKey.
|
|
1123
1808
|
*/
|
|
1124
|
-
|
|
1125
|
-
const
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1809
|
+
async resolveRawKey(keyProvider) {
|
|
1810
|
+
const target = keyProvider || (this.keys && this.activeKid ? this.keys[this.activeKid] : void 0) || this.defaultKey;
|
|
1811
|
+
if (!target) {
|
|
1812
|
+
throw new Error(
|
|
1813
|
+
`[${this.constructor.name}] No encryption key or keyProvider was supplied. Key cannot be empty or undefined. A valid key must be supplied from your authentication or secret management system.`
|
|
1814
|
+
);
|
|
1130
1815
|
}
|
|
1131
|
-
const
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
rawBytes,
|
|
1136
|
-
{ name: algorithm },
|
|
1137
|
-
extractable,
|
|
1138
|
-
usages
|
|
1816
|
+
const rawKey = typeof target === "function" ? await target() : target;
|
|
1817
|
+
if (!rawKey || typeof rawKey === "string" && rawKey.trim().length === 0 || typeof rawKey === "object" && "byteLength" in rawKey && rawKey.byteLength === 0) {
|
|
1818
|
+
throw new Error(
|
|
1819
|
+
`[${this.constructor.name}] No encryption key or keyProvider was supplied. Key provider resolved to empty or undefined key.`
|
|
1139
1820
|
);
|
|
1140
|
-
} finally {
|
|
1141
|
-
rawBytes.fill(0);
|
|
1142
1821
|
}
|
|
1822
|
+
return rawKey;
|
|
1143
1823
|
}
|
|
1144
|
-
// ── Asymmetric Key Pair Management ───────────────────────────────────────
|
|
1145
1824
|
/**
|
|
1146
|
-
*
|
|
1825
|
+
* Checks whether the given payload is an encrypted envelope.
|
|
1147
1826
|
*/
|
|
1148
|
-
static
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
{
|
|
1152
|
-
name: "RSA-OAEP",
|
|
1153
|
-
modulusLength: bits,
|
|
1154
|
-
publicExponent: new Uint8Array([1, 0, 1]),
|
|
1155
|
-
hash
|
|
1156
|
-
},
|
|
1157
|
-
extractable,
|
|
1158
|
-
["encrypt", "decrypt", "wrapKey", "unwrapKey"]
|
|
1159
|
-
);
|
|
1160
|
-
return {
|
|
1161
|
-
publicKey: pair.publicKey,
|
|
1162
|
-
privateKey: pair.privateKey
|
|
1163
|
-
};
|
|
1827
|
+
static isEncrypted(payload) {
|
|
1828
|
+
if (!payload || typeof payload !== "object") return false;
|
|
1829
|
+
return payload.__encrypted === true && typeof payload.ciphertext === "string" && typeof payload.iv === "string";
|
|
1164
1830
|
}
|
|
1165
1831
|
/**
|
|
1166
|
-
*
|
|
1832
|
+
* Checks whether the given payload is an encrypted envelope.
|
|
1167
1833
|
*/
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
const pair = await subtle.generateKey(
|
|
1171
|
-
{
|
|
1172
|
-
name: "ECDH",
|
|
1173
|
-
namedCurve: curve
|
|
1174
|
-
},
|
|
1175
|
-
extractable,
|
|
1176
|
-
["deriveKey", "deriveBits"]
|
|
1177
|
-
);
|
|
1178
|
-
return {
|
|
1179
|
-
publicKey: pair.publicKey,
|
|
1180
|
-
privateKey: pair.privateKey
|
|
1181
|
-
};
|
|
1834
|
+
isEncrypted(payload) {
|
|
1835
|
+
return AEncryptionProvider.isEncrypted(payload);
|
|
1182
1836
|
}
|
|
1183
1837
|
/**
|
|
1184
|
-
*
|
|
1838
|
+
* Clears configured keys and cached cryptographic material from memory.
|
|
1185
1839
|
*/
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1840
|
+
clearKeys() {
|
|
1841
|
+
this.defaultKey = void 0;
|
|
1842
|
+
this.keys = void 0;
|
|
1843
|
+
this.activeKid = void 0;
|
|
1844
|
+
this.allowedKids = void 0;
|
|
1189
1845
|
}
|
|
1190
1846
|
/**
|
|
1191
|
-
*
|
|
1847
|
+
* Alias for clearKeys() to conform with standard disposal patterns.
|
|
1192
1848
|
*/
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
return await subtle.exportKey("jwk", key);
|
|
1849
|
+
dispose() {
|
|
1850
|
+
this.clearKeys();
|
|
1196
1851
|
}
|
|
1197
1852
|
/**
|
|
1198
|
-
*
|
|
1853
|
+
* ECMAScript Explicit Resource Management protocol (using / await using declarations).
|
|
1199
1854
|
*/
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
if (isEC) {
|
|
1206
|
-
usages = [];
|
|
1207
|
-
if (!importAlgorithm || importAlgorithm === "ECDH") {
|
|
1208
|
-
importAlgorithm = { name: "ECDH", namedCurve: jwk.crv || "P-256" };
|
|
1209
|
-
}
|
|
1210
|
-
} else {
|
|
1211
|
-
usages = ["encrypt", "wrapKey"];
|
|
1212
|
-
if (!importAlgorithm || importAlgorithm === "RSA-OAEP") {
|
|
1213
|
-
if (jwk.kty === "RSA") {
|
|
1214
|
-
const hash = jwk.alg === "RSA-OAEP-512" ? "SHA-512" : "SHA-256";
|
|
1215
|
-
importAlgorithm = { name: "RSA-OAEP", hash };
|
|
1216
|
-
} else {
|
|
1217
|
-
throw new Error(`[CKeyManager] Unsupported JWK kty: ${jwk.kty}`);
|
|
1218
|
-
}
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
return await subtle.importKey("jwk", jwk, importAlgorithm, true, usages);
|
|
1855
|
+
[Symbol.dispose]() {
|
|
1856
|
+
this.dispose();
|
|
1857
|
+
}
|
|
1858
|
+
async [Symbol.asyncDispose]() {
|
|
1859
|
+
this.dispose();
|
|
1222
1860
|
}
|
|
1223
1861
|
/**
|
|
1224
|
-
*
|
|
1862
|
+
* Resolves effective Authenticated Additional Data (AAD) for the operation:
|
|
1863
|
+
* 1. If options.bindRequestContextAad is explicitly false, returns options.aad directly without context binding.
|
|
1864
|
+
* 2. When encrypting (isDecrypt=false): binds request context `req:${method}:${url}` only if options.bindRequestContextAad === true.
|
|
1865
|
+
* 3. When decrypting (isDecrypt=true): derives request context `req:${method}:${url}` if url or method is provided.
|
|
1866
|
+
* 4. If explicit options.aad is provided: combines with request context if bound, or returns options.aad directly.
|
|
1867
|
+
* 5. Otherwise returns undefined.
|
|
1225
1868
|
*/
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
let
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
if (
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1869
|
+
_resolveEffectiveAad(options, isDecrypt = false) {
|
|
1870
|
+
if (!options) return void 0;
|
|
1871
|
+
let rawAad = options.aad;
|
|
1872
|
+
if (typeof rawAad === "function") {
|
|
1873
|
+
rawAad = rawAad(void 0, "");
|
|
1874
|
+
}
|
|
1875
|
+
if (rawAad === void 0 && options.baseAad) {
|
|
1876
|
+
rawAad = options.baseAad;
|
|
1877
|
+
}
|
|
1878
|
+
if (options.bindRequestContextAad === false) return rawAad;
|
|
1879
|
+
const hasReqContext = Boolean(options.url || options.method);
|
|
1880
|
+
if (rawAad) {
|
|
1881
|
+
if (options.bindRequestContextAad === true && hasReqContext) {
|
|
1882
|
+
const m = (options.method || "POST").toUpperCase();
|
|
1883
|
+
const u = options.url || "";
|
|
1884
|
+
const baseAad = typeof rawAad === "string" ? rawAad : rawAad instanceof Uint8Array || rawAad instanceof ArrayBuffer ? textDecoder.decode(rawAad) : String(rawAad);
|
|
1885
|
+
if (baseAad.includes(`|req:${m}:${u}`)) {
|
|
1886
|
+
return baseAad;
|
|
1244
1887
|
}
|
|
1888
|
+
return `${baseAad}|req:${m}:${u}`;
|
|
1245
1889
|
}
|
|
1890
|
+
return rawAad;
|
|
1246
1891
|
}
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
static async importPublicKeyFromPEM(pem, algorithm = "RSA-OAEP", curve = "P-256", hash = "SHA-256") {
|
|
1253
|
-
const subtle = getSubtleCrypto();
|
|
1254
|
-
const cleaned = pem.replace(/-----BEGIN[ A-Z0-9_-]+-----/g, "").replace(/-----END[ A-Z0-9_-]+-----/g, "").replace(/\s+/g, "");
|
|
1255
|
-
const spkiBytes = CKeyManager.fromBase64(cleaned);
|
|
1256
|
-
let importAlgorithm;
|
|
1257
|
-
let usages;
|
|
1258
|
-
if (algorithm === "ECDH") {
|
|
1259
|
-
importAlgorithm = { name: "ECDH", namedCurve: curve };
|
|
1260
|
-
usages = [];
|
|
1261
|
-
} else {
|
|
1262
|
-
importAlgorithm = { name: "RSA-OAEP", hash };
|
|
1263
|
-
usages = ["encrypt", "wrapKey"];
|
|
1264
|
-
}
|
|
1265
|
-
try {
|
|
1266
|
-
return await subtle.importKey("spki", spkiBytes, importAlgorithm, true, usages);
|
|
1267
|
-
} finally {
|
|
1268
|
-
spkiBytes.fill(0);
|
|
1892
|
+
const shouldBindReq = isDecrypt ? hasReqContext : options.bindRequestContextAad === true && hasReqContext;
|
|
1893
|
+
if (shouldBindReq) {
|
|
1894
|
+
const m = (options.method || "POST").toUpperCase();
|
|
1895
|
+
const u = options.url || "";
|
|
1896
|
+
return `req:${m}:${u}`;
|
|
1269
1897
|
}
|
|
1898
|
+
return void 0;
|
|
1270
1899
|
}
|
|
1271
1900
|
/**
|
|
1272
|
-
*
|
|
1901
|
+
* Helper to deterministically bind record ID, contextual sibling fields (aadFields),
|
|
1902
|
+
* dynamic AAD callbacks, and field name into the AAD to prevent ciphertext swapping
|
|
1903
|
+
* between fields, records, or parent entities.
|
|
1273
1904
|
*/
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
);
|
|
1279
|
-
}
|
|
1280
|
-
const subtle = getSubtleCrypto();
|
|
1281
|
-
const cleaned = pem.replace(/-----BEGIN[ A-Z0-9_-]+-----/g, "").replace(/-----END[ A-Z0-9_-]+-----/g, "").replace(/\s+/g, "");
|
|
1282
|
-
const pkcs8Bytes = CKeyManager.fromBase64(cleaned);
|
|
1283
|
-
let importAlgorithm;
|
|
1284
|
-
let usages;
|
|
1285
|
-
if (algorithm === "ECDH") {
|
|
1286
|
-
importAlgorithm = { name: "ECDH", namedCurve: curve };
|
|
1287
|
-
usages = ["deriveKey", "deriveBits"];
|
|
1288
|
-
} else {
|
|
1289
|
-
importAlgorithm = { name: "RSA-OAEP", hash };
|
|
1290
|
-
usages = ["decrypt", "unwrapKey"];
|
|
1905
|
+
_bindFieldAad(data, field, options) {
|
|
1906
|
+
let baseAad = "";
|
|
1907
|
+
if (options?.baseAad) {
|
|
1908
|
+
baseAad = options.baseAad;
|
|
1291
1909
|
}
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1910
|
+
if (options?.aad) {
|
|
1911
|
+
if (typeof options.aad === "function") {
|
|
1912
|
+
const dynamicRes = options.aad(data, field);
|
|
1913
|
+
if (typeof dynamicRes === "string") {
|
|
1914
|
+
baseAad = baseAad ? `${baseAad}|${dynamicRes}` : dynamicRes;
|
|
1915
|
+
} else if (dynamicRes instanceof Uint8Array || dynamicRes instanceof ArrayBuffer) {
|
|
1916
|
+
const decoded = textDecoder.decode(dynamicRes);
|
|
1917
|
+
baseAad = baseAad ? `${baseAad}|${decoded}` : decoded;
|
|
1918
|
+
} else if (dynamicRes !== void 0 && dynamicRes !== null) {
|
|
1919
|
+
const str = String(dynamicRes);
|
|
1920
|
+
baseAad = baseAad ? `${baseAad}|${str}` : str;
|
|
1921
|
+
}
|
|
1922
|
+
} else if (typeof options.aad === "string") {
|
|
1923
|
+
baseAad = baseAad ? `${baseAad}|${options.aad}` : options.aad;
|
|
1924
|
+
} else if (options.aad instanceof Uint8Array || options.aad instanceof ArrayBuffer) {
|
|
1925
|
+
const decoded = textDecoder.decode(options.aad);
|
|
1926
|
+
baseAad = baseAad ? `${baseAad}|${decoded}` : decoded;
|
|
1927
|
+
} else {
|
|
1928
|
+
const str = String(options.aad);
|
|
1929
|
+
baseAad = baseAad ? `${baseAad}|${str}` : str;
|
|
1930
|
+
}
|
|
1296
1931
|
}
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1932
|
+
const recordId = data && typeof data === "object" && (data.id !== void 0 || data._id !== void 0) ? String(data.id ?? data._id) : "";
|
|
1933
|
+
const recordPart = recordId ? `|record:${recordId}` : "";
|
|
1934
|
+
let contextPart = "";
|
|
1935
|
+
if (options?.aadFields && options.aadFields.length > 0 && data && typeof data === "object") {
|
|
1936
|
+
const sortedFields = [...options.aadFields].sort();
|
|
1937
|
+
const parts = [];
|
|
1938
|
+
for (const f of sortedFields) {
|
|
1939
|
+
const val = data[f];
|
|
1940
|
+
let serializedVal = "";
|
|
1941
|
+
if (val === null) {
|
|
1942
|
+
serializedVal = "null";
|
|
1943
|
+
} else if (val !== void 0) {
|
|
1944
|
+
serializedVal = typeof val === "object" ? JSON.stringify(val) : String(val);
|
|
1945
|
+
}
|
|
1946
|
+
parts.push(`${f}=${serializedVal}`);
|
|
1947
|
+
}
|
|
1948
|
+
contextPart = `|ctx:${parts.join(",")}`;
|
|
1309
1949
|
}
|
|
1310
|
-
const
|
|
1311
|
-
return
|
|
1312
|
-
|
|
1313
|
-
|
|
1950
|
+
const reqPart = options?.bindRequestContextAad === true && (options?.url || options?.method) ? `|req:${(options.method || "POST").toUpperCase()}:${options.url || ""}` : "";
|
|
1951
|
+
return {
|
|
1952
|
+
...options,
|
|
1953
|
+
bindRequestContextAad: false,
|
|
1954
|
+
aad: `aad:${baseAad}${recordPart}${contextPart}${reqPart}|field:${field}`
|
|
1955
|
+
};
|
|
1314
1956
|
}
|
|
1315
1957
|
/**
|
|
1316
|
-
*
|
|
1958
|
+
* Validates envelope metadata against algorithm whitelists and key identifiers (kid).
|
|
1959
|
+
* In strict mode, allowedAlgorithms defaults to ['AES-GCM'].
|
|
1317
1960
|
*/
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
const
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1961
|
+
_validateEnvelopeMetadata(payload, options) {
|
|
1962
|
+
if (!payload || typeof payload !== "object") return void 0;
|
|
1963
|
+
const allowedAlgorithms = options?.allowedAlgorithms ?? (options?.strict ? ["AES-GCM"] : void 0);
|
|
1964
|
+
if (allowedAlgorithms && allowedAlgorithms.length > 0) {
|
|
1965
|
+
const alg = payload.alg;
|
|
1966
|
+
if (!alg || !allowedAlgorithms.includes(alg)) {
|
|
1967
|
+
return new Error(`[AEncryptionProvider] Algorithm mismatch: algorithm '${alg || "unknown"}' is not in allowedAlgorithms.`);
|
|
1968
|
+
}
|
|
1326
1969
|
}
|
|
1327
|
-
const
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1970
|
+
const effectiveKeys = options?.keys || this.keys;
|
|
1971
|
+
const effectiveAllowedKids = options?.allowedKids || this.allowedKids;
|
|
1972
|
+
const allowedKids = effectiveAllowedKids ?? (effectiveKeys ? Object.keys(effectiveKeys) : void 0);
|
|
1973
|
+
if (allowedKids && allowedKids.length > 0) {
|
|
1974
|
+
if (payload.kid && !allowedKids.includes(payload.kid)) {
|
|
1975
|
+
return new Error(`[AEncryptionProvider] Unauthorized key: kid '${payload.kid}' is not in allowedKids.`);
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
return void 0;
|
|
1331
1979
|
}
|
|
1332
|
-
// ── Key Wrapping & Unwrapping ────────────────────────────────────────────
|
|
1333
1980
|
/**
|
|
1334
|
-
*
|
|
1335
|
-
*
|
|
1981
|
+
* Normalizes encryption options with active keyring parameters:
|
|
1982
|
+
* If `keys` and `activeKid` are configured, selects `key = keys[activeKid]` and sets `kid = activeKid`.
|
|
1336
1983
|
*/
|
|
1337
|
-
|
|
1338
|
-
const
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1984
|
+
_resolveOutboundKeyringOptions(options) {
|
|
1985
|
+
const effectiveKeys = options?.keys || this.keys;
|
|
1986
|
+
const effectiveActiveKid = options?.activeKid || (options?.kid ? options.kid : this.activeKid);
|
|
1987
|
+
let effectiveKey = options?.key;
|
|
1988
|
+
let effectiveKid = options?.kid || effectiveActiveKid;
|
|
1989
|
+
if (effectiveKeys && effectiveActiveKid) {
|
|
1990
|
+
if (effectiveKeys[effectiveActiveKid]) {
|
|
1991
|
+
effectiveKey = options?.key || effectiveKeys[effectiveActiveKid];
|
|
1992
|
+
effectiveKid = effectiveActiveKid;
|
|
1993
|
+
} else if (!options?.key) {
|
|
1994
|
+
throw new Error(`[${this.constructor.name}] Active kid '${effectiveActiveKid}' not found in keyring keys.`);
|
|
1995
|
+
}
|
|
1996
|
+
} else if (effectiveKeys && !effectiveKey) {
|
|
1997
|
+
const kids = Object.keys(effectiveKeys);
|
|
1998
|
+
if (kids.length > 0) {
|
|
1999
|
+
effectiveKid = kids[0];
|
|
2000
|
+
effectiveKey = effectiveKeys[kids[0]];
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
if (effectiveKeys || effectiveActiveKid || effectiveKey !== options?.key || effectiveKid !== options?.kid) {
|
|
2004
|
+
return {
|
|
2005
|
+
...options,
|
|
2006
|
+
...effectiveKeys ? { keys: effectiveKeys } : {},
|
|
2007
|
+
...effectiveActiveKid ? { activeKid: effectiveActiveKid } : {},
|
|
2008
|
+
...effectiveKey !== void 0 ? { key: effectiveKey } : {},
|
|
2009
|
+
...effectiveKid !== void 0 ? { kid: effectiveKid } : {}
|
|
2010
|
+
};
|
|
1346
2011
|
}
|
|
1347
|
-
|
|
1348
|
-
return CKeyManager.toBase64(wrappedBuffer);
|
|
2012
|
+
return options || {};
|
|
1349
2013
|
}
|
|
1350
2014
|
/**
|
|
1351
|
-
*
|
|
2015
|
+
* Resolves the appropriate decryption key for a specific payload envelope based on its `kid`
|
|
2016
|
+
* and the configured keyring (`keys`, `activeKid`, or fallback `key`).
|
|
1352
2017
|
*/
|
|
1353
|
-
|
|
1354
|
-
const
|
|
1355
|
-
const
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
unwrapAlgorithm = "AES-KW";
|
|
2018
|
+
_resolveDecryptionKeyForEnvelope(payload, options) {
|
|
2019
|
+
const effectiveKeys = options?.keys || this.keys;
|
|
2020
|
+
const effectiveActiveKid = options?.activeKid || this.activeKid;
|
|
2021
|
+
if (payload && typeof payload === "object" && payload.kid && effectiveKeys) {
|
|
2022
|
+
if (effectiveKeys[payload.kid]) {
|
|
2023
|
+
return effectiveKeys[payload.kid];
|
|
2024
|
+
}
|
|
1361
2025
|
}
|
|
1362
|
-
|
|
1363
|
-
return
|
|
1364
|
-
format,
|
|
1365
|
-
wrappedBytes,
|
|
1366
|
-
unwrappingKey,
|
|
1367
|
-
unwrapAlgorithm,
|
|
1368
|
-
unwrappedKeyAlgorithm,
|
|
1369
|
-
extractable,
|
|
1370
|
-
keyUsages
|
|
1371
|
-
);
|
|
1372
|
-
} finally {
|
|
1373
|
-
wrappedBytes.fill(0);
|
|
2026
|
+
if (effectiveKeys && effectiveActiveKid && effectiveKeys[effectiveActiveKid]) {
|
|
2027
|
+
return effectiveKeys[effectiveActiveKid];
|
|
1374
2028
|
}
|
|
2029
|
+
return options?.key || this.defaultKey;
|
|
1375
2030
|
}
|
|
1376
2031
|
/**
|
|
1377
|
-
*
|
|
1378
|
-
*
|
|
2032
|
+
* Handles a per-field or per-payload decryption error according to configured options:
|
|
2033
|
+
* - Invokes onDecryptError callback if provided
|
|
2034
|
+
* - Attaches error metadata (__decryptErrors) to the record object
|
|
2035
|
+
* - Returns fallback cell placeholder ('[⚠ Decryption Error]') or rethrows
|
|
1379
2036
|
*/
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
2037
|
+
_handleDecryptError(err, record, field, value, index, options) {
|
|
2038
|
+
if (record && typeof record === "object" && record !== value) {
|
|
2039
|
+
record.__decryptErrors = {
|
|
2040
|
+
...record.__decryptErrors || {},
|
|
2041
|
+
[field || "__payload"]: err
|
|
2042
|
+
};
|
|
2043
|
+
}
|
|
2044
|
+
if (options?.onDecryptError === false || options?.onDecryptError === void 0) {
|
|
2045
|
+
throw err;
|
|
2046
|
+
}
|
|
2047
|
+
if (typeof options.onDecryptError === "function") {
|
|
2048
|
+
return options.onDecryptError({
|
|
2049
|
+
record,
|
|
2050
|
+
field,
|
|
2051
|
+
value,
|
|
2052
|
+
error: err,
|
|
2053
|
+
index
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
2056
|
+
if (typeof options.onDecryptError === "string") {
|
|
2057
|
+
return options.onDecryptError;
|
|
2058
|
+
}
|
|
2059
|
+
if (options.onDecryptError === true) {
|
|
2060
|
+
return "[\u26A0 Decryption Error]";
|
|
2061
|
+
}
|
|
2062
|
+
throw err;
|
|
1387
2063
|
}
|
|
1388
2064
|
/**
|
|
1389
|
-
*
|
|
1390
|
-
*
|
|
2065
|
+
* Encrypts a single scalar value with contextual AAD binding into an IEncryptedPayload envelope.
|
|
2066
|
+
* Binds recordId, fieldName, and sibling aadFields deterministically to protect against ciphertext swapping.
|
|
2067
|
+
*
|
|
2068
|
+
* @param value - Scalar value (string, number, boolean, object) to encrypt.
|
|
2069
|
+
* @param context - Field encryption context with recordId, fieldName, and optional sibling aadFields.
|
|
1391
2070
|
*/
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
if (
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
2071
|
+
async encryptField(value, context) {
|
|
2072
|
+
if (value === void 0 || value === null) return value;
|
|
2073
|
+
if (this.isEncrypted(value)) return value;
|
|
2074
|
+
const field = context?.fieldName || "field";
|
|
2075
|
+
const dummyData = {};
|
|
2076
|
+
if (context?.recordId !== void 0) {
|
|
2077
|
+
dummyData.id = context.recordId;
|
|
2078
|
+
}
|
|
2079
|
+
const { aadFields, ...restContext } = context || {};
|
|
2080
|
+
let boundOptions = { ...restContext };
|
|
2081
|
+
if (aadFields) {
|
|
2082
|
+
if (!Array.isArray(aadFields) && typeof aadFields === "object") {
|
|
2083
|
+
Object.assign(dummyData, aadFields);
|
|
2084
|
+
boundOptions.aadFields = Object.keys(aadFields);
|
|
2085
|
+
} else if (Array.isArray(aadFields)) {
|
|
2086
|
+
boundOptions.aadFields = aadFields;
|
|
2087
|
+
if (context?.record) {
|
|
2088
|
+
Object.assign(dummyData, context.record);
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
const resolvedOptions = this._bindFieldAad(dummyData, field, boundOptions);
|
|
2093
|
+
return this.encryptValue(value, resolvedOptions);
|
|
1410
2094
|
}
|
|
1411
2095
|
/**
|
|
1412
|
-
*
|
|
1413
|
-
*
|
|
2096
|
+
* Decrypts a single IEncryptedPayload envelope back into its scalar plaintext value using contextual AAD.
|
|
2097
|
+
* Validates recordId, fieldName, and sibling aadFields integrity.
|
|
2098
|
+
*
|
|
2099
|
+
* @param envelope - The IEncryptedPayload envelope to decrypt.
|
|
2100
|
+
* @param context - Field encryption context matching the encryption parameters.
|
|
1414
2101
|
*/
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
unwrappedKeyAlgorithm,
|
|
1434
|
-
extractable,
|
|
1435
|
-
["encrypt", "decrypt"]
|
|
1436
|
-
);
|
|
1437
|
-
} finally {
|
|
1438
|
-
new Uint8Array(unwrappedRawKey).fill(0);
|
|
2102
|
+
async decryptField(envelope, context) {
|
|
2103
|
+
if (!this.isEncrypted(envelope)) return envelope;
|
|
2104
|
+
const field = context?.fieldName || "field";
|
|
2105
|
+
const dummyData = {};
|
|
2106
|
+
if (context?.recordId !== void 0) {
|
|
2107
|
+
dummyData.id = context.recordId;
|
|
2108
|
+
}
|
|
2109
|
+
const { aadFields, ...restContext } = context || {};
|
|
2110
|
+
let boundOptions = { ...restContext };
|
|
2111
|
+
if (aadFields) {
|
|
2112
|
+
if (!Array.isArray(aadFields) && typeof aadFields === "object") {
|
|
2113
|
+
Object.assign(dummyData, aadFields);
|
|
2114
|
+
boundOptions.aadFields = Object.keys(aadFields);
|
|
2115
|
+
} else if (Array.isArray(aadFields)) {
|
|
2116
|
+
boundOptions.aadFields = aadFields;
|
|
2117
|
+
if (context?.record) {
|
|
2118
|
+
Object.assign(dummyData, context.record);
|
|
2119
|
+
}
|
|
1439
2120
|
}
|
|
1440
2121
|
}
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
);
|
|
2122
|
+
const resolvedOptions = this._bindFieldAad(dummyData, field, boundOptions);
|
|
2123
|
+
return this.decryptValue(envelope, resolvedOptions);
|
|
1444
2124
|
}
|
|
1445
|
-
// ── Key Derivation Functions (PBKDF2 & HKDF) ──────────────────────────────
|
|
1446
2125
|
/**
|
|
1447
|
-
*
|
|
1448
|
-
*
|
|
2126
|
+
* Top-level polymorphic encryption method.
|
|
2127
|
+
* Automatically performs value-level encryption:
|
|
2128
|
+
* - If data is an array of objects: recursively encrypts each row.
|
|
2129
|
+
* - If data is an object: encrypts specified fields (or all values if valueEncryption is true),
|
|
2130
|
+
* preserving JSON object keys so backend APIs and ORMs can parse keys and perform upserts.
|
|
2131
|
+
* - If data is a primitive or whole-payload envelope: encrypts as a single value.
|
|
1449
2132
|
*/
|
|
1450
|
-
|
|
1451
|
-
if (
|
|
1452
|
-
|
|
2133
|
+
async encrypt(data, options) {
|
|
2134
|
+
if (data === void 0 || data === null) return data;
|
|
2135
|
+
if (this.isEncrypted(data)) return data;
|
|
2136
|
+
options = this._resolveOutboundKeyringOptions(options);
|
|
2137
|
+
if (options?.valueEncryption === false && (!options.fields || options.fields.length === 0)) {
|
|
2138
|
+
return this.encryptValue(data, options);
|
|
1453
2139
|
}
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
2140
|
+
if (Array.isArray(data)) {
|
|
2141
|
+
if (data.length === 0) return data;
|
|
2142
|
+
if (options?.fields && options.fields.length > 0) {
|
|
2143
|
+
const fields = options.fields;
|
|
2144
|
+
let hasUnencrypted = false;
|
|
2145
|
+
for (let i = 0; i < data.length; i++) {
|
|
2146
|
+
const item = data[i];
|
|
2147
|
+
if (item && typeof item === "object") {
|
|
2148
|
+
for (let f = 0; f < fields.length; f++) {
|
|
2149
|
+
const val = item[fields[f]];
|
|
2150
|
+
if (val !== void 0 && val !== null && !this.isEncrypted(val)) {
|
|
2151
|
+
hasUnencrypted = true;
|
|
2152
|
+
break;
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
if (hasUnencrypted) break;
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
if (!hasUnencrypted) {
|
|
2159
|
+
return data;
|
|
2160
|
+
}
|
|
2161
|
+
await this.warmupKey(options);
|
|
2162
|
+
const itemOptions = { ...options, _warmed: true };
|
|
2163
|
+
return Promise.all(data.map((item) => this.encrypt(item, itemOptions)));
|
|
2164
|
+
}
|
|
2165
|
+
if (options?.valueEncryption === true) {
|
|
2166
|
+
await this.warmupKey(options);
|
|
2167
|
+
const itemOptions = { ...options, _warmed: true };
|
|
2168
|
+
return Promise.all(data.map((item) => this.encrypt(item, itemOptions)));
|
|
2169
|
+
}
|
|
2170
|
+
return this.encryptValue(data, options);
|
|
2171
|
+
}
|
|
2172
|
+
if (typeof data === "object" && !ArrayBuffer.isView(data) && !(data instanceof ArrayBuffer)) {
|
|
2173
|
+
if (options?.fields && options.fields.length > 0) {
|
|
2174
|
+
const fieldsToEncrypt = options.fields.filter(
|
|
2175
|
+
(field) => Object.prototype.hasOwnProperty.call(data, field) && data[field] !== void 0 && data[field] !== null && !this.isEncrypted(data[field])
|
|
1468
2176
|
);
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
2177
|
+
if (fieldsToEncrypt.length === 0) {
|
|
2178
|
+
return data;
|
|
2179
|
+
}
|
|
2180
|
+
if (!options?._warmed) {
|
|
2181
|
+
await this.warmupKey(options);
|
|
2182
|
+
}
|
|
2183
|
+
const result = { ...data };
|
|
2184
|
+
const entries = await Promise.all(
|
|
2185
|
+
fieldsToEncrypt.map(async (field) => {
|
|
2186
|
+
const fieldOptions = this._bindFieldAad(data, field, options);
|
|
2187
|
+
return [field, await this.encryptValue(result[field], fieldOptions)];
|
|
2188
|
+
})
|
|
1480
2189
|
);
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
2190
|
+
for (const [field, encryptedVal] of entries) {
|
|
2191
|
+
safeSetProperty(result, field, encryptedVal);
|
|
2192
|
+
}
|
|
2193
|
+
return result;
|
|
1485
2194
|
}
|
|
1486
|
-
if (options
|
|
1487
|
-
const
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
"HKDF",
|
|
1491
|
-
false,
|
|
1492
|
-
["deriveKey"]
|
|
2195
|
+
if (options?.valueEncryption === true) {
|
|
2196
|
+
const result = { ...data };
|
|
2197
|
+
const keysToEncrypt = Object.keys(result).filter(
|
|
2198
|
+
(key) => result[key] !== void 0 && result[key] !== null && !this.isEncrypted(result[key])
|
|
1493
2199
|
);
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
["encrypt", "decrypt"]
|
|
2200
|
+
if (keysToEncrypt.length === 0) {
|
|
2201
|
+
return result;
|
|
2202
|
+
}
|
|
2203
|
+
if (!options?._warmed) {
|
|
2204
|
+
await this.warmupKey(options);
|
|
2205
|
+
}
|
|
2206
|
+
const entries = await Promise.all(
|
|
2207
|
+
keysToEncrypt.map(async (key) => {
|
|
2208
|
+
const fieldOptions = this._bindFieldAad(data, key, options);
|
|
2209
|
+
return [key, await this.encryptValue(result[key], fieldOptions)];
|
|
2210
|
+
})
|
|
1506
2211
|
);
|
|
1507
|
-
|
|
1508
|
-
key
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
}
|
|
1512
|
-
throw new Error(`[CKeyManager] Unsupported KDF strategy: ${options.kdf}`);
|
|
1513
|
-
} finally {
|
|
1514
|
-
if (typeof passphrase === "string") {
|
|
1515
|
-
rawKeyBytes.fill(0);
|
|
2212
|
+
for (const [key, encryptedVal] of entries) {
|
|
2213
|
+
safeSetProperty(result, key, encryptedVal);
|
|
2214
|
+
}
|
|
2215
|
+
return result;
|
|
1516
2216
|
}
|
|
1517
2217
|
}
|
|
2218
|
+
return this.encryptValue(data, options);
|
|
1518
2219
|
}
|
|
1519
2220
|
/**
|
|
1520
|
-
*
|
|
1521
|
-
*
|
|
2221
|
+
* Top-level polymorphic decryption method.
|
|
2222
|
+
* Automatically performs value-level decryption:
|
|
2223
|
+
* - If payload is an encrypted envelope: validates metadata and decrypts directly via decryptValue.
|
|
2224
|
+
* - If payload is an array of objects: decrypts each row (batched with local memoization).
|
|
2225
|
+
* - If payload is an object: decrypts any encrypted value cells across fields.
|
|
2226
|
+
* In strict mode:
|
|
2227
|
+
* - Unencrypted plaintext in configured `fields` triggers a decryption error.
|
|
2228
|
+
* - Envelopes are validated against `allowedAlgorithms` and `allowedKids`.
|
|
2229
|
+
* - Errors are routed through `onDecryptError` or populated into `__decryptErrors`.
|
|
1522
2230
|
*/
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
2231
|
+
async decrypt(payload, options) {
|
|
2232
|
+
if (payload === void 0 || payload === null) return payload;
|
|
2233
|
+
if (this.isEncrypted(payload)) {
|
|
2234
|
+
const envelopeKey = this._resolveDecryptionKeyForEnvelope(payload, options);
|
|
2235
|
+
const envelopeOptions = envelopeKey !== options?.key ? { ...options, key: envelopeKey } : options;
|
|
2236
|
+
const metaErr = this._validateEnvelopeMetadata(payload, envelopeOptions);
|
|
2237
|
+
if (metaErr) {
|
|
2238
|
+
return this._handleDecryptError(metaErr, payload, "", payload, void 0, envelopeOptions);
|
|
2239
|
+
}
|
|
2240
|
+
try {
|
|
2241
|
+
return await this.decryptValue(payload, envelopeOptions);
|
|
2242
|
+
} catch (err) {
|
|
2243
|
+
return this._handleDecryptError(err, payload, "", payload, void 0, envelopeOptions);
|
|
2244
|
+
}
|
|
1530
2245
|
}
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
2246
|
+
if (Array.isArray(payload)) {
|
|
2247
|
+
if (payload.length === 0) return payload;
|
|
2248
|
+
const localMemo = /* @__PURE__ */ new Map();
|
|
2249
|
+
const decryptRow = async (item, rowIndex) => {
|
|
2250
|
+
if (!item) return item;
|
|
2251
|
+
if (this.isEncrypted(item)) {
|
|
2252
|
+
const envelopeKey = this._resolveDecryptionKeyForEnvelope(item, options);
|
|
2253
|
+
const envelopeOptions = envelopeKey !== options?.key ? { ...options, key: envelopeKey } : options;
|
|
2254
|
+
const metaErr = this._validateEnvelopeMetadata(item, envelopeOptions);
|
|
2255
|
+
if (metaErr) {
|
|
2256
|
+
return this._handleDecryptError(metaErr, item, "", item, rowIndex, envelopeOptions);
|
|
2257
|
+
}
|
|
2258
|
+
const ct = item.ciphertext;
|
|
2259
|
+
const iv = item.iv || "";
|
|
2260
|
+
const aadPart = options?.aad ? typeof options.aad === "string" ? options.aad : "" : "";
|
|
2261
|
+
const memoKey = `${ct}:${iv}:${aadPart}:${item.kid || ""}`;
|
|
2262
|
+
try {
|
|
2263
|
+
if (!localMemo.has(memoKey)) {
|
|
2264
|
+
localMemo.set(memoKey, this.decryptValue(item, envelopeOptions));
|
|
2265
|
+
}
|
|
2266
|
+
return await localMemo.get(memoKey);
|
|
2267
|
+
} catch (err) {
|
|
2268
|
+
return this._handleDecryptError(err, item, "", item, rowIndex, envelopeOptions);
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
if (typeof item === "object" && !ArrayBuffer.isView(item) && !(item instanceof ArrayBuffer)) {
|
|
2272
|
+
const targetKeys = options?.fields && options.fields.length > 0 ? options.fields.filter((f) => Object.prototype.hasOwnProperty.call(item, f)) : Object.keys(item);
|
|
2273
|
+
const keysToDecrypt = targetKeys.filter((key) => this.isEncrypted(item[key]));
|
|
2274
|
+
const strictViolationKeys = options?.strict === true && options?.fields && options.fields.length > 0 ? targetKeys.filter((key) => item[key] !== void 0 && item[key] !== null && !this.isEncrypted(item[key])) : [];
|
|
2275
|
+
if (keysToDecrypt.length === 0 && strictViolationKeys.length === 0) {
|
|
2276
|
+
return item;
|
|
2277
|
+
}
|
|
2278
|
+
const proto = Object.getPrototypeOf(item);
|
|
2279
|
+
const result = proto && proto !== Object.prototype ? Object.assign(Object.create(proto), item) : { ...item };
|
|
2280
|
+
for (const key of strictViolationKeys) {
|
|
2281
|
+
const err = new Error(`[AEncryptionProvider] Strict decryption violation: field '${key}' contains unencrypted plaintext.`);
|
|
2282
|
+
const fallbackVal = this._handleDecryptError(err, result, key, item[key], rowIndex, options);
|
|
2283
|
+
safeSetProperty(result, key, fallbackVal);
|
|
2284
|
+
}
|
|
2285
|
+
await Promise.all(
|
|
2286
|
+
keysToDecrypt.map(async (key) => {
|
|
2287
|
+
const env = item[key];
|
|
2288
|
+
const envelopeKey = this._resolveDecryptionKeyForEnvelope(env, options);
|
|
2289
|
+
const baseOptions = envelopeKey !== options?.key ? { ...options, key: envelopeKey } : options;
|
|
2290
|
+
const boundOptions = this._bindFieldAad(item, key, baseOptions);
|
|
2291
|
+
const hasBoundAad = boundOptions.aad !== baseOptions?.aad;
|
|
2292
|
+
const metaErr = this._validateEnvelopeMetadata(env, boundOptions);
|
|
2293
|
+
if (metaErr) {
|
|
2294
|
+
const fallbackVal = this._handleDecryptError(metaErr, result, key, env, rowIndex, boundOptions);
|
|
2295
|
+
safeSetProperty(result, key, fallbackVal);
|
|
2296
|
+
return;
|
|
2297
|
+
}
|
|
2298
|
+
const ct = env.ciphertext;
|
|
2299
|
+
const iv = env.iv || "";
|
|
2300
|
+
const aadPart = boundOptions.aad ? typeof boundOptions.aad === "string" ? boundOptions.aad : "" : "";
|
|
2301
|
+
const memoKey = `${ct}:${iv}:${aadPart}:${env.kid || ""}`;
|
|
2302
|
+
try {
|
|
2303
|
+
if (!localMemo.has(memoKey)) {
|
|
2304
|
+
const decryptPromise = (async () => {
|
|
2305
|
+
try {
|
|
2306
|
+
return await this.decryptValue(env, boundOptions);
|
|
2307
|
+
} catch (err) {
|
|
2308
|
+
const canFallback = hasBoundAad && (!options?.aadFields || options.aadFields.length === 0) && typeof options?.aad !== "function";
|
|
2309
|
+
if (canFallback && isAuthOrDecryptionError(err)) {
|
|
2310
|
+
return await this.decryptValue(env, baseOptions);
|
|
2311
|
+
}
|
|
2312
|
+
throw err;
|
|
2313
|
+
}
|
|
2314
|
+
})();
|
|
2315
|
+
localMemo.set(memoKey, decryptPromise);
|
|
2316
|
+
}
|
|
2317
|
+
const decryptedVal = await localMemo.get(memoKey);
|
|
2318
|
+
safeSetProperty(result, key, decryptedVal);
|
|
2319
|
+
} catch (err) {
|
|
2320
|
+
const fallbackVal = this._handleDecryptError(err, result, key, env, rowIndex, boundOptions);
|
|
2321
|
+
safeSetProperty(result, key, fallbackVal);
|
|
2322
|
+
}
|
|
2323
|
+
})
|
|
2324
|
+
);
|
|
2325
|
+
return result;
|
|
2326
|
+
}
|
|
2327
|
+
return item;
|
|
2328
|
+
};
|
|
2329
|
+
if (payload.length <= 100) {
|
|
2330
|
+
return Promise.all(payload.map((item, index) => decryptRow(item, index)));
|
|
2331
|
+
}
|
|
2332
|
+
const batchSize = 1e3;
|
|
2333
|
+
const results = new Array(payload.length);
|
|
2334
|
+
for (let i = 0; i < payload.length; i += batchSize) {
|
|
2335
|
+
const end = Math.min(i + batchSize, payload.length);
|
|
2336
|
+
const chunk = payload.slice(i, end);
|
|
2337
|
+
const chunkRes = await Promise.all(chunk.map((item, localIdx) => decryptRow(item, i + localIdx)));
|
|
2338
|
+
for (let j = 0; j < chunkRes.length; j++) {
|
|
2339
|
+
results[i + j] = chunkRes[j];
|
|
2340
|
+
}
|
|
2341
|
+
if (end < payload.length && (i > 0 && i % 5e3 === 0)) {
|
|
2342
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
return results;
|
|
2346
|
+
}
|
|
2347
|
+
if (typeof payload === "object" && !ArrayBuffer.isView(payload) && !(payload instanceof ArrayBuffer)) {
|
|
2348
|
+
const targetKeys = options?.fields && options.fields.length > 0 ? options.fields.filter((f) => Object.prototype.hasOwnProperty.call(payload, f)) : Object.keys(payload);
|
|
2349
|
+
const keysToDecrypt = targetKeys.filter((key) => this.isEncrypted(payload[key]));
|
|
2350
|
+
const strictViolationKeys = options?.strict === true && options?.fields && options.fields.length > 0 ? targetKeys.filter((key) => payload[key] !== void 0 && payload[key] !== null && !this.isEncrypted(payload[key])) : [];
|
|
2351
|
+
if (keysToDecrypt.length === 0 && strictViolationKeys.length === 0) {
|
|
2352
|
+
return payload;
|
|
2353
|
+
}
|
|
2354
|
+
const proto = Object.getPrototypeOf(payload);
|
|
2355
|
+
const result = proto && proto !== Object.prototype ? Object.assign(Object.create(proto), payload) : { ...payload };
|
|
2356
|
+
for (const key of strictViolationKeys) {
|
|
2357
|
+
const err = new Error(`[AEncryptionProvider] Strict decryption violation: field '${key}' contains unencrypted plaintext.`);
|
|
2358
|
+
const fallbackVal = this._handleDecryptError(err, result, key, payload[key], void 0, options);
|
|
2359
|
+
safeSetProperty(result, key, fallbackVal);
|
|
2360
|
+
}
|
|
2361
|
+
await Promise.all(
|
|
2362
|
+
keysToDecrypt.map(async (key) => {
|
|
2363
|
+
const env = payload[key];
|
|
2364
|
+
const envelopeKey = this._resolveDecryptionKeyForEnvelope(env, options);
|
|
2365
|
+
const baseOptions = envelopeKey !== options?.key ? { ...options, key: envelopeKey } : options;
|
|
2366
|
+
const boundOptions = this._bindFieldAad(payload, key, baseOptions);
|
|
2367
|
+
const hasBoundAad = boundOptions.aad !== baseOptions?.aad;
|
|
2368
|
+
const metaErr = this._validateEnvelopeMetadata(env, boundOptions);
|
|
2369
|
+
if (metaErr) {
|
|
2370
|
+
const fallbackVal = this._handleDecryptError(metaErr, result, key, env, void 0, boundOptions);
|
|
2371
|
+
safeSetProperty(result, key, fallbackVal);
|
|
2372
|
+
return;
|
|
2373
|
+
}
|
|
2374
|
+
try {
|
|
2375
|
+
let decryptedVal;
|
|
2376
|
+
try {
|
|
2377
|
+
decryptedVal = await this.decryptValue(env, boundOptions);
|
|
2378
|
+
} catch (err) {
|
|
2379
|
+
const canFallback = hasBoundAad && (!options?.aadFields || options.aadFields.length === 0) && typeof options?.aad !== "function";
|
|
2380
|
+
if (canFallback && isAuthOrDecryptionError(err)) {
|
|
2381
|
+
decryptedVal = await this.decryptValue(env, baseOptions);
|
|
2382
|
+
} else {
|
|
2383
|
+
throw err;
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
safeSetProperty(result, key, decryptedVal);
|
|
2387
|
+
} catch (err) {
|
|
2388
|
+
const fallbackVal = this._handleDecryptError(err, result, key, env, void 0, boundOptions);
|
|
2389
|
+
safeSetProperty(result, key, fallbackVal);
|
|
2390
|
+
}
|
|
2391
|
+
})
|
|
2392
|
+
);
|
|
2393
|
+
return result;
|
|
2394
|
+
}
|
|
2395
|
+
return payload;
|
|
1547
2396
|
}
|
|
1548
2397
|
}
|
|
1549
2398
|
|
|
@@ -1558,6 +2407,7 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
1558
2407
|
_cachedKeyId;
|
|
1559
2408
|
_configuredKid;
|
|
1560
2409
|
_decryptSaltCache = /* @__PURE__ */ new Map();
|
|
2410
|
+
_keyringCache = /* @__PURE__ */ new Map();
|
|
1561
2411
|
setDefaultKey(key) {
|
|
1562
2412
|
super.setDefaultKey(key);
|
|
1563
2413
|
this._cachedKey = void 0;
|
|
@@ -1567,6 +2417,7 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
1567
2417
|
this._cachedHash = void 0;
|
|
1568
2418
|
this._cachedKeyId = void 0;
|
|
1569
2419
|
this._decryptSaltCache.clear();
|
|
2420
|
+
this._keyringCache.clear();
|
|
1570
2421
|
}
|
|
1571
2422
|
clearKeys() {
|
|
1572
2423
|
super.clearKeys();
|
|
@@ -1577,9 +2428,10 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
1577
2428
|
this._cachedHash = void 0;
|
|
1578
2429
|
this._cachedKeyId = void 0;
|
|
1579
2430
|
this._decryptSaltCache.clear();
|
|
2431
|
+
this._keyringCache.clear();
|
|
1580
2432
|
}
|
|
1581
2433
|
constructor(key, options) {
|
|
1582
|
-
super(key);
|
|
2434
|
+
super(key, options);
|
|
1583
2435
|
if (options) {
|
|
1584
2436
|
if (options.kid || options.keyId) {
|
|
1585
2437
|
this._configuredKid = options.kid || options.keyId;
|
|
@@ -1629,14 +2481,21 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
1629
2481
|
}
|
|
1630
2482
|
async _resolveCryptoKeyInternal(keyProvider, kdfOverride, saltOverride, options, iterationsOverride) {
|
|
1631
2483
|
const rawKey = await this.resolveRawKey(keyProvider);
|
|
2484
|
+
const currentKeyId = getSafeKeyIdentifier(rawKey);
|
|
1632
2485
|
if (typeof rawKey !== "string" && rawKey.algorithm) {
|
|
1633
|
-
|
|
2486
|
+
const res2 = { cryptoKey: rawKey, kdf: "direct" };
|
|
2487
|
+
this._keyringCache.set(currentKeyId, res2);
|
|
2488
|
+
if (!keyProvider || keyProvider === this.defaultKey) {
|
|
1634
2489
|
this._cachedKey = rawKey;
|
|
1635
2490
|
this._cachedKdf = "direct";
|
|
1636
2491
|
}
|
|
1637
|
-
return
|
|
2492
|
+
return res2;
|
|
1638
2493
|
}
|
|
1639
2494
|
if (rawKey instanceof Uint8Array || rawKey instanceof ArrayBuffer) {
|
|
2495
|
+
const cached = this._keyringCache.get(currentKeyId);
|
|
2496
|
+
if (cached) {
|
|
2497
|
+
return cached;
|
|
2498
|
+
}
|
|
1640
2499
|
if (!keyProvider && this._cachedKey && this._cachedKdf === "raw") {
|
|
1641
2500
|
return { cryptoKey: this._cachedKey, kdf: "raw" };
|
|
1642
2501
|
}
|
|
@@ -1653,11 +2512,13 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
1653
2512
|
false,
|
|
1654
2513
|
["encrypt", "decrypt"]
|
|
1655
2514
|
);
|
|
1656
|
-
|
|
2515
|
+
const res2 = { cryptoKey: cryptoKey2, kdf: "raw" };
|
|
2516
|
+
this._keyringCache.set(currentKeyId, res2);
|
|
2517
|
+
if (!keyProvider || keyProvider === this.defaultKey) {
|
|
1657
2518
|
this._cachedKey = cryptoKey2;
|
|
1658
2519
|
this._cachedKdf = "raw";
|
|
1659
2520
|
}
|
|
1660
|
-
return
|
|
2521
|
+
return res2;
|
|
1661
2522
|
}
|
|
1662
2523
|
const passphrase = rawKey;
|
|
1663
2524
|
const kdf = (kdfOverride || options?.kdf || this._defaultKdf || "pbkdf2").toLowerCase();
|
|
@@ -1667,12 +2528,21 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
1667
2528
|
const matchesHash = !targetHash || this._cachedHash !== void 0 && targetHash.toLowerCase() === this._cachedHash.toLowerCase();
|
|
1668
2529
|
const matchesKdf = this._cachedKdf !== void 0 && kdf === this._cachedKdf.toLowerCase();
|
|
1669
2530
|
const matchesSalt = !saltOverride || this._cachedSalt !== void 0 && saltOverride === this._cachedSalt;
|
|
1670
|
-
const currentKeyId = getSafeKeyIdentifier(rawKey);
|
|
1671
2531
|
if (this._cachedKey && matchesSalt && !options?.kdfSalt && matchesKdf && matchesIter && matchesHash) {
|
|
1672
2532
|
if (this._cachedKeyId === currentKeyId) {
|
|
1673
2533
|
return { cryptoKey: this._cachedKey, salt: this._cachedSalt, kdf: this._cachedKdf, iter: this._cachedIter };
|
|
1674
2534
|
}
|
|
1675
2535
|
}
|
|
2536
|
+
if (!saltOverride && !options?.kdfSalt) {
|
|
2537
|
+
const cached = this._keyringCache.get(currentKeyId);
|
|
2538
|
+
if (cached) {
|
|
2539
|
+
const cMatchesKdf = !cached.kdf || cached.kdf.toLowerCase() === kdf;
|
|
2540
|
+
const cMatchesIter = !targetIter || cached.iter === targetIter;
|
|
2541
|
+
if (cMatchesKdf && cMatchesIter) {
|
|
2542
|
+
return cached;
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
1676
2546
|
if (kdf === "pbkdf2") {
|
|
1677
2547
|
const salt = saltOverride || options?.kdfSalt || this._kdfOptions?.salt;
|
|
1678
2548
|
const rawIterations = iterationsOverride || options?.kdfIterations || this._kdfOptions?.iterations || 31e4;
|
|
@@ -1684,15 +2554,19 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
1684
2554
|
iterations,
|
|
1685
2555
|
hash
|
|
1686
2556
|
});
|
|
2557
|
+
const res2 = { cryptoKey: derived.key, salt: derived.salt, kdf: "PBKDF2", iter: iterations };
|
|
1687
2558
|
if (!saltOverride && !options?.kdfSalt) {
|
|
1688
|
-
this.
|
|
1689
|
-
this.
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
2559
|
+
this._keyringCache.set(currentKeyId, res2);
|
|
2560
|
+
if (!keyProvider || keyProvider === this.defaultKey) {
|
|
2561
|
+
this._cachedKey = derived.key;
|
|
2562
|
+
this._cachedSalt = derived.salt;
|
|
2563
|
+
this._cachedKdf = "PBKDF2";
|
|
2564
|
+
this._cachedIter = iterations;
|
|
2565
|
+
this._cachedHash = hash;
|
|
2566
|
+
this._cachedKeyId = currentKeyId;
|
|
2567
|
+
}
|
|
1694
2568
|
}
|
|
1695
|
-
return
|
|
2569
|
+
return res2;
|
|
1696
2570
|
}
|
|
1697
2571
|
if (kdf === "hkdf") {
|
|
1698
2572
|
if (typeof rawKey === "string") {
|
|
@@ -1709,14 +2583,18 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
1709
2583
|
hash,
|
|
1710
2584
|
info
|
|
1711
2585
|
});
|
|
2586
|
+
const res2 = { cryptoKey: derived.key, salt: derived.salt, kdf: "HKDF" };
|
|
1712
2587
|
if (!saltOverride && !options?.kdfSalt) {
|
|
1713
|
-
this.
|
|
1714
|
-
this.
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
2588
|
+
this._keyringCache.set(currentKeyId, res2);
|
|
2589
|
+
if (!keyProvider || keyProvider === this.defaultKey) {
|
|
2590
|
+
this._cachedKey = derived.key;
|
|
2591
|
+
this._cachedSalt = derived.salt;
|
|
2592
|
+
this._cachedKdf = "HKDF";
|
|
2593
|
+
this._cachedHash = hash;
|
|
2594
|
+
this._cachedKeyId = currentKeyId;
|
|
2595
|
+
}
|
|
1718
2596
|
}
|
|
1719
|
-
return
|
|
2597
|
+
return res2;
|
|
1720
2598
|
}
|
|
1721
2599
|
if (kdf === "none") {
|
|
1722
2600
|
if (saltOverride) {
|
|
@@ -1748,11 +2626,13 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
1748
2626
|
} finally {
|
|
1749
2627
|
new Uint8Array(keyHash).fill(0);
|
|
1750
2628
|
}
|
|
1751
|
-
|
|
2629
|
+
const res = { cryptoKey, kdf: "none" };
|
|
2630
|
+
this._keyringCache.set(currentKeyId, res);
|
|
2631
|
+
if (!keyProvider || keyProvider === this.defaultKey) {
|
|
1752
2632
|
this._cachedKey = cryptoKey;
|
|
1753
2633
|
this._cachedKdf = "none";
|
|
1754
2634
|
}
|
|
1755
|
-
return
|
|
2635
|
+
return res;
|
|
1756
2636
|
}
|
|
1757
2637
|
static isEncrypted(payload) {
|
|
1758
2638
|
if (!payload || typeof payload !== "object") return false;
|
|
@@ -1762,6 +2642,29 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
1762
2642
|
return CWebCryptoEncryptionProvider.isEncrypted(payload);
|
|
1763
2643
|
}
|
|
1764
2644
|
async warmupKey(options) {
|
|
2645
|
+
const effectiveKeys = options?.keys || this.keys;
|
|
2646
|
+
if (effectiveKeys && Object.keys(effectiveKeys).length > 0) {
|
|
2647
|
+
const promises = Object.entries(effectiveKeys).map(async ([kid, keyProv]) => {
|
|
2648
|
+
const res2 = await this.resolveCryptoKey(keyProv, void 0, void 0, { ...options, key: keyProv, kid });
|
|
2649
|
+
const rawKey = await this.resolveRawKey(keyProv);
|
|
2650
|
+
const keyId = getSafeKeyIdentifier(rawKey);
|
|
2651
|
+
this._keyringCache.set(keyId, res2);
|
|
2652
|
+
this._keyringCache.set(`kid:${kid}`, res2);
|
|
2653
|
+
return { kid, res: res2 };
|
|
2654
|
+
});
|
|
2655
|
+
const results = await Promise.all(promises);
|
|
2656
|
+
const effectiveActiveKid = options?.activeKid || this.activeKid;
|
|
2657
|
+
const primary = effectiveActiveKid && results.find((r) => r.kid === effectiveActiveKid)?.res || results[0]?.res;
|
|
2658
|
+
if (primary) {
|
|
2659
|
+
this._cachedKey = primary.cryptoKey;
|
|
2660
|
+
this._cachedSalt = primary.salt;
|
|
2661
|
+
this._cachedKdf = primary.kdf;
|
|
2662
|
+
this._cachedIter = primary.iter;
|
|
2663
|
+
this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
|
|
2664
|
+
this._cachedKeyId = getKeyIdentifier(options?.key, this.defaultKey);
|
|
2665
|
+
}
|
|
2666
|
+
return;
|
|
2667
|
+
}
|
|
1765
2668
|
const res = await this.resolveCryptoKey(options?.key, void 0, void 0, options);
|
|
1766
2669
|
this._cachedKey = res.cryptoKey;
|
|
1767
2670
|
this._cachedSalt = res.salt;
|
|
@@ -1804,7 +2707,7 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
1804
2707
|
cryptoKey,
|
|
1805
2708
|
textEncoder$1.encode(jsonString)
|
|
1806
2709
|
);
|
|
1807
|
-
const kid = options?.kid || options?.keyId || this._configuredKid;
|
|
2710
|
+
const kid = options?.kid || options?.keyId || this.activeKid || this._configuredKid;
|
|
1808
2711
|
return {
|
|
1809
2712
|
__encrypted: true,
|
|
1810
2713
|
ciphertext: CKeyManager.toBase64(ciphertextBuffer),
|
|
@@ -3127,12 +4030,26 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3127
4030
|
_cachedHash;
|
|
3128
4031
|
_cachedKeyId;
|
|
3129
4032
|
_configuredKid;
|
|
4033
|
+
_keyringCache = /* @__PURE__ */ new Map();
|
|
4034
|
+
isKeyRetained(key) {
|
|
4035
|
+
if (key === this._cachedKey) return true;
|
|
4036
|
+
for (const entry of this._keyringCache.values()) {
|
|
4037
|
+
if (entry.key === key) return true;
|
|
4038
|
+
}
|
|
4039
|
+
return false;
|
|
4040
|
+
}
|
|
3130
4041
|
setDefaultKey(key) {
|
|
3131
4042
|
super.setDefaultKey(key);
|
|
3132
4043
|
if (this._cachedKey) {
|
|
3133
4044
|
this._cachedKey.fill(0);
|
|
3134
4045
|
this._cachedKey = void 0;
|
|
3135
4046
|
}
|
|
4047
|
+
for (const entry of this._keyringCache.values()) {
|
|
4048
|
+
if (entry.key !== this._cachedKey) {
|
|
4049
|
+
entry.key.fill(0);
|
|
4050
|
+
}
|
|
4051
|
+
}
|
|
4052
|
+
this._keyringCache.clear();
|
|
3136
4053
|
this._cachedSalt = void 0;
|
|
3137
4054
|
this._cachedKdf = void 0;
|
|
3138
4055
|
this._cachedIter = void 0;
|
|
@@ -3145,6 +4062,12 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3145
4062
|
this._cachedKey.fill(0);
|
|
3146
4063
|
this._cachedKey = void 0;
|
|
3147
4064
|
}
|
|
4065
|
+
for (const entry of this._keyringCache.values()) {
|
|
4066
|
+
if (entry.key !== this._cachedKey) {
|
|
4067
|
+
entry.key.fill(0);
|
|
4068
|
+
}
|
|
4069
|
+
}
|
|
4070
|
+
this._keyringCache.clear();
|
|
3148
4071
|
this._cachedSalt = void 0;
|
|
3149
4072
|
this._cachedKdf = void 0;
|
|
3150
4073
|
this._cachedIter = void 0;
|
|
@@ -3152,6 +4075,29 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3152
4075
|
this._cachedKeyId = void 0;
|
|
3153
4076
|
}
|
|
3154
4077
|
async warmupKey(options) {
|
|
4078
|
+
const effectiveKeys = options?.keys || this.keys;
|
|
4079
|
+
if (effectiveKeys && Object.keys(effectiveKeys).length > 0) {
|
|
4080
|
+
const promises = Object.entries(effectiveKeys).map(async ([kid, keyProv]) => {
|
|
4081
|
+
const res2 = await this.deriveKeyAsync(keyProv, void 0, void 0, { ...options, key: keyProv, kid });
|
|
4082
|
+
const raw = await this.resolveRawKeyAsync(keyProv);
|
|
4083
|
+
const keyId = getSafeKeyIdentifier(raw);
|
|
4084
|
+
this._keyringCache.set(keyId, res2);
|
|
4085
|
+
this._keyringCache.set(`kid:${kid}`, res2);
|
|
4086
|
+
return { kid, res: res2 };
|
|
4087
|
+
});
|
|
4088
|
+
const results = await Promise.all(promises);
|
|
4089
|
+
const effectiveActiveKid = options?.activeKid || this.activeKid;
|
|
4090
|
+
const primary = effectiveActiveKid && results.find((r) => r.kid === effectiveActiveKid)?.res || results[0]?.res;
|
|
4091
|
+
if (primary) {
|
|
4092
|
+
this._cachedKey = primary.key;
|
|
4093
|
+
this._cachedSalt = primary.salt;
|
|
4094
|
+
this._cachedKdf = primary.kdf;
|
|
4095
|
+
this._cachedIter = primary.iter;
|
|
4096
|
+
this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
|
|
4097
|
+
this._cachedKeyId = getSafeKeyIdentifier(await this.resolveRawKeyAsync(options?.key));
|
|
4098
|
+
}
|
|
4099
|
+
return;
|
|
4100
|
+
}
|
|
3155
4101
|
const res = await this.deriveKeyAsync(options?.key, void 0, void 0, options);
|
|
3156
4102
|
if (!options?.key) {
|
|
3157
4103
|
this._cachedKey = res.key;
|
|
@@ -3166,6 +4112,30 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3166
4112
|
* Synchronously pre-derives and caches the encryption key for subsequent synchronous operations.
|
|
3167
4113
|
*/
|
|
3168
4114
|
warmupKeySync(options) {
|
|
4115
|
+
const effectiveKeys = options?.keys || this.keys;
|
|
4116
|
+
if (effectiveKeys && Object.keys(effectiveKeys).length > 0) {
|
|
4117
|
+
const entries = Object.entries(effectiveKeys);
|
|
4118
|
+
const results = [];
|
|
4119
|
+
for (const [kid, keyProv] of entries) {
|
|
4120
|
+
const res2 = this.deriveKeySync(keyProv, void 0, void 0, { ...options, key: keyProv, kid });
|
|
4121
|
+
const raw = this.resolveRawKeySync(keyProv);
|
|
4122
|
+
const keyId = getSafeKeyIdentifier(raw);
|
|
4123
|
+
this._keyringCache.set(keyId, res2);
|
|
4124
|
+
this._keyringCache.set(`kid:${kid}`, res2);
|
|
4125
|
+
results.push({ kid, res: res2 });
|
|
4126
|
+
}
|
|
4127
|
+
const effectiveActiveKid = options?.activeKid || this.activeKid;
|
|
4128
|
+
const primary = effectiveActiveKid && results.find((r) => r.kid === effectiveActiveKid)?.res || results[0]?.res;
|
|
4129
|
+
if (primary) {
|
|
4130
|
+
this._cachedKey = primary.key;
|
|
4131
|
+
this._cachedSalt = primary.salt;
|
|
4132
|
+
this._cachedKdf = primary.kdf;
|
|
4133
|
+
this._cachedIter = primary.iter;
|
|
4134
|
+
this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
|
|
4135
|
+
this._cachedKeyId = getSafeKeyIdentifier(this.resolveRawKeySync(options?.key));
|
|
4136
|
+
}
|
|
4137
|
+
return;
|
|
4138
|
+
}
|
|
3169
4139
|
const res = this.deriveKeySync(options?.key, void 0, void 0, options);
|
|
3170
4140
|
if (!options?.key) {
|
|
3171
4141
|
this._cachedKey = res.key;
|
|
@@ -3184,7 +4154,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3184
4154
|
return Buffer.from(infoRaw);
|
|
3185
4155
|
}
|
|
3186
4156
|
constructor(key, options) {
|
|
3187
|
-
super(key);
|
|
4157
|
+
super(key, options);
|
|
3188
4158
|
if (options) {
|
|
3189
4159
|
if (options.kid || options.keyId) {
|
|
3190
4160
|
this._configuredKid = options.kid || options.keyId;
|
|
@@ -3202,7 +4172,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3202
4172
|
}
|
|
3203
4173
|
}
|
|
3204
4174
|
resolveRawKeySync(keyProvider) {
|
|
3205
|
-
const target = keyProvider || this.defaultKey;
|
|
4175
|
+
const target = keyProvider || (this.activeKid && this.keys ? this.keys[this.activeKid] : void 0) || this.defaultKey;
|
|
3206
4176
|
if (!target) {
|
|
3207
4177
|
throw new Error("[CNodeCryptoEncryptionProvider] No encryption key or keyProvider was supplied.");
|
|
3208
4178
|
}
|
|
@@ -3232,7 +4202,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3232
4202
|
return rawKey;
|
|
3233
4203
|
}
|
|
3234
4204
|
async resolveRawKeyAsync(keyProvider) {
|
|
3235
|
-
const target = keyProvider || this.defaultKey;
|
|
4205
|
+
const target = keyProvider || (this.activeKid && this.keys ? this.keys[this.activeKid] : void 0) || this.defaultKey;
|
|
3236
4206
|
if (!target) {
|
|
3237
4207
|
throw new Error("[CNodeCryptoEncryptionProvider] No encryption key or keyProvider was supplied.");
|
|
3238
4208
|
}
|
|
@@ -3263,15 +4233,16 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3263
4233
|
const currentKeyId = getSafeKeyIdentifier(raw);
|
|
3264
4234
|
if (Buffer.isBuffer(raw)) {
|
|
3265
4235
|
if (raw.length === 32) {
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
4236
|
+
const cached = this._keyringCache.get(currentKeyId);
|
|
4237
|
+
if (cached) return cached;
|
|
4238
|
+
const res2 = { key: Buffer.from(raw), kdf: "raw" };
|
|
4239
|
+
this._keyringCache.set(currentKeyId, res2);
|
|
4240
|
+
if (!keyProvider || keyProvider === this.defaultKey) {
|
|
4241
|
+
this._cachedKey = res2.key;
|
|
4242
|
+
this._cachedKdf = "raw";
|
|
4243
|
+
this._cachedKeyId = currentKeyId;
|
|
3273
4244
|
}
|
|
3274
|
-
return
|
|
4245
|
+
return res2;
|
|
3275
4246
|
}
|
|
3276
4247
|
throw new Error(
|
|
3277
4248
|
`[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 +4261,16 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3290
4261
|
return { key: this._cachedKey, salt: this._cachedSalt, kdf: this._cachedKdf, iter: this._cachedIter };
|
|
3291
4262
|
}
|
|
3292
4263
|
}
|
|
4264
|
+
if (!saltOverride && !options?.kdfSalt) {
|
|
4265
|
+
const cached = this._keyringCache.get(currentKeyId);
|
|
4266
|
+
if (cached) {
|
|
4267
|
+
const cMatchesKdf = !cached.kdf || cached.kdf.toLowerCase() === kdf;
|
|
4268
|
+
const cMatchesIter = !targetIter || cached.iter === targetIter;
|
|
4269
|
+
if (cMatchesKdf && cMatchesIter) {
|
|
4270
|
+
return cached;
|
|
4271
|
+
}
|
|
4272
|
+
}
|
|
4273
|
+
}
|
|
3293
4274
|
if (kdf === "pbkdf2") {
|
|
3294
4275
|
const s = saltOverride || options?.kdfSalt || this._kdfOptions?.salt;
|
|
3295
4276
|
const saltBytes = CKeyManager.resolveSalt(s);
|
|
@@ -3298,20 +4279,24 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3298
4279
|
const iterations = Math.min(Math.max(rawIter, 1e3), 6e5);
|
|
3299
4280
|
const hash = (options?.kdfHash || this._kdfOptions?.hash || "SHA-256").toLowerCase().replace("-", "");
|
|
3300
4281
|
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 {
|
|
4282
|
+
const res2 = {
|
|
3310
4283
|
key: key2,
|
|
3311
4284
|
salt: saltBuf.toString("base64"),
|
|
3312
4285
|
kdf: "PBKDF2",
|
|
3313
4286
|
iter: iterations
|
|
3314
4287
|
};
|
|
4288
|
+
if (!saltOverride && !options?.kdfSalt) {
|
|
4289
|
+
this._keyringCache.set(currentKeyId, res2);
|
|
4290
|
+
if (!keyProvider || keyProvider === this.defaultKey) {
|
|
4291
|
+
this._cachedKey = key2;
|
|
4292
|
+
this._cachedSalt = saltBuf.toString("base64");
|
|
4293
|
+
this._cachedKdf = "PBKDF2";
|
|
4294
|
+
this._cachedIter = iterations;
|
|
4295
|
+
this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
|
|
4296
|
+
this._cachedKeyId = currentKeyId;
|
|
4297
|
+
}
|
|
4298
|
+
}
|
|
4299
|
+
return res2;
|
|
3315
4300
|
}
|
|
3316
4301
|
if (kdf === "hkdf") {
|
|
3317
4302
|
if (typeof raw === "string") {
|
|
@@ -3325,49 +4310,56 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3325
4310
|
const hash = (options?.kdfHash || this._kdfOptions?.hash || "SHA-256").toLowerCase().replace("-", "");
|
|
3326
4311
|
const info = this.resolveInfoBuffer(options?.info || this._kdfOptions?.info);
|
|
3327
4312
|
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 {
|
|
4313
|
+
const res2 = {
|
|
3336
4314
|
key: key2,
|
|
3337
4315
|
salt: saltBuf.toString("base64"),
|
|
3338
4316
|
kdf: "HKDF"
|
|
3339
4317
|
};
|
|
4318
|
+
if (!saltOverride && !options?.kdfSalt) {
|
|
4319
|
+
this._keyringCache.set(currentKeyId, res2);
|
|
4320
|
+
if (!keyProvider || keyProvider === this.defaultKey) {
|
|
4321
|
+
this._cachedKey = key2;
|
|
4322
|
+
this._cachedSalt = saltBuf.toString("base64");
|
|
4323
|
+
this._cachedKdf = "HKDF";
|
|
4324
|
+
this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
|
|
4325
|
+
this._cachedKeyId = currentKeyId;
|
|
4326
|
+
}
|
|
4327
|
+
}
|
|
4328
|
+
return res2;
|
|
3340
4329
|
}
|
|
3341
4330
|
if (kdf === "none") {
|
|
3342
4331
|
console.warn(
|
|
3343
4332
|
"[CNodeCryptoEncryptionProvider] WARNING: Decrypting a legacy envelope with no KDF (raw SHA-256 key derivation). This is cryptographically weak. Migrate to pbkdf2 or hkdf."
|
|
3344
4333
|
);
|
|
3345
4334
|
}
|
|
3346
|
-
if (!keyProvider && this._cachedKey && this._cachedKdf === "none" && this._cachedKeyId === currentKeyId) {
|
|
4335
|
+
if ((!keyProvider || keyProvider === this.defaultKey) && this._cachedKey && this._cachedKdf === "none" && this._cachedKeyId === currentKeyId) {
|
|
3347
4336
|
return { key: this._cachedKey, kdf: "none" };
|
|
3348
4337
|
}
|
|
3349
4338
|
const key = crypto__namespace.createHash("sha256").update(passphrase, "utf8").digest();
|
|
3350
|
-
|
|
4339
|
+
const res = { key, kdf: "none" };
|
|
4340
|
+
this._keyringCache.set(currentKeyId, res);
|
|
4341
|
+
if (!keyProvider || keyProvider === this.defaultKey) {
|
|
3351
4342
|
this._cachedKey = key;
|
|
3352
4343
|
this._cachedKdf = "none";
|
|
3353
4344
|
this._cachedKeyId = currentKeyId;
|
|
3354
4345
|
}
|
|
3355
|
-
return
|
|
4346
|
+
return res;
|
|
3356
4347
|
}
|
|
3357
4348
|
async deriveKeyAsync(keyProvider, kdfOverride, saltOverride, options, iterationsOverride) {
|
|
3358
4349
|
const raw = await this.resolveRawKeyAsync(keyProvider);
|
|
3359
4350
|
const currentKeyId = getSafeKeyIdentifier(raw);
|
|
3360
4351
|
if (Buffer.isBuffer(raw)) {
|
|
3361
4352
|
if (raw.length === 32) {
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
4353
|
+
const cached = this._keyringCache.get(currentKeyId);
|
|
4354
|
+
if (cached) return cached;
|
|
4355
|
+
const res2 = { key: Buffer.from(raw), kdf: "raw" };
|
|
4356
|
+
this._keyringCache.set(currentKeyId, res2);
|
|
4357
|
+
if (!keyProvider || keyProvider === this.defaultKey) {
|
|
4358
|
+
this._cachedKey = res2.key;
|
|
4359
|
+
this._cachedKdf = "raw";
|
|
4360
|
+
this._cachedKeyId = currentKeyId;
|
|
3369
4361
|
}
|
|
3370
|
-
return
|
|
4362
|
+
return res2;
|
|
3371
4363
|
}
|
|
3372
4364
|
throw new Error(
|
|
3373
4365
|
`[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 +4378,16 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3386
4378
|
return { key: this._cachedKey, salt: this._cachedSalt, kdf: this._cachedKdf, iter: this._cachedIter };
|
|
3387
4379
|
}
|
|
3388
4380
|
}
|
|
4381
|
+
if (!saltOverride && !options?.kdfSalt) {
|
|
4382
|
+
const cached = this._keyringCache.get(currentKeyId);
|
|
4383
|
+
if (cached) {
|
|
4384
|
+
const cMatchesKdf = !cached.kdf || cached.kdf.toLowerCase() === kdf;
|
|
4385
|
+
const cMatchesIter = !targetIter || cached.iter === targetIter;
|
|
4386
|
+
if (cMatchesKdf && cMatchesIter) {
|
|
4387
|
+
return cached;
|
|
4388
|
+
}
|
|
4389
|
+
}
|
|
4390
|
+
}
|
|
3389
4391
|
if (kdf === "pbkdf2") {
|
|
3390
4392
|
const s = saltOverride || options?.kdfSalt || this._kdfOptions?.salt;
|
|
3391
4393
|
const saltBytes = CKeyManager.resolveSalt(s);
|
|
@@ -3399,20 +4401,24 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3399
4401
|
else resolve(derived);
|
|
3400
4402
|
});
|
|
3401
4403
|
});
|
|
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 {
|
|
4404
|
+
const res2 = {
|
|
3411
4405
|
key: key2,
|
|
3412
4406
|
salt: saltBuf.toString("base64"),
|
|
3413
4407
|
kdf: "PBKDF2",
|
|
3414
4408
|
iter: iterations
|
|
3415
4409
|
};
|
|
4410
|
+
if (!saltOverride && !options?.kdfSalt) {
|
|
4411
|
+
this._keyringCache.set(currentKeyId, res2);
|
|
4412
|
+
if (!keyProvider || keyProvider === this.defaultKey) {
|
|
4413
|
+
this._cachedKey = key2;
|
|
4414
|
+
this._cachedSalt = saltBuf.toString("base64");
|
|
4415
|
+
this._cachedKdf = "PBKDF2";
|
|
4416
|
+
this._cachedIter = iterations;
|
|
4417
|
+
this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
|
|
4418
|
+
this._cachedKeyId = currentKeyId;
|
|
4419
|
+
}
|
|
4420
|
+
}
|
|
4421
|
+
return res2;
|
|
3416
4422
|
}
|
|
3417
4423
|
if (kdf === "hkdf") {
|
|
3418
4424
|
if (typeof raw === "string") {
|
|
@@ -3431,34 +4437,40 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3431
4437
|
else resolve(Buffer.from(derived));
|
|
3432
4438
|
});
|
|
3433
4439
|
});
|
|
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 {
|
|
4440
|
+
const res2 = {
|
|
3442
4441
|
key: key2,
|
|
3443
4442
|
salt: saltBuf.toString("base64"),
|
|
3444
4443
|
kdf: "HKDF"
|
|
3445
4444
|
};
|
|
4445
|
+
if (!saltOverride && !options?.kdfSalt) {
|
|
4446
|
+
this._keyringCache.set(currentKeyId, res2);
|
|
4447
|
+
if (!keyProvider || keyProvider === this.defaultKey) {
|
|
4448
|
+
this._cachedKey = key2;
|
|
4449
|
+
this._cachedSalt = saltBuf.toString("base64");
|
|
4450
|
+
this._cachedKdf = "HKDF";
|
|
4451
|
+
this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
|
|
4452
|
+
this._cachedKeyId = currentKeyId;
|
|
4453
|
+
}
|
|
4454
|
+
}
|
|
4455
|
+
return res2;
|
|
3446
4456
|
}
|
|
3447
4457
|
if (kdf === "none") {
|
|
3448
4458
|
console.warn(
|
|
3449
4459
|
"[CNodeCryptoEncryptionProvider] WARNING: Decrypting a legacy envelope with no KDF (raw SHA-256 key derivation). This is cryptographically weak. Migrate to pbkdf2 or hkdf."
|
|
3450
4460
|
);
|
|
3451
4461
|
}
|
|
3452
|
-
if (!keyProvider && this._cachedKey && this._cachedKdf === "none" && this._cachedKeyId === currentKeyId) {
|
|
4462
|
+
if ((!keyProvider || keyProvider === this.defaultKey) && this._cachedKey && this._cachedKdf === "none" && this._cachedKeyId === currentKeyId) {
|
|
3453
4463
|
return { key: this._cachedKey, kdf: "none" };
|
|
3454
4464
|
}
|
|
3455
4465
|
const key = crypto__namespace.createHash("sha256").update(passphrase, "utf8").digest();
|
|
3456
|
-
|
|
4466
|
+
const res = { key, kdf: "none" };
|
|
4467
|
+
this._keyringCache.set(currentKeyId, res);
|
|
4468
|
+
if (!keyProvider || keyProvider === this.defaultKey) {
|
|
3457
4469
|
this._cachedKey = key;
|
|
3458
4470
|
this._cachedKdf = "none";
|
|
3459
4471
|
this._cachedKeyId = currentKeyId;
|
|
3460
4472
|
}
|
|
3461
|
-
return
|
|
4473
|
+
return res;
|
|
3462
4474
|
}
|
|
3463
4475
|
static isEncrypted(payload) {
|
|
3464
4476
|
if (!payload || typeof payload !== "object") return false;
|
|
@@ -3498,7 +4510,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3498
4510
|
]);
|
|
3499
4511
|
const authTag = cipher.getAuthTag();
|
|
3500
4512
|
const combinedCiphertext = Buffer.concat([encryptedBuffer, authTag]);
|
|
3501
|
-
const kid = options?.kid || options?.keyId || this._configuredKid;
|
|
4513
|
+
const kid = options?.kid || options?.keyId || this.activeKid || this._configuredKid;
|
|
3502
4514
|
return {
|
|
3503
4515
|
__encrypted: true,
|
|
3504
4516
|
ciphertext: combinedCiphertext.toString("base64"),
|
|
@@ -3511,7 +4523,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3511
4523
|
...hasAad ? { aad: true } : {}
|
|
3512
4524
|
};
|
|
3513
4525
|
} finally {
|
|
3514
|
-
if (
|
|
4526
|
+
if (!this.isKeyRetained(key)) {
|
|
3515
4527
|
key.fill(0);
|
|
3516
4528
|
}
|
|
3517
4529
|
}
|
|
@@ -3600,7 +4612,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3600
4612
|
if (err instanceof SyntaxError) throw err;
|
|
3601
4613
|
throw new Error(`[CNodeCryptoEncryptionProvider] Decryption failed: ${err?.message || "Authentication tag mismatch or corrupted ciphertext"}`);
|
|
3602
4614
|
} finally {
|
|
3603
|
-
if (
|
|
4615
|
+
if (!this.isKeyRetained(key)) {
|
|
3604
4616
|
key.fill(0);
|
|
3605
4617
|
}
|
|
3606
4618
|
}
|
|
@@ -3609,6 +4621,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3609
4621
|
encryptSync(data, options) {
|
|
3610
4622
|
if (data === void 0 || data === null) return data;
|
|
3611
4623
|
if (this.isEncrypted(data)) return data;
|
|
4624
|
+
options = this._resolveOutboundKeyringOptions(options);
|
|
3612
4625
|
if (Array.isArray(data)) {
|
|
3613
4626
|
if (options?.fields && options.fields.length > 0 || options?.valueEncryption === true) {
|
|
3614
4627
|
return data.map((item) => this.encryptSync(item, options));
|
|
@@ -3644,7 +4657,12 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3644
4657
|
decryptSync(payload, options) {
|
|
3645
4658
|
if (payload === void 0 || payload === null) return payload;
|
|
3646
4659
|
if (this.isEncrypted(payload)) {
|
|
3647
|
-
|
|
4660
|
+
const metaErr = this._validateEnvelopeMetadata(payload, options);
|
|
4661
|
+
if (metaErr) {
|
|
4662
|
+
return this._handleDecryptError(metaErr, payload, "", payload, void 0, options);
|
|
4663
|
+
}
|
|
4664
|
+
const envelopeKey = this._resolveDecryptionKeyForEnvelope(payload, options);
|
|
4665
|
+
return this.decryptValueSync(payload, { ...options, key: envelopeKey });
|
|
3648
4666
|
}
|
|
3649
4667
|
if (Array.isArray(payload)) {
|
|
3650
4668
|
return payload.map((item) => this.decryptSync(item, options));
|
|
@@ -3658,28 +4676,35 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3658
4676
|
for (const key of targetKeys) {
|
|
3659
4677
|
if (this.isEncrypted(result[key])) {
|
|
3660
4678
|
const fieldPayload = result[key];
|
|
4679
|
+
const metaErr = this._validateEnvelopeMetadata(fieldPayload, options);
|
|
4680
|
+
if (metaErr) {
|
|
4681
|
+
return this._handleDecryptError(metaErr, result, key, fieldPayload, void 0, options);
|
|
4682
|
+
}
|
|
4683
|
+
const envelopeKey = this._resolveDecryptionKeyForEnvelope(fieldPayload, options);
|
|
3661
4684
|
const salt = fieldPayload.salt || "__no_salt__";
|
|
3662
|
-
|
|
4685
|
+
const memoKey = `${salt}:${fieldPayload.kid || ""}`;
|
|
4686
|
+
let derivedKey = keyCache.get(memoKey);
|
|
3663
4687
|
if (!derivedKey) {
|
|
3664
4688
|
const kdfFromEnvelope = fieldPayload.kdf ? fieldPayload.kdf.toLowerCase() : "none";
|
|
3665
4689
|
const derived = this.deriveKeySync(
|
|
3666
|
-
|
|
4690
|
+
envelopeKey,
|
|
3667
4691
|
kdfFromEnvelope,
|
|
3668
4692
|
fieldPayload.salt,
|
|
3669
4693
|
options,
|
|
3670
4694
|
fieldPayload.iter
|
|
3671
4695
|
);
|
|
3672
4696
|
derivedKey = derived.key;
|
|
3673
|
-
keyCache.set(
|
|
4697
|
+
keyCache.set(memoKey, derivedKey);
|
|
3674
4698
|
}
|
|
3675
|
-
const boundOptions = this._bindFieldAad(payload, key, options);
|
|
4699
|
+
const boundOptions = this._bindFieldAad(payload, key, { ...options, key: envelopeKey });
|
|
3676
4700
|
const hasBoundAad = boundOptions.aad !== options?.aad;
|
|
3677
4701
|
try {
|
|
3678
4702
|
safeSetProperty(result, key, this.decryptCiphertextWithKey(fieldPayload, derivedKey, boundOptions));
|
|
3679
4703
|
} catch (err) {
|
|
3680
|
-
|
|
4704
|
+
const canFallback = hasBoundAad && (!options?.aadFields || options.aadFields.length === 0) && typeof options?.aad !== "function";
|
|
4705
|
+
if (canFallback && isAuthOrDecryptionError(err)) {
|
|
3681
4706
|
try {
|
|
3682
|
-
safeSetProperty(result, key, this.decryptCiphertextWithKey(fieldPayload, derivedKey, options));
|
|
4707
|
+
safeSetProperty(result, key, this.decryptCiphertextWithKey(fieldPayload, derivedKey, { ...options, key: envelopeKey }));
|
|
3683
4708
|
} catch (fallbackErr) {
|
|
3684
4709
|
throw fallbackErr;
|
|
3685
4710
|
}
|
|
@@ -3693,7 +4718,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3693
4718
|
return mutated ? result : payload;
|
|
3694
4719
|
} finally {
|
|
3695
4720
|
for (const k of keyCache.values()) {
|
|
3696
|
-
if (
|
|
4721
|
+
if (!this.isKeyRetained(k)) {
|
|
3697
4722
|
k.fill(0);
|
|
3698
4723
|
}
|
|
3699
4724
|
}
|
|
@@ -3741,7 +4766,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3741
4766
|
]);
|
|
3742
4767
|
const authTag = cipher.getAuthTag();
|
|
3743
4768
|
const combined = Buffer.concat([ciphertext, authTag]);
|
|
3744
|
-
const kid = options?.kid || options?.keyId || this._configuredKid;
|
|
4769
|
+
const kid = options?.kid || options?.keyId || this.activeKid || this._configuredKid;
|
|
3745
4770
|
return {
|
|
3746
4771
|
__encrypted: true,
|
|
3747
4772
|
ciphertext: combined.toString("base64"),
|
|
@@ -3754,7 +4779,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3754
4779
|
...hasAad ? { aad: true } : {}
|
|
3755
4780
|
};
|
|
3756
4781
|
} finally {
|
|
3757
|
-
if (
|
|
4782
|
+
if (!this.isKeyRetained(key)) {
|
|
3758
4783
|
key.fill(0);
|
|
3759
4784
|
}
|
|
3760
4785
|
}
|
|
@@ -3778,7 +4803,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3778
4803
|
if (err instanceof SyntaxError) throw err;
|
|
3779
4804
|
throw new Error(`[CNodeCryptoEncryptionProvider] Decryption failed: ${err?.message || "Authentication tag mismatch or corrupted ciphertext"}`);
|
|
3780
4805
|
} finally {
|
|
3781
|
-
if (
|
|
4806
|
+
if (!this.isKeyRetained(key)) {
|
|
3782
4807
|
key.fill(0);
|
|
3783
4808
|
}
|
|
3784
4809
|
}
|
|
@@ -3786,7 +4811,12 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3786
4811
|
async decrypt(payload, options) {
|
|
3787
4812
|
if (payload === void 0 || payload === null) return payload;
|
|
3788
4813
|
if (this.isEncrypted(payload)) {
|
|
3789
|
-
|
|
4814
|
+
const metaErr = this._validateEnvelopeMetadata(payload, options);
|
|
4815
|
+
if (metaErr) {
|
|
4816
|
+
return this._handleDecryptError(metaErr, payload, "", payload, void 0, options);
|
|
4817
|
+
}
|
|
4818
|
+
const envelopeKey = this._resolveDecryptionKeyForEnvelope(payload, options);
|
|
4819
|
+
return this.decryptValue(payload, { ...options, key: envelopeKey });
|
|
3790
4820
|
}
|
|
3791
4821
|
if (Array.isArray(payload)) {
|
|
3792
4822
|
return Promise.all(payload.map((item) => this.decrypt(item, options)));
|
|
@@ -3800,28 +4830,35 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3800
4830
|
for (const key of targetKeys) {
|
|
3801
4831
|
if (this.isEncrypted(result[key])) {
|
|
3802
4832
|
const fieldPayload = result[key];
|
|
4833
|
+
const metaErr = this._validateEnvelopeMetadata(fieldPayload, options);
|
|
4834
|
+
if (metaErr) {
|
|
4835
|
+
return this._handleDecryptError(metaErr, result, key, fieldPayload, void 0, options);
|
|
4836
|
+
}
|
|
4837
|
+
const envelopeKey = this._resolveDecryptionKeyForEnvelope(fieldPayload, options);
|
|
3803
4838
|
const salt = fieldPayload.salt || "__no_salt__";
|
|
3804
|
-
|
|
4839
|
+
const memoKey = `${salt}:${fieldPayload.kid || ""}`;
|
|
4840
|
+
let derivedKey = keyCache.get(memoKey);
|
|
3805
4841
|
if (!derivedKey) {
|
|
3806
4842
|
const kdfFromEnvelope = fieldPayload.kdf ? fieldPayload.kdf.toLowerCase() : "none";
|
|
3807
4843
|
const derived = await this.deriveKeyAsync(
|
|
3808
|
-
|
|
4844
|
+
envelopeKey,
|
|
3809
4845
|
kdfFromEnvelope,
|
|
3810
4846
|
fieldPayload.salt ? CKeyManager.fromBase64(fieldPayload.salt) : void 0,
|
|
3811
4847
|
options,
|
|
3812
4848
|
fieldPayload.iter
|
|
3813
4849
|
);
|
|
3814
4850
|
derivedKey = derived.key;
|
|
3815
|
-
keyCache.set(
|
|
4851
|
+
keyCache.set(memoKey, derivedKey);
|
|
3816
4852
|
}
|
|
3817
|
-
const boundOptions = this._bindFieldAad(payload, key, options);
|
|
4853
|
+
const boundOptions = this._bindFieldAad(payload, key, { ...options, key: envelopeKey });
|
|
3818
4854
|
const hasBoundAad = boundOptions.aad !== options?.aad;
|
|
3819
4855
|
try {
|
|
3820
4856
|
safeSetProperty(result, key, this.decryptCiphertextWithKey(fieldPayload, derivedKey, boundOptions));
|
|
3821
4857
|
} catch (err) {
|
|
3822
|
-
|
|
4858
|
+
const canFallback = hasBoundAad && (!options?.aadFields || options.aadFields.length === 0) && typeof options?.aad !== "function";
|
|
4859
|
+
if (canFallback && isAuthOrDecryptionError(err)) {
|
|
3823
4860
|
try {
|
|
3824
|
-
safeSetProperty(result, key, this.decryptCiphertextWithKey(fieldPayload, derivedKey, options));
|
|
4861
|
+
safeSetProperty(result, key, this.decryptCiphertextWithKey(fieldPayload, derivedKey, { ...options, key: envelopeKey }));
|
|
3825
4862
|
} catch (fallbackErr) {
|
|
3826
4863
|
throw fallbackErr;
|
|
3827
4864
|
}
|
|
@@ -3835,7 +4872,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3835
4872
|
return mutated ? result : payload;
|
|
3836
4873
|
} finally {
|
|
3837
4874
|
for (const k of keyCache.values()) {
|
|
3838
|
-
if (
|
|
4875
|
+
if (!this.isKeyRetained(k)) {
|
|
3839
4876
|
k.fill(0);
|
|
3840
4877
|
}
|
|
3841
4878
|
}
|
|
@@ -3866,7 +4903,11 @@ function createEncryptionProvider(options = {}) {
|
|
|
3866
4903
|
kdfIterations: options.kdfIterations,
|
|
3867
4904
|
kdfHash: options.kdfHash,
|
|
3868
4905
|
kdfSalt: options.kdfSalt,
|
|
3869
|
-
info: options.info
|
|
4906
|
+
info: options.info,
|
|
4907
|
+
kid: options.kid || options.keyId,
|
|
4908
|
+
keys: options.keys,
|
|
4909
|
+
activeKid: options.activeKid,
|
|
4910
|
+
allowedKids: options.allowedKids
|
|
3870
4911
|
});
|
|
3871
4912
|
}
|
|
3872
4913
|
if (alg === "AES-CBC") {
|
|
@@ -3903,7 +4944,11 @@ function createEncryptionProvider(options = {}) {
|
|
|
3903
4944
|
kdfIterations: options.kdfIterations,
|
|
3904
4945
|
kdfHash: options.kdfHash,
|
|
3905
4946
|
kdfSalt: options.kdfSalt,
|
|
3906
|
-
info: options.info
|
|
4947
|
+
info: options.info,
|
|
4948
|
+
kid: options.kid || options.keyId,
|
|
4949
|
+
keys: options.keys,
|
|
4950
|
+
activeKid: options.activeKid,
|
|
4951
|
+
allowedKids: options.allowedKids
|
|
3907
4952
|
});
|
|
3908
4953
|
}
|
|
3909
4954
|
throw new Error(
|
|
@@ -3966,7 +5011,19 @@ function isDataEncrypted(provider, data) {
|
|
|
3966
5011
|
function buildEncryptionContext(encryption, url, _defaultIdKey) {
|
|
3967
5012
|
return {
|
|
3968
5013
|
url,
|
|
5014
|
+
aad: encryption?.aad,
|
|
5015
|
+
aadFields: encryption?.aadFields,
|
|
5016
|
+
baseAad: encryption?.baseAad,
|
|
5017
|
+
bindRequestContextAad: encryption?.bindRequestContextAad,
|
|
5018
|
+
strict: encryption?.strict,
|
|
5019
|
+
allowedAlgorithms: encryption?.allowedAlgorithms,
|
|
5020
|
+
allowedKids: encryption?.allowedKids,
|
|
5021
|
+
onDecryptError: encryption?.onDecryptError,
|
|
3969
5022
|
key: encryption?.key,
|
|
5023
|
+
kid: encryption?.kid,
|
|
5024
|
+
keyId: encryption?.keyId,
|
|
5025
|
+
keys: encryption?.keys,
|
|
5026
|
+
activeKid: encryption?.activeKid,
|
|
3970
5027
|
valueEncryption: encryption?.valueEncryption,
|
|
3971
5028
|
fields: encryption?.fields,
|
|
3972
5029
|
publicKey: encryption?.publicKey,
|
|
@@ -3997,7 +5054,7 @@ async function decryptPageRows(rows, encryption, apiUrl, idKey) {
|
|
|
3997
5054
|
}
|
|
3998
5055
|
return isDataEncrypted(cipherProvider, row);
|
|
3999
5056
|
});
|
|
4000
|
-
if (!hasEncryptedRows) {
|
|
5057
|
+
if (!hasEncryptedRows && !encryption?.strict) {
|
|
4001
5058
|
return { rows, decrypted: false };
|
|
4002
5059
|
}
|
|
4003
5060
|
const ctx = buildEncryptionContext(encryption, apiUrl);
|
|
@@ -4160,6 +5217,13 @@ class CSearchIndex {
|
|
|
4160
5217
|
const signature = await subtle.sign("HMAC", key, textEncoder.encode(normalized));
|
|
4161
5218
|
return Array.from(new Uint8Array(signature)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
4162
5219
|
}
|
|
5220
|
+
/**
|
|
5221
|
+
* Computes a deterministic HMAC-SHA256 blind index hash for a term using the configured HMAC key.
|
|
5222
|
+
* Performs Unicode NFKC normalization, trimming, and lowercasing.
|
|
5223
|
+
*/
|
|
5224
|
+
async computeBlindIndex(term) {
|
|
5225
|
+
return this.computeToken(term);
|
|
5226
|
+
}
|
|
4163
5227
|
/**
|
|
4164
5228
|
* Computes HMAC-SHA256 tokens for all trigrams of a term.
|
|
4165
5229
|
*/
|
|
@@ -4189,9 +5253,21 @@ class CSearchIndex {
|
|
|
4189
5253
|
const token = await this.computeToken(rawVal);
|
|
4190
5254
|
const trigrams = await this.computeTrigramTokens(rawVal);
|
|
4191
5255
|
fieldMap.set(field, { token, trigrams });
|
|
5256
|
+
} else if (rawVal && typeof rawVal === "object" && rawVal.__encrypted === true) {
|
|
5257
|
+
const precomputedToken = row[`${field}_bidx`] || row[`${field}_token`];
|
|
5258
|
+
if (typeof precomputedToken === "string" && precomputedToken) {
|
|
5259
|
+
fieldMap.set(field, { token: precomputedToken, trigrams: /* @__PURE__ */ new Set() });
|
|
5260
|
+
}
|
|
4192
5261
|
}
|
|
4193
5262
|
}
|
|
4194
5263
|
}
|
|
5264
|
+
/**
|
|
5265
|
+
* Indexes a row using pre-decrypted plaintext field values.
|
|
5266
|
+
* Allows indexing rows that contain pre-encrypted ciphertext envelopes in allData.
|
|
5267
|
+
*/
|
|
5268
|
+
async indexRowDecrypted(row, decryptedPlaintext) {
|
|
5269
|
+
return this.indexRow(row, decryptedPlaintext);
|
|
5270
|
+
}
|
|
4195
5271
|
/**
|
|
4196
5272
|
* Indexes an array of rows.
|
|
4197
5273
|
*/
|
|
@@ -4240,7 +5316,7 @@ class CSearchIndex {
|
|
|
4240
5316
|
}
|
|
4241
5317
|
return true;
|
|
4242
5318
|
}
|
|
4243
|
-
if (operator === EDataFilterOperator.NotIn || operator === "notIn") {
|
|
5319
|
+
if (operator === EDataFilterOperator.NotIn || operator === "notIn" || operator === "nin") {
|
|
4244
5320
|
const queryTrigramTokens = await this.computeTrigramTokens(filterValue);
|
|
4245
5321
|
if (queryTrigramTokens.size === 0) return false;
|
|
4246
5322
|
for (const tok of queryTrigramTokens) {
|
|
@@ -4311,7 +5387,7 @@ class CSearchIndex {
|
|
|
4311
5387
|
}
|
|
4312
5388
|
return true;
|
|
4313
5389
|
});
|
|
4314
|
-
} else if (op === EDataFilterOperator.NotIn || op === "notIn") {
|
|
5390
|
+
} else if (op === EDataFilterOperator.NotIn || op === "notIn" || op === "nin") {
|
|
4315
5391
|
const queryTrigrams = await this.computeTrigramTokens(filterValue);
|
|
4316
5392
|
checks.push((row) => {
|
|
4317
5393
|
const entry = this.getEntry(row, key);
|
|
@@ -4430,6 +5506,36 @@ class AEncryptedClientProvider extends ADataProvider {
|
|
|
4430
5506
|
} : void 0;
|
|
4431
5507
|
return await encryptRowsChunked(rows, this.encryption, options, void 0, this.idKey, indexChunk);
|
|
4432
5508
|
}
|
|
5509
|
+
/**
|
|
5510
|
+
* Indexes pre-encrypted rows by performing a transient in-memory decryption pass
|
|
5511
|
+
* on search fields to populate CSearchIndex without mutating underlying encrypted records in allData.
|
|
5512
|
+
*/
|
|
5513
|
+
async indexPreEncryptedRows(rows) {
|
|
5514
|
+
if (!this.isEncryptionEnabled || !this._searchIndex || !rows || rows.length === 0) {
|
|
5515
|
+
return;
|
|
5516
|
+
}
|
|
5517
|
+
const cipher = getCipherProvider(this.encryption);
|
|
5518
|
+
if (!cipher) return;
|
|
5519
|
+
const ctx = buildEncryptionContext(this.encryption, this.apiUrl, this.idKey);
|
|
5520
|
+
let decryptedRows = [];
|
|
5521
|
+
try {
|
|
5522
|
+
const decResult = await cipher.decrypt(rows, ctx);
|
|
5523
|
+
decryptedRows = Array.isArray(decResult) ? decResult : [decResult];
|
|
5524
|
+
} catch (err) {
|
|
5525
|
+
console.warn("[Katlux Providers] indexPreEncryptedRows: Decryption failed during search index build", err);
|
|
5526
|
+
return;
|
|
5527
|
+
}
|
|
5528
|
+
await Promise.all(rows.map((row, idx) => {
|
|
5529
|
+
const decRow = decryptedRows[idx];
|
|
5530
|
+
const plainFields = {};
|
|
5531
|
+
for (const f of this.encryption?.fields || Object.keys(decRow || {})) {
|
|
5532
|
+
if (decRow && decRow[f] !== void 0) {
|
|
5533
|
+
plainFields[f] = decRow[f];
|
|
5534
|
+
}
|
|
5535
|
+
}
|
|
5536
|
+
return this._searchIndex.indexRow(row, plainFields);
|
|
5537
|
+
}));
|
|
5538
|
+
}
|
|
4433
5539
|
async decryptRows(rows) {
|
|
4434
5540
|
if (!this.isEncryptionEnabled || !rows || rows.length === 0) {
|
|
4435
5541
|
return { rows, decrypted: false };
|
|
@@ -4738,6 +5844,15 @@ class CFlatClientDataProvider extends AEncryptedClientProvider {
|
|
|
4738
5844
|
this.loading = true;
|
|
4739
5845
|
try {
|
|
4740
5846
|
const rows = Array.isArray(data) ? data : [];
|
|
5847
|
+
const isPreEnc = this.encryption?.preEncrypted ?? isDataEncrypted(void 0, rows);
|
|
5848
|
+
if (isPreEnc) {
|
|
5849
|
+
this.allData = rows;
|
|
5850
|
+
if (this.searchIndex) {
|
|
5851
|
+
await this.indexPreEncryptedRows(rows);
|
|
5852
|
+
}
|
|
5853
|
+
await this.loadPageData();
|
|
5854
|
+
return;
|
|
5855
|
+
}
|
|
4741
5856
|
const encryptedRows = await this.encryptRows(rows);
|
|
4742
5857
|
this.allData = encryptedRows;
|
|
4743
5858
|
await this.loadPageData();
|
|
@@ -4758,6 +5873,16 @@ class CFlatClientDataProvider extends AEncryptedClientProvider {
|
|
|
4758
5873
|
this.loading = true;
|
|
4759
5874
|
try {
|
|
4760
5875
|
const rows = Array.isArray(data) ? data : [];
|
|
5876
|
+
const isPreEnc = this.encryption?.preEncrypted ?? isDataEncrypted(void 0, rows);
|
|
5877
|
+
if (isPreEnc) {
|
|
5878
|
+
this.allData = rows;
|
|
5879
|
+
if (this.searchIndex) {
|
|
5880
|
+
await this.indexPreEncryptedRows(rows);
|
|
5881
|
+
}
|
|
5882
|
+
options?.onProgress?.({ processed: rows.length, total: rows.length, percent: 100 });
|
|
5883
|
+
await this.loadPageData();
|
|
5884
|
+
return;
|
|
5885
|
+
}
|
|
4761
5886
|
const encryptedRows = await this.encryptRowsChunked(rows, options);
|
|
4762
5887
|
this.allData = encryptedRows;
|
|
4763
5888
|
await this.loadPageData();
|
|
@@ -6704,8 +7829,18 @@ class RequestProvider {
|
|
|
6704
7829
|
method,
|
|
6705
7830
|
isServer,
|
|
6706
7831
|
aad: encOptions.aad,
|
|
7832
|
+
aadFields: encOptions.aadFields,
|
|
7833
|
+
baseAad: encOptions.baseAad,
|
|
6707
7834
|
bindRequestContextAad: encOptions.bindRequestContextAad,
|
|
7835
|
+
strict: encOptions.strict,
|
|
7836
|
+
allowedAlgorithms: encOptions.allowedAlgorithms,
|
|
7837
|
+
allowedKids: encOptions.allowedKids,
|
|
7838
|
+
onDecryptError: encOptions.onDecryptError,
|
|
6708
7839
|
key: encOptions.key,
|
|
7840
|
+
kid: encOptions.kid,
|
|
7841
|
+
keyId: encOptions.keyId,
|
|
7842
|
+
keys: encOptions.keys,
|
|
7843
|
+
activeKid: encOptions.activeKid,
|
|
6709
7844
|
valueEncryption: encOptions.valueEncryption,
|
|
6710
7845
|
fields: encOptions.fields,
|
|
6711
7846
|
publicKey: encOptions.publicKey,
|
|
@@ -6894,17 +8029,26 @@ class CAPIFlatClientDataProvider extends CFlatClientDataProvider {
|
|
|
6894
8029
|
if (data) {
|
|
6895
8030
|
const normalized = normalizeDataResult(data);
|
|
6896
8031
|
const rows = normalized ? normalized.rows : data.rows || data;
|
|
8032
|
+
const incomingRows = Array.isArray(rows) ? rows : [];
|
|
6897
8033
|
if (this._isAccumulating) {
|
|
6898
|
-
this.allData = [...this.allData, ...
|
|
8034
|
+
this.allData = [...this.allData, ...incomingRows];
|
|
6899
8035
|
} else {
|
|
6900
|
-
this.allData =
|
|
8036
|
+
this.allData = incomingRows;
|
|
6901
8037
|
}
|
|
6902
8038
|
this._filterDirty = true;
|
|
6903
8039
|
this._sortDirty = true;
|
|
6904
8040
|
if (normalized?.rowCount !== void 0) {
|
|
6905
8041
|
this.rowCount = normalized.rowCount;
|
|
6906
8042
|
}
|
|
6907
|
-
this._lastDataReceivedPromise =
|
|
8043
|
+
this._lastDataReceivedPromise = (async () => {
|
|
8044
|
+
if (this.isEncryptionEnabled && this.searchIndex && incomingRows.length > 0) {
|
|
8045
|
+
const isPreEnc = this.encryption?.preEncrypted ?? isDataEncrypted(void 0, incomingRows);
|
|
8046
|
+
if (isPreEnc) {
|
|
8047
|
+
await this.indexPreEncryptedRows(incomingRows);
|
|
8048
|
+
}
|
|
8049
|
+
}
|
|
8050
|
+
await this.loadPageData();
|
|
8051
|
+
})();
|
|
6908
8052
|
}
|
|
6909
8053
|
});
|
|
6910
8054
|
}
|
|
@@ -6990,6 +8134,12 @@ class CAPIFlatClientDataProvider extends CFlatClientDataProvider {
|
|
|
6990
8134
|
this.allData = normalized.rows;
|
|
6991
8135
|
this._filterDirty = true;
|
|
6992
8136
|
this._sortDirty = true;
|
|
8137
|
+
if (this.isEncryptionEnabled && this.searchIndex && this.allData.length > 0) {
|
|
8138
|
+
const isPreEnc = this.encryption?.preEncrypted ?? isDataEncrypted(void 0, this.allData);
|
|
8139
|
+
if (isPreEnc) {
|
|
8140
|
+
await this.indexPreEncryptedRows(this.allData);
|
|
8141
|
+
}
|
|
8142
|
+
}
|
|
6993
8143
|
await this.loadPageData();
|
|
6994
8144
|
this.initialLoad = false;
|
|
6995
8145
|
this.loading = false;
|
|
@@ -10244,6 +11394,7 @@ exports.CTreeServerDataProvider = CTreeServerDataProvider;
|
|
|
10244
11394
|
exports.CWebCryptoAESCBCProvider = CWebCryptoAESCBCProvider;
|
|
10245
11395
|
exports.CWebCryptoEncryptionProvider = CWebCryptoEncryptionProvider;
|
|
10246
11396
|
exports.CacheProviderFactory = CacheProviderFactory;
|
|
11397
|
+
exports.DECRYPT_ERROR_SENTINEL = DECRYPT_ERROR_SENTINEL;
|
|
10247
11398
|
exports.DefaultHydrationAdapter = DefaultHydrationAdapter;
|
|
10248
11399
|
exports.ECacheStrategy = ECacheStrategy;
|
|
10249
11400
|
exports.EDataFilterOperator = EDataFilterOperator;
|
|
@@ -10256,6 +11407,7 @@ exports.RequestQueue = RequestQueue;
|
|
|
10256
11407
|
exports.SENSITIVE_HEADER_KEYS = SENSITIVE_HEADER_KEYS;
|
|
10257
11408
|
exports.applyOperator = applyOperator;
|
|
10258
11409
|
exports.buildEncryptionContext = buildEncryptionContext;
|
|
11410
|
+
exports.canonicalizeJson = canonicalizeJson;
|
|
10259
11411
|
exports.clearCachedCipherProvider = clearCachedCipherProvider;
|
|
10260
11412
|
exports.clearSSRData = clearSSRData;
|
|
10261
11413
|
exports.compareAnd = compareAnd;
|