@kubun/credential 0.17.4 → 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 +15 -6
- package/lib/index.js +7 -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 -1073
- 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/package.json +16 -15
- package/lib/op.d.ts +0 -128
- package/lib/op.js +0 -304
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
import { normalizeDID } from '@kokuin/token';
|
|
2
|
+
import { HLC } from '@kubun/hlc';
|
|
3
|
+
import { recordPayloadDigest, verifySignedRecord } from '@kubun/records';
|
|
4
|
+
import { credentialRevocationRecordID, decodeCredentialRecord } from '@kubun/store-credential';
|
|
5
|
+
import { CredentialInvalidEntry, CredentialKeyVersionMismatch, CredentialNotAuthorized, CredentialNotFound, CredentialRepairPending, CredentialUnopenable } from './errors.js';
|
|
6
|
+
import { isSecretFactorKind, wrappingNamesRemovedDevice } from './manager-context.js';
|
|
7
|
+
import { getSuite } from './suite.js';
|
|
8
|
+
export function createRotateFunctions(ctx, internals) {
|
|
9
|
+
const { store, identity, runtime, authority, cache, nextHLC, params } = ctx;
|
|
10
|
+
const { entryRecord, winningBranch, putRecord, keyRecord, keyUnique, putWrappingRecord, putEntryRecord, entryGrant, branchProofs, suiteForVersion } = internals.records;
|
|
11
|
+
const { factorsFromDescriptors, loadKey, openKey, openSpecificBranch } = internals.open;
|
|
12
|
+
async function rotateKey({ keyID, rotateParams, revokedDID, excludedRecipients = new Set() }) {
|
|
13
|
+
const key = await loadKey(keyID);
|
|
14
|
+
// Rotation is administration, not use: it retires a content key every
|
|
15
|
+
// other recipient holds and re-makes their wrappings under one this
|
|
16
|
+
// identity chose. Asked before the first read, because everything below
|
|
17
|
+
// opens the key and decrypts every entry before it writes anything — a
|
|
18
|
+
// gate after that has already done the work it exists to refuse.
|
|
19
|
+
if (revokedDID === undefined && !await (rotateParams?.authority ?? authority).authorize('administer', {
|
|
20
|
+
key,
|
|
21
|
+
store
|
|
22
|
+
})) {
|
|
23
|
+
throw new CredentialNotAuthorized('administer', keyID);
|
|
24
|
+
}
|
|
25
|
+
// An older partial rotation may have advanced the key row before writing
|
|
26
|
+
// this device's wrapping. Recover from the highest version we can open;
|
|
27
|
+
// locked factors and rejected secrets still require the caller's input.
|
|
28
|
+
let sourceVersion = key.version;
|
|
29
|
+
let previousKey;
|
|
30
|
+
for(let candidate = key.version; candidate >= 1; candidate -= 1){
|
|
31
|
+
try {
|
|
32
|
+
previousKey = (await openKey(keyID, rotateParams?.inputs, candidate)).contentKey;
|
|
33
|
+
sourceVersion = candidate;
|
|
34
|
+
break;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (!(error instanceof CredentialUnopenable) || candidate === 1) throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (previousKey === undefined) throw new CredentialUnopenable(keyID, 'no openable version');
|
|
40
|
+
const previousSuite = getSuite(await suiteForVersion(keyID, sourceVersion, key.suite));
|
|
41
|
+
const suiteID = rotateParams?.suite ?? key.suite;
|
|
42
|
+
const suite = getSuite(suiteID);
|
|
43
|
+
const newVersion = key.version + 1;
|
|
44
|
+
// Open the old key before entering the transaction, then read every row
|
|
45
|
+
// being re-issued inside it. A concurrent grant or entry update must be
|
|
46
|
+
// included in the rotation, rather than replaced by an earlier snapshot.
|
|
47
|
+
// Both key buffers are our private clones. One try spans from here to the end so
|
|
48
|
+
// a throw anywhere in the rotation zeros them, not only the success path.
|
|
49
|
+
// Both keys are zeroed whether the transaction commits or rolls back.
|
|
50
|
+
let generatedKey;
|
|
51
|
+
try {
|
|
52
|
+
const contentKey = suite.generateContentKey(runtime);
|
|
53
|
+
generatedKey = contentKey;
|
|
54
|
+
// Mint the key-op stamp before the entries and wrappings it labels. All
|
|
55
|
+
// records commit together, so no reader sees its version bump alone.
|
|
56
|
+
const rotatedKey = {
|
|
57
|
+
keyID,
|
|
58
|
+
ownerDID: key.owner_did,
|
|
59
|
+
suite: suiteID,
|
|
60
|
+
version: newVersion,
|
|
61
|
+
state: key.state === 'retired' ? 'retired' : 'active'
|
|
62
|
+
};
|
|
63
|
+
const unique = await keyUnique(keyID);
|
|
64
|
+
const keyHLC = await nextHLC();
|
|
65
|
+
for(;;){
|
|
66
|
+
// Resolve recipient keys and derive any secret factors before taking a
|
|
67
|
+
// database lock. The locked read below validates this speculative set;
|
|
68
|
+
// a grant or rewrap that landed meanwhile makes us prepare again.
|
|
69
|
+
const observed = await store.listWrappings(keyID, sourceVersion);
|
|
70
|
+
const denied = new Set(excludedRecipients);
|
|
71
|
+
if (revokedDID !== undefined) denied.add(revokedDID);
|
|
72
|
+
const retained = observed.filter((wrapping)=>!wrappingNamesRemovedDevice(wrapping, denied)).map((wrapping)=>({
|
|
73
|
+
recipientDID: wrapping.recipient_did,
|
|
74
|
+
factors: factorsFromDescriptors(wrapping.factors, rotateParams?.inputs)
|
|
75
|
+
}));
|
|
76
|
+
const prepared = [];
|
|
77
|
+
for (const { recipientDID, factors } of retained){
|
|
78
|
+
const wrappingID = runtime.getRandomID();
|
|
79
|
+
const { descriptors, iv, wrappedKey } = await suite.wrap({
|
|
80
|
+
runtime,
|
|
81
|
+
contentKey,
|
|
82
|
+
factors,
|
|
83
|
+
wrappingID,
|
|
84
|
+
keyID,
|
|
85
|
+
keyVersion: newVersion,
|
|
86
|
+
methods: rotateParams?.methods
|
|
87
|
+
});
|
|
88
|
+
prepared.push({
|
|
89
|
+
wrappingID,
|
|
90
|
+
keyID,
|
|
91
|
+
keyVersion: newVersion,
|
|
92
|
+
factors: descriptors,
|
|
93
|
+
iv,
|
|
94
|
+
wrappedKey,
|
|
95
|
+
recipientDID
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
const branchID = await store.transaction(async (tx)=>{
|
|
99
|
+
await tx.lockKeyRow(keyID);
|
|
100
|
+
if ((await tx.getKey(keyID))?.version !== key.version) {
|
|
101
|
+
throw new CredentialKeyVersionMismatch(keyID, key.version, (await tx.getKey(keyID))?.version ?? -1);
|
|
102
|
+
}
|
|
103
|
+
if (JSON.stringify(await tx.listWrappings(keyID, sourceVersion)) !== JSON.stringify(observed)) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const previousContentHLCs = new Map();
|
|
107
|
+
const entryGrants = new Map();
|
|
108
|
+
const plaintexts = [];
|
|
109
|
+
try {
|
|
110
|
+
for (const entry of (await tx.listEntries(keyID))){
|
|
111
|
+
const entryID = entry.entry_id;
|
|
112
|
+
const grant = await entryGrant(entryID, tx, entry.owner_did);
|
|
113
|
+
if (grant !== undefined && normalizeDID(grant.grantDeviceDID) !== normalizeDID(identity.id)) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (entry.key_version !== sourceVersion) {
|
|
117
|
+
throw new CredentialKeyVersionMismatch(entry.entry_id, entry.key_version, sourceVersion);
|
|
118
|
+
}
|
|
119
|
+
const previous = await entryRecord(entryID, tx, entry.owner_did);
|
|
120
|
+
const previousOp = previous === null ? null : await verifySignedRecord(previous);
|
|
121
|
+
const stamp = previousOp?.claim.meta.contentHLC;
|
|
122
|
+
if (typeof stamp === 'string' && (previousOp === null || !excludedRecipients.has(previousOp.issuer))) {
|
|
123
|
+
previousContentHLCs.set(entryID, stamp);
|
|
124
|
+
}
|
|
125
|
+
if (grant !== undefined) entryGrants.set(entryID, grant);
|
|
126
|
+
plaintexts.push({
|
|
127
|
+
entryID,
|
|
128
|
+
plaintext: previousSuite.decryptEntry({
|
|
129
|
+
contentKey: previousKey,
|
|
130
|
+
iv: new Uint8Array(entry.iv),
|
|
131
|
+
ciphertext: new Uint8Array(entry.ciphertext),
|
|
132
|
+
keyID,
|
|
133
|
+
keyVersion: entry.key_version,
|
|
134
|
+
entryID
|
|
135
|
+
})
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
const { branchID, branchRecordID: branchIDRecord } = await keyRecord({
|
|
139
|
+
key: rotatedKey,
|
|
140
|
+
keyUnique: unique,
|
|
141
|
+
hlc: keyHLC,
|
|
142
|
+
authority: rotateParams?.authority ?? authority,
|
|
143
|
+
target: tx
|
|
144
|
+
});
|
|
145
|
+
for (const wrappingParams of prepared){
|
|
146
|
+
await putWrappingRecord({
|
|
147
|
+
wrapping: wrappingParams,
|
|
148
|
+
owner: key.owner_did,
|
|
149
|
+
branchID,
|
|
150
|
+
branchRecordID: branchIDRecord,
|
|
151
|
+
authority: rotateParams?.authority ?? authority,
|
|
152
|
+
target: tx
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
for (const { entryID, plaintext } of plaintexts){
|
|
156
|
+
const grant = entryGrants.get(entryID);
|
|
157
|
+
let encrypted;
|
|
158
|
+
try {
|
|
159
|
+
encrypted = suite.encryptEntry({
|
|
160
|
+
runtime,
|
|
161
|
+
contentKey,
|
|
162
|
+
plaintext,
|
|
163
|
+
keyID,
|
|
164
|
+
keyVersion: newVersion,
|
|
165
|
+
entryID
|
|
166
|
+
});
|
|
167
|
+
} finally{
|
|
168
|
+
plaintext.fill(0);
|
|
169
|
+
}
|
|
170
|
+
const entryHLC = await nextHLC();
|
|
171
|
+
await putEntryRecord({
|
|
172
|
+
entry: {
|
|
173
|
+
entryID,
|
|
174
|
+
keyID,
|
|
175
|
+
keyVersion: newVersion,
|
|
176
|
+
...encrypted,
|
|
177
|
+
hlc: entryHLC
|
|
178
|
+
},
|
|
179
|
+
owner: key.owner_did,
|
|
180
|
+
branchID,
|
|
181
|
+
contentHLC: previousContentHLCs.get(entryID) ?? entryHLC,
|
|
182
|
+
target: tx,
|
|
183
|
+
authority: rotateParams?.authority ?? authority,
|
|
184
|
+
grant
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
const revokedRecipients = new Set(excludedRecipients);
|
|
188
|
+
if (revokedDID !== undefined) revokedRecipients.add(revokedDID);
|
|
189
|
+
for (const recipientDID of revokedRecipients){
|
|
190
|
+
await putRecord({
|
|
191
|
+
type: 'credential.revocation',
|
|
192
|
+
id: credentialRevocationRecordID(keyID, recipientDID),
|
|
193
|
+
owner: key.owner_did,
|
|
194
|
+
hlc: await nextHLC(),
|
|
195
|
+
payload: {},
|
|
196
|
+
meta: {
|
|
197
|
+
keyID,
|
|
198
|
+
revokedDID: recipientDID
|
|
199
|
+
},
|
|
200
|
+
target: tx,
|
|
201
|
+
authority: rotateParams?.authority ?? authority
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
if (revokedRecipients.size > 0) {
|
|
205
|
+
for (const wrapping of (await tx.listWrappings(keyID))){
|
|
206
|
+
const recipients = [
|
|
207
|
+
wrapping.recipient_did,
|
|
208
|
+
...wrapping.factors.filter((factor)=>factor.kind === 'did').map((factor)=>factor.params.recipient)
|
|
209
|
+
];
|
|
210
|
+
if (recipients.some((recipient)=>typeof recipient === 'string' && revokedRecipients.has(normalizeDID(recipient)))) {
|
|
211
|
+
await tx.deleteWrapping(wrapping.wrapping_id);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return branchID;
|
|
216
|
+
} finally{
|
|
217
|
+
for (const { plaintext } of plaintexts)plaintext.fill(0);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
if (branchID == null) continue;
|
|
221
|
+
// The rotation's own key-op labels the branch it just made. `cache.set`
|
|
222
|
+
// stores its own clone, so zeroing the local generated key in the
|
|
223
|
+
// `finally` does not disturb the cached one.
|
|
224
|
+
cache.set(keyID, contentKey, newVersion, branchID);
|
|
225
|
+
return newVersion;
|
|
226
|
+
}
|
|
227
|
+
} finally{
|
|
228
|
+
previousKey.fill(0);
|
|
229
|
+
generatedKey?.fill(0);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
async function repairFork(keyID, inputs) {
|
|
233
|
+
await internals.removalRecovery.checkRemovalRecovery();
|
|
234
|
+
const key = await loadKey(keyID);
|
|
235
|
+
if (!await authority.authorize('administer', {
|
|
236
|
+
key,
|
|
237
|
+
store
|
|
238
|
+
})) {
|
|
239
|
+
throw new CredentialNotAuthorized('administer', keyID);
|
|
240
|
+
}
|
|
241
|
+
const branches = await store.listKeyBranches(keyID, key.version);
|
|
242
|
+
if (branches.length < 2) return {
|
|
243
|
+
status: 'not-needed'
|
|
244
|
+
};
|
|
245
|
+
const winner = await winningBranch(branches);
|
|
246
|
+
if (winner === null) throw new CredentialNotFound('Credential key branch', keyID);
|
|
247
|
+
const proofs = await branchProofs(keyID, key.version);
|
|
248
|
+
const digest = proofs.get(winner)?.repairDigest;
|
|
249
|
+
if (digest !== undefined && branches.every((branch)=>proofs.get(branch.branch_id)?.repairDigest === digest)) {
|
|
250
|
+
return {
|
|
251
|
+
status: 'not-needed'
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
const revocations = new Set((await store.listRevocations(keyID)).map((row)=>normalizeDID(row.recipient_did)));
|
|
255
|
+
const recovery = params.removalRecovery;
|
|
256
|
+
if (recovery !== undefined) {
|
|
257
|
+
for (const did of await recovery.removedDevices?.(key.owner_did) ?? []){
|
|
258
|
+
revocations.add(normalizeDID(did));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const wrappings = (await store.listWrappingsWithBranch(keyID, key.version)).filter((row)=>!wrappingNamesRemovedDevice(row, revocations));
|
|
262
|
+
const recipients = new Map();
|
|
263
|
+
const unaddressedWrappings = wrappings.filter((wrapping)=>wrapping.recipient_did === null);
|
|
264
|
+
const winnerRecipients = new Set();
|
|
265
|
+
for (const wrapping of wrappings){
|
|
266
|
+
const recipient = wrapping.recipient_did;
|
|
267
|
+
if (recipient === null) continue;
|
|
268
|
+
const normalized = normalizeDID(recipient);
|
|
269
|
+
if (wrapping.branch_id === winner) winnerRecipients.add(normalized);
|
|
270
|
+
if (!recipients.has(normalized) || wrapping.branch_id === winner) {
|
|
271
|
+
recipients.set(normalized, wrapping);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const candidates = [
|
|
275
|
+
...await store.listEntryCandidates(keyID, key.version),
|
|
276
|
+
...await store.listPriorEntryTombstones(keyID, key.version)
|
|
277
|
+
];
|
|
278
|
+
const loserAddsRecipient = [
|
|
279
|
+
...recipients.keys()
|
|
280
|
+
].some((did)=>!winnerRecipients.has(did)) || unaddressedWrappings.some((wrapping)=>wrapping.branch_id !== winner);
|
|
281
|
+
const potentialLoserEntry = candidates.length > 0;
|
|
282
|
+
if (!potentialLoserEntry && !loserAddsRecipient) return {
|
|
283
|
+
status: 'not-needed'
|
|
284
|
+
};
|
|
285
|
+
const keys = new Map();
|
|
286
|
+
try {
|
|
287
|
+
for (const branch of branches){
|
|
288
|
+
const proof = proofs.get(branch.branch_id);
|
|
289
|
+
if (proof === undefined) return {
|
|
290
|
+
status: 'pending'
|
|
291
|
+
};
|
|
292
|
+
const contentKey = await openSpecificBranch({
|
|
293
|
+
keyID,
|
|
294
|
+
version: key.version,
|
|
295
|
+
branchID: branch.branch_id,
|
|
296
|
+
suiteID: proof.suite,
|
|
297
|
+
inputs
|
|
298
|
+
});
|
|
299
|
+
if (contentKey === null) return {
|
|
300
|
+
status: 'pending'
|
|
301
|
+
};
|
|
302
|
+
keys.set(branch.branch_id, contentKey);
|
|
303
|
+
}
|
|
304
|
+
const byEntry = new Map();
|
|
305
|
+
const verifiedCandidates = await Promise.all(candidates.map(async (record)=>({
|
|
306
|
+
record,
|
|
307
|
+
op: await verifySignedRecord(record)
|
|
308
|
+
})));
|
|
309
|
+
for (const { record, op } of verifiedCandidates){
|
|
310
|
+
if (op === null) return {
|
|
311
|
+
status: 'pending'
|
|
312
|
+
};
|
|
313
|
+
const branchID = op.claim.meta.branchID;
|
|
314
|
+
const contentHLC = op.claim.meta.contentHLC;
|
|
315
|
+
const keyVersion = op.claim.meta.keyVersion;
|
|
316
|
+
const priorTombstone = record.payload === null && typeof keyVersion === 'number' && keyVersion < key.version;
|
|
317
|
+
if (typeof branchID !== 'string' && !(branchID === undefined && record.payload === null) || typeof contentHLC !== 'string' || typeof keyVersion !== 'number' || typeof branchID === 'string' && !priorTombstone && !keys.has(branchID)) continue;
|
|
318
|
+
const list = byEntry.get(record.id) ?? [];
|
|
319
|
+
list.push({
|
|
320
|
+
record,
|
|
321
|
+
branchID: priorTombstone ? '' : branchID ?? '',
|
|
322
|
+
contentHLC,
|
|
323
|
+
keyVersion
|
|
324
|
+
});
|
|
325
|
+
byEntry.set(record.id, list);
|
|
326
|
+
}
|
|
327
|
+
const compareCandidates = (a, b)=>{
|
|
328
|
+
const contentOrder = HLC.compare(HLC.parse(a.contentHLC), HLC.parse(b.contentHLC));
|
|
329
|
+
if (contentOrder !== 0) return contentOrder;
|
|
330
|
+
if (a.keyVersion !== b.keyVersion) return a.keyVersion - b.keyVersion;
|
|
331
|
+
return a.record.opHash < b.record.opHash ? -1 : a.record.opHash > b.record.opHash ? 1 : 0;
|
|
332
|
+
};
|
|
333
|
+
const decryptCandidate = async (candidate)=>{
|
|
334
|
+
if (candidate.record.payload === null) return null;
|
|
335
|
+
const proof = proofs.get(candidate.branchID);
|
|
336
|
+
const sourceKey = keys.get(candidate.branchID);
|
|
337
|
+
const op = await verifySignedRecord(candidate.record);
|
|
338
|
+
if (proof === undefined || sourceKey === undefined || op === null) {
|
|
339
|
+
throw new CredentialRepairPending(keyID);
|
|
340
|
+
}
|
|
341
|
+
const decoded = decodeCredentialRecord(candidate.record, op.claim.meta);
|
|
342
|
+
if (decoded.type !== 'credential.entry') throw new CredentialInvalidEntry(candidate.record.id);
|
|
343
|
+
return getSuite(proof.suite).decryptEntry({
|
|
344
|
+
contentKey: sourceKey,
|
|
345
|
+
iv: decoded.payload.iv,
|
|
346
|
+
ciphertext: decoded.payload.ciphertext,
|
|
347
|
+
keyID,
|
|
348
|
+
keyVersion: candidate.keyVersion,
|
|
349
|
+
entryID: candidate.record.id
|
|
350
|
+
});
|
|
351
|
+
};
|
|
352
|
+
const selected = [];
|
|
353
|
+
try {
|
|
354
|
+
let loserAddsEntry = false;
|
|
355
|
+
let unpreservableGrant = false;
|
|
356
|
+
for (const [entryID, versions] of byEntry){
|
|
357
|
+
versions.sort(compareCandidates);
|
|
358
|
+
const chosen = versions[versions.length - 1];
|
|
359
|
+
if (chosen === undefined) continue;
|
|
360
|
+
const plaintext = await decryptCandidate(chosen);
|
|
361
|
+
const winnerCandidate = versions.filter((candidate)=>candidate.branchID === winner).sort(compareCandidates).at(-1);
|
|
362
|
+
if (chosen.branchID !== winner) {
|
|
363
|
+
const winnerPlaintext = winnerCandidate === undefined ? undefined : await decryptCandidate(winnerCandidate);
|
|
364
|
+
const same = winnerPlaintext !== undefined && winnerCandidate?.contentHLC === chosen.contentHLC && (plaintext === null && winnerPlaintext === null || plaintext !== null && winnerPlaintext !== null && plaintext.length === winnerPlaintext.length && plaintext.every((byte, index)=>byte === winnerPlaintext[index]));
|
|
365
|
+
winnerPlaintext?.fill(0);
|
|
366
|
+
if (!same) loserAddsEntry = true;
|
|
367
|
+
}
|
|
368
|
+
const grant = await entryGrant(entryID, store, key.owner_did);
|
|
369
|
+
if (grant !== undefined && normalizeDID(grant.grantDeviceDID) !== normalizeDID(identity.id)) {
|
|
370
|
+
unpreservableGrant = true;
|
|
371
|
+
plaintext?.fill(0);
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
selected.push({
|
|
375
|
+
entryID,
|
|
376
|
+
plaintext,
|
|
377
|
+
contentHLC: chosen.contentHLC,
|
|
378
|
+
opHash: chosen.record.opHash,
|
|
379
|
+
grant
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
if (!loserAddsEntry && !loserAddsRecipient) return {
|
|
383
|
+
status: 'not-needed'
|
|
384
|
+
};
|
|
385
|
+
if (unpreservableGrant) return {
|
|
386
|
+
status: 'pending'
|
|
387
|
+
};
|
|
388
|
+
// Secret factors need the caller's input. Preserve the fork for a prompted retry
|
|
389
|
+
// instead of dropping a recovery wrap or changing its secret without consent.
|
|
390
|
+
if (wrappings.some((wrapping)=>wrapping.factors.some((factor)=>factor.kind !== 'did' && (!isSecretFactorKind(factor.kind) || inputs?.[factor.kind] == null)))) return {
|
|
391
|
+
status: 'pending'
|
|
392
|
+
};
|
|
393
|
+
const stable = JSON.stringify({
|
|
394
|
+
recipients: [
|
|
395
|
+
...recipients.keys()
|
|
396
|
+
].sort(),
|
|
397
|
+
entries: selected.map(({ entryID, contentHLC, opHash })=>({
|
|
398
|
+
entryID,
|
|
399
|
+
contentHLC,
|
|
400
|
+
opHash
|
|
401
|
+
})).sort((a, b)=>a.entryID.localeCompare(b.entryID))
|
|
402
|
+
});
|
|
403
|
+
const repairDigest = recordPayloadDigest(new TextEncoder().encode(stable));
|
|
404
|
+
if (repairDigest === null) throw new Error('Missing repair digest');
|
|
405
|
+
const newVersion = key.version + 1;
|
|
406
|
+
const suite = getSuite(key.suite);
|
|
407
|
+
const contentKey = suite.generateContentKey(runtime);
|
|
408
|
+
try {
|
|
409
|
+
const prepared = [];
|
|
410
|
+
for (const wrapping of [
|
|
411
|
+
...recipients.values(),
|
|
412
|
+
...unaddressedWrappings
|
|
413
|
+
]){
|
|
414
|
+
const wrappingID = runtime.getRandomID();
|
|
415
|
+
const wrapped = await suite.wrap({
|
|
416
|
+
runtime,
|
|
417
|
+
contentKey,
|
|
418
|
+
factors: factorsFromDescriptors(wrapping.factors, inputs),
|
|
419
|
+
wrappingID,
|
|
420
|
+
keyID,
|
|
421
|
+
keyVersion: newVersion
|
|
422
|
+
});
|
|
423
|
+
prepared.push({
|
|
424
|
+
wrappingID,
|
|
425
|
+
keyID,
|
|
426
|
+
keyVersion: newVersion,
|
|
427
|
+
factors: wrapped.descriptors,
|
|
428
|
+
iv: wrapped.iv,
|
|
429
|
+
wrappedKey: wrapped.wrappedKey,
|
|
430
|
+
recipientDID: wrapping.recipient_did
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
const unique = await keyUnique(keyID);
|
|
434
|
+
const keyHLC = await nextHLC();
|
|
435
|
+
const expectedBranches = branches.map((branch)=>branch.branch_id).sort();
|
|
436
|
+
await store.transaction(async (tx)=>{
|
|
437
|
+
await tx.lockKeyRow(keyID);
|
|
438
|
+
if ((await tx.getKey(keyID))?.version !== key.version) {
|
|
439
|
+
throw new CredentialKeyVersionMismatch(keyID, key.version, (await tx.getKey(keyID))?.version ?? -1);
|
|
440
|
+
}
|
|
441
|
+
if (JSON.stringify((await tx.listKeyBranches(keyID, key.version)).map((row)=>row.branch_id).sort()) !== JSON.stringify(expectedBranches)) {
|
|
442
|
+
throw new CredentialKeyVersionMismatch(keyID, key.version, -1);
|
|
443
|
+
}
|
|
444
|
+
const { branchID, branchRecordID } = await keyRecord({
|
|
445
|
+
key: {
|
|
446
|
+
keyID,
|
|
447
|
+
ownerDID: key.owner_did,
|
|
448
|
+
suite: key.suite,
|
|
449
|
+
version: newVersion,
|
|
450
|
+
state: key.state === 'retired' ? 'retired' : 'active'
|
|
451
|
+
},
|
|
452
|
+
keyUnique: unique,
|
|
453
|
+
hlc: keyHLC,
|
|
454
|
+
authority,
|
|
455
|
+
target: tx,
|
|
456
|
+
repairDigest
|
|
457
|
+
});
|
|
458
|
+
for (const wrapping of prepared){
|
|
459
|
+
await putWrappingRecord({
|
|
460
|
+
wrapping,
|
|
461
|
+
owner: key.owner_did,
|
|
462
|
+
branchID,
|
|
463
|
+
branchRecordID,
|
|
464
|
+
authority,
|
|
465
|
+
target: tx
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
for (const entry of selected){
|
|
469
|
+
if (entry.plaintext === null) {
|
|
470
|
+
await putRecord({
|
|
471
|
+
type: 'credential.entry',
|
|
472
|
+
id: entry.entryID,
|
|
473
|
+
owner: key.owner_did,
|
|
474
|
+
hlc: await nextHLC(),
|
|
475
|
+
payload: null,
|
|
476
|
+
meta: {
|
|
477
|
+
keyID,
|
|
478
|
+
keyVersion: newVersion,
|
|
479
|
+
contentHLC: entry.contentHLC,
|
|
480
|
+
branchID,
|
|
481
|
+
branchRecordID,
|
|
482
|
+
...entry.grant === undefined ? {} : {
|
|
483
|
+
grantDeviceDID: normalizeDID(entry.grant.grantDeviceDID),
|
|
484
|
+
grantName: entry.grant.grantName,
|
|
485
|
+
grantOwnerDID: normalizeDID(entry.grant.grantOwnerDID ?? key.owner_did)
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
target: tx,
|
|
489
|
+
authority
|
|
490
|
+
});
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
const encrypted = suite.encryptEntry({
|
|
494
|
+
runtime,
|
|
495
|
+
contentKey,
|
|
496
|
+
plaintext: entry.plaintext,
|
|
497
|
+
keyID,
|
|
498
|
+
keyVersion: newVersion,
|
|
499
|
+
entryID: entry.entryID
|
|
500
|
+
});
|
|
501
|
+
await putEntryRecord({
|
|
502
|
+
entry: {
|
|
503
|
+
entryID: entry.entryID,
|
|
504
|
+
keyID,
|
|
505
|
+
keyVersion: newVersion,
|
|
506
|
+
...encrypted,
|
|
507
|
+
hlc: await nextHLC()
|
|
508
|
+
},
|
|
509
|
+
owner: key.owner_did,
|
|
510
|
+
branchID,
|
|
511
|
+
contentHLC: entry.contentHLC,
|
|
512
|
+
target: tx,
|
|
513
|
+
authority,
|
|
514
|
+
grant: entry.grant
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
return {
|
|
519
|
+
status: 'rotated',
|
|
520
|
+
version: newVersion
|
|
521
|
+
};
|
|
522
|
+
} finally{
|
|
523
|
+
contentKey.fill(0);
|
|
524
|
+
}
|
|
525
|
+
} finally{
|
|
526
|
+
for (const entry of selected)entry.plaintext?.fill(0);
|
|
527
|
+
}
|
|
528
|
+
} finally{
|
|
529
|
+
for (const keyBytes of keys.values())keyBytes.fill(0);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return {
|
|
533
|
+
rotateKey,
|
|
534
|
+
repairFork
|
|
535
|
+
};
|
|
536
|
+
}
|