@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.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as crypto$1 from 'node:crypto';
|
|
2
2
|
import { $fetch } from 'ofetch';
|
|
3
3
|
|
|
4
|
+
const DECRYPT_ERROR_SENTINEL = Symbol.for("katlux.decrypt_error");
|
|
5
|
+
|
|
4
6
|
var EDataFilterOperator = /* @__PURE__ */ ((EDataFilterOperator2) => {
|
|
5
7
|
EDataFilterOperator2["Equal"] = "==";
|
|
6
8
|
EDataFilterOperator2["NotEqual"] = "!=";
|
|
@@ -655,878 +657,1725 @@ function isAuthOrDecryptionError(err) {
|
|
|
655
657
|
const msg = String(err.message || "").toLowerCase();
|
|
656
658
|
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");
|
|
657
659
|
}
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
660
|
+
function canonicalizeJson(value, seen = /* @__PURE__ */ new Set()) {
|
|
661
|
+
if (value === void 0) {
|
|
662
|
+
return "";
|
|
663
|
+
}
|
|
664
|
+
if (value === null || typeof value === "boolean") {
|
|
665
|
+
return JSON.stringify(value);
|
|
666
|
+
}
|
|
667
|
+
if (typeof value === "number") {
|
|
668
|
+
if (!Number.isFinite(value)) return "null";
|
|
669
|
+
if (Object.is(value, -0)) return "0";
|
|
670
|
+
return JSON.stringify(value);
|
|
671
|
+
}
|
|
672
|
+
if (typeof value === "string") {
|
|
673
|
+
return JSON.stringify(value);
|
|
674
|
+
}
|
|
675
|
+
if (typeof value === "bigint") {
|
|
676
|
+
throw new TypeError("Do not know how to serialize a BigInt");
|
|
677
|
+
}
|
|
678
|
+
if (typeof value === "symbol" || typeof value === "function") {
|
|
679
|
+
return "";
|
|
680
|
+
}
|
|
681
|
+
if (typeof value === "object") {
|
|
682
|
+
if (typeof value.toJSON === "function") {
|
|
683
|
+
return canonicalizeJson(value.toJSON(), seen);
|
|
684
|
+
}
|
|
685
|
+
if (seen.has(value)) {
|
|
686
|
+
throw new TypeError("Converting circular structure to JSON");
|
|
687
|
+
}
|
|
688
|
+
seen.add(value);
|
|
689
|
+
try {
|
|
690
|
+
if (Array.isArray(value)) {
|
|
691
|
+
const items = value.map((item) => {
|
|
692
|
+
if (item === void 0 || typeof item === "function" || typeof item === "symbol") {
|
|
693
|
+
return "null";
|
|
694
|
+
}
|
|
695
|
+
const res = canonicalizeJson(item, seen);
|
|
696
|
+
return res === "" ? "null" : res;
|
|
697
|
+
});
|
|
698
|
+
return `[${items.join(",")}]`;
|
|
699
|
+
}
|
|
700
|
+
const keys = Object.keys(value).sort();
|
|
701
|
+
const entries = [];
|
|
702
|
+
for (const key of keys) {
|
|
703
|
+
const val = value[key];
|
|
704
|
+
if (val === void 0 || typeof val === "function" || typeof val === "symbol") {
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
const serialized = canonicalizeJson(val, seen);
|
|
708
|
+
if (serialized !== "") {
|
|
709
|
+
entries.push(`${JSON.stringify(key)}:${serialized}`);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return `{${entries.join(",")}}`;
|
|
713
|
+
} finally {
|
|
714
|
+
seen.delete(value);
|
|
664
715
|
}
|
|
665
716
|
}
|
|
717
|
+
return JSON.stringify(value);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
class CKeyManager {
|
|
721
|
+
// ── Binary & Encoding Utilities ──────────────────────────────────────────
|
|
666
722
|
/**
|
|
667
|
-
*
|
|
723
|
+
* Generates a cryptographically secure random byte buffer of the specified length.
|
|
668
724
|
*/
|
|
669
|
-
|
|
670
|
-
|
|
725
|
+
static randomBytes(length) {
|
|
726
|
+
const bytes = new Uint8Array(length);
|
|
727
|
+
return getRandomValues(bytes);
|
|
671
728
|
}
|
|
672
729
|
/**
|
|
673
|
-
*
|
|
730
|
+
* Converts an ArrayBuffer or Uint8Array into a Base64-encoded string.
|
|
674
731
|
*/
|
|
675
|
-
|
|
676
|
-
return
|
|
732
|
+
static toBase64(buffer) {
|
|
733
|
+
return toBase64(buffer);
|
|
677
734
|
}
|
|
678
735
|
/**
|
|
679
|
-
*
|
|
680
|
-
*
|
|
736
|
+
* Converts a Base64 or Base64URL-encoded string back into a Uint8Array.
|
|
737
|
+
* Normalizes URL-safe characters and padding, strictly validating against the Base64 alphabet.
|
|
681
738
|
*/
|
|
682
|
-
|
|
739
|
+
static fromBase64(base64) {
|
|
740
|
+
if (typeof base64 !== "string") {
|
|
741
|
+
throw new Error("[CKeyManager] Invalid base64 input: expected string");
|
|
742
|
+
}
|
|
743
|
+
const cleaned = base64.replace(/\s+/g, "");
|
|
744
|
+
if (!cleaned) return new Uint8Array(0);
|
|
745
|
+
const normalized = cleaned.replace(/-/g, "+").replace(/_/g, "/");
|
|
746
|
+
const padded = normalized.padEnd(normalized.length + (4 - normalized.length % 4) % 4, "=");
|
|
747
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(padded)) {
|
|
748
|
+
throw new Error("[CKeyManager] Invalid base64 string");
|
|
749
|
+
}
|
|
750
|
+
if (typeof Buffer !== "undefined") {
|
|
751
|
+
const buf = Buffer.from(padded, "base64");
|
|
752
|
+
const arrayBuf = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
753
|
+
return new Uint8Array(arrayBuf);
|
|
754
|
+
}
|
|
755
|
+
const binary = atob(padded);
|
|
756
|
+
const bytes = new Uint8Array(binary.length);
|
|
757
|
+
for (let i = 0; i < binary.length; i++) {
|
|
758
|
+
bytes[i] = binary.charCodeAt(i);
|
|
759
|
+
}
|
|
760
|
+
return bytes;
|
|
683
761
|
}
|
|
684
762
|
/**
|
|
685
|
-
*
|
|
763
|
+
* Deterministically resolves a salt from Uint8Array, ArrayBuffer, Base64/Base64URL string, or raw text string.
|
|
764
|
+
* Guarantees 100% identical salt byte resolution between Node.js and Browser environments.
|
|
686
765
|
*/
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
if (
|
|
690
|
-
|
|
691
|
-
`[${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.`
|
|
692
|
-
);
|
|
766
|
+
static resolveSalt(salt) {
|
|
767
|
+
if (!salt) return CKeyManager.randomBytes(16);
|
|
768
|
+
if (ArrayBuffer.isView(salt)) {
|
|
769
|
+
return salt.byteLength > 0 ? new Uint8Array(salt.buffer.slice(salt.byteOffset, salt.byteOffset + salt.byteLength)) : CKeyManager.randomBytes(16);
|
|
693
770
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
throw new Error(
|
|
697
|
-
`[${this.constructor.name}] No encryption key or keyProvider was supplied. Key provider resolved to empty or undefined key.`
|
|
698
|
-
);
|
|
771
|
+
if (salt instanceof ArrayBuffer) {
|
|
772
|
+
return salt.byteLength > 0 ? new Uint8Array(salt.slice(0)) : CKeyManager.randomBytes(16);
|
|
699
773
|
}
|
|
700
|
-
|
|
774
|
+
if (typeof salt === "string") {
|
|
775
|
+
const trimmed = salt.trim();
|
|
776
|
+
if (!trimmed) return CKeyManager.randomBytes(16);
|
|
777
|
+
if (trimmed.startsWith("base64:")) {
|
|
778
|
+
return CKeyManager.fromBase64(trimmed.slice(7));
|
|
779
|
+
}
|
|
780
|
+
if (trimmed.endsWith("=") && /^[A-Za-z0-9+/]+={1,2}$/.test(trimmed)) {
|
|
781
|
+
try {
|
|
782
|
+
const decoded = CKeyManager.fromBase64(trimmed);
|
|
783
|
+
if (decoded.byteLength > 0) return decoded;
|
|
784
|
+
} catch {
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
const encoded = textEncoder$1.encode(trimmed);
|
|
788
|
+
return encoded.byteLength > 0 ? encoded : CKeyManager.randomBytes(16);
|
|
789
|
+
}
|
|
790
|
+
return CKeyManager.randomBytes(16);
|
|
701
791
|
}
|
|
792
|
+
// ── Symmetric Key Management ─────────────────────────────────────────────
|
|
702
793
|
/**
|
|
703
|
-
*
|
|
794
|
+
* Generates a random AES CryptoKey suitable for AES-GCM or AES-CBC.
|
|
795
|
+
*
|
|
796
|
+
* E2EE Threat Model Note: Focuses on protecting GDPR/tenant data against database and
|
|
797
|
+
* server-side leaks. Keys remain extractable by default to enable role-based key sharing,
|
|
798
|
+
* wrapping, and transfer between authorized client sessions in multi-user SaaS apps.
|
|
799
|
+
* Client-side malware/trojan protection is out of scope.
|
|
704
800
|
*/
|
|
705
|
-
static
|
|
706
|
-
|
|
707
|
-
|
|
801
|
+
static async generateAESKey(length = 256, algorithm = "AES-GCM", extractable = true) {
|
|
802
|
+
const subtle = getSubtleCrypto();
|
|
803
|
+
const usages = algorithm === "AES-KW" ? ["wrapKey", "unwrapKey"] : ["encrypt", "decrypt"];
|
|
804
|
+
return await subtle.generateKey(
|
|
805
|
+
{ name: algorithm, length },
|
|
806
|
+
extractable,
|
|
807
|
+
usages
|
|
808
|
+
);
|
|
708
809
|
}
|
|
709
810
|
/**
|
|
710
|
-
*
|
|
811
|
+
* Exports an AES CryptoKey as a raw base64 string.
|
|
711
812
|
*/
|
|
712
|
-
|
|
713
|
-
|
|
813
|
+
static async exportAESKey(key) {
|
|
814
|
+
const subtle = getSubtleCrypto();
|
|
815
|
+
const raw = await subtle.exportKey("raw", key);
|
|
816
|
+
try {
|
|
817
|
+
return CKeyManager.toBase64(raw);
|
|
818
|
+
} finally {
|
|
819
|
+
new Uint8Array(raw).fill(0);
|
|
820
|
+
}
|
|
714
821
|
}
|
|
715
822
|
/**
|
|
716
|
-
*
|
|
823
|
+
* Imports an AES CryptoKey from a raw base64 string.
|
|
717
824
|
*/
|
|
718
|
-
|
|
719
|
-
|
|
825
|
+
static async importAESKey(base64, algorithm = "AES-GCM", extractable = false) {
|
|
826
|
+
const subtle = getSubtleCrypto();
|
|
827
|
+
const rawBytes = CKeyManager.fromBase64(base64);
|
|
828
|
+
if (rawBytes.length !== 16 && rawBytes.length !== 24 && rawBytes.length !== 32) {
|
|
829
|
+
rawBytes.fill(0);
|
|
830
|
+
throw new Error(`[CKeyManager] Invalid AES key length: expected 16, 24, or 32 bytes (128, 192, or 256 bits), got ${rawBytes.length} bytes.`);
|
|
831
|
+
}
|
|
832
|
+
const usages = algorithm === "AES-KW" ? ["wrapKey", "unwrapKey"] : ["encrypt", "decrypt"];
|
|
833
|
+
try {
|
|
834
|
+
return await subtle.importKey(
|
|
835
|
+
"raw",
|
|
836
|
+
rawBytes,
|
|
837
|
+
{ name: algorithm },
|
|
838
|
+
extractable,
|
|
839
|
+
usages
|
|
840
|
+
);
|
|
841
|
+
} finally {
|
|
842
|
+
rawBytes.fill(0);
|
|
843
|
+
}
|
|
720
844
|
}
|
|
845
|
+
// ── Asymmetric Key Pair Management ───────────────────────────────────────
|
|
721
846
|
/**
|
|
722
|
-
*
|
|
847
|
+
* Generates an RSA-OAEP public/private key pair (2048 or 4096 bits).
|
|
723
848
|
*/
|
|
724
|
-
|
|
725
|
-
|
|
849
|
+
static async generateRSAKeyPair(bits = 4096, hash = "SHA-256", extractable = true) {
|
|
850
|
+
const subtle = getSubtleCrypto();
|
|
851
|
+
const pair = await subtle.generateKey(
|
|
852
|
+
{
|
|
853
|
+
name: "RSA-OAEP",
|
|
854
|
+
modulusLength: bits,
|
|
855
|
+
publicExponent: new Uint8Array([1, 0, 1]),
|
|
856
|
+
hash
|
|
857
|
+
},
|
|
858
|
+
extractable,
|
|
859
|
+
["encrypt", "decrypt", "wrapKey", "unwrapKey"]
|
|
860
|
+
);
|
|
861
|
+
return {
|
|
862
|
+
publicKey: pair.publicKey,
|
|
863
|
+
privateKey: pair.privateKey
|
|
864
|
+
};
|
|
726
865
|
}
|
|
727
866
|
/**
|
|
728
|
-
*
|
|
867
|
+
* Generates an ECDH public/private key pair for elliptic curve Diffie-Hellman.
|
|
729
868
|
*/
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
869
|
+
static async generateECDHKeyPair(curve = "P-256", extractable = true) {
|
|
870
|
+
const subtle = getSubtleCrypto();
|
|
871
|
+
const pair = await subtle.generateKey(
|
|
872
|
+
{
|
|
873
|
+
name: "ECDH",
|
|
874
|
+
namedCurve: curve
|
|
875
|
+
},
|
|
876
|
+
extractable,
|
|
877
|
+
["deriveKey", "deriveBits"]
|
|
878
|
+
);
|
|
879
|
+
return {
|
|
880
|
+
publicKey: pair.publicKey,
|
|
881
|
+
privateKey: pair.privateKey
|
|
882
|
+
};
|
|
735
883
|
}
|
|
736
884
|
/**
|
|
737
|
-
*
|
|
738
|
-
* 1. If options.bindRequestContextAad is explicitly false, returns options.aad directly without context binding.
|
|
739
|
-
* 2. When encrypting (isDecrypt=false): binds request context `req:${method}:${url}` only if options.bindRequestContextAad === true.
|
|
740
|
-
* 3. When decrypting (isDecrypt=true): derives request context `req:${method}:${url}` if url or method is provided.
|
|
741
|
-
* 4. If explicit options.aad is provided: combines with request context if bound, or returns options.aad directly.
|
|
742
|
-
* 5. Otherwise returns undefined.
|
|
885
|
+
* Exports a public CryptoKey to a portable JsonWebKey (JWK).
|
|
743
886
|
*/
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
const hasReqContext = Boolean(options.url || options.method);
|
|
748
|
-
if (options.aad) {
|
|
749
|
-
if (options.bindRequestContextAad === true && hasReqContext) {
|
|
750
|
-
const m = (options.method || "POST").toUpperCase();
|
|
751
|
-
const u = options.url || "";
|
|
752
|
-
const baseAad = typeof options.aad === "string" ? options.aad : options.aad instanceof Uint8Array || options.aad instanceof ArrayBuffer ? textDecoder.decode(options.aad) : String(options.aad);
|
|
753
|
-
return `${baseAad}|req:${m}:${u}`;
|
|
754
|
-
}
|
|
755
|
-
return options.aad;
|
|
756
|
-
}
|
|
757
|
-
const shouldBindReq = isDecrypt ? hasReqContext : options.bindRequestContextAad === true && hasReqContext;
|
|
758
|
-
if (shouldBindReq) {
|
|
759
|
-
const m = (options.method || "POST").toUpperCase();
|
|
760
|
-
const u = options.url || "";
|
|
761
|
-
return `req:${m}:${u}`;
|
|
762
|
-
}
|
|
763
|
-
return void 0;
|
|
887
|
+
static async exportPublicKey(key) {
|
|
888
|
+
const subtle = getSubtleCrypto();
|
|
889
|
+
return await subtle.exportKey("jwk", key);
|
|
764
890
|
}
|
|
765
891
|
/**
|
|
766
|
-
*
|
|
767
|
-
* to prevent ciphertext swapping between fields or records.
|
|
892
|
+
* Exports a private CryptoKey to a portable JsonWebKey (JWK).
|
|
768
893
|
*/
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
if (typeof options.aad === "string") baseAad = options.aad;
|
|
773
|
-
else if (options.aad instanceof Uint8Array || options.aad instanceof ArrayBuffer) {
|
|
774
|
-
baseAad = textDecoder.decode(options.aad);
|
|
775
|
-
} else {
|
|
776
|
-
baseAad = String(options.aad);
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
const recordId = data && typeof data === "object" && (data.id !== void 0 || data._id !== void 0) ? String(data.id ?? data._id) : "";
|
|
780
|
-
const recordPart = recordId ? `|record:${recordId}` : "";
|
|
781
|
-
const reqPart = options?.bindRequestContextAad === true && (options?.url || options?.method) ? `|req:${(options.method || "POST").toUpperCase()}:${options.url || ""}` : "";
|
|
782
|
-
return {
|
|
783
|
-
...options,
|
|
784
|
-
aad: `aad:${baseAad}${recordPart}${reqPart}|field:${field}`
|
|
785
|
-
};
|
|
894
|
+
static async exportPrivateKey(key) {
|
|
895
|
+
const subtle = getSubtleCrypto();
|
|
896
|
+
return await subtle.exportKey("jwk", key);
|
|
786
897
|
}
|
|
787
898
|
/**
|
|
788
|
-
*
|
|
789
|
-
* Automatically performs value-level encryption:
|
|
790
|
-
* - If data is an array of objects: recursively encrypts each row.
|
|
791
|
-
* - If data is an object: encrypts specified fields (or all values if valueEncryption is true),
|
|
792
|
-
* preserving JSON object keys so backend APIs and ORMs can parse keys and perform upserts.
|
|
793
|
-
* - If data is a primitive or whole-payload envelope: encrypts as a single value.
|
|
899
|
+
* Imports a public key from a JsonWebKey (JWK).
|
|
794
900
|
*/
|
|
795
|
-
async
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
if (
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
break;
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
if (hasUnencrypted) break;
|
|
817
|
-
}
|
|
901
|
+
static async importPublicKey(jwk, algorithm) {
|
|
902
|
+
const subtle = getSubtleCrypto();
|
|
903
|
+
let importAlgorithm = algorithm;
|
|
904
|
+
let usages;
|
|
905
|
+
const isEC = jwk.kty === "EC" || typeof algorithm === "object" && algorithm?.name === "ECDH" || algorithm === "ECDH";
|
|
906
|
+
if (isEC) {
|
|
907
|
+
usages = [];
|
|
908
|
+
if (!importAlgorithm || importAlgorithm === "ECDH") {
|
|
909
|
+
importAlgorithm = { name: "ECDH", namedCurve: jwk.crv || "P-256" };
|
|
910
|
+
}
|
|
911
|
+
} else {
|
|
912
|
+
usages = ["encrypt", "wrapKey"];
|
|
913
|
+
if (!importAlgorithm || importAlgorithm === "RSA-OAEP") {
|
|
914
|
+
if (jwk.kty === "RSA") {
|
|
915
|
+
const hash = jwk.alg === "RSA-OAEP-512" ? "SHA-512" : "SHA-256";
|
|
916
|
+
importAlgorithm = { name: "RSA-OAEP", hash };
|
|
917
|
+
} else {
|
|
918
|
+
throw new Error(`[CKeyManager] Unsupported JWK kty: ${jwk.kty}`);
|
|
818
919
|
}
|
|
819
|
-
|
|
820
|
-
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
return await subtle.importKey("jwk", jwk, importAlgorithm, true, usages);
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* Imports a private key from a JsonWebKey (JWK).
|
|
926
|
+
*/
|
|
927
|
+
static async importPrivateKey(jwk, algorithm) {
|
|
928
|
+
const subtle = getSubtleCrypto();
|
|
929
|
+
let importAlgorithm = algorithm;
|
|
930
|
+
let usages;
|
|
931
|
+
const isEC = jwk.kty === "EC" || typeof algorithm === "object" && algorithm?.name === "ECDH" || algorithm === "ECDH";
|
|
932
|
+
if (isEC) {
|
|
933
|
+
usages = ["deriveKey", "deriveBits"];
|
|
934
|
+
if (!importAlgorithm || importAlgorithm === "ECDH") {
|
|
935
|
+
importAlgorithm = { name: "ECDH", namedCurve: jwk.crv || "P-256" };
|
|
936
|
+
}
|
|
937
|
+
} else {
|
|
938
|
+
usages = ["decrypt", "unwrapKey"];
|
|
939
|
+
if (!importAlgorithm || importAlgorithm === "RSA-OAEP") {
|
|
940
|
+
if (jwk.kty === "RSA") {
|
|
941
|
+
const hash = jwk.alg === "RSA-OAEP-512" ? "SHA-512" : "SHA-256";
|
|
942
|
+
importAlgorithm = { name: "RSA-OAEP", hash };
|
|
943
|
+
} else {
|
|
944
|
+
throw new Error(`[CKeyManager] Unsupported JWK kty: ${jwk.kty}`);
|
|
821
945
|
}
|
|
822
|
-
await this.warmupKey(options);
|
|
823
|
-
const itemOptions = { ...options, _warmed: true };
|
|
824
|
-
return Promise.all(data.map((item) => this.encrypt(item, itemOptions)));
|
|
825
946
|
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
947
|
+
}
|
|
948
|
+
return await subtle.importKey("jwk", jwk, importAlgorithm, true, usages);
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Imports a public key from a PEM (SPKI) or base64 SPKI string.
|
|
952
|
+
*/
|
|
953
|
+
static async importPublicKeyFromPEM(pem, algorithm = "RSA-OAEP", curve = "P-256", hash = "SHA-256") {
|
|
954
|
+
const subtle = getSubtleCrypto();
|
|
955
|
+
const cleaned = pem.replace(/-----BEGIN[ A-Z0-9_-]+-----/g, "").replace(/-----END[ A-Z0-9_-]+-----/g, "").replace(/\s+/g, "");
|
|
956
|
+
const spkiBytes = CKeyManager.fromBase64(cleaned);
|
|
957
|
+
let importAlgorithm;
|
|
958
|
+
let usages;
|
|
959
|
+
if (algorithm === "ECDH") {
|
|
960
|
+
importAlgorithm = { name: "ECDH", namedCurve: curve };
|
|
961
|
+
usages = [];
|
|
962
|
+
} else if (algorithm === "ECDSA") {
|
|
963
|
+
importAlgorithm = { name: "ECDSA", namedCurve: curve };
|
|
964
|
+
usages = ["verify"];
|
|
965
|
+
} else {
|
|
966
|
+
importAlgorithm = { name: "RSA-OAEP", hash };
|
|
967
|
+
usages = ["encrypt", "wrapKey"];
|
|
968
|
+
}
|
|
969
|
+
try {
|
|
970
|
+
return await subtle.importKey("spki", spkiBytes, importAlgorithm, true, usages);
|
|
971
|
+
} finally {
|
|
972
|
+
spkiBytes.fill(0);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Imports an ECDSA public key from PEM (SPKI) or base64 SPKI string for signature verification.
|
|
977
|
+
*/
|
|
978
|
+
static async importECDSAPublicKeyFromPEM(pem, curve = "P-256") {
|
|
979
|
+
return CKeyManager.importPublicKeyFromPEM(pem, "ECDSA", curve);
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Imports a private key from a PEM (PKCS#8) or base64 PKCS#8 string.
|
|
983
|
+
*/
|
|
984
|
+
static async importPrivateKeyFromPEM(pem, algorithm = "RSA-OAEP", curve = "P-256", hash = "SHA-256") {
|
|
985
|
+
if (/BEGIN\s+(RSA|EC)\s+PRIVATE\s+KEY/i.test(pem)) {
|
|
986
|
+
throw new Error(
|
|
987
|
+
`[CKeyManager] Failed to import private key. The provided PEM appears to be in PKCS#1 or SEC1 format. Web Crypto requires PKCS#8 format. Convert your key using: openssl pkcs8 -topk8 -nocrypt -in private.pem -out private_pkcs8.pem.`
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
const subtle = getSubtleCrypto();
|
|
991
|
+
const cleaned = pem.replace(/-----BEGIN[ A-Z0-9_-]+-----/g, "").replace(/-----END[ A-Z0-9_-]+-----/g, "").replace(/\s+/g, "");
|
|
992
|
+
const pkcs8Bytes = CKeyManager.fromBase64(cleaned);
|
|
993
|
+
let importAlgorithm;
|
|
994
|
+
let usages;
|
|
995
|
+
if (algorithm === "ECDH") {
|
|
996
|
+
importAlgorithm = { name: "ECDH", namedCurve: curve };
|
|
997
|
+
usages = ["deriveKey", "deriveBits"];
|
|
998
|
+
} else if (algorithm === "ECDSA") {
|
|
999
|
+
importAlgorithm = { name: "ECDSA", namedCurve: curve };
|
|
1000
|
+
usages = ["sign"];
|
|
1001
|
+
} else {
|
|
1002
|
+
importAlgorithm = { name: "RSA-OAEP", hash };
|
|
1003
|
+
usages = ["decrypt", "unwrapKey"];
|
|
1004
|
+
}
|
|
1005
|
+
try {
|
|
1006
|
+
return await subtle.importKey("pkcs8", pkcs8Bytes, importAlgorithm, true, usages);
|
|
1007
|
+
} finally {
|
|
1008
|
+
pkcs8Bytes.fill(0);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Imports an ECDSA private key from PEM (PKCS#8) or base64 PKCS#8 string for signing.
|
|
1013
|
+
*/
|
|
1014
|
+
static async importECDSAPrivateKeyFromPEM(pem, curve = "P-256") {
|
|
1015
|
+
return CKeyManager.importPrivateKeyFromPEM(pem, "ECDSA", curve);
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Exports a public CryptoKey to a standard PEM (SPKI) string.
|
|
1019
|
+
*/
|
|
1020
|
+
static async exportPublicKeyToPEM(key) {
|
|
1021
|
+
const subtle = getSubtleCrypto();
|
|
1022
|
+
const spki = await subtle.exportKey("spki", key);
|
|
1023
|
+
let b64;
|
|
1024
|
+
try {
|
|
1025
|
+
b64 = CKeyManager.toBase64(spki);
|
|
1026
|
+
} finally {
|
|
1027
|
+
new Uint8Array(spki).fill(0);
|
|
1028
|
+
}
|
|
1029
|
+
const lines = b64.match(/.{1,64}/g) || [b64];
|
|
1030
|
+
return `-----BEGIN PUBLIC KEY-----
|
|
1031
|
+
${lines.join("\n")}
|
|
1032
|
+
-----END PUBLIC KEY-----`;
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Exports a private CryptoKey to a standard PEM (PKCS#8) string.
|
|
1036
|
+
*/
|
|
1037
|
+
static async exportPrivateKeyToPEM(key) {
|
|
1038
|
+
const subtle = getSubtleCrypto();
|
|
1039
|
+
const pkcs8 = await subtle.exportKey("pkcs8", key);
|
|
1040
|
+
let b64;
|
|
1041
|
+
try {
|
|
1042
|
+
b64 = CKeyManager.toBase64(pkcs8);
|
|
1043
|
+
} finally {
|
|
1044
|
+
new Uint8Array(pkcs8).fill(0);
|
|
1045
|
+
}
|
|
1046
|
+
const lines = b64.match(/.{1,64}/g) || [b64];
|
|
1047
|
+
return `-----BEGIN PRIVATE KEY-----
|
|
1048
|
+
${lines.join("\n")}
|
|
1049
|
+
-----END PRIVATE KEY-----`;
|
|
1050
|
+
}
|
|
1051
|
+
// ── Key Wrapping & Unwrapping ────────────────────────────────────────────
|
|
1052
|
+
/**
|
|
1053
|
+
* Wraps a target CryptoKey (e.g. an Account Symmetric Key) using a wrapping key
|
|
1054
|
+
* (AES-KW or RSA-OAEP).
|
|
1055
|
+
*/
|
|
1056
|
+
static async wrapKey(keyToWrap, wrappingKey, format = "raw") {
|
|
1057
|
+
const subtle = getSubtleCrypto();
|
|
1058
|
+
let wrapAlgorithm;
|
|
1059
|
+
if (wrappingKey.algorithm.name === "RSA-OAEP") {
|
|
1060
|
+
wrapAlgorithm = { name: "RSA-OAEP" };
|
|
1061
|
+
} else if (wrappingKey.algorithm.name === "AES-KW") {
|
|
1062
|
+
wrapAlgorithm = "AES-KW";
|
|
1063
|
+
} else {
|
|
1064
|
+
wrapAlgorithm = "AES-KW";
|
|
1065
|
+
}
|
|
1066
|
+
const wrappedBuffer = await subtle.wrapKey(format, keyToWrap, wrappingKey, wrapAlgorithm);
|
|
1067
|
+
return CKeyManager.toBase64(wrappedBuffer);
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* Unwraps a base64-encoded wrapped key using an unwrapping key (AES-KW or RSA-OAEP).
|
|
1071
|
+
*/
|
|
1072
|
+
static async unwrapKey(wrappedBase64, unwrappingKey, unwrappedKeyAlgorithm = { name: "AES-GCM", length: 256 }, extractable = true, keyUsages = ["encrypt", "decrypt"], format = "raw") {
|
|
1073
|
+
const subtle = getSubtleCrypto();
|
|
1074
|
+
const wrappedBytes = CKeyManager.fromBase64(wrappedBase64);
|
|
1075
|
+
let unwrapAlgorithm;
|
|
1076
|
+
if (unwrappingKey.algorithm.name === "RSA-OAEP") {
|
|
1077
|
+
unwrapAlgorithm = { name: "RSA-OAEP" };
|
|
1078
|
+
} else {
|
|
1079
|
+
unwrapAlgorithm = "AES-KW";
|
|
1080
|
+
}
|
|
1081
|
+
try {
|
|
1082
|
+
return await subtle.unwrapKey(
|
|
1083
|
+
format,
|
|
1084
|
+
wrappedBytes,
|
|
1085
|
+
unwrappingKey,
|
|
1086
|
+
unwrapAlgorithm,
|
|
1087
|
+
unwrappedKeyAlgorithm,
|
|
1088
|
+
extractable,
|
|
1089
|
+
keyUsages
|
|
1090
|
+
);
|
|
1091
|
+
} finally {
|
|
1092
|
+
wrappedBytes.fill(0);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* Generates a high-entropy symmetric AES-GCM Account Data Encryption Key (ADEK).
|
|
1097
|
+
* Designed for multi-user table and field-level encryption.
|
|
1098
|
+
*/
|
|
1099
|
+
static async generateAccountDataKey(length = 256, extractable = false) {
|
|
1100
|
+
const subtle = getSubtleCrypto();
|
|
1101
|
+
return await subtle.generateKey(
|
|
1102
|
+
{ name: "AES-GCM", length },
|
|
1103
|
+
extractable,
|
|
1104
|
+
["encrypt", "decrypt"]
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Wraps an Account Data Encryption Key (ADEK) for a specific user using the user's public key (RSA-OAEP).
|
|
1109
|
+
* Returns a Base64-encoded wrapped key string suitable for storage in role-map / access tables.
|
|
1110
|
+
*/
|
|
1111
|
+
static async wrapAccountKeyForUser(adek, userPublicKey) {
|
|
1112
|
+
const subtle = getSubtleCrypto();
|
|
1113
|
+
if (userPublicKey.algorithm.name === "RSA-OAEP") {
|
|
1114
|
+
const rawKey = await subtle.exportKey("raw", adek);
|
|
1115
|
+
try {
|
|
1116
|
+
const wrappedBuffer = await subtle.encrypt(
|
|
1117
|
+
{ name: "RSA-OAEP" },
|
|
1118
|
+
userPublicKey,
|
|
1119
|
+
rawKey
|
|
1120
|
+
);
|
|
1121
|
+
return CKeyManager.toBase64(wrappedBuffer);
|
|
1122
|
+
} finally {
|
|
1123
|
+
new Uint8Array(rawKey).fill(0);
|
|
830
1124
|
}
|
|
831
|
-
return this.encryptValue(data, options);
|
|
832
1125
|
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
1126
|
+
throw new Error(
|
|
1127
|
+
`[CKeyManager] Unsupported user key algorithm for wrapAccountKeyForUser: '${userPublicKey.algorithm.name}'. Expected 'RSA-OAEP'.`
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Unwraps a Base64-encoded Account Data Encryption Key (ADEK) using the user's private key (RSA-OAEP).
|
|
1132
|
+
* Returns the restored AES-GCM CryptoKey ready for use in CWebCryptoEncryptionProvider.
|
|
1133
|
+
*/
|
|
1134
|
+
static async unwrapAccountKeyForUser(wrappedKeyBase64, userPrivateKey, unwrappedKeyAlgorithm = { name: "AES-GCM", length: 256 }, extractable = false) {
|
|
1135
|
+
const subtle = getSubtleCrypto();
|
|
1136
|
+
if (userPrivateKey.algorithm.name === "RSA-OAEP") {
|
|
1137
|
+
const wrappedBytes = CKeyManager.fromBase64(wrappedKeyBase64);
|
|
1138
|
+
let unwrappedRawKey;
|
|
1139
|
+
try {
|
|
1140
|
+
unwrappedRawKey = await subtle.decrypt(
|
|
1141
|
+
{ name: "RSA-OAEP" },
|
|
1142
|
+
userPrivateKey,
|
|
1143
|
+
wrappedBytes
|
|
837
1144
|
);
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
return [field, await this.encryptValue(result[field], fieldOptions)];
|
|
849
|
-
})
|
|
1145
|
+
} finally {
|
|
1146
|
+
wrappedBytes.fill(0);
|
|
1147
|
+
}
|
|
1148
|
+
try {
|
|
1149
|
+
return await subtle.importKey(
|
|
1150
|
+
"raw",
|
|
1151
|
+
unwrappedRawKey,
|
|
1152
|
+
unwrappedKeyAlgorithm,
|
|
1153
|
+
extractable,
|
|
1154
|
+
["encrypt", "decrypt"]
|
|
850
1155
|
);
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
1156
|
+
} finally {
|
|
1157
|
+
new Uint8Array(unwrappedRawKey).fill(0);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
throw new Error(
|
|
1161
|
+
`[CKeyManager] Unsupported user key algorithm for unwrapAccountKeyForUser: '${userPrivateKey.algorithm.name}'. Expected 'RSA-OAEP'.`
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
/**
|
|
1165
|
+
* Wraps an HMAC blind indexing key for a specific recipient user using their RSA-OAEP public key.
|
|
1166
|
+
* Returns a Base64-encoded wrapped key string suitable for storage in role-map / access tables.
|
|
1167
|
+
*/
|
|
1168
|
+
static async wrapHMACKeyForUser(hmacKey, userPublicKey) {
|
|
1169
|
+
const subtle = getSubtleCrypto();
|
|
1170
|
+
if (userPublicKey.algorithm.name === "RSA-OAEP") {
|
|
1171
|
+
const rawKey = await subtle.exportKey("raw", hmacKey);
|
|
1172
|
+
try {
|
|
1173
|
+
const wrappedBuffer = await subtle.encrypt(
|
|
1174
|
+
{ name: "RSA-OAEP" },
|
|
1175
|
+
userPublicKey,
|
|
1176
|
+
rawKey
|
|
1177
|
+
);
|
|
1178
|
+
return CKeyManager.toBase64(wrappedBuffer);
|
|
1179
|
+
} finally {
|
|
1180
|
+
new Uint8Array(rawKey).fill(0);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
throw new Error(
|
|
1184
|
+
`[CKeyManager] Unsupported user key algorithm for wrapHMACKeyForUser: '${userPublicKey.algorithm.name}'. Expected 'RSA-OAEP'.`
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* Unwraps a Base64-encoded HMAC key using a recipient's RSA-OAEP private key.
|
|
1189
|
+
* Restores the CryptoKey configured for HMAC signing and verification in search indexing.
|
|
1190
|
+
*/
|
|
1191
|
+
static async unwrapHMACKeyForUser(wrappedKeyBase64, userPrivateKey, hash = "SHA-256", extractable = false) {
|
|
1192
|
+
const subtle = getSubtleCrypto();
|
|
1193
|
+
if (userPrivateKey.algorithm.name === "RSA-OAEP") {
|
|
1194
|
+
const wrappedBytes = CKeyManager.fromBase64(wrappedKeyBase64);
|
|
1195
|
+
let unwrappedRawKey;
|
|
1196
|
+
try {
|
|
1197
|
+
unwrappedRawKey = await subtle.decrypt(
|
|
1198
|
+
{ name: "RSA-OAEP" },
|
|
1199
|
+
userPrivateKey,
|
|
1200
|
+
wrappedBytes
|
|
1201
|
+
);
|
|
1202
|
+
} finally {
|
|
1203
|
+
wrappedBytes.fill(0);
|
|
1204
|
+
}
|
|
1205
|
+
try {
|
|
1206
|
+
return await subtle.importKey(
|
|
1207
|
+
"raw",
|
|
1208
|
+
unwrappedRawKey,
|
|
1209
|
+
{ name: "HMAC", hash: { name: hash } },
|
|
1210
|
+
extractable,
|
|
1211
|
+
["sign", "verify"]
|
|
1212
|
+
);
|
|
1213
|
+
} finally {
|
|
1214
|
+
new Uint8Array(unwrappedRawKey).fill(0);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
throw new Error(
|
|
1218
|
+
`[CKeyManager] Unsupported user key algorithm for unwrapHMACKeyForUser: '${userPrivateKey.algorithm.name}'. Expected 'RSA-OAEP'.`
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* Atomically re-wraps an Account Data Encryption Key (ADEK) for a new recipient
|
|
1223
|
+
* without exposing the raw extractable symmetric key to caller application code.
|
|
1224
|
+
*
|
|
1225
|
+
* @param wrappedKeyBase64 - The existing ADEK envelope wrapped with current user's public key
|
|
1226
|
+
* @param myPrivateKey - Current user's RSA-OAEP private key
|
|
1227
|
+
* @param recipientPublicKey - New recipient's RSA-OAEP public key
|
|
1228
|
+
* @returns Base64-encoded wrapped key envelope for the recipient
|
|
1229
|
+
*/
|
|
1230
|
+
static async rewrapAccountKeyForUser(wrappedKeyBase64, myPrivateKey, recipientPublicKey) {
|
|
1231
|
+
const subtle = getSubtleCrypto();
|
|
1232
|
+
if (!myPrivateKey || !myPrivateKey.algorithm || myPrivateKey.algorithm.name !== "RSA-OAEP" || !recipientPublicKey || !recipientPublicKey.algorithm || recipientPublicKey.algorithm.name !== "RSA-OAEP") {
|
|
1233
|
+
throw new Error(`[CKeyManager] rewrapAccountKeyForUser requires RSA-OAEP keys.`);
|
|
1234
|
+
}
|
|
1235
|
+
if (myPrivateKey.type === "public" || recipientPublicKey.type === "private") {
|
|
1236
|
+
throw new Error(
|
|
1237
|
+
`[CKeyManager] rewrapAccountKeyForUser requires an RSA-OAEP private key for decryption and an RSA-OAEP public key for encryption.`
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1240
|
+
const wrappedBytes = CKeyManager.fromBase64(wrappedKeyBase64);
|
|
1241
|
+
let rawKeyBuffer;
|
|
1242
|
+
try {
|
|
1243
|
+
rawKeyBuffer = await subtle.decrypt(
|
|
1244
|
+
{ name: "RSA-OAEP" },
|
|
1245
|
+
myPrivateKey,
|
|
1246
|
+
wrappedBytes
|
|
1247
|
+
);
|
|
1248
|
+
const rewrappedBuffer = await subtle.encrypt(
|
|
1249
|
+
{ name: "RSA-OAEP" },
|
|
1250
|
+
recipientPublicKey,
|
|
1251
|
+
rawKeyBuffer
|
|
1252
|
+
);
|
|
1253
|
+
return CKeyManager.toBase64(rewrappedBuffer);
|
|
1254
|
+
} finally {
|
|
1255
|
+
wrappedBytes.fill(0);
|
|
1256
|
+
if (rawKeyBuffer) {
|
|
1257
|
+
new Uint8Array(rawKeyBuffer).fill(0);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
// ── Key Derivation Functions (PBKDF2 & HKDF) ──────────────────────────────
|
|
1262
|
+
/**
|
|
1263
|
+
* Derives a symmetric AES-GCM CryptoKey from a passphrase using PBKDF2 or HKDF.
|
|
1264
|
+
* Returns the derived CryptoKey and the base64-encoded salt.
|
|
1265
|
+
*/
|
|
1266
|
+
static async deriveKey(passphrase, options) {
|
|
1267
|
+
if (!passphrase || typeof passphrase === "string" && passphrase.trim().length === 0 || typeof passphrase === "object" && "byteLength" in passphrase && passphrase.byteLength === 0) {
|
|
1268
|
+
throw new Error("[CKeyManager] Passphrase cannot be empty or undefined for key derivation.");
|
|
1269
|
+
}
|
|
1270
|
+
const subtle = getSubtleCrypto();
|
|
1271
|
+
const saltBytes = CKeyManager.resolveSalt(options.salt);
|
|
1272
|
+
const keyLength = options.keyLength || 256;
|
|
1273
|
+
const hash = options.hash || "SHA-256";
|
|
1274
|
+
const extractable = options.extractable === true;
|
|
1275
|
+
const rawKeyBytes = typeof passphrase === "string" ? textEncoder$1.encode(passphrase) : passphrase instanceof Uint8Array ? passphrase : new Uint8Array(passphrase);
|
|
1276
|
+
try {
|
|
1277
|
+
if (options.kdf === "pbkdf2") {
|
|
1278
|
+
const baseKey = await subtle.importKey(
|
|
1279
|
+
"raw",
|
|
1280
|
+
rawKeyBytes,
|
|
1281
|
+
"PBKDF2",
|
|
1282
|
+
false,
|
|
1283
|
+
["deriveKey"]
|
|
1284
|
+
);
|
|
1285
|
+
const derivedKey = await subtle.deriveKey(
|
|
1286
|
+
{
|
|
1287
|
+
name: "PBKDF2",
|
|
1288
|
+
salt: saltBytes,
|
|
1289
|
+
iterations: options.iterations || 31e4,
|
|
1290
|
+
hash
|
|
1291
|
+
},
|
|
1292
|
+
baseKey,
|
|
1293
|
+
{ name: "AES-GCM", length: keyLength },
|
|
1294
|
+
extractable,
|
|
1295
|
+
["encrypt", "decrypt"]
|
|
1296
|
+
);
|
|
1297
|
+
return {
|
|
1298
|
+
key: derivedKey,
|
|
1299
|
+
salt: CKeyManager.toBase64(saltBytes)
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
if (options.kdf === "hkdf") {
|
|
1303
|
+
const baseKey = await subtle.importKey(
|
|
1304
|
+
"raw",
|
|
1305
|
+
rawKeyBytes,
|
|
1306
|
+
"HKDF",
|
|
1307
|
+
false,
|
|
1308
|
+
["deriveKey"]
|
|
1309
|
+
);
|
|
1310
|
+
const infoBytes = resolveInfoBytes(options.info, "katlux-derived-aes-key");
|
|
1311
|
+
const derivedKey = await subtle.deriveKey(
|
|
1312
|
+
{
|
|
1313
|
+
name: "HKDF",
|
|
1314
|
+
salt: saltBytes,
|
|
1315
|
+
info: infoBytes,
|
|
1316
|
+
hash
|
|
1317
|
+
},
|
|
1318
|
+
baseKey,
|
|
1319
|
+
{ name: "AES-GCM", length: keyLength },
|
|
1320
|
+
extractable,
|
|
1321
|
+
["encrypt", "decrypt"]
|
|
1322
|
+
);
|
|
1323
|
+
return {
|
|
1324
|
+
key: derivedKey,
|
|
1325
|
+
salt: CKeyManager.toBase64(saltBytes)
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
throw new Error(`[CKeyManager] Unsupported KDF strategy: ${options.kdf}`);
|
|
1329
|
+
} finally {
|
|
1330
|
+
if (typeof passphrase === "string") {
|
|
1331
|
+
rawKeyBytes.fill(0);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* Derives an AES-GCM CryptoKey from a high-entropy secret (e.g. an ECDH shared secret key)
|
|
1337
|
+
* using HKDF.
|
|
1338
|
+
*/
|
|
1339
|
+
static async deriveKeyFromSecret(secretKey, options = {}) {
|
|
1340
|
+
const subtle = getSubtleCrypto();
|
|
1341
|
+
let saltBytes;
|
|
1342
|
+
if (options.salt !== void 0) {
|
|
1343
|
+
saltBytes = CKeyManager.resolveSalt(options.salt);
|
|
1344
|
+
} else {
|
|
1345
|
+
saltBytes = new Uint8Array(16);
|
|
1346
|
+
}
|
|
1347
|
+
const infoBytes = resolveInfoBytes(options.info, "katlux-ecdh-aes-gcm");
|
|
1348
|
+
const hash = options.hash || "SHA-256";
|
|
1349
|
+
const keyLength = options.keyLength || 256;
|
|
1350
|
+
const extractable = options.extractable === true;
|
|
1351
|
+
return await subtle.deriveKey(
|
|
1352
|
+
{
|
|
1353
|
+
name: "HKDF",
|
|
1354
|
+
salt: saltBytes,
|
|
1355
|
+
info: infoBytes,
|
|
1356
|
+
hash
|
|
1357
|
+
},
|
|
1358
|
+
secretKey,
|
|
1359
|
+
{ name: "AES-GCM", length: keyLength },
|
|
1360
|
+
extractable,
|
|
1361
|
+
["encrypt", "decrypt"]
|
|
1362
|
+
);
|
|
1363
|
+
}
|
|
1364
|
+
/**
|
|
1365
|
+
* Derives two domain-separated, cryptographically isolated keys from a user password and salt:
|
|
1366
|
+
* 1. `authKey`: For client authentication, SRP, or server challenge proofs.
|
|
1367
|
+
* 2. `encryptionKey`: For AES-GCM local and field-level data encryption.
|
|
1368
|
+
*
|
|
1369
|
+
* Stage 1 (Password Stretching): PBKDF2-HMAC-SHA256 stretches low-entropy password into master secret.
|
|
1370
|
+
* Stage 2 (Domain Expansion): HKDF-Expand derives subkeys using domain-separated context info tags.
|
|
1371
|
+
* Immediate memory zeroization of intermediate master secret buffer is guaranteed.
|
|
1372
|
+
*
|
|
1373
|
+
* @param password - Master user password or passphrase
|
|
1374
|
+
* @param salt - Salt (or omitted to generate a secure random 16-byte salt)
|
|
1375
|
+
* @param options - PBKDF2 and HKDF configuration options
|
|
1376
|
+
*/
|
|
1377
|
+
static async deriveAuthAndEncryptionKeys(password, salt, options = {}) {
|
|
1378
|
+
if (!password || typeof password === "string" && password.trim().length === 0 || typeof password === "object" && "byteLength" in password && password.byteLength === 0) {
|
|
1379
|
+
throw new Error("[CKeyManager] Password cannot be empty or undefined for key derivation.");
|
|
1380
|
+
}
|
|
1381
|
+
const subtle = getSubtleCrypto();
|
|
1382
|
+
let saltBytes;
|
|
1383
|
+
if (salt !== void 0) {
|
|
1384
|
+
saltBytes = CKeyManager.resolveSalt(salt);
|
|
1385
|
+
} else {
|
|
1386
|
+
saltBytes = CKeyManager.randomBytes(16);
|
|
1387
|
+
}
|
|
1388
|
+
const saltBase64 = CKeyManager.toBase64(saltBytes);
|
|
1389
|
+
const iterations = options.iterations || 31e4;
|
|
1390
|
+
const hash = options.hash || "SHA-256";
|
|
1391
|
+
const authKeyLength = options.authKeyLength || 256;
|
|
1392
|
+
const encryptionKeyLength = options.encryptionKeyLength || 256;
|
|
1393
|
+
const authAlgorithm = options.authAlgorithm || "HMAC";
|
|
1394
|
+
const authInfo = resolveInfoBytes(options.authInfo, "katlux-auth-key-v1");
|
|
1395
|
+
const encryptionInfo = resolveInfoBytes(options.encryptionInfo, "katlux-encryption-key-v1");
|
|
1396
|
+
const extractable = options.extractable === true;
|
|
1397
|
+
const rawPasswordBytes = typeof password === "string" ? textEncoder$1.encode(password) : password instanceof Uint8Array ? new Uint8Array(password.buffer, password.byteOffset, password.byteLength) : new Uint8Array(password);
|
|
1398
|
+
let masterBits;
|
|
1399
|
+
try {
|
|
1400
|
+
const baseKey = await subtle.importKey(
|
|
1401
|
+
"raw",
|
|
1402
|
+
rawPasswordBytes,
|
|
1403
|
+
"PBKDF2",
|
|
1404
|
+
false,
|
|
1405
|
+
["deriveBits"]
|
|
1406
|
+
);
|
|
1407
|
+
const bitsLength = hash === "SHA-512" ? 512 : 256;
|
|
1408
|
+
masterBits = await subtle.deriveBits(
|
|
1409
|
+
{
|
|
1410
|
+
name: "PBKDF2",
|
|
1411
|
+
salt: saltBytes,
|
|
1412
|
+
iterations,
|
|
1413
|
+
hash
|
|
1414
|
+
},
|
|
1415
|
+
baseKey,
|
|
1416
|
+
bitsLength
|
|
1417
|
+
);
|
|
1418
|
+
const hkdfKey = await subtle.importKey(
|
|
1419
|
+
"raw",
|
|
1420
|
+
masterBits,
|
|
1421
|
+
"HKDF",
|
|
1422
|
+
false,
|
|
1423
|
+
["deriveKey"]
|
|
1424
|
+
);
|
|
1425
|
+
const authDerivedAlgorithm = authAlgorithm === "AES-GCM" ? { name: "AES-GCM", length: authKeyLength } : { name: "HMAC", hash: { name: hash }, length: authKeyLength };
|
|
1426
|
+
const authUsages = authAlgorithm === "AES-GCM" ? ["encrypt", "decrypt"] : ["sign", "verify"];
|
|
1427
|
+
const authKey = await subtle.deriveKey(
|
|
1428
|
+
{
|
|
1429
|
+
name: "HKDF",
|
|
1430
|
+
hash,
|
|
1431
|
+
salt: new Uint8Array(0),
|
|
1432
|
+
info: authInfo
|
|
1433
|
+
},
|
|
1434
|
+
hkdfKey,
|
|
1435
|
+
authDerivedAlgorithm,
|
|
1436
|
+
extractable,
|
|
1437
|
+
authUsages
|
|
1438
|
+
);
|
|
1439
|
+
const encryptionKey = await subtle.deriveKey(
|
|
1440
|
+
{
|
|
1441
|
+
name: "HKDF",
|
|
1442
|
+
hash,
|
|
1443
|
+
salt: new Uint8Array(0),
|
|
1444
|
+
info: encryptionInfo
|
|
1445
|
+
},
|
|
1446
|
+
hkdfKey,
|
|
1447
|
+
{ name: "AES-GCM", length: encryptionKeyLength },
|
|
1448
|
+
extractable,
|
|
1449
|
+
["encrypt", "decrypt"]
|
|
1450
|
+
);
|
|
1451
|
+
return {
|
|
1452
|
+
authKey,
|
|
1453
|
+
encryptionKey,
|
|
1454
|
+
salt: saltBase64
|
|
1455
|
+
};
|
|
1456
|
+
} finally {
|
|
1457
|
+
if (typeof password === "string") {
|
|
1458
|
+
rawPasswordBytes.fill(0);
|
|
855
1459
|
}
|
|
856
|
-
if (
|
|
857
|
-
|
|
858
|
-
const keysToEncrypt = Object.keys(result).filter(
|
|
859
|
-
(key) => result[key] !== void 0 && result[key] !== null && !this.isEncrypted(result[key])
|
|
860
|
-
);
|
|
861
|
-
if (keysToEncrypt.length === 0) {
|
|
862
|
-
return result;
|
|
863
|
-
}
|
|
864
|
-
if (!options?._warmed) {
|
|
865
|
-
await this.warmupKey(options);
|
|
866
|
-
}
|
|
867
|
-
const entries = await Promise.all(
|
|
868
|
-
keysToEncrypt.map(async (key) => {
|
|
869
|
-
const fieldOptions = this._bindFieldAad(data, key, options);
|
|
870
|
-
return [key, await this.encryptValue(result[key], fieldOptions)];
|
|
871
|
-
})
|
|
872
|
-
);
|
|
873
|
-
for (const [key, encryptedVal] of entries) {
|
|
874
|
-
safeSetProperty(result, key, encryptedVal);
|
|
875
|
-
}
|
|
876
|
-
return result;
|
|
1460
|
+
if (masterBits) {
|
|
1461
|
+
new Uint8Array(masterBits).fill(0);
|
|
877
1462
|
}
|
|
878
1463
|
}
|
|
879
|
-
return this.encryptValue(data, options);
|
|
880
1464
|
}
|
|
1465
|
+
// ── Digital Signatures, Fingerprints & Access Grants ─────────────────────
|
|
881
1466
|
/**
|
|
882
|
-
*
|
|
883
|
-
* Automatically performs value-level decryption:
|
|
884
|
-
* - If payload is an encrypted envelope: decrypts directly via decryptValue.
|
|
885
|
-
* - If payload is an array of objects: decrypts each row.
|
|
886
|
-
* - If payload is an object: decrypts any encrypted value cells across fields.
|
|
1467
|
+
* Generates an ECDSA P-256 signing key pair.
|
|
887
1468
|
*/
|
|
1469
|
+
static async generateSigningKeyPair(curve = "P-256", extractable = true) {
|
|
1470
|
+
const subtle = getSubtleCrypto();
|
|
1471
|
+
const pair = await subtle.generateKey(
|
|
1472
|
+
{
|
|
1473
|
+
name: "ECDSA",
|
|
1474
|
+
namedCurve: curve
|
|
1475
|
+
},
|
|
1476
|
+
extractable,
|
|
1477
|
+
["sign", "verify"]
|
|
1478
|
+
);
|
|
1479
|
+
return {
|
|
1480
|
+
publicKey: pair.publicKey,
|
|
1481
|
+
privateKey: pair.privateKey
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
888
1484
|
/**
|
|
889
|
-
*
|
|
890
|
-
* Automatically performs value-level decryption:
|
|
891
|
-
* - If payload is an encrypted envelope: decrypts directly via decryptValue.
|
|
892
|
-
* - If payload is an array of objects: decrypts each row (batched with local memoization).
|
|
893
|
-
* - If payload is an object: decrypts any encrypted value cells across fields.
|
|
1485
|
+
* Generates an ECDSA signing key pair. Alias for generateSigningKeyPair.
|
|
894
1486
|
*/
|
|
895
|
-
async
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
const result = proto && proto !== Object.prototype ? Object.assign(Object.create(proto), item) : { ...item };
|
|
923
|
-
await Promise.all(
|
|
924
|
-
keysToDecrypt.map(async (key) => {
|
|
925
|
-
const env = item[key];
|
|
926
|
-
const boundOptions = this._bindFieldAad(item, key, options);
|
|
927
|
-
const hasBoundAad = boundOptions.aad !== options?.aad;
|
|
928
|
-
const ct = env.ciphertext;
|
|
929
|
-
const iv = env.iv || "";
|
|
930
|
-
const aadPart = boundOptions.aad ? typeof boundOptions.aad === "string" ? boundOptions.aad : "" : "";
|
|
931
|
-
const memoKey = `${ct}:${iv}:${aadPart}`;
|
|
932
|
-
if (!localMemo.has(memoKey)) {
|
|
933
|
-
const decryptPromise = this.decryptValue(env, boundOptions).catch((err) => {
|
|
934
|
-
if (hasBoundAad && isAuthOrDecryptionError(err)) {
|
|
935
|
-
return this.decryptValue(env, options);
|
|
936
|
-
}
|
|
937
|
-
throw err;
|
|
938
|
-
});
|
|
939
|
-
localMemo.set(memoKey, decryptPromise);
|
|
940
|
-
}
|
|
941
|
-
const decryptedVal = await localMemo.get(memoKey);
|
|
942
|
-
safeSetProperty(result, key, decryptedVal);
|
|
943
|
-
})
|
|
944
|
-
);
|
|
945
|
-
return result;
|
|
946
|
-
}
|
|
947
|
-
return item;
|
|
948
|
-
};
|
|
949
|
-
if (payload.length <= 100) {
|
|
950
|
-
return Promise.all(payload.map(decryptRow));
|
|
951
|
-
}
|
|
952
|
-
const batchSize = 1e3;
|
|
953
|
-
const results = new Array(payload.length);
|
|
954
|
-
for (let i = 0; i < payload.length; i += batchSize) {
|
|
955
|
-
const end = Math.min(i + batchSize, payload.length);
|
|
956
|
-
const chunk = payload.slice(i, end);
|
|
957
|
-
const chunkRes = await Promise.all(chunk.map(decryptRow));
|
|
958
|
-
for (let j = 0; j < chunkRes.length; j++) {
|
|
959
|
-
results[i + j] = chunkRes[j];
|
|
960
|
-
}
|
|
961
|
-
if (end < payload.length && (i > 0 && i % 5e3 === 0)) {
|
|
962
|
-
await new Promise((r) => setTimeout(r, 0));
|
|
1487
|
+
static async generateECDSAKeyPair(curve = "P-256", extractable = true) {
|
|
1488
|
+
return CKeyManager.generateSigningKeyPair(curve, extractable);
|
|
1489
|
+
}
|
|
1490
|
+
/**
|
|
1491
|
+
* Generates a deterministic SHA-256 fingerprint for a public key (SPKI or raw).
|
|
1492
|
+
* Formatted as `SHA256:<base64url>`.
|
|
1493
|
+
*/
|
|
1494
|
+
static async getKeyFingerprint(publicKey) {
|
|
1495
|
+
const subtle = getSubtleCrypto();
|
|
1496
|
+
let rawBytes;
|
|
1497
|
+
if (typeof publicKey === "object" && publicKey !== null && "algorithm" in publicKey) {
|
|
1498
|
+
const cryptoKey = publicKey;
|
|
1499
|
+
rawBytes = await subtle.exportKey("spki", cryptoKey);
|
|
1500
|
+
} else if (publicKey instanceof Uint8Array) {
|
|
1501
|
+
rawBytes = publicKey;
|
|
1502
|
+
} else if (publicKey instanceof ArrayBuffer) {
|
|
1503
|
+
rawBytes = publicKey;
|
|
1504
|
+
} else if (typeof publicKey === "string") {
|
|
1505
|
+
const trimmed = publicKey.trim();
|
|
1506
|
+
if (trimmed.startsWith("-----BEGIN")) {
|
|
1507
|
+
const b642 = trimmed.replace(/-----[^-]+-----/g, "").replace(/\s+/g, "");
|
|
1508
|
+
rawBytes = CKeyManager.fromBase64(b642);
|
|
1509
|
+
} else {
|
|
1510
|
+
try {
|
|
1511
|
+
rawBytes = CKeyManager.fromBase64(trimmed);
|
|
1512
|
+
} catch {
|
|
1513
|
+
rawBytes = textEncoder$1.encode(trimmed);
|
|
963
1514
|
}
|
|
964
1515
|
}
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
const targetKeys = options?.fields && options.fields.length > 0 ? options.fields.filter((f) => Object.prototype.hasOwnProperty.call(payload, f)) : Object.keys(payload);
|
|
969
|
-
const keysToDecrypt = targetKeys.filter((key) => this.isEncrypted(payload[key]));
|
|
970
|
-
if (keysToDecrypt.length === 0) {
|
|
971
|
-
return payload;
|
|
972
|
-
}
|
|
973
|
-
const proto = Object.getPrototypeOf(payload);
|
|
974
|
-
const result = proto && proto !== Object.prototype ? Object.assign(Object.create(proto), payload) : { ...payload };
|
|
975
|
-
const entries = await Promise.all(
|
|
976
|
-
keysToDecrypt.map(async (key) => {
|
|
977
|
-
const boundOptions = this._bindFieldAad(payload, key, options);
|
|
978
|
-
const hasBoundAad = boundOptions.aad !== options?.aad;
|
|
979
|
-
try {
|
|
980
|
-
return [key, await this.decryptValue(result[key], boundOptions)];
|
|
981
|
-
} catch (err) {
|
|
982
|
-
if (hasBoundAad && isAuthOrDecryptionError(err)) {
|
|
983
|
-
try {
|
|
984
|
-
return [key, await this.decryptValue(result[key], options)];
|
|
985
|
-
} catch (fallbackErr) {
|
|
986
|
-
throw fallbackErr;
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
throw err;
|
|
990
|
-
}
|
|
991
|
-
})
|
|
1516
|
+
} else {
|
|
1517
|
+
throw new Error(
|
|
1518
|
+
"[CKeyManager] Invalid key input for getKeyFingerprint: expected CryptoKey, Uint8Array, ArrayBuffer, or string."
|
|
992
1519
|
);
|
|
993
|
-
for (const [key, decryptedVal] of entries) {
|
|
994
|
-
safeSetProperty(result, key, decryptedVal);
|
|
995
|
-
}
|
|
996
|
-
return result;
|
|
997
1520
|
}
|
|
998
|
-
|
|
1521
|
+
const hashBuffer = await subtle.digest("SHA-256", rawBytes);
|
|
1522
|
+
const b64 = CKeyManager.toBase64(hashBuffer);
|
|
1523
|
+
const b64url = b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1524
|
+
return `SHA256:${b64url}`;
|
|
999
1525
|
}
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
class CKeyManager {
|
|
1003
|
-
// ── Binary & Encoding Utilities ──────────────────────────────────────────
|
|
1004
1526
|
/**
|
|
1005
|
-
*
|
|
1527
|
+
* Signs arbitrary data or JSON objects using an ECDSA P-256 private key.
|
|
1528
|
+
* Objects are automatically serialized via canonicalizeJson() before hashing.
|
|
1006
1529
|
*/
|
|
1007
|
-
static
|
|
1008
|
-
const
|
|
1009
|
-
|
|
1530
|
+
static async sign(data, privateKey) {
|
|
1531
|
+
const subtle = getSubtleCrypto();
|
|
1532
|
+
if (!privateKey || !privateKey.algorithm || privateKey.algorithm.name !== "ECDSA") {
|
|
1533
|
+
throw new Error("[CKeyManager] sign requires an ECDSA private key.");
|
|
1534
|
+
}
|
|
1535
|
+
let dataBytes;
|
|
1536
|
+
if (data instanceof Uint8Array) {
|
|
1537
|
+
dataBytes = data;
|
|
1538
|
+
} else if (ArrayBuffer.isView(data)) {
|
|
1539
|
+
dataBytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
1540
|
+
} else if (data instanceof ArrayBuffer) {
|
|
1541
|
+
dataBytes = new Uint8Array(data);
|
|
1542
|
+
} else if (typeof data === "string") {
|
|
1543
|
+
dataBytes = textEncoder$1.encode(data);
|
|
1544
|
+
} else {
|
|
1545
|
+
const canonical = canonicalizeJson(data);
|
|
1546
|
+
dataBytes = textEncoder$1.encode(canonical);
|
|
1547
|
+
}
|
|
1548
|
+
const namedCurve = privateKey.algorithm.namedCurve || "P-256";
|
|
1549
|
+
const hashName = namedCurve === "P-384" ? "SHA-384" : namedCurve === "P-521" ? "SHA-512" : "SHA-256";
|
|
1550
|
+
const signatureBuffer = await subtle.sign(
|
|
1551
|
+
{
|
|
1552
|
+
name: "ECDSA",
|
|
1553
|
+
hash: { name: hashName }
|
|
1554
|
+
},
|
|
1555
|
+
privateKey,
|
|
1556
|
+
dataBytes
|
|
1557
|
+
);
|
|
1558
|
+
return CKeyManager.toBase64(signatureBuffer);
|
|
1010
1559
|
}
|
|
1011
1560
|
/**
|
|
1012
|
-
*
|
|
1561
|
+
* Verifies an ECDSA P-256 signature against original data or JSON object.
|
|
1013
1562
|
*/
|
|
1014
|
-
static
|
|
1015
|
-
|
|
1563
|
+
static async verify(data, signature, publicKey) {
|
|
1564
|
+
const subtle = getSubtleCrypto();
|
|
1565
|
+
if (!publicKey || !publicKey.algorithm || publicKey.algorithm.name !== "ECDSA") {
|
|
1566
|
+
throw new Error("[CKeyManager] verify requires an ECDSA public key.");
|
|
1567
|
+
}
|
|
1568
|
+
if (typeof signature !== "string" || !signature.trim()) {
|
|
1569
|
+
return false;
|
|
1570
|
+
}
|
|
1571
|
+
try {
|
|
1572
|
+
const signatureBytes = CKeyManager.fromBase64(signature);
|
|
1573
|
+
let dataBytes;
|
|
1574
|
+
if (data instanceof Uint8Array) {
|
|
1575
|
+
dataBytes = data;
|
|
1576
|
+
} else if (ArrayBuffer.isView(data)) {
|
|
1577
|
+
dataBytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
1578
|
+
} else if (data instanceof ArrayBuffer) {
|
|
1579
|
+
dataBytes = new Uint8Array(data);
|
|
1580
|
+
} else if (typeof data === "string") {
|
|
1581
|
+
dataBytes = textEncoder$1.encode(data);
|
|
1582
|
+
} else {
|
|
1583
|
+
const canonical = canonicalizeJson(data);
|
|
1584
|
+
dataBytes = textEncoder$1.encode(canonical);
|
|
1585
|
+
}
|
|
1586
|
+
const namedCurve = publicKey.algorithm.namedCurve || "P-256";
|
|
1587
|
+
const hashName = namedCurve === "P-384" ? "SHA-384" : namedCurve === "P-521" ? "SHA-512" : "SHA-256";
|
|
1588
|
+
return await subtle.verify(
|
|
1589
|
+
{
|
|
1590
|
+
name: "ECDSA",
|
|
1591
|
+
hash: { name: hashName }
|
|
1592
|
+
},
|
|
1593
|
+
publicKey,
|
|
1594
|
+
signatureBytes,
|
|
1595
|
+
dataBytes
|
|
1596
|
+
);
|
|
1597
|
+
} catch {
|
|
1598
|
+
return false;
|
|
1599
|
+
}
|
|
1016
1600
|
}
|
|
1017
1601
|
/**
|
|
1018
|
-
*
|
|
1019
|
-
*
|
|
1602
|
+
* Computes a deterministic HMAC-SHA256 blind index hash for an arbitrary term using the supplied CryptoKey.
|
|
1603
|
+
* Performs Unicode NFKC normalization, trimming, and lowercasing before computing the signature.
|
|
1604
|
+
*
|
|
1605
|
+
* @param value - The input string, number, or object to compute the blind index for.
|
|
1606
|
+
* @param hmacKey - CryptoKey configured for HMAC signing.
|
|
1607
|
+
* @returns Lowercase hexadecimal HMAC-SHA256 blind index hash.
|
|
1020
1608
|
*/
|
|
1021
|
-
static
|
|
1022
|
-
if (
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
const normalized = cleaned.replace(/-/g, "+").replace(/_/g, "/");
|
|
1028
|
-
const padded = normalized.padEnd(normalized.length + (4 - normalized.length % 4) % 4, "=");
|
|
1029
|
-
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(padded)) {
|
|
1030
|
-
throw new Error("[CKeyManager] Invalid base64 string");
|
|
1609
|
+
static async computeBlindIndex(value, hmacKey) {
|
|
1610
|
+
if (value === null || value === void 0) return "";
|
|
1611
|
+
const normalized = String(value).normalize("NFKC").trim().toLowerCase();
|
|
1612
|
+
if (!normalized) return "";
|
|
1613
|
+
if (!hmacKey || !hmacKey.algorithm || hmacKey.algorithm.name !== "HMAC") {
|
|
1614
|
+
throw new Error("[CKeyManager] computeBlindIndex requires an HMAC CryptoKey.");
|
|
1031
1615
|
}
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1616
|
+
const subtle = getSubtleCrypto();
|
|
1617
|
+
const signature = await subtle.sign("HMAC", hmacKey, textEncoder$1.encode(normalized));
|
|
1618
|
+
return Array.from(new Uint8Array(signature)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* Signs a structured access grant, embedding signature and granter key fingerprint.
|
|
1622
|
+
*/
|
|
1623
|
+
static async signGrant(grant, granterPrivateKey, granterPublicKey) {
|
|
1624
|
+
if (!grant || typeof grant !== "object") {
|
|
1625
|
+
throw new Error("[CKeyManager] signGrant requires a valid grant object.");
|
|
1036
1626
|
}
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1627
|
+
let fingerprint;
|
|
1628
|
+
if (granterPublicKey) {
|
|
1629
|
+
fingerprint = await CKeyManager.getKeyFingerprint(granterPublicKey);
|
|
1630
|
+
} else {
|
|
1631
|
+
const subtle = getSubtleCrypto();
|
|
1632
|
+
try {
|
|
1633
|
+
const jwk = await subtle.exportKey("jwk", granterPrivateKey);
|
|
1634
|
+
const pubJwk = {
|
|
1635
|
+
kty: jwk.kty,
|
|
1636
|
+
crv: jwk.crv,
|
|
1637
|
+
x: jwk.x,
|
|
1638
|
+
y: jwk.y,
|
|
1639
|
+
ext: true,
|
|
1640
|
+
key_ops: ["verify"]
|
|
1641
|
+
};
|
|
1642
|
+
const pubKey = await subtle.importKey(
|
|
1643
|
+
"jwk",
|
|
1644
|
+
pubJwk,
|
|
1645
|
+
{ name: "ECDSA", namedCurve: jwk.crv || "P-256" },
|
|
1646
|
+
true,
|
|
1647
|
+
["verify"]
|
|
1648
|
+
);
|
|
1649
|
+
fingerprint = await CKeyManager.getKeyFingerprint(pubKey);
|
|
1650
|
+
} catch {
|
|
1651
|
+
throw new Error(
|
|
1652
|
+
"[CKeyManager] signGrant requires granterPublicKey when granterPrivateKey is non-extractable."
|
|
1653
|
+
);
|
|
1654
|
+
}
|
|
1041
1655
|
}
|
|
1042
|
-
|
|
1656
|
+
const signature = await CKeyManager.sign(grant, granterPrivateKey);
|
|
1657
|
+
return {
|
|
1658
|
+
grant: { ...grant },
|
|
1659
|
+
signature,
|
|
1660
|
+
fingerprint
|
|
1661
|
+
};
|
|
1043
1662
|
}
|
|
1044
1663
|
/**
|
|
1045
|
-
*
|
|
1046
|
-
* Guarantees 100% identical salt byte resolution between Node.js and Browser environments.
|
|
1664
|
+
* Verifies the authenticity and integrity of a signed access grant against granter public key.
|
|
1047
1665
|
*/
|
|
1048
|
-
static
|
|
1049
|
-
if (!
|
|
1050
|
-
|
|
1051
|
-
return salt.byteLength > 0 ? new Uint8Array(salt.buffer.slice(salt.byteOffset, salt.byteOffset + salt.byteLength)) : CKeyManager.randomBytes(16);
|
|
1666
|
+
static async verifyGrant(signedGrant, granterPublicKey, options) {
|
|
1667
|
+
if (!signedGrant || !signedGrant.grant || !signedGrant.signature || !signedGrant.fingerprint || !granterPublicKey) {
|
|
1668
|
+
return false;
|
|
1052
1669
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1670
|
+
const expectedFingerprint = await CKeyManager.getKeyFingerprint(granterPublicKey);
|
|
1671
|
+
if (signedGrant.fingerprint !== expectedFingerprint) {
|
|
1672
|
+
return false;
|
|
1055
1673
|
}
|
|
1056
|
-
if (
|
|
1057
|
-
const
|
|
1058
|
-
if (
|
|
1059
|
-
|
|
1060
|
-
return CKeyManager.fromBase64(trimmed.slice(7));
|
|
1061
|
-
}
|
|
1062
|
-
if (trimmed.endsWith("=") && /^[A-Za-z0-9+/]+={1,2}$/.test(trimmed)) {
|
|
1063
|
-
try {
|
|
1064
|
-
const decoded = CKeyManager.fromBase64(trimmed);
|
|
1065
|
-
if (decoded.byteLength > 0) return decoded;
|
|
1066
|
-
} catch {
|
|
1067
|
-
}
|
|
1674
|
+
if (options?.checkExpiry && signedGrant.grant.expiresAt !== void 0) {
|
|
1675
|
+
const now = options.now ?? Date.now();
|
|
1676
|
+
if (now > signedGrant.grant.expiresAt) {
|
|
1677
|
+
return false;
|
|
1068
1678
|
}
|
|
1069
|
-
const encoded = textEncoder$1.encode(trimmed);
|
|
1070
|
-
return encoded.byteLength > 0 ? encoded : CKeyManager.randomBytes(16);
|
|
1071
1679
|
}
|
|
1072
|
-
return CKeyManager.
|
|
1680
|
+
return await CKeyManager.verify(signedGrant.grant, signedGrant.signature, granterPublicKey);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
class AEncryptionProvider {
|
|
1685
|
+
defaultKey;
|
|
1686
|
+
_hmacKey;
|
|
1687
|
+
keys;
|
|
1688
|
+
activeKid;
|
|
1689
|
+
allowedKids;
|
|
1690
|
+
constructor(key, options) {
|
|
1691
|
+
if (key !== void 0) {
|
|
1692
|
+
this.defaultKey = key;
|
|
1693
|
+
}
|
|
1694
|
+
if (options?.keys) {
|
|
1695
|
+
this.keys = options.keys;
|
|
1696
|
+
}
|
|
1697
|
+
if (options?.activeKid) {
|
|
1698
|
+
this.activeKid = options.activeKid;
|
|
1699
|
+
}
|
|
1700
|
+
if (options?.allowedKids) {
|
|
1701
|
+
this.allowedKids = options.allowedKids;
|
|
1702
|
+
}
|
|
1703
|
+
if (options?.hmacKey) {
|
|
1704
|
+
this._hmacKey = options.hmacKey;
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
/**
|
|
1708
|
+
* Sets or updates the default secret key, CryptoKey, or async key provider function.
|
|
1709
|
+
*/
|
|
1710
|
+
setDefaultKey(key) {
|
|
1711
|
+
this.defaultKey = key || void 0;
|
|
1712
|
+
}
|
|
1713
|
+
/**
|
|
1714
|
+
* Retrieves the currently configured default key or key provider callback.
|
|
1715
|
+
*/
|
|
1716
|
+
getDefaultKey() {
|
|
1717
|
+
return this.defaultKey;
|
|
1718
|
+
}
|
|
1719
|
+
/**
|
|
1720
|
+
* Configures a keyring dictionary of keys with an optional active key identifier.
|
|
1721
|
+
*/
|
|
1722
|
+
setKeyring(keys, activeKid) {
|
|
1723
|
+
this.keys = keys;
|
|
1724
|
+
if (activeKid !== void 0) {
|
|
1725
|
+
this.activeKid = activeKid;
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
/**
|
|
1729
|
+
* Retrieves the currently configured keyring dictionary.
|
|
1730
|
+
*/
|
|
1731
|
+
getKeyring() {
|
|
1732
|
+
return this.keys;
|
|
1733
|
+
}
|
|
1734
|
+
/**
|
|
1735
|
+
* Sets the active Key Identifier (kid) to use for outbound encryption.
|
|
1736
|
+
*/
|
|
1737
|
+
setActiveKid(kid) {
|
|
1738
|
+
this.activeKid = kid;
|
|
1739
|
+
}
|
|
1740
|
+
/**
|
|
1741
|
+
* Retrieves the active Key Identifier (kid).
|
|
1742
|
+
*/
|
|
1743
|
+
getActiveKid() {
|
|
1744
|
+
return this.activeKid;
|
|
1745
|
+
}
|
|
1746
|
+
/**
|
|
1747
|
+
* Sets or updates the dedicated HMAC key or key provider callback for blind indexing.
|
|
1748
|
+
*/
|
|
1749
|
+
setHmacKey(key) {
|
|
1750
|
+
this._hmacKey = key || void 0;
|
|
1751
|
+
}
|
|
1752
|
+
/**
|
|
1753
|
+
* Retrieves the currently configured HMAC key or key provider callback.
|
|
1754
|
+
*/
|
|
1755
|
+
getHmacKey() {
|
|
1756
|
+
return this._hmacKey;
|
|
1073
1757
|
}
|
|
1074
|
-
// ── Symmetric Key Management ─────────────────────────────────────────────
|
|
1075
1758
|
/**
|
|
1076
|
-
*
|
|
1077
|
-
*
|
|
1078
|
-
* E2EE Threat Model Note: Focuses on protecting GDPR/tenant data against database and
|
|
1079
|
-
* server-side leaks. Keys remain extractable by default to enable role-based key sharing,
|
|
1080
|
-
* wrapping, and transfer between authorized client sessions in multi-user SaaS apps.
|
|
1081
|
-
* Client-side malware/trojan protection is out of scope.
|
|
1759
|
+
* Computes a deterministic HMAC-SHA256 blind index hash for an arbitrary term.
|
|
1760
|
+
* If no hmacKey is explicitly provided, uses the provider's configured hmacKey or default key.
|
|
1082
1761
|
*/
|
|
1083
|
-
|
|
1762
|
+
async computeBlindIndex(value, hmacKey) {
|
|
1763
|
+
if (value === null || value === void 0) return "";
|
|
1764
|
+
const keyMaterial = hmacKey || this._hmacKey || this.defaultKey;
|
|
1765
|
+
if (!keyMaterial) {
|
|
1766
|
+
throw new Error("[AEncryptionProvider] computeBlindIndex requires an HMAC key. Configure hmacKey or pass a key directly.");
|
|
1767
|
+
}
|
|
1768
|
+
const resolvedKey = typeof keyMaterial === "function" ? await keyMaterial() : keyMaterial;
|
|
1769
|
+
if (resolvedKey && typeof resolvedKey !== "string" && resolvedKey.algorithm) {
|
|
1770
|
+
return CKeyManager.computeBlindIndex(value, resolvedKey);
|
|
1771
|
+
}
|
|
1084
1772
|
const subtle = getSubtleCrypto();
|
|
1085
|
-
const
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1773
|
+
const keyBytes = typeof resolvedKey === "string" ? new TextEncoder().encode(resolvedKey) : resolvedKey instanceof Uint8Array ? resolvedKey : new Uint8Array(resolvedKey);
|
|
1774
|
+
const importedHmacKey = await subtle.importKey(
|
|
1775
|
+
"raw",
|
|
1776
|
+
keyBytes,
|
|
1777
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
1778
|
+
false,
|
|
1779
|
+
["sign"]
|
|
1090
1780
|
);
|
|
1781
|
+
return CKeyManager.computeBlindIndex(value, importedHmacKey);
|
|
1091
1782
|
}
|
|
1092
1783
|
/**
|
|
1093
|
-
*
|
|
1784
|
+
* Pre-derives and caches the native CryptoKey for subsequent cryptographic operations.
|
|
1785
|
+
* Subclasses should override this with a dedicated, non-side-effecting implementation.
|
|
1094
1786
|
*/
|
|
1095
|
-
|
|
1096
|
-
const subtle = getSubtleCrypto();
|
|
1097
|
-
const raw = await subtle.exportKey("raw", key);
|
|
1098
|
-
try {
|
|
1099
|
-
return CKeyManager.toBase64(raw);
|
|
1100
|
-
} finally {
|
|
1101
|
-
new Uint8Array(raw).fill(0);
|
|
1102
|
-
}
|
|
1787
|
+
async warmupKey(_options) {
|
|
1103
1788
|
}
|
|
1104
1789
|
/**
|
|
1105
|
-
*
|
|
1790
|
+
* Resolves raw key from keyProvider callback or defaultKey.
|
|
1106
1791
|
*/
|
|
1107
|
-
|
|
1108
|
-
const
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1792
|
+
async resolveRawKey(keyProvider) {
|
|
1793
|
+
const target = keyProvider || (this.keys && this.activeKid ? this.keys[this.activeKid] : void 0) || this.defaultKey;
|
|
1794
|
+
if (!target) {
|
|
1795
|
+
throw new Error(
|
|
1796
|
+
`[${this.constructor.name}] No encryption key or keyProvider was supplied. Key cannot be empty or undefined. A valid key must be supplied from your authentication or secret management system.`
|
|
1797
|
+
);
|
|
1113
1798
|
}
|
|
1114
|
-
const
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
rawBytes,
|
|
1119
|
-
{ name: algorithm },
|
|
1120
|
-
extractable,
|
|
1121
|
-
usages
|
|
1799
|
+
const rawKey = typeof target === "function" ? await target() : target;
|
|
1800
|
+
if (!rawKey || typeof rawKey === "string" && rawKey.trim().length === 0 || typeof rawKey === "object" && "byteLength" in rawKey && rawKey.byteLength === 0) {
|
|
1801
|
+
throw new Error(
|
|
1802
|
+
`[${this.constructor.name}] No encryption key or keyProvider was supplied. Key provider resolved to empty or undefined key.`
|
|
1122
1803
|
);
|
|
1123
|
-
} finally {
|
|
1124
|
-
rawBytes.fill(0);
|
|
1125
1804
|
}
|
|
1805
|
+
return rawKey;
|
|
1126
1806
|
}
|
|
1127
|
-
// ── Asymmetric Key Pair Management ───────────────────────────────────────
|
|
1128
1807
|
/**
|
|
1129
|
-
*
|
|
1808
|
+
* Checks whether the given payload is an encrypted envelope.
|
|
1130
1809
|
*/
|
|
1131
|
-
static
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
{
|
|
1135
|
-
name: "RSA-OAEP",
|
|
1136
|
-
modulusLength: bits,
|
|
1137
|
-
publicExponent: new Uint8Array([1, 0, 1]),
|
|
1138
|
-
hash
|
|
1139
|
-
},
|
|
1140
|
-
extractable,
|
|
1141
|
-
["encrypt", "decrypt", "wrapKey", "unwrapKey"]
|
|
1142
|
-
);
|
|
1143
|
-
return {
|
|
1144
|
-
publicKey: pair.publicKey,
|
|
1145
|
-
privateKey: pair.privateKey
|
|
1146
|
-
};
|
|
1810
|
+
static isEncrypted(payload) {
|
|
1811
|
+
if (!payload || typeof payload !== "object") return false;
|
|
1812
|
+
return payload.__encrypted === true && typeof payload.ciphertext === "string" && typeof payload.iv === "string";
|
|
1147
1813
|
}
|
|
1148
1814
|
/**
|
|
1149
|
-
*
|
|
1815
|
+
* Checks whether the given payload is an encrypted envelope.
|
|
1150
1816
|
*/
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
const pair = await subtle.generateKey(
|
|
1154
|
-
{
|
|
1155
|
-
name: "ECDH",
|
|
1156
|
-
namedCurve: curve
|
|
1157
|
-
},
|
|
1158
|
-
extractable,
|
|
1159
|
-
["deriveKey", "deriveBits"]
|
|
1160
|
-
);
|
|
1161
|
-
return {
|
|
1162
|
-
publicKey: pair.publicKey,
|
|
1163
|
-
privateKey: pair.privateKey
|
|
1164
|
-
};
|
|
1817
|
+
isEncrypted(payload) {
|
|
1818
|
+
return AEncryptionProvider.isEncrypted(payload);
|
|
1165
1819
|
}
|
|
1166
1820
|
/**
|
|
1167
|
-
*
|
|
1821
|
+
* Clears configured keys and cached cryptographic material from memory.
|
|
1168
1822
|
*/
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1823
|
+
clearKeys() {
|
|
1824
|
+
this.defaultKey = void 0;
|
|
1825
|
+
this.keys = void 0;
|
|
1826
|
+
this.activeKid = void 0;
|
|
1827
|
+
this.allowedKids = void 0;
|
|
1172
1828
|
}
|
|
1173
1829
|
/**
|
|
1174
|
-
*
|
|
1830
|
+
* Alias for clearKeys() to conform with standard disposal patterns.
|
|
1175
1831
|
*/
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
return await subtle.exportKey("jwk", key);
|
|
1832
|
+
dispose() {
|
|
1833
|
+
this.clearKeys();
|
|
1179
1834
|
}
|
|
1180
1835
|
/**
|
|
1181
|
-
*
|
|
1836
|
+
* ECMAScript Explicit Resource Management protocol (using / await using declarations).
|
|
1182
1837
|
*/
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
if (isEC) {
|
|
1189
|
-
usages = [];
|
|
1190
|
-
if (!importAlgorithm || importAlgorithm === "ECDH") {
|
|
1191
|
-
importAlgorithm = { name: "ECDH", namedCurve: jwk.crv || "P-256" };
|
|
1192
|
-
}
|
|
1193
|
-
} else {
|
|
1194
|
-
usages = ["encrypt", "wrapKey"];
|
|
1195
|
-
if (!importAlgorithm || importAlgorithm === "RSA-OAEP") {
|
|
1196
|
-
if (jwk.kty === "RSA") {
|
|
1197
|
-
const hash = jwk.alg === "RSA-OAEP-512" ? "SHA-512" : "SHA-256";
|
|
1198
|
-
importAlgorithm = { name: "RSA-OAEP", hash };
|
|
1199
|
-
} else {
|
|
1200
|
-
throw new Error(`[CKeyManager] Unsupported JWK kty: ${jwk.kty}`);
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
return await subtle.importKey("jwk", jwk, importAlgorithm, true, usages);
|
|
1838
|
+
[Symbol.dispose]() {
|
|
1839
|
+
this.dispose();
|
|
1840
|
+
}
|
|
1841
|
+
async [Symbol.asyncDispose]() {
|
|
1842
|
+
this.dispose();
|
|
1205
1843
|
}
|
|
1206
1844
|
/**
|
|
1207
|
-
*
|
|
1845
|
+
* Resolves effective Authenticated Additional Data (AAD) for the operation:
|
|
1846
|
+
* 1. If options.bindRequestContextAad is explicitly false, returns options.aad directly without context binding.
|
|
1847
|
+
* 2. When encrypting (isDecrypt=false): binds request context `req:${method}:${url}` only if options.bindRequestContextAad === true.
|
|
1848
|
+
* 3. When decrypting (isDecrypt=true): derives request context `req:${method}:${url}` if url or method is provided.
|
|
1849
|
+
* 4. If explicit options.aad is provided: combines with request context if bound, or returns options.aad directly.
|
|
1850
|
+
* 5. Otherwise returns undefined.
|
|
1208
1851
|
*/
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
let
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
if (
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1852
|
+
_resolveEffectiveAad(options, isDecrypt = false) {
|
|
1853
|
+
if (!options) return void 0;
|
|
1854
|
+
let rawAad = options.aad;
|
|
1855
|
+
if (typeof rawAad === "function") {
|
|
1856
|
+
rawAad = rawAad(void 0, "");
|
|
1857
|
+
}
|
|
1858
|
+
if (rawAad === void 0 && options.baseAad) {
|
|
1859
|
+
rawAad = options.baseAad;
|
|
1860
|
+
}
|
|
1861
|
+
if (options.bindRequestContextAad === false) return rawAad;
|
|
1862
|
+
const hasReqContext = Boolean(options.url || options.method);
|
|
1863
|
+
if (rawAad) {
|
|
1864
|
+
if (options.bindRequestContextAad === true && hasReqContext) {
|
|
1865
|
+
const m = (options.method || "POST").toUpperCase();
|
|
1866
|
+
const u = options.url || "";
|
|
1867
|
+
const baseAad = typeof rawAad === "string" ? rawAad : rawAad instanceof Uint8Array || rawAad instanceof ArrayBuffer ? textDecoder.decode(rawAad) : String(rawAad);
|
|
1868
|
+
if (baseAad.includes(`|req:${m}:${u}`)) {
|
|
1869
|
+
return baseAad;
|
|
1227
1870
|
}
|
|
1871
|
+
return `${baseAad}|req:${m}:${u}`;
|
|
1228
1872
|
}
|
|
1873
|
+
return rawAad;
|
|
1229
1874
|
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
static async importPublicKeyFromPEM(pem, algorithm = "RSA-OAEP", curve = "P-256", hash = "SHA-256") {
|
|
1236
|
-
const subtle = getSubtleCrypto();
|
|
1237
|
-
const cleaned = pem.replace(/-----BEGIN[ A-Z0-9_-]+-----/g, "").replace(/-----END[ A-Z0-9_-]+-----/g, "").replace(/\s+/g, "");
|
|
1238
|
-
const spkiBytes = CKeyManager.fromBase64(cleaned);
|
|
1239
|
-
let importAlgorithm;
|
|
1240
|
-
let usages;
|
|
1241
|
-
if (algorithm === "ECDH") {
|
|
1242
|
-
importAlgorithm = { name: "ECDH", namedCurve: curve };
|
|
1243
|
-
usages = [];
|
|
1244
|
-
} else {
|
|
1245
|
-
importAlgorithm = { name: "RSA-OAEP", hash };
|
|
1246
|
-
usages = ["encrypt", "wrapKey"];
|
|
1247
|
-
}
|
|
1248
|
-
try {
|
|
1249
|
-
return await subtle.importKey("spki", spkiBytes, importAlgorithm, true, usages);
|
|
1250
|
-
} finally {
|
|
1251
|
-
spkiBytes.fill(0);
|
|
1875
|
+
const shouldBindReq = isDecrypt ? hasReqContext : options.bindRequestContextAad === true && hasReqContext;
|
|
1876
|
+
if (shouldBindReq) {
|
|
1877
|
+
const m = (options.method || "POST").toUpperCase();
|
|
1878
|
+
const u = options.url || "";
|
|
1879
|
+
return `req:${m}:${u}`;
|
|
1252
1880
|
}
|
|
1881
|
+
return void 0;
|
|
1253
1882
|
}
|
|
1254
1883
|
/**
|
|
1255
|
-
*
|
|
1884
|
+
* Helper to deterministically bind record ID, contextual sibling fields (aadFields),
|
|
1885
|
+
* dynamic AAD callbacks, and field name into the AAD to prevent ciphertext swapping
|
|
1886
|
+
* between fields, records, or parent entities.
|
|
1256
1887
|
*/
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
);
|
|
1262
|
-
}
|
|
1263
|
-
const subtle = getSubtleCrypto();
|
|
1264
|
-
const cleaned = pem.replace(/-----BEGIN[ A-Z0-9_-]+-----/g, "").replace(/-----END[ A-Z0-9_-]+-----/g, "").replace(/\s+/g, "");
|
|
1265
|
-
const pkcs8Bytes = CKeyManager.fromBase64(cleaned);
|
|
1266
|
-
let importAlgorithm;
|
|
1267
|
-
let usages;
|
|
1268
|
-
if (algorithm === "ECDH") {
|
|
1269
|
-
importAlgorithm = { name: "ECDH", namedCurve: curve };
|
|
1270
|
-
usages = ["deriveKey", "deriveBits"];
|
|
1271
|
-
} else {
|
|
1272
|
-
importAlgorithm = { name: "RSA-OAEP", hash };
|
|
1273
|
-
usages = ["decrypt", "unwrapKey"];
|
|
1888
|
+
_bindFieldAad(data, field, options) {
|
|
1889
|
+
let baseAad = "";
|
|
1890
|
+
if (options?.baseAad) {
|
|
1891
|
+
baseAad = options.baseAad;
|
|
1274
1892
|
}
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1893
|
+
if (options?.aad) {
|
|
1894
|
+
if (typeof options.aad === "function") {
|
|
1895
|
+
const dynamicRes = options.aad(data, field);
|
|
1896
|
+
if (typeof dynamicRes === "string") {
|
|
1897
|
+
baseAad = baseAad ? `${baseAad}|${dynamicRes}` : dynamicRes;
|
|
1898
|
+
} else if (dynamicRes instanceof Uint8Array || dynamicRes instanceof ArrayBuffer) {
|
|
1899
|
+
const decoded = textDecoder.decode(dynamicRes);
|
|
1900
|
+
baseAad = baseAad ? `${baseAad}|${decoded}` : decoded;
|
|
1901
|
+
} else if (dynamicRes !== void 0 && dynamicRes !== null) {
|
|
1902
|
+
const str = String(dynamicRes);
|
|
1903
|
+
baseAad = baseAad ? `${baseAad}|${str}` : str;
|
|
1904
|
+
}
|
|
1905
|
+
} else if (typeof options.aad === "string") {
|
|
1906
|
+
baseAad = baseAad ? `${baseAad}|${options.aad}` : options.aad;
|
|
1907
|
+
} else if (options.aad instanceof Uint8Array || options.aad instanceof ArrayBuffer) {
|
|
1908
|
+
const decoded = textDecoder.decode(options.aad);
|
|
1909
|
+
baseAad = baseAad ? `${baseAad}|${decoded}` : decoded;
|
|
1910
|
+
} else {
|
|
1911
|
+
const str = String(options.aad);
|
|
1912
|
+
baseAad = baseAad ? `${baseAad}|${str}` : str;
|
|
1913
|
+
}
|
|
1279
1914
|
}
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1915
|
+
const recordId = data && typeof data === "object" && (data.id !== void 0 || data._id !== void 0) ? String(data.id ?? data._id) : "";
|
|
1916
|
+
const recordPart = recordId ? `|record:${recordId}` : "";
|
|
1917
|
+
let contextPart = "";
|
|
1918
|
+
if (options?.aadFields && options.aadFields.length > 0 && data && typeof data === "object") {
|
|
1919
|
+
const sortedFields = [...options.aadFields].sort();
|
|
1920
|
+
const parts = [];
|
|
1921
|
+
for (const f of sortedFields) {
|
|
1922
|
+
const val = data[f];
|
|
1923
|
+
let serializedVal = "";
|
|
1924
|
+
if (val === null) {
|
|
1925
|
+
serializedVal = "null";
|
|
1926
|
+
} else if (val !== void 0) {
|
|
1927
|
+
serializedVal = typeof val === "object" ? JSON.stringify(val) : String(val);
|
|
1928
|
+
}
|
|
1929
|
+
parts.push(`${f}=${serializedVal}`);
|
|
1930
|
+
}
|
|
1931
|
+
contextPart = `|ctx:${parts.join(",")}`;
|
|
1292
1932
|
}
|
|
1293
|
-
const
|
|
1294
|
-
return
|
|
1295
|
-
|
|
1296
|
-
|
|
1933
|
+
const reqPart = options?.bindRequestContextAad === true && (options?.url || options?.method) ? `|req:${(options.method || "POST").toUpperCase()}:${options.url || ""}` : "";
|
|
1934
|
+
return {
|
|
1935
|
+
...options,
|
|
1936
|
+
bindRequestContextAad: false,
|
|
1937
|
+
aad: `aad:${baseAad}${recordPart}${contextPart}${reqPart}|field:${field}`
|
|
1938
|
+
};
|
|
1297
1939
|
}
|
|
1298
1940
|
/**
|
|
1299
|
-
*
|
|
1941
|
+
* Validates envelope metadata against algorithm whitelists and key identifiers (kid).
|
|
1942
|
+
* In strict mode, allowedAlgorithms defaults to ['AES-GCM'].
|
|
1300
1943
|
*/
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
const
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1944
|
+
_validateEnvelopeMetadata(payload, options) {
|
|
1945
|
+
if (!payload || typeof payload !== "object") return void 0;
|
|
1946
|
+
const allowedAlgorithms = options?.allowedAlgorithms ?? (options?.strict ? ["AES-GCM"] : void 0);
|
|
1947
|
+
if (allowedAlgorithms && allowedAlgorithms.length > 0) {
|
|
1948
|
+
const alg = payload.alg;
|
|
1949
|
+
if (!alg || !allowedAlgorithms.includes(alg)) {
|
|
1950
|
+
return new Error(`[AEncryptionProvider] Algorithm mismatch: algorithm '${alg || "unknown"}' is not in allowedAlgorithms.`);
|
|
1951
|
+
}
|
|
1309
1952
|
}
|
|
1310
|
-
const
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1953
|
+
const effectiveKeys = options?.keys || this.keys;
|
|
1954
|
+
const effectiveAllowedKids = options?.allowedKids || this.allowedKids;
|
|
1955
|
+
const allowedKids = effectiveAllowedKids ?? (effectiveKeys ? Object.keys(effectiveKeys) : void 0);
|
|
1956
|
+
if (allowedKids && allowedKids.length > 0) {
|
|
1957
|
+
if (payload.kid && !allowedKids.includes(payload.kid)) {
|
|
1958
|
+
return new Error(`[AEncryptionProvider] Unauthorized key: kid '${payload.kid}' is not in allowedKids.`);
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
return void 0;
|
|
1314
1962
|
}
|
|
1315
|
-
// ── Key Wrapping & Unwrapping ────────────────────────────────────────────
|
|
1316
1963
|
/**
|
|
1317
|
-
*
|
|
1318
|
-
*
|
|
1964
|
+
* Normalizes encryption options with active keyring parameters:
|
|
1965
|
+
* If `keys` and `activeKid` are configured, selects `key = keys[activeKid]` and sets `kid = activeKid`.
|
|
1319
1966
|
*/
|
|
1320
|
-
|
|
1321
|
-
const
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1967
|
+
_resolveOutboundKeyringOptions(options) {
|
|
1968
|
+
const effectiveKeys = options?.keys || this.keys;
|
|
1969
|
+
const effectiveActiveKid = options?.activeKid || (options?.kid ? options.kid : this.activeKid);
|
|
1970
|
+
let effectiveKey = options?.key;
|
|
1971
|
+
let effectiveKid = options?.kid || effectiveActiveKid;
|
|
1972
|
+
if (effectiveKeys && effectiveActiveKid) {
|
|
1973
|
+
if (effectiveKeys[effectiveActiveKid]) {
|
|
1974
|
+
effectiveKey = options?.key || effectiveKeys[effectiveActiveKid];
|
|
1975
|
+
effectiveKid = effectiveActiveKid;
|
|
1976
|
+
} else if (!options?.key) {
|
|
1977
|
+
throw new Error(`[${this.constructor.name}] Active kid '${effectiveActiveKid}' not found in keyring keys.`);
|
|
1978
|
+
}
|
|
1979
|
+
} else if (effectiveKeys && !effectiveKey) {
|
|
1980
|
+
const kids = Object.keys(effectiveKeys);
|
|
1981
|
+
if (kids.length > 0) {
|
|
1982
|
+
effectiveKid = kids[0];
|
|
1983
|
+
effectiveKey = effectiveKeys[kids[0]];
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
if (effectiveKeys || effectiveActiveKid || effectiveKey !== options?.key || effectiveKid !== options?.kid) {
|
|
1987
|
+
return {
|
|
1988
|
+
...options,
|
|
1989
|
+
...effectiveKeys ? { keys: effectiveKeys } : {},
|
|
1990
|
+
...effectiveActiveKid ? { activeKid: effectiveActiveKid } : {},
|
|
1991
|
+
...effectiveKey !== void 0 ? { key: effectiveKey } : {},
|
|
1992
|
+
...effectiveKid !== void 0 ? { kid: effectiveKid } : {}
|
|
1993
|
+
};
|
|
1329
1994
|
}
|
|
1330
|
-
|
|
1331
|
-
return CKeyManager.toBase64(wrappedBuffer);
|
|
1995
|
+
return options || {};
|
|
1332
1996
|
}
|
|
1333
1997
|
/**
|
|
1334
|
-
*
|
|
1998
|
+
* Resolves the appropriate decryption key for a specific payload envelope based on its `kid`
|
|
1999
|
+
* and the configured keyring (`keys`, `activeKid`, or fallback `key`).
|
|
1335
2000
|
*/
|
|
1336
|
-
|
|
1337
|
-
const
|
|
1338
|
-
const
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
unwrapAlgorithm = "AES-KW";
|
|
2001
|
+
_resolveDecryptionKeyForEnvelope(payload, options) {
|
|
2002
|
+
const effectiveKeys = options?.keys || this.keys;
|
|
2003
|
+
const effectiveActiveKid = options?.activeKid || this.activeKid;
|
|
2004
|
+
if (payload && typeof payload === "object" && payload.kid && effectiveKeys) {
|
|
2005
|
+
if (effectiveKeys[payload.kid]) {
|
|
2006
|
+
return effectiveKeys[payload.kid];
|
|
2007
|
+
}
|
|
1344
2008
|
}
|
|
1345
|
-
|
|
1346
|
-
return
|
|
1347
|
-
format,
|
|
1348
|
-
wrappedBytes,
|
|
1349
|
-
unwrappingKey,
|
|
1350
|
-
unwrapAlgorithm,
|
|
1351
|
-
unwrappedKeyAlgorithm,
|
|
1352
|
-
extractable,
|
|
1353
|
-
keyUsages
|
|
1354
|
-
);
|
|
1355
|
-
} finally {
|
|
1356
|
-
wrappedBytes.fill(0);
|
|
2009
|
+
if (effectiveKeys && effectiveActiveKid && effectiveKeys[effectiveActiveKid]) {
|
|
2010
|
+
return effectiveKeys[effectiveActiveKid];
|
|
1357
2011
|
}
|
|
2012
|
+
return options?.key || this.defaultKey;
|
|
1358
2013
|
}
|
|
1359
2014
|
/**
|
|
1360
|
-
*
|
|
1361
|
-
*
|
|
2015
|
+
* Handles a per-field or per-payload decryption error according to configured options:
|
|
2016
|
+
* - Invokes onDecryptError callback if provided
|
|
2017
|
+
* - Attaches error metadata (__decryptErrors) to the record object
|
|
2018
|
+
* - Returns fallback cell placeholder ('[⚠ Decryption Error]') or rethrows
|
|
1362
2019
|
*/
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
2020
|
+
_handleDecryptError(err, record, field, value, index, options) {
|
|
2021
|
+
if (record && typeof record === "object" && record !== value) {
|
|
2022
|
+
record.__decryptErrors = {
|
|
2023
|
+
...record.__decryptErrors || {},
|
|
2024
|
+
[field || "__payload"]: err
|
|
2025
|
+
};
|
|
2026
|
+
}
|
|
2027
|
+
if (options?.onDecryptError === false || options?.onDecryptError === void 0) {
|
|
2028
|
+
throw err;
|
|
2029
|
+
}
|
|
2030
|
+
if (typeof options.onDecryptError === "function") {
|
|
2031
|
+
return options.onDecryptError({
|
|
2032
|
+
record,
|
|
2033
|
+
field,
|
|
2034
|
+
value,
|
|
2035
|
+
error: err,
|
|
2036
|
+
index
|
|
2037
|
+
});
|
|
2038
|
+
}
|
|
2039
|
+
if (typeof options.onDecryptError === "string") {
|
|
2040
|
+
return options.onDecryptError;
|
|
2041
|
+
}
|
|
2042
|
+
if (options.onDecryptError === true) {
|
|
2043
|
+
return "[\u26A0 Decryption Error]";
|
|
2044
|
+
}
|
|
2045
|
+
throw err;
|
|
1370
2046
|
}
|
|
1371
2047
|
/**
|
|
1372
|
-
*
|
|
1373
|
-
*
|
|
2048
|
+
* Encrypts a single scalar value with contextual AAD binding into an IEncryptedPayload envelope.
|
|
2049
|
+
* Binds recordId, fieldName, and sibling aadFields deterministically to protect against ciphertext swapping.
|
|
2050
|
+
*
|
|
2051
|
+
* @param value - Scalar value (string, number, boolean, object) to encrypt.
|
|
2052
|
+
* @param context - Field encryption context with recordId, fieldName, and optional sibling aadFields.
|
|
1374
2053
|
*/
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
if (
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
2054
|
+
async encryptField(value, context) {
|
|
2055
|
+
if (value === void 0 || value === null) return value;
|
|
2056
|
+
if (this.isEncrypted(value)) return value;
|
|
2057
|
+
const field = context?.fieldName || "field";
|
|
2058
|
+
const dummyData = {};
|
|
2059
|
+
if (context?.recordId !== void 0) {
|
|
2060
|
+
dummyData.id = context.recordId;
|
|
2061
|
+
}
|
|
2062
|
+
const { aadFields, ...restContext } = context || {};
|
|
2063
|
+
let boundOptions = { ...restContext };
|
|
2064
|
+
if (aadFields) {
|
|
2065
|
+
if (!Array.isArray(aadFields) && typeof aadFields === "object") {
|
|
2066
|
+
Object.assign(dummyData, aadFields);
|
|
2067
|
+
boundOptions.aadFields = Object.keys(aadFields);
|
|
2068
|
+
} else if (Array.isArray(aadFields)) {
|
|
2069
|
+
boundOptions.aadFields = aadFields;
|
|
2070
|
+
if (context?.record) {
|
|
2071
|
+
Object.assign(dummyData, context.record);
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
const resolvedOptions = this._bindFieldAad(dummyData, field, boundOptions);
|
|
2076
|
+
return this.encryptValue(value, resolvedOptions);
|
|
1393
2077
|
}
|
|
1394
2078
|
/**
|
|
1395
|
-
*
|
|
1396
|
-
*
|
|
2079
|
+
* Decrypts a single IEncryptedPayload envelope back into its scalar plaintext value using contextual AAD.
|
|
2080
|
+
* Validates recordId, fieldName, and sibling aadFields integrity.
|
|
2081
|
+
*
|
|
2082
|
+
* @param envelope - The IEncryptedPayload envelope to decrypt.
|
|
2083
|
+
* @param context - Field encryption context matching the encryption parameters.
|
|
1397
2084
|
*/
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
unwrappedKeyAlgorithm,
|
|
1417
|
-
extractable,
|
|
1418
|
-
["encrypt", "decrypt"]
|
|
1419
|
-
);
|
|
1420
|
-
} finally {
|
|
1421
|
-
new Uint8Array(unwrappedRawKey).fill(0);
|
|
2085
|
+
async decryptField(envelope, context) {
|
|
2086
|
+
if (!this.isEncrypted(envelope)) return envelope;
|
|
2087
|
+
const field = context?.fieldName || "field";
|
|
2088
|
+
const dummyData = {};
|
|
2089
|
+
if (context?.recordId !== void 0) {
|
|
2090
|
+
dummyData.id = context.recordId;
|
|
2091
|
+
}
|
|
2092
|
+
const { aadFields, ...restContext } = context || {};
|
|
2093
|
+
let boundOptions = { ...restContext };
|
|
2094
|
+
if (aadFields) {
|
|
2095
|
+
if (!Array.isArray(aadFields) && typeof aadFields === "object") {
|
|
2096
|
+
Object.assign(dummyData, aadFields);
|
|
2097
|
+
boundOptions.aadFields = Object.keys(aadFields);
|
|
2098
|
+
} else if (Array.isArray(aadFields)) {
|
|
2099
|
+
boundOptions.aadFields = aadFields;
|
|
2100
|
+
if (context?.record) {
|
|
2101
|
+
Object.assign(dummyData, context.record);
|
|
2102
|
+
}
|
|
1422
2103
|
}
|
|
1423
2104
|
}
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
);
|
|
2105
|
+
const resolvedOptions = this._bindFieldAad(dummyData, field, boundOptions);
|
|
2106
|
+
return this.decryptValue(envelope, resolvedOptions);
|
|
1427
2107
|
}
|
|
1428
|
-
// ── Key Derivation Functions (PBKDF2 & HKDF) ──────────────────────────────
|
|
1429
2108
|
/**
|
|
1430
|
-
*
|
|
1431
|
-
*
|
|
2109
|
+
* Top-level polymorphic encryption method.
|
|
2110
|
+
* Automatically performs value-level encryption:
|
|
2111
|
+
* - If data is an array of objects: recursively encrypts each row.
|
|
2112
|
+
* - If data is an object: encrypts specified fields (or all values if valueEncryption is true),
|
|
2113
|
+
* preserving JSON object keys so backend APIs and ORMs can parse keys and perform upserts.
|
|
2114
|
+
* - If data is a primitive or whole-payload envelope: encrypts as a single value.
|
|
1432
2115
|
*/
|
|
1433
|
-
|
|
1434
|
-
if (
|
|
1435
|
-
|
|
2116
|
+
async encrypt(data, options) {
|
|
2117
|
+
if (data === void 0 || data === null) return data;
|
|
2118
|
+
if (this.isEncrypted(data)) return data;
|
|
2119
|
+
options = this._resolveOutboundKeyringOptions(options);
|
|
2120
|
+
if (options?.valueEncryption === false && (!options.fields || options.fields.length === 0)) {
|
|
2121
|
+
return this.encryptValue(data, options);
|
|
1436
2122
|
}
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
2123
|
+
if (Array.isArray(data)) {
|
|
2124
|
+
if (data.length === 0) return data;
|
|
2125
|
+
if (options?.fields && options.fields.length > 0) {
|
|
2126
|
+
const fields = options.fields;
|
|
2127
|
+
let hasUnencrypted = false;
|
|
2128
|
+
for (let i = 0; i < data.length; i++) {
|
|
2129
|
+
const item = data[i];
|
|
2130
|
+
if (item && typeof item === "object") {
|
|
2131
|
+
for (let f = 0; f < fields.length; f++) {
|
|
2132
|
+
const val = item[fields[f]];
|
|
2133
|
+
if (val !== void 0 && val !== null && !this.isEncrypted(val)) {
|
|
2134
|
+
hasUnencrypted = true;
|
|
2135
|
+
break;
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
if (hasUnencrypted) break;
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
if (!hasUnencrypted) {
|
|
2142
|
+
return data;
|
|
2143
|
+
}
|
|
2144
|
+
await this.warmupKey(options);
|
|
2145
|
+
const itemOptions = { ...options, _warmed: true };
|
|
2146
|
+
return Promise.all(data.map((item) => this.encrypt(item, itemOptions)));
|
|
2147
|
+
}
|
|
2148
|
+
if (options?.valueEncryption === true) {
|
|
2149
|
+
await this.warmupKey(options);
|
|
2150
|
+
const itemOptions = { ...options, _warmed: true };
|
|
2151
|
+
return Promise.all(data.map((item) => this.encrypt(item, itemOptions)));
|
|
2152
|
+
}
|
|
2153
|
+
return this.encryptValue(data, options);
|
|
2154
|
+
}
|
|
2155
|
+
if (typeof data === "object" && !ArrayBuffer.isView(data) && !(data instanceof ArrayBuffer)) {
|
|
2156
|
+
if (options?.fields && options.fields.length > 0) {
|
|
2157
|
+
const fieldsToEncrypt = options.fields.filter(
|
|
2158
|
+
(field) => Object.prototype.hasOwnProperty.call(data, field) && data[field] !== void 0 && data[field] !== null && !this.isEncrypted(data[field])
|
|
1451
2159
|
);
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
2160
|
+
if (fieldsToEncrypt.length === 0) {
|
|
2161
|
+
return data;
|
|
2162
|
+
}
|
|
2163
|
+
if (!options?._warmed) {
|
|
2164
|
+
await this.warmupKey(options);
|
|
2165
|
+
}
|
|
2166
|
+
const result = { ...data };
|
|
2167
|
+
const entries = await Promise.all(
|
|
2168
|
+
fieldsToEncrypt.map(async (field) => {
|
|
2169
|
+
const fieldOptions = this._bindFieldAad(data, field, options);
|
|
2170
|
+
return [field, await this.encryptValue(result[field], fieldOptions)];
|
|
2171
|
+
})
|
|
1463
2172
|
);
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
2173
|
+
for (const [field, encryptedVal] of entries) {
|
|
2174
|
+
safeSetProperty(result, field, encryptedVal);
|
|
2175
|
+
}
|
|
2176
|
+
return result;
|
|
1468
2177
|
}
|
|
1469
|
-
if (options
|
|
1470
|
-
const
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
"HKDF",
|
|
1474
|
-
false,
|
|
1475
|
-
["deriveKey"]
|
|
2178
|
+
if (options?.valueEncryption === true) {
|
|
2179
|
+
const result = { ...data };
|
|
2180
|
+
const keysToEncrypt = Object.keys(result).filter(
|
|
2181
|
+
(key) => result[key] !== void 0 && result[key] !== null && !this.isEncrypted(result[key])
|
|
1476
2182
|
);
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
["encrypt", "decrypt"]
|
|
2183
|
+
if (keysToEncrypt.length === 0) {
|
|
2184
|
+
return result;
|
|
2185
|
+
}
|
|
2186
|
+
if (!options?._warmed) {
|
|
2187
|
+
await this.warmupKey(options);
|
|
2188
|
+
}
|
|
2189
|
+
const entries = await Promise.all(
|
|
2190
|
+
keysToEncrypt.map(async (key) => {
|
|
2191
|
+
const fieldOptions = this._bindFieldAad(data, key, options);
|
|
2192
|
+
return [key, await this.encryptValue(result[key], fieldOptions)];
|
|
2193
|
+
})
|
|
1489
2194
|
);
|
|
1490
|
-
|
|
1491
|
-
key
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
}
|
|
1495
|
-
throw new Error(`[CKeyManager] Unsupported KDF strategy: ${options.kdf}`);
|
|
1496
|
-
} finally {
|
|
1497
|
-
if (typeof passphrase === "string") {
|
|
1498
|
-
rawKeyBytes.fill(0);
|
|
2195
|
+
for (const [key, encryptedVal] of entries) {
|
|
2196
|
+
safeSetProperty(result, key, encryptedVal);
|
|
2197
|
+
}
|
|
2198
|
+
return result;
|
|
1499
2199
|
}
|
|
1500
2200
|
}
|
|
2201
|
+
return this.encryptValue(data, options);
|
|
1501
2202
|
}
|
|
1502
2203
|
/**
|
|
1503
|
-
*
|
|
1504
|
-
*
|
|
2204
|
+
* Top-level polymorphic decryption method.
|
|
2205
|
+
* Automatically performs value-level decryption:
|
|
2206
|
+
* - If payload is an encrypted envelope: validates metadata and decrypts directly via decryptValue.
|
|
2207
|
+
* - If payload is an array of objects: decrypts each row (batched with local memoization).
|
|
2208
|
+
* - If payload is an object: decrypts any encrypted value cells across fields.
|
|
2209
|
+
* In strict mode:
|
|
2210
|
+
* - Unencrypted plaintext in configured `fields` triggers a decryption error.
|
|
2211
|
+
* - Envelopes are validated against `allowedAlgorithms` and `allowedKids`.
|
|
2212
|
+
* - Errors are routed through `onDecryptError` or populated into `__decryptErrors`.
|
|
1505
2213
|
*/
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
2214
|
+
async decrypt(payload, options) {
|
|
2215
|
+
if (payload === void 0 || payload === null) return payload;
|
|
2216
|
+
if (this.isEncrypted(payload)) {
|
|
2217
|
+
const envelopeKey = this._resolveDecryptionKeyForEnvelope(payload, options);
|
|
2218
|
+
const envelopeOptions = envelopeKey !== options?.key ? { ...options, key: envelopeKey } : options;
|
|
2219
|
+
const metaErr = this._validateEnvelopeMetadata(payload, envelopeOptions);
|
|
2220
|
+
if (metaErr) {
|
|
2221
|
+
return this._handleDecryptError(metaErr, payload, "", payload, void 0, envelopeOptions);
|
|
2222
|
+
}
|
|
2223
|
+
try {
|
|
2224
|
+
return await this.decryptValue(payload, envelopeOptions);
|
|
2225
|
+
} catch (err) {
|
|
2226
|
+
return this._handleDecryptError(err, payload, "", payload, void 0, envelopeOptions);
|
|
2227
|
+
}
|
|
1513
2228
|
}
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
2229
|
+
if (Array.isArray(payload)) {
|
|
2230
|
+
if (payload.length === 0) return payload;
|
|
2231
|
+
const localMemo = /* @__PURE__ */ new Map();
|
|
2232
|
+
const decryptRow = async (item, rowIndex) => {
|
|
2233
|
+
if (!item) return item;
|
|
2234
|
+
if (this.isEncrypted(item)) {
|
|
2235
|
+
const envelopeKey = this._resolveDecryptionKeyForEnvelope(item, options);
|
|
2236
|
+
const envelopeOptions = envelopeKey !== options?.key ? { ...options, key: envelopeKey } : options;
|
|
2237
|
+
const metaErr = this._validateEnvelopeMetadata(item, envelopeOptions);
|
|
2238
|
+
if (metaErr) {
|
|
2239
|
+
return this._handleDecryptError(metaErr, item, "", item, rowIndex, envelopeOptions);
|
|
2240
|
+
}
|
|
2241
|
+
const ct = item.ciphertext;
|
|
2242
|
+
const iv = item.iv || "";
|
|
2243
|
+
const aadPart = options?.aad ? typeof options.aad === "string" ? options.aad : "" : "";
|
|
2244
|
+
const memoKey = `${ct}:${iv}:${aadPart}:${item.kid || ""}`;
|
|
2245
|
+
try {
|
|
2246
|
+
if (!localMemo.has(memoKey)) {
|
|
2247
|
+
localMemo.set(memoKey, this.decryptValue(item, envelopeOptions));
|
|
2248
|
+
}
|
|
2249
|
+
return await localMemo.get(memoKey);
|
|
2250
|
+
} catch (err) {
|
|
2251
|
+
return this._handleDecryptError(err, item, "", item, rowIndex, envelopeOptions);
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
if (typeof item === "object" && !ArrayBuffer.isView(item) && !(item instanceof ArrayBuffer)) {
|
|
2255
|
+
const targetKeys = options?.fields && options.fields.length > 0 ? options.fields.filter((f) => Object.prototype.hasOwnProperty.call(item, f)) : Object.keys(item);
|
|
2256
|
+
const keysToDecrypt = targetKeys.filter((key) => this.isEncrypted(item[key]));
|
|
2257
|
+
const strictViolationKeys = options?.strict === true && options?.fields && options.fields.length > 0 ? targetKeys.filter((key) => item[key] !== void 0 && item[key] !== null && !this.isEncrypted(item[key])) : [];
|
|
2258
|
+
if (keysToDecrypt.length === 0 && strictViolationKeys.length === 0) {
|
|
2259
|
+
return item;
|
|
2260
|
+
}
|
|
2261
|
+
const proto = Object.getPrototypeOf(item);
|
|
2262
|
+
const result = proto && proto !== Object.prototype ? Object.assign(Object.create(proto), item) : { ...item };
|
|
2263
|
+
for (const key of strictViolationKeys) {
|
|
2264
|
+
const err = new Error(`[AEncryptionProvider] Strict decryption violation: field '${key}' contains unencrypted plaintext.`);
|
|
2265
|
+
const fallbackVal = this._handleDecryptError(err, result, key, item[key], rowIndex, options);
|
|
2266
|
+
safeSetProperty(result, key, fallbackVal);
|
|
2267
|
+
}
|
|
2268
|
+
await Promise.all(
|
|
2269
|
+
keysToDecrypt.map(async (key) => {
|
|
2270
|
+
const env = item[key];
|
|
2271
|
+
const envelopeKey = this._resolveDecryptionKeyForEnvelope(env, options);
|
|
2272
|
+
const baseOptions = envelopeKey !== options?.key ? { ...options, key: envelopeKey } : options;
|
|
2273
|
+
const boundOptions = this._bindFieldAad(item, key, baseOptions);
|
|
2274
|
+
const hasBoundAad = boundOptions.aad !== baseOptions?.aad;
|
|
2275
|
+
const metaErr = this._validateEnvelopeMetadata(env, boundOptions);
|
|
2276
|
+
if (metaErr) {
|
|
2277
|
+
const fallbackVal = this._handleDecryptError(metaErr, result, key, env, rowIndex, boundOptions);
|
|
2278
|
+
safeSetProperty(result, key, fallbackVal);
|
|
2279
|
+
return;
|
|
2280
|
+
}
|
|
2281
|
+
const ct = env.ciphertext;
|
|
2282
|
+
const iv = env.iv || "";
|
|
2283
|
+
const aadPart = boundOptions.aad ? typeof boundOptions.aad === "string" ? boundOptions.aad : "" : "";
|
|
2284
|
+
const memoKey = `${ct}:${iv}:${aadPart}:${env.kid || ""}`;
|
|
2285
|
+
try {
|
|
2286
|
+
if (!localMemo.has(memoKey)) {
|
|
2287
|
+
const decryptPromise = (async () => {
|
|
2288
|
+
try {
|
|
2289
|
+
return await this.decryptValue(env, boundOptions);
|
|
2290
|
+
} catch (err) {
|
|
2291
|
+
const canFallback = hasBoundAad && (!options?.aadFields || options.aadFields.length === 0) && typeof options?.aad !== "function";
|
|
2292
|
+
if (canFallback && isAuthOrDecryptionError(err)) {
|
|
2293
|
+
return await this.decryptValue(env, baseOptions);
|
|
2294
|
+
}
|
|
2295
|
+
throw err;
|
|
2296
|
+
}
|
|
2297
|
+
})();
|
|
2298
|
+
localMemo.set(memoKey, decryptPromise);
|
|
2299
|
+
}
|
|
2300
|
+
const decryptedVal = await localMemo.get(memoKey);
|
|
2301
|
+
safeSetProperty(result, key, decryptedVal);
|
|
2302
|
+
} catch (err) {
|
|
2303
|
+
const fallbackVal = this._handleDecryptError(err, result, key, env, rowIndex, boundOptions);
|
|
2304
|
+
safeSetProperty(result, key, fallbackVal);
|
|
2305
|
+
}
|
|
2306
|
+
})
|
|
2307
|
+
);
|
|
2308
|
+
return result;
|
|
2309
|
+
}
|
|
2310
|
+
return item;
|
|
2311
|
+
};
|
|
2312
|
+
if (payload.length <= 100) {
|
|
2313
|
+
return Promise.all(payload.map((item, index) => decryptRow(item, index)));
|
|
2314
|
+
}
|
|
2315
|
+
const batchSize = 1e3;
|
|
2316
|
+
const results = new Array(payload.length);
|
|
2317
|
+
for (let i = 0; i < payload.length; i += batchSize) {
|
|
2318
|
+
const end = Math.min(i + batchSize, payload.length);
|
|
2319
|
+
const chunk = payload.slice(i, end);
|
|
2320
|
+
const chunkRes = await Promise.all(chunk.map((item, localIdx) => decryptRow(item, i + localIdx)));
|
|
2321
|
+
for (let j = 0; j < chunkRes.length; j++) {
|
|
2322
|
+
results[i + j] = chunkRes[j];
|
|
2323
|
+
}
|
|
2324
|
+
if (end < payload.length && (i > 0 && i % 5e3 === 0)) {
|
|
2325
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
return results;
|
|
2329
|
+
}
|
|
2330
|
+
if (typeof payload === "object" && !ArrayBuffer.isView(payload) && !(payload instanceof ArrayBuffer)) {
|
|
2331
|
+
const targetKeys = options?.fields && options.fields.length > 0 ? options.fields.filter((f) => Object.prototype.hasOwnProperty.call(payload, f)) : Object.keys(payload);
|
|
2332
|
+
const keysToDecrypt = targetKeys.filter((key) => this.isEncrypted(payload[key]));
|
|
2333
|
+
const strictViolationKeys = options?.strict === true && options?.fields && options.fields.length > 0 ? targetKeys.filter((key) => payload[key] !== void 0 && payload[key] !== null && !this.isEncrypted(payload[key])) : [];
|
|
2334
|
+
if (keysToDecrypt.length === 0 && strictViolationKeys.length === 0) {
|
|
2335
|
+
return payload;
|
|
2336
|
+
}
|
|
2337
|
+
const proto = Object.getPrototypeOf(payload);
|
|
2338
|
+
const result = proto && proto !== Object.prototype ? Object.assign(Object.create(proto), payload) : { ...payload };
|
|
2339
|
+
for (const key of strictViolationKeys) {
|
|
2340
|
+
const err = new Error(`[AEncryptionProvider] Strict decryption violation: field '${key}' contains unencrypted plaintext.`);
|
|
2341
|
+
const fallbackVal = this._handleDecryptError(err, result, key, payload[key], void 0, options);
|
|
2342
|
+
safeSetProperty(result, key, fallbackVal);
|
|
2343
|
+
}
|
|
2344
|
+
await Promise.all(
|
|
2345
|
+
keysToDecrypt.map(async (key) => {
|
|
2346
|
+
const env = payload[key];
|
|
2347
|
+
const envelopeKey = this._resolveDecryptionKeyForEnvelope(env, options);
|
|
2348
|
+
const baseOptions = envelopeKey !== options?.key ? { ...options, key: envelopeKey } : options;
|
|
2349
|
+
const boundOptions = this._bindFieldAad(payload, key, baseOptions);
|
|
2350
|
+
const hasBoundAad = boundOptions.aad !== baseOptions?.aad;
|
|
2351
|
+
const metaErr = this._validateEnvelopeMetadata(env, boundOptions);
|
|
2352
|
+
if (metaErr) {
|
|
2353
|
+
const fallbackVal = this._handleDecryptError(metaErr, result, key, env, void 0, boundOptions);
|
|
2354
|
+
safeSetProperty(result, key, fallbackVal);
|
|
2355
|
+
return;
|
|
2356
|
+
}
|
|
2357
|
+
try {
|
|
2358
|
+
let decryptedVal;
|
|
2359
|
+
try {
|
|
2360
|
+
decryptedVal = await this.decryptValue(env, boundOptions);
|
|
2361
|
+
} catch (err) {
|
|
2362
|
+
const canFallback = hasBoundAad && (!options?.aadFields || options.aadFields.length === 0) && typeof options?.aad !== "function";
|
|
2363
|
+
if (canFallback && isAuthOrDecryptionError(err)) {
|
|
2364
|
+
decryptedVal = await this.decryptValue(env, baseOptions);
|
|
2365
|
+
} else {
|
|
2366
|
+
throw err;
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
safeSetProperty(result, key, decryptedVal);
|
|
2370
|
+
} catch (err) {
|
|
2371
|
+
const fallbackVal = this._handleDecryptError(err, result, key, env, void 0, boundOptions);
|
|
2372
|
+
safeSetProperty(result, key, fallbackVal);
|
|
2373
|
+
}
|
|
2374
|
+
})
|
|
2375
|
+
);
|
|
2376
|
+
return result;
|
|
2377
|
+
}
|
|
2378
|
+
return payload;
|
|
1530
2379
|
}
|
|
1531
2380
|
}
|
|
1532
2381
|
|
|
@@ -1541,6 +2390,7 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
1541
2390
|
_cachedKeyId;
|
|
1542
2391
|
_configuredKid;
|
|
1543
2392
|
_decryptSaltCache = /* @__PURE__ */ new Map();
|
|
2393
|
+
_keyringCache = /* @__PURE__ */ new Map();
|
|
1544
2394
|
setDefaultKey(key) {
|
|
1545
2395
|
super.setDefaultKey(key);
|
|
1546
2396
|
this._cachedKey = void 0;
|
|
@@ -1550,6 +2400,7 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
1550
2400
|
this._cachedHash = void 0;
|
|
1551
2401
|
this._cachedKeyId = void 0;
|
|
1552
2402
|
this._decryptSaltCache.clear();
|
|
2403
|
+
this._keyringCache.clear();
|
|
1553
2404
|
}
|
|
1554
2405
|
clearKeys() {
|
|
1555
2406
|
super.clearKeys();
|
|
@@ -1560,9 +2411,10 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
1560
2411
|
this._cachedHash = void 0;
|
|
1561
2412
|
this._cachedKeyId = void 0;
|
|
1562
2413
|
this._decryptSaltCache.clear();
|
|
2414
|
+
this._keyringCache.clear();
|
|
1563
2415
|
}
|
|
1564
2416
|
constructor(key, options) {
|
|
1565
|
-
super(key);
|
|
2417
|
+
super(key, options);
|
|
1566
2418
|
if (options) {
|
|
1567
2419
|
if (options.kid || options.keyId) {
|
|
1568
2420
|
this._configuredKid = options.kid || options.keyId;
|
|
@@ -1612,14 +2464,21 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
1612
2464
|
}
|
|
1613
2465
|
async _resolveCryptoKeyInternal(keyProvider, kdfOverride, saltOverride, options, iterationsOverride) {
|
|
1614
2466
|
const rawKey = await this.resolveRawKey(keyProvider);
|
|
2467
|
+
const currentKeyId = getSafeKeyIdentifier(rawKey);
|
|
1615
2468
|
if (typeof rawKey !== "string" && rawKey.algorithm) {
|
|
1616
|
-
|
|
2469
|
+
const res2 = { cryptoKey: rawKey, kdf: "direct" };
|
|
2470
|
+
this._keyringCache.set(currentKeyId, res2);
|
|
2471
|
+
if (!keyProvider || keyProvider === this.defaultKey) {
|
|
1617
2472
|
this._cachedKey = rawKey;
|
|
1618
2473
|
this._cachedKdf = "direct";
|
|
1619
2474
|
}
|
|
1620
|
-
return
|
|
2475
|
+
return res2;
|
|
1621
2476
|
}
|
|
1622
2477
|
if (rawKey instanceof Uint8Array || rawKey instanceof ArrayBuffer) {
|
|
2478
|
+
const cached = this._keyringCache.get(currentKeyId);
|
|
2479
|
+
if (cached) {
|
|
2480
|
+
return cached;
|
|
2481
|
+
}
|
|
1623
2482
|
if (!keyProvider && this._cachedKey && this._cachedKdf === "raw") {
|
|
1624
2483
|
return { cryptoKey: this._cachedKey, kdf: "raw" };
|
|
1625
2484
|
}
|
|
@@ -1636,11 +2495,13 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
1636
2495
|
false,
|
|
1637
2496
|
["encrypt", "decrypt"]
|
|
1638
2497
|
);
|
|
1639
|
-
|
|
2498
|
+
const res2 = { cryptoKey: cryptoKey2, kdf: "raw" };
|
|
2499
|
+
this._keyringCache.set(currentKeyId, res2);
|
|
2500
|
+
if (!keyProvider || keyProvider === this.defaultKey) {
|
|
1640
2501
|
this._cachedKey = cryptoKey2;
|
|
1641
2502
|
this._cachedKdf = "raw";
|
|
1642
2503
|
}
|
|
1643
|
-
return
|
|
2504
|
+
return res2;
|
|
1644
2505
|
}
|
|
1645
2506
|
const passphrase = rawKey;
|
|
1646
2507
|
const kdf = (kdfOverride || options?.kdf || this._defaultKdf || "pbkdf2").toLowerCase();
|
|
@@ -1650,12 +2511,21 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
1650
2511
|
const matchesHash = !targetHash || this._cachedHash !== void 0 && targetHash.toLowerCase() === this._cachedHash.toLowerCase();
|
|
1651
2512
|
const matchesKdf = this._cachedKdf !== void 0 && kdf === this._cachedKdf.toLowerCase();
|
|
1652
2513
|
const matchesSalt = !saltOverride || this._cachedSalt !== void 0 && saltOverride === this._cachedSalt;
|
|
1653
|
-
const currentKeyId = getSafeKeyIdentifier(rawKey);
|
|
1654
2514
|
if (this._cachedKey && matchesSalt && !options?.kdfSalt && matchesKdf && matchesIter && matchesHash) {
|
|
1655
2515
|
if (this._cachedKeyId === currentKeyId) {
|
|
1656
2516
|
return { cryptoKey: this._cachedKey, salt: this._cachedSalt, kdf: this._cachedKdf, iter: this._cachedIter };
|
|
1657
2517
|
}
|
|
1658
2518
|
}
|
|
2519
|
+
if (!saltOverride && !options?.kdfSalt) {
|
|
2520
|
+
const cached = this._keyringCache.get(currentKeyId);
|
|
2521
|
+
if (cached) {
|
|
2522
|
+
const cMatchesKdf = !cached.kdf || cached.kdf.toLowerCase() === kdf;
|
|
2523
|
+
const cMatchesIter = !targetIter || cached.iter === targetIter;
|
|
2524
|
+
if (cMatchesKdf && cMatchesIter) {
|
|
2525
|
+
return cached;
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
1659
2529
|
if (kdf === "pbkdf2") {
|
|
1660
2530
|
const salt = saltOverride || options?.kdfSalt || this._kdfOptions?.salt;
|
|
1661
2531
|
const rawIterations = iterationsOverride || options?.kdfIterations || this._kdfOptions?.iterations || 31e4;
|
|
@@ -1667,15 +2537,19 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
1667
2537
|
iterations,
|
|
1668
2538
|
hash
|
|
1669
2539
|
});
|
|
2540
|
+
const res2 = { cryptoKey: derived.key, salt: derived.salt, kdf: "PBKDF2", iter: iterations };
|
|
1670
2541
|
if (!saltOverride && !options?.kdfSalt) {
|
|
1671
|
-
this.
|
|
1672
|
-
this.
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
2542
|
+
this._keyringCache.set(currentKeyId, res2);
|
|
2543
|
+
if (!keyProvider || keyProvider === this.defaultKey) {
|
|
2544
|
+
this._cachedKey = derived.key;
|
|
2545
|
+
this._cachedSalt = derived.salt;
|
|
2546
|
+
this._cachedKdf = "PBKDF2";
|
|
2547
|
+
this._cachedIter = iterations;
|
|
2548
|
+
this._cachedHash = hash;
|
|
2549
|
+
this._cachedKeyId = currentKeyId;
|
|
2550
|
+
}
|
|
1677
2551
|
}
|
|
1678
|
-
return
|
|
2552
|
+
return res2;
|
|
1679
2553
|
}
|
|
1680
2554
|
if (kdf === "hkdf") {
|
|
1681
2555
|
if (typeof rawKey === "string") {
|
|
@@ -1692,14 +2566,18 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
1692
2566
|
hash,
|
|
1693
2567
|
info
|
|
1694
2568
|
});
|
|
2569
|
+
const res2 = { cryptoKey: derived.key, salt: derived.salt, kdf: "HKDF" };
|
|
1695
2570
|
if (!saltOverride && !options?.kdfSalt) {
|
|
1696
|
-
this.
|
|
1697
|
-
this.
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
2571
|
+
this._keyringCache.set(currentKeyId, res2);
|
|
2572
|
+
if (!keyProvider || keyProvider === this.defaultKey) {
|
|
2573
|
+
this._cachedKey = derived.key;
|
|
2574
|
+
this._cachedSalt = derived.salt;
|
|
2575
|
+
this._cachedKdf = "HKDF";
|
|
2576
|
+
this._cachedHash = hash;
|
|
2577
|
+
this._cachedKeyId = currentKeyId;
|
|
2578
|
+
}
|
|
1701
2579
|
}
|
|
1702
|
-
return
|
|
2580
|
+
return res2;
|
|
1703
2581
|
}
|
|
1704
2582
|
if (kdf === "none") {
|
|
1705
2583
|
if (saltOverride) {
|
|
@@ -1731,11 +2609,13 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
1731
2609
|
} finally {
|
|
1732
2610
|
new Uint8Array(keyHash).fill(0);
|
|
1733
2611
|
}
|
|
1734
|
-
|
|
2612
|
+
const res = { cryptoKey, kdf: "none" };
|
|
2613
|
+
this._keyringCache.set(currentKeyId, res);
|
|
2614
|
+
if (!keyProvider || keyProvider === this.defaultKey) {
|
|
1735
2615
|
this._cachedKey = cryptoKey;
|
|
1736
2616
|
this._cachedKdf = "none";
|
|
1737
2617
|
}
|
|
1738
|
-
return
|
|
2618
|
+
return res;
|
|
1739
2619
|
}
|
|
1740
2620
|
static isEncrypted(payload) {
|
|
1741
2621
|
if (!payload || typeof payload !== "object") return false;
|
|
@@ -1745,6 +2625,29 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
1745
2625
|
return CWebCryptoEncryptionProvider.isEncrypted(payload);
|
|
1746
2626
|
}
|
|
1747
2627
|
async warmupKey(options) {
|
|
2628
|
+
const effectiveKeys = options?.keys || this.keys;
|
|
2629
|
+
if (effectiveKeys && Object.keys(effectiveKeys).length > 0) {
|
|
2630
|
+
const promises = Object.entries(effectiveKeys).map(async ([kid, keyProv]) => {
|
|
2631
|
+
const res2 = await this.resolveCryptoKey(keyProv, void 0, void 0, { ...options, key: keyProv, kid });
|
|
2632
|
+
const rawKey = await this.resolveRawKey(keyProv);
|
|
2633
|
+
const keyId = getSafeKeyIdentifier(rawKey);
|
|
2634
|
+
this._keyringCache.set(keyId, res2);
|
|
2635
|
+
this._keyringCache.set(`kid:${kid}`, res2);
|
|
2636
|
+
return { kid, res: res2 };
|
|
2637
|
+
});
|
|
2638
|
+
const results = await Promise.all(promises);
|
|
2639
|
+
const effectiveActiveKid = options?.activeKid || this.activeKid;
|
|
2640
|
+
const primary = effectiveActiveKid && results.find((r) => r.kid === effectiveActiveKid)?.res || results[0]?.res;
|
|
2641
|
+
if (primary) {
|
|
2642
|
+
this._cachedKey = primary.cryptoKey;
|
|
2643
|
+
this._cachedSalt = primary.salt;
|
|
2644
|
+
this._cachedKdf = primary.kdf;
|
|
2645
|
+
this._cachedIter = primary.iter;
|
|
2646
|
+
this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
|
|
2647
|
+
this._cachedKeyId = getKeyIdentifier(options?.key, this.defaultKey);
|
|
2648
|
+
}
|
|
2649
|
+
return;
|
|
2650
|
+
}
|
|
1748
2651
|
const res = await this.resolveCryptoKey(options?.key, void 0, void 0, options);
|
|
1749
2652
|
this._cachedKey = res.cryptoKey;
|
|
1750
2653
|
this._cachedSalt = res.salt;
|
|
@@ -1787,7 +2690,7 @@ class CWebCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
1787
2690
|
cryptoKey,
|
|
1788
2691
|
textEncoder$1.encode(jsonString)
|
|
1789
2692
|
);
|
|
1790
|
-
const kid = options?.kid || options?.keyId || this._configuredKid;
|
|
2693
|
+
const kid = options?.kid || options?.keyId || this.activeKid || this._configuredKid;
|
|
1791
2694
|
return {
|
|
1792
2695
|
__encrypted: true,
|
|
1793
2696
|
ciphertext: CKeyManager.toBase64(ciphertextBuffer),
|
|
@@ -3110,12 +4013,26 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3110
4013
|
_cachedHash;
|
|
3111
4014
|
_cachedKeyId;
|
|
3112
4015
|
_configuredKid;
|
|
4016
|
+
_keyringCache = /* @__PURE__ */ new Map();
|
|
4017
|
+
isKeyRetained(key) {
|
|
4018
|
+
if (key === this._cachedKey) return true;
|
|
4019
|
+
for (const entry of this._keyringCache.values()) {
|
|
4020
|
+
if (entry.key === key) return true;
|
|
4021
|
+
}
|
|
4022
|
+
return false;
|
|
4023
|
+
}
|
|
3113
4024
|
setDefaultKey(key) {
|
|
3114
4025
|
super.setDefaultKey(key);
|
|
3115
4026
|
if (this._cachedKey) {
|
|
3116
4027
|
this._cachedKey.fill(0);
|
|
3117
4028
|
this._cachedKey = void 0;
|
|
3118
4029
|
}
|
|
4030
|
+
for (const entry of this._keyringCache.values()) {
|
|
4031
|
+
if (entry.key !== this._cachedKey) {
|
|
4032
|
+
entry.key.fill(0);
|
|
4033
|
+
}
|
|
4034
|
+
}
|
|
4035
|
+
this._keyringCache.clear();
|
|
3119
4036
|
this._cachedSalt = void 0;
|
|
3120
4037
|
this._cachedKdf = void 0;
|
|
3121
4038
|
this._cachedIter = void 0;
|
|
@@ -3128,6 +4045,12 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3128
4045
|
this._cachedKey.fill(0);
|
|
3129
4046
|
this._cachedKey = void 0;
|
|
3130
4047
|
}
|
|
4048
|
+
for (const entry of this._keyringCache.values()) {
|
|
4049
|
+
if (entry.key !== this._cachedKey) {
|
|
4050
|
+
entry.key.fill(0);
|
|
4051
|
+
}
|
|
4052
|
+
}
|
|
4053
|
+
this._keyringCache.clear();
|
|
3131
4054
|
this._cachedSalt = void 0;
|
|
3132
4055
|
this._cachedKdf = void 0;
|
|
3133
4056
|
this._cachedIter = void 0;
|
|
@@ -3135,6 +4058,29 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3135
4058
|
this._cachedKeyId = void 0;
|
|
3136
4059
|
}
|
|
3137
4060
|
async warmupKey(options) {
|
|
4061
|
+
const effectiveKeys = options?.keys || this.keys;
|
|
4062
|
+
if (effectiveKeys && Object.keys(effectiveKeys).length > 0) {
|
|
4063
|
+
const promises = Object.entries(effectiveKeys).map(async ([kid, keyProv]) => {
|
|
4064
|
+
const res2 = await this.deriveKeyAsync(keyProv, void 0, void 0, { ...options, key: keyProv, kid });
|
|
4065
|
+
const raw = await this.resolveRawKeyAsync(keyProv);
|
|
4066
|
+
const keyId = getSafeKeyIdentifier(raw);
|
|
4067
|
+
this._keyringCache.set(keyId, res2);
|
|
4068
|
+
this._keyringCache.set(`kid:${kid}`, res2);
|
|
4069
|
+
return { kid, res: res2 };
|
|
4070
|
+
});
|
|
4071
|
+
const results = await Promise.all(promises);
|
|
4072
|
+
const effectiveActiveKid = options?.activeKid || this.activeKid;
|
|
4073
|
+
const primary = effectiveActiveKid && results.find((r) => r.kid === effectiveActiveKid)?.res || results[0]?.res;
|
|
4074
|
+
if (primary) {
|
|
4075
|
+
this._cachedKey = primary.key;
|
|
4076
|
+
this._cachedSalt = primary.salt;
|
|
4077
|
+
this._cachedKdf = primary.kdf;
|
|
4078
|
+
this._cachedIter = primary.iter;
|
|
4079
|
+
this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
|
|
4080
|
+
this._cachedKeyId = getSafeKeyIdentifier(await this.resolveRawKeyAsync(options?.key));
|
|
4081
|
+
}
|
|
4082
|
+
return;
|
|
4083
|
+
}
|
|
3138
4084
|
const res = await this.deriveKeyAsync(options?.key, void 0, void 0, options);
|
|
3139
4085
|
if (!options?.key) {
|
|
3140
4086
|
this._cachedKey = res.key;
|
|
@@ -3149,6 +4095,30 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3149
4095
|
* Synchronously pre-derives and caches the encryption key for subsequent synchronous operations.
|
|
3150
4096
|
*/
|
|
3151
4097
|
warmupKeySync(options) {
|
|
4098
|
+
const effectiveKeys = options?.keys || this.keys;
|
|
4099
|
+
if (effectiveKeys && Object.keys(effectiveKeys).length > 0) {
|
|
4100
|
+
const entries = Object.entries(effectiveKeys);
|
|
4101
|
+
const results = [];
|
|
4102
|
+
for (const [kid, keyProv] of entries) {
|
|
4103
|
+
const res2 = this.deriveKeySync(keyProv, void 0, void 0, { ...options, key: keyProv, kid });
|
|
4104
|
+
const raw = this.resolveRawKeySync(keyProv);
|
|
4105
|
+
const keyId = getSafeKeyIdentifier(raw);
|
|
4106
|
+
this._keyringCache.set(keyId, res2);
|
|
4107
|
+
this._keyringCache.set(`kid:${kid}`, res2);
|
|
4108
|
+
results.push({ kid, res: res2 });
|
|
4109
|
+
}
|
|
4110
|
+
const effectiveActiveKid = options?.activeKid || this.activeKid;
|
|
4111
|
+
const primary = effectiveActiveKid && results.find((r) => r.kid === effectiveActiveKid)?.res || results[0]?.res;
|
|
4112
|
+
if (primary) {
|
|
4113
|
+
this._cachedKey = primary.key;
|
|
4114
|
+
this._cachedSalt = primary.salt;
|
|
4115
|
+
this._cachedKdf = primary.kdf;
|
|
4116
|
+
this._cachedIter = primary.iter;
|
|
4117
|
+
this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
|
|
4118
|
+
this._cachedKeyId = getSafeKeyIdentifier(this.resolveRawKeySync(options?.key));
|
|
4119
|
+
}
|
|
4120
|
+
return;
|
|
4121
|
+
}
|
|
3152
4122
|
const res = this.deriveKeySync(options?.key, void 0, void 0, options);
|
|
3153
4123
|
if (!options?.key) {
|
|
3154
4124
|
this._cachedKey = res.key;
|
|
@@ -3167,7 +4137,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3167
4137
|
return Buffer.from(infoRaw);
|
|
3168
4138
|
}
|
|
3169
4139
|
constructor(key, options) {
|
|
3170
|
-
super(key);
|
|
4140
|
+
super(key, options);
|
|
3171
4141
|
if (options) {
|
|
3172
4142
|
if (options.kid || options.keyId) {
|
|
3173
4143
|
this._configuredKid = options.kid || options.keyId;
|
|
@@ -3185,7 +4155,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3185
4155
|
}
|
|
3186
4156
|
}
|
|
3187
4157
|
resolveRawKeySync(keyProvider) {
|
|
3188
|
-
const target = keyProvider || this.defaultKey;
|
|
4158
|
+
const target = keyProvider || (this.activeKid && this.keys ? this.keys[this.activeKid] : void 0) || this.defaultKey;
|
|
3189
4159
|
if (!target) {
|
|
3190
4160
|
throw new Error("[CNodeCryptoEncryptionProvider] No encryption key or keyProvider was supplied.");
|
|
3191
4161
|
}
|
|
@@ -3215,7 +4185,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3215
4185
|
return rawKey;
|
|
3216
4186
|
}
|
|
3217
4187
|
async resolveRawKeyAsync(keyProvider) {
|
|
3218
|
-
const target = keyProvider || this.defaultKey;
|
|
4188
|
+
const target = keyProvider || (this.activeKid && this.keys ? this.keys[this.activeKid] : void 0) || this.defaultKey;
|
|
3219
4189
|
if (!target) {
|
|
3220
4190
|
throw new Error("[CNodeCryptoEncryptionProvider] No encryption key or keyProvider was supplied.");
|
|
3221
4191
|
}
|
|
@@ -3246,15 +4216,16 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3246
4216
|
const currentKeyId = getSafeKeyIdentifier(raw);
|
|
3247
4217
|
if (Buffer.isBuffer(raw)) {
|
|
3248
4218
|
if (raw.length === 32) {
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
4219
|
+
const cached = this._keyringCache.get(currentKeyId);
|
|
4220
|
+
if (cached) return cached;
|
|
4221
|
+
const res2 = { key: Buffer.from(raw), kdf: "raw" };
|
|
4222
|
+
this._keyringCache.set(currentKeyId, res2);
|
|
4223
|
+
if (!keyProvider || keyProvider === this.defaultKey) {
|
|
4224
|
+
this._cachedKey = res2.key;
|
|
4225
|
+
this._cachedKdf = "raw";
|
|
4226
|
+
this._cachedKeyId = currentKeyId;
|
|
3256
4227
|
}
|
|
3257
|
-
return
|
|
4228
|
+
return res2;
|
|
3258
4229
|
}
|
|
3259
4230
|
throw new Error(
|
|
3260
4231
|
`[CNodeCryptoEncryptionProvider] AES-GCM-256 requires a 32-byte key. Received a Buffer of ${raw.length} bytes. Provide exactly 32 bytes or a string passphrase.`
|
|
@@ -3273,6 +4244,16 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3273
4244
|
return { key: this._cachedKey, salt: this._cachedSalt, kdf: this._cachedKdf, iter: this._cachedIter };
|
|
3274
4245
|
}
|
|
3275
4246
|
}
|
|
4247
|
+
if (!saltOverride && !options?.kdfSalt) {
|
|
4248
|
+
const cached = this._keyringCache.get(currentKeyId);
|
|
4249
|
+
if (cached) {
|
|
4250
|
+
const cMatchesKdf = !cached.kdf || cached.kdf.toLowerCase() === kdf;
|
|
4251
|
+
const cMatchesIter = !targetIter || cached.iter === targetIter;
|
|
4252
|
+
if (cMatchesKdf && cMatchesIter) {
|
|
4253
|
+
return cached;
|
|
4254
|
+
}
|
|
4255
|
+
}
|
|
4256
|
+
}
|
|
3276
4257
|
if (kdf === "pbkdf2") {
|
|
3277
4258
|
const s = saltOverride || options?.kdfSalt || this._kdfOptions?.salt;
|
|
3278
4259
|
const saltBytes = CKeyManager.resolveSalt(s);
|
|
@@ -3281,20 +4262,24 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3281
4262
|
const iterations = Math.min(Math.max(rawIter, 1e3), 6e5);
|
|
3282
4263
|
const hash = (options?.kdfHash || this._kdfOptions?.hash || "SHA-256").toLowerCase().replace("-", "");
|
|
3283
4264
|
const key2 = crypto$1.pbkdf2Sync(passphrase, saltBuf, iterations, 32, hash);
|
|
3284
|
-
|
|
3285
|
-
this._cachedKey = key2;
|
|
3286
|
-
this._cachedSalt = saltBuf.toString("base64");
|
|
3287
|
-
this._cachedKdf = "PBKDF2";
|
|
3288
|
-
this._cachedIter = iterations;
|
|
3289
|
-
this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
|
|
3290
|
-
this._cachedKeyId = currentKeyId;
|
|
3291
|
-
}
|
|
3292
|
-
return {
|
|
4265
|
+
const res2 = {
|
|
3293
4266
|
key: key2,
|
|
3294
4267
|
salt: saltBuf.toString("base64"),
|
|
3295
4268
|
kdf: "PBKDF2",
|
|
3296
4269
|
iter: iterations
|
|
3297
4270
|
};
|
|
4271
|
+
if (!saltOverride && !options?.kdfSalt) {
|
|
4272
|
+
this._keyringCache.set(currentKeyId, res2);
|
|
4273
|
+
if (!keyProvider || keyProvider === this.defaultKey) {
|
|
4274
|
+
this._cachedKey = key2;
|
|
4275
|
+
this._cachedSalt = saltBuf.toString("base64");
|
|
4276
|
+
this._cachedKdf = "PBKDF2";
|
|
4277
|
+
this._cachedIter = iterations;
|
|
4278
|
+
this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
|
|
4279
|
+
this._cachedKeyId = currentKeyId;
|
|
4280
|
+
}
|
|
4281
|
+
}
|
|
4282
|
+
return res2;
|
|
3298
4283
|
}
|
|
3299
4284
|
if (kdf === "hkdf") {
|
|
3300
4285
|
if (typeof raw === "string") {
|
|
@@ -3308,49 +4293,56 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3308
4293
|
const hash = (options?.kdfHash || this._kdfOptions?.hash || "SHA-256").toLowerCase().replace("-", "");
|
|
3309
4294
|
const info = this.resolveInfoBuffer(options?.info || this._kdfOptions?.info);
|
|
3310
4295
|
const key2 = Buffer.from(crypto$1.hkdfSync(hash, passphrase, saltBuf, info, 32));
|
|
3311
|
-
|
|
3312
|
-
this._cachedKey = key2;
|
|
3313
|
-
this._cachedSalt = saltBuf.toString("base64");
|
|
3314
|
-
this._cachedKdf = "HKDF";
|
|
3315
|
-
this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
|
|
3316
|
-
this._cachedKeyId = currentKeyId;
|
|
3317
|
-
}
|
|
3318
|
-
return {
|
|
4296
|
+
const res2 = {
|
|
3319
4297
|
key: key2,
|
|
3320
4298
|
salt: saltBuf.toString("base64"),
|
|
3321
4299
|
kdf: "HKDF"
|
|
3322
4300
|
};
|
|
4301
|
+
if (!saltOverride && !options?.kdfSalt) {
|
|
4302
|
+
this._keyringCache.set(currentKeyId, res2);
|
|
4303
|
+
if (!keyProvider || keyProvider === this.defaultKey) {
|
|
4304
|
+
this._cachedKey = key2;
|
|
4305
|
+
this._cachedSalt = saltBuf.toString("base64");
|
|
4306
|
+
this._cachedKdf = "HKDF";
|
|
4307
|
+
this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
|
|
4308
|
+
this._cachedKeyId = currentKeyId;
|
|
4309
|
+
}
|
|
4310
|
+
}
|
|
4311
|
+
return res2;
|
|
3323
4312
|
}
|
|
3324
4313
|
if (kdf === "none") {
|
|
3325
4314
|
console.warn(
|
|
3326
4315
|
"[CNodeCryptoEncryptionProvider] WARNING: Decrypting a legacy envelope with no KDF (raw SHA-256 key derivation). This is cryptographically weak. Migrate to pbkdf2 or hkdf."
|
|
3327
4316
|
);
|
|
3328
4317
|
}
|
|
3329
|
-
if (!keyProvider && this._cachedKey && this._cachedKdf === "none" && this._cachedKeyId === currentKeyId) {
|
|
4318
|
+
if ((!keyProvider || keyProvider === this.defaultKey) && this._cachedKey && this._cachedKdf === "none" && this._cachedKeyId === currentKeyId) {
|
|
3330
4319
|
return { key: this._cachedKey, kdf: "none" };
|
|
3331
4320
|
}
|
|
3332
4321
|
const key = crypto$1.createHash("sha256").update(passphrase, "utf8").digest();
|
|
3333
|
-
|
|
4322
|
+
const res = { key, kdf: "none" };
|
|
4323
|
+
this._keyringCache.set(currentKeyId, res);
|
|
4324
|
+
if (!keyProvider || keyProvider === this.defaultKey) {
|
|
3334
4325
|
this._cachedKey = key;
|
|
3335
4326
|
this._cachedKdf = "none";
|
|
3336
4327
|
this._cachedKeyId = currentKeyId;
|
|
3337
4328
|
}
|
|
3338
|
-
return
|
|
4329
|
+
return res;
|
|
3339
4330
|
}
|
|
3340
4331
|
async deriveKeyAsync(keyProvider, kdfOverride, saltOverride, options, iterationsOverride) {
|
|
3341
4332
|
const raw = await this.resolveRawKeyAsync(keyProvider);
|
|
3342
4333
|
const currentKeyId = getSafeKeyIdentifier(raw);
|
|
3343
4334
|
if (Buffer.isBuffer(raw)) {
|
|
3344
4335
|
if (raw.length === 32) {
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
4336
|
+
const cached = this._keyringCache.get(currentKeyId);
|
|
4337
|
+
if (cached) return cached;
|
|
4338
|
+
const res2 = { key: Buffer.from(raw), kdf: "raw" };
|
|
4339
|
+
this._keyringCache.set(currentKeyId, res2);
|
|
4340
|
+
if (!keyProvider || keyProvider === this.defaultKey) {
|
|
4341
|
+
this._cachedKey = res2.key;
|
|
4342
|
+
this._cachedKdf = "raw";
|
|
4343
|
+
this._cachedKeyId = currentKeyId;
|
|
3352
4344
|
}
|
|
3353
|
-
return
|
|
4345
|
+
return res2;
|
|
3354
4346
|
}
|
|
3355
4347
|
throw new Error(
|
|
3356
4348
|
`[CNodeCryptoEncryptionProvider] AES-GCM-256 requires a 32-byte key. Received a Buffer of ${raw.length} bytes. Provide exactly 32 bytes or a string passphrase.`
|
|
@@ -3369,6 +4361,16 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3369
4361
|
return { key: this._cachedKey, salt: this._cachedSalt, kdf: this._cachedKdf, iter: this._cachedIter };
|
|
3370
4362
|
}
|
|
3371
4363
|
}
|
|
4364
|
+
if (!saltOverride && !options?.kdfSalt) {
|
|
4365
|
+
const cached = this._keyringCache.get(currentKeyId);
|
|
4366
|
+
if (cached) {
|
|
4367
|
+
const cMatchesKdf = !cached.kdf || cached.kdf.toLowerCase() === kdf;
|
|
4368
|
+
const cMatchesIter = !targetIter || cached.iter === targetIter;
|
|
4369
|
+
if (cMatchesKdf && cMatchesIter) {
|
|
4370
|
+
return cached;
|
|
4371
|
+
}
|
|
4372
|
+
}
|
|
4373
|
+
}
|
|
3372
4374
|
if (kdf === "pbkdf2") {
|
|
3373
4375
|
const s = saltOverride || options?.kdfSalt || this._kdfOptions?.salt;
|
|
3374
4376
|
const saltBytes = CKeyManager.resolveSalt(s);
|
|
@@ -3382,20 +4384,24 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3382
4384
|
else resolve(derived);
|
|
3383
4385
|
});
|
|
3384
4386
|
});
|
|
3385
|
-
|
|
3386
|
-
this._cachedKey = key2;
|
|
3387
|
-
this._cachedSalt = saltBuf.toString("base64");
|
|
3388
|
-
this._cachedKdf = "PBKDF2";
|
|
3389
|
-
this._cachedIter = iterations;
|
|
3390
|
-
this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
|
|
3391
|
-
this._cachedKeyId = currentKeyId;
|
|
3392
|
-
}
|
|
3393
|
-
return {
|
|
4387
|
+
const res2 = {
|
|
3394
4388
|
key: key2,
|
|
3395
4389
|
salt: saltBuf.toString("base64"),
|
|
3396
4390
|
kdf: "PBKDF2",
|
|
3397
4391
|
iter: iterations
|
|
3398
4392
|
};
|
|
4393
|
+
if (!saltOverride && !options?.kdfSalt) {
|
|
4394
|
+
this._keyringCache.set(currentKeyId, res2);
|
|
4395
|
+
if (!keyProvider || keyProvider === this.defaultKey) {
|
|
4396
|
+
this._cachedKey = key2;
|
|
4397
|
+
this._cachedSalt = saltBuf.toString("base64");
|
|
4398
|
+
this._cachedKdf = "PBKDF2";
|
|
4399
|
+
this._cachedIter = iterations;
|
|
4400
|
+
this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
|
|
4401
|
+
this._cachedKeyId = currentKeyId;
|
|
4402
|
+
}
|
|
4403
|
+
}
|
|
4404
|
+
return res2;
|
|
3399
4405
|
}
|
|
3400
4406
|
if (kdf === "hkdf") {
|
|
3401
4407
|
if (typeof raw === "string") {
|
|
@@ -3414,34 +4420,40 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3414
4420
|
else resolve(Buffer.from(derived));
|
|
3415
4421
|
});
|
|
3416
4422
|
});
|
|
3417
|
-
|
|
3418
|
-
this._cachedKey = key2;
|
|
3419
|
-
this._cachedSalt = saltBuf.toString("base64");
|
|
3420
|
-
this._cachedKdf = "HKDF";
|
|
3421
|
-
this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
|
|
3422
|
-
this._cachedKeyId = currentKeyId;
|
|
3423
|
-
}
|
|
3424
|
-
return {
|
|
4423
|
+
const res2 = {
|
|
3425
4424
|
key: key2,
|
|
3426
4425
|
salt: saltBuf.toString("base64"),
|
|
3427
4426
|
kdf: "HKDF"
|
|
3428
4427
|
};
|
|
4428
|
+
if (!saltOverride && !options?.kdfSalt) {
|
|
4429
|
+
this._keyringCache.set(currentKeyId, res2);
|
|
4430
|
+
if (!keyProvider || keyProvider === this.defaultKey) {
|
|
4431
|
+
this._cachedKey = key2;
|
|
4432
|
+
this._cachedSalt = saltBuf.toString("base64");
|
|
4433
|
+
this._cachedKdf = "HKDF";
|
|
4434
|
+
this._cachedHash = options?.kdfHash || this._kdfOptions?.hash || "SHA-256";
|
|
4435
|
+
this._cachedKeyId = currentKeyId;
|
|
4436
|
+
}
|
|
4437
|
+
}
|
|
4438
|
+
return res2;
|
|
3429
4439
|
}
|
|
3430
4440
|
if (kdf === "none") {
|
|
3431
4441
|
console.warn(
|
|
3432
4442
|
"[CNodeCryptoEncryptionProvider] WARNING: Decrypting a legacy envelope with no KDF (raw SHA-256 key derivation). This is cryptographically weak. Migrate to pbkdf2 or hkdf."
|
|
3433
4443
|
);
|
|
3434
4444
|
}
|
|
3435
|
-
if (!keyProvider && this._cachedKey && this._cachedKdf === "none" && this._cachedKeyId === currentKeyId) {
|
|
4445
|
+
if ((!keyProvider || keyProvider === this.defaultKey) && this._cachedKey && this._cachedKdf === "none" && this._cachedKeyId === currentKeyId) {
|
|
3436
4446
|
return { key: this._cachedKey, kdf: "none" };
|
|
3437
4447
|
}
|
|
3438
4448
|
const key = crypto$1.createHash("sha256").update(passphrase, "utf8").digest();
|
|
3439
|
-
|
|
4449
|
+
const res = { key, kdf: "none" };
|
|
4450
|
+
this._keyringCache.set(currentKeyId, res);
|
|
4451
|
+
if (!keyProvider || keyProvider === this.defaultKey) {
|
|
3440
4452
|
this._cachedKey = key;
|
|
3441
4453
|
this._cachedKdf = "none";
|
|
3442
4454
|
this._cachedKeyId = currentKeyId;
|
|
3443
4455
|
}
|
|
3444
|
-
return
|
|
4456
|
+
return res;
|
|
3445
4457
|
}
|
|
3446
4458
|
static isEncrypted(payload) {
|
|
3447
4459
|
if (!payload || typeof payload !== "object") return false;
|
|
@@ -3481,7 +4493,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3481
4493
|
]);
|
|
3482
4494
|
const authTag = cipher.getAuthTag();
|
|
3483
4495
|
const combinedCiphertext = Buffer.concat([encryptedBuffer, authTag]);
|
|
3484
|
-
const kid = options?.kid || options?.keyId || this._configuredKid;
|
|
4496
|
+
const kid = options?.kid || options?.keyId || this.activeKid || this._configuredKid;
|
|
3485
4497
|
return {
|
|
3486
4498
|
__encrypted: true,
|
|
3487
4499
|
ciphertext: combinedCiphertext.toString("base64"),
|
|
@@ -3494,7 +4506,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3494
4506
|
...hasAad ? { aad: true } : {}
|
|
3495
4507
|
};
|
|
3496
4508
|
} finally {
|
|
3497
|
-
if (
|
|
4509
|
+
if (!this.isKeyRetained(key)) {
|
|
3498
4510
|
key.fill(0);
|
|
3499
4511
|
}
|
|
3500
4512
|
}
|
|
@@ -3583,7 +4595,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3583
4595
|
if (err instanceof SyntaxError) throw err;
|
|
3584
4596
|
throw new Error(`[CNodeCryptoEncryptionProvider] Decryption failed: ${err?.message || "Authentication tag mismatch or corrupted ciphertext"}`);
|
|
3585
4597
|
} finally {
|
|
3586
|
-
if (
|
|
4598
|
+
if (!this.isKeyRetained(key)) {
|
|
3587
4599
|
key.fill(0);
|
|
3588
4600
|
}
|
|
3589
4601
|
}
|
|
@@ -3592,6 +4604,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3592
4604
|
encryptSync(data, options) {
|
|
3593
4605
|
if (data === void 0 || data === null) return data;
|
|
3594
4606
|
if (this.isEncrypted(data)) return data;
|
|
4607
|
+
options = this._resolveOutboundKeyringOptions(options);
|
|
3595
4608
|
if (Array.isArray(data)) {
|
|
3596
4609
|
if (options?.fields && options.fields.length > 0 || options?.valueEncryption === true) {
|
|
3597
4610
|
return data.map((item) => this.encryptSync(item, options));
|
|
@@ -3627,7 +4640,12 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3627
4640
|
decryptSync(payload, options) {
|
|
3628
4641
|
if (payload === void 0 || payload === null) return payload;
|
|
3629
4642
|
if (this.isEncrypted(payload)) {
|
|
3630
|
-
|
|
4643
|
+
const metaErr = this._validateEnvelopeMetadata(payload, options);
|
|
4644
|
+
if (metaErr) {
|
|
4645
|
+
return this._handleDecryptError(metaErr, payload, "", payload, void 0, options);
|
|
4646
|
+
}
|
|
4647
|
+
const envelopeKey = this._resolveDecryptionKeyForEnvelope(payload, options);
|
|
4648
|
+
return this.decryptValueSync(payload, { ...options, key: envelopeKey });
|
|
3631
4649
|
}
|
|
3632
4650
|
if (Array.isArray(payload)) {
|
|
3633
4651
|
return payload.map((item) => this.decryptSync(item, options));
|
|
@@ -3641,28 +4659,35 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3641
4659
|
for (const key of targetKeys) {
|
|
3642
4660
|
if (this.isEncrypted(result[key])) {
|
|
3643
4661
|
const fieldPayload = result[key];
|
|
4662
|
+
const metaErr = this._validateEnvelopeMetadata(fieldPayload, options);
|
|
4663
|
+
if (metaErr) {
|
|
4664
|
+
return this._handleDecryptError(metaErr, result, key, fieldPayload, void 0, options);
|
|
4665
|
+
}
|
|
4666
|
+
const envelopeKey = this._resolveDecryptionKeyForEnvelope(fieldPayload, options);
|
|
3644
4667
|
const salt = fieldPayload.salt || "__no_salt__";
|
|
3645
|
-
|
|
4668
|
+
const memoKey = `${salt}:${fieldPayload.kid || ""}`;
|
|
4669
|
+
let derivedKey = keyCache.get(memoKey);
|
|
3646
4670
|
if (!derivedKey) {
|
|
3647
4671
|
const kdfFromEnvelope = fieldPayload.kdf ? fieldPayload.kdf.toLowerCase() : "none";
|
|
3648
4672
|
const derived = this.deriveKeySync(
|
|
3649
|
-
|
|
4673
|
+
envelopeKey,
|
|
3650
4674
|
kdfFromEnvelope,
|
|
3651
4675
|
fieldPayload.salt,
|
|
3652
4676
|
options,
|
|
3653
4677
|
fieldPayload.iter
|
|
3654
4678
|
);
|
|
3655
4679
|
derivedKey = derived.key;
|
|
3656
|
-
keyCache.set(
|
|
4680
|
+
keyCache.set(memoKey, derivedKey);
|
|
3657
4681
|
}
|
|
3658
|
-
const boundOptions = this._bindFieldAad(payload, key, options);
|
|
4682
|
+
const boundOptions = this._bindFieldAad(payload, key, { ...options, key: envelopeKey });
|
|
3659
4683
|
const hasBoundAad = boundOptions.aad !== options?.aad;
|
|
3660
4684
|
try {
|
|
3661
4685
|
safeSetProperty(result, key, this.decryptCiphertextWithKey(fieldPayload, derivedKey, boundOptions));
|
|
3662
4686
|
} catch (err) {
|
|
3663
|
-
|
|
4687
|
+
const canFallback = hasBoundAad && (!options?.aadFields || options.aadFields.length === 0) && typeof options?.aad !== "function";
|
|
4688
|
+
if (canFallback && isAuthOrDecryptionError(err)) {
|
|
3664
4689
|
try {
|
|
3665
|
-
safeSetProperty(result, key, this.decryptCiphertextWithKey(fieldPayload, derivedKey, options));
|
|
4690
|
+
safeSetProperty(result, key, this.decryptCiphertextWithKey(fieldPayload, derivedKey, { ...options, key: envelopeKey }));
|
|
3666
4691
|
} catch (fallbackErr) {
|
|
3667
4692
|
throw fallbackErr;
|
|
3668
4693
|
}
|
|
@@ -3676,7 +4701,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3676
4701
|
return mutated ? result : payload;
|
|
3677
4702
|
} finally {
|
|
3678
4703
|
for (const k of keyCache.values()) {
|
|
3679
|
-
if (
|
|
4704
|
+
if (!this.isKeyRetained(k)) {
|
|
3680
4705
|
k.fill(0);
|
|
3681
4706
|
}
|
|
3682
4707
|
}
|
|
@@ -3724,7 +4749,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3724
4749
|
]);
|
|
3725
4750
|
const authTag = cipher.getAuthTag();
|
|
3726
4751
|
const combined = Buffer.concat([ciphertext, authTag]);
|
|
3727
|
-
const kid = options?.kid || options?.keyId || this._configuredKid;
|
|
4752
|
+
const kid = options?.kid || options?.keyId || this.activeKid || this._configuredKid;
|
|
3728
4753
|
return {
|
|
3729
4754
|
__encrypted: true,
|
|
3730
4755
|
ciphertext: combined.toString("base64"),
|
|
@@ -3737,7 +4762,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3737
4762
|
...hasAad ? { aad: true } : {}
|
|
3738
4763
|
};
|
|
3739
4764
|
} finally {
|
|
3740
|
-
if (
|
|
4765
|
+
if (!this.isKeyRetained(key)) {
|
|
3741
4766
|
key.fill(0);
|
|
3742
4767
|
}
|
|
3743
4768
|
}
|
|
@@ -3761,7 +4786,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3761
4786
|
if (err instanceof SyntaxError) throw err;
|
|
3762
4787
|
throw new Error(`[CNodeCryptoEncryptionProvider] Decryption failed: ${err?.message || "Authentication tag mismatch or corrupted ciphertext"}`);
|
|
3763
4788
|
} finally {
|
|
3764
|
-
if (
|
|
4789
|
+
if (!this.isKeyRetained(key)) {
|
|
3765
4790
|
key.fill(0);
|
|
3766
4791
|
}
|
|
3767
4792
|
}
|
|
@@ -3769,7 +4794,12 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3769
4794
|
async decrypt(payload, options) {
|
|
3770
4795
|
if (payload === void 0 || payload === null) return payload;
|
|
3771
4796
|
if (this.isEncrypted(payload)) {
|
|
3772
|
-
|
|
4797
|
+
const metaErr = this._validateEnvelopeMetadata(payload, options);
|
|
4798
|
+
if (metaErr) {
|
|
4799
|
+
return this._handleDecryptError(metaErr, payload, "", payload, void 0, options);
|
|
4800
|
+
}
|
|
4801
|
+
const envelopeKey = this._resolveDecryptionKeyForEnvelope(payload, options);
|
|
4802
|
+
return this.decryptValue(payload, { ...options, key: envelopeKey });
|
|
3773
4803
|
}
|
|
3774
4804
|
if (Array.isArray(payload)) {
|
|
3775
4805
|
return Promise.all(payload.map((item) => this.decrypt(item, options)));
|
|
@@ -3783,28 +4813,35 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3783
4813
|
for (const key of targetKeys) {
|
|
3784
4814
|
if (this.isEncrypted(result[key])) {
|
|
3785
4815
|
const fieldPayload = result[key];
|
|
4816
|
+
const metaErr = this._validateEnvelopeMetadata(fieldPayload, options);
|
|
4817
|
+
if (metaErr) {
|
|
4818
|
+
return this._handleDecryptError(metaErr, result, key, fieldPayload, void 0, options);
|
|
4819
|
+
}
|
|
4820
|
+
const envelopeKey = this._resolveDecryptionKeyForEnvelope(fieldPayload, options);
|
|
3786
4821
|
const salt = fieldPayload.salt || "__no_salt__";
|
|
3787
|
-
|
|
4822
|
+
const memoKey = `${salt}:${fieldPayload.kid || ""}`;
|
|
4823
|
+
let derivedKey = keyCache.get(memoKey);
|
|
3788
4824
|
if (!derivedKey) {
|
|
3789
4825
|
const kdfFromEnvelope = fieldPayload.kdf ? fieldPayload.kdf.toLowerCase() : "none";
|
|
3790
4826
|
const derived = await this.deriveKeyAsync(
|
|
3791
|
-
|
|
4827
|
+
envelopeKey,
|
|
3792
4828
|
kdfFromEnvelope,
|
|
3793
4829
|
fieldPayload.salt ? CKeyManager.fromBase64(fieldPayload.salt) : void 0,
|
|
3794
4830
|
options,
|
|
3795
4831
|
fieldPayload.iter
|
|
3796
4832
|
);
|
|
3797
4833
|
derivedKey = derived.key;
|
|
3798
|
-
keyCache.set(
|
|
4834
|
+
keyCache.set(memoKey, derivedKey);
|
|
3799
4835
|
}
|
|
3800
|
-
const boundOptions = this._bindFieldAad(payload, key, options);
|
|
4836
|
+
const boundOptions = this._bindFieldAad(payload, key, { ...options, key: envelopeKey });
|
|
3801
4837
|
const hasBoundAad = boundOptions.aad !== options?.aad;
|
|
3802
4838
|
try {
|
|
3803
4839
|
safeSetProperty(result, key, this.decryptCiphertextWithKey(fieldPayload, derivedKey, boundOptions));
|
|
3804
4840
|
} catch (err) {
|
|
3805
|
-
|
|
4841
|
+
const canFallback = hasBoundAad && (!options?.aadFields || options.aadFields.length === 0) && typeof options?.aad !== "function";
|
|
4842
|
+
if (canFallback && isAuthOrDecryptionError(err)) {
|
|
3806
4843
|
try {
|
|
3807
|
-
safeSetProperty(result, key, this.decryptCiphertextWithKey(fieldPayload, derivedKey, options));
|
|
4844
|
+
safeSetProperty(result, key, this.decryptCiphertextWithKey(fieldPayload, derivedKey, { ...options, key: envelopeKey }));
|
|
3808
4845
|
} catch (fallbackErr) {
|
|
3809
4846
|
throw fallbackErr;
|
|
3810
4847
|
}
|
|
@@ -3818,7 +4855,7 @@ class CNodeCryptoEncryptionProvider extends AEncryptionProvider {
|
|
|
3818
4855
|
return mutated ? result : payload;
|
|
3819
4856
|
} finally {
|
|
3820
4857
|
for (const k of keyCache.values()) {
|
|
3821
|
-
if (
|
|
4858
|
+
if (!this.isKeyRetained(k)) {
|
|
3822
4859
|
k.fill(0);
|
|
3823
4860
|
}
|
|
3824
4861
|
}
|
|
@@ -3849,7 +4886,11 @@ function createEncryptionProvider(options = {}) {
|
|
|
3849
4886
|
kdfIterations: options.kdfIterations,
|
|
3850
4887
|
kdfHash: options.kdfHash,
|
|
3851
4888
|
kdfSalt: options.kdfSalt,
|
|
3852
|
-
info: options.info
|
|
4889
|
+
info: options.info,
|
|
4890
|
+
kid: options.kid || options.keyId,
|
|
4891
|
+
keys: options.keys,
|
|
4892
|
+
activeKid: options.activeKid,
|
|
4893
|
+
allowedKids: options.allowedKids
|
|
3853
4894
|
});
|
|
3854
4895
|
}
|
|
3855
4896
|
if (alg === "AES-CBC") {
|
|
@@ -3886,7 +4927,11 @@ function createEncryptionProvider(options = {}) {
|
|
|
3886
4927
|
kdfIterations: options.kdfIterations,
|
|
3887
4928
|
kdfHash: options.kdfHash,
|
|
3888
4929
|
kdfSalt: options.kdfSalt,
|
|
3889
|
-
info: options.info
|
|
4930
|
+
info: options.info,
|
|
4931
|
+
kid: options.kid || options.keyId,
|
|
4932
|
+
keys: options.keys,
|
|
4933
|
+
activeKid: options.activeKid,
|
|
4934
|
+
allowedKids: options.allowedKids
|
|
3890
4935
|
});
|
|
3891
4936
|
}
|
|
3892
4937
|
throw new Error(
|
|
@@ -3949,7 +4994,19 @@ function isDataEncrypted(provider, data) {
|
|
|
3949
4994
|
function buildEncryptionContext(encryption, url, _defaultIdKey) {
|
|
3950
4995
|
return {
|
|
3951
4996
|
url,
|
|
4997
|
+
aad: encryption?.aad,
|
|
4998
|
+
aadFields: encryption?.aadFields,
|
|
4999
|
+
baseAad: encryption?.baseAad,
|
|
5000
|
+
bindRequestContextAad: encryption?.bindRequestContextAad,
|
|
5001
|
+
strict: encryption?.strict,
|
|
5002
|
+
allowedAlgorithms: encryption?.allowedAlgorithms,
|
|
5003
|
+
allowedKids: encryption?.allowedKids,
|
|
5004
|
+
onDecryptError: encryption?.onDecryptError,
|
|
3952
5005
|
key: encryption?.key,
|
|
5006
|
+
kid: encryption?.kid,
|
|
5007
|
+
keyId: encryption?.keyId,
|
|
5008
|
+
keys: encryption?.keys,
|
|
5009
|
+
activeKid: encryption?.activeKid,
|
|
3953
5010
|
valueEncryption: encryption?.valueEncryption,
|
|
3954
5011
|
fields: encryption?.fields,
|
|
3955
5012
|
publicKey: encryption?.publicKey,
|
|
@@ -3980,7 +5037,7 @@ async function decryptPageRows(rows, encryption, apiUrl, idKey) {
|
|
|
3980
5037
|
}
|
|
3981
5038
|
return isDataEncrypted(cipherProvider, row);
|
|
3982
5039
|
});
|
|
3983
|
-
if (!hasEncryptedRows) {
|
|
5040
|
+
if (!hasEncryptedRows && !encryption?.strict) {
|
|
3984
5041
|
return { rows, decrypted: false };
|
|
3985
5042
|
}
|
|
3986
5043
|
const ctx = buildEncryptionContext(encryption, apiUrl);
|
|
@@ -4143,6 +5200,13 @@ class CSearchIndex {
|
|
|
4143
5200
|
const signature = await subtle.sign("HMAC", key, textEncoder.encode(normalized));
|
|
4144
5201
|
return Array.from(new Uint8Array(signature)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
4145
5202
|
}
|
|
5203
|
+
/**
|
|
5204
|
+
* Computes a deterministic HMAC-SHA256 blind index hash for a term using the configured HMAC key.
|
|
5205
|
+
* Performs Unicode NFKC normalization, trimming, and lowercasing.
|
|
5206
|
+
*/
|
|
5207
|
+
async computeBlindIndex(term) {
|
|
5208
|
+
return this.computeToken(term);
|
|
5209
|
+
}
|
|
4146
5210
|
/**
|
|
4147
5211
|
* Computes HMAC-SHA256 tokens for all trigrams of a term.
|
|
4148
5212
|
*/
|
|
@@ -4172,9 +5236,21 @@ class CSearchIndex {
|
|
|
4172
5236
|
const token = await this.computeToken(rawVal);
|
|
4173
5237
|
const trigrams = await this.computeTrigramTokens(rawVal);
|
|
4174
5238
|
fieldMap.set(field, { token, trigrams });
|
|
5239
|
+
} else if (rawVal && typeof rawVal === "object" && rawVal.__encrypted === true) {
|
|
5240
|
+
const precomputedToken = row[`${field}_bidx`] || row[`${field}_token`];
|
|
5241
|
+
if (typeof precomputedToken === "string" && precomputedToken) {
|
|
5242
|
+
fieldMap.set(field, { token: precomputedToken, trigrams: /* @__PURE__ */ new Set() });
|
|
5243
|
+
}
|
|
4175
5244
|
}
|
|
4176
5245
|
}
|
|
4177
5246
|
}
|
|
5247
|
+
/**
|
|
5248
|
+
* Indexes a row using pre-decrypted plaintext field values.
|
|
5249
|
+
* Allows indexing rows that contain pre-encrypted ciphertext envelopes in allData.
|
|
5250
|
+
*/
|
|
5251
|
+
async indexRowDecrypted(row, decryptedPlaintext) {
|
|
5252
|
+
return this.indexRow(row, decryptedPlaintext);
|
|
5253
|
+
}
|
|
4178
5254
|
/**
|
|
4179
5255
|
* Indexes an array of rows.
|
|
4180
5256
|
*/
|
|
@@ -4223,7 +5299,7 @@ class CSearchIndex {
|
|
|
4223
5299
|
}
|
|
4224
5300
|
return true;
|
|
4225
5301
|
}
|
|
4226
|
-
if (operator === EDataFilterOperator.NotIn || operator === "notIn") {
|
|
5302
|
+
if (operator === EDataFilterOperator.NotIn || operator === "notIn" || operator === "nin") {
|
|
4227
5303
|
const queryTrigramTokens = await this.computeTrigramTokens(filterValue);
|
|
4228
5304
|
if (queryTrigramTokens.size === 0) return false;
|
|
4229
5305
|
for (const tok of queryTrigramTokens) {
|
|
@@ -4294,7 +5370,7 @@ class CSearchIndex {
|
|
|
4294
5370
|
}
|
|
4295
5371
|
return true;
|
|
4296
5372
|
});
|
|
4297
|
-
} else if (op === EDataFilterOperator.NotIn || op === "notIn") {
|
|
5373
|
+
} else if (op === EDataFilterOperator.NotIn || op === "notIn" || op === "nin") {
|
|
4298
5374
|
const queryTrigrams = await this.computeTrigramTokens(filterValue);
|
|
4299
5375
|
checks.push((row) => {
|
|
4300
5376
|
const entry = this.getEntry(row, key);
|
|
@@ -4413,6 +5489,36 @@ class AEncryptedClientProvider extends ADataProvider {
|
|
|
4413
5489
|
} : void 0;
|
|
4414
5490
|
return await encryptRowsChunked(rows, this.encryption, options, void 0, this.idKey, indexChunk);
|
|
4415
5491
|
}
|
|
5492
|
+
/**
|
|
5493
|
+
* Indexes pre-encrypted rows by performing a transient in-memory decryption pass
|
|
5494
|
+
* on search fields to populate CSearchIndex without mutating underlying encrypted records in allData.
|
|
5495
|
+
*/
|
|
5496
|
+
async indexPreEncryptedRows(rows) {
|
|
5497
|
+
if (!this.isEncryptionEnabled || !this._searchIndex || !rows || rows.length === 0) {
|
|
5498
|
+
return;
|
|
5499
|
+
}
|
|
5500
|
+
const cipher = getCipherProvider(this.encryption);
|
|
5501
|
+
if (!cipher) return;
|
|
5502
|
+
const ctx = buildEncryptionContext(this.encryption, this.apiUrl, this.idKey);
|
|
5503
|
+
let decryptedRows = [];
|
|
5504
|
+
try {
|
|
5505
|
+
const decResult = await cipher.decrypt(rows, ctx);
|
|
5506
|
+
decryptedRows = Array.isArray(decResult) ? decResult : [decResult];
|
|
5507
|
+
} catch (err) {
|
|
5508
|
+
console.warn("[Katlux Providers] indexPreEncryptedRows: Decryption failed during search index build", err);
|
|
5509
|
+
return;
|
|
5510
|
+
}
|
|
5511
|
+
await Promise.all(rows.map((row, idx) => {
|
|
5512
|
+
const decRow = decryptedRows[idx];
|
|
5513
|
+
const plainFields = {};
|
|
5514
|
+
for (const f of this.encryption?.fields || Object.keys(decRow || {})) {
|
|
5515
|
+
if (decRow && decRow[f] !== void 0) {
|
|
5516
|
+
plainFields[f] = decRow[f];
|
|
5517
|
+
}
|
|
5518
|
+
}
|
|
5519
|
+
return this._searchIndex.indexRow(row, plainFields);
|
|
5520
|
+
}));
|
|
5521
|
+
}
|
|
4416
5522
|
async decryptRows(rows) {
|
|
4417
5523
|
if (!this.isEncryptionEnabled || !rows || rows.length === 0) {
|
|
4418
5524
|
return { rows, decrypted: false };
|
|
@@ -4721,6 +5827,15 @@ class CFlatClientDataProvider extends AEncryptedClientProvider {
|
|
|
4721
5827
|
this.loading = true;
|
|
4722
5828
|
try {
|
|
4723
5829
|
const rows = Array.isArray(data) ? data : [];
|
|
5830
|
+
const isPreEnc = this.encryption?.preEncrypted ?? isDataEncrypted(void 0, rows);
|
|
5831
|
+
if (isPreEnc) {
|
|
5832
|
+
this.allData = rows;
|
|
5833
|
+
if (this.searchIndex) {
|
|
5834
|
+
await this.indexPreEncryptedRows(rows);
|
|
5835
|
+
}
|
|
5836
|
+
await this.loadPageData();
|
|
5837
|
+
return;
|
|
5838
|
+
}
|
|
4724
5839
|
const encryptedRows = await this.encryptRows(rows);
|
|
4725
5840
|
this.allData = encryptedRows;
|
|
4726
5841
|
await this.loadPageData();
|
|
@@ -4741,6 +5856,16 @@ class CFlatClientDataProvider extends AEncryptedClientProvider {
|
|
|
4741
5856
|
this.loading = true;
|
|
4742
5857
|
try {
|
|
4743
5858
|
const rows = Array.isArray(data) ? data : [];
|
|
5859
|
+
const isPreEnc = this.encryption?.preEncrypted ?? isDataEncrypted(void 0, rows);
|
|
5860
|
+
if (isPreEnc) {
|
|
5861
|
+
this.allData = rows;
|
|
5862
|
+
if (this.searchIndex) {
|
|
5863
|
+
await this.indexPreEncryptedRows(rows);
|
|
5864
|
+
}
|
|
5865
|
+
options?.onProgress?.({ processed: rows.length, total: rows.length, percent: 100 });
|
|
5866
|
+
await this.loadPageData();
|
|
5867
|
+
return;
|
|
5868
|
+
}
|
|
4744
5869
|
const encryptedRows = await this.encryptRowsChunked(rows, options);
|
|
4745
5870
|
this.allData = encryptedRows;
|
|
4746
5871
|
await this.loadPageData();
|
|
@@ -6687,8 +7812,18 @@ class RequestProvider {
|
|
|
6687
7812
|
method,
|
|
6688
7813
|
isServer,
|
|
6689
7814
|
aad: encOptions.aad,
|
|
7815
|
+
aadFields: encOptions.aadFields,
|
|
7816
|
+
baseAad: encOptions.baseAad,
|
|
6690
7817
|
bindRequestContextAad: encOptions.bindRequestContextAad,
|
|
7818
|
+
strict: encOptions.strict,
|
|
7819
|
+
allowedAlgorithms: encOptions.allowedAlgorithms,
|
|
7820
|
+
allowedKids: encOptions.allowedKids,
|
|
7821
|
+
onDecryptError: encOptions.onDecryptError,
|
|
6691
7822
|
key: encOptions.key,
|
|
7823
|
+
kid: encOptions.kid,
|
|
7824
|
+
keyId: encOptions.keyId,
|
|
7825
|
+
keys: encOptions.keys,
|
|
7826
|
+
activeKid: encOptions.activeKid,
|
|
6692
7827
|
valueEncryption: encOptions.valueEncryption,
|
|
6693
7828
|
fields: encOptions.fields,
|
|
6694
7829
|
publicKey: encOptions.publicKey,
|
|
@@ -6877,17 +8012,26 @@ class CAPIFlatClientDataProvider extends CFlatClientDataProvider {
|
|
|
6877
8012
|
if (data) {
|
|
6878
8013
|
const normalized = normalizeDataResult(data);
|
|
6879
8014
|
const rows = normalized ? normalized.rows : data.rows || data;
|
|
8015
|
+
const incomingRows = Array.isArray(rows) ? rows : [];
|
|
6880
8016
|
if (this._isAccumulating) {
|
|
6881
|
-
this.allData = [...this.allData, ...
|
|
8017
|
+
this.allData = [...this.allData, ...incomingRows];
|
|
6882
8018
|
} else {
|
|
6883
|
-
this.allData =
|
|
8019
|
+
this.allData = incomingRows;
|
|
6884
8020
|
}
|
|
6885
8021
|
this._filterDirty = true;
|
|
6886
8022
|
this._sortDirty = true;
|
|
6887
8023
|
if (normalized?.rowCount !== void 0) {
|
|
6888
8024
|
this.rowCount = normalized.rowCount;
|
|
6889
8025
|
}
|
|
6890
|
-
this._lastDataReceivedPromise =
|
|
8026
|
+
this._lastDataReceivedPromise = (async () => {
|
|
8027
|
+
if (this.isEncryptionEnabled && this.searchIndex && incomingRows.length > 0) {
|
|
8028
|
+
const isPreEnc = this.encryption?.preEncrypted ?? isDataEncrypted(void 0, incomingRows);
|
|
8029
|
+
if (isPreEnc) {
|
|
8030
|
+
await this.indexPreEncryptedRows(incomingRows);
|
|
8031
|
+
}
|
|
8032
|
+
}
|
|
8033
|
+
await this.loadPageData();
|
|
8034
|
+
})();
|
|
6891
8035
|
}
|
|
6892
8036
|
});
|
|
6893
8037
|
}
|
|
@@ -6973,6 +8117,12 @@ class CAPIFlatClientDataProvider extends CFlatClientDataProvider {
|
|
|
6973
8117
|
this.allData = normalized.rows;
|
|
6974
8118
|
this._filterDirty = true;
|
|
6975
8119
|
this._sortDirty = true;
|
|
8120
|
+
if (this.isEncryptionEnabled && this.searchIndex && this.allData.length > 0) {
|
|
8121
|
+
const isPreEnc = this.encryption?.preEncrypted ?? isDataEncrypted(void 0, this.allData);
|
|
8122
|
+
if (isPreEnc) {
|
|
8123
|
+
await this.indexPreEncryptedRows(this.allData);
|
|
8124
|
+
}
|
|
8125
|
+
}
|
|
6976
8126
|
await this.loadPageData();
|
|
6977
8127
|
this.initialLoad = false;
|
|
6978
8128
|
this.loading = false;
|
|
@@ -10200,4 +11350,4 @@ function isEncrypted(payload) {
|
|
|
10200
11350
|
return AEncryptionProvider.isEncrypted(payload);
|
|
10201
11351
|
}
|
|
10202
11352
|
|
|
10203
|
-
export { AAPIDataProvider, ADataProvider, AEncryptedClientProvider, AEncryptionProvider, CAPIFlatClientDataProvider, CAPIFlatServerDataProvider, CAPITreeClientDataProvider, CAPITreeServerDataProvider, CApplicationCache, CBrowserSessionCache, CCookieCache, CECDHEncryptionProvider, CFlatClientDataProvider, CFlatServerDataProvider, CIndexedDBCache, CKeyManager, CLocalStorageCache, CMemoryCache, CNodeCryptoEncryptionProvider, CRSAEncryptionProvider, CSearchIndex, CServerSessionCache, CTreeClientDataProvider, CTreeServerDataProvider, CWebCryptoAESCBCProvider, CWebCryptoEncryptionProvider, CacheProviderFactory, DefaultHydrationAdapter, ECacheStrategy, EDataFilterOperator, EQueueStorage, KatluxIndexedDB, QueueStorageFactory, RequestProvider, RequestProviderRegistry, RequestQueue, SENSITIVE_HEADER_KEYS, applyOperator, buildEncryptionContext, clearCachedCipherProvider, clearSSRData, compareAnd, compareEqual, compareIn, compareOr, compareValues, createEncryptionProvider, debounce, decryptPageRows, encryptRows, encryptRowsChunked, evaluateFilter, extractTrigrams, fastKeyHash, fetchWithSSR, filterData, filterTreeData, getAppRoute, getAppRouter, getCipherProvider, getHydrationAdapter, getKeyIdentifier, getPlatformApp, getRandomValues, getSSRData, getSSRPayload, getSafeKeyIdentifier, getSubtleCrypto, isAuthOrDecryptionError, isDataEncrypted, isEncrypted, normalizeIndexTerm, onPlatformReady, resolveInfoBytes, safeSetProperty, setHydrationAdapter, sortData, textDecoder, textEncoder$1 as textEncoder, toBase64, useDebounce, useFilterLogic, useSortLogic };
|
|
11353
|
+
export { AAPIDataProvider, ADataProvider, AEncryptedClientProvider, AEncryptionProvider, CAPIFlatClientDataProvider, CAPIFlatServerDataProvider, CAPITreeClientDataProvider, CAPITreeServerDataProvider, CApplicationCache, CBrowserSessionCache, CCookieCache, CECDHEncryptionProvider, CFlatClientDataProvider, CFlatServerDataProvider, CIndexedDBCache, CKeyManager, CLocalStorageCache, CMemoryCache, CNodeCryptoEncryptionProvider, CRSAEncryptionProvider, CSearchIndex, CServerSessionCache, CTreeClientDataProvider, CTreeServerDataProvider, CWebCryptoAESCBCProvider, CWebCryptoEncryptionProvider, CacheProviderFactory, DECRYPT_ERROR_SENTINEL, DefaultHydrationAdapter, ECacheStrategy, EDataFilterOperator, EQueueStorage, KatluxIndexedDB, QueueStorageFactory, RequestProvider, RequestProviderRegistry, RequestQueue, SENSITIVE_HEADER_KEYS, applyOperator, buildEncryptionContext, canonicalizeJson, clearCachedCipherProvider, clearSSRData, compareAnd, compareEqual, compareIn, compareOr, compareValues, createEncryptionProvider, debounce, decryptPageRows, encryptRows, encryptRowsChunked, evaluateFilter, extractTrigrams, fastKeyHash, fetchWithSSR, filterData, filterTreeData, getAppRoute, getAppRouter, getCipherProvider, getHydrationAdapter, getKeyIdentifier, getPlatformApp, getRandomValues, getSSRData, getSSRPayload, getSafeKeyIdentifier, getSubtleCrypto, isAuthOrDecryptionError, isDataEncrypted, isEncrypted, normalizeIndexTerm, onPlatformReady, resolveInfoBytes, safeSetProperty, setHydrationAdapter, sortData, textDecoder, textEncoder$1 as textEncoder, toBase64, useDebounce, useFilterLogic, useSortLogic };
|