@kubun/credential 0.17.3 → 0.18.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/lib/api.d.ts +61 -30
- package/lib/api.js +455 -111
- package/lib/authority.d.ts +4 -0
- package/lib/authority.js +3 -0
- package/lib/branch.d.ts +4 -0
- package/lib/branch.js +24 -0
- package/lib/builder.d.ts +5 -0
- package/lib/builder.js +8 -6
- package/lib/canonical-token.d.ts +2 -0
- package/lib/canonical-token.js +10 -0
- package/lib/errors.d.ts +39 -1
- package/lib/errors.js +77 -0
- package/lib/fork-repair.d.ts +19 -0
- package/lib/fork-repair.js +3 -0
- package/lib/grantor-authority.d.ts +26 -0
- package/lib/grantor-authority.js +43 -0
- package/lib/index.d.ts +16 -6
- package/lib/index.js +8 -2
- package/lib/key-cache.d.ts +4 -3
- package/lib/key-cache.js +22 -15
- package/lib/manager-context.d.ts +50 -0
- package/lib/manager-context.js +82 -0
- package/lib/manager-entries.d.ts +3 -0
- package/lib/manager-entries.js +585 -0
- package/lib/manager-keys.d.ts +3 -0
- package/lib/manager-keys.js +413 -0
- package/lib/manager-open.d.ts +45 -0
- package/lib/manager-open.js +421 -0
- package/lib/manager-records.d.ts +94 -0
- package/lib/manager-records.js +291 -0
- package/lib/manager-removal-recovery.d.ts +7 -0
- package/lib/manager-removal-recovery.js +211 -0
- package/lib/manager-rotate.d.ts +15 -0
- package/lib/manager-rotate.js +536 -0
- package/lib/manager-types.d.ts +337 -0
- package/lib/manager-types.js +1 -0
- package/lib/manager.d.ts +2 -293
- package/lib/manager.js +38 -1072
- package/lib/manifest-token.d.ts +31 -0
- package/lib/manifest-token.js +51 -0
- package/lib/provider.d.ts +15 -2
- package/lib/provider.js +39 -0
- package/lib/record-types.d.ts +65 -0
- package/lib/record-types.js +865 -0
- package/lib/refreshing-credential-provider.d.ts +6 -2
- package/lib/refreshing-credential-provider.js +76 -13
- package/lib/suite.d.ts +5 -4
- package/lib/suite.js +2 -2
- package/lib/wrappable.d.ts +14 -0
- package/lib/wrappable.js +15 -0
- package/package.json +16 -15
- package/lib/op.d.ts +0 -128
- package/lib/op.js +0 -304
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import { normalizeDID } from '@kokuin/token';
|
|
2
|
+
import { AmbiguousCredentialWrapping, CredentialKeyLocked, CredentialKeyVersionMismatch, CredentialNotFound, CredentialRepairPending, CredentialSecretRejected, CredentialTampered, CredentialUnopenable, NoAvailableFactor, UnsupportedCredentialSuite } from './errors.js';
|
|
3
|
+
import { isSecretFactorKind } from './manager-context.js';
|
|
4
|
+
import { getSuite, isSupportedSuite } from './suite.js';
|
|
5
|
+
export function createOpenFunctions(ctx, internals) {
|
|
6
|
+
const { store, identity, runtime, authority, decrypting, cache, state, ensureFloored, nextHLC } = ctx;
|
|
7
|
+
const { winningBranch, putRecord, putWrappingRecord, suiteForVersion } = internals.records;
|
|
8
|
+
// Input-bearing opens (a supplied pin/passphrase) never dedup: their outcome
|
|
9
|
+
// and the PIN throttle are caller-specific, so one caller's attempt must not
|
|
10
|
+
// stand in for another's.
|
|
11
|
+
const hasSuppliedInputs = (inputs)=>inputs != null && Object.values(inputs).some((value)=>value != null);
|
|
12
|
+
async function isSelfRevoked(keyID) {
|
|
13
|
+
const self = normalizeDID(identity.id);
|
|
14
|
+
const revoked = (await store.listRevocations(keyID)).some((revocation)=>normalizeDID(revocation.recipient_did) === self);
|
|
15
|
+
if (revoked) cache.evict(keyID);
|
|
16
|
+
return revoked;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Whether this device could ever satisfy a wrapping, given the chance to
|
|
20
|
+
* prompt.
|
|
21
|
+
*
|
|
22
|
+
* This is what separates "locked" from "unopenable", and the two need opposite
|
|
23
|
+
* responses from a caller. `recipient` is normalized at the comparison and
|
|
24
|
+
* never at the write: it is a key-resolution input stored in peer:4 long form,
|
|
25
|
+
* and folding it would make the wrapping unopenable.
|
|
26
|
+
*/ function satisfiableFactors(descriptors) {
|
|
27
|
+
const needed = [];
|
|
28
|
+
for (const descriptor of descriptors){
|
|
29
|
+
if (isSecretFactorKind(descriptor.kind)) {
|
|
30
|
+
needed.push(descriptor.kind);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (descriptor.kind !== 'did') {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const recipient = descriptor.params.recipient;
|
|
37
|
+
if (decrypting == null || typeof recipient !== 'string') {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
if (normalizeDID(recipient) !== normalizeDID(identity.id)) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return needed;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The wrapping this device holds by its own `did`.
|
|
48
|
+
*
|
|
49
|
+
* Not found by `recipient_did`: that column is populated only for the
|
|
50
|
+
* single-`did` headless default, so a `[did, pin]` wrapping has it null. The
|
|
51
|
+
* descriptors are the authority. More than one match throws rather than
|
|
52
|
+
* picking — silently re-wrapping the wrong row would leave the other one
|
|
53
|
+
* un-gated and look like success.
|
|
54
|
+
*/ async function ownWrapping(keyID, keyVersion) {
|
|
55
|
+
const wrappings = await store.listWrappings(keyID, keyVersion);
|
|
56
|
+
const mine = wrappings.filter((wrapping)=>satisfiableFactors(wrapping.factors) != null && wrapping.factors.some((descriptor)=>descriptor.kind === 'did' && typeof descriptor.params.recipient === 'string' && normalizeDID(descriptor.params.recipient) === normalizeDID(identity.id)));
|
|
57
|
+
if (mine.length === 0) {
|
|
58
|
+
throw new CredentialNotFound('Credential wrapping for this device on key', keyID);
|
|
59
|
+
}
|
|
60
|
+
if (mine.length > 1) {
|
|
61
|
+
throw new AmbiguousCredentialWrapping(keyID, mine.length);
|
|
62
|
+
}
|
|
63
|
+
return mine[0];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Rebuild wrap-time inputs from stored descriptors.
|
|
67
|
+
*
|
|
68
|
+
* A `did` descriptor carries its recipient, so it reconstructs itself. A
|
|
69
|
+
* secret one carries a salt and a cost but never the secret, so the caller has
|
|
70
|
+
* to supply it again — the new wrapping's key is derived from every surviving
|
|
71
|
+
* factor, not only the one being changed.
|
|
72
|
+
*/ function factorsFromDescriptors(descriptors, inputs) {
|
|
73
|
+
return descriptors.map((descriptor)=>{
|
|
74
|
+
if (descriptor.kind === 'did') {
|
|
75
|
+
const recipient = descriptor.params.recipient;
|
|
76
|
+
if (typeof recipient !== 'string') {
|
|
77
|
+
throw new CredentialTampered('did factor descriptor carries no recipient');
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
kind: 'did',
|
|
81
|
+
recipientDID: recipient
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (!isSecretFactorKind(descriptor.kind)) {
|
|
85
|
+
throw new NoAvailableFactor(descriptor.kind);
|
|
86
|
+
}
|
|
87
|
+
const input = inputs?.[descriptor.kind];
|
|
88
|
+
if (input == null) {
|
|
89
|
+
throw new NoAvailableFactor(descriptor.kind);
|
|
90
|
+
}
|
|
91
|
+
const { m, t, p } = descriptor.params;
|
|
92
|
+
// The cost travels with the factor: re-wrapping is not the moment to
|
|
93
|
+
// silently reset a wrapping that was deliberately made expensive.
|
|
94
|
+
const cost = typeof m === 'number' && typeof t === 'number' && typeof p === 'number' ? {
|
|
95
|
+
m,
|
|
96
|
+
t,
|
|
97
|
+
p
|
|
98
|
+
} : undefined;
|
|
99
|
+
return {
|
|
100
|
+
kind: descriptor.kind,
|
|
101
|
+
input,
|
|
102
|
+
cost
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Re-wrap this device's own wrapping under a new factor list.
|
|
108
|
+
*
|
|
109
|
+
* The new row and old tombstone commit together under the key lock. Their
|
|
110
|
+
* order also keeps the old wrapping available until the new one is stored.
|
|
111
|
+
*/ async function rewrapOwn({ keyID, inputs, change, methods, authority: recordAuthority = authority }) {
|
|
112
|
+
await ensureFloored();
|
|
113
|
+
const key = await loadKey(keyID);
|
|
114
|
+
const existing = await ownWrapping(keyID, key.version);
|
|
115
|
+
const { contentKey, branchID } = await openKey(keyID, inputs);
|
|
116
|
+
const suite = getSuite(key.suite);
|
|
117
|
+
const wrappingID = runtime.getRandomID();
|
|
118
|
+
// `contentKey` is our own private clone from the cache; zero it once its use — this
|
|
119
|
+
// rewrap — is done, on success or throw. The `try` opens right after the
|
|
120
|
+
// acquisition so the caller-supplied `change` throwing also cannot leak it.
|
|
121
|
+
let wrapped;
|
|
122
|
+
try {
|
|
123
|
+
const factors = change(factorsFromDescriptors(existing.factors, inputs));
|
|
124
|
+
wrapped = await suite.wrap({
|
|
125
|
+
runtime,
|
|
126
|
+
contentKey,
|
|
127
|
+
factors,
|
|
128
|
+
wrappingID,
|
|
129
|
+
keyID,
|
|
130
|
+
keyVersion: key.version,
|
|
131
|
+
methods
|
|
132
|
+
});
|
|
133
|
+
} finally{
|
|
134
|
+
contentKey.fill(0);
|
|
135
|
+
}
|
|
136
|
+
const { descriptors, iv, wrappedKey } = wrapped;
|
|
137
|
+
const replacement = {
|
|
138
|
+
wrappingID,
|
|
139
|
+
keyID,
|
|
140
|
+
keyVersion: key.version,
|
|
141
|
+
factors: descriptors,
|
|
142
|
+
iv,
|
|
143
|
+
wrappedKey,
|
|
144
|
+
recipientDID: existing.recipient_did
|
|
145
|
+
};
|
|
146
|
+
const written = await store.transaction(async (tx)=>{
|
|
147
|
+
await tx.lockKeyRow(keyID);
|
|
148
|
+
const current = await tx.getKey(keyID);
|
|
149
|
+
if (current == null) throw new CredentialNotFound('Credential key', keyID);
|
|
150
|
+
const currentWrapping = (await tx.listWrappings(keyID, key.version)).find((wrapping)=>wrapping.wrapping_id === existing.wrapping_id);
|
|
151
|
+
if (current.version !== key.version || JSON.stringify(currentWrapping) !== JSON.stringify(existing)) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
await putWrappingRecord({
|
|
155
|
+
wrapping: replacement,
|
|
156
|
+
owner: key.owner_did,
|
|
157
|
+
branchID,
|
|
158
|
+
authority: recordAuthority,
|
|
159
|
+
target: tx
|
|
160
|
+
});
|
|
161
|
+
await putRecord({
|
|
162
|
+
type: 'credential.wrapping',
|
|
163
|
+
id: existing.wrapping_id,
|
|
164
|
+
owner: key.owner_did,
|
|
165
|
+
hlc: await nextHLC(),
|
|
166
|
+
payload: null,
|
|
167
|
+
meta: {
|
|
168
|
+
keyID,
|
|
169
|
+
keyVersion: existing.key_version,
|
|
170
|
+
recipient: existing.recipient_did
|
|
171
|
+
},
|
|
172
|
+
target: tx,
|
|
173
|
+
authority: recordAuthority
|
|
174
|
+
});
|
|
175
|
+
return true;
|
|
176
|
+
});
|
|
177
|
+
if (!written) await rewrapOwn({
|
|
178
|
+
keyID,
|
|
179
|
+
inputs,
|
|
180
|
+
change,
|
|
181
|
+
methods,
|
|
182
|
+
authority: recordAuthority
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
async function loadKey(keyID, atVersion) {
|
|
186
|
+
const key = await store.getKey(keyID);
|
|
187
|
+
if (key == null) {
|
|
188
|
+
throw new CredentialNotFound('Credential key', keyID);
|
|
189
|
+
}
|
|
190
|
+
if (!isSupportedSuite(key.suite) && (atVersion === undefined || atVersion === key.version)) {
|
|
191
|
+
throw new UnsupportedCredentialSuite(key.suite);
|
|
192
|
+
}
|
|
193
|
+
return key;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Recover a key's content key by trying its wrappings.
|
|
197
|
+
*
|
|
198
|
+
* The wrappings are an OR: any one opening is enough. If none opens, which
|
|
199
|
+
* failure this is depends on whether any of them COULD have opened with input
|
|
200
|
+
* this device is able to ask for — locked and unopenable are the same event
|
|
201
|
+
* with opposite remedies.
|
|
202
|
+
*/ /**
|
|
203
|
+
* Recover a key's content key. Wrapped by `openKey`, which adds the input-free
|
|
204
|
+
* dedup; this is the actual open. Captures the lock generation and epoch at
|
|
205
|
+
* entry so a `lock`/`lockAll` that lands before the terminal `cache.set` can
|
|
206
|
+
* suppress it — the key is still returned to this caller, but the cache is not
|
|
207
|
+
* re-warmed behind a lock.
|
|
208
|
+
*/ async function doOpenKey({ keyID, inputs, genAtStart, epochAtStart, atVersion }) {
|
|
209
|
+
// `genAtStart`/`epochAtStart` are captured by `openKey` at the moment the
|
|
210
|
+
// caller entered — before any dedup wait — so a lock that lands while a
|
|
211
|
+
// waiter is queued still suppresses that waiter's eventual re-warm.
|
|
212
|
+
const lockedSince = ()=>(state.keyGeneration.get(keyID) ?? 0) !== genAtStart || state.lockAllEpoch !== epochAtStart;
|
|
213
|
+
const key = await loadKey(keyID, atVersion);
|
|
214
|
+
const version = atVersion ?? key.version;
|
|
215
|
+
if (version > key.version) {
|
|
216
|
+
throw new CredentialKeyVersionMismatch(keyID, version, key.version);
|
|
217
|
+
}
|
|
218
|
+
if (await isSelfRevoked(keyID)) {
|
|
219
|
+
throw new CredentialUnopenable(keyID, 'recipient revoked');
|
|
220
|
+
}
|
|
221
|
+
const branchRows = await store.listKeyBranches(keyID, version);
|
|
222
|
+
const proven = new Set(branchRows.map((branch)=>branch.branch_id));
|
|
223
|
+
const winner = proven.size >= 2 ? await winningBranch(branchRows) : null;
|
|
224
|
+
const cached = cache.get(keyID, version);
|
|
225
|
+
if (cached != null) {
|
|
226
|
+
if (winner == null || cached.branchID === winner) {
|
|
227
|
+
return {
|
|
228
|
+
contentKey: cached.contentKey,
|
|
229
|
+
version,
|
|
230
|
+
branchID: cached.branchID
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
// A fork-losing hit: `cached.contentKey` is our private clone that
|
|
234
|
+
// we are about to discard for a re-unwrap, so zero it rather than orphan it.
|
|
235
|
+
cached.contentKey.fill(0);
|
|
236
|
+
cache.evictVersion(keyID, version);
|
|
237
|
+
}
|
|
238
|
+
const suite = getSuite(await suiteForVersion(keyID, version, key.suite));
|
|
239
|
+
const wrappings = await store.listWrappingsWithBranch(keyID, version);
|
|
240
|
+
if (wrappings.length === 0) {
|
|
241
|
+
throw new CredentialUnopenable(keyID, `no wrapping at version ${version}`);
|
|
242
|
+
}
|
|
243
|
+
// Concurrent rotations leave two content-key branches under one version. The
|
|
244
|
+
// labels a keyless party could already partition by resolve, here, which one
|
|
245
|
+
// this device opens — so every device converges on the same branch instead
|
|
246
|
+
// of the first-listed wrapping, which is order-dependent and diverges. An
|
|
247
|
+
// empty label is branch-agnostic (legacy/non-rotation state), not a branch of
|
|
248
|
+
// its own, so it never counts toward a fork.
|
|
249
|
+
// A label no key-op vouches for cannot be allowed to win or to open.
|
|
250
|
+
const eligible = wrappings.filter((wrapping)=>wrapping.branch_id === '' || proven.has(wrapping.branch_id));
|
|
251
|
+
// No fork: try every wrapping in listed order, unchanged. Fork: only the
|
|
252
|
+
// winner-labelled wrappings, with empty-label ones as a last-resort fallback
|
|
253
|
+
// — a loser-labelled wrapping opens the wrong content key and is skipped.
|
|
254
|
+
const toTry = winner == null ? eligible : [
|
|
255
|
+
...eligible.filter((wrapping)=>wrapping.branch_id === winner),
|
|
256
|
+
...eligible.filter((wrapping)=>wrapping.branch_id === '')
|
|
257
|
+
];
|
|
258
|
+
const reasons = [];
|
|
259
|
+
const promptable = new Set();
|
|
260
|
+
let rejected = null;
|
|
261
|
+
for (const wrapping of toTry){
|
|
262
|
+
const satisfiable = satisfiableFactors(wrapping.factors);
|
|
263
|
+
if (satisfiable == null) {
|
|
264
|
+
reasons.push(`wrapping ${wrapping.wrapping_id}: not addressed to this device`);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
try {
|
|
268
|
+
const contentKey = await suite.unwrap({
|
|
269
|
+
identity: decrypting,
|
|
270
|
+
descriptors: wrapping.factors,
|
|
271
|
+
inputs,
|
|
272
|
+
iv: new Uint8Array(wrapping.iv),
|
|
273
|
+
wrappedKey: new Uint8Array(wrapping.wrapped_key),
|
|
274
|
+
wrappingID: wrapping.wrapping_id,
|
|
275
|
+
keyID,
|
|
276
|
+
keyVersion: wrapping.key_version
|
|
277
|
+
});
|
|
278
|
+
// In a fork every device names the same winner keylessly, so caching the
|
|
279
|
+
// winner as the resolved branch converges post-fork writes even when an
|
|
280
|
+
// empty-label fallback wrapping is what actually opened.
|
|
281
|
+
const branchID = winner ?? wrapping.branch_id;
|
|
282
|
+
// Suppress the re-warm if a lock landed while we were unwrapping: the
|
|
283
|
+
// caller still gets its key, but the cache stays cleared.
|
|
284
|
+
if (!lockedSince()) {
|
|
285
|
+
cache.set(keyID, contentKey, version, branchID);
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
contentKey,
|
|
289
|
+
version,
|
|
290
|
+
branchID
|
|
291
|
+
};
|
|
292
|
+
} catch (error) {
|
|
293
|
+
reasons.push(error instanceof Error ? error.message : String(error));
|
|
294
|
+
if (error instanceof CredentialSecretRejected) {
|
|
295
|
+
rejected = error;
|
|
296
|
+
}
|
|
297
|
+
for (const kind of satisfiable){
|
|
298
|
+
promptable.add(kind);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// A secret WAS given and was wrong. Distinct from the case below, and it has
|
|
303
|
+
// to survive: a caller retries on one and prompts on the other, and the PIN
|
|
304
|
+
// throttle only counts an attempt that was actually made.
|
|
305
|
+
if (rejected != null) {
|
|
306
|
+
throw rejected;
|
|
307
|
+
}
|
|
308
|
+
// A wrapping this device is addressed by, that failed only for want of a
|
|
309
|
+
// secret nobody was asked for, is a prompt — not a dead end.
|
|
310
|
+
if (promptable.size > 0) {
|
|
311
|
+
throw new CredentialKeyLocked(keyID, [
|
|
312
|
+
...promptable
|
|
313
|
+
]);
|
|
314
|
+
}
|
|
315
|
+
if (winner !== null && eligible.some((wrapping)=>wrapping.branch_id !== winner && satisfiableFactors(wrapping.factors) !== null)) {
|
|
316
|
+
throw new CredentialRepairPending(keyID);
|
|
317
|
+
}
|
|
318
|
+
throw new CredentialUnopenable(keyID, reasons.join('; '));
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Open a key, collapsing concurrent input-free opens of the same key into one
|
|
322
|
+
* unwrap. A waiter shares only the fact that an open ran, then takes its own
|
|
323
|
+
* clone from the cache; on a miss — the leader hit a lock and skipped its set,
|
|
324
|
+
* or failed — it falls through to its own open. Input-bearing opens never join
|
|
325
|
+
* the herd (see `hasSuppliedInputs`).
|
|
326
|
+
*/ async function openKey(keyID, inputs, atVersion) {
|
|
327
|
+
// Captured once, at entry, so a lock landing while this call is queued behind
|
|
328
|
+
// another open still counts against it (see `doOpenKey`).
|
|
329
|
+
const genAtStart = state.keyGeneration.get(keyID) ?? 0;
|
|
330
|
+
const epochAtStart = state.lockAllEpoch;
|
|
331
|
+
if (hasSuppliedInputs(inputs)) {
|
|
332
|
+
return doOpenKey({
|
|
333
|
+
keyID,
|
|
334
|
+
inputs,
|
|
335
|
+
genAtStart,
|
|
336
|
+
epochAtStart,
|
|
337
|
+
atVersion
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
const flightID = atVersion === undefined ? keyID : `${keyID}:v${atVersion}`;
|
|
341
|
+
// Join any open already in flight for this key; on a miss, re-contend for
|
|
342
|
+
// leadership rather than opening in parallel — so post-miss opens (a leader
|
|
343
|
+
// that failed or was lock-suppressed) serialize into one leader at a time
|
|
344
|
+
// instead of re-forming the herd.
|
|
345
|
+
while(true){
|
|
346
|
+
const pending = state.inFlight.get(flightID);
|
|
347
|
+
if (pending == null) {
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
await pending;
|
|
351
|
+
if (await isSelfRevoked(keyID)) {
|
|
352
|
+
throw new CredentialUnopenable(keyID, 'recipient revoked');
|
|
353
|
+
}
|
|
354
|
+
const version = atVersion ?? (await loadKey(keyID)).version;
|
|
355
|
+
const hit = cache.get(keyID, version);
|
|
356
|
+
if (hit != null) {
|
|
357
|
+
return {
|
|
358
|
+
contentKey: hit.contentKey,
|
|
359
|
+
version,
|
|
360
|
+
branchID: hit.branchID
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
const run = doOpenKey({
|
|
365
|
+
keyID,
|
|
366
|
+
inputs,
|
|
367
|
+
genAtStart,
|
|
368
|
+
epochAtStart,
|
|
369
|
+
atVersion
|
|
370
|
+
});
|
|
371
|
+
// A gate that never rejects, so a waiter's `await pending` cannot throw the
|
|
372
|
+
// leader's error into the wrong caller; the waiter re-derives its own result
|
|
373
|
+
// from the cache (or its own open) after the gate settles.
|
|
374
|
+
const gate = run.then(()=>undefined, ()=>undefined);
|
|
375
|
+
state.inFlight.set(flightID, gate);
|
|
376
|
+
try {
|
|
377
|
+
return await run;
|
|
378
|
+
} finally{
|
|
379
|
+
// Only clear our own entry — a later leader may already have replaced it.
|
|
380
|
+
if (state.inFlight.get(flightID) === gate) {
|
|
381
|
+
state.inFlight.delete(flightID);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
async function openSpecificBranch({ keyID, version, branchID, suiteID, inputs }) {
|
|
386
|
+
const suite = getSuite(suiteID);
|
|
387
|
+
for (const wrapping of (await store.listWrappingsWithBranch(keyID, version))){
|
|
388
|
+
const needed = satisfiableFactors(wrapping.factors);
|
|
389
|
+
if (wrapping.branch_id !== branchID || needed === null || needed.some((kind)=>inputs?.[kind] == null)) {
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
try {
|
|
393
|
+
return await suite.unwrap({
|
|
394
|
+
identity: decrypting,
|
|
395
|
+
descriptors: wrapping.factors,
|
|
396
|
+
inputs,
|
|
397
|
+
iv: new Uint8Array(wrapping.iv),
|
|
398
|
+
wrappedKey: new Uint8Array(wrapping.wrapped_key),
|
|
399
|
+
wrappingID: wrapping.wrapping_id,
|
|
400
|
+
keyID,
|
|
401
|
+
keyVersion: version
|
|
402
|
+
});
|
|
403
|
+
} catch {
|
|
404
|
+
// Another recipient's wrapping or corrupt local material cannot open this branch.
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
return {
|
|
410
|
+
isSelfRevoked,
|
|
411
|
+
satisfiableFactors,
|
|
412
|
+
ownWrapping,
|
|
413
|
+
factorsFromDescriptors,
|
|
414
|
+
loadKey,
|
|
415
|
+
doOpenKey,
|
|
416
|
+
openKey,
|
|
417
|
+
openSpecificBranch,
|
|
418
|
+
rewrapOwn,
|
|
419
|
+
hasSuppliedInputs
|
|
420
|
+
};
|
|
421
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { type SignedRecord } from '@kubun/records';
|
|
2
|
+
import type { CredentialFactorDescriptor, CredentialRecordPayloads, CredentialRecordType, CredentialStoreAPI, PutCredentialKeyParams } from '@kubun/store-credential';
|
|
3
|
+
import type { CredentialAuthority } from './authority.js';
|
|
4
|
+
import type { ManagerContext } from './manager-context.js';
|
|
5
|
+
import type { GrantEntryParams } from './manager-types.js';
|
|
6
|
+
type PutRecordParams<T extends CredentialRecordType> = {
|
|
7
|
+
type: T;
|
|
8
|
+
id: string;
|
|
9
|
+
owner: string;
|
|
10
|
+
hlc: string;
|
|
11
|
+
payload: CredentialRecordPayloads[T] | null;
|
|
12
|
+
meta: Record<string, unknown>;
|
|
13
|
+
target?: CredentialStoreAPI;
|
|
14
|
+
authority?: CredentialAuthority;
|
|
15
|
+
};
|
|
16
|
+
type PutKeyRecordParams = {
|
|
17
|
+
key: PutCredentialKeyParams;
|
|
18
|
+
keyUnique: string;
|
|
19
|
+
hlc: string;
|
|
20
|
+
authority?: CredentialAuthority;
|
|
21
|
+
target?: CredentialStoreAPI;
|
|
22
|
+
repairDigest?: string;
|
|
23
|
+
};
|
|
24
|
+
type BranchRecordIDParams = {
|
|
25
|
+
keyID: string;
|
|
26
|
+
version: number;
|
|
27
|
+
branchID: string;
|
|
28
|
+
target?: CredentialStoreAPI;
|
|
29
|
+
};
|
|
30
|
+
type PutWrappingRecordParams = {
|
|
31
|
+
wrapping: {
|
|
32
|
+
wrappingID: string;
|
|
33
|
+
keyID: string;
|
|
34
|
+
keyVersion: number;
|
|
35
|
+
factors: Array<CredentialFactorDescriptor>;
|
|
36
|
+
iv: Uint8Array;
|
|
37
|
+
wrappedKey: Uint8Array;
|
|
38
|
+
recipientDID?: string | null;
|
|
39
|
+
};
|
|
40
|
+
owner: string;
|
|
41
|
+
branchID: string;
|
|
42
|
+
branchRecordID?: string;
|
|
43
|
+
authority?: CredentialAuthority;
|
|
44
|
+
target?: CredentialStoreAPI;
|
|
45
|
+
};
|
|
46
|
+
type PutEntryRecordParams = {
|
|
47
|
+
entry: {
|
|
48
|
+
entryID: string;
|
|
49
|
+
keyID: string;
|
|
50
|
+
keyVersion: number;
|
|
51
|
+
iv: Uint8Array;
|
|
52
|
+
ciphertext: Uint8Array;
|
|
53
|
+
hlc: string;
|
|
54
|
+
};
|
|
55
|
+
owner: string;
|
|
56
|
+
branchID: string;
|
|
57
|
+
contentHLC?: string;
|
|
58
|
+
target?: CredentialStoreAPI;
|
|
59
|
+
authority?: CredentialAuthority;
|
|
60
|
+
grant?: GrantEntryParams;
|
|
61
|
+
};
|
|
62
|
+
type EntryGrantInfo = {
|
|
63
|
+
grantDeviceDID: string;
|
|
64
|
+
grantName: string;
|
|
65
|
+
grantOwnerDID: string | undefined;
|
|
66
|
+
};
|
|
67
|
+
export type RecordFunctions = {
|
|
68
|
+
storedKeyRecord: (keyID: string) => Promise<SignedRecord | null>;
|
|
69
|
+
entryRecord: (entryID: string, target?: CredentialStoreAPI, ownerDID?: string) => Promise<SignedRecord | null>;
|
|
70
|
+
winningBranch: (branches: ReadonlyArray<{
|
|
71
|
+
branch_id: string;
|
|
72
|
+
op_jwt: string;
|
|
73
|
+
}>) => Promise<string | null>;
|
|
74
|
+
putRecord: <T extends CredentialRecordType>(args: PutRecordParams<T>) => Promise<SignedRecord & {
|
|
75
|
+
type: T;
|
|
76
|
+
}>;
|
|
77
|
+
keyRecord: (args: PutKeyRecordParams) => Promise<{
|
|
78
|
+
branchID: string;
|
|
79
|
+
branchRecordID: string;
|
|
80
|
+
}>;
|
|
81
|
+
keyUnique: (keyID: string) => Promise<string>;
|
|
82
|
+
branchRecordID: (args: BranchRecordIDParams) => Promise<string>;
|
|
83
|
+
putWrappingRecord: (args: PutWrappingRecordParams) => Promise<void>;
|
|
84
|
+
putEntryRecord: (args: PutEntryRecordParams) => Promise<void>;
|
|
85
|
+
entryGrant: (entryID: string, target?: CredentialStoreAPI, ownerDID?: string) => Promise<EntryGrantInfo | undefined>;
|
|
86
|
+
writableGrant: (entryID: string, ownerDID?: string) => Promise<EntryGrantInfo | undefined>;
|
|
87
|
+
branchProofs: (keyID: string, version: number) => Promise<Map<string, {
|
|
88
|
+
suite: number;
|
|
89
|
+
repairDigest?: string;
|
|
90
|
+
}>>;
|
|
91
|
+
suiteForVersion: (keyID: string, version: number, currentSuite: number) => Promise<number>;
|
|
92
|
+
};
|
|
93
|
+
export declare function createRecordFunctions(ctx: ManagerContext): RecordFunctions;
|
|
94
|
+
export {};
|