@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,291 @@
|
|
|
1
|
+
import { normalizeDID } from '@kokuin/token';
|
|
2
|
+
import { recordClaimDigest, recordPayloadDigest, signRecordOp, verifyRecordOp, verifySignedRecord } from '@kubun/records';
|
|
3
|
+
import { credentialRecordAdapter, decodeCredentialRecord, encodeCredentialRecordPayload } from '@kubun/store-credential';
|
|
4
|
+
import { CredentialNotAuthorized, CredentialNotFound } from './errors.js';
|
|
5
|
+
export function createRecordFunctions(ctx) {
|
|
6
|
+
const { params, store, identity, authority, nextHLC } = ctx;
|
|
7
|
+
const records = ()=>credentialRecordAdapter(store);
|
|
8
|
+
const selectedCopy = (decision)=>decision?.copies[0] ?? null;
|
|
9
|
+
async function storedKeyRecord(keyID) {
|
|
10
|
+
const key = await store.getKey(keyID);
|
|
11
|
+
if (key === undefined) return null;
|
|
12
|
+
return selectedCopy(await records().get({
|
|
13
|
+
type: 'credential.key',
|
|
14
|
+
owner: key.owner_did,
|
|
15
|
+
id: keyID
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
18
|
+
async function entryRecord(entryID, target = store, ownerDID) {
|
|
19
|
+
const entry = await target.getEntry(entryID, ownerDID);
|
|
20
|
+
const key = entry === undefined ? undefined : await target.getKey(entry.key_id, entry.owner_did);
|
|
21
|
+
if (key === undefined) return null;
|
|
22
|
+
return selectedCopy(await credentialRecordAdapter(target).get({
|
|
23
|
+
type: 'credential.entry',
|
|
24
|
+
owner: key.owner_did,
|
|
25
|
+
id: entryID
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
async function winningBranch(branches) {
|
|
29
|
+
let winner = null;
|
|
30
|
+
const verified = await Promise.all(branches.map(async (branch)=>({
|
|
31
|
+
branch,
|
|
32
|
+
op: await verifyRecordOp(branch.op_jwt)
|
|
33
|
+
})));
|
|
34
|
+
for (const { branch, op } of verified){
|
|
35
|
+
if (op === null || branch.branch_id !== `${op.claim.hlc}:${op.opHash}`) continue;
|
|
36
|
+
const candidate = {
|
|
37
|
+
branchID: branch.branch_id,
|
|
38
|
+
digest: recordClaimDigest(op.claim),
|
|
39
|
+
opHash: op.opHash
|
|
40
|
+
};
|
|
41
|
+
if (winner === null || candidate.digest > winner.digest || candidate.digest === winner.digest && candidate.opHash > winner.opHash) {
|
|
42
|
+
winner = candidate;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return winner?.branchID ?? null;
|
|
46
|
+
}
|
|
47
|
+
async function putRecord(args) {
|
|
48
|
+
const { type, id, owner, hlc, payload, meta } = args;
|
|
49
|
+
const target = args.target ?? store;
|
|
50
|
+
const recordAuthority = args.authority ?? authority;
|
|
51
|
+
if (params.removalRecovery === undefined && normalizeDID(owner).startsWith('did:kokuin:')) {
|
|
52
|
+
throw new CredentialNotAuthorized('administer', id);
|
|
53
|
+
}
|
|
54
|
+
const bytes = payload === null ? null : encodeCredentialRecordPayload(type, payload);
|
|
55
|
+
const { opJWT, opHash } = await signRecordOp(identity, {
|
|
56
|
+
type,
|
|
57
|
+
id,
|
|
58
|
+
owner: normalizeDID(owner),
|
|
59
|
+
hlc,
|
|
60
|
+
payloadDigest: recordPayloadDigest(bytes),
|
|
61
|
+
deleted: bytes === null,
|
|
62
|
+
meta: recordAuthority.delegationTokens?.length ? {
|
|
63
|
+
...meta,
|
|
64
|
+
delegationTokens: [
|
|
65
|
+
...recordAuthority.delegationTokens
|
|
66
|
+
]
|
|
67
|
+
} : meta
|
|
68
|
+
});
|
|
69
|
+
const record = {
|
|
70
|
+
type,
|
|
71
|
+
id,
|
|
72
|
+
owner: normalizeDID(owner),
|
|
73
|
+
hlc,
|
|
74
|
+
payload: bytes,
|
|
75
|
+
opJWT,
|
|
76
|
+
opHash
|
|
77
|
+
};
|
|
78
|
+
await target.putRecord(record);
|
|
79
|
+
return record;
|
|
80
|
+
}
|
|
81
|
+
async function keyRecord(args) {
|
|
82
|
+
const { key, keyUnique, hlc, repairDigest } = args;
|
|
83
|
+
const recordAuthority = args.authority ?? authority;
|
|
84
|
+
const target = args.target ?? store;
|
|
85
|
+
const record = await putRecord({
|
|
86
|
+
type: 'credential.key',
|
|
87
|
+
id: key.keyID,
|
|
88
|
+
owner: key.ownerDID,
|
|
89
|
+
hlc,
|
|
90
|
+
payload: {
|
|
91
|
+
suite: key.suite,
|
|
92
|
+
state: key.state
|
|
93
|
+
},
|
|
94
|
+
meta: {
|
|
95
|
+
keyID: key.keyID,
|
|
96
|
+
keyUnique,
|
|
97
|
+
version: key.version,
|
|
98
|
+
state: key.state
|
|
99
|
+
},
|
|
100
|
+
target,
|
|
101
|
+
authority: recordAuthority
|
|
102
|
+
});
|
|
103
|
+
const branchID = `${record.hlc}:${record.opHash}`;
|
|
104
|
+
const payload = {
|
|
105
|
+
authorDID: normalizeDID(identity.id),
|
|
106
|
+
hlc: record.hlc,
|
|
107
|
+
opJWT: record.opJWT,
|
|
108
|
+
opHash: record.opHash,
|
|
109
|
+
suite: key.suite
|
|
110
|
+
};
|
|
111
|
+
const branchIDRecord = recordPayloadDigest(encodeCredentialRecordPayload('credential.key-branch', payload));
|
|
112
|
+
if (branchIDRecord === null) throw new Error('Key branch payload has no digest');
|
|
113
|
+
await putRecord({
|
|
114
|
+
type: 'credential.key-branch',
|
|
115
|
+
id: branchIDRecord,
|
|
116
|
+
owner: key.ownerDID,
|
|
117
|
+
hlc,
|
|
118
|
+
payload,
|
|
119
|
+
meta: {
|
|
120
|
+
keyID: key.keyID,
|
|
121
|
+
keyVersion: key.version,
|
|
122
|
+
branchID,
|
|
123
|
+
...repairDigest === undefined ? {} : {
|
|
124
|
+
repairDigest
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
target,
|
|
128
|
+
authority: recordAuthority
|
|
129
|
+
});
|
|
130
|
+
return {
|
|
131
|
+
branchID,
|
|
132
|
+
branchRecordID: branchIDRecord
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
async function keyUnique(keyID) {
|
|
136
|
+
const stored = await storedKeyRecord(keyID);
|
|
137
|
+
const op = stored === null ? null : await verifySignedRecord(stored);
|
|
138
|
+
const unique = op?.claim.meta.keyUnique;
|
|
139
|
+
if (typeof unique !== 'string') throw new CredentialNotFound('Credential key record', keyID);
|
|
140
|
+
return unique;
|
|
141
|
+
}
|
|
142
|
+
async function branchRecordID(args) {
|
|
143
|
+
const { keyID, version, branchID } = args;
|
|
144
|
+
const target = args.target ?? store;
|
|
145
|
+
const branches = await credentialRecordAdapter(target).list([
|
|
146
|
+
'credential.key-branch'
|
|
147
|
+
]);
|
|
148
|
+
for (const decision of branches){
|
|
149
|
+
const branch = decision.copies[0];
|
|
150
|
+
if (branch === undefined) continue;
|
|
151
|
+
const op = await verifySignedRecord(branch);
|
|
152
|
+
if (op?.claim.meta.keyID === keyID && op.claim.meta.keyVersion === version && op.claim.meta.branchID === branchID) return branch.id;
|
|
153
|
+
}
|
|
154
|
+
throw new CredentialNotFound('Credential key branch record', branchID);
|
|
155
|
+
}
|
|
156
|
+
async function putWrappingRecord(args) {
|
|
157
|
+
const { wrapping, owner, branchID } = args;
|
|
158
|
+
const recordAuthority = args.authority ?? authority;
|
|
159
|
+
const target = args.target ?? store;
|
|
160
|
+
await putRecord({
|
|
161
|
+
type: 'credential.wrapping',
|
|
162
|
+
id: wrapping.wrappingID,
|
|
163
|
+
owner,
|
|
164
|
+
hlc: await nextHLC(),
|
|
165
|
+
payload: {
|
|
166
|
+
factors: wrapping.factors,
|
|
167
|
+
iv: wrapping.iv,
|
|
168
|
+
wrappedKey: wrapping.wrappedKey
|
|
169
|
+
},
|
|
170
|
+
meta: {
|
|
171
|
+
keyID: wrapping.keyID,
|
|
172
|
+
keyVersion: wrapping.keyVersion,
|
|
173
|
+
branchID,
|
|
174
|
+
branchRecordID: args.branchRecordID ?? await branchRecordID({
|
|
175
|
+
keyID: wrapping.keyID,
|
|
176
|
+
version: wrapping.keyVersion,
|
|
177
|
+
branchID,
|
|
178
|
+
target
|
|
179
|
+
}),
|
|
180
|
+
recipient: wrapping.recipientDID ?? null
|
|
181
|
+
},
|
|
182
|
+
target,
|
|
183
|
+
authority: recordAuthority
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
async function putEntryRecord(args) {
|
|
187
|
+
const { entry, owner, branchID, grant } = args;
|
|
188
|
+
const contentHLC = args.contentHLC ?? entry.hlc;
|
|
189
|
+
const target = args.target ?? store;
|
|
190
|
+
const recordAuthority = args.authority ?? authority;
|
|
191
|
+
const branchIDRecord = await branchRecordID({
|
|
192
|
+
keyID: entry.keyID,
|
|
193
|
+
version: entry.keyVersion,
|
|
194
|
+
branchID,
|
|
195
|
+
target
|
|
196
|
+
});
|
|
197
|
+
await putRecord({
|
|
198
|
+
type: 'credential.entry',
|
|
199
|
+
id: entry.entryID,
|
|
200
|
+
owner,
|
|
201
|
+
hlc: entry.hlc,
|
|
202
|
+
payload: {
|
|
203
|
+
iv: entry.iv,
|
|
204
|
+
ciphertext: entry.ciphertext
|
|
205
|
+
},
|
|
206
|
+
meta: {
|
|
207
|
+
keyID: entry.keyID,
|
|
208
|
+
keyVersion: entry.keyVersion,
|
|
209
|
+
contentHLC,
|
|
210
|
+
branchID,
|
|
211
|
+
branchRecordID: branchIDRecord,
|
|
212
|
+
...grant === undefined ? {} : {
|
|
213
|
+
grantDeviceDID: normalizeDID(grant.grantDeviceDID),
|
|
214
|
+
grantName: grant.grantName,
|
|
215
|
+
grantOwnerDID: normalizeDID(grant.grantOwnerDID ?? owner)
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
target,
|
|
219
|
+
authority: recordAuthority
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
async function entryGrant(entryID, target = store, ownerDID) {
|
|
223
|
+
const record = await entryRecord(entryID, target, ownerDID);
|
|
224
|
+
const op = record === null ? null : await verifySignedRecord(record);
|
|
225
|
+
const device = op?.claim.meta.grantDeviceDID;
|
|
226
|
+
const name = op?.claim.meta.grantName;
|
|
227
|
+
const accountOwner = op?.claim.meta.grantOwnerDID;
|
|
228
|
+
return typeof device === 'string' && typeof name === 'string' ? {
|
|
229
|
+
grantDeviceDID: device,
|
|
230
|
+
grantName: name,
|
|
231
|
+
grantOwnerDID: typeof accountOwner === 'string' ? accountOwner : op?.claim.owner
|
|
232
|
+
} : undefined;
|
|
233
|
+
}
|
|
234
|
+
async function writableGrant(entryID, ownerDID) {
|
|
235
|
+
const grant = await entryGrant(entryID, store, ownerDID);
|
|
236
|
+
if (grant !== undefined && normalizeDID(grant.grantDeviceDID) !== normalizeDID(identity.id)) {
|
|
237
|
+
throw new CredentialNotAuthorized('administer', entryID);
|
|
238
|
+
}
|
|
239
|
+
return grant;
|
|
240
|
+
}
|
|
241
|
+
async function suiteForVersion(keyID, version, currentSuite) {
|
|
242
|
+
const current = await store.getKey(keyID);
|
|
243
|
+
if (current?.version === version) return currentSuite;
|
|
244
|
+
const branches = await store.listKeyBranches(keyID, version);
|
|
245
|
+
const winner = await winningBranch(branches);
|
|
246
|
+
for (const decision of (await records().list([
|
|
247
|
+
'credential.key-branch'
|
|
248
|
+
]))){
|
|
249
|
+
const record = decision.copies[0];
|
|
250
|
+
if (record === undefined) continue;
|
|
251
|
+
const op = await verifySignedRecord(record);
|
|
252
|
+
if (op?.claim.meta.keyID !== keyID || op.claim.meta.keyVersion !== version || op.claim.meta.branchID !== winner) continue;
|
|
253
|
+
const decoded = decodeCredentialRecord(record, op.claim.meta);
|
|
254
|
+
if (decoded.type === 'credential.key-branch') return decoded.payload.suite;
|
|
255
|
+
}
|
|
256
|
+
throw new CredentialNotFound('Historical credential suite', `${keyID}@${version}`);
|
|
257
|
+
}
|
|
258
|
+
async function branchProofs(keyID, version) {
|
|
259
|
+
const proofs = new Map();
|
|
260
|
+
for (const decision of (await records().list([
|
|
261
|
+
'credential.key-branch'
|
|
262
|
+
]))){
|
|
263
|
+
const record = decision.copies[0];
|
|
264
|
+
if (record === undefined) continue;
|
|
265
|
+
const op = await verifySignedRecord(record);
|
|
266
|
+
if (op?.claim.meta.keyID !== keyID || op.claim.meta.keyVersion !== version) continue;
|
|
267
|
+
const decoded = decodeCredentialRecord(record, op.claim.meta);
|
|
268
|
+
if (decoded.type !== 'credential.key-branch') continue;
|
|
269
|
+
proofs.set(op.claim.meta.branchID, {
|
|
270
|
+
suite: decoded.payload.suite,
|
|
271
|
+
repairDigest: op.claim.meta.repairDigest
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
return proofs;
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
storedKeyRecord,
|
|
278
|
+
entryRecord,
|
|
279
|
+
winningBranch,
|
|
280
|
+
putRecord,
|
|
281
|
+
keyRecord,
|
|
282
|
+
keyUnique,
|
|
283
|
+
branchRecordID,
|
|
284
|
+
putWrappingRecord,
|
|
285
|
+
putEntryRecord,
|
|
286
|
+
entryGrant,
|
|
287
|
+
writableGrant,
|
|
288
|
+
branchProofs,
|
|
289
|
+
suiteForVersion
|
|
290
|
+
};
|
|
291
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ManagerContext, type ManagerInternals } from './manager-context.js';
|
|
2
|
+
import type { RecoverRemovedDevicesParams } from './manager-types.js';
|
|
3
|
+
export type RemovalRecoveryFunctions = {
|
|
4
|
+
recoverRemovedDevices: (options: RecoverRemovedDevicesParams) => Promise<number>;
|
|
5
|
+
checkRemovalRecovery: () => Promise<void>;
|
|
6
|
+
};
|
|
7
|
+
export declare function createRemovalRecoveryFunctions(ctx: ManagerContext, internals: ManagerInternals): RemovalRecoveryFunctions;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { normalizeDID } from '@kokuin/token';
|
|
2
|
+
import { HLC } from '@kubun/hlc';
|
|
3
|
+
import { recordPayloadDigest, signRecordOp, verifySignedRecord } from '@kubun/records';
|
|
4
|
+
import { credentialRecordAdapter, decodeCredentialRecord } from '@kubun/store-credential';
|
|
5
|
+
import { CredentialKeyVersionMismatch, CredentialNotFound } from './errors.js';
|
|
6
|
+
import { wrappingNamesRemovedDevice } from './manager-context.js';
|
|
7
|
+
import { getSuite } from './suite.js';
|
|
8
|
+
export function createRemovalRecoveryFunctions(ctx, internals) {
|
|
9
|
+
const { store, identity, runtime, hlc, nextHLC, params, state } = ctx;
|
|
10
|
+
const { storedKeyRecord, winningBranch, branchRecordID, putEntryRecord, suiteForVersion } = internals.records;
|
|
11
|
+
const { openKey } = internals.open;
|
|
12
|
+
const records = ()=>credentialRecordAdapter(store);
|
|
13
|
+
async function recoverRemovedDevices(options) {
|
|
14
|
+
if (params.removalRecovery === undefined) {
|
|
15
|
+
// The explicit call supplied the missing recovery context. Keep that context scoped
|
|
16
|
+
// to this recovery, rather than letting unrelated writes bypass the constructor guard.
|
|
17
|
+
return internals.createManager({
|
|
18
|
+
...params,
|
|
19
|
+
removalRecovery: {
|
|
20
|
+
...options,
|
|
21
|
+
headVersion: async (ownerDID)=>JSON.stringify([
|
|
22
|
+
...await options.removedDevices?.(ownerDID) ?? new Set()
|
|
23
|
+
].sort())
|
|
24
|
+
}
|
|
25
|
+
}).recoverRemovedDevices(options);
|
|
26
|
+
}
|
|
27
|
+
const controller = await options.resolveController(identity.id);
|
|
28
|
+
if (controller === null) return 0;
|
|
29
|
+
const owner = normalizeDID(controller);
|
|
30
|
+
const self = normalizeDID(identity.id);
|
|
31
|
+
const recordTypes = [
|
|
32
|
+
'credential.key',
|
|
33
|
+
'credential.wrapping',
|
|
34
|
+
'credential.entry',
|
|
35
|
+
'credential.revocation'
|
|
36
|
+
];
|
|
37
|
+
const removed = new Set();
|
|
38
|
+
for (const decision of (await records().list(recordTypes))){
|
|
39
|
+
if (decision.key.owner !== owner) continue;
|
|
40
|
+
for (const record of decision.copies){
|
|
41
|
+
const op = await verifySignedRecord(record, options.methods);
|
|
42
|
+
if (op === null || op.issuer === owner || op.issuer === self) continue;
|
|
43
|
+
if (await options.resolveController(op.issuer) !== owner) removed.add(op.issuer);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (removed.size === 0) return 0;
|
|
47
|
+
for (const did of await options.removedDevices?.(owner) ?? []){
|
|
48
|
+
removed.add(normalizeDID(did));
|
|
49
|
+
}
|
|
50
|
+
let written = 0;
|
|
51
|
+
for (const key of (await store.listKeys(owner))){
|
|
52
|
+
const record = await storedKeyRecord(key.key_id);
|
|
53
|
+
const op = record === null ? null : await verifySignedRecord(record, options.methods);
|
|
54
|
+
if (op === null || !removed.has(op.issuer) || op.claim.deleted) continue;
|
|
55
|
+
try {
|
|
56
|
+
await internals.rotate.rotateKey({
|
|
57
|
+
keyID: key.key_id,
|
|
58
|
+
rotateParams: {
|
|
59
|
+
methods: options.methods
|
|
60
|
+
},
|
|
61
|
+
excludedRecipients: removed
|
|
62
|
+
});
|
|
63
|
+
written += 1;
|
|
64
|
+
} catch (error) {
|
|
65
|
+
// A second local recovery may have rotated the same key after our scan.
|
|
66
|
+
// Its transaction committed the complete rotation; reissue below still
|
|
67
|
+
// picks up any former-device records it left behind.
|
|
68
|
+
if (!(error instanceof CredentialKeyVersionMismatch) || ((await store.getKey(key.key_id))?.version ?? 0) <= key.version) {
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Re-read after rotation: it may already have replaced some former-device entries.
|
|
74
|
+
for (const decision of (await records().list(recordTypes))){
|
|
75
|
+
if (decision.key.owner !== owner || decision.key.type === 'credential.key') continue;
|
|
76
|
+
let record;
|
|
77
|
+
let op = null;
|
|
78
|
+
for (const copy of decision.copies){
|
|
79
|
+
const verified = await verifySignedRecord(copy, options.methods);
|
|
80
|
+
if (verified !== null && removed.has(verified.issuer)) {
|
|
81
|
+
record = copy;
|
|
82
|
+
op = verified;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (record === undefined || op === null) continue;
|
|
87
|
+
if (record.type === 'credential.entry') {
|
|
88
|
+
const grantDevice = op.claim.meta.grantDeviceDID;
|
|
89
|
+
if (typeof grantDevice === 'string' && removed.has(normalizeDID(grantDevice))) continue;
|
|
90
|
+
}
|
|
91
|
+
if (record.type === 'credential.wrapping') {
|
|
92
|
+
if (record.payload !== null) {
|
|
93
|
+
const decoded = decodeCredentialRecord(record, op.claim.meta);
|
|
94
|
+
if (decoded.type === 'credential.wrapping' && wrappingNamesRemovedDevice({
|
|
95
|
+
recipient_did: op.claim.meta.recipient ?? null,
|
|
96
|
+
factors: decoded.payload.factors
|
|
97
|
+
}, removed)) continue;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const oldStamp = HLC.tryParse(record.hlc);
|
|
101
|
+
if (oldStamp !== null) hlc.receive(oldStamp);
|
|
102
|
+
const stamp = await nextHLC();
|
|
103
|
+
let meta = op.claim.meta;
|
|
104
|
+
if (record.type === 'credential.entry') {
|
|
105
|
+
const keyID = op.claim.meta.keyID;
|
|
106
|
+
const oldVersion = op.claim.meta.keyVersion;
|
|
107
|
+
if (typeof keyID !== 'string' || typeof oldVersion !== 'number') continue;
|
|
108
|
+
const key = await store.getKey(keyID);
|
|
109
|
+
if (key === undefined) continue;
|
|
110
|
+
meta = {
|
|
111
|
+
...meta,
|
|
112
|
+
contentHLC: stamp,
|
|
113
|
+
keyVersion: key.version
|
|
114
|
+
};
|
|
115
|
+
if (record.payload === null && oldVersion !== key.version) {
|
|
116
|
+
const branches = await store.listKeyBranches(keyID, key.version);
|
|
117
|
+
if (branches.length === 0) {
|
|
118
|
+
delete meta.branchID;
|
|
119
|
+
delete meta.branchRecordID;
|
|
120
|
+
} else {
|
|
121
|
+
const currentBranchID = await winningBranch(branches);
|
|
122
|
+
if (currentBranchID === null) throw new CredentialNotFound('Credential key branch', keyID);
|
|
123
|
+
meta.branchID = currentBranchID;
|
|
124
|
+
meta.branchRecordID = await branchRecordID({
|
|
125
|
+
keyID,
|
|
126
|
+
version: key.version,
|
|
127
|
+
branchID: currentBranchID
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (record.payload !== null && oldVersion !== key.version) {
|
|
132
|
+
const entry = await store.getEntry(record.id, record.owner);
|
|
133
|
+
if (entry === undefined) continue;
|
|
134
|
+
const source = await openKey(keyID, undefined, oldVersion);
|
|
135
|
+
let target;
|
|
136
|
+
let plaintext;
|
|
137
|
+
try {
|
|
138
|
+
target = await openKey(keyID);
|
|
139
|
+
const sourceSuite = getSuite(await suiteForVersion(keyID, oldVersion, key.suite));
|
|
140
|
+
plaintext = sourceSuite.decryptEntry({
|
|
141
|
+
contentKey: source.contentKey,
|
|
142
|
+
iv: new Uint8Array(entry.iv),
|
|
143
|
+
ciphertext: new Uint8Array(entry.ciphertext),
|
|
144
|
+
keyID,
|
|
145
|
+
keyVersion: oldVersion,
|
|
146
|
+
entryID: record.id
|
|
147
|
+
});
|
|
148
|
+
const encrypted = getSuite(key.suite).encryptEntry({
|
|
149
|
+
runtime,
|
|
150
|
+
contentKey: target.contentKey,
|
|
151
|
+
plaintext,
|
|
152
|
+
keyID,
|
|
153
|
+
keyVersion: key.version,
|
|
154
|
+
entryID: record.id
|
|
155
|
+
});
|
|
156
|
+
await putEntryRecord({
|
|
157
|
+
entry: {
|
|
158
|
+
entryID: record.id,
|
|
159
|
+
keyID,
|
|
160
|
+
keyVersion: key.version,
|
|
161
|
+
...encrypted,
|
|
162
|
+
hlc: stamp
|
|
163
|
+
},
|
|
164
|
+
owner: record.owner,
|
|
165
|
+
branchID: target.branchID,
|
|
166
|
+
contentHLC: stamp
|
|
167
|
+
});
|
|
168
|
+
} finally{
|
|
169
|
+
plaintext?.fill(0);
|
|
170
|
+
source.contentKey.fill(0);
|
|
171
|
+
target?.contentKey.fill(0);
|
|
172
|
+
}
|
|
173
|
+
written += 1;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const { opJWT, opHash } = await signRecordOp(identity, {
|
|
178
|
+
type: record.type,
|
|
179
|
+
id: record.id,
|
|
180
|
+
owner: record.owner,
|
|
181
|
+
hlc: stamp,
|
|
182
|
+
payloadDigest: recordPayloadDigest(record.payload),
|
|
183
|
+
deleted: record.payload === null,
|
|
184
|
+
meta
|
|
185
|
+
});
|
|
186
|
+
await store.putRecord({
|
|
187
|
+
...record,
|
|
188
|
+
hlc: stamp,
|
|
189
|
+
opJWT,
|
|
190
|
+
opHash
|
|
191
|
+
}, options.methods);
|
|
192
|
+
written += 1;
|
|
193
|
+
}
|
|
194
|
+
return written;
|
|
195
|
+
}
|
|
196
|
+
async function checkRemovalRecovery() {
|
|
197
|
+
const recovery = params.removalRecovery;
|
|
198
|
+
if (recovery === undefined) return;
|
|
199
|
+
if ('ensure' in recovery) return await recovery.ensure();
|
|
200
|
+
const controller = await recovery.resolveController(identity.id);
|
|
201
|
+
if (controller === null) return;
|
|
202
|
+
const version = await recovery.headVersion(normalizeDID(controller));
|
|
203
|
+
if (version === state.completedRecoveryHead) return;
|
|
204
|
+
await recoverRemovedDevices(recovery);
|
|
205
|
+
state.completedRecoveryHead = version;
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
recoverRemovedDevices,
|
|
209
|
+
checkRemovalRecovery
|
|
210
|
+
};
|
|
211
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ForkRepairResult } from './fork-repair.js';
|
|
2
|
+
import { type ManagerContext, type ManagerInternals } from './manager-context.js';
|
|
3
|
+
import type { RotateKeyParams } from './manager-types.js';
|
|
4
|
+
import { type FactorInputs } from './suite.js';
|
|
5
|
+
export type RotateKeyRunParams = {
|
|
6
|
+
keyID: string;
|
|
7
|
+
rotateParams?: RotateKeyParams;
|
|
8
|
+
revokedDID?: string;
|
|
9
|
+
excludedRecipients?: ReadonlySet<string>;
|
|
10
|
+
};
|
|
11
|
+
export type RotateFunctions = {
|
|
12
|
+
rotateKey: (params: RotateKeyRunParams) => Promise<number>;
|
|
13
|
+
repairFork: (keyID: string, inputs?: FactorInputs) => Promise<ForkRepairResult>;
|
|
14
|
+
};
|
|
15
|
+
export declare function createRotateFunctions(ctx: ManagerContext, internals: ManagerInternals): RotateFunctions;
|