@kokuin/store-vault 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +126 -2
- package/lib/api.d.ts +5 -0
- package/lib/api.js +504 -0
- package/lib/batch.d.ts +89 -0
- package/lib/batch.js +159 -0
- package/lib/coordinator-context.d.ts +71 -0
- package/lib/coordinator-context.js +410 -0
- package/lib/coordinator-entries.d.ts +20 -0
- package/lib/coordinator-entries.js +258 -0
- package/lib/coordinator-open.d.ts +9 -0
- package/lib/coordinator-open.js +141 -0
- package/lib/coordinator-records.d.ts +27 -0
- package/lib/coordinator-records.js +132 -0
- package/lib/coordinator-rotate.d.ts +9 -0
- package/lib/coordinator-rotate.js +436 -0
- package/lib/coordinator-types.d.ts +139 -0
- package/lib/coordinator-types.js +1 -0
- package/lib/coordinator.d.ts +2 -0
- package/lib/coordinator.js +459 -0
- package/lib/definition.d.ts +6 -0
- package/lib/definition.js +12 -0
- package/lib/index.d.ts +12 -0
- package/lib/index.js +7 -0
- package/lib/migrations.d.ts +2 -0
- package/lib/migrations.js +52 -0
- package/lib/record-context.d.ts +24 -0
- package/lib/record-context.js +34 -0
- package/lib/record-definition.d.ts +7 -0
- package/lib/record-definition.js +93 -0
- package/lib/record-tables.d.ts +47 -0
- package/lib/record-tables.js +1 -0
- package/lib/records-adapter.d.ts +3 -0
- package/lib/records-adapter.js +197 -0
- package/lib/records-ports.d.ts +4 -0
- package/lib/records-ports.js +72 -0
- package/lib/records-projection.d.ts +3 -0
- package/lib/records-projection.js +105 -0
- package/lib/records-rows.d.ts +54 -0
- package/lib/records-rows.js +155 -0
- package/lib/records.d.ts +59 -0
- package/lib/records.js +233 -0
- package/lib/settle.d.ts +12 -0
- package/lib/settle.js +18 -0
- package/lib/tables.d.ts +42 -0
- package/lib/tables.js +1 -0
- package/lib/types.d.ts +74 -0
- package/lib/types.js +1 -0
- package/package.json +45 -3
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { withKeyLocks, withStoreTransaction } from '@hozon/db';
|
|
2
|
+
import { getRecordDecisions, recordClaimDigest, recordKeyText, recordPayloadDigest, signRecordOp, verifySignedRecord } from '@kokuin/records';
|
|
3
|
+
import { checkDescriptorDigest, createKeyCache, decodeVaultRecord, encodeVaultRecordPayload, selectWinningBranch, VaultTampered } from '@kokuin/vault';
|
|
4
|
+
import { getVaultStore } from './definition.js';
|
|
5
|
+
import { VaultWriteConflict, vaultDatabases } from './record-context.js';
|
|
6
|
+
import { createVaultRecords, VaultRecordRowError } from './records.js';
|
|
7
|
+
import { settleAll, settledValue } from './settle.js';
|
|
8
|
+
export class VaultNotAuthorized extends Error {
|
|
9
|
+
constructor(verb){
|
|
10
|
+
super(`Vault ${verb} authority refused the operation`);
|
|
11
|
+
this.name = 'VaultNotAuthorized';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export class VaultNotFound extends Error {
|
|
15
|
+
constructor(){
|
|
16
|
+
super('Vault key, entry or authenticated evidence not found');
|
|
17
|
+
this.name = 'VaultNotFound';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export class VaultRepairPending extends Error {
|
|
21
|
+
constructor(){
|
|
22
|
+
super('Vault branch material is unavailable for repair');
|
|
23
|
+
this.name = 'VaultRepairPending';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export class VaultSupersededBranch extends Error {
|
|
27
|
+
constructor(){
|
|
28
|
+
super('Entry belongs to a superseded content-key branch');
|
|
29
|
+
this.name = 'VaultSupersededBranch';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export class VaultKeyLocked extends Error {
|
|
33
|
+
constructor(){
|
|
34
|
+
super('Vault key requires supplied factors');
|
|
35
|
+
this.name = 'VaultKeyLocked';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function createCoordinatorContext(params) {
|
|
39
|
+
if (params.authority === undefined) throw new TypeError('CredentialAuthority is required');
|
|
40
|
+
const cache = createKeyCache(params.cache);
|
|
41
|
+
const generations = new Map();
|
|
42
|
+
const cacheID = (address)=>JSON.stringify([
|
|
43
|
+
address.ownerDID,
|
|
44
|
+
address.keyID
|
|
45
|
+
]);
|
|
46
|
+
// One records object per store handle. It holds no per-call state: its adapter reads through the
|
|
47
|
+
// handle's own database, and lock state lives only on adapters built inside `transaction`.
|
|
48
|
+
const recordsByStore = new WeakMap();
|
|
49
|
+
const recordsFor = (store)=>{
|
|
50
|
+
if (params.mode !== 'replicated') throw new TypeError('Local coordinator has no records path');
|
|
51
|
+
let records = recordsByStore.get(store);
|
|
52
|
+
if (records === undefined) {
|
|
53
|
+
records = createVaultRecords({
|
|
54
|
+
...params.records,
|
|
55
|
+
store,
|
|
56
|
+
authority: params.authority
|
|
57
|
+
});
|
|
58
|
+
recordsByStore.set(store, records);
|
|
59
|
+
}
|
|
60
|
+
return records;
|
|
61
|
+
};
|
|
62
|
+
const records = params.mode === 'replicated' ? recordsFor(params.store) : undefined;
|
|
63
|
+
async function authorize(operation, verb, address, store = params.store) {
|
|
64
|
+
const allowed = await params.authority.authorize({
|
|
65
|
+
source: 'local',
|
|
66
|
+
mode: params.mode,
|
|
67
|
+
operation,
|
|
68
|
+
verb,
|
|
69
|
+
owner: address.ownerDID,
|
|
70
|
+
issuer: params.principal,
|
|
71
|
+
keyID: address.keyID,
|
|
72
|
+
entryID: address.entryID,
|
|
73
|
+
transportContext: address.transportContext,
|
|
74
|
+
request: address,
|
|
75
|
+
store: {
|
|
76
|
+
getKey: (a)=>store.getKey(a),
|
|
77
|
+
getEntry: (a)=>store.getEntry(a),
|
|
78
|
+
listKeys: (a)=>store.listKeys(a),
|
|
79
|
+
listWrappings: (a)=>store.listWrappings(a),
|
|
80
|
+
listEntries: (a)=>store.listEntries(a)
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
if (!allowed) throw new VaultNotAuthorized(verb);
|
|
84
|
+
}
|
|
85
|
+
async function key(address, store = params.store) {
|
|
86
|
+
const row = await store.getKey(address);
|
|
87
|
+
if (row === undefined) throw new VaultNotFound();
|
|
88
|
+
return row;
|
|
89
|
+
}
|
|
90
|
+
async function signed(type, address, payload, meta, hlc) {
|
|
91
|
+
if (params.mode !== 'replicated') throw new TypeError('Local writes are unsigned');
|
|
92
|
+
const bytes = payload === null ? null : encodeVaultRecordPayload(type, payload);
|
|
93
|
+
const stamp = hlc ?? await params.nextHLC();
|
|
94
|
+
const claim = {
|
|
95
|
+
type,
|
|
96
|
+
id: address.id,
|
|
97
|
+
owner: address.ownerDID,
|
|
98
|
+
hlc: stamp,
|
|
99
|
+
payloadDigest: recordPayloadDigest(bytes),
|
|
100
|
+
deleted: bytes === null,
|
|
101
|
+
meta
|
|
102
|
+
};
|
|
103
|
+
return {
|
|
104
|
+
type,
|
|
105
|
+
id: address.id,
|
|
106
|
+
owner: address.ownerDID,
|
|
107
|
+
hlc: stamp,
|
|
108
|
+
payload: bytes,
|
|
109
|
+
...await signRecordOp(params.signer, claim, params.records.opKind)
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
async function verified(record) {
|
|
113
|
+
if (params.mode !== 'replicated') throw new TypeError('Local rows have no signed evidence');
|
|
114
|
+
const op = await verifySignedRecord(record, params.records.opKind, params.records.methods);
|
|
115
|
+
if (op === null) throw new VaultNotFound();
|
|
116
|
+
return op;
|
|
117
|
+
}
|
|
118
|
+
async function stored(address, store = params.store) {
|
|
119
|
+
const record = (await recordsFor(store).adapter.get(address))?.copies[0];
|
|
120
|
+
if (record === undefined) throw new VaultNotFound();
|
|
121
|
+
return {
|
|
122
|
+
record,
|
|
123
|
+
op: await verified(record)
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
async function storedMany(addresses, store = params.store) {
|
|
127
|
+
const decisions = await getRecordDecisions(recordsFor(store).adapter, addresses);
|
|
128
|
+
return settleAll(addresses.map(async (address)=>{
|
|
129
|
+
const record = decisions.get(recordKeyText(address))?.copies[0];
|
|
130
|
+
if (record === undefined) throw new VaultNotFound();
|
|
131
|
+
return {
|
|
132
|
+
record,
|
|
133
|
+
op: await verified(record)
|
|
134
|
+
};
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
function checkWrapping(row, evidence) {
|
|
138
|
+
const decoded = decodeVaultRecord(evidence.record, evidence.op.claim.meta);
|
|
139
|
+
if (decoded.type !== 'vault.wrapping' || decoded.meta.keyID !== row.keyID || decoded.meta.keyVersion !== row.keyVersion || decoded.meta.branchID !== row.provenance || decoded.meta.recipient !== row.recipientDID) throw new VaultTampered('Wrapping projection disagrees with authenticated metadata');
|
|
140
|
+
checkDescriptorDigest({
|
|
141
|
+
descriptors: row.descriptors,
|
|
142
|
+
expectedDescriptorDigest: decoded.payload.descriptorDigest
|
|
143
|
+
});
|
|
144
|
+
const equal = (a, b)=>{
|
|
145
|
+
return a.length === b.length && a.every((v, i)=>v === b[i]);
|
|
146
|
+
};
|
|
147
|
+
if (!equal(row.iv, decoded.payload.iv) || !equal(row.wrappedKey, decoded.payload.wrappedKey)) throw new VaultTampered('Wrapping projection disagrees with authenticated ciphertext');
|
|
148
|
+
return {
|
|
149
|
+
...evidence,
|
|
150
|
+
expectedDescriptorDigest: decoded.payload.descriptorDigest
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
async function wrappingEvidences(rows, store = params.store) {
|
|
154
|
+
if (params.mode === 'local') return [];
|
|
155
|
+
const evidence = await storedMany(rows.map((row)=>({
|
|
156
|
+
type: 'vault.wrapping',
|
|
157
|
+
owner: row.ownerDID,
|
|
158
|
+
id: row.wrappingID
|
|
159
|
+
})), store);
|
|
160
|
+
return rows.map((row, index)=>{
|
|
161
|
+
const item = evidence[index];
|
|
162
|
+
if (item === undefined) throw new VaultNotFound();
|
|
163
|
+
return checkWrapping(row, item);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
async function wrappingEvidence(row, store = params.store) {
|
|
167
|
+
if (params.mode === 'local') return undefined;
|
|
168
|
+
const evidence = await stored({
|
|
169
|
+
type: 'vault.wrapping',
|
|
170
|
+
owner: row.ownerDID,
|
|
171
|
+
id: row.wrappingID
|
|
172
|
+
}, store);
|
|
173
|
+
return checkWrapping(row, evidence);
|
|
174
|
+
}
|
|
175
|
+
async function branches(address, version, store = params.store) {
|
|
176
|
+
return params.mode === 'local' ? [] : recordsFor(store).listKeyBranches({
|
|
177
|
+
...address,
|
|
178
|
+
keyVersion: version
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
async function branch(address, version, store = params.store, specific) {
|
|
182
|
+
if (params.mode === 'local') return {
|
|
183
|
+
branchID: '',
|
|
184
|
+
branchRecordID: '',
|
|
185
|
+
suite: (await key(address, store)).suite
|
|
186
|
+
};
|
|
187
|
+
const rows = await branches(address, version, store);
|
|
188
|
+
if (rows.length === 0) throw new VaultRepairPending();
|
|
189
|
+
const winner = selectWinningBranch(rows.map((row)=>row.introducingOp));
|
|
190
|
+
const branchID = specific ?? `${winner.claim.hlc}:${winner.opHash}`;
|
|
191
|
+
const proofs = rows.filter((row)=>row.branchID === branchID).map((row)=>row.branchRecordID).sort();
|
|
192
|
+
if (proofs.length === 0) throw new VaultRepairPending();
|
|
193
|
+
const adapter = recordsFor(store).adapter;
|
|
194
|
+
// A projection row that names no matching record is local corruption. `repairFork` rebuilds it.
|
|
195
|
+
const corrupt = (id, reason)=>{
|
|
196
|
+
params.records.onCorruptRow?.({
|
|
197
|
+
type: 'vault.key-branch',
|
|
198
|
+
id,
|
|
199
|
+
error: new Error(reason)
|
|
200
|
+
});
|
|
201
|
+
};
|
|
202
|
+
const keys = proofs.map((id)=>({
|
|
203
|
+
type: 'vault.key-branch',
|
|
204
|
+
owner: address.ownerDID,
|
|
205
|
+
id
|
|
206
|
+
}));
|
|
207
|
+
// One batched read. A corrupt row fails the whole batch, so then each proof is read alone and
|
|
208
|
+
// only the corrupt one is reported.
|
|
209
|
+
const read = async ()=>{
|
|
210
|
+
try {
|
|
211
|
+
const decisions = await getRecordDecisions(adapter, keys);
|
|
212
|
+
return keys.map((key)=>decisions.get(recordKeyText(key)) ?? null);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
if (!(error instanceof VaultRecordRowError)) throw error;
|
|
215
|
+
}
|
|
216
|
+
const each = [];
|
|
217
|
+
for (const key of keys){
|
|
218
|
+
try {
|
|
219
|
+
each.push(await adapter.get(key));
|
|
220
|
+
} catch (error) {
|
|
221
|
+
if (!(error instanceof VaultRecordRowError)) throw error;
|
|
222
|
+
each.push(error);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return each;
|
|
226
|
+
};
|
|
227
|
+
const decisions = await read();
|
|
228
|
+
// Verify every proof together, then consume the results in proof order, as a sequential
|
|
229
|
+
// scan would: the first failure reached is the one thrown.
|
|
230
|
+
const checks = await Promise.allSettled(decisions.map((decision)=>{
|
|
231
|
+
const record = decision instanceof VaultRecordRowError ? undefined : decision?.copies[0];
|
|
232
|
+
return record === undefined ? Promise.resolve(null) : verified(record);
|
|
233
|
+
}));
|
|
234
|
+
// Read only the projected proof records. Any mismatch fails closed, as a full scan would.
|
|
235
|
+
for (const [index, id] of proofs.entries()){
|
|
236
|
+
const decision = decisions[index] ?? null;
|
|
237
|
+
if (decision instanceof VaultRecordRowError) {
|
|
238
|
+
params.records.onCorruptRow?.({
|
|
239
|
+
type: 'vault.key-branch',
|
|
240
|
+
id,
|
|
241
|
+
error: decision
|
|
242
|
+
});
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const record = decision?.copies[0];
|
|
246
|
+
if (record === undefined) {
|
|
247
|
+
corrupt(id, 'Key-branch projection names a missing record');
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
// A present record was checked by `verified`, which throws rather than yield null.
|
|
251
|
+
const op = settledValue(checks[index]);
|
|
252
|
+
if (op.claim.meta.keyID !== address.keyID || op.claim.meta.keyVersion !== version || op.claim.meta.branchID !== branchID) {
|
|
253
|
+
corrupt(id, 'Key-branch projection disagrees with its signed record');
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const decoded = decodeVaultRecord(record, op.claim.meta);
|
|
257
|
+
if (decoded.type === 'vault.key-branch') return {
|
|
258
|
+
branchID,
|
|
259
|
+
branchRecordID: record.id,
|
|
260
|
+
suite: decoded.payload.suite
|
|
261
|
+
};
|
|
262
|
+
corrupt(id, 'Key-branch projection names a record of another type');
|
|
263
|
+
}
|
|
264
|
+
throw new VaultRepairPending();
|
|
265
|
+
}
|
|
266
|
+
async function write(store, output, preconditions, call) {
|
|
267
|
+
const result = await recordsFor(store).write({
|
|
268
|
+
records: output,
|
|
269
|
+
preconditions,
|
|
270
|
+
transportContext: call.transportContext
|
|
271
|
+
});
|
|
272
|
+
if (result.rejected.length > 0 || result.parked.length > 0) throw new VaultWriteConflict();
|
|
273
|
+
const decisions = await getRecordDecisions(recordsFor(store).adapter, output);
|
|
274
|
+
await settleAll(output.map(async (record)=>{
|
|
275
|
+
const selected = decisions.get(recordKeyText(record))?.copies[0];
|
|
276
|
+
if (selected === undefined) throw new VaultWriteConflict();
|
|
277
|
+
const [selectedOp, expectedOp] = await settleAll([
|
|
278
|
+
verified(selected),
|
|
279
|
+
verified(record)
|
|
280
|
+
]);
|
|
281
|
+
if (recordClaimDigest(selectedOp.claim) !== recordClaimDigest(expectedOp.claim)) throw new VaultWriteConflict();
|
|
282
|
+
}));
|
|
283
|
+
}
|
|
284
|
+
async function keyRecords(row, store, unique, consumer) {
|
|
285
|
+
const previous = unique === undefined ? await stored({
|
|
286
|
+
type: 'vault.key',
|
|
287
|
+
owner: row.ownerDID,
|
|
288
|
+
id: row.keyID
|
|
289
|
+
}, store) : undefined;
|
|
290
|
+
const keyUnique = unique ?? previous?.op.claim.meta.keyUnique;
|
|
291
|
+
if (typeof keyUnique !== 'string') throw new VaultNotFound();
|
|
292
|
+
const record = await signed('vault.key', {
|
|
293
|
+
ownerDID: row.ownerDID,
|
|
294
|
+
id: row.keyID
|
|
295
|
+
}, {
|
|
296
|
+
suite: row.suite,
|
|
297
|
+
state: row.state
|
|
298
|
+
}, {
|
|
299
|
+
keyID: row.keyID,
|
|
300
|
+
keyUnique,
|
|
301
|
+
version: row.version,
|
|
302
|
+
state: row.state,
|
|
303
|
+
consumer: consumer === undefined ? previous?.op.claim.meta.consumer ?? null : consumer
|
|
304
|
+
});
|
|
305
|
+
const op = await verified(record);
|
|
306
|
+
const branchID = `${record.hlc}:${record.opHash}`;
|
|
307
|
+
const payload = {
|
|
308
|
+
authorDID: op.issuer,
|
|
309
|
+
hlc: record.hlc,
|
|
310
|
+
opJWT: record.opJWT,
|
|
311
|
+
opHash: record.opHash,
|
|
312
|
+
suite: row.suite
|
|
313
|
+
};
|
|
314
|
+
const branchRecordID = recordPayloadDigest(encodeVaultRecordPayload('vault.key-branch', payload));
|
|
315
|
+
if (branchRecordID === null) throw new Error('Branch has no payload digest');
|
|
316
|
+
const proof = await signed('vault.key-branch', {
|
|
317
|
+
ownerDID: row.ownerDID,
|
|
318
|
+
id: branchRecordID
|
|
319
|
+
}, payload, {
|
|
320
|
+
keyID: row.keyID,
|
|
321
|
+
keyVersion: row.version,
|
|
322
|
+
branchID,
|
|
323
|
+
consumer: consumer ?? previous?.op.claim.meta.consumer ?? null
|
|
324
|
+
}, record.hlc);
|
|
325
|
+
return {
|
|
326
|
+
records: [
|
|
327
|
+
record,
|
|
328
|
+
proof
|
|
329
|
+
],
|
|
330
|
+
branchID,
|
|
331
|
+
branchRecordID
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
async function locked(addresses, fn) {
|
|
335
|
+
const database = vaultDatabases.get(params.store);
|
|
336
|
+
if (database === undefined) throw new TypeError('Coordinator requires a registered vault store');
|
|
337
|
+
return withKeyLocks(database.context.provider, 'vault_keys', addresses.map((a)=>[
|
|
338
|
+
JSON.stringify([
|
|
339
|
+
a.ownerDID,
|
|
340
|
+
a.keyID
|
|
341
|
+
])
|
|
342
|
+
]), async (provider)=>{
|
|
343
|
+
const store = await getVaultStore(provider);
|
|
344
|
+
const transaction = vaultDatabases.get(store);
|
|
345
|
+
if (transaction === undefined) throw new TypeError('Transaction-bound vault store is unavailable');
|
|
346
|
+
return withStoreTransaction(transaction.db, ()=>fn(store));
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
async function read(fn) {
|
|
350
|
+
const database = vaultDatabases.get(params.store);
|
|
351
|
+
if (database === undefined) throw new TypeError('Coordinator requires a registered vault store');
|
|
352
|
+
return database.context.provider.withTransaction(async (provider)=>{
|
|
353
|
+
const store = await getVaultStore(provider);
|
|
354
|
+
const transaction = vaultDatabases.get(store);
|
|
355
|
+
if (transaction === undefined) throw new TypeError('Transaction-bound vault store is unavailable');
|
|
356
|
+
return withStoreTransaction(transaction.db, ()=>fn(store));
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
function evict(address) {
|
|
360
|
+
const id = cacheID(address);
|
|
361
|
+
generations.set(id, {
|
|
362
|
+
ownerDID: address.ownerDID,
|
|
363
|
+
value: (generations.get(id)?.value ?? 0) + 1
|
|
364
|
+
});
|
|
365
|
+
cache.evict(id);
|
|
366
|
+
}
|
|
367
|
+
function evictAll(ownerDID) {
|
|
368
|
+
for (const [id, generation] of generations){
|
|
369
|
+
if (generation.ownerDID !== ownerDID) continue;
|
|
370
|
+
generation.value += 1;
|
|
371
|
+
cache.evict(id);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
function generation(address) {
|
|
375
|
+
const id = cacheID(address);
|
|
376
|
+
let current = generations.get(id);
|
|
377
|
+
if (current === undefined) {
|
|
378
|
+
current = {
|
|
379
|
+
ownerDID: address.ownerDID,
|
|
380
|
+
value: 0
|
|
381
|
+
};
|
|
382
|
+
generations.set(id, current);
|
|
383
|
+
}
|
|
384
|
+
return String(current.value);
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
params,
|
|
388
|
+
cache,
|
|
389
|
+
cacheID,
|
|
390
|
+
records,
|
|
391
|
+
recordsFor,
|
|
392
|
+
authorize,
|
|
393
|
+
key,
|
|
394
|
+
signed,
|
|
395
|
+
verified,
|
|
396
|
+
stored,
|
|
397
|
+
storedMany,
|
|
398
|
+
wrappingEvidence,
|
|
399
|
+
wrappingEvidences,
|
|
400
|
+
branches,
|
|
401
|
+
branch,
|
|
402
|
+
write,
|
|
403
|
+
keyRecords,
|
|
404
|
+
locked,
|
|
405
|
+
read,
|
|
406
|
+
evict,
|
|
407
|
+
evictAll,
|
|
408
|
+
generation
|
|
409
|
+
};
|
|
410
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { CoordinatorContext } from './coordinator-context.js';
|
|
2
|
+
import type { OpenFunctions } from './coordinator-open.js';
|
|
3
|
+
import type { RecordFunctions } from './coordinator-records.js';
|
|
4
|
+
import type { CoordinatorCall, EntryUpdateParams, EntryWriteParams } from './coordinator-types.js';
|
|
5
|
+
import type { EntryAddress, UpdateEntryResult } from './types.js';
|
|
6
|
+
export type ReKeyEntryParams = EntryAddress & CoordinatorCall & {
|
|
7
|
+
toKeyID: string;
|
|
8
|
+
inputs?: EntryUpdateParams['inputs'];
|
|
9
|
+
};
|
|
10
|
+
export type EntryFunctions = {
|
|
11
|
+
putEntry(request: EntryWriteParams): Promise<EntryAddress>;
|
|
12
|
+
deleteEntry(request: EntryAddress & CoordinatorCall): Promise<void>;
|
|
13
|
+
reKeyEntry(request: ReKeyEntryParams): Promise<void>;
|
|
14
|
+
updateEntry(request: EntryUpdateParams): Promise<void>;
|
|
15
|
+
updateEntryIfPresent(request: EntryUpdateParams): Promise<'updated' | 'absent'>;
|
|
16
|
+
updateEntryIfUnchanged(request: EntryUpdateParams & {
|
|
17
|
+
expectedHLC: string;
|
|
18
|
+
}): Promise<UpdateEntryResult>;
|
|
19
|
+
};
|
|
20
|
+
export declare function createEntryFunctions(ctx: CoordinatorContext, open: OpenFunctions, records: RecordFunctions): EntryFunctions;
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { getSuite } from '@kokuin/vault';
|
|
2
|
+
import { VaultNotFound } from './coordinator-context.js';
|
|
3
|
+
import { VaultWriteConflict } from './record-context.js';
|
|
4
|
+
import { settleAll } from './settle.js';
|
|
5
|
+
const zeroKey = (opened)=>{
|
|
6
|
+
opened.contentKey.fill(0);
|
|
7
|
+
};
|
|
8
|
+
const zeroBytes = (bytes)=>{
|
|
9
|
+
bytes.fill(0);
|
|
10
|
+
};
|
|
11
|
+
export function createEntryFunctions(ctx, open, records) {
|
|
12
|
+
const { params } = ctx;
|
|
13
|
+
async function save(row, store, call, expectedHLC, consumer) {
|
|
14
|
+
if (params.mode === 'local') {
|
|
15
|
+
await store.putEntry(row);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const key = await ctx.key(row, store);
|
|
19
|
+
const [branch, branches] = await settleAll([
|
|
20
|
+
ctx.branch(key, key.version, store),
|
|
21
|
+
ctx.branches(key, key.version, store)
|
|
22
|
+
]);
|
|
23
|
+
const record = await records.entry(row, branch, row.hlc, consumer);
|
|
24
|
+
await ctx.write(store, [
|
|
25
|
+
record
|
|
26
|
+
], {
|
|
27
|
+
key: {
|
|
28
|
+
...key,
|
|
29
|
+
expectedVersion: key.version,
|
|
30
|
+
expectedBranches: branches.map((b)=>b.branchID)
|
|
31
|
+
},
|
|
32
|
+
entry: {
|
|
33
|
+
...row,
|
|
34
|
+
expectedHLC
|
|
35
|
+
}
|
|
36
|
+
}, call);
|
|
37
|
+
}
|
|
38
|
+
async function putEntry(request) {
|
|
39
|
+
await ctx.authorize('putEntry', 'administer', request);
|
|
40
|
+
return ctx.locked([
|
|
41
|
+
request
|
|
42
|
+
], async (store)=>{
|
|
43
|
+
const current = await store.getEntry(request);
|
|
44
|
+
if (current !== undefined) throw new VaultWriteConflict();
|
|
45
|
+
const [key, opened, hlc] = await settleAll([
|
|
46
|
+
ctx.key(request, store),
|
|
47
|
+
open.open(request, store),
|
|
48
|
+
params.nextHLC()
|
|
49
|
+
], [
|
|
50
|
+
undefined,
|
|
51
|
+
zeroKey
|
|
52
|
+
]);
|
|
53
|
+
try {
|
|
54
|
+
const encrypted = getSuite(opened.suite).encryptEntry({
|
|
55
|
+
...request,
|
|
56
|
+
runtime: params.runtime,
|
|
57
|
+
contentKey: opened.contentKey,
|
|
58
|
+
keyVersion: key.version
|
|
59
|
+
});
|
|
60
|
+
const row = {
|
|
61
|
+
...request,
|
|
62
|
+
keyVersion: key.version,
|
|
63
|
+
...encrypted,
|
|
64
|
+
hlc,
|
|
65
|
+
provenance: opened.branchID
|
|
66
|
+
};
|
|
67
|
+
await save(row, store, request, null, request.consumer ?? null);
|
|
68
|
+
return {
|
|
69
|
+
ownerDID: request.ownerDID,
|
|
70
|
+
entryID: request.entryID
|
|
71
|
+
};
|
|
72
|
+
} finally{
|
|
73
|
+
opened.contentKey.fill(0);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
async function update(request, operation) {
|
|
78
|
+
await ctx.authorize(operation, 'administer', request);
|
|
79
|
+
const observed = await params.store.getEntry(request);
|
|
80
|
+
if (observed === undefined) return 'absent';
|
|
81
|
+
return ctx.locked([
|
|
82
|
+
observed
|
|
83
|
+
], async (store)=>{
|
|
84
|
+
const existing = await store.getEntry(request);
|
|
85
|
+
if (existing === undefined) return 'absent';
|
|
86
|
+
if (existing.keyID !== observed.keyID) throw new VaultWriteConflict();
|
|
87
|
+
if (request.expectedHLC !== undefined && existing.hlc !== request.expectedHLC) return 'superseded';
|
|
88
|
+
await ctx.authorize(operation, 'administer', {
|
|
89
|
+
...request,
|
|
90
|
+
keyID: existing.keyID
|
|
91
|
+
}, store);
|
|
92
|
+
const [opened, hlc, info] = await settleAll([
|
|
93
|
+
open.open({
|
|
94
|
+
...existing,
|
|
95
|
+
inputs: request.inputs
|
|
96
|
+
}, store),
|
|
97
|
+
params.nextHLC(),
|
|
98
|
+
records.entryInfo(existing, store)
|
|
99
|
+
], [
|
|
100
|
+
zeroKey
|
|
101
|
+
]);
|
|
102
|
+
try {
|
|
103
|
+
const row = {
|
|
104
|
+
...existing,
|
|
105
|
+
keyVersion: opened.version,
|
|
106
|
+
hlc,
|
|
107
|
+
provenance: opened.branchID,
|
|
108
|
+
...getSuite(opened.suite).encryptEntry({
|
|
109
|
+
...existing,
|
|
110
|
+
keyVersion: opened.version,
|
|
111
|
+
runtime: params.runtime,
|
|
112
|
+
contentKey: opened.contentKey,
|
|
113
|
+
plaintext: request.plaintext
|
|
114
|
+
})
|
|
115
|
+
};
|
|
116
|
+
await save(row, store, request, existing.hlc, info.consumer);
|
|
117
|
+
return 'updated';
|
|
118
|
+
} finally{
|
|
119
|
+
opened.contentKey.fill(0);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
async function deleteEntry(request) {
|
|
124
|
+
await ctx.authorize('deleteEntry', 'administer', request);
|
|
125
|
+
const observed = await params.store.getEntry(request);
|
|
126
|
+
if (observed === undefined) return;
|
|
127
|
+
await ctx.locked([
|
|
128
|
+
observed
|
|
129
|
+
], async (store)=>{
|
|
130
|
+
const existing = await store.getEntry(request);
|
|
131
|
+
if (existing === undefined) return;
|
|
132
|
+
if (existing.keyID !== observed.keyID) throw new VaultWriteConflict();
|
|
133
|
+
await ctx.authorize('deleteEntry', 'administer', {
|
|
134
|
+
...request,
|
|
135
|
+
keyID: existing.keyID
|
|
136
|
+
}, store);
|
|
137
|
+
if (params.mode === 'local') {
|
|
138
|
+
await store.deleteEntry(request);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const keyBranch = async ()=>{
|
|
142
|
+
const key = await ctx.key(existing, store);
|
|
143
|
+
return {
|
|
144
|
+
key,
|
|
145
|
+
branch: await ctx.branch(existing, key.version, store)
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
const [{ key, branch }, info, hlc] = await settleAll([
|
|
149
|
+
keyBranch(),
|
|
150
|
+
records.entryInfo(existing, store),
|
|
151
|
+
params.nextHLC()
|
|
152
|
+
]);
|
|
153
|
+
const row = {
|
|
154
|
+
...existing,
|
|
155
|
+
hlc,
|
|
156
|
+
keyVersion: key.version
|
|
157
|
+
};
|
|
158
|
+
const record = await records.entry(row, branch, row.hlc, info.consumer, true);
|
|
159
|
+
await ctx.write(store, [
|
|
160
|
+
record
|
|
161
|
+
], {
|
|
162
|
+
key: {
|
|
163
|
+
...key,
|
|
164
|
+
expectedVersion: key.version
|
|
165
|
+
},
|
|
166
|
+
entry: {
|
|
167
|
+
...request,
|
|
168
|
+
expectedHLC: existing.hlc
|
|
169
|
+
}
|
|
170
|
+
}, request);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
async function reKeyEntry(request) {
|
|
174
|
+
await ctx.authorize('reKeyEntry', 'administer', request);
|
|
175
|
+
const observed = await params.store.getEntry(request);
|
|
176
|
+
if (observed === undefined) throw new VaultNotFound();
|
|
177
|
+
const target = {
|
|
178
|
+
ownerDID: request.ownerDID,
|
|
179
|
+
keyID: request.toKeyID
|
|
180
|
+
};
|
|
181
|
+
await settleAll([
|
|
182
|
+
ctx.authorize('reKeyEntry', 'administer', {
|
|
183
|
+
...request,
|
|
184
|
+
keyID: observed.keyID
|
|
185
|
+
}),
|
|
186
|
+
ctx.authorize('reKeyEntry', 'administer', {
|
|
187
|
+
...request,
|
|
188
|
+
keyID: target.keyID
|
|
189
|
+
})
|
|
190
|
+
]);
|
|
191
|
+
await ctx.locked([
|
|
192
|
+
observed,
|
|
193
|
+
target
|
|
194
|
+
], async (store)=>{
|
|
195
|
+
const existing = await store.getEntry(request);
|
|
196
|
+
if (existing === undefined || existing.keyID !== observed.keyID || existing.hlc !== observed.hlc) throw new VaultWriteConflict();
|
|
197
|
+
const decrypting = open.decrypt(existing, {
|
|
198
|
+
...existing,
|
|
199
|
+
inputs: request.inputs
|
|
200
|
+
}, store);
|
|
201
|
+
// A rejected source PIN must not also spend an attempt on the target key's throttle.
|
|
202
|
+
const opening = request.inputs?.pin === undefined ? open.open({
|
|
203
|
+
...target,
|
|
204
|
+
inputs: request.inputs
|
|
205
|
+
}, store) : decrypting.then(()=>open.open({
|
|
206
|
+
...target,
|
|
207
|
+
inputs: request.inputs
|
|
208
|
+
}, store));
|
|
209
|
+
const [plaintext, opened, info, hlc] = await settleAll([
|
|
210
|
+
decrypting,
|
|
211
|
+
opening,
|
|
212
|
+
records.entryInfo(existing, store),
|
|
213
|
+
params.nextHLC()
|
|
214
|
+
], [
|
|
215
|
+
zeroBytes,
|
|
216
|
+
zeroKey
|
|
217
|
+
]);
|
|
218
|
+
try {
|
|
219
|
+
const row = {
|
|
220
|
+
...existing,
|
|
221
|
+
...target,
|
|
222
|
+
keyVersion: opened.version,
|
|
223
|
+
hlc,
|
|
224
|
+
provenance: opened.branchID,
|
|
225
|
+
...getSuite(opened.suite).encryptEntry({
|
|
226
|
+
...target,
|
|
227
|
+
keyVersion: opened.version,
|
|
228
|
+
entryID: existing.entryID,
|
|
229
|
+
contentKey: opened.contentKey,
|
|
230
|
+
runtime: params.runtime,
|
|
231
|
+
plaintext
|
|
232
|
+
})
|
|
233
|
+
};
|
|
234
|
+
if (params.mode === 'local') await store.moveEntry(row);
|
|
235
|
+
else await save(row, store, request, existing.hlc, info.consumer);
|
|
236
|
+
} finally{
|
|
237
|
+
opened.contentKey.fill(0);
|
|
238
|
+
plaintext.fill(0);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
putEntry,
|
|
244
|
+
deleteEntry,
|
|
245
|
+
reKeyEntry,
|
|
246
|
+
async updateEntry (request) {
|
|
247
|
+
if (await update(request, 'updateEntry') !== 'updated') throw new VaultNotFound();
|
|
248
|
+
},
|
|
249
|
+
async updateEntryIfPresent (request) {
|
|
250
|
+
const result = await update(request, 'updateEntryIfPresent');
|
|
251
|
+
if (result === 'superseded') throw new VaultWriteConflict();
|
|
252
|
+
return result;
|
|
253
|
+
},
|
|
254
|
+
updateEntryIfUnchanged: (request)=>{
|
|
255
|
+
return update(request, 'updateEntryIfUnchanged');
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CoordinatorContext } from './coordinator-context.js';
|
|
2
|
+
import type { OpenedKey, OpenKeyParams } from './coordinator-types.js';
|
|
3
|
+
import type { EntryRow, VaultStoreAPI } from './types.js';
|
|
4
|
+
export type OpenFunctions = {
|
|
5
|
+
open(address: OpenKeyParams, store?: VaultStoreAPI, specific?: string): Promise<OpenedKey>;
|
|
6
|
+
decrypt(row: EntryRow | undefined, address: OpenKeyParams, store?: VaultStoreAPI, specific?: boolean): Promise<Uint8Array>;
|
|
7
|
+
revoked(address: OpenKeyParams, store: VaultStoreAPI): Promise<boolean>;
|
|
8
|
+
};
|
|
9
|
+
export declare function createOpenFunctions(ctx: CoordinatorContext): OpenFunctions;
|