From c6fff70bae6fc76909ea877d00969729b29d7674 Mon Sep 17 00:00:00 2001 From: Tony Garnock-Jones Date: Wed, 1 Dec 2021 16:27:06 +0100 Subject: [PATCH] WIP moving to novy --- Makefile | 6 + packages/core/src/runtime/actor.ts | 309 ++++++++++++++++++ packages/core/src/runtime/assertions.ts | 62 ---- packages/core/src/runtime/rewrite.ts | 328 ++++++++++++++++++++ packages/core/src/transport/cryptography.ts | 57 ++++ packages/core/src/transport/sturdy.ts | 76 +++++ packages/core/test/skeleton.test.ts | 2 +- veryclean.do | 2 - 8 files changed, 777 insertions(+), 65 deletions(-) create mode 100644 packages/core/src/runtime/actor.ts delete mode 100644 packages/core/src/runtime/assertions.ts create mode 100644 packages/core/src/runtime/rewrite.ts create mode 100644 packages/core/src/transport/cryptography.ts create mode 100644 packages/core/src/transport/sturdy.ts delete mode 100644 veryclean.do diff --git a/Makefile b/Makefile index 38ebe4a..5f3a537 100644 --- a/Makefile +++ b/Makefile @@ -21,3 +21,9 @@ all: watch: inotifytest make -j$$(nproc) all + +pull-protocols: + git subtree pull -P packages/core/protocols \ + -m 'Merge latest changes from the syndicate-protocols repository' \ + git@git.syndicate-lang.org:syndicate-lang/syndicate-protocols \ + main diff --git a/packages/core/src/runtime/actor.ts b/packages/core/src/runtime/actor.ts new file mode 100644 index 0000000..30d19f2 --- /dev/null +++ b/packages/core/src/runtime/actor.ts @@ -0,0 +1,309 @@ +//--------------------------------------------------------------------------- +// @syndicate-lang/core, an implementation of Syndicate dataspaces for JS. +// Copyright (C) 2016-2021 Tony Garnock-Jones +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +//--------------------------------------------------------------------------- + +import { IdentitySet, Value } from '@preserves/core'; +import { Attenuation, runRewrites } from './rewrite.js'; +import { queueTask } from './task.js'; + +export type AnyValue = Value; + +//--------------------------------------------------------------------------- + +if ('stackTraceLimit' in Error) { + Error.stackTraceLimit = Infinity; +} + +export type Assertion = Value; +export type Handle = number; +export type ExitReason = null | { ok: true } | { ok: false, err: Error }; +export type LocalAction = (t: Turn) => void; + +export interface Entity { + assert(turn: Turn, assertion: Assertion, handle: Handle): void; + retract(turn: Turn, handle: Handle): void; + message(turn: Turn, body: Assertion): void; + sync(turn: Turn, peer: Ref): void; +} + +export type Cap = Ref; + +export interface Ref { + readonly relay: Facet; + readonly target: Partial; + readonly attenuation?: Attenuation; +} + +//--------------------------------------------------------------------------- + +export function isRef(v: any): v is Ref { + return 'relay' in v && v.relay instanceof Facet && 'target' in v; +} + +export function toRef(_v: any): Ref | undefined { + return isRef(_v) ? _v : void 0; +} + +type OutboundAssertion = { handle: Handle, peer: Ref, established: boolean }; +type OutboundMap = Map; + +let nextActorId = 0; +export const __setNextActorId = (v: number) => nextActorId = v; + +export class Actor { + readonly id = nextActorId++; + readonly root: Facet; + exitReason: ExitReason = null; + readonly exitHooks: Array = []; + + constructor(bootProc: LocalAction, initialAssertions: OutboundMap = new Map()) { + this.root = new Facet(this, null, initialAssertions); + Turn.for(new Facet(this, this.root), stopIfInertAfter(bootProc)); + } + + atExit(a: LocalAction): void { + this.exitHooks.push(a); + } + + terminateWith(t: Turn, reason: Exclude) { + if (this.exitReason !== null) return; + this.exitReason = reason; + if (!reason.ok) { + console.error(`Actor ${this.id} crashed:`, reason.err); + } + this.exitHooks.forEach(hook => hook(t)); + queueTask(() => Turn.for(this.root, t => this.root._terminate(t, reason.ok), true)); + } +} + +export class Facet { + readonly id = nextActorId++; + readonly actor: Actor; + readonly parent: Facet | null; + readonly children = new Set(); + readonly outbound: OutboundMap; + readonly shutdownActions: Array = []; + // ^ shutdownActions are not exitHooks - those run even on error. These are for clean shutdown + isLive = true; + inertCheckPreventers = 0; + + constructor(actor: Actor, parent: Facet | null, initialAssertions: OutboundMap = new Map()) { + this.actor = actor; + this.parent = parent; + if (parent) parent.children.add(this); + this.outbound = initialAssertions; + } + + onStop(a: LocalAction): void { + this.shutdownActions.push(a); + } + + isInert(): boolean { + return this.children.size === 0 && this.outbound.size === 0 && this.inertCheckPreventers === 0; + } + + preventInertCheck(): () => void { + let armed = true; + this.inertCheckPreventers++; + return () => { + if (!armed) return; + armed = false; + this.inertCheckPreventers--; + }; + } + + _terminate(t: Turn, orderly: boolean): void { + if (!this.isLive) return; + this.isLive = false; + + const parent = this.parent; + if (parent) parent.children.delete(this); + + t._inFacet(this, t => { + this.children.forEach(child => child._terminate(t, orderly)); + if (orderly) this.shutdownActions.forEach(a => a(t)); + this.outbound.forEach(e => t._retract(e)); + + if (orderly) { + queueTask(() => { + if (parent) { + if (parent.isInert()) { + Turn.for(parent, t => parent._terminate(t, true)); + } + } else { + Turn.for(this.actor.root, t => this.actor.terminateWith(t, { ok: true }), true); + } + }); + } + }); + } +} + +export function _sync_impl(turn: Turn, e: Partial, peer: Ref): void { + e.sync ? e.sync!(turn, peer) : turn.message(peer, true); +} + +let nextHandle = 0; +let nextTurnId = 0; + +export class Turn { + readonly id = nextTurnId++; + readonly activeFacet: Facet; + queues: Map | null; + + static for(facet: Facet, f: LocalAction, zombieTurn = false): void { + if (!zombieTurn) { + if (facet.actor.exitReason !== null) return; + if (!facet.isLive) return; + } + const t = new Turn(facet); + try { + f(t); + t.queues!.forEach((q, facet) => queueTask(() => Turn.for(facet, t=> q.forEach(f => f(t))))); + t.queues = null; + } catch (err) { + Turn.for(facet.actor.root, t => facet.actor.terminateWith(t, { ok: false, err })); + } + } + + private constructor(facet: Facet, queues = new Map()) { + this.activeFacet = facet; + this.queues = queues; + } + + _inFacet(facet: Facet, f: LocalAction): void { + const t = new Turn(facet, this.queues!); + f(t); + t.queues = null; + } + + ref>(e: T): Ref { + return { relay: this.activeFacet, target: e }; + } + + facet(bootProc: LocalAction): Facet { + const newFacet = new Facet(this.activeFacet.actor, this.activeFacet); + this._inFacet(newFacet, stopIfInertAfter(bootProc)); + return newFacet; + } + + stop(facet: Facet = this.activeFacet, continuation?: LocalAction) { + this.enqueue(facet.parent!, t => { + facet._terminate(t, true); + if (continuation) continuation(t); + }); + } + + spawn(bootProc: LocalAction, initialAssertions = new IdentitySet()): void { + this.enqueue(this.activeFacet, () => { + const newOutbound: OutboundMap = new Map(); + initialAssertions.forEach(key => { + newOutbound.set(key, this.activeFacet.outbound.get(key)!); // we trust initialAssertions + this.activeFacet.outbound.delete(key); + }); + queueTask(() => new Actor(bootProc, newOutbound)); + }); + } + + stopActor(): void { + this.enqueue(this.activeFacet.actor.root, t => this.activeFacet.actor.terminateWith(t, { ok: true })); + } + + crash(err: Error): void { + this.enqueue(this.activeFacet.actor.root, t => this.activeFacet.actor.terminateWith(t, { ok: false, err })); + } + + assert(ref: Ref, assertion: Assertion): Handle { + const h = nextHandle++; + this._assert(ref, assertion, h); + return h; + } + + _assert(ref: Ref, assertion: Assertion, h: Handle) { + const a = runRewrites(ref.attenuation, assertion); + if (a !== null) { + const e = { handle: h, peer: ref, established: false }; + this.activeFacet.outbound.set(h, e); + this.enqueue(ref.relay, t => { + e.established = true; + ref.target.assert?.(t, a, h); + }); + } + } + + retract(h: Handle | undefined): void { + if (h !== void 0) { + const e = this.activeFacet.outbound.get(h); + if (e === void 0) return; + this._retract(e); + } + } + + replace(ref: Ref, h: Handle | undefined, assertion: Assertion | undefined): Handle | undefined { + const newHandle = assertion === void 0 ? void 0 : this.assert(ref, assertion); + this.retract(h); + return newHandle; + } + + _retract(e: OutboundAssertion): void { + this.activeFacet.outbound.delete(e.handle); + this.enqueue(e.peer.relay, t => { + if (e.established) { + e.established = false; + e.peer.target.retract?.(t, e.handle); + } + }); + } + + sync(ref: Ref): Promise { + return new Promise(resolve => this._sync(ref, this.ref({ message: resolve }))); + } + + _sync(ref: Ref, peer: Ref): void { + this.enqueue(ref.relay, t => _sync_impl(t, ref.target, peer)); + } + + message(ref: Ref, assertion: Assertion): void { + const a = runRewrites(ref.attenuation, assertion); + if (a !== null) this.enqueue(ref.relay, t => ref.target.message?.(t, assertion)); + } + + enqueue(relay: Facet, a: LocalAction): void { + if (this.queues === null) { + throw new Error("Attempt to reuse a committed Turn"); + } + this.queues.get(relay)?.push(a) ?? this.queues.set(relay, [a]); + } + + freshen(a: LocalAction): void { + if (this.queues !== null) { + throw new Error("Attempt to freshen a non-stale Turn"); + } + Turn.for(this.activeFacet, a); + } +} + +function stopIfInertAfter(a: LocalAction): LocalAction { + return t => { + a(t); + t.enqueue(t.activeFacet, t => { + if ((t.activeFacet.parent && !t.activeFacet.parent.isLive) || t.activeFacet.isInert()) { + t.stop(); + } + }); + }; +} diff --git a/packages/core/src/runtime/assertions.ts b/packages/core/src/runtime/assertions.ts deleted file mode 100644 index 15b3179..0000000 --- a/packages/core/src/runtime/assertions.ts +++ /dev/null @@ -1,62 +0,0 @@ -//--------------------------------------------------------------------------- -// @syndicate-lang/core, an implementation of Syndicate dataspaces for JS. -// Copyright (C) 2016-2021 Tony Garnock-Jones -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -//--------------------------------------------------------------------------- - -import { Record, RecordConstructor, AsPreserve, Value } from '@preserves/core'; - -export class Seal { - readonly contents: any; - - constructor(contents: any) { - this.contents = contents; - } - - [AsPreserve](): any { // should return Value; we are cheating - return this; - } -} - -interface Discard extends RecordConstructor { - _instance: Record; -} - -const _Discard = Symbol.for('discard'); -export const Discard: Discard = (function () { - let Discard: any = Record.makeConstructor<{}>()(_Discard, []); - Discard._instance = Discard(); - return Discard; -})(); - -export const _Capture = Symbol.for('capture'); -export const Capture = Record.makeConstructor<{specification: Value}>()( - _Capture, ['specification']); - -export const _Observe = Symbol.for('observe'); -export const Observe = Record.makeConstructor<{specification: Value}>()( - _Observe, ['specification']); - -export const _Inbound = Symbol.for('inbound'); -export const Inbound = Record.makeConstructor<{assertion: Value}>()( - _Inbound, ['assertion']); - -export const _Outbound = Symbol.for('outbound'); -export const Outbound = Record.makeConstructor<{assertion: Value}>()( - _Outbound, ['assertion']); - -export const _Instance = Symbol.for('instance'); -export const Instance = Record.makeConstructor<{uniqueId: Value}>()( - _Instance, ['uniqueId']); diff --git a/packages/core/src/runtime/rewrite.ts b/packages/core/src/runtime/rewrite.ts new file mode 100644 index 0000000..488552f --- /dev/null +++ b/packages/core/src/runtime/rewrite.ts @@ -0,0 +1,328 @@ +//--------------------------------------------------------------------------- +// @syndicate-lang/core, an implementation of Syndicate dataspaces for JS. +// Copyright (C) 2016-2021 Tony Garnock-Jones +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +//--------------------------------------------------------------------------- + +import type { Assertion, Handle, Ref, Turn } from "./actor.js"; +import { Bytes, Dictionary, DoubleFloat, embed, IdentityMap, is, isEmbedded, Record, SingleFloat, Tuple } from "@preserves/core"; +import { SturdyValue } from "../transport/sturdy.js"; + +import { + Alts, + Attenuation, + CArr, + CDict, + CRec, + Caveat, + ConstructorSpec, + Lit, + PAnd, + PAtom, + PBind, + PCompound, + PCompoundMembers, + PDiscard, + PNot, + PEmbedded, + Pattern, + Rewrite, + TRef, + Template, + _embedded, +} from '../gen/sturdy.js'; +export * from '../gen/sturdy.js'; + +export type Bindings = Array; + +export function match(p: Pattern, v: Assertion): Bindings | null { + let bindings: Bindings = []; + + function walk(p: Pattern, v: Assertion): boolean { + switch (p._variant) { + case 'PDiscard': + return true; + case 'PAtom': + switch (p.value._variant) { + case 'Boolean': return typeof v === 'boolean'; + case 'ByteString': return Bytes.isBytes(v); + case 'Double': return DoubleFloat.isDouble(v); + case 'Float': return SingleFloat.isSingle(v); + case 'SignedInteger': return typeof v === 'number'; + case 'String': return typeof v === 'string'; + case 'Symbol': return typeof v === 'symbol'; + } + case 'PEmbedded': + return isEmbedded(v); + case 'PBind': + if (walk(p.value.pattern, v)) { + bindings.push(v); + return true; + } + return false; + case 'PAnd': + for (const pp of p.value.patterns) { + if (!walk(pp, v)) return false; + } + return true; + case 'PNot': { + const savedBindings = bindings; + bindings = []; + const result = !walk(p.value.pattern, v) + bindings = savedBindings; + return result; + } + case 'Lit': + return is(p.value.value, v); + case 'PCompound': { + const ctor = p.value.ctor; + const members = p.value.members; + switch (ctor._variant) { + case 'CRec': + if (!Record.isRecord, Ref>(v)) return false; + if (!is(ctor.value.label, v.label)) return false; + if (ctor.value.arity !== v.length) return false; + for (const [key, pp] of members) { + if (typeof key !== 'number') return false; + if (!walk(pp, v[key])) return false; + } + return true; + case 'CArr': + if (!Array.isArray(v)) return false; + if ('label' in v) return false; + if (ctor.value.arity !== v.length) return false; + for (const [key, pp] of members) { + if (typeof key !== 'number') return false; + if (!walk(pp, v[key])) return false; + } + return true; + case 'CDict': + if (!Dictionary.isDictionary(v)) return false; + for (const [key, pp] of members) { + const vv = v.get(key); + if (vv === void 0) return false; + if (!walk(pp, vv)) return false; + } + return true; + } + } + default: + ((_p : never) => {})(p); + return false; + } + } + + return walk(p, v) ? bindings : null; +} + +export function instantiate(t: Template, b: Bindings): Assertion { + function walk(t: Template): Assertion { + switch (t._variant) { + case 'TAttenuate': { + const v = walk(t.value.template); + if (!isEmbedded(v)) { + throw new Error(`Attempt to attenuate non-capability: ${v.asPreservesText()}`); + } + const r = v.embeddedValue; + return embed(attenuate(r, ... t.value.attenuation)); + } + case 'TRef': { + const n = t.value.binding; + const v = b[n]; + if (v === void 0) throw new Error(`Unbound reference: ${n}`); + return v; + } + case 'Lit': + return t.value.value; + case 'TCompound': { + const ctor = t.value.ctor; + const members = t.value.members; + switch (ctor._variant) { + case 'CRec': { + const v = [] as unknown as Record; + v.length = ctor.value.arity; + v.label = ctor.value.label; + for (const [key, tt] of members) { + v[key as number] = walk(tt); + } + return v; + } + case 'CArr': { + const v = []; + v.length = ctor.value.arity; + for (const [key, tt] of members) { + v[key as number] = walk(tt); + } + return v; + } + case 'CDict': { + const v = new Dictionary(); + for (const [key, tt] of members) { + v.set(key, walk(tt)); + } + return v; + } + } + } + } + } + + return walk(t); +} + +export function rewrite(r: Rewrite, v: Assertion): Assertion | null { + const bindings = match(r.pattern, v); + if (bindings === null) return null; + return instantiate(r.template, bindings); +} + +export function examineAlternatives(cav: Caveat, v: Assertion): Assertion | null { + if (cav._variant === 'Alts') { + for (const r of cav.value.alternatives) { + const w = rewrite(r, v); + if (w !== null) return w; + } + return null; + } else { + return rewrite(cav.value, v); + } +} + +export function runRewrites(a: Attenuation | undefined, v: Assertion): Assertion | null { + if (a !== void 0) { + for (const stage of a) { + const w = examineAlternatives(stage, v); + if (w === null) return null; + v = w; + } + } + return v; +} + +export function rfilter(... patterns: Pattern[]): Caveat { + const ps = patterns.map(p => Rewrite({ + pattern: Pattern.PBind(PBind(p)), + template: Template.TRef(TRef(0)) + })); + return ps.length === 1 ? Caveat.Rewrite(ps[0]) : Caveat.Alts(Alts(ps)); +} + +export function attenuate(ref: Ref, ... a: Attenuation): Ref { + if (a.length === 0) return ref; + return { ... ref, attenuation: [... a, ... (ref.attenuation ?? [])] }; +} + +export function forwarder(t: Turn, ref: Ref): { proxy: Ref, revoker: Ref } { + let underlying: Ref | null = ref; + let handleMap = new IdentityMap(); + let proxy = t.ref({ + assert(turn: Turn, assertion: Assertion, handle: Handle): void { + if (underlying === null) return; + handleMap.set(handle, turn.assert(underlying, assertion)); + }, + retract(turn: Turn, handle: Handle): void { + if (underlying === null) return; + turn.retract(handleMap.get(handle)); + handleMap.delete(handle); + }, + message(turn: Turn, body: Assertion): void { + if (underlying === null) return; + turn.message(underlying, body); + }, + sync(turn: Turn, peer: Ref): void { + if (underlying === null) return; + turn._sync(underlying, peer); + }, + }); + let revoker = t.ref({ + message(turn: Turn, _body: Assertion): void { + underlying = null; + handleMap.forEach(h => turn.retract(h)); + }, + }); + return { proxy, revoker }; +} + +export function pRec(label: SturdyValue, ... members: Array): Pattern { + return Pattern.PCompound(PCompound({ + ctor: ConstructorSpec.CRec(CRec({ label: label, arity: members.length })), + members: PCompoundMembers(new Dictionary<_embedded, Pattern>( + members.map((p, i) => [i, p] as const).filter(e => e[1]._variant !== 'PDiscard')))})); +} + +export function pArr(... members: Array): Pattern { + return Pattern.PCompound(PCompound({ + ctor: ConstructorSpec.CArr(CArr(members.length)), + members: PCompoundMembers(new Dictionary<_embedded, Pattern>( + members.map((p, i) => [i, p] as const).filter(e => e[1]._variant !== 'PDiscard')))})); +} + +export function pDict(... entries: [SturdyValue, Pattern][]): Pattern { + return Pattern.PCompound(PCompound({ + ctor: ConstructorSpec.CDict(CDict()), + members: PCompoundMembers(new Dictionary<_embedded, Pattern>(entries))})); +} + +export function pLit(value: SturdyValue): Pattern { + return Pattern.Lit(Lit(value)); +} + +export function pNot(p: Pattern): Pattern { + return Pattern.PNot(PNot(p)); +} + +export function pAnd(... ps: Pattern[]): Pattern { + return Pattern.PAnd(PAnd(ps)); +} + +export function pBind(pattern: Pattern): Pattern { + return Pattern.PBind(PBind(pattern)); +} + +export function pEmbedded(): Pattern { + return Pattern.PEmbedded(PEmbedded()); +} + +export function pSymbol(): Pattern { + return Pattern.PAtom(PAtom.Symbol()); +} + +export function pByteString(): Pattern { + return Pattern.PAtom(PAtom.ByteString()); +} + +export function pString(): Pattern { + return Pattern.PAtom(PAtom.String()); +} + +export function pSignedInteger(): Pattern { + return Pattern.PAtom(PAtom.SignedInteger()); +} + +export function pDouble(): Pattern { + return Pattern.PAtom(PAtom.Double()); +} + +export function pFloat(): Pattern { + return Pattern.PAtom(PAtom.Float()); +} + +export function pBoolean(): Pattern { + return Pattern.PAtom(PAtom.Boolean()); +} + +export function pDiscard(): Pattern { + return Pattern.PDiscard(PDiscard()); +} diff --git a/packages/core/src/transport/cryptography.ts b/packages/core/src/transport/cryptography.ts new file mode 100644 index 0000000..50e5271 --- /dev/null +++ b/packages/core/src/transport/cryptography.ts @@ -0,0 +1,57 @@ +//--------------------------------------------------------------------------- +// @syndicate-lang/core, an implementation of Syndicate dataspaces for JS. +// Copyright (C) 2016-2021 Tony Garnock-Jones +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +//--------------------------------------------------------------------------- + +import { Bytes, underlying } from '@preserves/core'; +import * as node_crypto from 'crypto'; + +export const KEY_LENGTH = 16; // 128 bits + +export const newKey: () => Promise = + (typeof crypto !== 'undefined' && 'getRandomValues' in crypto) + ? (async () => { + const bs = new Bytes(KEY_LENGTH); + crypto.getRandomValues(underlying(bs)); + return bs; + }) + : (async () => Bytes.from(node_crypto.randomBytes(KEY_LENGTH))); + +export const mac: (secretKey: Bytes, data: Bytes) => Promise = + (typeof crypto !== 'undefined' && 'subtle' in crypto) + ? (async (secretKey, data) => { + if (secretKey.length !== KEY_LENGTH) throw new Error("Invalid key length"); + const k = await window.crypto.subtle.importKey( + "raw", + underlying(secretKey), + { + hash: 'SHA-256', + name: 'HMAC', + }, + false, + ['sign']); + const bs = await window.crypto.subtle.sign({ name: 'HMAC' }, k, underlying(data)); + return Bytes.from(new Uint8Array(bs, 0, KEY_LENGTH)); + }) + : (typeof node_crypto.createHmac !== 'undefined') + ? (async (secretKey, data) => { + const hmac = node_crypto.createHmac('sha256', underlying(secretKey)); + hmac.update(underlying(data)); + return Bytes.from(hmac.digest().subarray(0, KEY_LENGTH)); + }) + : (async (_secretKey, _data) => { + throw new Error('No HMAC SHA-256 available'); + }); diff --git a/packages/core/src/transport/sturdy.ts b/packages/core/src/transport/sturdy.ts new file mode 100644 index 0000000..d57cf22 --- /dev/null +++ b/packages/core/src/transport/sturdy.ts @@ -0,0 +1,76 @@ +//--------------------------------------------------------------------------- +// @syndicate-lang/core, an implementation of Syndicate dataspaces for JS. +// Copyright (C) 2016-2021 Tony Garnock-Jones +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +//--------------------------------------------------------------------------- + +// Basically Macaroons [1] in a Dataspace context +// +// [1]: Birgisson, Arnar, Joe Gibbs Politz, Úlfar Erlingsson, Ankur +// Taly, Michael Vrable, and Mark Lentczner. “Macaroons: Cookies with +// Contextual Caveats for Decentralized Authorization in the Cloud.” +// In Network and Distributed System Security Symposium. San Diego, +// California: Internet Society, 2014. + +import { mac } from './cryptography'; +import { Bytes, decode, encode, is, neverEmbeddedType, Value } from '@preserves/core'; +import * as S from '../gen/sturdy.js'; +export * from '../gen/sturdy.js'; + +export type SturdyValue = Value; + +export const KEY_LENGTH = 16; // 128 bits + +export function embeddedNotAllowed(): never { + throw new Error("Embedded Ref not permitted in SturdyRef"); +} + +export function sturdyEncode(v: SturdyValue): Bytes { + return encode(v, { + canonical: true, + includeAnnotations: false, + embeddedEncode: neverEmbeddedType, + }); +} + +export function sturdyDecode(bs: Bytes): SturdyValue { + return decode(bs, { + includeAnnotations: false, + embeddedDecode: neverEmbeddedType, + }); +} + +export async function mint(oid: SturdyValue, secretKey: Bytes): Promise { + return S.SturdyRef({ + oid, + caveatChain: [], + sig: await mac(secretKey, sturdyEncode(oid)), + }); +} + +export async function attenuate(r: S.SturdyRef, ... a: S.Attenuation): Promise { + return S.SturdyRef({ + oid: r.oid, + caveatChain: [... r.caveatChain, a], + sig: await mac(r.sig, sturdyEncode(S.fromAttenuation(a))) + }); +} + +export async function validate(r: S.SturdyRef, secretKey: Bytes): Promise { + const sig = await r.caveatChain.reduce( + async (sig, a) => mac(await sig, sturdyEncode(S.fromAttenuation(a))), + mac(secretKey, sturdyEncode(r.oid))); + return is(sig, r.sig); +} diff --git a/packages/core/test/skeleton.test.ts b/packages/core/test/skeleton.test.ts index dda4558..f2db79b 100644 --- a/packages/core/test/skeleton.test.ts +++ b/packages/core/test/skeleton.test.ts @@ -21,7 +21,7 @@ const assert = require('assert'); const Immutable = require('immutable'); const Syndicate = require('../src/index.js'); -const { Seal, Skeleton, Capture, Discard, Record, Observe } = Syndicate; +const { Skeleton, Capture, Discard, Record, Observe } = Syndicate; const __ = Discard(); const _$ = Capture(Discard()); diff --git a/veryclean.do b/veryclean.do deleted file mode 100644 index a3b6bcf..0000000 --- a/veryclean.do +++ /dev/null @@ -1,2 +0,0 @@ -for p in packages/*/; do echo $p/veryclean; done | xargs redo -rm -rf node_modules