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