@hsuite/smart-engines-sdk 3.4.0 → 3.4.1
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/CHANGELOG.md +18 -0
- package/dist/index.d.ts +42 -2
- package/dist/index.js +251 -2
- package/dist/index.js.map +1 -1
- package/dist/ipfs-access-key/index.d.ts +27 -0
- package/dist/ipfs-access-key/index.js +84 -0
- package/dist/ipfs-access-key/index.js.map +1 -0
- package/dist/k8s-secret-reader/index.d.ts +15 -0
- package/dist/k8s-secret-reader/index.js +76 -0
- package/dist/k8s-secret-reader/index.js.map +1 -0
- package/dist/pqc-verify-envelope/index.d.ts +40 -0
- package/dist/pqc-verify-envelope/index.js +244 -0
- package/dist/pqc-verify-envelope/index.js.map +1 -0
- package/package.json +14 -2
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Generated by dts-bundle-generator v9.5.1
|
|
2
|
+
|
|
3
|
+
export type IpfsAccessKeyEnvelope = {
|
|
4
|
+
version: "kyber-aes-v1";
|
|
5
|
+
kemAlgorithm: "ml-kem-768" | "ml-kem-1024";
|
|
6
|
+
kemCiphertext: string;
|
|
7
|
+
recipientPkFingerprint: string;
|
|
8
|
+
aesAlgorithm: "aes-256-gcm";
|
|
9
|
+
aesIv: string;
|
|
10
|
+
aesCiphertext: string;
|
|
11
|
+
aesAuthTag: string;
|
|
12
|
+
kdfAlgorithm: "hkdf-sha384";
|
|
13
|
+
kdfSalt: string;
|
|
14
|
+
kdfLabel: string;
|
|
15
|
+
encryptedAt: number;
|
|
16
|
+
};
|
|
17
|
+
export declare const IPFS_ACCESS_KEY_KDF_LABEL_PREFIX = "ipfs-pinned-access-key-v1::";
|
|
18
|
+
export declare function buildIpfsAccessKeyKdfLabel(cid: string): string;
|
|
19
|
+
export declare function fingerprintRecipientPk(recipientPk: Uint8Array): string;
|
|
20
|
+
export declare function retrieveAndDecryptAccessKey(params: {
|
|
21
|
+
cid: string;
|
|
22
|
+
recipientSk: Uint8Array;
|
|
23
|
+
recipientPk: Uint8Array;
|
|
24
|
+
envelopeProvider: (cid: string, recipientPkFingerprint: string) => Promise<IpfsAccessKeyEnvelope | null>;
|
|
25
|
+
}): Promise<Uint8Array | null>;
|
|
26
|
+
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var mlKem = require('@noble/post-quantum/ml-kem');
|
|
4
|
+
var hkdf = require('@noble/hashes/hkdf');
|
|
5
|
+
var sha2 = require('@noble/hashes/sha2');
|
|
6
|
+
|
|
7
|
+
// src/ipfs-access-key/retrieve-access-key.ts
|
|
8
|
+
var IPFS_ACCESS_KEY_KDF_LABEL_PREFIX = "ipfs-pinned-access-key-v1::";
|
|
9
|
+
function buildIpfsAccessKeyKdfLabel(cid) {
|
|
10
|
+
return `${IPFS_ACCESS_KEY_KDF_LABEL_PREFIX}${cid}`;
|
|
11
|
+
}
|
|
12
|
+
function fingerprintRecipientPk(recipientPk) {
|
|
13
|
+
const hash = sha2.sha384(recipientPk).subarray(0, 16);
|
|
14
|
+
return bytesToHex(hash);
|
|
15
|
+
}
|
|
16
|
+
async function retrieveAndDecryptAccessKey(params) {
|
|
17
|
+
const fp = fingerprintRecipientPk(params.recipientPk);
|
|
18
|
+
const envelope = await params.envelopeProvider(params.cid, fp);
|
|
19
|
+
if (!envelope) return null;
|
|
20
|
+
const expectedLabel = buildIpfsAccessKeyKdfLabel(params.cid);
|
|
21
|
+
if (envelope.kdfLabel !== expectedLabel) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`ipfs-access-key envelope label mismatch: expected='${expectedLabel}', actual='${envelope.kdfLabel}'`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
const kem = envelope.kemAlgorithm === "ml-kem-768" ? mlKem.ml_kem768 : mlKem.ml_kem1024;
|
|
27
|
+
const kemCt = base64ToBytes(envelope.kemCiphertext);
|
|
28
|
+
const sharedSecret = kem.decapsulate(kemCt, params.recipientSk);
|
|
29
|
+
const kdfSalt = base64ToBytes(envelope.kdfSalt);
|
|
30
|
+
const labelBytes = new TextEncoder().encode(envelope.kdfLabel);
|
|
31
|
+
const derivedKey = hkdf.hkdf(sha2.sha384, sharedSecret, kdfSalt, labelBytes, 32);
|
|
32
|
+
const aesIv = base64ToBytes(envelope.aesIv);
|
|
33
|
+
const aesCt = base64ToBytes(envelope.aesCiphertext);
|
|
34
|
+
const aesTag = base64ToBytes(envelope.aesAuthTag);
|
|
35
|
+
const ctWithTag = new Uint8Array(new ArrayBuffer(aesCt.length + aesTag.length));
|
|
36
|
+
ctWithTag.set(aesCt, 0);
|
|
37
|
+
ctWithTag.set(aesTag, aesCt.length);
|
|
38
|
+
const derivedKeyBuf = new Uint8Array(new ArrayBuffer(derivedKey.length));
|
|
39
|
+
derivedKeyBuf.set(derivedKey, 0);
|
|
40
|
+
const aesIvBuf = new Uint8Array(new ArrayBuffer(aesIv.length));
|
|
41
|
+
aesIvBuf.set(aesIv, 0);
|
|
42
|
+
const subtle = getWebCrypto().subtle;
|
|
43
|
+
const cryptoKey = await subtle.importKey(
|
|
44
|
+
"raw",
|
|
45
|
+
derivedKeyBuf,
|
|
46
|
+
{ name: "AES-GCM" },
|
|
47
|
+
false,
|
|
48
|
+
["decrypt"]
|
|
49
|
+
);
|
|
50
|
+
const plaintext = await subtle.decrypt(
|
|
51
|
+
{ name: "AES-GCM", iv: aesIvBuf, tagLength: 128 },
|
|
52
|
+
cryptoKey,
|
|
53
|
+
ctWithTag
|
|
54
|
+
);
|
|
55
|
+
return new Uint8Array(plaintext);
|
|
56
|
+
}
|
|
57
|
+
function bytesToHex(bytes) {
|
|
58
|
+
let out = "";
|
|
59
|
+
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
function base64ToBytes(b64) {
|
|
63
|
+
if (typeof Buffer !== "undefined") {
|
|
64
|
+
return new Uint8Array(Buffer.from(b64, "base64"));
|
|
65
|
+
}
|
|
66
|
+
const bin = atob(b64);
|
|
67
|
+
const out = new Uint8Array(bin.length);
|
|
68
|
+
for (let i = 0; i < bin.length; i += 1) out[i] = bin.charCodeAt(i);
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
function getWebCrypto() {
|
|
72
|
+
const g = globalThis;
|
|
73
|
+
if (g.crypto?.subtle) return g.crypto;
|
|
74
|
+
throw new Error(
|
|
75
|
+
"retrieveAndDecryptAccessKey: WebCrypto (globalThis.crypto.subtle) is not available in this runtime. Use Node 18+ or a browser/edge env."
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
exports.IPFS_ACCESS_KEY_KDF_LABEL_PREFIX = IPFS_ACCESS_KEY_KDF_LABEL_PREFIX;
|
|
80
|
+
exports.buildIpfsAccessKeyKdfLabel = buildIpfsAccessKeyKdfLabel;
|
|
81
|
+
exports.fingerprintRecipientPk = fingerprintRecipientPk;
|
|
82
|
+
exports.retrieveAndDecryptAccessKey = retrieveAndDecryptAccessKey;
|
|
83
|
+
//# sourceMappingURL=index.js.map
|
|
84
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/ipfs-access-key/retrieve-access-key.ts"],"names":["sha384","ml_kem768","ml_kem1024","hkdf"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Customer-side helper for retrieving an IPFS pinned-content\n * access-key envelope and decapsulating the wrapped CEK — Arc 10.\n *\n * Spec: `docs/superpowers/specs/2026-05-29-kyber-mlkem-production-readiness.md`\n * §4 row 9.\n *\n * # Layering\n *\n * Validators expose the envelope read path via `IpfsAccessKeyService.getEnvelope`\n * (libs/ipfs-core). Customers don't import that module — they import this\n * SDK helper. The SDK does NOT impose a fetch mechanism (HTTP vs direct\n * Mongo vs custom RPC); the caller passes an `envelopeProvider` callback\n * that returns the raw envelope JSON.\n *\n * # No key material on the wire\n *\n * The caller's Kyber secret key stays in their process. Decap runs locally.\n * The `envelopeProvider` only ever fetches a public artifact (the hybrid\n * envelope contains zero plaintext bytes — the wrapped CEK is encrypted to\n * the caller's pk).\n */\nimport { ml_kem768, ml_kem1024 } from '@noble/post-quantum/ml-kem';\nimport { hkdf } from '@noble/hashes/hkdf';\nimport { sha384 } from '@noble/hashes/sha2';\n\n/**\n * Minimal shape of the access-key envelope as it appears on the wire.\n * Mirrors `KyberAesEnvelopeV1` from\n * `libs/security-core/src/encryption-pqc/envelope.types.ts` but inlined\n * here so the SDK has zero workspace dependencies.\n */\nexport type IpfsAccessKeyEnvelope = {\n version: 'kyber-aes-v1';\n kemAlgorithm: 'ml-kem-768' | 'ml-kem-1024';\n kemCiphertext: string;\n recipientPkFingerprint: string;\n aesAlgorithm: 'aes-256-gcm';\n aesIv: string;\n aesCiphertext: string;\n aesAuthTag: string;\n kdfAlgorithm: 'hkdf-sha384';\n kdfSalt: string;\n kdfLabel: string;\n encryptedAt: number;\n};\n\n/**\n * Per-surface HKDF label prefix for IPFS access-key envelopes. Must match\n * the producer-side constant `IPFS_ACCESS_KEY_KDF_LABEL_PREFIX` in\n * `libs/ipfs-core/src/access-key-envelope/access-key-envelope.types.ts`.\n *\n * The full per-CID label is `${PREFIX}${cid}` — see\n * `buildIpfsAccessKeyKdfLabel`.\n */\nexport const IPFS_ACCESS_KEY_KDF_LABEL_PREFIX = 'ipfs-pinned-access-key-v1::';\n\n/** Build the per-CID HKDF label expected by `retrieveAndDecryptAccessKey`. */\nexport function buildIpfsAccessKeyKdfLabel(cid: string): string {\n return `${IPFS_ACCESS_KEY_KDF_LABEL_PREFIX}${cid}`;\n}\n\n/**\n * Compute the recipient-pk fingerprint exactly as the validator does:\n * `hex(sha384(pk)[0..16])`. The fingerprint identifies WHICH envelope row\n * to fetch when a CID has multiple recipients.\n */\nexport function fingerprintRecipientPk(recipientPk: Uint8Array): string {\n const hash = sha384(recipientPk).subarray(0, 16);\n return bytesToHex(hash);\n}\n\n/**\n * Fetch + decap the IPFS access-key envelope for `cid`.\n *\n * @param params.cid the IPFS CID whose CEK we want to unwrap.\n * @param params.recipientSk caller's Kyber secret key (must correspond\n * to the pk used by the producer).\n * @param params.recipientPk caller's Kyber public key — used to compute\n * the fingerprint passed to `envelopeProvider`. (We can't derive the pk\n * from the sk without re-running keygen on the seed, which the SDK does\n * not store, so the caller passes the pk explicitly.)\n * @param params.envelopeProvider caller-supplied fetch — given the CID +\n * their fingerprint, returns the raw envelope JSON or `null` if no row\n * exists. Caller chooses the fetch mechanism (HTTP API to the validator,\n * direct Mongo read, custom RPC, etc.).\n *\n * @returns the original CEK bytes, or `null` if no envelope row exists\n * for `(cid, fingerprint)` — back-compat path: content pinned before\n * Arc 10 has no envelope, caller falls back to \"no access control\".\n *\n * @throws if envelope's `kdfLabel` does not match the per-CID label\n * (substitution attack detected). AES-GCM auth-tag failure propagates\n * from the noble decryption path.\n */\nexport async function retrieveAndDecryptAccessKey(params: {\n cid: string;\n recipientSk: Uint8Array;\n recipientPk: Uint8Array;\n envelopeProvider: (\n cid: string,\n recipientPkFingerprint: string,\n ) => Promise<IpfsAccessKeyEnvelope | null>;\n}): Promise<Uint8Array | null> {\n const fp = fingerprintRecipientPk(params.recipientPk);\n const envelope = await params.envelopeProvider(params.cid, fp);\n if (!envelope) return null;\n\n const expectedLabel = buildIpfsAccessKeyKdfLabel(params.cid);\n if (envelope.kdfLabel !== expectedLabel) {\n throw new Error(\n `ipfs-access-key envelope label mismatch: expected='${expectedLabel}', ` +\n `actual='${envelope.kdfLabel}'`,\n );\n }\n\n // KEM decapsulation.\n const kem = envelope.kemAlgorithm === 'ml-kem-768' ? ml_kem768 : ml_kem1024;\n const kemCt = base64ToBytes(envelope.kemCiphertext);\n const sharedSecret = kem.decapsulate(kemCt, params.recipientSk);\n\n // HKDF-SHA384 → 32-byte AES-256 key.\n const kdfSalt = base64ToBytes(envelope.kdfSalt);\n const labelBytes = new TextEncoder().encode(envelope.kdfLabel);\n const derivedKey = hkdf(sha384, sharedSecret, kdfSalt, labelBytes, 32);\n\n // AES-256-GCM decrypt via WebCrypto (universal — works in Node, browser,\n // Deno, edge runtimes; the SDK avoids hard-coding Node's `crypto`).\n const aesIv = base64ToBytes(envelope.aesIv);\n const aesCt = base64ToBytes(envelope.aesCiphertext);\n const aesTag = base64ToBytes(envelope.aesAuthTag);\n\n // WebCrypto AES-GCM expects ct || tag concatenated. Backed by a fresh\n // ArrayBuffer (not SharedArrayBuffer) so the TypeScript lib's strict\n // BufferSource shape accepts it across all target runtimes.\n const ctWithTag = new Uint8Array(new ArrayBuffer(aesCt.length + aesTag.length));\n ctWithTag.set(aesCt, 0);\n ctWithTag.set(aesTag, aesCt.length);\n\n const derivedKeyBuf = new Uint8Array(new ArrayBuffer(derivedKey.length));\n derivedKeyBuf.set(derivedKey, 0);\n\n const aesIvBuf = new Uint8Array(new ArrayBuffer(aesIv.length));\n aesIvBuf.set(aesIv, 0);\n\n const subtle = getWebCrypto().subtle;\n const cryptoKey = await subtle.importKey(\n 'raw',\n derivedKeyBuf,\n { name: 'AES-GCM' },\n false,\n ['decrypt'],\n );\n const plaintext = await subtle.decrypt(\n { name: 'AES-GCM', iv: aesIvBuf, tagLength: 128 },\n cryptoKey,\n ctWithTag,\n );\n return new Uint8Array(plaintext);\n}\n\n// ── helpers (vendored to avoid Node-only deps) ────────────────────────\n\nfunction bytesToHex(bytes: Uint8Array): string {\n let out = '';\n for (const b of bytes) out += b.toString(16).padStart(2, '0');\n return out;\n}\n\nfunction base64ToBytes(b64: string): Uint8Array {\n // Universal base64 decode: works in Node (Buffer) and browsers (atob).\n if (typeof Buffer !== 'undefined') {\n return new Uint8Array(Buffer.from(b64, 'base64'));\n }\n const bin = atob(b64);\n const out = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i += 1) out[i] = bin.charCodeAt(i);\n return out;\n}\n\n/**\n * Minimal structural slice of the WebCrypto interface we depend on. Avoids\n * a hard dep on the lib.DOM `Crypto` global type — the SDK ships to Node,\n * browsers, Deno, and edge runtimes and we don't want to pull lib.DOM into\n * the build typecheck.\n */\ntype WebCryptoSlice = {\n subtle: {\n importKey(\n format: 'raw',\n keyData: ArrayBuffer | ArrayBufferView,\n algorithm: { name: 'AES-GCM' },\n extractable: boolean,\n keyUsages: ReadonlyArray<'decrypt'>,\n ): Promise<unknown>;\n decrypt(\n algorithm: { name: 'AES-GCM'; iv: ArrayBuffer | ArrayBufferView; tagLength: number },\n key: unknown,\n data: ArrayBuffer | ArrayBufferView,\n ): Promise<ArrayBuffer>;\n };\n};\n\nfunction getWebCrypto(): WebCryptoSlice {\n const g = globalThis as unknown as { crypto?: WebCryptoSlice };\n if (g.crypto?.subtle) return g.crypto;\n // Node 18+ exposes globalThis.crypto; older runtimes fall through here.\n throw new Error(\n 'retrieveAndDecryptAccessKey: WebCrypto (globalThis.crypto.subtle) is ' +\n 'not available in this runtime. Use Node 18+ or a browser/edge env.',\n );\n}\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Generated by dts-bundle-generator v9.5.1
|
|
2
|
+
|
|
3
|
+
export declare const K8S_SECRET_MATERIALIZATION_LABEL_ROOT = "k8s-secret-materialization-v1";
|
|
4
|
+
export type KyberKemAlgorithm = "ml-kem-768" | "ml-kem-1024";
|
|
5
|
+
export type DecryptK8sSecretValueOptions = {
|
|
6
|
+
labelSuffix?: string;
|
|
7
|
+
};
|
|
8
|
+
export declare class K8sSecretLabelMismatchError extends Error {
|
|
9
|
+
readonly expectedLabel: string;
|
|
10
|
+
readonly actualLabel: string;
|
|
11
|
+
constructor(expectedLabel: string, actualLabel: string);
|
|
12
|
+
}
|
|
13
|
+
export declare function decryptK8sSecretValue(writerEmittedValue: string, clusterKyberSk: Uint8Array, algo?: KyberKemAlgorithm, options?: DecryptK8sSecretValueOptions): Promise<Uint8Array>;
|
|
14
|
+
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var crypto = require('crypto');
|
|
4
|
+
var mlKem = require('@noble/post-quantum/ml-kem');
|
|
5
|
+
var hkdf = require('@noble/hashes/hkdf');
|
|
6
|
+
var sha2 = require('@noble/hashes/sha2');
|
|
7
|
+
|
|
8
|
+
// src/k8s-secret-reader/decrypt-k8s-secret-value.ts
|
|
9
|
+
var K8S_SECRET_MATERIALIZATION_LABEL_ROOT = "k8s-secret-materialization-v1";
|
|
10
|
+
var K8sSecretLabelMismatchError = class extends Error {
|
|
11
|
+
constructor(expectedLabel, actualLabel) {
|
|
12
|
+
super(
|
|
13
|
+
`K8s Secret envelope kdfLabel mismatch: expected='${expectedLabel}', actual='${actualLabel}'`
|
|
14
|
+
);
|
|
15
|
+
this.expectedLabel = expectedLabel;
|
|
16
|
+
this.actualLabel = actualLabel;
|
|
17
|
+
this.name = "K8sSecretLabelMismatchError";
|
|
18
|
+
}
|
|
19
|
+
expectedLabel;
|
|
20
|
+
actualLabel;
|
|
21
|
+
};
|
|
22
|
+
async function decryptK8sSecretValue(writerEmittedValue, clusterKyberSk, algo = "ml-kem-768", options) {
|
|
23
|
+
const suffix = options?.labelSuffix ?? "";
|
|
24
|
+
const expectedLabel = `${K8S_SECRET_MATERIALIZATION_LABEL_ROOT}${suffix}`;
|
|
25
|
+
let decodedText;
|
|
26
|
+
try {
|
|
27
|
+
decodedText = Buffer.from(writerEmittedValue, "base64").toString("utf-8");
|
|
28
|
+
} catch {
|
|
29
|
+
return new Uint8Array(Buffer.from(writerEmittedValue, "utf-8"));
|
|
30
|
+
}
|
|
31
|
+
let parsed;
|
|
32
|
+
try {
|
|
33
|
+
parsed = JSON.parse(decodedText);
|
|
34
|
+
} catch {
|
|
35
|
+
return new Uint8Array(Buffer.from(writerEmittedValue, "utf-8"));
|
|
36
|
+
}
|
|
37
|
+
if (isKyberAesV1Envelope(parsed)) {
|
|
38
|
+
return decryptHybridInline(parsed, clusterKyberSk, expectedLabel, algo);
|
|
39
|
+
}
|
|
40
|
+
return new Uint8Array(Buffer.from(decodedText, "utf-8"));
|
|
41
|
+
}
|
|
42
|
+
async function decryptHybridInline(env, clusterKyberSk, expectedLabel, algo) {
|
|
43
|
+
if (env.kdfLabel !== expectedLabel) {
|
|
44
|
+
throw new K8sSecretLabelMismatchError(expectedLabel, env.kdfLabel);
|
|
45
|
+
}
|
|
46
|
+
if (env.kemAlgorithm !== algo) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`K8s Secret envelope kemAlgorithm mismatch: caller='${algo}', envelope='${env.kemAlgorithm}'. Pass the matching algorithm argument to decryptK8sSecretValue.`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
const kem = env.kemAlgorithm === "ml-kem-768" ? mlKem.ml_kem768 : mlKem.ml_kem1024;
|
|
52
|
+
const kemCt = Buffer.from(env.kemCiphertext, "base64");
|
|
53
|
+
const sharedSecret = kem.decapsulate(kemCt, clusterKyberSk);
|
|
54
|
+
const kdfSalt = Buffer.from(env.kdfSalt, "base64");
|
|
55
|
+
const labelBytes = new TextEncoder().encode(env.kdfLabel);
|
|
56
|
+
const derivedKey = hkdf.hkdf(sha2.sha384, sharedSecret, kdfSalt, labelBytes, 32);
|
|
57
|
+
const aesIv = Buffer.from(env.aesIv, "base64");
|
|
58
|
+
const aesCt = Buffer.from(env.aesCiphertext, "base64");
|
|
59
|
+
const aesTag = Buffer.from(env.aesAuthTag, "base64");
|
|
60
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", derivedKey, aesIv);
|
|
61
|
+
decipher.setAuthTag(aesTag);
|
|
62
|
+
const ptHead = decipher.update(aesCt);
|
|
63
|
+
const ptTail = decipher.final();
|
|
64
|
+
return new Uint8Array(Buffer.concat([ptHead, ptTail]));
|
|
65
|
+
}
|
|
66
|
+
function isKyberAesV1Envelope(v) {
|
|
67
|
+
if (typeof v !== "object" || v === null) return false;
|
|
68
|
+
const env = v;
|
|
69
|
+
return env["version"] === "kyber-aes-v1" && typeof env["kemAlgorithm"] === "string" && typeof env["kemCiphertext"] === "string" && typeof env["aesCiphertext"] === "string" && typeof env["aesAuthTag"] === "string" && typeof env["aesIv"] === "string" && typeof env["kdfSalt"] === "string" && typeof env["kdfLabel"] === "string";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
exports.K8S_SECRET_MATERIALIZATION_LABEL_ROOT = K8S_SECRET_MATERIALIZATION_LABEL_ROOT;
|
|
73
|
+
exports.K8sSecretLabelMismatchError = K8sSecretLabelMismatchError;
|
|
74
|
+
exports.decryptK8sSecretValue = decryptK8sSecretValue;
|
|
75
|
+
//# sourceMappingURL=index.js.map
|
|
76
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/k8s-secret-reader/decrypt-k8s-secret-value.ts"],"names":["ml_kem768","ml_kem1024","hkdf","sha384","createDecipheriv"],"mappings":";;;;;;;;AAgFO,IAAM,qCAAA,GACX;AA6CK,IAAM,2BAAA,GAAN,cAA0C,KAAA,CAAM;AAAA,EACrD,WAAA,CACkB,eACA,WAAA,EAChB;AACA,IAAA,KAAA;AAAA,MACE,CAAA,iDAAA,EAAoD,aAAa,CAAA,WAAA,EAAc,WAAW,CAAA,CAAA;AAAA,KAC5F;AALgB,IAAA,IAAA,CAAA,aAAA,GAAA,aAAA;AACA,IAAA,IAAA,CAAA,WAAA,GAAA,WAAA;AAKhB,IAAA,IAAA,CAAK,IAAA,GAAO,6BAAA;AAAA,EACd;AAAA,EAPkB,aAAA;AAAA,EACA,WAAA;AAOpB;AA0BA,eAAsB,qBAAA,CACpB,kBAAA,EACA,cAAA,EACA,IAAA,GAA0B,cAC1B,OAAA,EACqB;AACrB,EAAA,MAAM,MAAA,GAAS,SAAS,WAAA,IAAe,EAAA;AACvC,EAAA,MAAM,aAAA,GAAgB,CAAA,EAAG,qCAAqC,CAAA,EAAG,MAAM,CAAA,CAAA;AAOvE,EAAA,IAAI,WAAA;AACJ,EAAA,IAAI;AACF,IAAA,WAAA,GAAc,OAAO,IAAA,CAAK,kBAAA,EAAoB,QAAQ,CAAA,CAAE,SAAS,OAAO,CAAA;AAAA,EAC1E,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAI,UAAA,CAAW,MAAA,CAAO,IAAA,CAAK,kBAAA,EAAoB,OAAO,CAAC,CAAA;AAAA,EAChE;AAKA,EAAA,IAAI,MAAA;AACJ,EAAA,IAAI;AACF,IAAA,MAAA,GAAS,IAAA,CAAK,MAAM,WAAW,CAAA;AAAA,EACjC,CAAA,CAAA,MAAQ;AAIN,IAAA,OAAO,IAAI,UAAA,CAAW,MAAA,CAAO,IAAA,CAAK,kBAAA,EAAoB,OAAO,CAAC,CAAA;AAAA,EAChE;AAIA,EAAA,IAAI,oBAAA,CAAqB,MAAM,CAAA,EAAG;AAChC,IAAA,OAAO,mBAAA,CAAoB,MAAA,EAAQ,cAAA,EAAgB,aAAA,EAAe,IAAI,CAAA;AAAA,EACxE;AAKA,EAAA,OAAO,IAAI,UAAA,CAAW,MAAA,CAAO,IAAA,CAAK,WAAA,EAAa,OAAO,CAAC,CAAA;AACzD;AAiBA,eAAe,mBAAA,CACb,GAAA,EACA,cAAA,EACA,aAAA,EACA,IAAA,EACqB;AACrB,EAAA,IAAI,GAAA,CAAI,aAAa,aAAA,EAAe;AAClC,IAAA,MAAM,IAAI,2BAAA,CAA4B,aAAA,EAAe,GAAA,CAAI,QAAQ,CAAA;AAAA,EACnE;AACA,EAAA,IAAI,GAAA,CAAI,iBAAiB,IAAA,EAAM;AAC7B,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,mDAAA,EAAsD,IAAI,CAAA,aAAA,EAAgB,GAAA,CAAI,YAAY,CAAA,iEAAA;AAAA,KAE5F;AAAA,EACF;AACA,EAAA,MAAM,GAAA,GAAM,GAAA,CAAI,YAAA,KAAiB,YAAA,GAAeA,eAAA,GAAYC,gBAAA;AAC5D,EAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,IAAA,CAAK,GAAA,CAAI,eAAe,QAAQ,CAAA;AACrD,EAAA,MAAM,YAAA,GAAe,GAAA,CAAI,WAAA,CAAY,KAAA,EAAO,cAAc,CAAA;AAC1D,EAAA,MAAM,OAAA,GAAU,MAAA,CAAO,IAAA,CAAK,GAAA,CAAI,SAAS,QAAQ,CAAA;AACjD,EAAA,MAAM,aAAa,IAAI,WAAA,EAAY,CAAE,MAAA,CAAO,IAAI,QAAQ,CAAA;AACxD,EAAA,MAAM,aAAaC,SAAA,CAAKC,WAAA,EAAQ,YAAA,EAAc,OAAA,EAAS,YAAY,EAAE,CAAA;AACrE,EAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,IAAA,CAAK,GAAA,CAAI,OAAO,QAAQ,CAAA;AAC7C,EAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,IAAA,CAAK,GAAA,CAAI,eAAe,QAAQ,CAAA;AACrD,EAAA,MAAM,MAAA,GAAS,MAAA,CAAO,IAAA,CAAK,GAAA,CAAI,YAAY,QAAQ,CAAA;AACnD,EAAA,MAAM,QAAA,GAAWC,uBAAA,CAAiB,aAAA,EAAe,UAAA,EAAY,KAAK,CAAA;AAClE,EAAA,QAAA,CAAS,WAAW,MAAM,CAAA;AAC1B,EAAA,MAAM,MAAA,GAAS,QAAA,CAAS,MAAA,CAAO,KAAK,CAAA;AACpC,EAAA,MAAM,MAAA,GAAS,SAAS,KAAA,EAAM;AAC9B,EAAA,OAAO,IAAI,WAAW,MAAA,CAAO,MAAA,CAAO,CAAC,MAAA,EAAQ,MAAM,CAAC,CAAC,CAAA;AACvD;AAYA,SAAS,qBAAqB,CAAA,EAAyC;AACrE,EAAA,IAAI,OAAO,CAAA,KAAM,QAAA,IAAY,CAAA,KAAM,MAAM,OAAO,KAAA;AAChD,EAAA,MAAM,GAAA,GAAM,CAAA;AACZ,EAAA,OACE,IAAI,SAAS,CAAA,KAAM,cAAA,IACnB,OAAO,IAAI,cAAc,CAAA,KAAM,QAAA,IAC/B,OAAO,IAAI,eAAe,CAAA,KAAM,QAAA,IAChC,OAAO,IAAI,eAAe,CAAA,KAAM,QAAA,IAChC,OAAO,IAAI,YAAY,CAAA,KAAM,QAAA,IAC7B,OAAO,IAAI,OAAO,CAAA,KAAM,QAAA,IACxB,OAAO,IAAI,SAAS,CAAA,KAAM,YAC1B,OAAO,GAAA,CAAI,UAAU,CAAA,KAAM,QAAA;AAE/B","file":"index.js","sourcesContent":["/**\n * Kyber Arc 9 — K8s Secret value decryption helper.\n *\n * Spec: `docs/superpowers/specs/2026-05-29-kyber-mlkem-production-readiness.md`\n * §4 row 8.\n *\n * # Contract\n *\n * Consumers of a Secret materialized by\n * `K8sSecretWriterService.writeOpaqueSecret` may receive EITHER:\n *\n * - **Envelope shape**: `base64(JSON.stringify(KyberAesEnvelopeV1))` —\n * the post-Arc-9 write path. Decrypts the inlined hybrid envelope\n * (KEM-decap → HKDF → AES-256-GCM) under the per-surface HKDF label\n * root + caller's `labelSuffix`. Mirror of\n * `libs/security-core/src/encryption-pqc/decrypt-hybrid.ts` —\n * intentionally inlined so the SDK has no security-core runtime\n * dependency (only the @noble/* primitives, which the SDK already\n * ships for the sibling `pqc-verify` + `pqc-verify-envelope` subpaths).\n * - **Legacy plaintext shape**: raw plaintext string written before Arc 9\n * wired the wrap. Returned bit-for-bit as UTF-8 bytes for back-compat\n * during the 12-month migration window per spec §7.\n *\n * The helper auto-detects the shape by base64-decoding then JSON-parsing\n * the value and checking `version === 'kyber-aes-v1'`. Any other shape →\n * legacy pass-through.\n *\n * # Label suffix discipline\n *\n * The writer's `options.labelSuffix` MUST match the reader's\n * `options.labelSuffix` — they compose the same final HKDF label\n * (`'k8s-secret-materialization-v1' + suffix`). Mismatch throws\n * `KyberLabelMismatchError` BEFORE KEM decap (substitution-attack defense).\n *\n * # Secret-key handling\n *\n * Callers MUST supply the cluster Kyber secret key. On smart-validator\n * this is `DkgKyberKeypairProvider.getClusterKyberKeypair(algo).secretKey`\n * — the SAME source the writer's `getClusterKyberPublicKey` consults.\n * Apps without TSS infrastructure can't decrypt envelopes — by design —\n * and either consume plaintext-only Secrets during back-compat OR receive\n * a delegated decryption endpoint from the validator (future arc).\n *\n * @example Validator-side consumer\n * ```ts\n * import { decryptK8sSecretValue } from '@hsuite/smart-engines-sdk/k8s-secret-reader';\n *\n * const pair = await clusterKyberProvider.getClusterKyberKeypair('ml-kem-768');\n * try {\n * const usernameBytes = await decryptK8sSecretValue(\n * // K8s `data` is base64 of the writer-emitted value — decode once first.\n * Buffer.from(secret.data['username'], 'base64').toString('utf-8'),\n * pair.secretKey,\n * 'ml-kem-768',\n * { labelSuffix: ':harbor-admin' },\n * );\n * const username = new TextDecoder().decode(usernameBytes);\n * } finally {\n * pair.secretKey.fill(0); // wipe after decap\n * }\n * ```\n */\nimport { createDecipheriv } from 'crypto';\nimport { ml_kem768, ml_kem1024 } from '@noble/post-quantum/ml-kem';\nimport { hkdf } from '@noble/hashes/hkdf';\nimport { sha384 } from '@noble/hashes/sha2';\n\n/**\n * Per-surface HKDF label root used by the K8s secret materialization\n * writer + reader pair. Re-exported so consumers can verify they're\n * composing the same label without having to import the provisioner-\n * internal constant.\n *\n * Final label composition: `${K8S_SECRET_MATERIALIZATION_LABEL_ROOT}${labelSuffix ?? ''}`.\n *\n * Examples:\n * - generic (no suffix): `'k8s-secret-materialization-v1'`\n * - harbor admin : `'k8s-secret-materialization-v1::harbor-admin'`\n * - smart-app env : `'k8s-secret-materialization-v1::smart-app-env'`\n */\nexport const K8S_SECRET_MATERIALIZATION_LABEL_ROOT =\n 'k8s-secret-materialization-v1';\n\n/**\n * ML-KEM parameter set literal (mirrors\n * `KyberKemAlgorithm` from `@hsuite/smart-engines-security-core`,\n * inlined to keep the SDK's runtime free of the security-core dep).\n */\nexport type KyberKemAlgorithm = 'ml-kem-768' | 'ml-kem-1024';\n\n/**\n * Options for {@link decryptK8sSecretValue}.\n *\n * @field labelSuffix - per-call-site discriminator the WRITER passed\n * (must match exactly; mismatch throws a label-mismatch error BEFORE\n * KEM decap). Default empty string (generic label root).\n */\nexport type DecryptK8sSecretValueOptions = {\n labelSuffix?: string;\n};\n\n/**\n * Internal — minimal KyberAesEnvelopeV1 shape sufficient for routing +\n * decap. Mirrors `libs/security-core/src/encryption-pqc/envelope.types.ts`\n * KyberAesEnvelopeV1. Inlined to keep the SDK security-core-free.\n */\ntype KyberAesEnvelopeV1Like = {\n version: 'kyber-aes-v1';\n kemAlgorithm: 'ml-kem-768' | 'ml-kem-1024';\n kemCiphertext: string;\n recipientPkFingerprint: string;\n aesAlgorithm: 'aes-256-gcm';\n aesIv: string;\n aesCiphertext: string;\n aesAuthTag: string;\n kdfAlgorithm: 'hkdf-sha384';\n kdfSalt: string;\n kdfLabel: string;\n encryptedAt: number;\n};\n\n/**\n * Thrown when the envelope's `kdfLabel` does not match the expected label\n * (writer/reader suffix mismatch or cross-surface substitution attempt).\n * Aborts BEFORE KEM decap (no timing leak on decap path).\n */\nexport class K8sSecretLabelMismatchError extends Error {\n constructor(\n public readonly expectedLabel: string,\n public readonly actualLabel: string,\n ) {\n super(\n `K8s Secret envelope kdfLabel mismatch: expected='${expectedLabel}', actual='${actualLabel}'`,\n );\n this.name = 'K8sSecretLabelMismatchError';\n }\n}\n\n/**\n * Decrypt a K8s Secret value written by `K8sSecretWriterService.writeOpaqueSecret`.\n *\n * @param writerEmittedValue - the value AS WRITTEN by the writer (the\n * string the writer placed in `stringData[key]`). When reading from a\n * live K8s Secret object via the API the `data` map is base64-encoded\n * by K8s — caller must base64-decode ONCE before calling this helper.\n * When reading from a file-system mount of the Secret (the typical\n * consumer path), the file content IS the writer-emitted value\n * directly.\n * @param clusterKyberSk - cluster Kyber secret key matching the\n * `kemAlgorithm` the writer used. Caller is responsible for memory\n * hygiene (wipe after decap).\n * @param algo - ML-KEM parameter set the writer used. Default\n * `'ml-kem-768'` (matches the writer's default for generic K8s secrets);\n * harbor admin sites use `'ml-kem-1024'`.\n * @param options - see {@link DecryptK8sSecretValueOptions}.\n * @returns plaintext bytes. UTF-8 strings can be reconstructed via\n * `new TextDecoder().decode(bytes)`.\n *\n * @throws {K8sSecretLabelMismatchError} if the envelope's `kdfLabel`\n * doesn't match the composed expected label.\n * @throws on AES-GCM auth failure (wrong sk, tampered envelope).\n */\nexport async function decryptK8sSecretValue(\n writerEmittedValue: string,\n clusterKyberSk: Uint8Array,\n algo: KyberKemAlgorithm = 'ml-kem-768',\n options?: DecryptK8sSecretValueOptions,\n): Promise<Uint8Array> {\n const suffix = options?.labelSuffix ?? '';\n const expectedLabel = `${K8S_SECRET_MATERIALIZATION_LABEL_ROOT}${suffix}`;\n\n // Step 1 — base64-decode the writer-emitted value. The writer's wrap\n // path emits `base64(JSON.stringify(envelope))`; the legacy plaintext\n // path emits the raw plaintext string. Either way base64-decoding\n // SHOULD succeed (raw plaintext as base64 may produce garbage bytes,\n // which won't JSON-parse below → legacy fall-through).\n let decodedText: string;\n try {\n decodedText = Buffer.from(writerEmittedValue, 'base64').toString('utf-8');\n } catch {\n return new Uint8Array(Buffer.from(writerEmittedValue, 'utf-8'));\n }\n\n // Step 2 — try JSON-parse for envelope detection. Pre-Arc-9 plaintext\n // values are arbitrary user strings; the vast majority WILL NOT parse\n // as JSON, so this is a cheap discriminator.\n let parsed: unknown;\n try {\n parsed = JSON.parse(decodedText);\n } catch {\n // Not JSON → legacy plaintext during compat window. Return the\n // ORIGINAL bytes the writer stored (the writer didn't base64-wrap\n // legacy values either — they went through as-is in `stringData`).\n return new Uint8Array(Buffer.from(writerEmittedValue, 'utf-8'));\n }\n\n // Step 3 — version discriminator. Envelope-shaped JSON with the\n // canonical version literal → decap via the inlined hybrid path.\n if (isKyberAesV1Envelope(parsed)) {\n return decryptHybridInline(parsed, clusterKyberSk, expectedLabel, algo);\n }\n\n // Step 4 — JSON-shaped but NOT our envelope (some pre-Arc-9 caller\n // stored a JSON blob in a Secret value). Return the decoded UTF-8 bytes\n // verbatim so the consumer sees the original JSON string.\n return new Uint8Array(Buffer.from(decodedText, 'utf-8'));\n}\n\n/**\n * Inlined hybrid-decrypt — bit-for-bit equivalent of\n * `libs/security-core/src/encryption-pqc/decrypt-hybrid.ts`. The SDK\n * cannot import security-core (would force every downstream npm consumer\n * to pull the workspace package), so the primitive is duplicated under\n * the same noble dependency set.\n *\n * Steps:\n * 1. Label gate — pre-decap check. Fail-loud BEFORE KEM work.\n * 2. KEM decap with `clusterKyberSk`. The envelope's `kemAlgorithm`\n * MUST match the caller's `algo` argument (otherwise decap silently\n * yields garbage → AES-GCM auth fails in step 4). Cross-checked here.\n * 3. HKDF-SHA384(sharedSecret, salt, label) → 32-byte AES-256 key.\n * 4. AES-256-GCM decrypt with the envelope's IV + auth tag.\n */\nasync function decryptHybridInline(\n env: KyberAesEnvelopeV1Like,\n clusterKyberSk: Uint8Array,\n expectedLabel: string,\n algo: KyberKemAlgorithm,\n): Promise<Uint8Array> {\n if (env.kdfLabel !== expectedLabel) {\n throw new K8sSecretLabelMismatchError(expectedLabel, env.kdfLabel);\n }\n if (env.kemAlgorithm !== algo) {\n throw new Error(\n `K8s Secret envelope kemAlgorithm mismatch: caller='${algo}', envelope='${env.kemAlgorithm}'. ` +\n `Pass the matching algorithm argument to decryptK8sSecretValue.`,\n );\n }\n const kem = env.kemAlgorithm === 'ml-kem-768' ? ml_kem768 : ml_kem1024;\n const kemCt = Buffer.from(env.kemCiphertext, 'base64');\n const sharedSecret = kem.decapsulate(kemCt, clusterKyberSk);\n const kdfSalt = Buffer.from(env.kdfSalt, 'base64');\n const labelBytes = new TextEncoder().encode(env.kdfLabel);\n const derivedKey = hkdf(sha384, sharedSecret, kdfSalt, labelBytes, 32);\n const aesIv = Buffer.from(env.aesIv, 'base64');\n const aesCt = Buffer.from(env.aesCiphertext, 'base64');\n const aesTag = Buffer.from(env.aesAuthTag, 'base64');\n const decipher = createDecipheriv('aes-256-gcm', derivedKey, aesIv);\n decipher.setAuthTag(aesTag);\n const ptHead = decipher.update(aesCt);\n const ptTail = decipher.final();\n return new Uint8Array(Buffer.concat([ptHead, ptTail]));\n}\n\n/**\n * Discriminator — returns `true` iff the parsed value is a Kyber-AES-v1\n * envelope shape. Checks the `version` literal + minimum field presence\n * to defend against partial/corrupted JSON that happens to be a valid\n * object but lacks the envelope fields.\n *\n * Schema-level validation (kemAlgorithm enum, field types, base64\n * correctness) is handled by the noble libs at decap time — this is\n * purely the routing decision.\n */\nfunction isKyberAesV1Envelope(v: unknown): v is KyberAesEnvelopeV1Like {\n if (typeof v !== 'object' || v === null) return false;\n const env = v as Record<string, unknown>;\n return (\n env['version'] === 'kyber-aes-v1' &&\n typeof env['kemAlgorithm'] === 'string' &&\n typeof env['kemCiphertext'] === 'string' &&\n typeof env['aesCiphertext'] === 'string' &&\n typeof env['aesAuthTag'] === 'string' &&\n typeof env['aesIv'] === 'string' &&\n typeof env['kdfSalt'] === 'string' &&\n typeof env['kdfLabel'] === 'string'\n );\n}\n"]}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Generated by dts-bundle-generator v9.5.1
|
|
2
|
+
|
|
3
|
+
export type EnvelopeVersionLiteral = "kyber-aes-v1" | "aes-v0";
|
|
4
|
+
export type SchemaValidationResult = {
|
|
5
|
+
ok: true;
|
|
6
|
+
version: EnvelopeVersionLiteral;
|
|
7
|
+
} | {
|
|
8
|
+
ok: false;
|
|
9
|
+
reason: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function validateEnvelopeSchema(envelope: unknown): SchemaValidationResult;
|
|
12
|
+
export declare const KYBER_MIN_TIMESTAMP_MS = 1704067200000;
|
|
13
|
+
export type VerificationDetails = {
|
|
14
|
+
version: string;
|
|
15
|
+
kemAlgorithm?: "ml-kem-768" | "ml-kem-1024";
|
|
16
|
+
recipientPkFingerprint?: string;
|
|
17
|
+
kdfLabel?: string;
|
|
18
|
+
encryptedAt?: number;
|
|
19
|
+
schemaValid: boolean;
|
|
20
|
+
base64Valid: boolean;
|
|
21
|
+
timestampPlausible: boolean;
|
|
22
|
+
};
|
|
23
|
+
export type EnvelopeVerificationResult = {
|
|
24
|
+
valid: true;
|
|
25
|
+
version: EnvelopeVersionLiteral;
|
|
26
|
+
details: VerificationDetails;
|
|
27
|
+
} | {
|
|
28
|
+
valid: false;
|
|
29
|
+
reason: string;
|
|
30
|
+
details?: Partial<VerificationDetails>;
|
|
31
|
+
};
|
|
32
|
+
export type VerifyEnvelopeOptions = {
|
|
33
|
+
expectedLabel?: string;
|
|
34
|
+
expectedRecipientPkFingerprint?: string;
|
|
35
|
+
minTimestamp?: number;
|
|
36
|
+
maxTimestamp?: number;
|
|
37
|
+
};
|
|
38
|
+
export declare function verifyPqcEnvelope(envelope: unknown, options?: VerifyEnvelopeOptions): Promise<EnvelopeVerificationResult>;
|
|
39
|
+
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/pqc-verify-envelope/envelope-schema-validator.ts
|
|
4
|
+
var KEM_CT_LEN = {
|
|
5
|
+
"ml-kem-768": 1088,
|
|
6
|
+
"ml-kem-1024": 1568
|
|
7
|
+
};
|
|
8
|
+
var AES_IV_LEN = 12;
|
|
9
|
+
var AES_TAG_LEN = 16;
|
|
10
|
+
var KDF_SALT_LEN = 16;
|
|
11
|
+
function tryDecodeBase64(s) {
|
|
12
|
+
if (typeof s !== "string" || s.length === 0) return null;
|
|
13
|
+
try {
|
|
14
|
+
const buf = Buffer.from(s, "base64");
|
|
15
|
+
if (buf.toString("base64").replace(/=+$/, "") !== s.replace(/=+$/, "")) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return new Uint8Array(buf);
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function isString(v) {
|
|
24
|
+
return typeof v === "string";
|
|
25
|
+
}
|
|
26
|
+
function isNonEmptyString(v) {
|
|
27
|
+
return typeof v === "string" && v.length > 0;
|
|
28
|
+
}
|
|
29
|
+
function isFiniteNumber(v) {
|
|
30
|
+
return typeof v === "number" && Number.isFinite(v);
|
|
31
|
+
}
|
|
32
|
+
function isPlainObject(v) {
|
|
33
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
34
|
+
}
|
|
35
|
+
function validateEnvelopeSchema(envelope) {
|
|
36
|
+
if (!isPlainObject(envelope)) {
|
|
37
|
+
return { ok: false, reason: "envelope must be a JSON object" };
|
|
38
|
+
}
|
|
39
|
+
const version = envelope.version;
|
|
40
|
+
if (version === "kyber-aes-v1") {
|
|
41
|
+
return validateKyberAesV1(envelope);
|
|
42
|
+
}
|
|
43
|
+
if (version === "aes-v0") {
|
|
44
|
+
return validateAesV0(envelope);
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
ok: false,
|
|
48
|
+
reason: `unknown envelope version: ${JSON.stringify(version)}`
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function validateKyberAesV1(env) {
|
|
52
|
+
const kemAlgorithm = env.kemAlgorithm;
|
|
53
|
+
if (kemAlgorithm !== "ml-kem-768" && kemAlgorithm !== "ml-kem-1024") {
|
|
54
|
+
return {
|
|
55
|
+
ok: false,
|
|
56
|
+
reason: `kemAlgorithm must be 'ml-kem-768' or 'ml-kem-1024'`
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (!isNonEmptyString(env.kemCiphertext)) {
|
|
60
|
+
return { ok: false, reason: "kemCiphertext must be a non-empty base64 string" };
|
|
61
|
+
}
|
|
62
|
+
const kemCtBytes = tryDecodeBase64(env.kemCiphertext);
|
|
63
|
+
if (!kemCtBytes) {
|
|
64
|
+
return { ok: false, reason: "kemCiphertext is not valid base64" };
|
|
65
|
+
}
|
|
66
|
+
const expectedKemLen = KEM_CT_LEN[kemAlgorithm];
|
|
67
|
+
if (kemCtBytes.length !== expectedKemLen) {
|
|
68
|
+
return {
|
|
69
|
+
ok: false,
|
|
70
|
+
reason: `kemCiphertext length ${kemCtBytes.length} != expected ${expectedKemLen} for ${kemAlgorithm}`
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (!isString(env.recipientPkFingerprint) || !/^[0-9a-f]{32}$/i.test(env.recipientPkFingerprint)) {
|
|
74
|
+
return {
|
|
75
|
+
ok: false,
|
|
76
|
+
reason: "recipientPkFingerprint must be a 32-char hex string (16-byte sha384 prefix)"
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (env.aesAlgorithm !== "aes-256-gcm") {
|
|
80
|
+
return { ok: false, reason: `aesAlgorithm must be 'aes-256-gcm'` };
|
|
81
|
+
}
|
|
82
|
+
if (!isNonEmptyString(env.aesIv)) {
|
|
83
|
+
return { ok: false, reason: "aesIv must be a non-empty base64 string" };
|
|
84
|
+
}
|
|
85
|
+
const ivBytes = tryDecodeBase64(env.aesIv);
|
|
86
|
+
if (!ivBytes) return { ok: false, reason: "aesIv is not valid base64" };
|
|
87
|
+
if (ivBytes.length !== AES_IV_LEN) {
|
|
88
|
+
return {
|
|
89
|
+
ok: false,
|
|
90
|
+
reason: `aesIv length ${ivBytes.length} != expected ${AES_IV_LEN} (AES-GCM 96-bit nonce)`
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (typeof env.aesCiphertext !== "string") {
|
|
94
|
+
return { ok: false, reason: "aesCiphertext must be a base64 string" };
|
|
95
|
+
}
|
|
96
|
+
if (env.aesCiphertext.length > 0) {
|
|
97
|
+
const ctBytes = tryDecodeBase64(env.aesCiphertext);
|
|
98
|
+
if (!ctBytes) return { ok: false, reason: "aesCiphertext is not valid base64" };
|
|
99
|
+
}
|
|
100
|
+
if (!isNonEmptyString(env.aesAuthTag)) {
|
|
101
|
+
return { ok: false, reason: "aesAuthTag must be a non-empty base64 string" };
|
|
102
|
+
}
|
|
103
|
+
const tagBytes = tryDecodeBase64(env.aesAuthTag);
|
|
104
|
+
if (!tagBytes) return { ok: false, reason: "aesAuthTag is not valid base64" };
|
|
105
|
+
if (tagBytes.length !== AES_TAG_LEN) {
|
|
106
|
+
return {
|
|
107
|
+
ok: false,
|
|
108
|
+
reason: `aesAuthTag length ${tagBytes.length} != expected ${AES_TAG_LEN} (AES-GCM 128-bit tag)`
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (env.kdfAlgorithm !== "hkdf-sha384") {
|
|
112
|
+
return { ok: false, reason: `kdfAlgorithm must be 'hkdf-sha384'` };
|
|
113
|
+
}
|
|
114
|
+
if (!isNonEmptyString(env.kdfSalt)) {
|
|
115
|
+
return { ok: false, reason: "kdfSalt must be a non-empty base64 string" };
|
|
116
|
+
}
|
|
117
|
+
const saltBytes = tryDecodeBase64(env.kdfSalt);
|
|
118
|
+
if (!saltBytes) return { ok: false, reason: "kdfSalt is not valid base64" };
|
|
119
|
+
if (saltBytes.length !== KDF_SALT_LEN) {
|
|
120
|
+
return {
|
|
121
|
+
ok: false,
|
|
122
|
+
reason: `kdfSalt length ${saltBytes.length} != expected ${KDF_SALT_LEN}`
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (!isNonEmptyString(env.kdfLabel)) {
|
|
126
|
+
return { ok: false, reason: "kdfLabel must be a non-empty string" };
|
|
127
|
+
}
|
|
128
|
+
if (env.kdfLabel.length > 256) {
|
|
129
|
+
return {
|
|
130
|
+
ok: false,
|
|
131
|
+
reason: `kdfLabel length ${env.kdfLabel.length} exceeds 256-char limit`
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (!isFiniteNumber(env.encryptedAt)) {
|
|
135
|
+
return { ok: false, reason: "encryptedAt must be a finite number (millis epoch)" };
|
|
136
|
+
}
|
|
137
|
+
return { ok: true, version: "kyber-aes-v1" };
|
|
138
|
+
}
|
|
139
|
+
function validateAesV0(env) {
|
|
140
|
+
if (!isNonEmptyString(env.aesIv)) {
|
|
141
|
+
return { ok: false, reason: "aesIv must be a non-empty base64 string" };
|
|
142
|
+
}
|
|
143
|
+
const ivBytes = tryDecodeBase64(env.aesIv);
|
|
144
|
+
if (!ivBytes) return { ok: false, reason: "aesIv is not valid base64" };
|
|
145
|
+
if (ivBytes.length !== AES_IV_LEN) {
|
|
146
|
+
return {
|
|
147
|
+
ok: false,
|
|
148
|
+
reason: `aesIv length ${ivBytes.length} != expected ${AES_IV_LEN} (AES-GCM 96-bit nonce)`
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (typeof env.aesCiphertext !== "string") {
|
|
152
|
+
return { ok: false, reason: "aesCiphertext must be a base64 string" };
|
|
153
|
+
}
|
|
154
|
+
if (env.aesCiphertext.length > 0) {
|
|
155
|
+
const ctBytes = tryDecodeBase64(env.aesCiphertext);
|
|
156
|
+
if (!ctBytes) return { ok: false, reason: "aesCiphertext is not valid base64" };
|
|
157
|
+
}
|
|
158
|
+
if (!isNonEmptyString(env.aesAuthTag)) {
|
|
159
|
+
return { ok: false, reason: "aesAuthTag must be a non-empty base64 string" };
|
|
160
|
+
}
|
|
161
|
+
const tagBytes = tryDecodeBase64(env.aesAuthTag);
|
|
162
|
+
if (!tagBytes) return { ok: false, reason: "aesAuthTag is not valid base64" };
|
|
163
|
+
if (tagBytes.length !== AES_TAG_LEN) {
|
|
164
|
+
return {
|
|
165
|
+
ok: false,
|
|
166
|
+
reason: `aesAuthTag length ${tagBytes.length} != expected ${AES_TAG_LEN} (AES-GCM 128-bit tag)`
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return { ok: true, version: "aes-v0" };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/pqc-verify-envelope/verify-pqc-envelope.ts
|
|
173
|
+
var KYBER_MIN_TIMESTAMP_MS = 17040672e5;
|
|
174
|
+
async function verifyPqcEnvelope(envelope, options = {}) {
|
|
175
|
+
const schema = validateEnvelopeSchema(envelope);
|
|
176
|
+
if (!schema.ok) {
|
|
177
|
+
return {
|
|
178
|
+
valid: false,
|
|
179
|
+
reason: `schema-invalid: ${schema.reason}`,
|
|
180
|
+
details: { schemaValid: false, base64Valid: false, timestampPlausible: false }
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const env = envelope;
|
|
184
|
+
const version = schema.version;
|
|
185
|
+
const details = {
|
|
186
|
+
version,
|
|
187
|
+
schemaValid: true,
|
|
188
|
+
base64Valid: true,
|
|
189
|
+
// computed below for kyber-aes-v1; legacy has no encryptedAt so treat as plausible.
|
|
190
|
+
timestampPlausible: true
|
|
191
|
+
};
|
|
192
|
+
if (version === "kyber-aes-v1") {
|
|
193
|
+
details.kemAlgorithm = env.kemAlgorithm;
|
|
194
|
+
details.recipientPkFingerprint = env.recipientPkFingerprint;
|
|
195
|
+
details.kdfLabel = env.kdfLabel;
|
|
196
|
+
details.encryptedAt = env.encryptedAt;
|
|
197
|
+
const minTs = options.minTimestamp ?? KYBER_MIN_TIMESTAMP_MS;
|
|
198
|
+
const maxTs = options.maxTimestamp ?? Date.now();
|
|
199
|
+
const ts = details.encryptedAt;
|
|
200
|
+
if (ts < minTs) {
|
|
201
|
+
details.timestampPlausible = false;
|
|
202
|
+
return {
|
|
203
|
+
valid: false,
|
|
204
|
+
reason: `timestamp-before-minimum: encryptedAt=${ts} < min=${minTs}`,
|
|
205
|
+
details
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
if (ts > maxTs) {
|
|
209
|
+
details.timestampPlausible = false;
|
|
210
|
+
return {
|
|
211
|
+
valid: false,
|
|
212
|
+
reason: `timestamp-in-future: encryptedAt=${ts} > max=${maxTs}`,
|
|
213
|
+
details
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
if (options.expectedLabel !== void 0) {
|
|
217
|
+
if (details.kdfLabel !== options.expectedLabel) {
|
|
218
|
+
return {
|
|
219
|
+
valid: false,
|
|
220
|
+
reason: `label-mismatch: expected='${options.expectedLabel}' actual='${details.kdfLabel}'`,
|
|
221
|
+
details
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (options.expectedRecipientPkFingerprint !== void 0) {
|
|
226
|
+
const a = (details.recipientPkFingerprint ?? "").toLowerCase();
|
|
227
|
+
const b = options.expectedRecipientPkFingerprint.toLowerCase();
|
|
228
|
+
if (a !== b) {
|
|
229
|
+
return {
|
|
230
|
+
valid: false,
|
|
231
|
+
reason: `recipient-pk-fingerprint-mismatch: expected='${b}' actual='${a}'`,
|
|
232
|
+
details
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return { valid: true, version, details };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
exports.KYBER_MIN_TIMESTAMP_MS = KYBER_MIN_TIMESTAMP_MS;
|
|
241
|
+
exports.validateEnvelopeSchema = validateEnvelopeSchema;
|
|
242
|
+
exports.verifyPqcEnvelope = verifyPqcEnvelope;
|
|
243
|
+
//# sourceMappingURL=index.js.map
|
|
244
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/pqc-verify-envelope/envelope-schema-validator.ts","../../src/pqc-verify-envelope/verify-pqc-envelope.ts"],"names":[],"mappings":";;;AAwDA,IAAM,UAAA,GAA2D;AAAA,EAC/D,YAAA,EAAc,IAAA;AAAA,EACd,aAAA,EAAe;AACjB,CAAA;AAGA,IAAM,UAAA,GAAa,EAAA;AAEnB,IAAM,WAAA,GAAc,EAAA;AAEpB,IAAM,YAAA,GAAe,EAAA;AAUrB,SAAS,gBAAgB,CAAA,EAA8B;AACrD,EAAA,IAAI,OAAO,CAAA,KAAM,QAAA,IAAY,CAAA,CAAE,MAAA,KAAW,GAAG,OAAO,IAAA;AACpD,EAAA,IAAI;AACF,IAAA,MAAM,GAAA,GAAM,MAAA,CAAO,IAAA,CAAK,CAAA,EAAG,QAAQ,CAAA;AAGnC,IAAA,IAAI,GAAA,CAAI,QAAA,CAAS,QAAQ,CAAA,CAAE,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAA,KAAM,CAAA,CAAE,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAA,EAAG;AACtE,MAAA,OAAO,IAAA;AAAA,IACT;AACA,IAAA,OAAO,IAAI,WAAW,GAAG,CAAA;AAAA,EAC3B,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACF;AAEA,SAAS,SAAS,CAAA,EAAyB;AACzC,EAAA,OAAO,OAAO,CAAA,KAAM,QAAA;AACtB;AAEA,SAAS,iBAAiB,CAAA,EAAyB;AACjD,EAAA,OAAO,OAAO,CAAA,KAAM,QAAA,IAAY,CAAA,CAAE,MAAA,GAAS,CAAA;AAC7C;AAEA,SAAS,eAAe,CAAA,EAAyB;AAC/C,EAAA,OAAO,OAAO,CAAA,KAAM,QAAA,IAAY,MAAA,CAAO,SAAS,CAAC,CAAA;AACnD;AAEA,SAAS,cAAc,CAAA,EAA0C;AAC/D,EAAA,OAAO,OAAO,MAAM,QAAA,IAAY,CAAA,KAAM,QAAQ,CAAC,KAAA,CAAM,QAAQ,CAAC,CAAA;AAChE;AAWO,SAAS,uBAAuB,QAAA,EAA2C;AAChF,EAAA,IAAI,CAAC,aAAA,CAAc,QAAQ,CAAA,EAAG;AAC5B,IAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,gCAAA,EAAiC;AAAA,EAC/D;AAEA,EAAA,MAAM,UAAW,QAAA,CAAmC,OAAA;AACpD,EAAA,IAAI,YAAY,cAAA,EAAgB;AAC9B,IAAA,OAAO,mBAAmB,QAAQ,CAAA;AAAA,EACpC;AACA,EAAA,IAAI,YAAY,QAAA,EAAU;AACxB,IAAA,OAAO,cAAc,QAAQ,CAAA;AAAA,EAC/B;AACA,EAAA,OAAO;AAAA,IACL,EAAA,EAAI,KAAA;AAAA,IACJ,MAAA,EAAQ,CAAA,0BAAA,EAA6B,IAAA,CAAK,SAAA,CAAU,OAAO,CAAC,CAAA;AAAA,GAC9D;AACF;AAEA,SAAS,mBAAmB,GAAA,EAAsD;AAEhF,EAAA,MAAM,eAAe,GAAA,CAAI,YAAA;AACzB,EAAA,IAAI,YAAA,KAAiB,YAAA,IAAgB,YAAA,KAAiB,aAAA,EAAe;AACnE,IAAA,OAAO;AAAA,MACL,EAAA,EAAI,KAAA;AAAA,MACJ,MAAA,EAAQ,CAAA,kDAAA;AAAA,KACV;AAAA,EACF;AAGA,EAAA,IAAI,CAAC,gBAAA,CAAiB,GAAA,CAAI,aAAa,CAAA,EAAG;AACxC,IAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,iDAAA,EAAkD;AAAA,EAChF;AACA,EAAA,MAAM,UAAA,GAAa,eAAA,CAAgB,GAAA,CAAI,aAAa,CAAA;AACpD,EAAA,IAAI,CAAC,UAAA,EAAY;AACf,IAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,mCAAA,EAAoC;AAAA,EAClE;AACA,EAAA,MAAM,cAAA,GAAiB,WAAW,YAAY,CAAA;AAC9C,EAAA,IAAI,UAAA,CAAW,WAAW,cAAA,EAAgB;AACxC,IAAA,OAAO;AAAA,MACL,EAAA,EAAI,KAAA;AAAA,MACJ,QAAQ,CAAA,qBAAA,EAAwB,UAAA,CAAW,MAAM,CAAA,aAAA,EAAgB,cAAc,QAAQ,YAAY,CAAA;AAAA,KACrG;AAAA,EACF;AAGA,EAAA,IAAI,CAAC,QAAA,CAAS,GAAA,CAAI,sBAAsB,CAAA,IAAK,CAAC,iBAAA,CAAkB,IAAA,CAAK,GAAA,CAAI,sBAAsB,CAAA,EAAG;AAChG,IAAA,OAAO;AAAA,MACL,EAAA,EAAI,KAAA;AAAA,MACJ,MAAA,EAAQ;AAAA,KACV;AAAA,EACF;AAGA,EAAA,IAAI,GAAA,CAAI,iBAAiB,aAAA,EAAe;AACtC,IAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,CAAA,kCAAA,CAAA,EAAqC;AAAA,EACnE;AAGA,EAAA,IAAI,CAAC,gBAAA,CAAiB,GAAA,CAAI,KAAK,CAAA,EAAG;AAChC,IAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,yCAAA,EAA0C;AAAA,EACxE;AACA,EAAA,MAAM,OAAA,GAAU,eAAA,CAAgB,GAAA,CAAI,KAAK,CAAA;AACzC,EAAA,IAAI,CAAC,OAAA,EAAS,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,QAAQ,2BAAA,EAA4B;AACtE,EAAA,IAAI,OAAA,CAAQ,WAAW,UAAA,EAAY;AACjC,IAAA,OAAO;AAAA,MACL,EAAA,EAAI,KAAA;AAAA,MACJ,MAAA,EAAQ,CAAA,aAAA,EAAgB,OAAA,CAAQ,MAAM,gBAAgB,UAAU,CAAA,uBAAA;AAAA,KAClE;AAAA,EACF;AAGA,EAAA,IAAI,OAAO,GAAA,CAAI,aAAA,KAAkB,QAAA,EAAU;AACzC,IAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,uCAAA,EAAwC;AAAA,EACtE;AAEA,EAAA,IAAI,GAAA,CAAI,aAAA,CAAc,MAAA,GAAS,CAAA,EAAG;AAChC,IAAA,MAAM,OAAA,GAAU,eAAA,CAAgB,GAAA,CAAI,aAAa,CAAA;AACjD,IAAA,IAAI,CAAC,OAAA,EAAS,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,QAAQ,mCAAA,EAAoC;AAAA,EAChF;AAGA,EAAA,IAAI,CAAC,gBAAA,CAAiB,GAAA,CAAI,UAAU,CAAA,EAAG;AACrC,IAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,8CAAA,EAA+C;AAAA,EAC7E;AACA,EAAA,MAAM,QAAA,GAAW,eAAA,CAAgB,GAAA,CAAI,UAAU,CAAA;AAC/C,EAAA,IAAI,CAAC,QAAA,EAAU,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,QAAQ,gCAAA,EAAiC;AAC5E,EAAA,IAAI,QAAA,CAAS,WAAW,WAAA,EAAa;AACnC,IAAA,OAAO;AAAA,MACL,EAAA,EAAI,KAAA;AAAA,MACJ,MAAA,EAAQ,CAAA,kBAAA,EAAqB,QAAA,CAAS,MAAM,gBAAgB,WAAW,CAAA,sBAAA;AAAA,KACzE;AAAA,EACF;AAGA,EAAA,IAAI,GAAA,CAAI,iBAAiB,aAAA,EAAe;AACtC,IAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,CAAA,kCAAA,CAAA,EAAqC;AAAA,EACnE;AAGA,EAAA,IAAI,CAAC,gBAAA,CAAiB,GAAA,CAAI,OAAO,CAAA,EAAG;AAClC,IAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,2CAAA,EAA4C;AAAA,EAC1E;AACA,EAAA,MAAM,SAAA,GAAY,eAAA,CAAgB,GAAA,CAAI,OAAO,CAAA;AAC7C,EAAA,IAAI,CAAC,SAAA,EAAW,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,QAAQ,6BAAA,EAA8B;AAC1E,EAAA,IAAI,SAAA,CAAU,WAAW,YAAA,EAAc;AACrC,IAAA,OAAO;AAAA,MACL,EAAA,EAAI,KAAA;AAAA,MACJ,MAAA,EAAQ,CAAA,eAAA,EAAkB,SAAA,CAAU,MAAM,gBAAgB,YAAY,CAAA;AAAA,KACxE;AAAA,EACF;AAIA,EAAA,IAAI,CAAC,gBAAA,CAAiB,GAAA,CAAI,QAAQ,CAAA,EAAG;AACnC,IAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,qCAAA,EAAsC;AAAA,EACpE;AACA,EAAA,IAAI,GAAA,CAAI,QAAA,CAAS,MAAA,GAAS,GAAA,EAAK;AAC7B,IAAA,OAAO;AAAA,MACL,EAAA,EAAI,KAAA;AAAA,MACJ,MAAA,EAAQ,CAAA,gBAAA,EAAmB,GAAA,CAAI,QAAA,CAAS,MAAM,CAAA,uBAAA;AAAA,KAChD;AAAA,EACF;AAGA,EAAA,IAAI,CAAC,cAAA,CAAe,GAAA,CAAI,WAAW,CAAA,EAAG;AACpC,IAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,oDAAA,EAAqD;AAAA,EACnF;AAEA,EAAA,OAAO,EAAE,EAAA,EAAI,IAAA,EAAM,OAAA,EAAS,cAAA,EAAe;AAC7C;AAEA,SAAS,cAAc,GAAA,EAAsD;AAC3E,EAAA,IAAI,CAAC,gBAAA,CAAiB,GAAA,CAAI,KAAK,CAAA,EAAG;AAChC,IAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,yCAAA,EAA0C;AAAA,EACxE;AACA,EAAA,MAAM,OAAA,GAAU,eAAA,CAAgB,GAAA,CAAI,KAAK,CAAA;AACzC,EAAA,IAAI,CAAC,OAAA,EAAS,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,QAAQ,2BAAA,EAA4B;AACtE,EAAA,IAAI,OAAA,CAAQ,WAAW,UAAA,EAAY;AACjC,IAAA,OAAO;AAAA,MACL,EAAA,EAAI,KAAA;AAAA,MACJ,MAAA,EAAQ,CAAA,aAAA,EAAgB,OAAA,CAAQ,MAAM,gBAAgB,UAAU,CAAA,uBAAA;AAAA,KAClE;AAAA,EACF;AAEA,EAAA,IAAI,OAAO,GAAA,CAAI,aAAA,KAAkB,QAAA,EAAU;AACzC,IAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,uCAAA,EAAwC;AAAA,EACtE;AACA,EAAA,IAAI,GAAA,CAAI,aAAA,CAAc,MAAA,GAAS,CAAA,EAAG;AAChC,IAAA,MAAM,OAAA,GAAU,eAAA,CAAgB,GAAA,CAAI,aAAa,CAAA;AACjD,IAAA,IAAI,CAAC,OAAA,EAAS,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,QAAQ,mCAAA,EAAoC;AAAA,EAChF;AAEA,EAAA,IAAI,CAAC,gBAAA,CAAiB,GAAA,CAAI,UAAU,CAAA,EAAG;AACrC,IAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,8CAAA,EAA+C;AAAA,EAC7E;AACA,EAAA,MAAM,QAAA,GAAW,eAAA,CAAgB,GAAA,CAAI,UAAU,CAAA;AAC/C,EAAA,IAAI,CAAC,QAAA,EAAU,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,QAAQ,gCAAA,EAAiC;AAC5E,EAAA,IAAI,QAAA,CAAS,WAAW,WAAA,EAAa;AACnC,IAAA,OAAO;AAAA,MACL,EAAA,EAAI,KAAA;AAAA,MACJ,MAAA,EAAQ,CAAA,kBAAA,EAAqB,QAAA,CAAS,MAAM,gBAAgB,WAAW,CAAA,sBAAA;AAAA,KACzE;AAAA,EACF;AAEA,EAAA,OAAO,EAAE,EAAA,EAAI,IAAA,EAAM,OAAA,EAAS,QAAA,EAAS;AACvC;;;AC5OO,IAAM,sBAAA,GAAyB;AAoGtC,eAAsB,iBAAA,CACpB,QAAA,EACA,OAAA,GAAiC,EAAC,EACG;AAErC,EAAA,MAAM,MAAA,GAAS,uBAAuB,QAAQ,CAAA;AAC9C,EAAA,IAAI,CAAC,OAAO,EAAA,EAAI;AACd,IAAA,OAAO;AAAA,MACL,KAAA,EAAO,KAAA;AAAA,MACP,MAAA,EAAQ,CAAA,gBAAA,EAAmB,MAAA,CAAO,MAAM,CAAA,CAAA;AAAA,MACxC,SAAS,EAAE,WAAA,EAAa,OAAO,WAAA,EAAa,KAAA,EAAO,oBAAoB,KAAA;AAAM,KAC/E;AAAA,EACF;AAEA,EAAA,MAAM,GAAA,GAAM,QAAA;AACZ,EAAA,MAAM,UAAU,MAAA,CAAO,OAAA;AAGvB,EAAA,MAAM,OAAA,GAA+B;AAAA,IACnC,OAAA;AAAA,IACA,WAAA,EAAa,IAAA;AAAA,IACb,WAAA,EAAa,IAAA;AAAA;AAAA,IAEb,kBAAA,EAAoB;AAAA,GACtB;AAEA,EAAA,IAAI,YAAY,cAAA,EAAgB;AAC9B,IAAA,OAAA,CAAQ,eAAe,GAAA,CAAI,YAAA;AAC3B,IAAA,OAAA,CAAQ,yBAAyB,GAAA,CAAI,sBAAA;AACrC,IAAA,OAAA,CAAQ,WAAW,GAAA,CAAI,QAAA;AACvB,IAAA,OAAA,CAAQ,cAAc,GAAA,CAAI,WAAA;AAG1B,IAAA,MAAM,KAAA,GAAQ,QAAQ,YAAA,IAAgB,sBAAA;AACtC,IAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,YAAA,IAAgB,IAAA,CAAK,GAAA,EAAI;AAC/C,IAAA,MAAM,KAAK,OAAA,CAAQ,WAAA;AACnB,IAAA,IAAI,KAAK,KAAA,EAAO;AACd,MAAA,OAAA,CAAQ,kBAAA,GAAqB,KAAA;AAC7B,MAAA,OAAO;AAAA,QACL,KAAA,EAAO,KAAA;AAAA,QACP,MAAA,EAAQ,CAAA,sCAAA,EAAyC,EAAE,CAAA,OAAA,EAAU,KAAK,CAAA,CAAA;AAAA,QAClE;AAAA,OACF;AAAA,IACF;AACA,IAAA,IAAI,KAAK,KAAA,EAAO;AACd,MAAA,OAAA,CAAQ,kBAAA,GAAqB,KAAA;AAC7B,MAAA,OAAO;AAAA,QACL,KAAA,EAAO,KAAA;AAAA,QACP,MAAA,EAAQ,CAAA,iCAAA,EAAoC,EAAE,CAAA,OAAA,EAAU,KAAK,CAAA,CAAA;AAAA,QAC7D;AAAA,OACF;AAAA,IACF;AAGA,IAAA,IAAI,OAAA,CAAQ,kBAAkB,MAAA,EAAW;AACvC,MAAA,IAAI,OAAA,CAAQ,QAAA,KAAa,OAAA,CAAQ,aAAA,EAAe;AAC9C,QAAA,OAAO;AAAA,UACL,KAAA,EAAO,KAAA;AAAA,UACP,QAAQ,CAAA,0BAAA,EAA6B,OAAA,CAAQ,aAAa,CAAA,UAAA,EAAa,QAAQ,QAAQ,CAAA,CAAA,CAAA;AAAA,UACvF;AAAA,SACF;AAAA,MACF;AAAA,IACF;AACA,IAAA,IAAI,OAAA,CAAQ,mCAAmC,MAAA,EAAW;AACxD,MAAA,MAAM,CAAA,GAAA,CAAK,OAAA,CAAQ,sBAAA,IAA0B,EAAA,EAAI,WAAA,EAAY;AAC7D,MAAA,MAAM,CAAA,GAAI,OAAA,CAAQ,8BAAA,CAA+B,WAAA,EAAY;AAC7D,MAAA,IAAI,MAAM,CAAA,EAAG;AACX,QAAA,OAAO;AAAA,UACL,KAAA,EAAO,KAAA;AAAA,UACP,MAAA,EAAQ,CAAA,6CAAA,EAAgD,CAAC,CAAA,UAAA,EAAa,CAAC,CAAA,CAAA,CAAA;AAAA,UACvE;AAAA,SACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAMA,EAAA,OAAO,EAAE,KAAA,EAAO,IAAA,EAAM,OAAA,EAAS,OAAA,EAAQ;AACzC","file":"index.js","sourcesContent":["/**\n * Kyber Arc 11 — envelope schema validator (zero-dep, fail-soft).\n *\n * Pure structural validation for the two envelope versions defined by\n * `libs/security-core/src/encryption-pqc/envelope.types.ts`:\n *\n * - `kyber-aes-v1` — modern hybrid (ML-KEM + HKDF-SHA384 + AES-256-GCM)\n * - `aes-v0` — legacy AES-only (read-only 12-month compat window)\n *\n * Customer-facing — therefore intentionally re-declares the envelope shape\n * here (instead of importing from `security-core`) so the SDK bundle stays\n * decoupled from the platform-internal package tree. The schema literals\n * MUST stay in sync with `security-core/src/encryption-pqc/envelope.types.ts`;\n * any divergence is a bug.\n *\n * Validation is structure-only — no decryption, no key access, no\n * cryptographic verification. Customers use this to catch malformed\n * envelopes BEFORE handing them to a decrypt routine (so the platform's\n * format invariants are visible at the SDK layer).\n *\n * @see docs/superpowers/specs/2026-05-29-kyber-mlkem-production-readiness.md §5 Arc 11\n * @see docs/superpowers/specs/2026-05-29-kyber-mlkem-production-readiness.md §6.1\n */\n\n/**\n * Discriminator literals accepted by `validateEnvelopeSchema`.\n *\n * Keep this union in sync with\n * `libs/security-core/src/encryption-pqc/envelope.types.ts` (`AnyEnvelope`).\n * Any new envelope version added there MUST add a literal here AND a\n * matching branch in `validateEnvelopeSchema`.\n */\nexport type EnvelopeVersionLiteral = 'kyber-aes-v1' | 'aes-v0';\n\n/**\n * Structural validation result for one envelope.\n *\n * - `ok: true` → caller can trust every required field exists with the\n * correct type, base64 fields decode, and version-specific length\n * invariants hold.\n * - `ok: false` → first failure is reported; further failures are NOT\n * accumulated (early-exit keeps the verifier output single-cause).\n */\nexport type SchemaValidationResult =\n | { ok: true; version: EnvelopeVersionLiteral }\n | { ok: false; reason: string };\n\n/**\n * ML-KEM ciphertext byte-length invariants (NIST FIPS-203).\n * - ML-KEM-768 → 1088 bytes\n * - ML-KEM-1024 → 1568 bytes\n *\n * These are exact, not bounds — any deviation indicates a malformed\n * envelope (truncation, padding, wrong-algo emit). Verifier rejects\n * mismatches outright.\n */\nconst KEM_CT_LEN: Record<'ml-kem-768' | 'ml-kem-1024', number> = {\n 'ml-kem-768': 1088,\n 'ml-kem-1024': 1568,\n};\n\n/** AES-GCM standard nonce = 96 bits = 12 bytes. */\nconst AES_IV_LEN = 12;\n/** AES-GCM auth tag = 128 bits = 16 bytes. */\nconst AES_TAG_LEN = 16;\n/** Per-envelope HKDF salt = 16 bytes (matches `encryptHybrid` writer). */\nconst KDF_SALT_LEN = 16;\n\n/**\n * Decode a base64 string, returning `null` on any decode error (illegal\n * chars, length not multiple of 4, etc.) rather than throwing — the\n * verifier surface fail-softs every failure mode.\n *\n * Uses `Buffer.from(s, 'base64')` which is permissive (silently drops\n * illegal chars). The follow-up length check is the real gate.\n */\nfunction tryDecodeBase64(s: string): Uint8Array | null {\n if (typeof s !== 'string' || s.length === 0) return null;\n try {\n const buf = Buffer.from(s, 'base64');\n // Round-trip check: re-encode and compare. Catches malformed input\n // that `Buffer.from` silently accepted.\n if (buf.toString('base64').replace(/=+$/, '') !== s.replace(/=+$/, '')) {\n return null;\n }\n return new Uint8Array(buf);\n } catch {\n return null;\n }\n}\n\nfunction isString(v: unknown): v is string {\n return typeof v === 'string';\n}\n\nfunction isNonEmptyString(v: unknown): v is string {\n return typeof v === 'string' && v.length > 0;\n}\n\nfunction isFiniteNumber(v: unknown): v is number {\n return typeof v === 'number' && Number.isFinite(v);\n}\n\nfunction isPlainObject(v: unknown): v is Record<string, unknown> {\n return typeof v === 'object' && v !== null && !Array.isArray(v);\n}\n\n/**\n * Validate the structural shape of an envelope.\n *\n * Branches on `version` discriminator and checks the per-version contract.\n * Returns `{ ok: false, reason }` on the first failure — single-cause\n * diagnostics keep verifier output actionable.\n *\n * Side-effect-free, throws never.\n */\nexport function validateEnvelopeSchema(envelope: unknown): SchemaValidationResult {\n if (!isPlainObject(envelope)) {\n return { ok: false, reason: 'envelope must be a JSON object' };\n }\n\n const version = (envelope as { version?: unknown }).version;\n if (version === 'kyber-aes-v1') {\n return validateKyberAesV1(envelope);\n }\n if (version === 'aes-v0') {\n return validateAesV0(envelope);\n }\n return {\n ok: false,\n reason: `unknown envelope version: ${JSON.stringify(version)}`,\n };\n}\n\nfunction validateKyberAesV1(env: Record<string, unknown>): SchemaValidationResult {\n // kemAlgorithm\n const kemAlgorithm = env.kemAlgorithm;\n if (kemAlgorithm !== 'ml-kem-768' && kemAlgorithm !== 'ml-kem-1024') {\n return {\n ok: false,\n reason: `kemAlgorithm must be 'ml-kem-768' or 'ml-kem-1024'`,\n };\n }\n\n // kemCiphertext (base64) — exact length per FIPS-203.\n if (!isNonEmptyString(env.kemCiphertext)) {\n return { ok: false, reason: 'kemCiphertext must be a non-empty base64 string' };\n }\n const kemCtBytes = tryDecodeBase64(env.kemCiphertext);\n if (!kemCtBytes) {\n return { ok: false, reason: 'kemCiphertext is not valid base64' };\n }\n const expectedKemLen = KEM_CT_LEN[kemAlgorithm];\n if (kemCtBytes.length !== expectedKemLen) {\n return {\n ok: false,\n reason: `kemCiphertext length ${kemCtBytes.length} != expected ${expectedKemLen} for ${kemAlgorithm}`,\n };\n }\n\n // recipientPkFingerprint — hex string, 32 chars (16-byte sha384 prefix).\n if (!isString(env.recipientPkFingerprint) || !/^[0-9a-f]{32}$/i.test(env.recipientPkFingerprint)) {\n return {\n ok: false,\n reason: 'recipientPkFingerprint must be a 32-char hex string (16-byte sha384 prefix)',\n };\n }\n\n // aesAlgorithm (pinned)\n if (env.aesAlgorithm !== 'aes-256-gcm') {\n return { ok: false, reason: `aesAlgorithm must be 'aes-256-gcm'` };\n }\n\n // aesIv (base64, 12 bytes)\n if (!isNonEmptyString(env.aesIv)) {\n return { ok: false, reason: 'aesIv must be a non-empty base64 string' };\n }\n const ivBytes = tryDecodeBase64(env.aesIv);\n if (!ivBytes) return { ok: false, reason: 'aesIv is not valid base64' };\n if (ivBytes.length !== AES_IV_LEN) {\n return {\n ok: false,\n reason: `aesIv length ${ivBytes.length} != expected ${AES_IV_LEN} (AES-GCM 96-bit nonce)`,\n };\n }\n\n // aesCiphertext (base64) — any length, including 0 (empty plaintext is valid).\n if (typeof env.aesCiphertext !== 'string') {\n return { ok: false, reason: 'aesCiphertext must be a base64 string' };\n }\n // Permit empty string explicitly (0-byte ciphertext from empty plaintext).\n if (env.aesCiphertext.length > 0) {\n const ctBytes = tryDecodeBase64(env.aesCiphertext);\n if (!ctBytes) return { ok: false, reason: 'aesCiphertext is not valid base64' };\n }\n\n // aesAuthTag (base64, 16 bytes)\n if (!isNonEmptyString(env.aesAuthTag)) {\n return { ok: false, reason: 'aesAuthTag must be a non-empty base64 string' };\n }\n const tagBytes = tryDecodeBase64(env.aesAuthTag);\n if (!tagBytes) return { ok: false, reason: 'aesAuthTag is not valid base64' };\n if (tagBytes.length !== AES_TAG_LEN) {\n return {\n ok: false,\n reason: `aesAuthTag length ${tagBytes.length} != expected ${AES_TAG_LEN} (AES-GCM 128-bit tag)`,\n };\n }\n\n // kdfAlgorithm (pinned)\n if (env.kdfAlgorithm !== 'hkdf-sha384') {\n return { ok: false, reason: `kdfAlgorithm must be 'hkdf-sha384'` };\n }\n\n // kdfSalt (base64, 16 bytes)\n if (!isNonEmptyString(env.kdfSalt)) {\n return { ok: false, reason: 'kdfSalt must be a non-empty base64 string' };\n }\n const saltBytes = tryDecodeBase64(env.kdfSalt);\n if (!saltBytes) return { ok: false, reason: 'kdfSalt is not valid base64' };\n if (saltBytes.length !== KDF_SALT_LEN) {\n return {\n ok: false,\n reason: `kdfSalt length ${saltBytes.length} != expected ${KDF_SALT_LEN}`,\n };\n }\n\n // kdfLabel — non-empty, reasonable upper bound (256 chars) to catch\n // pathological inputs without imposing a real schema constraint.\n if (!isNonEmptyString(env.kdfLabel)) {\n return { ok: false, reason: 'kdfLabel must be a non-empty string' };\n }\n if (env.kdfLabel.length > 256) {\n return {\n ok: false,\n reason: `kdfLabel length ${env.kdfLabel.length} exceeds 256-char limit`,\n };\n }\n\n // encryptedAt — finite number (millis epoch).\n if (!isFiniteNumber(env.encryptedAt)) {\n return { ok: false, reason: 'encryptedAt must be a finite number (millis epoch)' };\n }\n\n return { ok: true, version: 'kyber-aes-v1' };\n}\n\nfunction validateAesV0(env: Record<string, unknown>): SchemaValidationResult {\n if (!isNonEmptyString(env.aesIv)) {\n return { ok: false, reason: 'aesIv must be a non-empty base64 string' };\n }\n const ivBytes = tryDecodeBase64(env.aesIv);\n if (!ivBytes) return { ok: false, reason: 'aesIv is not valid base64' };\n if (ivBytes.length !== AES_IV_LEN) {\n return {\n ok: false,\n reason: `aesIv length ${ivBytes.length} != expected ${AES_IV_LEN} (AES-GCM 96-bit nonce)`,\n };\n }\n\n if (typeof env.aesCiphertext !== 'string') {\n return { ok: false, reason: 'aesCiphertext must be a base64 string' };\n }\n if (env.aesCiphertext.length > 0) {\n const ctBytes = tryDecodeBase64(env.aesCiphertext);\n if (!ctBytes) return { ok: false, reason: 'aesCiphertext is not valid base64' };\n }\n\n if (!isNonEmptyString(env.aesAuthTag)) {\n return { ok: false, reason: 'aesAuthTag must be a non-empty base64 string' };\n }\n const tagBytes = tryDecodeBase64(env.aesAuthTag);\n if (!tagBytes) return { ok: false, reason: 'aesAuthTag is not valid base64' };\n if (tagBytes.length !== AES_TAG_LEN) {\n return {\n ok: false,\n reason: `aesAuthTag length ${tagBytes.length} != expected ${AES_TAG_LEN} (AES-GCM 128-bit tag)`,\n };\n }\n\n return { ok: true, version: 'aes-v0' };\n}\n","/**\n * Kyber Arc 11 — `verifyPqcEnvelope` external verifier primitive.\n *\n * Observation-only customer surface for the Kyber/ML-KEM hybrid envelopes\n * the platform emits (Arcs 2-10 write paths). Does NOT decrypt — verifier\n * holds no key material. Checks structural well-formedness + claim\n * plausibility so customers can detect malformed envelopes BEFORE handing\n * them to a decryption routine.\n *\n * Three classes of checks:\n *\n * 1. Schema (`envelope-schema-validator.ts`) — discriminator literal,\n * required fields, types, base64 decodability, exact-length field\n * invariants per FIPS-203 (ML-KEM ciphertext) + AES-GCM (iv/tag).\n *\n * 2. Timestamp plausibility — `encryptedAt` must fall between FIPS-203\n * standardization (2024-01-01, before which no hybrid envelope could\n * have been validly emitted) and `Date.now()` (future timestamps\n * indicate clock skew / forged envelope).\n *\n * 3. Optional caller claims — when supplied, `expectedLabel` and\n * `expectedRecipientPkFingerprint` are byte-equality checked against\n * the envelope fields. Useful for verifying an envelope was emitted\n * for the surface the caller expects.\n *\n * Fail-soft at every step — returns a discriminated `EnvelopeVerificationResult`\n * with `reason` on failure. Throws only on truly impossible inputs (e.g.\n * the caller passing a non-Promise to `await`).\n *\n * @see docs/superpowers/specs/2026-05-29-kyber-mlkem-production-readiness.md §5 Arc 11\n * @see libs/security-core/src/encryption-pqc/envelope.types.ts (KyberAesEnvelopeV1, AesEnvelopeV0Legacy)\n * @see libs/security-core/src/encryption-pqc/encrypt-hybrid.ts (the producer this verifier observes)\n */\nimport {\n validateEnvelopeSchema,\n type EnvelopeVersionLiteral,\n} from './envelope-schema-validator';\n\n/**\n * FIPS-203 standardization date — millis since epoch for 2024-01-01 00:00 UTC.\n *\n * No production hybrid envelope can legitimately predate this. Customer-facing\n * floor for `encryptedAt` plausibility check; overridable via\n * `VerifyEnvelopeOptions.minTimestamp` for test/staging fixtures.\n */\nexport const KYBER_MIN_TIMESTAMP_MS = 1_704_067_200_000;\n\n/**\n * Detailed verification metadata exposed to callers regardless of valid/invalid\n * outcome. Partial on failure (whichever fields were inspectable before the\n * first failure).\n */\nexport type VerificationDetails = {\n /** Envelope version literal (`kyber-aes-v1` or `aes-v0`). */\n version: string;\n /** ML-KEM parameter set, only present for `kyber-aes-v1`. */\n kemAlgorithm?: 'ml-kem-768' | 'ml-kem-1024';\n /** 32-char hex sha384-prefix of the recipient pk; only present for hybrid. */\n recipientPkFingerprint?: string;\n /** Per-surface HKDF label as recorded by the writer. */\n kdfLabel?: string;\n /** Writer wall-clock at emit time (millis epoch). */\n encryptedAt?: number;\n /** True iff all schema-shape checks passed. */\n schemaValid: boolean;\n /** True iff every base64 field decoded successfully. (Subsumed by `schemaValid`.) */\n base64Valid: boolean;\n /** True iff `encryptedAt` fell within `[minTimestamp, maxTimestamp]`. */\n timestampPlausible: boolean;\n};\n\n/**\n * Discriminated verification outcome. `valid: true` indicates the envelope is\n * STRUCTURALLY well-formed for its declared version AND every caller-supplied\n * claim matched. It does NOT indicate the platform actually emitted the\n * envelope (no signature here) nor that the ciphertext will decrypt (no key\n * here) — those gates belong to PQC-Sig's `verifyPqcAttestation` (signature\n * provenance) and the platform's `decryptHybrid` / `decryptAny` (ciphertext\n * authenticity).\n */\nexport type EnvelopeVerificationResult =\n | {\n valid: true;\n version: EnvelopeVersionLiteral;\n details: VerificationDetails;\n }\n | {\n valid: false;\n reason: string;\n details?: Partial<VerificationDetails>;\n };\n\n/**\n * Caller-facing knobs for `verifyPqcEnvelope`.\n *\n * @field expectedLabel — when set, the envelope's `kdfLabel` MUST equal this\n * value (byte-equality). Surface-mismatch defense — catches envelopes\n * addressed to a different consumer being replayed at this consumer.\n * Only meaningful for `kyber-aes-v1` envelopes (legacy `aes-v0` has no label).\n *\n * @field expectedRecipientPkFingerprint — when set, the envelope's\n * `recipientPkFingerprint` MUST equal this value (case-insensitive hex\n * compare). Useful for asserting the envelope was emitted for a specific\n * cluster Kyber keypair. Only meaningful for `kyber-aes-v1`.\n *\n * @field minTimestamp — lower bound (inclusive, millis epoch) for\n * `encryptedAt`. Default: `KYBER_MIN_TIMESTAMP_MS` (2024-01-01).\n *\n * @field maxTimestamp — upper bound (inclusive, millis epoch) for\n * `encryptedAt`. Default: `Date.now()` evaluated at verify time.\n * Pass a fixed future-skew tolerance (e.g. `Date.now() + 60_000`) if\n * verifying envelopes from a known-skewed source.\n */\nexport type VerifyEnvelopeOptions = {\n expectedLabel?: string;\n expectedRecipientPkFingerprint?: string;\n minTimestamp?: number;\n maxTimestamp?: number;\n};\n\n/**\n * Verify a Kyber/ML-KEM hybrid envelope (or legacy AES-v0 envelope) for\n * structural well-formedness + claim consistency.\n *\n * Pure function — no I/O, no key material, no decryption. Customer code\n * calls this BEFORE passing the envelope to a decryption surface; if it\n * returns `{ valid: false }`, the envelope is malformed and decryption\n * would fail anyway (often with a less-actionable error).\n *\n * Async signature for forward-compatibility with potential network checks\n * (e.g. fetch recipient-pk registry to resolve the fingerprint to a known\n * cluster). The Arc 11 baseline does NOT make any network calls.\n *\n * @example\n * ```ts\n * import { verifyPqcEnvelope } from '@hsuite/smart-engines-sdk/pqc-verify-envelope';\n *\n * const result = await verifyPqcEnvelope(envelopeJson, {\n * expectedLabel: 'smart-app-secret-envelope-v1',\n * });\n * if (!result.valid) {\n * throw new Error(`Bad envelope: ${result.reason}`);\n * }\n * ```\n */\nexport async function verifyPqcEnvelope(\n envelope: unknown,\n options: VerifyEnvelopeOptions = {},\n): Promise<EnvelopeVerificationResult> {\n // ── Step 1: Schema validation ─────────────────────────────────────────────\n const schema = validateEnvelopeSchema(envelope);\n if (!schema.ok) {\n return {\n valid: false,\n reason: `schema-invalid: ${schema.reason}`,\n details: { schemaValid: false, base64Valid: false, timestampPlausible: false },\n };\n }\n\n const env = envelope as Record<string, unknown>;\n const version = schema.version;\n\n // ── Step 2: Build inspectable details ─────────────────────────────────────\n const details: VerificationDetails = {\n version,\n schemaValid: true,\n base64Valid: true,\n // computed below for kyber-aes-v1; legacy has no encryptedAt so treat as plausible.\n timestampPlausible: true,\n };\n\n if (version === 'kyber-aes-v1') {\n details.kemAlgorithm = env.kemAlgorithm as 'ml-kem-768' | 'ml-kem-1024';\n details.recipientPkFingerprint = env.recipientPkFingerprint as string;\n details.kdfLabel = env.kdfLabel as string;\n details.encryptedAt = env.encryptedAt as number;\n\n // ── Step 3: Timestamp plausibility ─────────────────────────────────────\n const minTs = options.minTimestamp ?? KYBER_MIN_TIMESTAMP_MS;\n const maxTs = options.maxTimestamp ?? Date.now();\n const ts = details.encryptedAt as number;\n if (ts < minTs) {\n details.timestampPlausible = false;\n return {\n valid: false,\n reason: `timestamp-before-minimum: encryptedAt=${ts} < min=${minTs}`,\n details,\n };\n }\n if (ts > maxTs) {\n details.timestampPlausible = false;\n return {\n valid: false,\n reason: `timestamp-in-future: encryptedAt=${ts} > max=${maxTs}`,\n details,\n };\n }\n\n // ── Step 4: Optional caller-claim checks ───────────────────────────────\n if (options.expectedLabel !== undefined) {\n if (details.kdfLabel !== options.expectedLabel) {\n return {\n valid: false,\n reason: `label-mismatch: expected='${options.expectedLabel}' actual='${details.kdfLabel}'`,\n details,\n };\n }\n }\n if (options.expectedRecipientPkFingerprint !== undefined) {\n const a = (details.recipientPkFingerprint ?? '').toLowerCase();\n const b = options.expectedRecipientPkFingerprint.toLowerCase();\n if (a !== b) {\n return {\n valid: false,\n reason: `recipient-pk-fingerprint-mismatch: expected='${b}' actual='${a}'`,\n details,\n };\n }\n }\n }\n // aes-v0: no extra checks beyond schema. Legacy envelopes are read-only\n // through the 12-month Arc 12 compat window; verifier accepts them as\n // structurally valid but offers no claim checks (label/fingerprint don't\n // exist on the legacy shape).\n\n return { valid: true, version, details };\n}\n"]}
|