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