@byok-sdk/core 0.28.2 → 0.29.0
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/README.md +11 -0
- package/dist/sealed-payload/index.d.ts +15 -0
- package/dist/sealed-payload/index.js +1048 -0
- package/dist/sealed-payload/index.js.map +1 -0
- package/dist/sealed-payload/member-chain.d.ts +58 -0
- package/dist/sealed-payload/primitives.d.ts +53 -0
- package/dist/sealed-payload/scope-key.d.ts +91 -0
- package/dist/sealed-payload/sealed-content.d.ts +185 -0
- package/package.json +5 -1
|
@@ -0,0 +1,1048 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { ByokCoreError } from '@byok-sdk/core';
|
|
3
|
+
|
|
4
|
+
// src/sealed-payload/sealed-content.ts
|
|
5
|
+
var CONTENT_HASH_PATTERN = /^sha256:[0-9a-f]{64}$/;
|
|
6
|
+
var DEVICE_PROOF_SCHEMA_ID = "byok-device-proof-v1";
|
|
7
|
+
var DEVICE_PROOF_VERSION = 1;
|
|
8
|
+
var DEVICE_PROOF_ALGORITHMS = ["ed25519"];
|
|
9
|
+
var BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
10
|
+
var HTTP_METHOD_PATTERN = /^[A-Z]+$/;
|
|
11
|
+
function canonicalizationFailure(path, reason) {
|
|
12
|
+
return new ByokCoreError(
|
|
13
|
+
"proof_canonicalization_failed",
|
|
14
|
+
`Cannot canonicalize ${path}: ${reason}`
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
function isPlainObject(value) {
|
|
18
|
+
const prototype = Object.getPrototypeOf(value);
|
|
19
|
+
return prototype === Object.prototype || prototype === null;
|
|
20
|
+
}
|
|
21
|
+
function canonicalizeValue(value, path, seen) {
|
|
22
|
+
if (value === null) return "null";
|
|
23
|
+
switch (typeof value) {
|
|
24
|
+
case "boolean":
|
|
25
|
+
return value ? "true" : "false";
|
|
26
|
+
case "string":
|
|
27
|
+
return JSON.stringify(value);
|
|
28
|
+
case "number": {
|
|
29
|
+
if (!Number.isSafeInteger(value)) {
|
|
30
|
+
throw canonicalizationFailure(
|
|
31
|
+
path,
|
|
32
|
+
`only safe integers are canonicalizable, received ${String(value)}`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
return String(value === 0 ? 0 : value);
|
|
36
|
+
}
|
|
37
|
+
case "undefined":
|
|
38
|
+
throw canonicalizationFailure(path, "undefined has no canonical form");
|
|
39
|
+
case "bigint":
|
|
40
|
+
throw canonicalizationFailure(path, "bigint is not a JSON type");
|
|
41
|
+
case "function":
|
|
42
|
+
case "symbol":
|
|
43
|
+
throw canonicalizationFailure(path, `${typeof value} is not a JSON type`);
|
|
44
|
+
case "object":
|
|
45
|
+
break;
|
|
46
|
+
default:
|
|
47
|
+
throw canonicalizationFailure(path, `unsupported type ${typeof value}`);
|
|
48
|
+
}
|
|
49
|
+
const objectValue = value;
|
|
50
|
+
if (seen.has(objectValue)) {
|
|
51
|
+
throw canonicalizationFailure(path, "circular reference");
|
|
52
|
+
}
|
|
53
|
+
seen.add(objectValue);
|
|
54
|
+
try {
|
|
55
|
+
if (Array.isArray(objectValue)) {
|
|
56
|
+
const parts2 = objectValue.map(
|
|
57
|
+
(entry, index) => canonicalizeValue(entry, `${path}[${index}]`, seen)
|
|
58
|
+
);
|
|
59
|
+
return `[${parts2.join(",")}]`;
|
|
60
|
+
}
|
|
61
|
+
if (!isPlainObject(objectValue)) {
|
|
62
|
+
throw canonicalizationFailure(
|
|
63
|
+
path,
|
|
64
|
+
"only plain objects and arrays are canonicalizable"
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const keys = Object.keys(objectValue).sort();
|
|
68
|
+
const record = objectValue;
|
|
69
|
+
const parts = keys.map((key) => {
|
|
70
|
+
const encodedKey = JSON.stringify(key);
|
|
71
|
+
const encodedValue = canonicalizeValue(record[key], `${path}.${key}`, seen);
|
|
72
|
+
return `${encodedKey}:${encodedValue}`;
|
|
73
|
+
});
|
|
74
|
+
return `{${parts.join(",")}}`;
|
|
75
|
+
} finally {
|
|
76
|
+
seen.delete(objectValue);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function canonicalizeJson(value) {
|
|
80
|
+
return canonicalizeValue(value, "<root>", /* @__PURE__ */ new Set());
|
|
81
|
+
}
|
|
82
|
+
function canonicalizeJsonBytes(value) {
|
|
83
|
+
return new TextEncoder().encode(canonicalizeJson(value));
|
|
84
|
+
}
|
|
85
|
+
var DeviceProofProtectedClaimsSchema = z.strictObject({
|
|
86
|
+
version: z.literal(DEVICE_PROOF_VERSION),
|
|
87
|
+
tenantId: z.string().min(1),
|
|
88
|
+
productId: z.string().min(1),
|
|
89
|
+
deviceId: z.string().min(1),
|
|
90
|
+
/** Identity of the signing key, resolved against the device row at verify time. */
|
|
91
|
+
keyId: z.string().min(1),
|
|
92
|
+
/** Rotation generation. An older epoch is rejected by the verifier, not here. */
|
|
93
|
+
keyEpoch: z.number().int().nonnegative(),
|
|
94
|
+
/** Idempotency key: exact replay returns the original result, reuse with a different body is a conflict. */
|
|
95
|
+
requestId: z.string().min(1),
|
|
96
|
+
/** Logical operation, e.g. `truth.write`. */
|
|
97
|
+
operation: z.string().min(1),
|
|
98
|
+
/** Tenant-scoped resource the operation targets. */
|
|
99
|
+
resource: z.string().min(1),
|
|
100
|
+
method: z.string().regex(HTTP_METHOD_PATTERN).optional(),
|
|
101
|
+
path: z.string().startsWith("/").optional(),
|
|
102
|
+
operationId: z.string().min(1).optional(),
|
|
103
|
+
bodySha256: z.string().regex(CONTENT_HASH_PATTERN),
|
|
104
|
+
/** Bound alongside the hash so a truncation attack cannot hide behind a hash collision claim. */
|
|
105
|
+
bodySize: z.number().int().nonnegative(),
|
|
106
|
+
issuedAt: z.iso.datetime(),
|
|
107
|
+
expiresAt: z.iso.datetime().optional(),
|
|
108
|
+
nonce: z.string().min(1).optional()
|
|
109
|
+
}).superRefine((claims, ctx) => {
|
|
110
|
+
const hasRequestLine = claims.method !== void 0 && claims.path !== void 0;
|
|
111
|
+
const hasOperationId = claims.operationId !== void 0;
|
|
112
|
+
if (hasRequestLine === hasOperationId) {
|
|
113
|
+
ctx.addIssue({
|
|
114
|
+
code: "custom",
|
|
115
|
+
message: "Claims must carry exactly one request binding: both `method` and `path`, or `operationId`."
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
if (!hasRequestLine && (claims.method !== void 0 || claims.path !== void 0)) {
|
|
119
|
+
ctx.addIssue({
|
|
120
|
+
code: "custom",
|
|
121
|
+
message: "`method` and `path` must be supplied together."
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
z.strictObject({
|
|
126
|
+
schema: z.literal(DEVICE_PROOF_SCHEMA_ID),
|
|
127
|
+
algorithm: z.enum(DEVICE_PROOF_ALGORITHMS),
|
|
128
|
+
protected: DeviceProofProtectedClaimsSchema,
|
|
129
|
+
/** base64url, unpadded. */
|
|
130
|
+
signature: z.string().regex(BASE64URL_PATTERN)
|
|
131
|
+
});
|
|
132
|
+
var CANONICAL_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
133
|
+
function isCanonicalTimestamp(value) {
|
|
134
|
+
if (typeof value !== "string") return false;
|
|
135
|
+
if (!CANONICAL_TIMESTAMP_PATTERN.test(value)) return false;
|
|
136
|
+
const parsed = Date.parse(value);
|
|
137
|
+
if (!Number.isFinite(parsed)) return false;
|
|
138
|
+
return new Date(parsed).toISOString() === value;
|
|
139
|
+
}
|
|
140
|
+
function webCryptoSubtle() {
|
|
141
|
+
const subtle = globalThis.crypto?.subtle;
|
|
142
|
+
if (subtle === void 0) {
|
|
143
|
+
throw new ByokCoreError("webcrypto_unavailable", "WebCrypto subtle is unavailable in this runtime");
|
|
144
|
+
}
|
|
145
|
+
return subtle;
|
|
146
|
+
}
|
|
147
|
+
var KEM_ID = 16;
|
|
148
|
+
var KDF_ID = 1;
|
|
149
|
+
var AEAD_ID = 1;
|
|
150
|
+
var HPKE_P256_PUBLIC_KEY_BYTES = 65;
|
|
151
|
+
var N_SECRET = 32;
|
|
152
|
+
var N_K = 16;
|
|
153
|
+
var N_N = 12;
|
|
154
|
+
var N_H = 32;
|
|
155
|
+
var HPKE_AEAD_TAG_BYTES = 16;
|
|
156
|
+
var MODE_BASE = 0;
|
|
157
|
+
var HPKE_V1 = utf8("HPKE-v1");
|
|
158
|
+
var KEM_SUITE_ID = concat(utf8("KEM"), i2osp(KEM_ID, 2));
|
|
159
|
+
var HPKE_SUITE_ID = concat(utf8("HPKE"), i2osp(KEM_ID, 2), i2osp(KDF_ID, 2), i2osp(AEAD_ID, 2));
|
|
160
|
+
var EMPTY = new Uint8Array(0);
|
|
161
|
+
var ZERO_SALT = new Uint8Array(N_H);
|
|
162
|
+
function utf8(value) {
|
|
163
|
+
return new TextEncoder().encode(value);
|
|
164
|
+
}
|
|
165
|
+
function concat(...parts) {
|
|
166
|
+
const length = parts.reduce((total, part) => total + part.byteLength, 0);
|
|
167
|
+
const out = new Uint8Array(length);
|
|
168
|
+
let offset = 0;
|
|
169
|
+
for (const part of parts) {
|
|
170
|
+
out.set(part, offset);
|
|
171
|
+
offset += part.byteLength;
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
function i2osp(value, length) {
|
|
176
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
177
|
+
throw new ByokCoreError("sealed_secret_invalid", "HPKE integer encoding requires a non-negative safe integer");
|
|
178
|
+
}
|
|
179
|
+
const out = new Uint8Array(length);
|
|
180
|
+
let remaining = value;
|
|
181
|
+
for (let index = length - 1; index >= 0; index -= 1) {
|
|
182
|
+
out[index] = remaining % 256;
|
|
183
|
+
remaining = Math.floor(remaining / 256);
|
|
184
|
+
}
|
|
185
|
+
if (remaining !== 0) {
|
|
186
|
+
throw new ByokCoreError("sealed_secret_invalid", "HPKE integer does not fit the requested width");
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
function copy(bytes) {
|
|
191
|
+
return new Uint8Array(bytes);
|
|
192
|
+
}
|
|
193
|
+
async function hmacSha256(key, data) {
|
|
194
|
+
const subtle = webCryptoSubtle();
|
|
195
|
+
const hmacKey = await subtle.importKey("raw", copy(key), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
196
|
+
return new Uint8Array(await subtle.sign("HMAC", hmacKey, copy(data)));
|
|
197
|
+
}
|
|
198
|
+
async function hkdfExpand(prk, info, length) {
|
|
199
|
+
if (length > 255 * N_H) {
|
|
200
|
+
throw new ByokCoreError("sealed_secret_invalid", "HKDF output length exceeds its limit");
|
|
201
|
+
}
|
|
202
|
+
const out = new Uint8Array(length);
|
|
203
|
+
let previous = EMPTY;
|
|
204
|
+
let offset = 0;
|
|
205
|
+
for (let counter = 1; offset < length; counter += 1) {
|
|
206
|
+
previous = await hmacSha256(prk, concat(previous, info, Uint8Array.of(counter)));
|
|
207
|
+
const take = Math.min(previous.byteLength, length - offset);
|
|
208
|
+
out.set(previous.subarray(0, take), offset);
|
|
209
|
+
offset += take;
|
|
210
|
+
}
|
|
211
|
+
return out;
|
|
212
|
+
}
|
|
213
|
+
async function labeledExtract(suiteId, salt, label, ikm) {
|
|
214
|
+
return hmacSha256(salt.byteLength === 0 ? ZERO_SALT : salt, concat(HPKE_V1, suiteId, utf8(label), ikm));
|
|
215
|
+
}
|
|
216
|
+
async function labeledExpand(suiteId, prk, label, info, length) {
|
|
217
|
+
return hkdfExpand(prk, concat(i2osp(length, 2), HPKE_V1, suiteId, utf8(label), info), length);
|
|
218
|
+
}
|
|
219
|
+
async function importP256PublicKey(raw) {
|
|
220
|
+
if (raw.byteLength !== HPKE_P256_PUBLIC_KEY_BYTES || raw[0] !== 4) {
|
|
221
|
+
throw new ByokCoreError("sealed_secret_invalid", "P-256 public key must be a 65-byte uncompressed point");
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
return await webCryptoSubtle().importKey("raw", copy(raw), { name: "ECDH", namedCurve: "P-256" }, true, []);
|
|
225
|
+
} catch (cause) {
|
|
226
|
+
throw new ByokCoreError("sealed_secret_invalid", "P-256 public key is not a valid curve point", { cause });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async function ecdhP256(privateKey, publicKey) {
|
|
230
|
+
const bits = await webCryptoSubtle().deriveBits({ name: "ECDH", public: publicKey }, privateKey, N_SECRET * 8);
|
|
231
|
+
return new Uint8Array(bits);
|
|
232
|
+
}
|
|
233
|
+
async function extractAndExpand(dh, kemContext) {
|
|
234
|
+
const eaePrk = await labeledExtract(KEM_SUITE_ID, EMPTY, "eae_prk", dh);
|
|
235
|
+
return labeledExpand(KEM_SUITE_ID, eaePrk, "shared_secret", kemContext, N_SECRET);
|
|
236
|
+
}
|
|
237
|
+
async function keyScheduleBase(sharedSecret, info) {
|
|
238
|
+
const pskIdHash = await labeledExtract(HPKE_SUITE_ID, EMPTY, "psk_id_hash", EMPTY);
|
|
239
|
+
const infoHash = await labeledExtract(HPKE_SUITE_ID, EMPTY, "info_hash", info);
|
|
240
|
+
const context = concat(Uint8Array.of(MODE_BASE), pskIdHash, infoHash);
|
|
241
|
+
const secret = await labeledExtract(HPKE_SUITE_ID, sharedSecret, "secret", EMPTY);
|
|
242
|
+
return {
|
|
243
|
+
sharedSecret,
|
|
244
|
+
key: await labeledExpand(HPKE_SUITE_ID, secret, "key", context, N_K),
|
|
245
|
+
baseNonce: await labeledExpand(HPKE_SUITE_ID, secret, "base_nonce", context, N_N),
|
|
246
|
+
exporterSecret: await labeledExpand(HPKE_SUITE_ID, secret, "exp", context, N_H)
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
async function setupBaseSender(recipientPublicKey, info, ephemeral) {
|
|
250
|
+
const pkR = await importP256PublicKey(recipientPublicKey);
|
|
251
|
+
let pair = ephemeral;
|
|
252
|
+
if (pair === void 0) {
|
|
253
|
+
const subtle = webCryptoSubtle();
|
|
254
|
+
const generated = await subtle.generateKey({ name: "ECDH", namedCurve: "P-256" }, true, ["deriveBits"]);
|
|
255
|
+
pair = {
|
|
256
|
+
privateKey: generated.privateKey,
|
|
257
|
+
publicKey: new Uint8Array(await subtle.exportKey("raw", generated.publicKey))
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
const enc = copy(pair.publicKey);
|
|
261
|
+
const dh = await ecdhP256(pair.privateKey, pkR);
|
|
262
|
+
const sharedSecret = await extractAndExpand(dh, concat(enc, recipientPublicKey));
|
|
263
|
+
return { enc, schedule: await keyScheduleBase(sharedSecret, info) };
|
|
264
|
+
}
|
|
265
|
+
async function setupBaseRecipient(enc, recipientPrivateKey, recipientPublicKey, info) {
|
|
266
|
+
if (recipientPublicKey.byteLength !== HPKE_P256_PUBLIC_KEY_BYTES || recipientPublicKey[0] !== 4) {
|
|
267
|
+
throw new ByokCoreError("sealed_secret_invalid", "P-256 recipient public key must be a 65-byte uncompressed point");
|
|
268
|
+
}
|
|
269
|
+
const pkE = await importP256PublicKey(enc);
|
|
270
|
+
let dh;
|
|
271
|
+
try {
|
|
272
|
+
dh = await ecdhP256(recipientPrivateKey, pkE);
|
|
273
|
+
} catch (cause) {
|
|
274
|
+
throw new ByokCoreError("sealed_secret_open_failed", "HPKE decapsulation failed", { cause });
|
|
275
|
+
}
|
|
276
|
+
const sharedSecret = await extractAndExpand(dh, concat(enc, recipientPublicKey));
|
|
277
|
+
return keyScheduleBase(sharedSecret, info);
|
|
278
|
+
}
|
|
279
|
+
function hpkeNonce(baseNonce, sequence) {
|
|
280
|
+
const seq = i2osp(sequence, N_N);
|
|
281
|
+
const out = new Uint8Array(N_N);
|
|
282
|
+
for (let index = 0; index < N_N; index += 1) out[index] = baseNonce[index] ^ seq[index];
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
async function aesGcmKey(key, usage) {
|
|
286
|
+
return webCryptoSubtle().importKey("raw", copy(key), { name: "AES-GCM" }, false, [usage]);
|
|
287
|
+
}
|
|
288
|
+
async function hpkeAeadSeal(schedule, sequence, aad, plaintext) {
|
|
289
|
+
const ciphertext = await webCryptoSubtle().encrypt(
|
|
290
|
+
{ name: "AES-GCM", iv: hpkeNonce(schedule.baseNonce, sequence), additionalData: copy(aad), tagLength: 128 },
|
|
291
|
+
await aesGcmKey(schedule.key, "encrypt"),
|
|
292
|
+
copy(plaintext)
|
|
293
|
+
);
|
|
294
|
+
return new Uint8Array(ciphertext);
|
|
295
|
+
}
|
|
296
|
+
async function hpkeAeadOpen(schedule, sequence, aad, ciphertext) {
|
|
297
|
+
if (ciphertext.byteLength < HPKE_AEAD_TAG_BYTES) {
|
|
298
|
+
throw new ByokCoreError("sealed_secret_invalid", "HPKE ciphertext is shorter than its authentication tag");
|
|
299
|
+
}
|
|
300
|
+
try {
|
|
301
|
+
const plaintext = await webCryptoSubtle().decrypt(
|
|
302
|
+
{ name: "AES-GCM", iv: hpkeNonce(schedule.baseNonce, sequence), additionalData: copy(aad), tagLength: 128 },
|
|
303
|
+
await aesGcmKey(schedule.key, "decrypt"),
|
|
304
|
+
copy(ciphertext)
|
|
305
|
+
);
|
|
306
|
+
return new Uint8Array(plaintext);
|
|
307
|
+
} catch (cause) {
|
|
308
|
+
throw new ByokCoreError("sealed_secret_open_failed", "HPKE ciphertext failed authentication", { cause });
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
async function hpkeSealBase(input) {
|
|
312
|
+
const { enc, schedule } = await setupBaseSender(input.recipientPublicKey, input.info);
|
|
313
|
+
return { enc, ciphertext: await hpkeAeadSeal(schedule, 0, input.aad, input.plaintext) };
|
|
314
|
+
}
|
|
315
|
+
async function hpkeOpenBase(input) {
|
|
316
|
+
const schedule = await setupBaseRecipient(input.enc, input.recipientPrivateKey, input.recipientPublicKey, input.info);
|
|
317
|
+
return hpkeAeadOpen(schedule, 0, input.aad, input.ciphertext);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/sealed-provider-secret.ts
|
|
321
|
+
var PROVIDER_SECRET_HPKE_SUITE = "hpke-base.dhkem-p256-hkdf-sha256.hkdf-sha256.aes-128-gcm";
|
|
322
|
+
var PROVIDER_PROVISIONING_REQUEST_VERSION = 1;
|
|
323
|
+
var PROVIDER_PROVISIONING_OPERATIONS = ["configure", "update_model", "replace_secret", "delete"];
|
|
324
|
+
var PROVIDER_PROVISIONING_SECRET_OPERATIONS = ["configure", "replace_secret"];
|
|
325
|
+
var PROVIDER_SECRET_MAX_PADDED_BYTES = 4096;
|
|
326
|
+
var SEALING_KEY_ID_LENGTH = 22;
|
|
327
|
+
var B64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
328
|
+
var B64URL_PATTERN = /^[A-Za-z0-9_-]*$/;
|
|
329
|
+
function encodeBase64Url(bytes) {
|
|
330
|
+
let out = "";
|
|
331
|
+
let index = 0;
|
|
332
|
+
for (; index + 3 <= bytes.length; index += 3) {
|
|
333
|
+
const value = bytes[index] << 16 | bytes[index + 1] << 8 | bytes[index + 2];
|
|
334
|
+
out += B64URL_ALPHABET[value >> 18 & 63] + B64URL_ALPHABET[value >> 12 & 63] + B64URL_ALPHABET[value >> 6 & 63] + B64URL_ALPHABET[value & 63];
|
|
335
|
+
}
|
|
336
|
+
const rest = bytes.length - index;
|
|
337
|
+
if (rest === 1) {
|
|
338
|
+
const value = bytes[index] << 16;
|
|
339
|
+
out += B64URL_ALPHABET[value >> 18 & 63] + B64URL_ALPHABET[value >> 12 & 63];
|
|
340
|
+
} else if (rest === 2) {
|
|
341
|
+
const value = bytes[index] << 16 | bytes[index + 1] << 8;
|
|
342
|
+
out += B64URL_ALPHABET[value >> 18 & 63] + B64URL_ALPHABET[value >> 12 & 63] + B64URL_ALPHABET[value >> 6 & 63];
|
|
343
|
+
}
|
|
344
|
+
return out;
|
|
345
|
+
}
|
|
346
|
+
function decodeBase64Url(value, field) {
|
|
347
|
+
if (!B64URL_PATTERN.test(value) || value.length % 4 === 1) {
|
|
348
|
+
throw new ByokCoreError("sealed_secret_invalid", `${field} is not canonical base64url`);
|
|
349
|
+
}
|
|
350
|
+
const out = new Uint8Array(Math.floor(value.length * 3 / 4));
|
|
351
|
+
let buffer = 0;
|
|
352
|
+
let bits = 0;
|
|
353
|
+
let offset = 0;
|
|
354
|
+
for (const char of value) {
|
|
355
|
+
buffer = buffer << 6 | B64URL_ALPHABET.indexOf(char);
|
|
356
|
+
bits += 6;
|
|
357
|
+
if (bits >= 8) {
|
|
358
|
+
bits -= 8;
|
|
359
|
+
out[offset] = buffer >> bits & 255;
|
|
360
|
+
offset += 1;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (encodeBase64Url(out) !== value) {
|
|
364
|
+
throw new ByokCoreError("sealed_secret_invalid", `${field} is not canonical base64url`);
|
|
365
|
+
}
|
|
366
|
+
return out;
|
|
367
|
+
}
|
|
368
|
+
function base64UrlField(bytes) {
|
|
369
|
+
return z.string().superRefine((value, ctx) => {
|
|
370
|
+
try {
|
|
371
|
+
const decoded = decodeBase64Url(value, "field");
|
|
372
|
+
if (bytes.exact !== void 0 && decoded.byteLength !== bytes.exact) {
|
|
373
|
+
ctx.addIssue({ code: "custom", message: `must decode to exactly ${bytes.exact} bytes` });
|
|
374
|
+
}
|
|
375
|
+
if (bytes.max !== void 0 && decoded.byteLength > bytes.max) {
|
|
376
|
+
ctx.addIssue({ code: "custom", message: `must decode to at most ${bytes.max} bytes` });
|
|
377
|
+
}
|
|
378
|
+
} catch {
|
|
379
|
+
ctx.addIssue({ code: "custom", message: "must be canonical unpadded base64url" });
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
var SealingPublicJwkSchema = z.strictObject({
|
|
384
|
+
kty: z.literal("EC"),
|
|
385
|
+
crv: z.literal("P-256"),
|
|
386
|
+
x: base64UrlField({ exact: 32 }),
|
|
387
|
+
y: base64UrlField({ exact: 32 })
|
|
388
|
+
});
|
|
389
|
+
var SealingKeyIdSchema = z.string().regex(new RegExp(`^[A-Za-z0-9_-]{${SEALING_KEY_ID_LENGTH}}$`));
|
|
390
|
+
function parseSealingPublicJwk(input) {
|
|
391
|
+
const result = SealingPublicJwkSchema.safeParse(input);
|
|
392
|
+
if (!result.success) {
|
|
393
|
+
throw new ByokCoreError("sealed_secret_invalid", "Sealing public key must be a strict P-256 EC public JWK");
|
|
394
|
+
}
|
|
395
|
+
return result.data;
|
|
396
|
+
}
|
|
397
|
+
function sealingPublicKeyBytes(publicJwk) {
|
|
398
|
+
const jwk = parseSealingPublicJwk(publicJwk);
|
|
399
|
+
const raw = new Uint8Array(HPKE_P256_PUBLIC_KEY_BYTES);
|
|
400
|
+
raw[0] = 4;
|
|
401
|
+
raw.set(decodeBase64Url(jwk.x, "x"), 1);
|
|
402
|
+
raw.set(decodeBase64Url(jwk.y, "y"), 33);
|
|
403
|
+
return raw;
|
|
404
|
+
}
|
|
405
|
+
z.strictObject({
|
|
406
|
+
version: z.literal(1),
|
|
407
|
+
keyId: SealingKeyIdSchema,
|
|
408
|
+
epoch: z.number().int().positive().max(Number.MAX_SAFE_INTEGER),
|
|
409
|
+
publicJwk: SealingPublicJwkSchema
|
|
410
|
+
});
|
|
411
|
+
var OpaqueIdSchema = z.string().min(1).max(200).superRefine((value, ctx) => {
|
|
412
|
+
if (value.trim() !== value || /[\u0000-\u001f\u007f]/u.test(value)) {
|
|
413
|
+
ctx.addIssue({ code: "custom", message: "must not carry surrounding whitespace or control characters" });
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
var CanonicalTimestampSchema = z.string().refine(isCanonicalTimestamp, "must be a canonical ISO-8601 UTC instant");
|
|
417
|
+
var ProfileRefSchema = z.string().min(1).max(64).regex(/^[a-z0-9]+(?:[-_][a-z0-9]+)*$/u);
|
|
418
|
+
var ProviderKindSchema = z.string().min(1).max(64).regex(/^[a-z0-9]+(?:[-_][a-z0-9]+)*$/u);
|
|
419
|
+
var ModelIdSchema = z.string().min(1).max(160).refine((value) => value.trim() === value && !/[\u0000-\u001f\u007f]/u.test(value), "model id is invalid");
|
|
420
|
+
var CapabilitySchema = z.string().min(1).max(64).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/u);
|
|
421
|
+
var PiModelJsonSchema = z.custom((value) => {
|
|
422
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
423
|
+
try {
|
|
424
|
+
canonicalizeJson(value);
|
|
425
|
+
return true;
|
|
426
|
+
} catch {
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
}, "piModel must be a canonical JSON object");
|
|
430
|
+
var ModelConfigFields = {
|
|
431
|
+
agentId: OpaqueIdSchema,
|
|
432
|
+
providerKind: ProviderKindSchema,
|
|
433
|
+
modelId: ModelIdSchema,
|
|
434
|
+
piModel: PiModelJsonSchema,
|
|
435
|
+
capabilities: z.array(CapabilitySchema).max(8).refine((values) => new Set(values).size === values.length, "capabilities must be unique")
|
|
436
|
+
};
|
|
437
|
+
var ProviderProvisioningConfigV1Schema = z.discriminatedUnion("operation", [
|
|
438
|
+
z.strictObject({ operation: z.literal("configure"), ...ModelConfigFields }),
|
|
439
|
+
z.strictObject({ operation: z.literal("update_model"), ...ModelConfigFields }),
|
|
440
|
+
z.strictObject({ operation: z.literal("replace_secret"), agentId: OpaqueIdSchema, providerKind: ProviderKindSchema }),
|
|
441
|
+
z.strictObject({ operation: z.literal("delete"), agentId: OpaqueIdSchema })
|
|
442
|
+
]);
|
|
443
|
+
var ProviderProvisioningExpectedProfileSchema = z.union([
|
|
444
|
+
z.null(),
|
|
445
|
+
z.strictObject({
|
|
446
|
+
profileRef: ProfileRefSchema,
|
|
447
|
+
profileRevision: z.string().regex(/^(?:0|[1-9][0-9]{0,19})$/u),
|
|
448
|
+
profileHash: z.string().regex(CONTENT_HASH_PATTERN)
|
|
449
|
+
})
|
|
450
|
+
]);
|
|
451
|
+
var ProviderProvisioningIdentityRevisionSchema = z.string().min(1).max(128).regex(/^[A-Za-z0-9._:-]+$/u);
|
|
452
|
+
var ProviderProvisioningHeaderV1Schema = z.strictObject({
|
|
453
|
+
tenantId: OpaqueIdSchema,
|
|
454
|
+
deviceId: OpaqueIdSchema,
|
|
455
|
+
agentId: OpaqueIdSchema,
|
|
456
|
+
requestId: OpaqueIdSchema,
|
|
457
|
+
operation: z.enum(PROVIDER_PROVISIONING_OPERATIONS),
|
|
458
|
+
/** Non-secret, strictly increasing per bot (A3); a device rejects anything not above its high-watermark. */
|
|
459
|
+
operationGeneration: z.number().int().positive().max(Number.MAX_SAFE_INTEGER),
|
|
460
|
+
expectedProfile: ProviderProvisioningExpectedProfileSchema,
|
|
461
|
+
/** The device enrollment revision the Host authorized against (F2). */
|
|
462
|
+
expectedEnrollmentRevision: ProviderProvisioningIdentityRevisionSchema,
|
|
463
|
+
/** The agent placement revision the Host authorized against (F2). */
|
|
464
|
+
expectedPlacementRevision: ProviderProvisioningIdentityRevisionSchema,
|
|
465
|
+
configDigest: z.string().regex(CONTENT_HASH_PATTERN),
|
|
466
|
+
issuedAt: CanonicalTimestampSchema,
|
|
467
|
+
expiresAt: CanonicalTimestampSchema
|
|
468
|
+
});
|
|
469
|
+
var MAX_CIPHERTEXT_BYTES = PROVIDER_SECRET_MAX_PADDED_BYTES + HPKE_AEAD_TAG_BYTES;
|
|
470
|
+
var SealedProviderSecretV1Schema = z.strictObject({
|
|
471
|
+
suite: z.literal(PROVIDER_SECRET_HPKE_SUITE),
|
|
472
|
+
keyId: SealingKeyIdSchema,
|
|
473
|
+
enc: base64UrlField({ exact: HPKE_P256_PUBLIC_KEY_BYTES }),
|
|
474
|
+
ciphertext: base64UrlField({ max: MAX_CIPHERTEXT_BYTES })
|
|
475
|
+
});
|
|
476
|
+
function isSecretOperation(operation) {
|
|
477
|
+
return PROVIDER_PROVISIONING_SECRET_OPERATIONS.includes(operation);
|
|
478
|
+
}
|
|
479
|
+
z.strictObject({
|
|
480
|
+
version: z.literal(PROVIDER_PROVISIONING_REQUEST_VERSION),
|
|
481
|
+
header: ProviderProvisioningHeaderV1Schema,
|
|
482
|
+
config: ProviderProvisioningConfigV1Schema,
|
|
483
|
+
sealed: SealedProviderSecretV1Schema.optional()
|
|
484
|
+
}).superRefine((request, ctx) => {
|
|
485
|
+
if (request.header.operation !== request.config.operation) {
|
|
486
|
+
ctx.addIssue({ code: "custom", path: ["config", "operation"], message: "config operation must equal header operation" });
|
|
487
|
+
}
|
|
488
|
+
if (request.header.agentId !== request.config.agentId) {
|
|
489
|
+
ctx.addIssue({ code: "custom", path: ["config", "agentId"], message: "config agentId must equal header agentId" });
|
|
490
|
+
}
|
|
491
|
+
if (isSecretOperation(request.header.operation) !== (request.sealed !== void 0)) {
|
|
492
|
+
ctx.addIssue({ code: "custom", path: ["sealed"], message: "a sealed secret is required exactly for configure and replace_secret" });
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// src/sealed-payload/primitives.ts
|
|
497
|
+
var P256PublicJwkSchema = SealingPublicJwkSchema;
|
|
498
|
+
var ECDSA_P256_SIGNATURE_BYTES = 64;
|
|
499
|
+
function invalidInput(message) {
|
|
500
|
+
return new ByokCoreError("sealed_secret_invalid", message);
|
|
501
|
+
}
|
|
502
|
+
function utf82(value) {
|
|
503
|
+
return new TextEncoder().encode(value);
|
|
504
|
+
}
|
|
505
|
+
function copy2(bytes) {
|
|
506
|
+
return new Uint8Array(bytes);
|
|
507
|
+
}
|
|
508
|
+
function concat2(...parts) {
|
|
509
|
+
const out = new Uint8Array(parts.reduce((total, part) => total + part.byteLength, 0));
|
|
510
|
+
let offset = 0;
|
|
511
|
+
for (const part of parts) {
|
|
512
|
+
out.set(part, offset);
|
|
513
|
+
offset += part.byteLength;
|
|
514
|
+
}
|
|
515
|
+
return out;
|
|
516
|
+
}
|
|
517
|
+
function hex(bytes) {
|
|
518
|
+
let out = "";
|
|
519
|
+
for (const byte of bytes) out += byte.toString(16).padStart(2, "0");
|
|
520
|
+
return out;
|
|
521
|
+
}
|
|
522
|
+
function randomBytes(length) {
|
|
523
|
+
const source = globalThis.crypto;
|
|
524
|
+
if (source?.getRandomValues === void 0) {
|
|
525
|
+
throw new ByokCoreError("webcrypto_unavailable", "WebCrypto getRandomValues is unavailable in this runtime");
|
|
526
|
+
}
|
|
527
|
+
return source.getRandomValues(new Uint8Array(length));
|
|
528
|
+
}
|
|
529
|
+
async function sha256(bytes) {
|
|
530
|
+
return new Uint8Array(await webCryptoSubtle().digest("SHA-256", copy2(bytes)));
|
|
531
|
+
}
|
|
532
|
+
async function hmacSha2562(key, data) {
|
|
533
|
+
const subtle = webCryptoSubtle();
|
|
534
|
+
const hmacKey = await subtle.importKey("raw", copy2(key), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
535
|
+
return new Uint8Array(await subtle.sign("HMAC", hmacKey, copy2(data)));
|
|
536
|
+
}
|
|
537
|
+
async function hkdfSha256(ikm, salt, info, length) {
|
|
538
|
+
const subtle = webCryptoSubtle();
|
|
539
|
+
const key = await subtle.importKey("raw", copy2(ikm), { name: "HKDF" }, false, ["deriveBits"]);
|
|
540
|
+
const bits = await subtle.deriveBits({ name: "HKDF", hash: "SHA-256", salt: copy2(salt), info: utf82(info) }, key, length * 8);
|
|
541
|
+
return new Uint8Array(bits);
|
|
542
|
+
}
|
|
543
|
+
function base64UrlBytes(bytes) {
|
|
544
|
+
return z.string().refine((value) => {
|
|
545
|
+
try {
|
|
546
|
+
return decodeBase64Url(value, "field").byteLength === bytes;
|
|
547
|
+
} catch {
|
|
548
|
+
return false;
|
|
549
|
+
}
|
|
550
|
+
}, `must be canonical unpadded base64url of ${bytes} bytes`);
|
|
551
|
+
}
|
|
552
|
+
var ECDSA_P256 = { name: "ECDSA", namedCurve: "P-256" };
|
|
553
|
+
var ECDSA_SHA256 = { name: "ECDSA", hash: "SHA-256" };
|
|
554
|
+
function ecdsaSubtle() {
|
|
555
|
+
return webCryptoSubtle();
|
|
556
|
+
}
|
|
557
|
+
async function p256PublicKeyBytes(publicJwk) {
|
|
558
|
+
const raw = sealingPublicKeyBytes(publicJwk);
|
|
559
|
+
await importEcdsaPublicKey(publicJwk);
|
|
560
|
+
return raw;
|
|
561
|
+
}
|
|
562
|
+
async function importEcdsaPublicKey(publicJwk) {
|
|
563
|
+
const jwk = P256PublicJwkSchema.parse(publicJwk);
|
|
564
|
+
try {
|
|
565
|
+
return await ecdsaSubtle().importKey("jwk", { kty: jwk.kty, crv: jwk.crv, x: jwk.x, y: jwk.y }, ECDSA_P256, true, ["verify"]);
|
|
566
|
+
} catch (cause) {
|
|
567
|
+
throw new ByokCoreError("sealed_secret_invalid", "P-256 public key is not a valid curve point", { cause });
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
async function ecdsaSign(publicJwk, privateKey, data) {
|
|
571
|
+
let signature;
|
|
572
|
+
try {
|
|
573
|
+
signature = encodeBase64Url(new Uint8Array(await ecdsaSubtle().sign(ECDSA_SHA256, privateKey, copy2(data))));
|
|
574
|
+
} catch (cause) {
|
|
575
|
+
throw new ByokCoreError("sealed_secret_invalid", "Signing key is not an ECDSA P-256 private key", { cause });
|
|
576
|
+
}
|
|
577
|
+
if (!await ecdsaVerify(publicJwk, signature, data)) {
|
|
578
|
+
throw invalidInput("Signing private key does not pair with the signing public key");
|
|
579
|
+
}
|
|
580
|
+
return signature;
|
|
581
|
+
}
|
|
582
|
+
async function ecdsaVerify(publicJwk, signature, data) {
|
|
583
|
+
let signatureBytes;
|
|
584
|
+
try {
|
|
585
|
+
signatureBytes = decodeBase64Url(signature, "sig");
|
|
586
|
+
} catch {
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
if (signatureBytes.byteLength !== ECDSA_P256_SIGNATURE_BYTES) return false;
|
|
590
|
+
const key = await importEcdsaPublicKey(publicJwk);
|
|
591
|
+
return ecdsaSubtle().verify(ECDSA_SHA256, key, signatureBytes, copy2(data));
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// src/sealed-payload/member-chain.ts
|
|
595
|
+
var MEMBER_ID_DOMAIN = "byok-member-id-v1\n";
|
|
596
|
+
var MEMBER_ID_PATTERN = /^[A-Za-z0-9_-]{43}$/u;
|
|
597
|
+
var MEMBER_CHAIN_VERSION = 1;
|
|
598
|
+
var MEMBER_CHAIN_DOMAIN_PREFIX = "byok-member-chain-v1\n";
|
|
599
|
+
var MEMBER_CHAIN_OPS = ["add", "remove", "rotate", "scope-policy", "shred"];
|
|
600
|
+
var CanonicalJsonObjectSchema = z.custom((value) => {
|
|
601
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
602
|
+
try {
|
|
603
|
+
canonicalizeJson(value);
|
|
604
|
+
return true;
|
|
605
|
+
} catch {
|
|
606
|
+
return false;
|
|
607
|
+
}
|
|
608
|
+
}, "must be a canonical JSON object");
|
|
609
|
+
var unsignedEntryShape = {
|
|
610
|
+
v: z.literal(MEMBER_CHAIN_VERSION),
|
|
611
|
+
tenantId: z.string().min(1),
|
|
612
|
+
seq: z.number().int().positive().max(Number.MAX_SAFE_INTEGER),
|
|
613
|
+
prev: z.union([z.null(), z.string().regex(CONTENT_HASH_PATTERN)]),
|
|
614
|
+
op: z.enum(MEMBER_CHAIN_OPS),
|
|
615
|
+
subject: CanonicalJsonObjectSchema,
|
|
616
|
+
signer: z.string().regex(MEMBER_ID_PATTERN)
|
|
617
|
+
};
|
|
618
|
+
function firstEntryHasNoPrev(entry, ctx) {
|
|
619
|
+
if (entry.seq === 1 !== (entry.prev === null)) {
|
|
620
|
+
ctx.addIssue({ code: "custom", path: ["prev"], message: "the first entry, and only the first, has no prev" });
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
var UnsignedMemberChainEntryV1Schema = z.strictObject(unsignedEntryShape).superRefine(firstEntryHasNoPrev);
|
|
624
|
+
var MemberChainEntryV1Schema = z.strictObject({ ...unsignedEntryShape, sig: base64UrlBytes(64) }).superRefine(firstEntryHasNoPrev);
|
|
625
|
+
async function deriveMemberId(signingPublicJwk) {
|
|
626
|
+
const raw = await p256PublicKeyBytes(signingPublicJwk);
|
|
627
|
+
return encodeBase64Url(await sha256(concat2(utf82(MEMBER_ID_DOMAIN), raw)));
|
|
628
|
+
}
|
|
629
|
+
function parseUnsignedEntry(entry) {
|
|
630
|
+
const parsed = UnsignedMemberChainEntryV1Schema.safeParse(entry);
|
|
631
|
+
if (!parsed.success) throw invalidInput("Member chain entry is invalid");
|
|
632
|
+
return parsed.data;
|
|
633
|
+
}
|
|
634
|
+
function signingInput(entry) {
|
|
635
|
+
return concat2(
|
|
636
|
+
utf82(MEMBER_CHAIN_DOMAIN_PREFIX),
|
|
637
|
+
canonicalizeJsonBytes({
|
|
638
|
+
v: entry.v,
|
|
639
|
+
tenantId: entry.tenantId,
|
|
640
|
+
seq: entry.seq,
|
|
641
|
+
prev: entry.prev,
|
|
642
|
+
op: entry.op,
|
|
643
|
+
subject: entry.subject,
|
|
644
|
+
signer: entry.signer
|
|
645
|
+
})
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
function memberChainSigningInput(entry) {
|
|
649
|
+
return signingInput(parseUnsignedEntry(entry));
|
|
650
|
+
}
|
|
651
|
+
async function memberChainEntryHash(entry) {
|
|
652
|
+
return `sha256:${hex(await sha256(memberChainSigningInput(entry)))}`;
|
|
653
|
+
}
|
|
654
|
+
async function signMemberChainEntry(entry, signer) {
|
|
655
|
+
const unsigned = parseUnsignedEntry(entry);
|
|
656
|
+
if (await deriveMemberId(signer.publicJwk) !== unsigned.signer) {
|
|
657
|
+
throw invalidInput("Member chain entry signer does not match the signing key");
|
|
658
|
+
}
|
|
659
|
+
return { ...unsigned, sig: await ecdsaSign(signer.publicJwk, signer.privateKey, signingInput(unsigned)) };
|
|
660
|
+
}
|
|
661
|
+
async function verifyMemberChainEntry(entry, signerPublicJwk) {
|
|
662
|
+
const parsed = MemberChainEntryV1Schema.safeParse(entry);
|
|
663
|
+
if (!parsed.success || await deriveMemberId(signerPublicJwk) !== parsed.data.signer) {
|
|
664
|
+
return { ok: false, reason: "member_chain_entry_invalid" };
|
|
665
|
+
}
|
|
666
|
+
const input = signingInput(parsed.data);
|
|
667
|
+
if (!await ecdsaVerify(signerPublicJwk, parsed.data.sig, input)) {
|
|
668
|
+
return { ok: false, reason: "member_chain_signature_invalid" };
|
|
669
|
+
}
|
|
670
|
+
return { ok: true, entry: parsed.data, hash: `sha256:${hex(await sha256(input))}` };
|
|
671
|
+
}
|
|
672
|
+
var SCOPE_KEY_BYTES = 32;
|
|
673
|
+
var SCOPE_KEY_ID_PATTERN = /^[A-Za-z0-9_-]{22}$/u;
|
|
674
|
+
var KEY_ID_MESSAGE = "byok-scope-key-id-v1";
|
|
675
|
+
var CONTENT_KEY_INFO = "byok-sdk.sealed.v1.enc";
|
|
676
|
+
var MAC_KEY_INFO = "byok-sdk.sealed.v1.mac";
|
|
677
|
+
var SEALED_SALT_BYTES = 32;
|
|
678
|
+
var SCOPE_KEY_PACKAGE_VERSION = 1;
|
|
679
|
+
var SCOPE_KEY_HPKE_INFO = "byok-sdk.scope-key.v1";
|
|
680
|
+
var SCOPE_KEY_PACKAGE_DOMAIN_PREFIX = "byok-scope-key-package-v1\n";
|
|
681
|
+
var P256_POINT_BYTES = 65;
|
|
682
|
+
var AEAD_TAG_BYTES = 16;
|
|
683
|
+
function assertScopeKey(scopeKey) {
|
|
684
|
+
if (scopeKey.byteLength !== SCOPE_KEY_BYTES) throw invalidInput(`Scope key must be ${SCOPE_KEY_BYTES} bytes`);
|
|
685
|
+
}
|
|
686
|
+
function generateScopeKey() {
|
|
687
|
+
return randomBytes(SCOPE_KEY_BYTES);
|
|
688
|
+
}
|
|
689
|
+
async function deriveScopeKeyId(scopeKey) {
|
|
690
|
+
assertScopeKey(scopeKey);
|
|
691
|
+
const mac = await hmacSha2562(scopeKey, utf82(KEY_ID_MESSAGE));
|
|
692
|
+
return encodeBase64Url(mac.subarray(0, 16));
|
|
693
|
+
}
|
|
694
|
+
async function deriveSealedContentKey(scopeKey, salt) {
|
|
695
|
+
assertScopeKey(scopeKey);
|
|
696
|
+
if (salt.byteLength !== SEALED_SALT_BYTES) throw invalidInput(`Sealed content salt must be ${SEALED_SALT_BYTES} bytes`);
|
|
697
|
+
return hkdfSha256(scopeKey, salt, CONTENT_KEY_INFO, 32);
|
|
698
|
+
}
|
|
699
|
+
async function deriveSealedMacKey(scopeKey) {
|
|
700
|
+
assertScopeKey(scopeKey);
|
|
701
|
+
return hkdfSha256(scopeKey, new Uint8Array(32), MAC_KEY_INFO, 32);
|
|
702
|
+
}
|
|
703
|
+
var ScopeKeyPackageV1Schema = z.strictObject({
|
|
704
|
+
v: z.literal(SCOPE_KEY_PACKAGE_VERSION),
|
|
705
|
+
tenantId: z.string().min(1),
|
|
706
|
+
scopeId: z.string().min(1),
|
|
707
|
+
epoch: z.number().int().positive().max(Number.MAX_SAFE_INTEGER),
|
|
708
|
+
keyId: z.string().regex(SCOPE_KEY_ID_PATTERN),
|
|
709
|
+
recipient: z.string().regex(MEMBER_ID_PATTERN),
|
|
710
|
+
sender: z.string().regex(MEMBER_ID_PATTERN),
|
|
711
|
+
enc: base64UrlBytes(P256_POINT_BYTES),
|
|
712
|
+
ciphertext: base64UrlBytes(SCOPE_KEY_BYTES + AEAD_TAG_BYTES),
|
|
713
|
+
sig: base64UrlBytes(64)
|
|
714
|
+
});
|
|
715
|
+
function packageAad(identity) {
|
|
716
|
+
return canonicalizeJsonBytes({
|
|
717
|
+
v: identity.v,
|
|
718
|
+
tenantId: identity.tenantId,
|
|
719
|
+
scopeId: identity.scopeId,
|
|
720
|
+
epoch: identity.epoch,
|
|
721
|
+
keyId: identity.keyId,
|
|
722
|
+
recipient: identity.recipient,
|
|
723
|
+
sender: identity.sender
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
function packageSigningInput(pkg) {
|
|
727
|
+
return concat2(
|
|
728
|
+
utf82(SCOPE_KEY_PACKAGE_DOMAIN_PREFIX),
|
|
729
|
+
canonicalizeJsonBytes({
|
|
730
|
+
v: pkg.v,
|
|
731
|
+
tenantId: pkg.tenantId,
|
|
732
|
+
scopeId: pkg.scopeId,
|
|
733
|
+
epoch: pkg.epoch,
|
|
734
|
+
keyId: pkg.keyId,
|
|
735
|
+
recipient: pkg.recipient,
|
|
736
|
+
sender: pkg.sender,
|
|
737
|
+
enc: pkg.enc,
|
|
738
|
+
ciphertext: pkg.ciphertext
|
|
739
|
+
})
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
var PackageIdentitySchema = ScopeKeyPackageV1Schema.omit({ enc: true, ciphertext: true, sig: true });
|
|
743
|
+
async function sealScopeKeyPackage(input) {
|
|
744
|
+
const parsed = PackageIdentitySchema.safeParse({
|
|
745
|
+
v: SCOPE_KEY_PACKAGE_VERSION,
|
|
746
|
+
tenantId: input.tenantId,
|
|
747
|
+
scopeId: input.scopeId,
|
|
748
|
+
epoch: input.epoch,
|
|
749
|
+
keyId: await deriveScopeKeyId(input.scopeKey),
|
|
750
|
+
recipient: input.recipient.memberId,
|
|
751
|
+
sender: await deriveMemberId(input.sender.signingPublicJwk)
|
|
752
|
+
});
|
|
753
|
+
if (!parsed.success) throw invalidInput("Scope key package input is invalid");
|
|
754
|
+
const identity = parsed.data;
|
|
755
|
+
const { enc, ciphertext } = await hpkeSealBase({
|
|
756
|
+
recipientPublicKey: await p256PublicKeyBytes(input.recipient.kemPublicJwk),
|
|
757
|
+
info: utf82(SCOPE_KEY_HPKE_INFO),
|
|
758
|
+
aad: packageAad(identity),
|
|
759
|
+
plaintext: input.scopeKey
|
|
760
|
+
});
|
|
761
|
+
const unsigned = { ...identity, enc: encodeBase64Url(enc), ciphertext: encodeBase64Url(ciphertext) };
|
|
762
|
+
return { ...unsigned, sig: await ecdsaSign(input.sender.signingPublicJwk, input.sender.signingPrivateKey, packageSigningInput(unsigned)) };
|
|
763
|
+
}
|
|
764
|
+
async function openScopeKeyPackage(input) {
|
|
765
|
+
const parsed = ScopeKeyPackageV1Schema.safeParse(input.package);
|
|
766
|
+
if (!parsed.success) return { ok: false, reason: "key_package_invalid" };
|
|
767
|
+
const pkg = parsed.data;
|
|
768
|
+
if (pkg.recipient !== input.recipient.memberId || pkg.tenantId !== input.expected.tenantId || pkg.scopeId !== input.expected.scopeId || pkg.epoch !== input.expected.epoch || pkg.sender !== await deriveMemberId(input.senderSigningPublicJwk)) {
|
|
769
|
+
return { ok: false, reason: "key_package_invalid" };
|
|
770
|
+
}
|
|
771
|
+
if (!await ecdsaVerify(input.senderSigningPublicJwk, pkg.sig, packageSigningInput(pkg))) {
|
|
772
|
+
return { ok: false, reason: "key_package_signature_invalid" };
|
|
773
|
+
}
|
|
774
|
+
let scopeKey;
|
|
775
|
+
try {
|
|
776
|
+
scopeKey = await hpkeOpenBase({
|
|
777
|
+
recipientPrivateKey: input.recipient.kemPrivateKey,
|
|
778
|
+
recipientPublicKey: await p256PublicKeyBytes(input.recipient.kemPublicJwk),
|
|
779
|
+
enc: decodeBase64Url(pkg.enc, "enc"),
|
|
780
|
+
info: utf82(SCOPE_KEY_HPKE_INFO),
|
|
781
|
+
aad: packageAad(pkg),
|
|
782
|
+
ciphertext: decodeBase64Url(pkg.ciphertext, "ciphertext")
|
|
783
|
+
});
|
|
784
|
+
} catch (error) {
|
|
785
|
+
if (error instanceof ByokCoreError) return { ok: false, reason: "key_package_open_failed" };
|
|
786
|
+
throw error;
|
|
787
|
+
}
|
|
788
|
+
if (scopeKey.byteLength !== SCOPE_KEY_BYTES || await deriveScopeKeyId(scopeKey) !== pkg.keyId) {
|
|
789
|
+
scopeKey.fill(0);
|
|
790
|
+
return { ok: false, reason: "key_package_open_failed" };
|
|
791
|
+
}
|
|
792
|
+
return { ok: true, scopeKey, keyId: pkg.keyId };
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// src/sealed-payload/sealed-content.ts
|
|
796
|
+
var SEALED_CONTENT_VERSION = 1;
|
|
797
|
+
var SEALED_CONTENT_SUITE = "byok-sealed-v1.hkdf-sha256.aes-256-gcm";
|
|
798
|
+
var SEALED_CATEGORIES = ["task-input", "agent-message", "task-output", "memory-projection"];
|
|
799
|
+
var SEALED_DIRECTIONS = ["server-to-daemon", "daemon-to-server"];
|
|
800
|
+
var SEALED_NONCE_BYTES = 12;
|
|
801
|
+
var AEAD_TAG_BYTES2 = 16;
|
|
802
|
+
var FRAME_HEADER_BYTES = 4;
|
|
803
|
+
var ALIGNED_FRAME_LIMIT = 16 * 1024;
|
|
804
|
+
var FRAME_ALIGNMENT = 256;
|
|
805
|
+
var MAX_CONTENT_LENGTH = 4294967295;
|
|
806
|
+
var CONTENT_MAC_PREFIX = "byok-content-mac-v1\n";
|
|
807
|
+
var CONTENT_MAC_PATTERN = /^hmac-sha256:[0-9a-f]{64}$/u;
|
|
808
|
+
var FAMILY_PATTERN = /^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)+$/u;
|
|
809
|
+
var IDENTITY_FIELD_PATTERN = /^[a-z][A-Za-z0-9]*$/u;
|
|
810
|
+
var ROUTINE_TEMPLATE_FAMILY = "routine.template";
|
|
811
|
+
function floorLog2(value) {
|
|
812
|
+
let exponent = 0;
|
|
813
|
+
for (let remaining = value; remaining >= 2; remaining = Math.floor(remaining / 2)) exponent += 1;
|
|
814
|
+
return exponent;
|
|
815
|
+
}
|
|
816
|
+
function sealedPaddedLength(frameLength) {
|
|
817
|
+
if (!Number.isSafeInteger(frameLength) || frameLength < FRAME_HEADER_BYTES) {
|
|
818
|
+
throw invalidInput("Sealed frame length must be a safe integer of at least 4");
|
|
819
|
+
}
|
|
820
|
+
if (frameLength <= ALIGNED_FRAME_LIMIT) return Math.ceil(frameLength / FRAME_ALIGNMENT) * FRAME_ALIGNMENT;
|
|
821
|
+
const exponent = floorLog2(frameLength);
|
|
822
|
+
const step = 2 ** (exponent - (floorLog2(exponent) + 1));
|
|
823
|
+
return Math.ceil(frameLength / step) * step;
|
|
824
|
+
}
|
|
825
|
+
function isLegalPaddedLength(paddedLength) {
|
|
826
|
+
return Number.isSafeInteger(paddedLength) && paddedLength >= FRAME_ALIGNMENT && sealedPaddedLength(paddedLength) === paddedLength;
|
|
827
|
+
}
|
|
828
|
+
function encodeSealedFrame(content) {
|
|
829
|
+
if (content.byteLength > MAX_CONTENT_LENGTH) throw invalidInput("Sealed content exceeds the 32-bit frame length");
|
|
830
|
+
const frame = new Uint8Array(sealedPaddedLength(FRAME_HEADER_BYTES + content.byteLength));
|
|
831
|
+
new DataView(frame.buffer).setUint32(0, content.byteLength, false);
|
|
832
|
+
frame.set(content, FRAME_HEADER_BYTES);
|
|
833
|
+
return frame;
|
|
834
|
+
}
|
|
835
|
+
function decodeSealedFrame(frame, paddedLength, maxContentLength) {
|
|
836
|
+
if (frame.byteLength !== paddedLength || paddedLength < FRAME_HEADER_BYTES) return void 0;
|
|
837
|
+
const contentLength = new DataView(frame.buffer, frame.byteOffset, frame.byteLength).getUint32(0, false);
|
|
838
|
+
if (contentLength > paddedLength - FRAME_HEADER_BYTES) return void 0;
|
|
839
|
+
if (sealedPaddedLength(FRAME_HEADER_BYTES + contentLength) !== paddedLength) return void 0;
|
|
840
|
+
if (contentLength > maxContentLength) return void 0;
|
|
841
|
+
for (let index = FRAME_HEADER_BYTES + contentLength; index < paddedLength; index += 1) {
|
|
842
|
+
if (frame[index] !== 0) return void 0;
|
|
843
|
+
}
|
|
844
|
+
return frame.subarray(FRAME_HEADER_BYTES, FRAME_HEADER_BYTES + contentLength);
|
|
845
|
+
}
|
|
846
|
+
var NonEmptyStringSchema = z.string().min(1);
|
|
847
|
+
var PositiveIntegerSchema = z.number().int().positive().max(Number.MAX_SAFE_INTEGER);
|
|
848
|
+
var SealedContentHeaderV1Schema = z.strictObject({
|
|
849
|
+
v: z.literal(SEALED_CONTENT_VERSION),
|
|
850
|
+
suite: z.literal(SEALED_CONTENT_SUITE),
|
|
851
|
+
category: z.enum(SEALED_CATEGORIES),
|
|
852
|
+
scope: z.strictObject({
|
|
853
|
+
id: NonEmptyStringSchema,
|
|
854
|
+
epoch: PositiveIntegerSchema,
|
|
855
|
+
keyId: z.string().regex(SCOPE_KEY_ID_PATTERN)
|
|
856
|
+
}),
|
|
857
|
+
chainHead: z.strictObject({
|
|
858
|
+
seq: PositiveIntegerSchema,
|
|
859
|
+
hash: z.string().regex(CONTENT_HASH_PATTERN)
|
|
860
|
+
}),
|
|
861
|
+
sender: z.string().regex(MEMBER_ID_PATTERN),
|
|
862
|
+
salt: base64UrlBytes(SEALED_SALT_BYTES),
|
|
863
|
+
nonce: base64UrlBytes(SEALED_NONCE_BYTES),
|
|
864
|
+
paddedLength: PositiveIntegerSchema.refine(isLegalPaddedLength, "must be a legal padded frame length"),
|
|
865
|
+
contentMac: z.string().regex(CONTENT_MAC_PATTERN)
|
|
866
|
+
});
|
|
867
|
+
function isCanonicalCategoryList(categories) {
|
|
868
|
+
return categories.every((category, index) => index === 0 || SEALED_CATEGORIES.indexOf(categories[index - 1]) < SEALED_CATEGORIES.indexOf(category));
|
|
869
|
+
}
|
|
870
|
+
var SealedBindingSchema = z.strictObject({
|
|
871
|
+
tenantId: NonEmptyStringSchema,
|
|
872
|
+
family: z.string().regex(FAMILY_PATTERN),
|
|
873
|
+
direction: z.enum(SEALED_DIRECTIONS),
|
|
874
|
+
identity: z.record(
|
|
875
|
+
z.string().regex(IDENTITY_FIELD_PATTERN),
|
|
876
|
+
z.union([NonEmptyStringSchema, z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER)])
|
|
877
|
+
).refine((identity) => Object.keys(identity).length > 0, "identity must name at least one field"),
|
|
878
|
+
deviceId: z.union([z.null(), NonEmptyStringSchema]),
|
|
879
|
+
agentRef: z.union([z.null(), z.strictObject({ agentId: NonEmptyStringSchema, profileRevision: NonEmptyStringSchema })]),
|
|
880
|
+
categories: z.array(z.enum(SEALED_CATEGORIES)).min(1).refine(isCanonicalCategoryList, "categories must be unique and in canonical order"),
|
|
881
|
+
notAfter: z.union([z.null(), z.string().refine(isCanonicalTimestamp, "must be a canonical UTC instant")])
|
|
882
|
+
}).superRefine((binding, ctx) => {
|
|
883
|
+
const template = binding.family === ROUTINE_TEMPLATE_FAMILY;
|
|
884
|
+
if (binding.deviceId === null !== template || binding.notAfter === null === template) {
|
|
885
|
+
ctx.addIssue({
|
|
886
|
+
code: "custom",
|
|
887
|
+
path: ["deviceId"],
|
|
888
|
+
message: "a routine template binds an expiry and no device; every other family binds a device and no expiry"
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
function parseBinding(binding) {
|
|
893
|
+
const parsed = SealedBindingSchema.safeParse(binding);
|
|
894
|
+
if (!parsed.success) throw invalidInput("Sealed content binding is invalid");
|
|
895
|
+
return parsed.data;
|
|
896
|
+
}
|
|
897
|
+
function assertMaxContentLength(maxContentLength) {
|
|
898
|
+
if (!Number.isSafeInteger(maxContentLength) || maxContentLength < 0 || maxContentLength > MAX_CONTENT_LENGTH) {
|
|
899
|
+
throw invalidInput("Sealed content limit must be a non-negative 32-bit integer");
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
function aadBytes(header, binding) {
|
|
903
|
+
return canonicalizeJsonBytes({
|
|
904
|
+
v: header.v,
|
|
905
|
+
suite: header.suite,
|
|
906
|
+
category: header.category,
|
|
907
|
+
tenantId: binding.tenantId,
|
|
908
|
+
scopeId: header.scope.id,
|
|
909
|
+
epoch: header.scope.epoch,
|
|
910
|
+
keyId: header.scope.keyId,
|
|
911
|
+
chainHead: { seq: header.chainHead.seq, hash: header.chainHead.hash },
|
|
912
|
+
sender: header.sender,
|
|
913
|
+
family: binding.family,
|
|
914
|
+
direction: binding.direction,
|
|
915
|
+
identity: binding.identity,
|
|
916
|
+
deviceId: binding.deviceId,
|
|
917
|
+
agentRef: binding.agentRef === null ? null : { agentId: binding.agentRef.agentId, profileRevision: binding.agentRef.profileRevision },
|
|
918
|
+
categories: [...binding.categories],
|
|
919
|
+
notAfter: binding.notAfter,
|
|
920
|
+
paddedLength: header.paddedLength,
|
|
921
|
+
contentMac: header.contentMac
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
function isNonEmptyPlainObject(value) {
|
|
925
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
926
|
+
const prototype = Object.getPrototypeOf(value);
|
|
927
|
+
return (prototype === Object.prototype || prototype === null) && Object.keys(value).length > 0;
|
|
928
|
+
}
|
|
929
|
+
function canonicalPlaintext(plaintext) {
|
|
930
|
+
if (!isNonEmptyPlainObject(plaintext)) throw invalidInput("Sealed plaintext must be a non-empty plain JSON object");
|
|
931
|
+
try {
|
|
932
|
+
return canonicalizeJson(plaintext);
|
|
933
|
+
} catch (cause) {
|
|
934
|
+
throw new ByokCoreError("sealed_secret_invalid", "Sealed plaintext is not canonical JSON", { cause });
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
function parseCanonicalPlaintext(content) {
|
|
938
|
+
let text;
|
|
939
|
+
let value;
|
|
940
|
+
try {
|
|
941
|
+
text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(content);
|
|
942
|
+
value = JSON.parse(text);
|
|
943
|
+
if (!isNonEmptyPlainObject(value) || canonicalizeJson(value) !== text) return void 0;
|
|
944
|
+
} catch {
|
|
945
|
+
return void 0;
|
|
946
|
+
}
|
|
947
|
+
return { value, text };
|
|
948
|
+
}
|
|
949
|
+
async function contentMacOf(scopeKey, category, binding, plaintextText) {
|
|
950
|
+
const macKey = await deriveSealedMacKey(scopeKey);
|
|
951
|
+
try {
|
|
952
|
+
const input = `${CONTENT_MAC_PREFIX}${category}
|
|
953
|
+
${binding.family}
|
|
954
|
+
${canonicalizeJson(binding.identity)}
|
|
955
|
+
${plaintextText}`;
|
|
956
|
+
return `hmac-sha256:${hex(await hmacSha2562(macKey, utf82(input)))}`;
|
|
957
|
+
} finally {
|
|
958
|
+
macKey.fill(0);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
async function aesGcm(mode, key, nonce, aad, data) {
|
|
962
|
+
const subtle = webCryptoSubtle();
|
|
963
|
+
const cryptoKey = await subtle.importKey("raw", copy2(key), { name: "AES-GCM" }, false, [mode]);
|
|
964
|
+
const params = { name: "AES-GCM", iv: copy2(nonce), additionalData: copy2(aad), tagLength: 128 };
|
|
965
|
+
const out = mode === "encrypt" ? await subtle.encrypt(params, cryptoKey, copy2(data)) : await subtle.decrypt(params, cryptoKey, copy2(data));
|
|
966
|
+
return new Uint8Array(out);
|
|
967
|
+
}
|
|
968
|
+
async function sealContent(input) {
|
|
969
|
+
return sealContentWithRandomness(input, { salt: randomBytes(SEALED_SALT_BYTES), nonce: randomBytes(SEALED_NONCE_BYTES) });
|
|
970
|
+
}
|
|
971
|
+
async function sealContentWithRandomness(input, randomness) {
|
|
972
|
+
const binding = parseBinding(input.binding);
|
|
973
|
+
assertMaxContentLength(input.maxContentLength);
|
|
974
|
+
if (!binding.categories.includes(input.category)) {
|
|
975
|
+
throw invalidInput("Sealed content category is not one of the task sealing categories");
|
|
976
|
+
}
|
|
977
|
+
const plaintextText = canonicalPlaintext(input.plaintext);
|
|
978
|
+
const content = utf82(plaintextText);
|
|
979
|
+
if (content.byteLength > input.maxContentLength) throw invalidInput("Sealed content exceeds its plaintext limit");
|
|
980
|
+
const frame = encodeSealedFrame(content);
|
|
981
|
+
const { salt, nonce } = randomness;
|
|
982
|
+
const parsed = SealedContentHeaderV1Schema.safeParse({
|
|
983
|
+
v: SEALED_CONTENT_VERSION,
|
|
984
|
+
suite: SEALED_CONTENT_SUITE,
|
|
985
|
+
category: input.category,
|
|
986
|
+
scope: { id: input.scope.id, epoch: input.scope.epoch, keyId: await deriveScopeKeyId(input.scopeKey) },
|
|
987
|
+
chainHead: { seq: input.chainHead.seq, hash: input.chainHead.hash },
|
|
988
|
+
sender: input.sender,
|
|
989
|
+
salt: encodeBase64Url(salt),
|
|
990
|
+
nonce: encodeBase64Url(nonce),
|
|
991
|
+
paddedLength: frame.byteLength,
|
|
992
|
+
contentMac: await contentMacOf(input.scopeKey, input.category, binding, plaintextText)
|
|
993
|
+
});
|
|
994
|
+
if (!parsed.success) throw invalidInput("Sealed content header input is invalid");
|
|
995
|
+
const contentKey = await deriveSealedContentKey(input.scopeKey, salt);
|
|
996
|
+
try {
|
|
997
|
+
const ciphertext = await aesGcm("encrypt", contentKey, nonce, aadBytes(parsed.data, binding), frame);
|
|
998
|
+
return { header: parsed.data, ciphertext };
|
|
999
|
+
} finally {
|
|
1000
|
+
contentKey.fill(0);
|
|
1001
|
+
frame.fill(0);
|
|
1002
|
+
content.fill(0);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
var SealedScopePolicySchema = z.strictObject({ scopeId: NonEmptyStringSchema, epoch: PositiveIntegerSchema });
|
|
1006
|
+
function failure(reason) {
|
|
1007
|
+
return { ok: false, reason };
|
|
1008
|
+
}
|
|
1009
|
+
async function openSealedContent(input) {
|
|
1010
|
+
assertMaxContentLength(input.maxContentLength);
|
|
1011
|
+
const parsedBinding = SealedBindingSchema.safeParse(input.binding);
|
|
1012
|
+
const parsedSealing = SealedScopePolicySchema.safeParse(input.sealing);
|
|
1013
|
+
if (!parsedBinding.success || !parsedSealing.success) return failure("sealing_policy_mismatch");
|
|
1014
|
+
const binding = parsedBinding.data;
|
|
1015
|
+
const sealing = parsedSealing.data;
|
|
1016
|
+
const parsed = SealedContentHeaderV1Schema.safeParse(input.header);
|
|
1017
|
+
if (!parsed.success) return failure("sealed_open_failed");
|
|
1018
|
+
const header = parsed.data;
|
|
1019
|
+
if (!binding.categories.includes(header.category) || header.scope.id !== sealing.scopeId || header.scope.epoch < sealing.epoch) {
|
|
1020
|
+
return failure("sealing_policy_mismatch");
|
|
1021
|
+
}
|
|
1022
|
+
if (await deriveScopeKeyId(input.scopeKey) !== header.scope.keyId) return failure("sealed_key_unavailable");
|
|
1023
|
+
if (input.ciphertext.byteLength !== header.paddedLength + AEAD_TAG_BYTES2) return failure("sealed_open_failed");
|
|
1024
|
+
const contentKey = await deriveSealedContentKey(input.scopeKey, decodeBase64Url(header.salt, "salt"));
|
|
1025
|
+
let frame;
|
|
1026
|
+
try {
|
|
1027
|
+
frame = await aesGcm("decrypt", contentKey, decodeBase64Url(header.nonce, "nonce"), aadBytes(header, binding), input.ciphertext);
|
|
1028
|
+
} catch {
|
|
1029
|
+
return failure("sealed_open_failed");
|
|
1030
|
+
} finally {
|
|
1031
|
+
contentKey.fill(0);
|
|
1032
|
+
}
|
|
1033
|
+
try {
|
|
1034
|
+
const content = decodeSealedFrame(frame, header.paddedLength, input.maxContentLength);
|
|
1035
|
+
const plaintext = content === void 0 ? void 0 : parseCanonicalPlaintext(content);
|
|
1036
|
+
if (plaintext === void 0) return failure("sealed_open_failed");
|
|
1037
|
+
if (await contentMacOf(input.scopeKey, header.category, binding, plaintext.text) !== header.contentMac) {
|
|
1038
|
+
return failure("sealed_content_mac_mismatch");
|
|
1039
|
+
}
|
|
1040
|
+
return { ok: true, plaintext: plaintext.value, header };
|
|
1041
|
+
} finally {
|
|
1042
|
+
frame.fill(0);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
export { MEMBER_CHAIN_OPS, SEALED_CATEGORIES, SEALED_CONTENT_SUITE, SEALED_CONTENT_VERSION, SEALED_DIRECTIONS, SealedContentHeaderV1Schema, deriveMemberId, deriveScopeKeyId, generateScopeKey, memberChainEntryHash, openScopeKeyPackage, openSealedContent, sealContent, sealScopeKeyPackage, signMemberChainEntry, verifyMemberChainEntry };
|
|
1047
|
+
//# sourceMappingURL=index.js.map
|
|
1048
|
+
//# sourceMappingURL=index.js.map
|