@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.cjs
CHANGED
|
@@ -26,6 +26,7 @@ __export(index_exports, {
|
|
|
26
26
|
CloudClient: () => CloudClient,
|
|
27
27
|
DEFAULT_LOCAL_NODE_PORTS: () => DEFAULT_LOCAL_NODE_PORTS,
|
|
28
28
|
EphemeralClient: () => EphemeralClient,
|
|
29
|
+
HANDSHAKE_PATH: () => HANDSHAKE_PATH,
|
|
29
30
|
HTTPError: () => HTTPError,
|
|
30
31
|
IntentRefusedError: () => IntentRefusedError,
|
|
31
32
|
LocalStorageTokenStore: () => LocalStorageTokenStore,
|
|
@@ -1782,6 +1783,15 @@ var AdminApiClient = class {
|
|
|
1782
1783
|
async setTeeAuthoringPolicy(groupId, request) {
|
|
1783
1784
|
await this.httpClient.put(`/admin-api/groups/${groupId}/settings/tee-authoring-policy`, request);
|
|
1784
1785
|
}
|
|
1786
|
+
/**
|
|
1787
|
+
* Turns TEE authorship off in the namespace: every TEE loses its authority,
|
|
1788
|
+
* and admitted TEEs stay members. The node applies the same op as
|
|
1789
|
+
* `setTeeAuthoringPolicy(groupId, { allowedMrtd: [] })`. `groupId` must be a
|
|
1790
|
+
* namespace root.
|
|
1791
|
+
*/
|
|
1792
|
+
async disableTeeAuthoringPolicy(groupId) {
|
|
1793
|
+
await this.httpClient.delete(`/admin-api/groups/${groupId}/settings/tee-authoring-policy`);
|
|
1794
|
+
}
|
|
1785
1795
|
// ---- Group / member / context metadata ----
|
|
1786
1796
|
async setGroupMetadata(groupId, request) {
|
|
1787
1797
|
await this.httpClient.put(`/admin-api/groups/${groupId}/metadata`, request);
|
|
@@ -2862,12 +2872,12 @@ var CERT_DOMAIN = new TextEncoder().encode("calimero.device.cert.v1");
|
|
|
2862
2872
|
var ACCOUNT_ID_DOMAIN = new TextEncoder().encode("calimero.account.genesis.v1");
|
|
2863
2873
|
var DEVICE_ID_DOMAIN = new TextEncoder().encode("calimero.device.id.v1");
|
|
2864
2874
|
var ACCOUNT_GENESIS_VERSION = 2;
|
|
2865
|
-
async function mintDeviceId(account,
|
|
2866
|
-
if (
|
|
2867
|
-
throw new Error(`nonce must be 16 bytes, got ${
|
|
2875
|
+
async function mintDeviceId(account, nonce2) {
|
|
2876
|
+
if (nonce2.length !== 16) {
|
|
2877
|
+
throw new Error(`nonce must be 16 bytes, got ${nonce2.length}`);
|
|
2868
2878
|
}
|
|
2869
2879
|
const accountBytes = fromHex(account, "account", 32);
|
|
2870
|
-
return hex(await domainHash(DEVICE_ID_DOMAIN, [accountBytes,
|
|
2880
|
+
return hex(await domainHash(DEVICE_ID_DOMAIN, [accountBytes, nonce2]));
|
|
2871
2881
|
}
|
|
2872
2882
|
async function deviceCertPayload(input) {
|
|
2873
2883
|
return domainHash(CERT_DOMAIN, [
|
|
@@ -5097,12 +5107,12 @@ function base64(bytes) {
|
|
|
5097
5107
|
var ROUTING_PROOF_DOMAIN = new TextEncoder().encode(
|
|
5098
5108
|
"calimero.mdma.routing-read.v1\0"
|
|
5099
5109
|
);
|
|
5100
|
-
async function signRoutingChallenge(
|
|
5110
|
+
async function signRoutingChallenge(nonce2, deviceSecret) {
|
|
5101
5111
|
fromHex(deviceSecret, "deviceSecret", 32);
|
|
5102
5112
|
const key = await importSigningKey(deviceSecret);
|
|
5103
5113
|
const message = concat(
|
|
5104
5114
|
ROUTING_PROOF_DOMAIN,
|
|
5105
|
-
new TextEncoder().encode(
|
|
5115
|
+
new TextEncoder().encode(nonce2)
|
|
5106
5116
|
);
|
|
5107
5117
|
const signature = new Uint8Array(
|
|
5108
5118
|
await crypto.subtle.sign({ name: "Ed25519" }, key, message)
|
|
@@ -5192,11 +5202,11 @@ var CloudClient = class {
|
|
|
5192
5202
|
*/
|
|
5193
5203
|
async signInWithAccount(rootSecret) {
|
|
5194
5204
|
const root = await accountRootFromSecret(rootSecret);
|
|
5195
|
-
const { nonce } = await this.getAccountLoginChallenge();
|
|
5205
|
+
const { nonce: nonce2 } = await this.getAccountLoginChallenge();
|
|
5196
5206
|
return this.submitAccountLogin({
|
|
5197
5207
|
rootPublicKey: root.publicKey,
|
|
5198
|
-
nonce,
|
|
5199
|
-
signature: await signAccountLogin({ rootSecret, nonce })
|
|
5208
|
+
nonce: nonce2,
|
|
5209
|
+
signature: await signAccountLogin({ rootSecret, nonce: nonce2 })
|
|
5200
5210
|
});
|
|
5201
5211
|
}
|
|
5202
5212
|
/**
|
|
@@ -5615,12 +5625,12 @@ var CloudClient = class {
|
|
|
5615
5625
|
*/
|
|
5616
5626
|
async linkAccount(rootSecret) {
|
|
5617
5627
|
const root = await accountRootFromSecret(rootSecret);
|
|
5618
|
-
const { nonce } = await this.getAccountLinkChallenge();
|
|
5628
|
+
const { nonce: nonce2 } = await this.getAccountLinkChallenge();
|
|
5619
5629
|
return this.submitAccountLink({
|
|
5620
5630
|
accountId: root.accountId,
|
|
5621
5631
|
rootPublicKey: root.publicKey,
|
|
5622
|
-
nonce,
|
|
5623
|
-
signature: await signAccountLink({ rootSecret, nonce })
|
|
5632
|
+
nonce: nonce2,
|
|
5633
|
+
signature: await signAccountLink({ rootSecret, nonce: nonce2 })
|
|
5624
5634
|
});
|
|
5625
5635
|
}
|
|
5626
5636
|
/**
|
|
@@ -6305,7 +6315,7 @@ async function signWarrant(input) {
|
|
|
6305
6315
|
const publicKey = await derivePublicKey(input.deviceSecret);
|
|
6306
6316
|
const method = new TextEncoder().encode(input.method);
|
|
6307
6317
|
const commitment = await intentHash(input.method, input.argsJson);
|
|
6308
|
-
const
|
|
6318
|
+
const nonce2 = u64le(input.nonce);
|
|
6309
6319
|
const notAfter = u64le(input.notAfter);
|
|
6310
6320
|
const preimage = await domainHash(SIGN_DOMAIN2, [
|
|
6311
6321
|
context,
|
|
@@ -6319,7 +6329,7 @@ async function signWarrant(input) {
|
|
|
6319
6329
|
...accountHeads,
|
|
6320
6330
|
u64le(governanceFloor.length),
|
|
6321
6331
|
...governanceFloor,
|
|
6322
|
-
|
|
6332
|
+
nonce2,
|
|
6323
6333
|
notAfter
|
|
6324
6334
|
]);
|
|
6325
6335
|
const signature = new Uint8Array(
|
|
@@ -6339,7 +6349,7 @@ async function signWarrant(input) {
|
|
|
6339
6349
|
...accountHeads,
|
|
6340
6350
|
u32le(governanceFloor.length),
|
|
6341
6351
|
...governanceFloor,
|
|
6342
|
-
|
|
6352
|
+
nonce2,
|
|
6343
6353
|
notAfter,
|
|
6344
6354
|
signature
|
|
6345
6355
|
)
|
|
@@ -6396,7 +6406,7 @@ var RelayClient = class {
|
|
|
6396
6406
|
*/
|
|
6397
6407
|
async execute(contextId, method, argsJson = {}) {
|
|
6398
6408
|
const executor = this.executorAccount ?? (await this.describe(contextId)).executorAccount;
|
|
6399
|
-
const
|
|
6409
|
+
const nonce2 = await this.config.nonces.next();
|
|
6400
6410
|
const ttl = this.config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
|
|
6401
6411
|
const warrant = await signWarrant({
|
|
6402
6412
|
context: contextId,
|
|
@@ -6404,7 +6414,7 @@ var RelayClient = class {
|
|
|
6404
6414
|
executor,
|
|
6405
6415
|
method,
|
|
6406
6416
|
argsJson,
|
|
6407
|
-
nonce,
|
|
6417
|
+
nonce: nonce2,
|
|
6408
6418
|
notAfter: BigInt(Math.floor(Date.now() / 1e3)) + BigInt(ttl),
|
|
6409
6419
|
deviceSecret: this.config.deviceSecret
|
|
6410
6420
|
});
|
|
@@ -6594,14 +6604,10 @@ async function defaultNonceSource(deviceSecret) {
|
|
|
6594
6604
|
}
|
|
6595
6605
|
}
|
|
6596
6606
|
|
|
6597
|
-
// src/sealed/
|
|
6598
|
-
var
|
|
6599
|
-
var
|
|
6600
|
-
var
|
|
6601
|
-
var NONCE_LEN = 12;
|
|
6602
|
-
var REQUEST_DOMAIN = "calimero/sealed-http/v1/request";
|
|
6603
|
-
var RESPONSE_DOMAIN = "calimero/sealed-http/v1/response";
|
|
6604
|
-
var TRANSPORT_BINDING_DOMAIN = "calimero.tee-attest.transport-key.v1";
|
|
6607
|
+
// src/sealed/noise.ts
|
|
6608
|
+
var PROTOCOL_NAME = "Noise_NK_25519_AESGCM_SHA256";
|
|
6609
|
+
var DH_LEN = 32;
|
|
6610
|
+
var TAG_LEN = 16;
|
|
6605
6611
|
var PKCS8_X25519_PREFIX = new Uint8Array([
|
|
6606
6612
|
48,
|
|
6607
6613
|
46,
|
|
@@ -6620,12 +6626,147 @@ var PKCS8_X25519_PREFIX = new Uint8Array([
|
|
|
6620
6626
|
4,
|
|
6621
6627
|
32
|
|
6622
6628
|
]);
|
|
6629
|
+
var SymmetricState = class _SymmetricState {
|
|
6630
|
+
constructor(h) {
|
|
6631
|
+
this.k = null;
|
|
6632
|
+
this.n = 0;
|
|
6633
|
+
this.ck = h;
|
|
6634
|
+
this.h = h;
|
|
6635
|
+
}
|
|
6636
|
+
static async initialize() {
|
|
6637
|
+
const name = new TextEncoder().encode(PROTOCOL_NAME);
|
|
6638
|
+
const h = new Uint8Array(32);
|
|
6639
|
+
h.set(name);
|
|
6640
|
+
return new _SymmetricState(h);
|
|
6641
|
+
}
|
|
6642
|
+
async mixHash(data) {
|
|
6643
|
+
this.h = await sha256(concat(this.h, data));
|
|
6644
|
+
}
|
|
6645
|
+
async mixKey(inputKeyMaterial) {
|
|
6646
|
+
const [ck, key] = await hkdf2(this.ck, inputKeyMaterial);
|
|
6647
|
+
this.ck = ck;
|
|
6648
|
+
this.k = await crypto.subtle.importKey("raw", key, "AES-GCM", false, ["encrypt", "decrypt"]);
|
|
6649
|
+
this.n = 0;
|
|
6650
|
+
}
|
|
6651
|
+
async encryptAndHash(plaintext) {
|
|
6652
|
+
if (!this.k) throw new Error("Noise: no cipher key yet");
|
|
6653
|
+
const ciphertext = new Uint8Array(
|
|
6654
|
+
await crypto.subtle.encrypt(
|
|
6655
|
+
{ name: "AES-GCM", iv: noiseNonce(this.n), additionalData: this.h },
|
|
6656
|
+
this.k,
|
|
6657
|
+
plaintext
|
|
6658
|
+
)
|
|
6659
|
+
);
|
|
6660
|
+
this.n += 1;
|
|
6661
|
+
await this.mixHash(ciphertext);
|
|
6662
|
+
return ciphertext;
|
|
6663
|
+
}
|
|
6664
|
+
async decryptAndHash(ciphertext) {
|
|
6665
|
+
if (!this.k) throw new Error("Noise: no cipher key yet");
|
|
6666
|
+
const plaintext = new Uint8Array(
|
|
6667
|
+
await crypto.subtle.decrypt(
|
|
6668
|
+
{ name: "AES-GCM", iv: noiseNonce(this.n), additionalData: this.h },
|
|
6669
|
+
this.k,
|
|
6670
|
+
ciphertext
|
|
6671
|
+
)
|
|
6672
|
+
);
|
|
6673
|
+
this.n += 1;
|
|
6674
|
+
await this.mixHash(ciphertext);
|
|
6675
|
+
return plaintext;
|
|
6676
|
+
}
|
|
6677
|
+
/** The initiator-to-responder key, then the responder-to-initiator key. */
|
|
6678
|
+
split() {
|
|
6679
|
+
return hkdf2(this.ck, new Uint8Array());
|
|
6680
|
+
}
|
|
6681
|
+
};
|
|
6682
|
+
async function initiate(remoteStatic, prologue, fixedEphemeral) {
|
|
6683
|
+
const state = await SymmetricState.initialize();
|
|
6684
|
+
await state.mixHash(prologue);
|
|
6685
|
+
await state.mixHash(remoteStatic);
|
|
6686
|
+
const ephemeral = await x25519KeyPair(fixedEphemeral);
|
|
6687
|
+
await state.mixHash(ephemeral.publicKey);
|
|
6688
|
+
await state.mixKey(await dh(ephemeral.privateKey, remoteStatic));
|
|
6689
|
+
const message1 = concat(ephemeral.publicKey, await state.encryptAndHash(new Uint8Array()));
|
|
6690
|
+
return {
|
|
6691
|
+
message1,
|
|
6692
|
+
async finish(message2) {
|
|
6693
|
+
if (message2.length < DH_LEN + TAG_LEN) throw new Error("Noise: message 2 is too short");
|
|
6694
|
+
const remoteEphemeral = message2.slice(0, DH_LEN);
|
|
6695
|
+
await state.mixHash(remoteEphemeral);
|
|
6696
|
+
await state.mixKey(await dh(ephemeral.privateKey, remoteEphemeral));
|
|
6697
|
+
const payload = await state.decryptAndHash(message2.slice(DH_LEN));
|
|
6698
|
+
return { payload, split: await state.split() };
|
|
6699
|
+
}
|
|
6700
|
+
};
|
|
6701
|
+
}
|
|
6702
|
+
async function x25519KeyPair(secret) {
|
|
6703
|
+
if (!secret) {
|
|
6704
|
+
const pair = await crypto.subtle.generateKey({ name: "X25519" }, true, [
|
|
6705
|
+
"deriveBits"
|
|
6706
|
+
]);
|
|
6707
|
+
const publicKey = new Uint8Array(await crypto.subtle.exportKey("raw", pair.publicKey));
|
|
6708
|
+
return { privateKey: pair.privateKey, publicKey };
|
|
6709
|
+
}
|
|
6710
|
+
const privateKey = await crypto.subtle.importKey(
|
|
6711
|
+
"pkcs8",
|
|
6712
|
+
concat(PKCS8_X25519_PREFIX, secret),
|
|
6713
|
+
{ name: "X25519" },
|
|
6714
|
+
true,
|
|
6715
|
+
["deriveBits"]
|
|
6716
|
+
);
|
|
6717
|
+
const jwk = await crypto.subtle.exportKey("jwk", privateKey);
|
|
6718
|
+
return { privateKey, publicKey: base64UrlDecode(jwk.x) };
|
|
6719
|
+
}
|
|
6720
|
+
async function dh(privateKey, publicKey) {
|
|
6721
|
+
const peer = await crypto.subtle.importKey("raw", publicKey, { name: "X25519" }, false, []);
|
|
6722
|
+
return new Uint8Array(
|
|
6723
|
+
await crypto.subtle.deriveBits({ name: "X25519", public: peer }, privateKey, 256)
|
|
6724
|
+
);
|
|
6725
|
+
}
|
|
6726
|
+
function noiseNonce(n) {
|
|
6727
|
+
const nonce2 = new Uint8Array(12);
|
|
6728
|
+
new DataView(nonce2.buffer).setBigUint64(4, BigInt(n), false);
|
|
6729
|
+
return nonce2;
|
|
6730
|
+
}
|
|
6731
|
+
async function sha256(data) {
|
|
6732
|
+
return new Uint8Array(await crypto.subtle.digest("SHA-256", data));
|
|
6733
|
+
}
|
|
6734
|
+
async function hmacSha256(key, data) {
|
|
6735
|
+
const hmacKey = await crypto.subtle.importKey("raw", key, { name: "HMAC", hash: "SHA-256" }, false, [
|
|
6736
|
+
"sign"
|
|
6737
|
+
]);
|
|
6738
|
+
return new Uint8Array(await crypto.subtle.sign("HMAC", hmacKey, data));
|
|
6739
|
+
}
|
|
6740
|
+
async function hkdf2(chainingKey, inputKeyMaterial) {
|
|
6741
|
+
const temp = await hmacSha256(chainingKey, inputKeyMaterial);
|
|
6742
|
+
const first = await hmacSha256(temp, new Uint8Array([1]));
|
|
6743
|
+
const second = await hmacSha256(temp, concat(first, new Uint8Array([2])));
|
|
6744
|
+
return [first, second];
|
|
6745
|
+
}
|
|
6746
|
+
function base64UrlDecode(value) {
|
|
6747
|
+
const base643 = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
6748
|
+
const binary = atob(base643 + "=".repeat((4 - base643.length % 4) % 4));
|
|
6749
|
+
return Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
6750
|
+
}
|
|
6751
|
+
|
|
6752
|
+
// src/sealed/sealed.ts
|
|
6753
|
+
var HANDSHAKE_PATH = "/sealed/v2/handshake";
|
|
6754
|
+
var SEALED_PATH = "/sealed/v2";
|
|
6755
|
+
var SEALED_CONTENT_TYPE = "application/vnd.calimero.sealed";
|
|
6756
|
+
var VERSION = 2;
|
|
6757
|
+
var PROLOGUE = new TextEncoder().encode("calimero/sealed-http/v2");
|
|
6758
|
+
var SESSION_ID_LEN = 16;
|
|
6759
|
+
var HEADER_LEN = 1 + SESSION_ID_LEN + 8;
|
|
6760
|
+
var FRAME_HEAD = 0;
|
|
6761
|
+
var FRAME_DATA = 1;
|
|
6762
|
+
var FRAME_END = 2;
|
|
6763
|
+
var RENEW_BEFORE_MS = 6e4;
|
|
6764
|
+
var TRANSPORT_BINDING_DOMAIN = "calimero.tee-attest.transport-key.v1";
|
|
6765
|
+
var NULL_BODY_STATUSES = [101, 204, 205, 304];
|
|
6623
6766
|
var utf8 = new TextEncoder();
|
|
6624
6767
|
var StaleTransportKeyError = class extends Error {
|
|
6625
6768
|
constructor() {
|
|
6626
|
-
super(
|
|
6627
|
-
"The node no longer holds the transport key this request was sealed to; attest again for the current one"
|
|
6628
|
-
);
|
|
6769
|
+
super("The node no longer holds the attested transport key; attest again for the current one");
|
|
6629
6770
|
this.name = "StaleTransportKeyError";
|
|
6630
6771
|
}
|
|
6631
6772
|
};
|
|
@@ -6648,9 +6789,9 @@ async function fetchAttestedTransportKey(admin, verify, options = {}) {
|
|
|
6648
6789
|
if (options.applicationId === void 0 !== (options.applicationHash === void 0)) {
|
|
6649
6790
|
throw new Error("applicationId and applicationHash are given together or not at all");
|
|
6650
6791
|
}
|
|
6651
|
-
const
|
|
6792
|
+
const nonce2 = hex(crypto.getRandomValues(new Uint8Array(32)));
|
|
6652
6793
|
const attested = await admin.teeAttest({
|
|
6653
|
-
nonce,
|
|
6794
|
+
nonce: nonce2,
|
|
6654
6795
|
applicationId: options.applicationId,
|
|
6655
6796
|
bindTransportKey: true
|
|
6656
6797
|
});
|
|
@@ -6660,7 +6801,7 @@ async function fetchAttestedTransportKey(admin, verify, options = {}) {
|
|
|
6660
6801
|
const transportKey = fromHex(attested.transportPublicKey, "transportPublicKey", 32);
|
|
6661
6802
|
const inner = options.applicationHash ? fromHex(options.applicationHash, "applicationHash", 32) : new Uint8Array(32);
|
|
6662
6803
|
const reportDataSuffix = hex(await transportKeyBinding(inner, transportKey));
|
|
6663
|
-
if (!await verify({ quoteB64: attested.quoteB64, nonce, reportDataSuffix })) {
|
|
6804
|
+
if (!await verify({ quoteB64: attested.quoteB64, nonce: nonce2, reportDataSuffix })) {
|
|
6664
6805
|
throw new Error("The attestation did not verify, so its transport key is not trusted");
|
|
6665
6806
|
}
|
|
6666
6807
|
return transportKey;
|
|
@@ -6668,12 +6809,37 @@ async function fetchAttestedTransportKey(admin, verify, options = {}) {
|
|
|
6668
6809
|
function createSealedFetch(options) {
|
|
6669
6810
|
const base = new URL(options.baseUrl);
|
|
6670
6811
|
const basePath = base.pathname.replace(/\/+$/, "");
|
|
6671
|
-
const
|
|
6672
|
-
const transportKey = options.transportPublicKey;
|
|
6673
|
-
if (transportKey.length !== 32) {
|
|
6674
|
-
throw new Error("transportPublicKey must be 32 bytes");
|
|
6675
|
-
}
|
|
6812
|
+
const nodeUrl = `${base.origin}${basePath}`;
|
|
6676
6813
|
const baseFetch = options.fetch ?? ((input, init) => fetch(input, init));
|
|
6814
|
+
const attest = typeof options.transportPublicKey === "function" ? options.transportPublicKey : void 0;
|
|
6815
|
+
let transportKey = attest ? Promise.resolve().then(attest) : Promise.resolve(options.transportPublicKey);
|
|
6816
|
+
let session;
|
|
6817
|
+
const openSession = async () => {
|
|
6818
|
+
try {
|
|
6819
|
+
return await handshake(baseFetch, nodeUrl, await checkedKey(transportKey));
|
|
6820
|
+
} catch (error) {
|
|
6821
|
+
if (!(error instanceof StaleTransportKeyError) || !attest) throw error;
|
|
6822
|
+
transportKey = Promise.resolve().then(attest);
|
|
6823
|
+
return handshake(baseFetch, nodeUrl, await checkedKey(transportKey));
|
|
6824
|
+
}
|
|
6825
|
+
};
|
|
6826
|
+
const currentSession = async () => {
|
|
6827
|
+
const pending = session ?? renew(void 0);
|
|
6828
|
+
const open = await pending;
|
|
6829
|
+
if (Date.now() < open.renewAt) return [open, pending];
|
|
6830
|
+
const renewed = renew(pending);
|
|
6831
|
+
return [await renewed, renewed];
|
|
6832
|
+
};
|
|
6833
|
+
const renew = (stale) => {
|
|
6834
|
+
if (session === stale) {
|
|
6835
|
+
const opening = openSession();
|
|
6836
|
+
session = opening;
|
|
6837
|
+
opening.catch(() => {
|
|
6838
|
+
if (session === opening) session = void 0;
|
|
6839
|
+
});
|
|
6840
|
+
}
|
|
6841
|
+
return session;
|
|
6842
|
+
};
|
|
6677
6843
|
return async (input, init) => {
|
|
6678
6844
|
const request = new Request(input, init);
|
|
6679
6845
|
const url = new URL(request.url);
|
|
@@ -6682,126 +6848,190 @@ function createSealedFetch(options) {
|
|
|
6682
6848
|
}
|
|
6683
6849
|
const headers = [];
|
|
6684
6850
|
request.headers.forEach((value, name) => headers.push([name, value]));
|
|
6685
|
-
const head = {
|
|
6686
|
-
method: request.method,
|
|
6687
|
-
path: url.pathname.slice(basePath.length) + url.search,
|
|
6688
|
-
headers,
|
|
6689
|
-
ts: Math.floor(Date.now() / 1e3)
|
|
6690
|
-
};
|
|
6851
|
+
const head = { method: request.method, path: url.pathname.slice(basePath.length) + url.search, headers };
|
|
6691
6852
|
const body = new Uint8Array(await request.arrayBuffer());
|
|
6692
|
-
const
|
|
6693
|
-
|
|
6694
|
-
|
|
6695
|
-
|
|
6696
|
-
|
|
6697
|
-
|
|
6698
|
-
|
|
6699
|
-
|
|
6700
|
-
|
|
6853
|
+
const send = async (open2) => {
|
|
6854
|
+
const { requestId, header, envelope } = await sealRequest(open2, head, body);
|
|
6855
|
+
const response = await baseFetch(`${nodeUrl}${SEALED_PATH}`, {
|
|
6856
|
+
method: "POST",
|
|
6857
|
+
headers: { "content-type": SEALED_CONTENT_TYPE },
|
|
6858
|
+
body: envelope,
|
|
6859
|
+
signal: request.signal
|
|
6860
|
+
});
|
|
6861
|
+
if (response.headers.get("content-type") !== SEALED_CONTENT_TYPE) {
|
|
6862
|
+
throw await envelopeRefusal(response);
|
|
6863
|
+
}
|
|
6864
|
+
return openResponse(response, open2.responseKey, header, requestId);
|
|
6865
|
+
};
|
|
6866
|
+
const [open, pending] = await currentSession();
|
|
6867
|
+
try {
|
|
6868
|
+
return await send(open);
|
|
6869
|
+
} catch (error) {
|
|
6870
|
+
if (!(error instanceof SealedTransportError) || error.code !== "unknown_session") throw error;
|
|
6871
|
+
return send(await renew(pending));
|
|
6701
6872
|
}
|
|
6702
|
-
const opened = await exchange.open(new Uint8Array(await response.arrayBuffer()));
|
|
6703
|
-
const nullBody = [101, 204, 205, 304].includes(opened.head.status);
|
|
6704
|
-
return new Response(nullBody ? null : opened.body, {
|
|
6705
|
-
status: opened.head.status,
|
|
6706
|
-
headers: opened.head.headers
|
|
6707
|
-
});
|
|
6708
6873
|
};
|
|
6709
6874
|
}
|
|
6710
|
-
async function
|
|
6711
|
-
|
|
6712
|
-
|
|
6713
|
-
|
|
6714
|
-
|
|
6715
|
-
|
|
6716
|
-
|
|
6717
|
-
|
|
6875
|
+
async function checkedKey(key) {
|
|
6876
|
+
const bytes = await key;
|
|
6877
|
+
if (bytes.length !== 32) throw new Error("transportPublicKey must be 32 bytes");
|
|
6878
|
+
return bytes;
|
|
6879
|
+
}
|
|
6880
|
+
async function handshake(baseFetch, nodeUrl, transportKey) {
|
|
6881
|
+
const noise = await initiate(transportKey, PROLOGUE);
|
|
6882
|
+
const response = await baseFetch(`${nodeUrl}${HANDSHAKE_PATH}`, {
|
|
6883
|
+
method: "POST",
|
|
6884
|
+
headers: { "content-type": SEALED_CONTENT_TYPE },
|
|
6885
|
+
body: concat(new Uint8Array([VERSION]), transportKey, noise.message1)
|
|
6886
|
+
});
|
|
6887
|
+
if (response.headers.get("content-type") !== SEALED_CONTENT_TYPE) {
|
|
6888
|
+
throw await envelopeRefusal(response);
|
|
6718
6889
|
}
|
|
6719
|
-
|
|
6720
|
-
return
|
|
6890
|
+
const reply = new Uint8Array(await response.arrayBuffer());
|
|
6891
|
+
return sessionFrom(noise, reply);
|
|
6721
6892
|
}
|
|
6722
|
-
async function
|
|
6723
|
-
|
|
6724
|
-
|
|
6725
|
-
|
|
6726
|
-
await
|
|
6727
|
-
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
}
|
|
6731
|
-
|
|
6732
|
-
const
|
|
6733
|
-
const
|
|
6734
|
-
const
|
|
6735
|
-
|
|
6736
|
-
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
6740
|
-
);
|
|
6741
|
-
const responseAad = concat(new Uint8Array([VERSION]), salt);
|
|
6893
|
+
async function sessionFrom(noise, reply) {
|
|
6894
|
+
if (reply[0] !== VERSION) throw new Error("The handshake reply is malformed");
|
|
6895
|
+
let finished;
|
|
6896
|
+
try {
|
|
6897
|
+
finished = await noise.finish(reply.slice(1));
|
|
6898
|
+
} catch {
|
|
6899
|
+
throw new Error("The handshake reply did not open: it did not come from the attested node");
|
|
6900
|
+
}
|
|
6901
|
+
const { payload, split } = finished;
|
|
6902
|
+
if (payload.length !== SESSION_ID_LEN + 4) throw new Error("The handshake reply is malformed");
|
|
6903
|
+
const sessionId = payload.slice(0, SESSION_ID_LEN);
|
|
6904
|
+
const lifetimeSecs = new DataView(payload.buffer, payload.byteOffset).getUint32(SESSION_ID_LEN, false);
|
|
6905
|
+
const [requestKey, responseKey] = await Promise.all([
|
|
6906
|
+
crypto.subtle.importKey("raw", split[0], "AES-GCM", false, ["encrypt"]),
|
|
6907
|
+
crypto.subtle.importKey("raw", split[1], "AES-GCM", false, ["decrypt"])
|
|
6908
|
+
]);
|
|
6909
|
+
split[0].fill(0);
|
|
6910
|
+
split[1].fill(0);
|
|
6742
6911
|
return {
|
|
6743
|
-
|
|
6744
|
-
|
|
6745
|
-
|
|
6746
|
-
|
|
6747
|
-
|
|
6748
|
-
|
|
6749
|
-
|
|
6750
|
-
|
|
6751
|
-
|
|
6752
|
-
|
|
6753
|
-
responseKey,
|
|
6754
|
-
sealed.slice(1 + NONCE_LEN)
|
|
6755
|
-
)
|
|
6756
|
-
);
|
|
6757
|
-
} catch {
|
|
6758
|
-
throw new Error("The sealed response did not open: it was not sealed by the attested node");
|
|
6759
|
-
}
|
|
6760
|
-
const view = new DataView(plaintext.buffer, plaintext.byteOffset, plaintext.byteLength);
|
|
6761
|
-
const len = view.getUint32(0, false);
|
|
6762
|
-
const head2 = JSON.parse(new TextDecoder().decode(plaintext.slice(4, 4 + len)));
|
|
6763
|
-
return { head: head2, body: plaintext.slice(4 + len) };
|
|
6912
|
+
requestKey,
|
|
6913
|
+
responseKey,
|
|
6914
|
+
nextRequestId: 1,
|
|
6915
|
+
renewAt: Date.now() + lifetimeSecs * 1e3 - RENEW_BEFORE_MS,
|
|
6916
|
+
header(requestId) {
|
|
6917
|
+
const header = new Uint8Array(HEADER_LEN);
|
|
6918
|
+
header[0] = VERSION;
|
|
6919
|
+
header.set(sessionId, 1);
|
|
6920
|
+
new DataView(header.buffer).setBigUint64(1 + SESSION_ID_LEN, BigInt(requestId), false);
|
|
6921
|
+
return header;
|
|
6764
6922
|
}
|
|
6765
6923
|
};
|
|
6766
6924
|
}
|
|
6925
|
+
async function sealRequest(session, head, body) {
|
|
6926
|
+
const requestId = session.nextRequestId;
|
|
6927
|
+
session.nextRequestId += 1;
|
|
6928
|
+
const header = session.header(requestId);
|
|
6929
|
+
const sealed = await crypto.subtle.encrypt(
|
|
6930
|
+
{ name: "AES-GCM", iv: nonce(requestId, 0), additionalData: header },
|
|
6931
|
+
session.requestKey,
|
|
6932
|
+
joinInner(head, body)
|
|
6933
|
+
);
|
|
6934
|
+
return { requestId, header, envelope: concat(header, new Uint8Array(sealed)) };
|
|
6935
|
+
}
|
|
6936
|
+
function nonce(requestId, index) {
|
|
6937
|
+
const bytes = new Uint8Array(12);
|
|
6938
|
+
const view = new DataView(bytes.buffer);
|
|
6939
|
+
view.setBigUint64(0, BigInt(requestId), false);
|
|
6940
|
+
view.setUint32(8, index, false);
|
|
6941
|
+
return bytes;
|
|
6942
|
+
}
|
|
6767
6943
|
function joinInner(head, body) {
|
|
6768
6944
|
const encoded = utf8.encode(JSON.stringify(head));
|
|
6769
6945
|
const len = new Uint8Array(4);
|
|
6770
6946
|
new DataView(len.buffer).setUint32(0, encoded.length, false);
|
|
6771
6947
|
return concat(len, encoded, body);
|
|
6772
6948
|
}
|
|
6773
|
-
async function
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
6781
|
-
|
|
6949
|
+
async function openResponse(response, responseKey, header, requestId) {
|
|
6950
|
+
if (!response.body) throw new Error("The sealed response has no body");
|
|
6951
|
+
const frames = frameReader(response.body.getReader());
|
|
6952
|
+
let index = 0;
|
|
6953
|
+
const next = async () => {
|
|
6954
|
+
const sealed = await frames.next();
|
|
6955
|
+
if (!sealed) return null;
|
|
6956
|
+
let plain;
|
|
6957
|
+
try {
|
|
6958
|
+
plain = new Uint8Array(
|
|
6959
|
+
await crypto.subtle.decrypt(
|
|
6960
|
+
{ name: "AES-GCM", iv: nonce(requestId, index), additionalData: header },
|
|
6961
|
+
responseKey,
|
|
6962
|
+
sealed
|
|
6963
|
+
)
|
|
6964
|
+
);
|
|
6965
|
+
} catch {
|
|
6966
|
+
throw new Error("A sealed response frame did not open: it was not sealed by the attested node");
|
|
6967
|
+
}
|
|
6968
|
+
index += 1;
|
|
6969
|
+
return { kind: plain[0], data: plain.slice(1) };
|
|
6970
|
+
};
|
|
6971
|
+
const first = await next();
|
|
6972
|
+
if (!first || first.kind !== FRAME_HEAD) throw new Error("The sealed response is malformed");
|
|
6973
|
+
const head = JSON.parse(new TextDecoder().decode(first.data));
|
|
6974
|
+
if (NULL_BODY_STATUSES.includes(head.status)) {
|
|
6975
|
+
await frames.cancel();
|
|
6976
|
+
return new Response(null, { status: head.status, headers: head.headers });
|
|
6977
|
+
}
|
|
6978
|
+
const body = new ReadableStream({
|
|
6979
|
+
async pull(controller) {
|
|
6980
|
+
try {
|
|
6981
|
+
const frame = await next();
|
|
6982
|
+
if (!frame) {
|
|
6983
|
+
controller.error(new Error("The sealed response was cut short"));
|
|
6984
|
+
} else if (frame.kind === FRAME_DATA) {
|
|
6985
|
+
controller.enqueue(frame.data);
|
|
6986
|
+
} else if (frame.kind === FRAME_END) {
|
|
6987
|
+
controller.close();
|
|
6988
|
+
await frames.cancel();
|
|
6989
|
+
} else {
|
|
6990
|
+
controller.error(new Error("The sealed response is malformed"));
|
|
6991
|
+
}
|
|
6992
|
+
} catch (error) {
|
|
6993
|
+
controller.error(error);
|
|
6994
|
+
}
|
|
6995
|
+
},
|
|
6996
|
+
cancel() {
|
|
6997
|
+
return frames.cancel();
|
|
6998
|
+
}
|
|
6999
|
+
});
|
|
7000
|
+
return new Response(body, { status: head.status, headers: head.headers });
|
|
6782
7001
|
}
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
|
|
6788
|
-
|
|
6789
|
-
|
|
6790
|
-
|
|
6791
|
-
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
|
|
6798
|
-
|
|
6799
|
-
|
|
7002
|
+
function frameReader(reader) {
|
|
7003
|
+
let buffer = new Uint8Array();
|
|
7004
|
+
const fill = async (length) => {
|
|
7005
|
+
while (buffer.length < length) {
|
|
7006
|
+
const { done, value } = await reader.read();
|
|
7007
|
+
if (done) return false;
|
|
7008
|
+
buffer = concat(buffer, value);
|
|
7009
|
+
}
|
|
7010
|
+
return true;
|
|
7011
|
+
};
|
|
7012
|
+
return {
|
|
7013
|
+
async next() {
|
|
7014
|
+
if (!await fill(4)) return null;
|
|
7015
|
+
const length = new DataView(buffer.buffer, buffer.byteOffset).getUint32(0, false);
|
|
7016
|
+
if (!await fill(4 + length)) return null;
|
|
7017
|
+
const frame = buffer.slice(4, 4 + length);
|
|
7018
|
+
buffer = buffer.slice(4 + length);
|
|
7019
|
+
return frame;
|
|
7020
|
+
},
|
|
7021
|
+
cancel: () => reader.cancel()
|
|
7022
|
+
};
|
|
6800
7023
|
}
|
|
6801
|
-
function
|
|
6802
|
-
|
|
6803
|
-
|
|
6804
|
-
|
|
7024
|
+
async function envelopeRefusal(response) {
|
|
7025
|
+
let code;
|
|
7026
|
+
let message = `Sealed request refused: HTTP ${response.status}`;
|
|
7027
|
+
try {
|
|
7028
|
+
const body = await response.json();
|
|
7029
|
+
code = body?.error?.code;
|
|
7030
|
+
if (body?.error?.message) message = `Sealed request refused: ${body.error.message}`;
|
|
7031
|
+
} catch {
|
|
7032
|
+
}
|
|
7033
|
+
if (code === "stale_transport_key") return new StaleTransportKeyError();
|
|
7034
|
+
return new SealedTransportError(response.status, code, message);
|
|
6805
7035
|
}
|
|
6806
7036
|
|
|
6807
7037
|
// src/namespace-op/namespace-op.ts
|