@calimero-network/mero-js 19.22.0 → 19.24.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/dist/admin-api/admin-client.d.ts +7 -0
- package/dist/admin-api/admin-client.js +9 -0
- package/dist/index.browser.mjs +2 -2
- package/dist/index.browser.mjs.map +4 -4
- package/dist/index.cjs +368 -138
- package/dist/index.cjs.map +4 -4
- package/dist/index.mjs +368 -138
- package/dist/index.mjs.map +4 -4
- package/dist/sealed/index.d.ts +1 -1
- package/dist/sealed/index.js +1 -1
- package/dist/sealed/noise.d.ts +44 -0
- package/dist/sealed/noise.js +137 -0
- package/dist/sealed/sealed.d.ts +60 -40
- package/dist/sealed/sealed.js +270 -126
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1677,6 +1677,15 @@ var AdminApiClient = class {
|
|
|
1677
1677
|
async setTeeAuthoringPolicy(groupId, request) {
|
|
1678
1678
|
await this.httpClient.put(`/admin-api/groups/${groupId}/settings/tee-authoring-policy`, request);
|
|
1679
1679
|
}
|
|
1680
|
+
/**
|
|
1681
|
+
* Turns TEE authorship off in the namespace: every TEE loses its authority,
|
|
1682
|
+
* and admitted TEEs stay members. The node applies the same op as
|
|
1683
|
+
* `setTeeAuthoringPolicy(groupId, { allowedMrtd: [] })`. `groupId` must be a
|
|
1684
|
+
* namespace root.
|
|
1685
|
+
*/
|
|
1686
|
+
async disableTeeAuthoringPolicy(groupId) {
|
|
1687
|
+
await this.httpClient.delete(`/admin-api/groups/${groupId}/settings/tee-authoring-policy`);
|
|
1688
|
+
}
|
|
1680
1689
|
// ---- Group / member / context metadata ----
|
|
1681
1690
|
async setGroupMetadata(groupId, request) {
|
|
1682
1691
|
await this.httpClient.put(`/admin-api/groups/${groupId}/metadata`, request);
|
|
@@ -2757,12 +2766,12 @@ var CERT_DOMAIN = new TextEncoder().encode("calimero.device.cert.v1");
|
|
|
2757
2766
|
var ACCOUNT_ID_DOMAIN = new TextEncoder().encode("calimero.account.genesis.v1");
|
|
2758
2767
|
var DEVICE_ID_DOMAIN = new TextEncoder().encode("calimero.device.id.v1");
|
|
2759
2768
|
var ACCOUNT_GENESIS_VERSION = 2;
|
|
2760
|
-
async function mintDeviceId(account,
|
|
2761
|
-
if (
|
|
2762
|
-
throw new Error(`nonce must be 16 bytes, got ${
|
|
2769
|
+
async function mintDeviceId(account, nonce2) {
|
|
2770
|
+
if (nonce2.length !== 16) {
|
|
2771
|
+
throw new Error(`nonce must be 16 bytes, got ${nonce2.length}`);
|
|
2763
2772
|
}
|
|
2764
2773
|
const accountBytes = fromHex(account, "account", 32);
|
|
2765
|
-
return hex(await domainHash(DEVICE_ID_DOMAIN, [accountBytes,
|
|
2774
|
+
return hex(await domainHash(DEVICE_ID_DOMAIN, [accountBytes, nonce2]));
|
|
2766
2775
|
}
|
|
2767
2776
|
async function deviceCertPayload(input) {
|
|
2768
2777
|
return domainHash(CERT_DOMAIN, [
|
|
@@ -4992,12 +5001,12 @@ function base64(bytes) {
|
|
|
4992
5001
|
var ROUTING_PROOF_DOMAIN = new TextEncoder().encode(
|
|
4993
5002
|
"calimero.mdma.routing-read.v1\0"
|
|
4994
5003
|
);
|
|
4995
|
-
async function signRoutingChallenge(
|
|
5004
|
+
async function signRoutingChallenge(nonce2, deviceSecret) {
|
|
4996
5005
|
fromHex(deviceSecret, "deviceSecret", 32);
|
|
4997
5006
|
const key = await importSigningKey(deviceSecret);
|
|
4998
5007
|
const message = concat(
|
|
4999
5008
|
ROUTING_PROOF_DOMAIN,
|
|
5000
|
-
new TextEncoder().encode(
|
|
5009
|
+
new TextEncoder().encode(nonce2)
|
|
5001
5010
|
);
|
|
5002
5011
|
const signature = new Uint8Array(
|
|
5003
5012
|
await crypto.subtle.sign({ name: "Ed25519" }, key, message)
|
|
@@ -5087,11 +5096,11 @@ var CloudClient = class {
|
|
|
5087
5096
|
*/
|
|
5088
5097
|
async signInWithAccount(rootSecret) {
|
|
5089
5098
|
const root = await accountRootFromSecret(rootSecret);
|
|
5090
|
-
const { nonce } = await this.getAccountLoginChallenge();
|
|
5099
|
+
const { nonce: nonce2 } = await this.getAccountLoginChallenge();
|
|
5091
5100
|
return this.submitAccountLogin({
|
|
5092
5101
|
rootPublicKey: root.publicKey,
|
|
5093
|
-
nonce,
|
|
5094
|
-
signature: await signAccountLogin({ rootSecret, nonce })
|
|
5102
|
+
nonce: nonce2,
|
|
5103
|
+
signature: await signAccountLogin({ rootSecret, nonce: nonce2 })
|
|
5095
5104
|
});
|
|
5096
5105
|
}
|
|
5097
5106
|
/**
|
|
@@ -5510,12 +5519,12 @@ var CloudClient = class {
|
|
|
5510
5519
|
*/
|
|
5511
5520
|
async linkAccount(rootSecret) {
|
|
5512
5521
|
const root = await accountRootFromSecret(rootSecret);
|
|
5513
|
-
const { nonce } = await this.getAccountLinkChallenge();
|
|
5522
|
+
const { nonce: nonce2 } = await this.getAccountLinkChallenge();
|
|
5514
5523
|
return this.submitAccountLink({
|
|
5515
5524
|
accountId: root.accountId,
|
|
5516
5525
|
rootPublicKey: root.publicKey,
|
|
5517
|
-
nonce,
|
|
5518
|
-
signature: await signAccountLink({ rootSecret, nonce })
|
|
5526
|
+
nonce: nonce2,
|
|
5527
|
+
signature: await signAccountLink({ rootSecret, nonce: nonce2 })
|
|
5519
5528
|
});
|
|
5520
5529
|
}
|
|
5521
5530
|
/**
|
|
@@ -6200,7 +6209,7 @@ async function signWarrant(input) {
|
|
|
6200
6209
|
const publicKey = await derivePublicKey(input.deviceSecret);
|
|
6201
6210
|
const method = new TextEncoder().encode(input.method);
|
|
6202
6211
|
const commitment = await intentHash(input.method, input.argsJson);
|
|
6203
|
-
const
|
|
6212
|
+
const nonce2 = u64le(input.nonce);
|
|
6204
6213
|
const notAfter = u64le(input.notAfter);
|
|
6205
6214
|
const preimage = await domainHash(SIGN_DOMAIN2, [
|
|
6206
6215
|
context,
|
|
@@ -6214,7 +6223,7 @@ async function signWarrant(input) {
|
|
|
6214
6223
|
...accountHeads,
|
|
6215
6224
|
u64le(governanceFloor.length),
|
|
6216
6225
|
...governanceFloor,
|
|
6217
|
-
|
|
6226
|
+
nonce2,
|
|
6218
6227
|
notAfter
|
|
6219
6228
|
]);
|
|
6220
6229
|
const signature = new Uint8Array(
|
|
@@ -6234,7 +6243,7 @@ async function signWarrant(input) {
|
|
|
6234
6243
|
...accountHeads,
|
|
6235
6244
|
u32le(governanceFloor.length),
|
|
6236
6245
|
...governanceFloor,
|
|
6237
|
-
|
|
6246
|
+
nonce2,
|
|
6238
6247
|
notAfter,
|
|
6239
6248
|
signature
|
|
6240
6249
|
)
|
|
@@ -6291,7 +6300,7 @@ var RelayClient = class {
|
|
|
6291
6300
|
*/
|
|
6292
6301
|
async execute(contextId, method, argsJson = {}) {
|
|
6293
6302
|
const executor = this.executorAccount ?? (await this.describe(contextId)).executorAccount;
|
|
6294
|
-
const
|
|
6303
|
+
const nonce2 = await this.config.nonces.next();
|
|
6295
6304
|
const ttl = this.config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
|
|
6296
6305
|
const warrant = await signWarrant({
|
|
6297
6306
|
context: contextId,
|
|
@@ -6299,7 +6308,7 @@ var RelayClient = class {
|
|
|
6299
6308
|
executor,
|
|
6300
6309
|
method,
|
|
6301
6310
|
argsJson,
|
|
6302
|
-
nonce,
|
|
6311
|
+
nonce: nonce2,
|
|
6303
6312
|
notAfter: BigInt(Math.floor(Date.now() / 1e3)) + BigInt(ttl),
|
|
6304
6313
|
deviceSecret: this.config.deviceSecret
|
|
6305
6314
|
});
|
|
@@ -6489,14 +6498,10 @@ async function defaultNonceSource(deviceSecret) {
|
|
|
6489
6498
|
}
|
|
6490
6499
|
}
|
|
6491
6500
|
|
|
6492
|
-
// src/sealed/
|
|
6493
|
-
var
|
|
6494
|
-
var
|
|
6495
|
-
var
|
|
6496
|
-
var NONCE_LEN = 12;
|
|
6497
|
-
var REQUEST_DOMAIN = "calimero/sealed-http/v1/request";
|
|
6498
|
-
var RESPONSE_DOMAIN = "calimero/sealed-http/v1/response";
|
|
6499
|
-
var TRANSPORT_BINDING_DOMAIN = "calimero.tee-attest.transport-key.v1";
|
|
6501
|
+
// src/sealed/noise.ts
|
|
6502
|
+
var PROTOCOL_NAME = "Noise_NK_25519_AESGCM_SHA256";
|
|
6503
|
+
var DH_LEN = 32;
|
|
6504
|
+
var TAG_LEN = 16;
|
|
6500
6505
|
var PKCS8_X25519_PREFIX = new Uint8Array([
|
|
6501
6506
|
48,
|
|
6502
6507
|
46,
|
|
@@ -6515,12 +6520,147 @@ var PKCS8_X25519_PREFIX = new Uint8Array([
|
|
|
6515
6520
|
4,
|
|
6516
6521
|
32
|
|
6517
6522
|
]);
|
|
6523
|
+
var SymmetricState = class _SymmetricState {
|
|
6524
|
+
constructor(h) {
|
|
6525
|
+
this.k = null;
|
|
6526
|
+
this.n = 0;
|
|
6527
|
+
this.ck = h;
|
|
6528
|
+
this.h = h;
|
|
6529
|
+
}
|
|
6530
|
+
static async initialize() {
|
|
6531
|
+
const name = new TextEncoder().encode(PROTOCOL_NAME);
|
|
6532
|
+
const h = new Uint8Array(32);
|
|
6533
|
+
h.set(name);
|
|
6534
|
+
return new _SymmetricState(h);
|
|
6535
|
+
}
|
|
6536
|
+
async mixHash(data) {
|
|
6537
|
+
this.h = await sha256(concat(this.h, data));
|
|
6538
|
+
}
|
|
6539
|
+
async mixKey(inputKeyMaterial) {
|
|
6540
|
+
const [ck, key] = await hkdf2(this.ck, inputKeyMaterial);
|
|
6541
|
+
this.ck = ck;
|
|
6542
|
+
this.k = await crypto.subtle.importKey("raw", key, "AES-GCM", false, ["encrypt", "decrypt"]);
|
|
6543
|
+
this.n = 0;
|
|
6544
|
+
}
|
|
6545
|
+
async encryptAndHash(plaintext) {
|
|
6546
|
+
if (!this.k) throw new Error("Noise: no cipher key yet");
|
|
6547
|
+
const ciphertext = new Uint8Array(
|
|
6548
|
+
await crypto.subtle.encrypt(
|
|
6549
|
+
{ name: "AES-GCM", iv: noiseNonce(this.n), additionalData: this.h },
|
|
6550
|
+
this.k,
|
|
6551
|
+
plaintext
|
|
6552
|
+
)
|
|
6553
|
+
);
|
|
6554
|
+
this.n += 1;
|
|
6555
|
+
await this.mixHash(ciphertext);
|
|
6556
|
+
return ciphertext;
|
|
6557
|
+
}
|
|
6558
|
+
async decryptAndHash(ciphertext) {
|
|
6559
|
+
if (!this.k) throw new Error("Noise: no cipher key yet");
|
|
6560
|
+
const plaintext = new Uint8Array(
|
|
6561
|
+
await crypto.subtle.decrypt(
|
|
6562
|
+
{ name: "AES-GCM", iv: noiseNonce(this.n), additionalData: this.h },
|
|
6563
|
+
this.k,
|
|
6564
|
+
ciphertext
|
|
6565
|
+
)
|
|
6566
|
+
);
|
|
6567
|
+
this.n += 1;
|
|
6568
|
+
await this.mixHash(ciphertext);
|
|
6569
|
+
return plaintext;
|
|
6570
|
+
}
|
|
6571
|
+
/** The initiator-to-responder key, then the responder-to-initiator key. */
|
|
6572
|
+
split() {
|
|
6573
|
+
return hkdf2(this.ck, new Uint8Array());
|
|
6574
|
+
}
|
|
6575
|
+
};
|
|
6576
|
+
async function initiate(remoteStatic, prologue, fixedEphemeral) {
|
|
6577
|
+
const state = await SymmetricState.initialize();
|
|
6578
|
+
await state.mixHash(prologue);
|
|
6579
|
+
await state.mixHash(remoteStatic);
|
|
6580
|
+
const ephemeral = await x25519KeyPair(fixedEphemeral);
|
|
6581
|
+
await state.mixHash(ephemeral.publicKey);
|
|
6582
|
+
await state.mixKey(await dh(ephemeral.privateKey, remoteStatic));
|
|
6583
|
+
const message1 = concat(ephemeral.publicKey, await state.encryptAndHash(new Uint8Array()));
|
|
6584
|
+
return {
|
|
6585
|
+
message1,
|
|
6586
|
+
async finish(message2) {
|
|
6587
|
+
if (message2.length < DH_LEN + TAG_LEN) throw new Error("Noise: message 2 is too short");
|
|
6588
|
+
const remoteEphemeral = message2.slice(0, DH_LEN);
|
|
6589
|
+
await state.mixHash(remoteEphemeral);
|
|
6590
|
+
await state.mixKey(await dh(ephemeral.privateKey, remoteEphemeral));
|
|
6591
|
+
const payload = await state.decryptAndHash(message2.slice(DH_LEN));
|
|
6592
|
+
return { payload, split: await state.split() };
|
|
6593
|
+
}
|
|
6594
|
+
};
|
|
6595
|
+
}
|
|
6596
|
+
async function x25519KeyPair(secret) {
|
|
6597
|
+
if (!secret) {
|
|
6598
|
+
const pair = await crypto.subtle.generateKey({ name: "X25519" }, true, [
|
|
6599
|
+
"deriveBits"
|
|
6600
|
+
]);
|
|
6601
|
+
const publicKey = new Uint8Array(await crypto.subtle.exportKey("raw", pair.publicKey));
|
|
6602
|
+
return { privateKey: pair.privateKey, publicKey };
|
|
6603
|
+
}
|
|
6604
|
+
const privateKey = await crypto.subtle.importKey(
|
|
6605
|
+
"pkcs8",
|
|
6606
|
+
concat(PKCS8_X25519_PREFIX, secret),
|
|
6607
|
+
{ name: "X25519" },
|
|
6608
|
+
true,
|
|
6609
|
+
["deriveBits"]
|
|
6610
|
+
);
|
|
6611
|
+
const jwk = await crypto.subtle.exportKey("jwk", privateKey);
|
|
6612
|
+
return { privateKey, publicKey: base64UrlDecode(jwk.x) };
|
|
6613
|
+
}
|
|
6614
|
+
async function dh(privateKey, publicKey) {
|
|
6615
|
+
const peer = await crypto.subtle.importKey("raw", publicKey, { name: "X25519" }, false, []);
|
|
6616
|
+
return new Uint8Array(
|
|
6617
|
+
await crypto.subtle.deriveBits({ name: "X25519", public: peer }, privateKey, 256)
|
|
6618
|
+
);
|
|
6619
|
+
}
|
|
6620
|
+
function noiseNonce(n) {
|
|
6621
|
+
const nonce2 = new Uint8Array(12);
|
|
6622
|
+
new DataView(nonce2.buffer).setBigUint64(4, BigInt(n), false);
|
|
6623
|
+
return nonce2;
|
|
6624
|
+
}
|
|
6625
|
+
async function sha256(data) {
|
|
6626
|
+
return new Uint8Array(await crypto.subtle.digest("SHA-256", data));
|
|
6627
|
+
}
|
|
6628
|
+
async function hmacSha256(key, data) {
|
|
6629
|
+
const hmacKey = await crypto.subtle.importKey("raw", key, { name: "HMAC", hash: "SHA-256" }, false, [
|
|
6630
|
+
"sign"
|
|
6631
|
+
]);
|
|
6632
|
+
return new Uint8Array(await crypto.subtle.sign("HMAC", hmacKey, data));
|
|
6633
|
+
}
|
|
6634
|
+
async function hkdf2(chainingKey, inputKeyMaterial) {
|
|
6635
|
+
const temp = await hmacSha256(chainingKey, inputKeyMaterial);
|
|
6636
|
+
const first = await hmacSha256(temp, new Uint8Array([1]));
|
|
6637
|
+
const second = await hmacSha256(temp, concat(first, new Uint8Array([2])));
|
|
6638
|
+
return [first, second];
|
|
6639
|
+
}
|
|
6640
|
+
function base64UrlDecode(value) {
|
|
6641
|
+
const base643 = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
6642
|
+
const binary = atob(base643 + "=".repeat((4 - base643.length % 4) % 4));
|
|
6643
|
+
return Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
6644
|
+
}
|
|
6645
|
+
|
|
6646
|
+
// src/sealed/sealed.ts
|
|
6647
|
+
var HANDSHAKE_PATH = "/sealed/v2/handshake";
|
|
6648
|
+
var SEALED_PATH = "/sealed/v2";
|
|
6649
|
+
var SEALED_CONTENT_TYPE = "application/vnd.calimero.sealed";
|
|
6650
|
+
var VERSION = 2;
|
|
6651
|
+
var PROLOGUE = new TextEncoder().encode("calimero/sealed-http/v2");
|
|
6652
|
+
var SESSION_ID_LEN = 16;
|
|
6653
|
+
var HEADER_LEN = 1 + SESSION_ID_LEN + 8;
|
|
6654
|
+
var FRAME_HEAD = 0;
|
|
6655
|
+
var FRAME_DATA = 1;
|
|
6656
|
+
var FRAME_END = 2;
|
|
6657
|
+
var RENEW_BEFORE_MS = 6e4;
|
|
6658
|
+
var TRANSPORT_BINDING_DOMAIN = "calimero.tee-attest.transport-key.v1";
|
|
6659
|
+
var NULL_BODY_STATUSES = [101, 204, 205, 304];
|
|
6518
6660
|
var utf8 = new TextEncoder();
|
|
6519
6661
|
var StaleTransportKeyError = class extends Error {
|
|
6520
6662
|
constructor() {
|
|
6521
|
-
super(
|
|
6522
|
-
"The node no longer holds the transport key this request was sealed to; attest again for the current one"
|
|
6523
|
-
);
|
|
6663
|
+
super("The node no longer holds the attested transport key; attest again for the current one");
|
|
6524
6664
|
this.name = "StaleTransportKeyError";
|
|
6525
6665
|
}
|
|
6526
6666
|
};
|
|
@@ -6543,9 +6683,9 @@ async function fetchAttestedTransportKey(admin, verify, options = {}) {
|
|
|
6543
6683
|
if (options.applicationId === void 0 !== (options.applicationHash === void 0)) {
|
|
6544
6684
|
throw new Error("applicationId and applicationHash are given together or not at all");
|
|
6545
6685
|
}
|
|
6546
|
-
const
|
|
6686
|
+
const nonce2 = hex(crypto.getRandomValues(new Uint8Array(32)));
|
|
6547
6687
|
const attested = await admin.teeAttest({
|
|
6548
|
-
nonce,
|
|
6688
|
+
nonce: nonce2,
|
|
6549
6689
|
applicationId: options.applicationId,
|
|
6550
6690
|
bindTransportKey: true
|
|
6551
6691
|
});
|
|
@@ -6555,7 +6695,7 @@ async function fetchAttestedTransportKey(admin, verify, options = {}) {
|
|
|
6555
6695
|
const transportKey = fromHex(attested.transportPublicKey, "transportPublicKey", 32);
|
|
6556
6696
|
const inner = options.applicationHash ? fromHex(options.applicationHash, "applicationHash", 32) : new Uint8Array(32);
|
|
6557
6697
|
const reportDataSuffix = hex(await transportKeyBinding(inner, transportKey));
|
|
6558
|
-
if (!await verify({ quoteB64: attested.quoteB64, nonce, reportDataSuffix })) {
|
|
6698
|
+
if (!await verify({ quoteB64: attested.quoteB64, nonce: nonce2, reportDataSuffix })) {
|
|
6559
6699
|
throw new Error("The attestation did not verify, so its transport key is not trusted");
|
|
6560
6700
|
}
|
|
6561
6701
|
return transportKey;
|
|
@@ -6563,12 +6703,37 @@ async function fetchAttestedTransportKey(admin, verify, options = {}) {
|
|
|
6563
6703
|
function createSealedFetch(options) {
|
|
6564
6704
|
const base = new URL(options.baseUrl);
|
|
6565
6705
|
const basePath = base.pathname.replace(/\/+$/, "");
|
|
6566
|
-
const
|
|
6567
|
-
const transportKey = options.transportPublicKey;
|
|
6568
|
-
if (transportKey.length !== 32) {
|
|
6569
|
-
throw new Error("transportPublicKey must be 32 bytes");
|
|
6570
|
-
}
|
|
6706
|
+
const nodeUrl = `${base.origin}${basePath}`;
|
|
6571
6707
|
const baseFetch = options.fetch ?? ((input, init) => fetch(input, init));
|
|
6708
|
+
const attest = typeof options.transportPublicKey === "function" ? options.transportPublicKey : void 0;
|
|
6709
|
+
let transportKey = attest ? Promise.resolve().then(attest) : Promise.resolve(options.transportPublicKey);
|
|
6710
|
+
let session;
|
|
6711
|
+
const openSession = async () => {
|
|
6712
|
+
try {
|
|
6713
|
+
return await handshake(baseFetch, nodeUrl, await checkedKey(transportKey));
|
|
6714
|
+
} catch (error) {
|
|
6715
|
+
if (!(error instanceof StaleTransportKeyError) || !attest) throw error;
|
|
6716
|
+
transportKey = Promise.resolve().then(attest);
|
|
6717
|
+
return handshake(baseFetch, nodeUrl, await checkedKey(transportKey));
|
|
6718
|
+
}
|
|
6719
|
+
};
|
|
6720
|
+
const currentSession = async () => {
|
|
6721
|
+
const pending = session ?? renew(void 0);
|
|
6722
|
+
const open = await pending;
|
|
6723
|
+
if (Date.now() < open.renewAt) return [open, pending];
|
|
6724
|
+
const renewed = renew(pending);
|
|
6725
|
+
return [await renewed, renewed];
|
|
6726
|
+
};
|
|
6727
|
+
const renew = (stale) => {
|
|
6728
|
+
if (session === stale) {
|
|
6729
|
+
const opening = openSession();
|
|
6730
|
+
session = opening;
|
|
6731
|
+
opening.catch(() => {
|
|
6732
|
+
if (session === opening) session = void 0;
|
|
6733
|
+
});
|
|
6734
|
+
}
|
|
6735
|
+
return session;
|
|
6736
|
+
};
|
|
6572
6737
|
return async (input, init) => {
|
|
6573
6738
|
const request = new Request(input, init);
|
|
6574
6739
|
const url = new URL(request.url);
|
|
@@ -6577,126 +6742,190 @@ function createSealedFetch(options) {
|
|
|
6577
6742
|
}
|
|
6578
6743
|
const headers = [];
|
|
6579
6744
|
request.headers.forEach((value, name) => headers.push([name, value]));
|
|
6580
|
-
const head = {
|
|
6581
|
-
method: request.method,
|
|
6582
|
-
path: url.pathname.slice(basePath.length) + url.search,
|
|
6583
|
-
headers,
|
|
6584
|
-
ts: Math.floor(Date.now() / 1e3)
|
|
6585
|
-
};
|
|
6745
|
+
const head = { method: request.method, path: url.pathname.slice(basePath.length) + url.search, headers };
|
|
6586
6746
|
const body = new Uint8Array(await request.arrayBuffer());
|
|
6587
|
-
const
|
|
6588
|
-
|
|
6589
|
-
|
|
6590
|
-
|
|
6591
|
-
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
6747
|
+
const send = async (open2) => {
|
|
6748
|
+
const { requestId, header, envelope } = await sealRequest(open2, head, body);
|
|
6749
|
+
const response = await baseFetch(`${nodeUrl}${SEALED_PATH}`, {
|
|
6750
|
+
method: "POST",
|
|
6751
|
+
headers: { "content-type": SEALED_CONTENT_TYPE },
|
|
6752
|
+
body: envelope,
|
|
6753
|
+
signal: request.signal
|
|
6754
|
+
});
|
|
6755
|
+
if (response.headers.get("content-type") !== SEALED_CONTENT_TYPE) {
|
|
6756
|
+
throw await envelopeRefusal(response);
|
|
6757
|
+
}
|
|
6758
|
+
return openResponse(response, open2.responseKey, header, requestId);
|
|
6759
|
+
};
|
|
6760
|
+
const [open, pending] = await currentSession();
|
|
6761
|
+
try {
|
|
6762
|
+
return await send(open);
|
|
6763
|
+
} catch (error) {
|
|
6764
|
+
if (!(error instanceof SealedTransportError) || error.code !== "unknown_session") throw error;
|
|
6765
|
+
return send(await renew(pending));
|
|
6596
6766
|
}
|
|
6597
|
-
const opened = await exchange.open(new Uint8Array(await response.arrayBuffer()));
|
|
6598
|
-
const nullBody = [101, 204, 205, 304].includes(opened.head.status);
|
|
6599
|
-
return new Response(nullBody ? null : opened.body, {
|
|
6600
|
-
status: opened.head.status,
|
|
6601
|
-
headers: opened.head.headers
|
|
6602
|
-
});
|
|
6603
6767
|
};
|
|
6604
6768
|
}
|
|
6605
|
-
async function
|
|
6606
|
-
|
|
6607
|
-
|
|
6608
|
-
|
|
6609
|
-
|
|
6610
|
-
|
|
6611
|
-
|
|
6612
|
-
|
|
6769
|
+
async function checkedKey(key) {
|
|
6770
|
+
const bytes = await key;
|
|
6771
|
+
if (bytes.length !== 32) throw new Error("transportPublicKey must be 32 bytes");
|
|
6772
|
+
return bytes;
|
|
6773
|
+
}
|
|
6774
|
+
async function handshake(baseFetch, nodeUrl, transportKey) {
|
|
6775
|
+
const noise = await initiate(transportKey, PROLOGUE);
|
|
6776
|
+
const response = await baseFetch(`${nodeUrl}${HANDSHAKE_PATH}`, {
|
|
6777
|
+
method: "POST",
|
|
6778
|
+
headers: { "content-type": SEALED_CONTENT_TYPE },
|
|
6779
|
+
body: concat(new Uint8Array([VERSION]), transportKey, noise.message1)
|
|
6780
|
+
});
|
|
6781
|
+
if (response.headers.get("content-type") !== SEALED_CONTENT_TYPE) {
|
|
6782
|
+
throw await envelopeRefusal(response);
|
|
6613
6783
|
}
|
|
6614
|
-
|
|
6615
|
-
return
|
|
6784
|
+
const reply = new Uint8Array(await response.arrayBuffer());
|
|
6785
|
+
return sessionFrom(noise, reply);
|
|
6616
6786
|
}
|
|
6617
|
-
async function
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
await
|
|
6622
|
-
|
|
6623
|
-
|
|
6624
|
-
|
|
6625
|
-
}
|
|
6626
|
-
|
|
6627
|
-
const
|
|
6628
|
-
const
|
|
6629
|
-
const
|
|
6630
|
-
|
|
6631
|
-
|
|
6632
|
-
|
|
6633
|
-
|
|
6634
|
-
|
|
6635
|
-
);
|
|
6636
|
-
const responseAad = concat(new Uint8Array([VERSION]), salt);
|
|
6787
|
+
async function sessionFrom(noise, reply) {
|
|
6788
|
+
if (reply[0] !== VERSION) throw new Error("The handshake reply is malformed");
|
|
6789
|
+
let finished;
|
|
6790
|
+
try {
|
|
6791
|
+
finished = await noise.finish(reply.slice(1));
|
|
6792
|
+
} catch {
|
|
6793
|
+
throw new Error("The handshake reply did not open: it did not come from the attested node");
|
|
6794
|
+
}
|
|
6795
|
+
const { payload, split } = finished;
|
|
6796
|
+
if (payload.length !== SESSION_ID_LEN + 4) throw new Error("The handshake reply is malformed");
|
|
6797
|
+
const sessionId = payload.slice(0, SESSION_ID_LEN);
|
|
6798
|
+
const lifetimeSecs = new DataView(payload.buffer, payload.byteOffset).getUint32(SESSION_ID_LEN, false);
|
|
6799
|
+
const [requestKey, responseKey] = await Promise.all([
|
|
6800
|
+
crypto.subtle.importKey("raw", split[0], "AES-GCM", false, ["encrypt"]),
|
|
6801
|
+
crypto.subtle.importKey("raw", split[1], "AES-GCM", false, ["decrypt"])
|
|
6802
|
+
]);
|
|
6803
|
+
split[0].fill(0);
|
|
6804
|
+
split[1].fill(0);
|
|
6637
6805
|
return {
|
|
6638
|
-
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
6642
|
-
|
|
6643
|
-
|
|
6644
|
-
|
|
6645
|
-
|
|
6646
|
-
|
|
6647
|
-
|
|
6648
|
-
responseKey,
|
|
6649
|
-
sealed.slice(1 + NONCE_LEN)
|
|
6650
|
-
)
|
|
6651
|
-
);
|
|
6652
|
-
} catch {
|
|
6653
|
-
throw new Error("The sealed response did not open: it was not sealed by the attested node");
|
|
6654
|
-
}
|
|
6655
|
-
const view = new DataView(plaintext.buffer, plaintext.byteOffset, plaintext.byteLength);
|
|
6656
|
-
const len = view.getUint32(0, false);
|
|
6657
|
-
const head2 = JSON.parse(new TextDecoder().decode(plaintext.slice(4, 4 + len)));
|
|
6658
|
-
return { head: head2, body: plaintext.slice(4 + len) };
|
|
6806
|
+
requestKey,
|
|
6807
|
+
responseKey,
|
|
6808
|
+
nextRequestId: 1,
|
|
6809
|
+
renewAt: Date.now() + lifetimeSecs * 1e3 - RENEW_BEFORE_MS,
|
|
6810
|
+
header(requestId) {
|
|
6811
|
+
const header = new Uint8Array(HEADER_LEN);
|
|
6812
|
+
header[0] = VERSION;
|
|
6813
|
+
header.set(sessionId, 1);
|
|
6814
|
+
new DataView(header.buffer).setBigUint64(1 + SESSION_ID_LEN, BigInt(requestId), false);
|
|
6815
|
+
return header;
|
|
6659
6816
|
}
|
|
6660
6817
|
};
|
|
6661
6818
|
}
|
|
6819
|
+
async function sealRequest(session, head, body) {
|
|
6820
|
+
const requestId = session.nextRequestId;
|
|
6821
|
+
session.nextRequestId += 1;
|
|
6822
|
+
const header = session.header(requestId);
|
|
6823
|
+
const sealed = await crypto.subtle.encrypt(
|
|
6824
|
+
{ name: "AES-GCM", iv: nonce(requestId, 0), additionalData: header },
|
|
6825
|
+
session.requestKey,
|
|
6826
|
+
joinInner(head, body)
|
|
6827
|
+
);
|
|
6828
|
+
return { requestId, header, envelope: concat(header, new Uint8Array(sealed)) };
|
|
6829
|
+
}
|
|
6830
|
+
function nonce(requestId, index) {
|
|
6831
|
+
const bytes = new Uint8Array(12);
|
|
6832
|
+
const view = new DataView(bytes.buffer);
|
|
6833
|
+
view.setBigUint64(0, BigInt(requestId), false);
|
|
6834
|
+
view.setUint32(8, index, false);
|
|
6835
|
+
return bytes;
|
|
6836
|
+
}
|
|
6662
6837
|
function joinInner(head, body) {
|
|
6663
6838
|
const encoded = utf8.encode(JSON.stringify(head));
|
|
6664
6839
|
const len = new Uint8Array(4);
|
|
6665
6840
|
new DataView(len.buffer).setUint32(0, encoded.length, false);
|
|
6666
6841
|
return concat(len, encoded, body);
|
|
6667
6842
|
}
|
|
6668
|
-
async function
|
|
6669
|
-
|
|
6670
|
-
|
|
6671
|
-
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
|
|
6676
|
-
|
|
6843
|
+
async function openResponse(response, responseKey, header, requestId) {
|
|
6844
|
+
if (!response.body) throw new Error("The sealed response has no body");
|
|
6845
|
+
const frames = frameReader(response.body.getReader());
|
|
6846
|
+
let index = 0;
|
|
6847
|
+
const next = async () => {
|
|
6848
|
+
const sealed = await frames.next();
|
|
6849
|
+
if (!sealed) return null;
|
|
6850
|
+
let plain;
|
|
6851
|
+
try {
|
|
6852
|
+
plain = new Uint8Array(
|
|
6853
|
+
await crypto.subtle.decrypt(
|
|
6854
|
+
{ name: "AES-GCM", iv: nonce(requestId, index), additionalData: header },
|
|
6855
|
+
responseKey,
|
|
6856
|
+
sealed
|
|
6857
|
+
)
|
|
6858
|
+
);
|
|
6859
|
+
} catch {
|
|
6860
|
+
throw new Error("A sealed response frame did not open: it was not sealed by the attested node");
|
|
6861
|
+
}
|
|
6862
|
+
index += 1;
|
|
6863
|
+
return { kind: plain[0], data: plain.slice(1) };
|
|
6864
|
+
};
|
|
6865
|
+
const first = await next();
|
|
6866
|
+
if (!first || first.kind !== FRAME_HEAD) throw new Error("The sealed response is malformed");
|
|
6867
|
+
const head = JSON.parse(new TextDecoder().decode(first.data));
|
|
6868
|
+
if (NULL_BODY_STATUSES.includes(head.status)) {
|
|
6869
|
+
await frames.cancel();
|
|
6870
|
+
return new Response(null, { status: head.status, headers: head.headers });
|
|
6871
|
+
}
|
|
6872
|
+
const body = new ReadableStream({
|
|
6873
|
+
async pull(controller) {
|
|
6874
|
+
try {
|
|
6875
|
+
const frame = await next();
|
|
6876
|
+
if (!frame) {
|
|
6877
|
+
controller.error(new Error("The sealed response was cut short"));
|
|
6878
|
+
} else if (frame.kind === FRAME_DATA) {
|
|
6879
|
+
controller.enqueue(frame.data);
|
|
6880
|
+
} else if (frame.kind === FRAME_END) {
|
|
6881
|
+
controller.close();
|
|
6882
|
+
await frames.cancel();
|
|
6883
|
+
} else {
|
|
6884
|
+
controller.error(new Error("The sealed response is malformed"));
|
|
6885
|
+
}
|
|
6886
|
+
} catch (error) {
|
|
6887
|
+
controller.error(error);
|
|
6888
|
+
}
|
|
6889
|
+
},
|
|
6890
|
+
cancel() {
|
|
6891
|
+
return frames.cancel();
|
|
6892
|
+
}
|
|
6893
|
+
});
|
|
6894
|
+
return new Response(body, { status: head.status, headers: head.headers });
|
|
6677
6895
|
}
|
|
6678
|
-
|
|
6679
|
-
|
|
6680
|
-
|
|
6681
|
-
|
|
6682
|
-
|
|
6683
|
-
|
|
6684
|
-
|
|
6685
|
-
|
|
6686
|
-
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
|
-
|
|
6692
|
-
|
|
6693
|
-
|
|
6694
|
-
|
|
6896
|
+
function frameReader(reader) {
|
|
6897
|
+
let buffer = new Uint8Array();
|
|
6898
|
+
const fill = async (length) => {
|
|
6899
|
+
while (buffer.length < length) {
|
|
6900
|
+
const { done, value } = await reader.read();
|
|
6901
|
+
if (done) return false;
|
|
6902
|
+
buffer = concat(buffer, value);
|
|
6903
|
+
}
|
|
6904
|
+
return true;
|
|
6905
|
+
};
|
|
6906
|
+
return {
|
|
6907
|
+
async next() {
|
|
6908
|
+
if (!await fill(4)) return null;
|
|
6909
|
+
const length = new DataView(buffer.buffer, buffer.byteOffset).getUint32(0, false);
|
|
6910
|
+
if (!await fill(4 + length)) return null;
|
|
6911
|
+
const frame = buffer.slice(4, 4 + length);
|
|
6912
|
+
buffer = buffer.slice(4 + length);
|
|
6913
|
+
return frame;
|
|
6914
|
+
},
|
|
6915
|
+
cancel: () => reader.cancel()
|
|
6916
|
+
};
|
|
6695
6917
|
}
|
|
6696
|
-
function
|
|
6697
|
-
|
|
6698
|
-
|
|
6699
|
-
|
|
6918
|
+
async function envelopeRefusal(response) {
|
|
6919
|
+
let code;
|
|
6920
|
+
let message = `Sealed request refused: HTTP ${response.status}`;
|
|
6921
|
+
try {
|
|
6922
|
+
const body = await response.json();
|
|
6923
|
+
code = body?.error?.code;
|
|
6924
|
+
if (body?.error?.message) message = `Sealed request refused: ${body.error.message}`;
|
|
6925
|
+
} catch {
|
|
6926
|
+
}
|
|
6927
|
+
if (code === "stale_transport_key") return new StaleTransportKeyError();
|
|
6928
|
+
return new SealedTransportError(response.status, code, message);
|
|
6700
6929
|
}
|
|
6701
6930
|
|
|
6702
6931
|
// src/namespace-op/namespace-op.ts
|
|
@@ -7033,6 +7262,7 @@ export {
|
|
|
7033
7262
|
CloudClient,
|
|
7034
7263
|
DEFAULT_LOCAL_NODE_PORTS,
|
|
7035
7264
|
EphemeralClient,
|
|
7265
|
+
HANDSHAKE_PATH,
|
|
7036
7266
|
HTTPError,
|
|
7037
7267
|
IntentRefusedError,
|
|
7038
7268
|
LocalStorageTokenStore,
|