import * as Automerge from 'automerge'; import { JSONSerializable } from './format'; import { Authoritative, Signer, PGPSigner, Signable, Signature } from './auth'; import { engineSpec, cryptoSpec, networkSpec, VersionedSpec } from './constants'; const fullSpec:VersionedSpec = { name: 'cic', version: '1', ext: { network: cryptoSpec, engine: engineSpec, }, } class Envelope { o = fullSpec constructor(payload:Object) { this.set(payload); } public set(payload:Object) { this.o['payload'] = payload } public get():string { return this.o['payload']; } public toJSON() { return JSON.stringify(this.o); } public static fromJSON(s:string): Envelope { const e = new Envelope(undefined); e.o = JSON.parse(s); return e; } public unwrap(): Syncable { return Syncable.fromJSON(this.o['payload']); } } class ArgPair { k:string v:any constructor(k:string, v:any) { this.k = k; this.v = v; } } class SignablePart implements Signable { s: string constructor(s:string) { this.s = s; } public digest():string { return this.s; } } function orderDict(src) { let dst; if (Array.isArray(src)) { dst = []; src.forEach((v) => { if (typeof(v) == 'object') { v = orderDict(v); } dst.push(v); }); } else { dst = {} Object.keys(src).sort().forEach((k) => { let v = src[k]; if (typeof(v) == 'object') { v = orderDict(v); } dst[k] = v; }); } return dst; } class Syncable implements JSONSerializable, Authoritative, Signable { id: string timestamp: number m: any // automerge object e: Envelope signer: Signer onwrap: (string) => void onauthenticate: (boolean) => void // TODO: Move data to sub-object so timestamp, id, signature don't collide constructor(id:string, v:Object) { this.id = id; const o = { 'id': id, 'timestamp': Math.floor(Date.now() / 1000), 'data': v, } //this.m = Automerge.from(v) this.m = Automerge.from(o) } public setSigner(signer:Signer) { this.signer = signer; this.signer.onsign = (s) => { this.wrap(s); }; } // TODO: To keep integrity, the non-link key/value pairs for each step also need to be hashed public digest(): string { const links = []; Automerge.getAllChanges(this.m).forEach((ch:Object) => { const op:Array = ch['ops']; ch['ops'].forEach((op:Array) => { if (op['action'] == 'link') { //console.log('op link', op); links.push([op['obj'], op['value']]); } }); }); //return JSON.stringify(links); const j = JSON.stringify(links); return Buffer.from(j).toString('base64'); } private wrap(s:any) { this.m = Automerge.change(this.m, 'sign', (doc) => { doc['signature'] = s; }); this.e = new Envelope(this.toJSON()); console.log('wrappin s', s, typeof(s)); this.e.o['digest'] = s.digest; if (this.onwrap !== undefined) { this.onwrap(this.e); } } // private _verifyLoop(i:number, history:Array, signable:Signable, result:boolean) { // if (!result) { // this.onauthenticate(false); // return; // } else if (history.length == 0) { // this.onauthenticate(true); // return; // } // const h = history.shift() // if (i % 2 == 0) { // i++; // signable = { // digest: () => { // return Automerge.save(h.snapshot) // }, // }; // this._verifyLoop(i, history, signable, true); // } else { // i++; // const signature = h.snapshot['signature']; // console.debug('signature', signature, signable.digest()); // this.signer.onverify = (v) => { // this._verifyLoop(i, history, signable, v) // } // this.signer.verify(signable, signature); // } // } // // // TODO: This should replay the graph and check signatures on each step // public _authenticate(full:boolean=false) { // let h = Automerge.getHistory(this.m); // h.forEach((m) => { // //console.debug(m.snapshot); // }); // const signable = { // digest: () => { return '' }, // } // if (!full) { // h = h.slice(h.length-2); // } // this._verifyLoop(0, h, signable, true); // } public authenticate(full:boolean=false) { if (full) { console.warn('only doing shallow authentication for now, sorry'); } //console.log('authenticating', signable.digest()); //console.log('signature', this.m.signature); this.signer.onverify = (v) => { //this._verifyLoop(i, history, signable, v) this.onauthenticate(v); } this.signer.verify(this.m.signature.digest, this.m.signature); } public sign() { //this.signer.prepare(this); this.signer.sign(this.digest()); } public update(changes:Array, changesDescription:string) { this.m = Automerge.change(this.m, changesDescription, (m) => { changes.forEach((c) => { let path = c.k.split('.'); let target = m['data']; while (path.length > 1) { const part = path.shift(); target = target[part]; } target[path[0]] = c.v; }); m['timestamp'] = Math.floor(Date.now() / 1000); }); } public replace(o:Object, changesDescription:string) { this.m = Automerge.change(this.m, changesDescription, (m) => { Object.keys(o).forEach((k) => { m['data'][k] = o[k]; }); Object.keys(m).forEach((k) => { if (o[k] == undefined) { delete m['data'][k]; } }); m['timestamp'] = Math.floor(Date.now() / 1000); }); } public merge(s:Syncable) { this.m = Automerge.merge(s.m, this.m); } public toJSON(): string { const s = Automerge.save(this.m); const o = JSON.parse(s); const oo = orderDict(o) return JSON.stringify(oo); } public static fromJSON(s:string): Syncable { const doc = Automerge.load(s); let y = new Syncable(doc['id'], {}); y.m = doc return y } } export { JSONSerializable, Syncable, ArgPair, Envelope };