signify-ts
Version:
Signing at the edge for KERI, ACDC, and KERIA
723 lines (656 loc) • 21.5 kB
text/typescript
import { Salter } from './salter';
import { Algos, SaltyCreator, RandyCreator } from './manager';
import { MtrDex } from './matter';
import { Tier } from './salter';
import { Encrypter } from '../core/encrypter';
import { Decrypter } from './decrypter';
import { b } from './core';
import { Cipher } from './cipher';
import { Diger } from './diger';
import { Prefixer } from './prefixer';
import { Signer } from './signer';
import { HabState, State } from './state';
/** External module definition */
export interface ExternalModuleType {
new (pidx: number, args: KeeperParams): Keeper;
}
export interface ExternalModule {
type: string;
name: string;
module: ExternalModuleType;
}
export type KeeperResult = [string[], string[]];
export type SignResult = string[];
export interface KeeperParams {
[key: string]: unknown;
}
export interface SaltyParams extends KeeperParams {
pidx: number;
kidx: number;
tier: Tier;
transferable: boolean;
stem: string | undefined;
icodes: string[] | undefined;
ncodes: string[] | undefined;
dcode: string | undefined;
sxlt: string | undefined;
}
export interface RandyParams extends KeeperParams {
nxts?: string[];
prxs?: string[];
transferable: boolean;
}
export interface GroupParams extends KeeperParams {
mhab: HabState;
}
export interface Keeper<T extends KeeperParams = KeeperParams> {
algo: Algos;
signers: Signer[];
params(): T;
incept(transferable: boolean): Promise<KeeperResult>;
rotate(
ncodes: string[],
transferable: boolean,
states?: State[],
rstates?: State[]
): Promise<KeeperResult>;
sign(
ser: Uint8Array,
indexed?: boolean,
indices?: number[],
ondices?: number[]
): Promise<SignResult>;
}
export class KeyManager {
private modules: Record<string, ExternalModuleType> = {};
constructor(
private salter: Salter,
externalModules: ExternalModule[] = []
) {
this.salter = salter;
for (const mod of externalModules) {
this.modules[mod.type] = mod.module;
}
}
new(algo: Algos, pidx: number, kargs: any) {
switch (algo) {
case Algos.salty:
return new SaltyKeeper(
this.salter!,
pidx,
kargs['kidx'],
kargs['tier'],
kargs['transferable'],
kargs['stem'],
kargs['code'],
kargs['count'],
kargs['icodes'],
kargs['ncode'],
kargs['ncount'],
kargs['ncodes'],
kargs['dcode'],
kargs['bran'],
kargs['sxlt']
);
case Algos.randy:
return new RandyKeeper(
this.salter!,
kargs['code'],
kargs['count'],
kargs['icodes'],
kargs['transferable'],
kargs['ncode'],
kargs['ncount'],
kargs['ncodes'],
kargs['dcode'],
kargs['prxs'],
kargs['nxts']
);
case Algos.group:
return new GroupKeeper(
this,
kargs['mhab'],
kargs['states'],
kargs['rstates'],
kargs['keys'],
kargs['ndigs']
);
case Algos.extern: {
const ModuleConstructor = this.modules[kargs.extern_type];
if (!ModuleConstructor) {
throw new Error(
`unsupported external module type ${kargs.extern_type}`
);
}
return new ModuleConstructor(pidx, kargs);
}
default:
throw new Error('Unknown algo');
}
}
get(aid: HabState): Keeper {
if (aid[Algos.salty]) {
const kargs = aid[Algos.salty];
return new SaltyKeeper(
this.salter,
kargs['pidx'],
kargs['kidx'],
kargs['tier'],
kargs['transferable'],
kargs['stem'],
undefined,
undefined,
kargs['icodes'],
undefined,
undefined,
kargs['ncodes'],
kargs['dcode'],
undefined,
kargs['sxlt']
);
} else if (aid[Algos.randy]) {
const pre = new Prefixer({ qb64: aid['prefix'] });
const kargs = aid[Algos.randy]!;
return new RandyKeeper(
this.salter,
undefined,
undefined,
undefined,
pre.transferable,
undefined,
undefined,
[],
undefined,
kargs['prxs'],
kargs['nxts']
);
} else if (aid[Algos.group]) {
const kargs = aid[Algos.group];
return new GroupKeeper(
this,
kargs['mhab'],
undefined,
undefined,
kargs['keys'],
kargs['ndigs']
);
} else if (aid[Algos.extern]) {
const kargs = aid[Algos.extern];
const typ = kargs.extern_type;
if (typ in this.modules) {
const mod = new this.modules[typ](kargs['pidx'], kargs);
return mod;
} else {
throw new Error(`unsupported external module type ${typ}`);
}
} else {
throw new Error(`Algo not allowed yet`);
}
}
}
export class SaltyKeeper implements Keeper {
private aeid: string;
private encrypter: Encrypter;
private decrypter: Decrypter;
private salter: Salter;
private pidx: number;
private kidx: number;
private tier: Tier;
private transferable: boolean;
private stem: string | undefined;
private code: string;
private count: number;
private icodes: string[] | undefined;
private ncode: string;
private ncount: number;
private ncodes: string[] | undefined;
private dcode: string | undefined;
private sxlt: string | undefined;
private bran: string | undefined;
private creator: SaltyCreator;
public algo: Algos = Algos.salty;
public signers: Signer[];
constructor(
salter: Salter,
pidx: number,
kidx: number = 0,
tier = Tier.low,
transferable = false,
stem: string | undefined = undefined,
code = MtrDex.Ed25519_Seed,
count = 1,
icodes: string[] | undefined = undefined,
ncode = MtrDex.Ed25519_Seed,
ncount = 1,
ncodes: string[] | undefined = undefined,
dcode = MtrDex.Blake3_256,
bran: string | undefined = undefined,
sxlt: string | undefined = undefined
) {
// # Salter is the entered passcode and used for enc/dec of salts for each AID
this.salter = salter;
const signer = this.salter.signer(code, transferable, undefined, tier);
this.aeid = signer.verfer.qb64;
this.encrypter = new Encrypter({}, b(this.aeid));
this.decrypter = new Decrypter({}, signer.qb64b);
this.code = code;
this.ncode = ncode;
this.tier = tier;
this.icodes =
icodes == undefined ? new Array<string>(count).fill(code) : icodes;
this.ncodes =
ncodes == undefined
? new Array<string>(ncount).fill(ncode)
: ncodes;
this.dcode = dcode;
this.pidx = pidx;
this.kidx = kidx;
this.transferable = transferable;
this.count = count;
this.ncount = ncount;
this.stem = stem == undefined ? 'signify:aid' : stem;
if (bran != undefined) {
this.bran = MtrDex.Salt_128 + 'A' + bran!.slice(0, 21);
this.creator = new SaltyCreator(this.bran, this.tier, this.stem);
this.sxlt = this.encrypter.encrypt(b(this.creator.salt)).qb64;
} else if (sxlt == undefined) {
this.creator = new SaltyCreator(undefined, this.tier, this.stem);
this.sxlt = this.encrypter.encrypt(b(this.creator.salt)).qb64;
} else {
this.sxlt = sxlt;
const ciph = new Cipher({ qb64: this.sxlt });
this.creator = new SaltyCreator(
this.decrypter.decrypt(null, ciph).qb64,
tier,
this.stem
);
}
this.signers = this.creator.create(
this.icodes,
this.ncount,
this.ncode,
this.transferable,
this.pidx,
0,
this.kidx,
false
).signers;
}
params(): SaltyParams {
return {
sxlt: this.sxlt,
pidx: this.pidx,
kidx: this.kidx,
stem: this.stem,
tier: this.tier,
icodes: this.icodes,
ncodes: this.ncodes,
dcode: this.dcode,
transferable: this.transferable,
};
}
async incept(transferable: boolean): Promise<KeeperResult> {
this.transferable = transferable;
this.kidx = 0;
const signers = this.creator.create(
this.icodes,
this.count,
this.code,
this.transferable,
this.pidx,
0,
this.kidx,
false
);
const verfers = signers.signers.map((signer) => signer.verfer.qb64);
const nsigners = this.creator.create(
this.ncodes,
this.ncount,
this.ncode,
this.transferable,
this.pidx,
0,
this.icodes?.length,
false
);
const digers = nsigners.signers.map(
(nsigner) =>
new Diger({ code: this.dcode }, nsigner.verfer.qb64b).qb64
);
return [verfers, digers];
}
async rotate(
ncodes: string[],
transferable: boolean
): Promise<[string[], string[]]> {
this.ncodes = ncodes;
this.transferable = transferable;
const signers = this.creator.create(
this.ncodes,
this.ncount,
this.ncode,
this.transferable,
this.pidx,
0,
this.kidx + this.icodes!.length,
false
);
const verfers = signers.signers.map((signer) => signer.verfer.qb64);
this.kidx = this.kidx! + this.icodes!.length;
const nsigners = this.creator.create(
this.ncodes,
this.ncount,
this.ncode,
this.transferable,
this.pidx,
0,
this.kidx + this.icodes!.length,
false
);
const digers = nsigners.signers.map(
(nsigner) =>
new Diger({ code: this.dcode }, nsigner.verfer.qb64b).qb64
);
return [verfers, digers];
}
async sign(
ser: Uint8Array,
indexed = true,
indices: number[] | undefined = undefined,
ondices: number[] | undefined = undefined
): Promise<SignResult> {
const signers = this.creator.create(
this.icodes,
this.ncount,
this.ncode,
this.transferable,
this.pidx,
0,
this.kidx,
false
);
if (indexed) {
const sigers = [];
let i = 0;
for (const [j, signer] of signers.signers.entries()) {
if (indices != undefined) {
i = indices![j];
if (typeof i != 'number' || i < 0) {
throw new Error(
`Invalid signing index = ${i}, not whole number.`
);
}
} else {
i = j;
}
let o = 0;
if (ondices != undefined) {
o = ondices![j];
if (
(o == undefined ||
(typeof o == 'number' &&
typeof o != 'number' &&
o >= 0))!
) {
throw new Error(
`Invalid ondex = ${o}, not whole number.`
);
}
} else {
o = i;
}
sigers.push(
signer.sign(ser, i, o == undefined ? true : false, o)
);
}
return sigers.map((siger) => siger.qb64);
} else {
const cigars = [];
for (const [, signer] of signers.signers.entries()) {
cigars.push(signer.sign(ser));
}
return cigars.map((cigar) => cigar.qb64);
}
}
}
export class RandyKeeper implements Keeper {
private salter: Salter;
private code: string;
private count: number;
private icodes: string[] | undefined;
private transferable: boolean;
private ncount: number;
private ncodes: string[] | undefined;
private ncode: string;
private dcode: string | undefined;
private prxs: string[] | undefined;
private nxts: string[] | undefined;
private aeid: string;
private encrypter: Encrypter;
private decrypter: Decrypter;
private creator: RandyCreator;
public algo: Algos = Algos.randy;
public signers: Signer[];
constructor(
salter: Salter,
code = MtrDex.Ed25519_Seed,
count = 1,
icodes: string[] | undefined = undefined,
transferable = false,
ncode = MtrDex.Ed25519_Seed,
ncount = 1,
ncodes: string[],
dcode = MtrDex.Blake3_256,
prxs: string[] | undefined = undefined,
nxts: string[] | undefined = undefined
) {
this.salter = salter;
this.icodes =
icodes == undefined ? new Array<string>(count).fill(code) : icodes;
this.ncodes =
ncodes == undefined
? new Array<string>(ncount).fill(ncode)
: ncodes;
this.code = code;
this.ncode = ncode;
this.count = count;
this.ncount = ncount;
const signer = this.salter.signer(code, transferable);
this.aeid = signer.verfer.qb64;
this.encrypter = new Encrypter({}, b(this.aeid));
this.decrypter = new Decrypter({}, signer.qb64b);
this.nxts = nxts ?? [];
this.prxs = prxs ?? [];
this.transferable = transferable;
this.icodes = icodes;
this.ncodes = ncodes;
this.dcode = dcode;
this.creator = new RandyCreator();
this.signers = this.prxs.map((prx) =>
this.decrypter.decrypt(
new Cipher({ qb64: prx }).qb64b,
undefined,
this.transferable
)
);
}
params(): RandyParams {
return {
nxts: this.nxts,
prxs: this.prxs,
transferable: this.transferable,
};
}
async incept(transferable: boolean): Promise<KeeperResult> {
this.transferable = transferable;
const signers = this.creator.create(
this.icodes,
this.count,
this.code,
this.transferable
);
this.prxs = signers.signers.map(
(signer) => this.encrypter.encrypt(undefined, signer).qb64
);
const verfers = signers.signers.map((signer) => signer.verfer.qb64);
const nsigners = this.creator.create(
this.ncodes,
this.ncount,
this.ncode,
this.transferable
);
this.nxts = nsigners.signers.map(
(signer) => this.encrypter.encrypt(undefined, signer).qb64
);
const digers = nsigners.signers.map(
(nsigner) =>
new Diger({ code: this.dcode }, nsigner.verfer.qb64b).qb64
);
return [verfers, digers];
}
async rotate(
ncodes: string[],
transferable: boolean
): Promise<KeeperResult> {
this.ncodes = ncodes;
this.transferable = transferable;
this.prxs = this.nxts;
const signers = this.nxts!.map((nxt) =>
this.decrypter.decrypt(
undefined,
new Cipher({ qb64: nxt }),
this.transferable
)
);
const verfers = signers.map((signer) => signer.verfer.qb64);
const nsigners = this.creator.create(
this.ncodes,
this.ncount,
this.ncode,
this.transferable
);
this.nxts = nsigners.signers.map(
(signer) => this.encrypter.encrypt(undefined, signer).qb64
);
const digers = nsigners.signers.map(
(nsigner) =>
new Diger({ code: this.dcode }, nsigner.verfer.qb64b).qb64
);
return [verfers, digers];
}
async sign(
ser: Uint8Array,
indexed = true,
indices: number[] | undefined = undefined,
ondices: number[] | undefined = undefined
): Promise<SignResult> {
const signers = this.prxs!.map((prx) =>
this.decrypter.decrypt(
new Cipher({ qb64: prx }).qb64b,
undefined,
this.transferable
)
);
if (indexed) {
const sigers = [];
let i = 0;
for (const [j, signer] of signers.entries()) {
if (indices != undefined) {
i = indices![j];
if (typeof i != 'number' || i < 0) {
throw new Error(
`Invalid signing index = ${i}, not whole number.`
);
}
} else {
i = j;
}
let o = 0;
if (ondices != undefined) {
o = ondices![j];
if (
(o == undefined ||
(typeof o == 'number' &&
typeof o != 'number' &&
o >= 0))!
) {
throw new Error(
`Invalid ondex = ${o}, not whole number.`
);
}
} else {
o = i;
}
sigers.push(
signer.sign(ser, i, o == undefined ? true : false, o)
);
}
return sigers.map((siger) => siger.qb64);
} else {
const cigars = [];
for (const [, signer] of signers.entries()) {
cigars.push(signer.sign(ser));
}
return cigars.map((cigar) => cigar.qb64);
}
}
}
export class GroupKeeper implements Keeper {
private manager: KeyManager;
private mhab: HabState;
private gkeys: string[] = [];
private gdigs: string[] = [];
public algo: Algos = Algos.group;
public signers: Signer[];
constructor(
manager: KeyManager,
mhab: HabState,
states: State[] | undefined = undefined,
rstates: State[] | undefined = undefined,
keys: string[] = [],
ndigs: string[] = []
) {
this.manager = manager;
if (states != undefined) {
keys = states.map((state) => state['k'][0]);
}
if (rstates != undefined) {
ndigs = rstates.map((state) => state['n'][0]);
}
this.gkeys = states?.map((state) => state['k'][0]) ?? keys;
this.gdigs = rstates?.map((state) => state['n'][0]) ?? ndigs;
this.mhab = mhab;
this.signers = [];
}
async incept(): Promise<KeeperResult> {
return [this.gkeys, this.gdigs];
}
async rotate(
_ncodes: string[],
_transferable: boolean,
states: State[],
rstates: State[]
): Promise<KeeperResult> {
this.gkeys = states.map((state) => state['k'][0]);
this.gdigs = rstates.map((state) => state['n'][0]);
return [this.gkeys, this.gdigs];
}
async sign(ser: Uint8Array, indexed: boolean = true): Promise<SignResult> {
if (!this.mhab.state) {
throw new Error(`No state in mhab`);
}
const key = this.mhab['state']['k'][0];
const ndig = this.mhab['state']['n'][0];
const csi = this.gkeys!.indexOf(key);
const pni = this.gdigs!.indexOf(ndig);
const mkeeper = this.manager.get(this.mhab);
return await mkeeper.sign(ser, indexed, [csi], [pni]);
}
params() {
return {
mhab: this.mhab,
keys: this.gkeys,
ndigs: this.gdigs,
};
}
}