signify-ts
Version:
Signing at the edge for KERI, ACDC, and KERIA
373 lines (330 loc) • 11.2 kB
text/typescript
import { SaltyCreator } from '../core/manager';
import { Salter, Tier } from '../core/salter';
import { MtrDex } from '../core/matter';
import { Diger } from '../core/diger';
import { incept, rotate, interact } from '../core/eventing';
import { Serder } from '../core/serder';
import { Tholder } from '../core/tholder';
import { Ilks, b, Serials, Versionage } from '../core/core';
import { Verfer } from '../core/verfer';
import { Encrypter } from '../core/encrypter';
import { Decrypter } from '../core/decrypter';
import { Cipher } from '../core/cipher';
import { Seqner } from '../core/seqner';
import { CesrNumber } from '../core/number';
/**
* Agent is a custodial entity that can be used in conjuntion with a local Client to establish the
* KERI "signing at the edge" semantic
*/
export class Agent {
pre: string;
anchor: string;
verfer: Verfer | null;
state: any | null;
sn: number | undefined;
said: string | undefined;
constructor(agent: any) {
this.pre = '';
this.anchor = '';
this.verfer = null;
this.state = null;
this.sn = 0;
this.said = '';
this.parse(agent);
}
private parse(agent: Agent) {
const [state, verfer] = this.event(agent);
this.sn = new CesrNumber({}, undefined, state['s']).num;
this.said = state['d'];
if (state['et'] !== Ilks.dip) {
throw new Error(`invalid inception event type ${state['et']}`);
}
this.pre = state['i'];
if (!state['di']) {
throw new Error('no anchor to controller AID');
}
this.anchor = state['di'];
this.verfer = verfer;
this.state = state;
}
private event(evt: any): [any, Verfer, Diger] {
if (evt['k'].length !== 1) {
throw new Error(`agent inception event can only have one key`);
}
const verfer = new Verfer({ qb64: evt['k'][0] });
if (evt['n'].length !== 1) {
throw new Error(`agent inception event can only have one next key`);
}
const diger = new Diger({ qb64: evt['n'][0] });
const tholder = new Tholder({ sith: evt['kt'] });
if (tholder.num !== 1) {
throw new Error(`invalid threshold ${tholder.num}, must be 1`);
}
const ntholder = new Tholder({ sith: evt['nt'] });
if (ntholder.num !== 1) {
throw new Error(
`invalid next threshold ${ntholder.num}, must be 1`
);
}
return [evt, verfer, diger];
}
}
/**
* Controller is responsible for managing signing keys for the client and agent. The client
* signing key represents the Account for the client on the agent
*/
export class Controller {
private bran: string;
public stem: string;
public tier: Tier;
public ridx: number;
public salter: any;
public signer: any;
private nsigner: any;
public serder: Serder;
private keys: string[];
public ndigs: string[];
constructor(
bran: string,
tier: Tier,
ridx: number = 0,
state: any | null = null
) {
this.bran = MtrDex.Salt_128 + 'A' + bran.substring(0, 21); // qb64 salt for seed
this.stem = 'signify:controller';
this.tier = tier;
this.ridx = ridx;
this.salter = new Salter({ qb64: this.bran, tier: this.tier });
const creator = new SaltyCreator(
this.salter.qb64,
this.tier,
this.stem
);
this.signer = creator
.create(
undefined,
1,
MtrDex.Ed25519_Seed,
true,
0,
this.ridx,
0,
false
)
.signers.pop();
this.nsigner = creator
.create(
undefined,
1,
MtrDex.Ed25519_Seed,
true,
0,
this.ridx + 1,
0,
false
)
.signers.pop();
this.keys = [this.signer.verfer.qb64];
this.ndigs = [
new Diger({ code: MtrDex.Blake3_256 }, this.nsigner.verfer.qb64b)
.qb64,
];
if (state == null || state['ee']['s'] == 0) {
this.serder = incept({
keys: this.keys,
isith: '1',
nsith: '1',
ndigs: this.ndigs,
code: MtrDex.Blake3_256,
toad: '0',
wits: [],
});
} else {
this.serder = new Serder(state['ee']);
}
}
approveDelegation(_agent: Agent) {
const seqner = new Seqner({ sn: _agent.sn });
const anchor = { i: _agent.pre, s: seqner.snh, d: _agent.said };
const sn = new CesrNumber({}, undefined, this.serder.ked['s']).num + 1;
this.serder = interact({
pre: this.serder.pre,
dig: this.serder.ked['d'],
sn: sn,
data: [anchor],
version: Versionage,
kind: Serials.JSON,
});
return [this.signer.sign(this.serder.raw, 0).qb64];
}
get pre(): string {
return this.serder.pre;
}
get event() {
const siger = this.signer.sign(this.serder.raw, 0);
return [this.serder, siger];
}
get verfers(): [] {
return this.signer.verfer();
}
derive(state: any) {
if (state != undefined && state['ee']['s'] === '0') {
return incept({
keys: this.keys,
isith: '1',
nsith: '1',
ndigs: this.ndigs,
code: MtrDex.Blake3_256,
toad: '0',
wits: [],
});
} else {
return new Serder({ ked: state.controller['ee'] });
}
}
rotate(bran: string, aids: Array<any>) {
const nbran = MtrDex.Salt_128 + 'A' + bran.substring(0, 21); // qb64 salt for seed
const nsalter = new Salter({ qb64: nbran, tier: this.tier });
const nsigner = this.salter.signer(undefined, false);
const creator = new SaltyCreator(
this.salter.qb64,
this.tier,
this.stem
);
const signer = creator
.create(
undefined,
1,
MtrDex.Ed25519_Seed,
true,
0,
this.ridx + 1,
0,
false
)
.signers.pop();
const ncreator = new SaltyCreator(nsalter.qb64, this.tier, this.stem);
this.signer = ncreator
.create(
undefined,
1,
MtrDex.Ed25519_Seed,
true,
0,
this.ridx,
0,
false
)
.signers.pop();
this.nsigner = ncreator
.create(
undefined,
1,
MtrDex.Ed25519_Seed,
true,
0,
this.ridx + 1,
0,
false
)
.signers.pop();
this.keys = [this.signer.verfer.qb64, signer?.verfer.qb64];
this.ndigs = [new Diger({}, this.nsigner.verfer.qb64b).qb64];
const rot = rotate({
pre: this.pre,
keys: this.keys,
dig: this.serder.ked['d'],
isith: ['1', '0'],
nsith: '1',
ndigs: this.ndigs,
});
const sigs = [
signer?.sign(b(rot.raw), 1, false, 0).qb64,
this.signer.sign(rot.raw, 0).qb64,
];
const encrypter = new Encrypter({}, b(nsigner.verfer.qb64));
const decrypter = new Decrypter({}, nsigner.qb64b);
const sxlt = encrypter.encrypt(b(this.bran)).qb64;
const keys: Record<any, any> = {};
for (const aid of aids) {
const pre: string = aid['prefix'] as string;
if ('salty' in aid) {
const salty: any = aid['salty'];
const cipher = new Cipher({ qb64: salty['sxlt'] });
const dnxt = decrypter.decrypt(null, cipher).qb64;
// Now we have the AID salt, use it to verify against the current public keys
const acreator = new SaltyCreator(
dnxt,
salty['tier'],
salty['stem']
);
const signers = acreator.create(
salty['icodes'],
undefined,
MtrDex.Ed25519_Seed,
salty['transferable'],
salty['pidx'],
0,
salty['kidx'],
false
);
const _signers = [];
for (const signer of signers.signers) {
_signers.push(signer.verfer.qb64);
}
const pubs = aid['state']['k'];
if (pubs.join(',') != _signers.join(',')) {
throw new Error('Invalid Salty AID');
}
const asxlt = encrypter.encrypt(b(dnxt)).qb64;
keys[pre] = {
sxlt: asxlt,
};
} else if ('randy' in aid) {
const randy = aid['randy'];
const prxs = randy['prxs'];
const nxts = randy['nxts'];
const nprxs = [];
const signers = [];
for (const prx of prxs) {
const cipher = new Cipher({ qb64: prx });
const dsigner = decrypter.decrypt(null, cipher, true);
signers.push(dsigner);
nprxs.push(encrypter.encrypt(b(dsigner.qb64)).qb64);
}
const pubs = aid['state']['k'];
const _signers = [];
for (const signer of signers) {
_signers.push(signer.verfer.qb64);
}
if (pubs.join(',') != _signers.join(',')) {
throw new Error(
`unable to rotate, validation of encrypted public keys ${pubs} failed`
);
}
const nnxts = [];
for (const nxt of nxts) {
nnxts.push(this.recrypt(nxt, decrypter, encrypter));
}
keys[pre] = {
prxs: nprxs,
nxts: nnxts,
};
} else {
throw new Error('invalid aid type ');
}
}
const data = {
rot: rot.ked,
sigs: sigs,
sxlt: sxlt,
keys: keys,
};
return data;
}
recrypt(enc: string, decrypter: Decrypter, encrypter: Encrypter) {
const cipher = new Cipher({ qb64: enc });
const dnxt = decrypter.decrypt(null, cipher).qb64;
return encrypter.encrypt(b(dnxt)).qb64;
}
}