From cd5452b6249cffbb4209795121e3a6974dc7e255 Mon Sep 17 00:00:00 2001 From: Tony Garnock-Jones Date: Thu, 9 Feb 2023 22:41:43 +0100 Subject: [PATCH] Implement new gatekeeper protocol for syndicate-js --- .../server-config/standard-dataspace.pr | 4 +- examples/example-simple-chat/src/index.ts | 55 +- packages/core/src/runtime/supervise.ts | 6 + packages/core/src/transport/sturdy.ts | 33 +- packages/create/template/src/index.ts | 13 +- .../template/syndicate-server.config.pr | 2 +- packages/ws-relay/package.json | 5 +- packages/ws-relay/protocols/schemas/ws.prs | 8 - packages/ws-relay/src/index.ts | 595 ++++++++++++------ yarn.lock | 2 +- 10 files changed, 461 insertions(+), 262 deletions(-) delete mode 100644 packages/ws-relay/protocols/schemas/ws.prs diff --git a/examples/example-simple-chat/server-config/standard-dataspace.pr b/examples/example-simple-chat/server-config/standard-dataspace.pr index 72e5bec..6355688 100644 --- a/examples/example-simple-chat/server-config/standard-dataspace.pr +++ b/examples/example-simple-chat/server-config/standard-dataspace.pr @@ -1,9 +1,9 @@ let ?ds = dataspace ; Connect using ] > - + $ds #f> ; Connect using ] > let ?sk = #x"7626eb6c0ee79cb928a0c3f6e29621e9119da6735859f1425956a49fd937e586" let ?pk = #x"21f6cd4e11e7e37711d6b3084ff18cded8fc8abf293aa47d43e8bb86dda65516" - + $ds #f> diff --git a/examples/example-simple-chat/src/index.ts b/examples/example-simple-chat/src/index.ts index a7ae14e..5540e9d 100644 --- a/examples/example-simple-chat/src/index.ts +++ b/examples/example-simple-chat/src/index.ts @@ -4,10 +4,10 @@ import { fromJS, Bytes, Dataspace, Ref, Sturdy, AnyValue, Reader, Schemas, Embedded, stringify } from "@syndicate-lang/core"; import { boot as bootHtml, Anchor, template as html, HtmlFragments, GlobalEvent, UIAttribute, UIChangeableProperty } from "@syndicate-lang/html"; import { boot as bootWakeDetector, WakeEvent } from "./wake-detector"; -import { boot as bootWsRelay, ForceRelayDisconnect, RelayAddress, Resolved, Noise } from "@syndicate-lang/ws-relay"; +import { boot as bootWsRelay } from "@syndicate-lang/ws-relay"; import { Present, Says } from './gen/simpleChatProtocol'; - -const Transport = Schemas.transportAddress; +import G = Schemas.gatekeeper; +import N = Schemas.noise; export function main() { document.getElementById('chat_form')!.onsubmit = e => { e.preventDefault(); return false; }; @@ -31,9 +31,9 @@ function bootChat(ds: Ref) { on asserted UIChangeableProperty('#nym', 'value', $v: string) => nym.value = v; during UIChangeableProperty('#route', 'value', $routeText: string) => { - let route: Noise.Route | null = null; + let route: G.Route | null = null; try { - route = Noise.asRoute(new Reader(routeText).next()); + route = G.asRoute(new Reader(routeText).next()); } catch (e) { console.error(e); } @@ -41,16 +41,20 @@ function bootChat(ds: Ref) { if (route) contactRemote(route); } - function contactRemote(route: Noise.Route) { + function contactRemote(route: G.Route) { console.log('contactRemote', route); - during Resolved({ + during G.ResolvePath({ "route": route, - "addr": $addr: RelayAddress, - "resolved": $remoteDs_e: Embedded, + "addr": $addr, + "control": $control_e: Embedded, + "resolved": G.Resolved.accepted($remoteDs_e: Embedded), }) => { const remoteDs = remoteDs_e.embeddedValue; + const control = control_e.embeddedValue; - on message WakeEvent() => send message ForceRelayDisconnect(addr); + on message WakeEvent() => at control { + send message G.ForceDisconnect(); + } outputState('connected', 'connected to ' + stringify(addr)); on stop outputState('disconnected', 'disconnected from ' + stringify(addr)); @@ -94,28 +98,33 @@ function setDataspaceAddress() { `${localWs}://${document.location.hostname}:9001/`, ]; const transports: AnyValue[] = - wsurls.map(u => fromJS(RelayAddress(Transport.WebSocket(u)))); + wsurls.map(u => fromJS(Schemas.transportAddress.WebSocket(u))); - route.value = stringify(Noise.Route({ + route.value = stringify(G.Route({ "transports": transports, - "steps": [Noise.RouteStep.NoiseStep(Noise.NoiseStep(Noise.NoiseSpec({ - "service": "syndicate", - "key": Bytes.fromHex("21f6cd4e11e7e37711d6b3084ff18cded8fc8abf293aa47d43e8bb86dda65516"), - "protocol": Noise.NoiseProtocol.absent(), - "preSharedKeys": Noise.NoisePreSharedKeys.absent(), - })))], + "pathSteps": [G.PathStep({ + "stepType": N.$noise, + "detail": fromJS(N.NoiseSpec({ + "service": "syndicate", + "key": Bytes.fromHex("21f6cd4e11e7e37711d6b3084ff18cded8fc8abf293aa47d43e8bb86dda65516"), + "protocol": N.NoiseProtocol.absent(), + "preSharedKeys": N.NoisePreSharedKeys.absent(), + })), + })], })); // To use a sturdyref instead: // - // route.value = stringify(Noise.Route({ + // route.value = stringify(G.Route({ // "transports": transports, - // "steps": [Noise.RouteStep.GatekeeperStep( - // Sturdy.SturdyRef({ + // "pathSteps": [G.PathStep({ + // "stepType": Sturdy.$ref, + // "detail": fromJS(Sturdy.Parameters({ // "oid": "syndicate", - // "caveatChain": [], // "sig": Bytes.fromHex('69ca300c1dbfa08fba692102dd82311a'), - // }))], + // "caveats": Sturdy.CaveatsField.absent(), + // })), + // })], // })); // // ... and of course you can chain these things, depending on server setup. diff --git a/packages/core/src/runtime/supervise.ts b/packages/core/src/runtime/supervise.ts index ed7783a..4d3995f 100644 --- a/packages/core/src/runtime/supervise.ts +++ b/packages/core/src/runtime/supervise.ts @@ -36,6 +36,12 @@ export class Supervisor { readonly state: Field = Turn.active.field('started'); supervisee: Actor | null = null; + static always(nameFunction: () => AnyValue, bootFunction: LocalAction): Supervisor { + return new Supervisor({ restartPolicy: SupervisorRestartPolicy.ALWAYS }, + nameFunction, + bootFunction); + } + constructor(config: Partial, nameFunction: () => AnyValue, bootFunction: LocalAction) diff --git a/packages/core/src/transport/sturdy.ts b/packages/core/src/transport/sturdy.ts index 146e7e6..c03439f 100644 --- a/packages/core/src/transport/sturdy.ts +++ b/packages/core/src/transport/sturdy.ts @@ -38,26 +38,35 @@ export function sturdyDecode(bs: Bytes): SturdyValue { } export async function mint(oid: SturdyValue, secretKey: Bytes): Promise { - return S.SturdyRef({ - oid, - caveatChain: [], - sig: await mac(secretKey, sturdyEncode(oid)), - }); + return S.SturdyRef(S.Parameters({ + oid, + sig: await mac(secretKey, sturdyEncode(oid)), + caveats: S.CaveatsField.absent(), + })); } async function chainMac(key: Bytes | Promise, caveats: S.Caveat[]): Promise { return caveats.reduce(async (key, c) => mac(await key, sturdyEncode(S.fromCaveat(c))), key); } +export function caveatChain(r: S.SturdyRef): S.Caveat[] { + switch (r.parameters.caveats._variant) { + case "present": return r.parameters.caveats.caveats; + case "absent": return []; + case "invalid": throw new Error("Invalid caveats on sturdyref"); + } +} + export async function attenuate(r: S.SturdyRef, ... a: S.Caveat[]): Promise { - return S.SturdyRef({ - oid: r.oid, - caveatChain: [... r.caveatChain, ... a], - sig: await chainMac(r.sig, a), - }); + if (a.length === 0) return r; + return S.SturdyRef(S.Parameters({ + oid: r.parameters.oid, + caveats: S.CaveatsField.present([... caveatChain(r), ... a]), + sig: await chainMac(r.parameters.sig, a), + })); } export async function validate(r: S.SturdyRef, secretKey: Bytes): Promise { - const sig = await chainMac(await mac(secretKey, sturdyEncode(r.oid)), r.caveatChain); - return is(sig, r.sig); + const sig = await chainMac(await mac(secretKey, sturdyEncode(r.parameters.oid)), caveatChain(r)); + return is(sig, r.parameters.sig); } diff --git a/packages/create/template/src/index.ts b/packages/create/template/src/index.ts index 1c69986..f24c187 100644 --- a/packages/create/template/src/index.ts +++ b/packages/create/template/src/index.ts @@ -5,6 +5,7 @@ import { Dataspace, Ref, Sturdy, Reader, Schemas, Embedded, randomId, fromJS } f import html from "@syndicate-lang/html"; import wsRelay from "@syndicate-lang/ws-relay"; import { ExampleDefinition } from './gen/example'; +import G = Schemas.gatekeeper; export function main() { Dataspace.boot(ds => { @@ -25,19 +26,19 @@ function bootApp(ds: Ref) { const this_instance = randomId(16); - const route = wsRelay.Noise.Route({ + const route = G.Route({ "transports": [fromJS(Schemas.transportAddress.WebSocket( `ws://${document.location.hostname}:9001/`))], - "steps": [wsRelay.Noise.RouteStep.GatekeeperStep(Sturdy.asSturdyRef( - new Reader('').next()))], + "pathSteps": [G.asPathStep(fromJS(Sturdy.asSturdyRef( + new Reader( + '').next())))], }); - during wsRelay.Resolved({ + during G.ResolvePath({ "route": route, - "resolved": $remoteDs_e: Embedded, + "resolved": G.Resolved.accepted($remoteDs_e: Embedded), }) => { const remoteDs = remoteDs_e.embeddedValue; - at remoteDs { assert ExampleDefinition(this_instance); during ExampleDefinition($who: string) => { diff --git a/packages/create/template/syndicate-server.config.pr b/packages/create/template/syndicate-server.config.pr index 46bdac6..4d461b2 100644 --- a/packages/create/template/syndicate-server.config.pr +++ b/packages/create/template/syndicate-server.config.pr @@ -9,4 +9,4 @@ ; Create a dataspace entity, and register it with the gatekeeper with name `"syndicate"` and an ; empty secret key: let ?ds = dataspace - + $ds #f> diff --git a/packages/ws-relay/package.json b/packages/ws-relay/package.json index 7dd7a18..d69d258 100644 --- a/packages/ws-relay/package.json +++ b/packages/ws-relay/package.json @@ -12,9 +12,7 @@ "url": "https://git.syndicate-lang.org/syndicate-lang/syndicate-js" }, "scripts": { - "prepare": "yarn regenerate && yarn compile && yarn rollup", - "regenerate": "rm -rf ./src/gen && preserves-schema-ts --module EntityRef=@syndicate-lang/core --module transportAddress=@syndicate-lang/core:Schemas.transportAddress --module noise=@syndicate-lang/core:Schemas.noise --module sturdy=@syndicate-lang/core:Schemas.sturdy --xref ../../node_modules/@syndicate-lang/core/protocols/schemas --output ./src/gen ./protocols/schemas", - "regenerate:watch": "yarn regenerate --watch", + "prepare": "yarn compile && yarn rollup", "compile": "syndicate-tsc", "compile:watch": "syndicate-tsc -w --verbose --intermediate-directory src.ts", "rollup": "rollup -c", @@ -28,7 +26,6 @@ "author": "Tony Garnock-Jones ", "dependencies": { "@preserves/core": ">=0.20.2", - "@preserves/schema": ">=0.21.2", "@syndicate-lang/core": "^0.13.1", "salty-crypto": "0.3" }, diff --git a/packages/ws-relay/protocols/schemas/ws.prs b/packages/ws-relay/protocols/schemas/ws.prs deleted file mode 100644 index 35bcbd1..0000000 --- a/packages/ws-relay/protocols/schemas/ws.prs +++ /dev/null @@ -1,8 +0,0 @@ -version 1 . - -Resolved = . - -ViaRelay = . -ForceRelayDisconnect = . - -RelayAddress = transportAddress.WebSocket . diff --git a/packages/ws-relay/src/index.ts b/packages/ws-relay/src/index.ts index 0cff3ee..3c73a72 100644 --- a/packages/ws-relay/src/index.ts +++ b/packages/ws-relay/src/index.ts @@ -1,218 +1,403 @@ /// SPDX-License-Identifier: GPL-3.0-or-later /// SPDX-FileCopyrightText: Copyright © 2016-2023 Tony Garnock-Jones -import { QuasiValue as Q, Assertion, Bytes, Ref, Relay, Turn, Supervisor, SupervisorRestartPolicy, Observe, Schemas, assertionFacetObserver, canonicalEncode, fromJS, isEmbedded, underlying } from "@syndicate-lang/core"; -import * as G from "./gen/ws"; -export * from "./gen/ws"; -import * as N from "@syndicate-lang/core/lib/gen/noise"; -export * as Noise from "@syndicate-lang/core/lib/gen/noise"; +import { + Assertion, + Bytes, + Dataflow, + Embedded, + Observe, + QuasiValue as Q, + Ref, + Relay, + Schemas, + Supervisor, + Turn, + assertionFacetObserver, + canonicalEncode, + fromJS, + isEmbedded, + stringify, + underlying, +} from "@syndicate-lang/core"; +import G = Schemas.gatekeeper; +import S = Schemas.sturdy; +import N = Schemas.noise; +import T = Schemas.transportAddress; import * as SaltyCrypto from 'salty-crypto'; +type TransportState = { + addr: T.WebSocket, + control: Ref, + peer: Ref, +}; + export function boot(ds: Ref, debug: boolean = false) { - spawn named 'wsRelay' { + spawn named 'transportConnector' { at ds { - during Observe({ "pattern": :pattern G.Resolved({ - "route": \$routePatValue, - "addr": \_, - "resolved": \Q.bind(), + during Observe({ "pattern": :pattern G.TransportConnection({ + "addr": \$addrPatValue, + "control": \_, + "resolved": \_, }) }) => { - const route = Q.drop_lit(routePatValue, N.toRoute); - if (!route) return; - const addrs: G.RelayAddress[] = []; - route.transports.forEach(t => { - const a = G.toRelayAddress(t); - if (a) addrs.push(a); - }); - wsConnect(addrs, (e, addr) => { - at ds { - stop on message G.ForceRelayDisconnect(addr); - } - resolve(e, route.steps, e => { - assert G.Resolved({ - "route": route, - "addr": addr, - "resolved": e, - }); - }); - }); - } - } - - function tryConnection(addr: G.RelayAddress): Promise { - return new Promise((resolve, reject) => { - console.log('@syndicate-lang/ws-relay trying', addr); - const ws = new WebSocket(addr.url); - ws.binaryType = 'arraybuffer'; - ws.onopen = () => resolve(ws); - ws.onclose = () => reject(null); - ws.onerror = (e) => reject(e); - }); - } - - async function establishConnection( - addrs: G.RelayAddress[], - ): Promise<{ws: WebSocket, addr: G.RelayAddress} | null> { - for (let i = 0; i < addrs.length; i++) { - const addr = addrs[i]; - try { - return { ws: await tryConnection(addr), addr }; - } catch (e) { - console.log( - '@syndicate-lang/ws-relay attempt to contact', addr.url, 'failed with', e); - } - } - return null; - } - - function wsConnect(addrs: G.RelayAddress[], k: (e: Ref, addr: G.RelayAddress) => void) { - let counter = 0; - new Supervisor({ - restartPolicy: SupervisorRestartPolicy.ALWAYS, - }, () => ['wsRelay', fromJS(addrs), counter++], () => { - const facet = Turn.activeFacet; - facet.preventInertCheck(); - - establishConnection(addrs).then(result => facet.turn(() => { - if (result === null) { - console.log('@syndicate-lang/ws-relay no successful connection'); - stop {} - } else { - const {ws, addr} = result; - on stop { - ws.close(); - } - ws.onclose = () => facet.turn(() => { stop {} }); - ws.onerror = () => facet.turn(() => - Turn.active.crash(new Error("WebSocket error"))); - const relay = new Relay.Relay({ - debug, - trustPeer: true, - packetWriter: bs => ws.send(bs), - setup(r: Relay.Relay) { - ws.onmessage = e => facet.turn(() => - r.accept(new Uint8Array(e.data))); - }, - initialOid: 0, - }); - k(relay.peer!, addr); - } - })); - }); - } - - function resolve(e: Ref, steps: N.RouteStep[], k: (e: Ref) => void) { - if (steps.length === 0) { - k(e); - } else { - const [step, ...more] = steps; - switch (step._variant) { - case "NoiseStep": { - noiseConnect(e, step.value.spec, e => resolve(e, more, k)); - break; - } - case "GatekeeperStep": { - at e { - assert Schemas.gatekeeper.Resolve({ - "sturdyref": step.value, - "observer": create assertionFacetObserver(e => { - if (isEmbedded(e)) { - resolve(e.embeddedValue, more, k); - } - }), - }); - } - break; - } - default: ((_: never) => {})(step); - } - } - } - - function noiseConnect(e: Ref, spec: N.NoiseSpec, k: (e: Ref) => void) { - const noiseSessionFacet = Turn.activeFacet; - const algorithms = SaltyCrypto.Noise_25519_ChaChaPoly_BLAKE2s; - const protocol = - spec.protocol._variant === "present" ? spec.protocol.protocol : - spec.protocol._variant === "absent" ? N.fromDefaultProtocol(N.DefaultProtocol()) as string : - (() => { throw new Error("Invalid noise protocol name"); })(); - const patternName = SaltyCrypto.matchPattern(algorithms, protocol); - if (patternName === null) throw new Error("Unsupported protocol " + protocol); - const preSharedKeys = - spec.preSharedKeys._variant === "present" ? spec.preSharedKeys.preSharedKeys : - spec.preSharedKeys._variant === "absent" ? [] : - (() => { throw new Error("Invalid pre-shared keys"); })(); - const prologue = underlying(canonicalEncode(spec.service)); - const H = new SaltyCrypto.Handshake( - algorithms, - patternName, - 'initiator', - { - prologue, - remoteStaticPublicKey: underlying(spec.key), - preSharedKeys: preSharedKeys.map(underlying), - }); - let transportState: SaltyCrypto.TransportState | null = null; - let responderSession: Ref | null = null; - let relay: Relay.Relay | null = null; - function maybeTransition(s: SaltyCrypto.TransportState | null) { - if (transportState !== null) { - throw new Error("Unexpected double-transition to transport state"); - } - transportState = s; - if (transportState !== null) { - k(new Relay.Relay({ - debug, - trustPeer: true, - packetWriter: bs => noiseSessionFacet.turn(() => { - const fragments = transportState!.send.encrypt_large(bs).map(Bytes.from); - at responderSession! { - send message ((fragments.length === 1) - ? N.Packet.complete(fragments[0]) - : N.Packet.fragmented(fragments)); - } - }), - setup(r: Relay.Relay) { - relay = r; - }, - initialOid: 0, - }).peer!); - } - } - at e { - assert N.Connect({ - "serviceSelector": spec.service, - "initiatorSession": create ({ - ... assertionFacetObserver(e => { - const accept = N.asAccept(e); - responderSession = accept.responderSession; - const { packet, finished } = H.writeMessage(new Uint8Array()); - at responderSession { - send message Bytes.from(packet); - } - maybeTransition(finished); - }), - message(body: Assertion) { - const p = N.asPacket(body); - if (transportState) { - const packet = transportState.recv.decrypt_large( - p._variant === 'complete' - ? [underlying(p.value)] - : p.value.map(underlying)); - relay!.accept(packet); - } else { - if (p._variant !== 'complete') { - throw new Error("Unexpected fragmentation in handshake"); - } - const { message, finished } = H.readMessage(underlying(p.value)); - if (message.byteLength !== 0) { - throw new Error("Unexpected payload during handshake"); - } - maybeTransition(finished); - } - }, - }), + const addr = Q.drop_lit(addrPatValue, T.toWebSocket); + if (!addr) return; + let counter =0; + Supervisor.always(() => ['transportConnector', fromJS(addr), counter++], () => { + console.log('connecting', addr.url, counter); + connectTo(addr); }); } } } + + function connectTo(addr: T.WebSocket) { + const facet = Turn.activeFacet; + facet.preventInertCheck(); + const controlEntity = { + message(a0: Assertion): void { + const a = G.toTransportControl(a0); + if (!a) return; + stop {} // ForceDisconnect + }, + }; + let final = false; + function succeed(ws: WebSocket) { + if (final) return; + final = true; + console.log('opened', ws); + on stop { + console.log('closing', ws); + ws.close(); + } + ws.onclose = () => facet.turn(() => { stop {} }); + ws.onerror = () => facet.turn(() => + Turn.active.crash(new Error("WebSocket error"))); + const relay = new Relay.Relay({ + debug, + trustPeer: true, + packetWriter: bs => ws.send(bs), + setup(r: Relay.Relay) { + ws.onmessage = e => facet.turn(() => + r.accept(new Uint8Array(e.data))); + }, + initialOid: 0, + }); + console.log('succeed', addr.url); + at ds { + assert G.TransportConnection({ + "addr": fromJS(addr), + "control": create controlEntity, + "resolved": G.Resolved.accepted(relay.peer!), + }); + } + } + function fail(detail: Assertion) { + if (final) return; + final = true; + console.log('fail', addr.url, detail); + at ds { + assert G.TransportConnection({ + "addr": fromJS(addr), + "control": create controlEntity, + "resolved": G.Resolved.Rejected(G.Rejected(detail)), + }); + } + setTimeout(() => facet.turn(() => { stop {} }), 10000); + } + try { + const ws = new WebSocket(addr.url); + ws.binaryType = 'arraybuffer'; + ws.onopen = () => facet.turn(() => succeed(ws)); + ws.onclose = () => facet.turn(() => fail(Symbol.for('closed'))); + ws.onerror = (e) => facet.turn(() => fail(Symbol.for('websocket-error-event'))); + } catch (e) { + console.error('Failed opening websocket', addr.url, e); + fail(Symbol.for('websocket-exception')); + } + } + + spawn named 'pathResolver' { + at ds { + during Observe({ "pattern": :pattern G.ResolvePath({ + "route": \$routePatValue, + "addr": \_, + "control": \_, + "resolved": \_, + }) }) => { + const route = Q.drop_lit(routePatValue, G.toRoute); + if (!route) return; + const candidates: Dataflow.Field[] = []; + route.transports.forEach(t => { + const addr = T.toWebSocket(t); + if (!addr) return; + let counter = 0; + field state: TransportState | null = null; + candidates.push(state); + console.log('tracking', addr.url); + during G.TransportConnection({ + "addr": addr, + "control": $control: Embedded, + "resolved": $resolved: G.Resolved, + }) => { + const me = counter++; + switch (resolved._variant) { + case "accepted": + state.value = { + addr, + control: control.embeddedValue, + peer: resolved.responderSession, + }; + break; + case "Rejected": + state.value = null; + break; + } + on stop { + if (counter === me) { + state.value = null; + } + } + } + }); + field best: TransportState | null = null; + field rootPeer: Ref | null = null; + dataflow { + best.value = null; + for (const c of candidates) { + if (c.value !== null) { + if (best.value === null) best.value = c.value; + } + } + rootPeer.value = best.value?.peer ?? null; + } + resolve(() => rootPeer.value, route.pathSteps, (r) => { + const s = best.value!; + console.log('leaf', s.addr.url, stringify(r)); + assert G.ResolvePath({ + "route": route, + "addr": fromJS(s.addr), + "control": s.control, + "resolved": r, + }); + }); + } + } + } + + function resolve( + e: () => Ref | null, + steps: G.PathStep[], + k: (r: G.Resolved) => void, + ) { + if (steps.length === 0) { + const peer = e(); + k(peer === null + ? G.Resolved.Rejected(G.Rejected(Symbol.for('not-connected'))) + : G.Resolved.accepted(peer)); + } else { + const [step, ...more] = steps; + at ds { + during G.ResolvedPathStep({ + "origin": (e()!), + "pathStep": step, + "resolved": $resolved: G.Resolved, + }) when (e()) => { + switch (resolved._variant) { + case "accepted": + resolve(() => resolved.responderSession, more, k); + break; + case "Rejected": + k(resolved); + break; + } + } + } + } + } + + spawn named 'noiseStep' { + at ds { + during Observe({ "pattern": :pattern G.ResolvedPathStep({ + "origin": \$originPatValue, + "pathStep": G.PathStep({ + "stepType": N.$noise, + "detail": \$detailPatValue, + }), + "resolved": \_, + }) }) => { + const origin0 = Q.drop_lit(originPatValue); + if (!origin0 || !isEmbedded(origin0)) return; + const origin = origin0.embeddedValue; + + const detail0 = Q.drop_lit(detailPatValue, N.toNoisePathStepDetail); + if (!detail0) return; + const spec: N.NoiseSpec = detail0; + + const algorithms = SaltyCrypto.Noise_25519_ChaChaPoly_BLAKE2s; + const protocol = + spec.protocol._variant === "present" ? spec.protocol.protocol : + spec.protocol._variant === "absent" ? N.fromDefaultProtocol(N.DefaultProtocol()) as string : + (() => { throw new Error("Invalid noise protocol name"); })(); + const patternName = SaltyCrypto.matchPattern(algorithms, protocol); + if (patternName === null) throw new Error("Unsupported protocol " + protocol); + const preSharedKeys = + spec.preSharedKeys._variant === "present" ? spec.preSharedKeys.preSharedKeys : + spec.preSharedKeys._variant === "absent" ? [] : + (() => { throw new Error("Invalid pre-shared keys"); })(); + const prologue = underlying(canonicalEncode(spec.service)); + + const H = new SaltyCrypto.Handshake( + algorithms, + patternName, + 'initiator', + { + prologue, + remoteStaticPublicKey: underlying(spec.key), + preSharedKeys: preSharedKeys.map(underlying), + }); + + let transportState: SaltyCrypto.TransportState | null = null; + let responderSession: Ref | null = null; + let relay: Relay.Relay | null = null; + + function maybeTransition(s: SaltyCrypto.TransportState | null) { + if (transportState !== null) { + throw new Error("Unexpected double-transition to transport state"); + } + transportState = s; + if (transportState !== null) { + const noiseSessionFacet = Turn.activeFacet; + const peer = new Relay.Relay({ + debug, + trustPeer: true, + packetWriter: bs => noiseSessionFacet.turn(() => { + const fragments = transportState!.send.encrypt_large(bs).map(Bytes.from); + at responderSession! { + send message ((fragments.length === 1) + ? N.Packet.complete(fragments[0]) + : N.Packet.fragmented(fragments)); + } + }), + setup(r: Relay.Relay) { + relay = r; + }, + initialOid: 0, + }).peer!; + assert G.ResolvedPathStep({ + "origin": origin, + "pathStep": G.PathStep({ + "stepType": N.$noise, + "detail": fromJS(spec), + }), + "resolved": G.Resolved.accepted(peer), + }); + } + } + + react { + at origin { + assert G.Resolve({ + "step": G.Step({ + "stepType": N.$noise, + "detail": fromJS(N.ServiceSelector(spec.service)), + }), + "observer": create ({ + ... assertionFacetObserver(e => { + const response = G.toResolved(e); + if (!response) return; + switch (response._variant) { + case "accepted": + responderSession = response.responderSession; + const { packet, finished } = H.writeMessage(new Uint8Array()); + at responderSession { + send message Bytes.from(packet); + } + maybeTransition(finished); + break; + case "Rejected": + stop { + at ds { + assert G.ResolvedPathStep({ + "origin": origin, + "pathStep": G.PathStep({ + "stepType": N.$noise, + "detail": fromJS(N.NoisePathStepDetail(spec)), + }), + "resolved": response, + }); + } + } + } + }), + message(body: Assertion) { + const p = N.asPacket(body); + if (transportState) { + const packet = transportState.recv.decrypt_large( + p._variant === 'complete' + ? [underlying(p.value)] + : p.value.map(underlying)); + relay!.accept(packet); + } else { + if (p._variant !== 'complete') { + throw new Error("Unexpected fragmentation in handshake"); + } + const { message, finished } = H.readMessage(underlying(p.value)); + if (message.byteLength !== 0) { + throw new Error("Unexpected payload during handshake"); + } + maybeTransition(finished); + } + }, + }), + }); + } + } + } + } + } + + spawn named 'sturdyRefStep' { + at ds { + during Observe({ "pattern": :pattern G.ResolvedPathStep({ + "origin": \$originPatValue, + "pathStep": G.PathStep({ + "stepType": S.$ref, + "detail": \$detailPatValue, + }), + "resolved": \_, + }) }) => { + const origin0 = Q.drop_lit(originPatValue); + if (!origin0 || !isEmbedded(origin0)) return; + const origin = origin0.embeddedValue; + + const detail0 = Q.drop_lit(detailPatValue, S.toSturdyPathStepDetail); + if (!detail0) return; + const parameters: S.Parameters = detail0; + + at origin { + assert G.Resolve({ + "step": G.Step({ + "stepType": S.$ref, + "detail": fromJS(parameters), + }), + "observer": create assertionFacetObserver(e => { + const response = G.toResolved(e); + if (!response) return; + at ds { + assert G.ResolvedPathStep({ + "origin": origin, + "pathStep": G.PathStep({ + "stepType": S.$ref, + "detail": fromJS(parameters), + }), + "resolved": response, + }); + } + }), + }); + } + } + } + } } diff --git a/yarn.lock b/yarn.lock index 5f03c19..f3e4113 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5804,7 +5804,7 @@ safe-regex-test@^1.0.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -salty-crypto@0.3: +salty-crypto@0.3, salty-crypto@0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/salty-crypto/-/salty-crypto-0.3.1.tgz#1242cd948d8152aff104d0945980996b1602ba0e" integrity sha512-dCbF8/UzMV8oXPmMCHWzSp7u8G6NA+pHbaFBVqVgLVBiyxuwVn9iPbyt9WXT0EoXfL9sKt7U1mV+bgsGw5cV7A==