@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
package/lib/op.js
DELETED
|
@@ -1,304 +0,0 @@
|
|
|
1
|
-
import { isVerifiedToken, normalizeDID, stringifyToken, verifyToken } from '@kokuin/token';
|
|
2
|
-
import { toB64U } from '@sozai/codec';
|
|
3
|
-
import { credentialEntryDigest, credentialKeyDigest, credentialWrappingDigest, digestParts } from './digest.js';
|
|
4
|
-
/** The digest of the bytes a row holds, per subject kind. */ export function credentialRowDigest(subject) {
|
|
5
|
-
switch(subject.kind){
|
|
6
|
-
case 'key':
|
|
7
|
-
return credentialKeyDigest({
|
|
8
|
-
ownerDID: subject.row.owner_did,
|
|
9
|
-
suite: subject.row.suite,
|
|
10
|
-
version: subject.row.version,
|
|
11
|
-
state: subject.row.state
|
|
12
|
-
});
|
|
13
|
-
case 'wrapping':
|
|
14
|
-
return credentialWrappingDigest({
|
|
15
|
-
wrappingID: subject.row.wrapping_id,
|
|
16
|
-
recipientDID: subject.row.recipient_did,
|
|
17
|
-
iv: toB64U(new Uint8Array(subject.row.iv)),
|
|
18
|
-
wrappedKey: toB64U(new Uint8Array(subject.row.wrapped_key)),
|
|
19
|
-
factors: subject.row.factors
|
|
20
|
-
});
|
|
21
|
-
case 'entry':
|
|
22
|
-
return credentialEntryDigest({
|
|
23
|
-
entryID: subject.row.entry_id,
|
|
24
|
-
iv: toB64U(new Uint8Array(subject.row.iv)),
|
|
25
|
-
ciphertext: toB64U(new Uint8Array(subject.row.ciphertext)),
|
|
26
|
-
hlc: subject.row.hlc
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
/** The digest of the bytes a row is about to be written with, per subject kind. */ export function credentialPutDigest(subject) {
|
|
31
|
-
switch(subject.kind){
|
|
32
|
-
case 'key':
|
|
33
|
-
return credentialKeyDigest({
|
|
34
|
-
ownerDID: subject.params.ownerDID,
|
|
35
|
-
suite: subject.params.suite,
|
|
36
|
-
version: subject.params.version,
|
|
37
|
-
state: subject.params.state
|
|
38
|
-
});
|
|
39
|
-
case 'wrapping':
|
|
40
|
-
return credentialWrappingDigest({
|
|
41
|
-
wrappingID: subject.params.wrappingID,
|
|
42
|
-
// An omitted recipient and an explicit null are the same wrapping, as
|
|
43
|
-
// the signer folds them.
|
|
44
|
-
recipientDID: subject.params.recipientDID ?? null,
|
|
45
|
-
iv: toB64U(subject.params.iv),
|
|
46
|
-
wrappedKey: toB64U(subject.params.wrappedKey),
|
|
47
|
-
factors: subject.params.factors
|
|
48
|
-
});
|
|
49
|
-
case 'entry':
|
|
50
|
-
return credentialEntryDigest({
|
|
51
|
-
entryID: subject.params.entryID,
|
|
52
|
-
iv: toB64U(subject.params.iv),
|
|
53
|
-
ciphertext: toB64U(subject.params.ciphertext),
|
|
54
|
-
hlc: subject.params.hlc
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Whether a verified claim speaks for exactly these bytes under exactly this
|
|
60
|
-
* identity.
|
|
61
|
-
*
|
|
62
|
-
* The identifiers are checked along with the digest: a claim over another row's
|
|
63
|
-
* content would otherwise authenticate this one as soon as the two held the same
|
|
64
|
-
* bytes, which two wrappings of the same key routinely do not but two
|
|
65
|
-
* empty-state keys would. `keyID` matters beyond that — it is the column the
|
|
66
|
-
* sync lane buckets a vault by, so an op filed under another key's id would
|
|
67
|
-
* travel with the wrong set.
|
|
68
|
-
*
|
|
69
|
-
* `stamp` is the row's own `hlc` where the row has one, which is entries only.
|
|
70
|
-
* It has to equal the claim's: the op row keeps the claim's stamp and the entry
|
|
71
|
-
* row keeps its own, and the two disagreeing would order a row differently from
|
|
72
|
-
* the op that speaks for it.
|
|
73
|
-
*/ function matchesClaim(claim, subject, contentDigest) {
|
|
74
|
-
return claim.subjectKind === subject.kind && claim.subjectID === subject.subjectID && claim.keyID === subject.keyID && claim.keyVersion === subject.keyVersion && (subject.stamp == null || claim.hlc === subject.stamp) && !claim.deleted && claim.contentDigest === contentDigest;
|
|
75
|
-
}
|
|
76
|
-
/** Whether a verified claim speaks for a stored row. */ export function credentialOpMatchesRow(claim, subject) {
|
|
77
|
-
const identity = subject.kind === 'key' ? {
|
|
78
|
-
kind: 'key',
|
|
79
|
-
subjectID: subject.row.key_id,
|
|
80
|
-
keyID: subject.row.key_id,
|
|
81
|
-
keyVersion: subject.row.version
|
|
82
|
-
} : subject.kind === 'wrapping' ? {
|
|
83
|
-
kind: 'wrapping',
|
|
84
|
-
subjectID: subject.row.wrapping_id,
|
|
85
|
-
keyID: subject.row.key_id,
|
|
86
|
-
keyVersion: subject.row.key_version
|
|
87
|
-
} : {
|
|
88
|
-
kind: 'entry',
|
|
89
|
-
subjectID: subject.row.entry_id,
|
|
90
|
-
keyID: subject.row.key_id,
|
|
91
|
-
keyVersion: subject.row.key_version,
|
|
92
|
-
stamp: subject.row.hlc
|
|
93
|
-
};
|
|
94
|
-
return matchesClaim(claim, identity, credentialRowDigest(subject));
|
|
95
|
-
}
|
|
96
|
-
/** Whether a verified claim speaks for a row about to be written. */ export function credentialOpMatchesPut(claim, subject) {
|
|
97
|
-
const identity = subject.kind === 'key' ? {
|
|
98
|
-
kind: 'key',
|
|
99
|
-
subjectID: subject.params.keyID,
|
|
100
|
-
keyID: subject.params.keyID,
|
|
101
|
-
keyVersion: subject.params.version
|
|
102
|
-
} : subject.kind === 'wrapping' ? {
|
|
103
|
-
kind: 'wrapping',
|
|
104
|
-
subjectID: subject.params.wrappingID,
|
|
105
|
-
keyID: subject.params.keyID,
|
|
106
|
-
keyVersion: subject.params.keyVersion
|
|
107
|
-
} : {
|
|
108
|
-
kind: 'entry',
|
|
109
|
-
subjectID: subject.params.entryID,
|
|
110
|
-
keyID: subject.params.keyID,
|
|
111
|
-
keyVersion: subject.params.keyVersion,
|
|
112
|
-
stamp: subject.params.hlc
|
|
113
|
-
};
|
|
114
|
-
return matchesClaim(claim, identity, credentialPutDigest(subject));
|
|
115
|
-
}
|
|
116
|
-
async function signOp(identity, claim) {
|
|
117
|
-
const signed = await identity.signToken({
|
|
118
|
-
...claim
|
|
119
|
-
}, {
|
|
120
|
-
embedLongForm: true
|
|
121
|
-
});
|
|
122
|
-
const opJWT = stringifyToken(signed);
|
|
123
|
-
return {
|
|
124
|
-
keyID: claim.keyID,
|
|
125
|
-
// Normalized, unlike a wrapping's recipient: this one is compared against a
|
|
126
|
-
// key's owner and against a verified issuer, never resolved to a document.
|
|
127
|
-
authorDID: normalizeDID(identity.id),
|
|
128
|
-
hlc: claim.hlc,
|
|
129
|
-
opJWT,
|
|
130
|
-
// The token addresses itself, the way the graph mutation log addresses a
|
|
131
|
-
// mutation by its hash.
|
|
132
|
-
opHash: digestParts([
|
|
133
|
-
opJWT
|
|
134
|
-
]),
|
|
135
|
-
// A wrapping or entry mirrors the branch it signed; anything without one
|
|
136
|
-
// gets the empty label. A key-op overrides this from its own `opHash`.
|
|
137
|
-
branchID: claim.branchID ?? ''
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
export function createCredentialOpSigner(identity) {
|
|
141
|
-
async function tombstone(subject, hlc) {
|
|
142
|
-
return await signOp(identity, {
|
|
143
|
-
subjectKind: subject.subjectKind,
|
|
144
|
-
subjectID: subject.subjectID,
|
|
145
|
-
keyID: subject.keyID,
|
|
146
|
-
keyVersion: subject.keyVersion,
|
|
147
|
-
hlc,
|
|
148
|
-
contentDigest: null,
|
|
149
|
-
deleted: true
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
return {
|
|
153
|
-
async key (params, hlc) {
|
|
154
|
-
const input = await signOp(identity, {
|
|
155
|
-
subjectKind: 'key',
|
|
156
|
-
subjectID: params.keyID,
|
|
157
|
-
keyID: params.keyID,
|
|
158
|
-
keyVersion: params.version,
|
|
159
|
-
hlc,
|
|
160
|
-
contentDigest: credentialKeyDigest({
|
|
161
|
-
ownerDID: params.ownerDID,
|
|
162
|
-
suite: params.suite,
|
|
163
|
-
version: params.version,
|
|
164
|
-
state: params.state
|
|
165
|
-
}),
|
|
166
|
-
deleted: false
|
|
167
|
-
});
|
|
168
|
-
// The key-op labels its own branch. It cannot claim this in the signed
|
|
169
|
-
// token — the label folds `opHash`, which the signature produces — so it
|
|
170
|
-
// rides on the input and a receiver recomputes it from the op's `hlc` and
|
|
171
|
-
// `opHash`, which is what authenticates it.
|
|
172
|
-
return {
|
|
173
|
-
...input,
|
|
174
|
-
branchID: `${hlc}:${input.opHash}`
|
|
175
|
-
};
|
|
176
|
-
},
|
|
177
|
-
async wrapping (params, hlc, branchID) {
|
|
178
|
-
return await signOp(identity, {
|
|
179
|
-
subjectKind: 'wrapping',
|
|
180
|
-
subjectID: params.wrappingID,
|
|
181
|
-
keyID: params.keyID,
|
|
182
|
-
keyVersion: params.keyVersion,
|
|
183
|
-
hlc,
|
|
184
|
-
contentDigest: credentialWrappingDigest({
|
|
185
|
-
wrappingID: params.wrappingID,
|
|
186
|
-
// An omitted recipient and an explicit null are the same wrapping, and
|
|
187
|
-
// the stored row keeps only one of the two spellings.
|
|
188
|
-
recipientDID: params.recipientDID ?? null,
|
|
189
|
-
iv: toB64U(params.iv),
|
|
190
|
-
wrappedKey: toB64U(params.wrappedKey),
|
|
191
|
-
factors: params.factors
|
|
192
|
-
}),
|
|
193
|
-
deleted: false,
|
|
194
|
-
branchID
|
|
195
|
-
});
|
|
196
|
-
},
|
|
197
|
-
async entry (params, branchID) {
|
|
198
|
-
return await signOp(identity, {
|
|
199
|
-
subjectKind: 'entry',
|
|
200
|
-
subjectID: params.entryID,
|
|
201
|
-
keyID: params.keyID,
|
|
202
|
-
keyVersion: params.keyVersion,
|
|
203
|
-
hlc: params.hlc,
|
|
204
|
-
contentDigest: credentialEntryDigest({
|
|
205
|
-
entryID: params.entryID,
|
|
206
|
-
iv: toB64U(params.iv),
|
|
207
|
-
ciphertext: toB64U(params.ciphertext),
|
|
208
|
-
hlc: params.hlc
|
|
209
|
-
}),
|
|
210
|
-
deleted: false,
|
|
211
|
-
branchID
|
|
212
|
-
});
|
|
213
|
-
},
|
|
214
|
-
tombstone,
|
|
215
|
-
async wrappingTombstone (subject, hlc) {
|
|
216
|
-
return {
|
|
217
|
-
...await tombstone(subject, hlc),
|
|
218
|
-
subjectID: subject.subjectID
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
function isSubjectKind(value) {
|
|
224
|
-
return value === 'key' || value === 'wrapping' || value === 'entry';
|
|
225
|
-
}
|
|
226
|
-
/**
|
|
227
|
-
* Split a `branchID` into its `hlc` and `opHash` halves.
|
|
228
|
-
*
|
|
229
|
-
* The label is `${hlc}:${opHash}`, and both halves — an HLC and a DID nodeID —
|
|
230
|
-
* carry their own colons, but `opHash` is base64url and carries none, so the
|
|
231
|
-
* LAST colon is the boundary however many precede it.
|
|
232
|
-
*/ function splitBranchID(branchID) {
|
|
233
|
-
const boundary = branchID.lastIndexOf(':');
|
|
234
|
-
return boundary < 0 ? {
|
|
235
|
-
hlc: branchID,
|
|
236
|
-
opHash: ''
|
|
237
|
-
} : {
|
|
238
|
-
hlc: branchID.slice(0, boundary),
|
|
239
|
-
opHash: branchID.slice(boundary + 1)
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
/**
|
|
243
|
-
* Order two branch labels, keyless and total.
|
|
244
|
-
*
|
|
245
|
-
* The `hlc` halves are compared as strings: the serialized HLC form is
|
|
246
|
-
* lexically total-ordered by construction (fixed-width ISO wall time, then a
|
|
247
|
-
* zero-padded counter, then the nodeID), the same property the store's
|
|
248
|
-
* tombstone check leans on. Two concurrent rotations neither dominate, so the
|
|
249
|
-
* `opHash` breaks the tie — also lexical, and distinct per op, which is what
|
|
250
|
-
* makes the order total and identical on every device.
|
|
251
|
-
*/ export function compareBranchIDs(a, b) {
|
|
252
|
-
const left = splitBranchID(a);
|
|
253
|
-
const right = splitBranchID(b);
|
|
254
|
-
if (left.hlc !== right.hlc) {
|
|
255
|
-
return left.hlc < right.hlc ? -1 : 1;
|
|
256
|
-
}
|
|
257
|
-
return left.opHash < right.opHash ? -1 : left.opHash > right.opHash ? 1 : 0;
|
|
258
|
-
}
|
|
259
|
-
/**
|
|
260
|
-
* The single branch a keyless party names as the winner: the one whose
|
|
261
|
-
* introducing key-op dominates by HLC, `opHash` breaking genuine concurrency.
|
|
262
|
-
* Order-independent — every device folds the same set to the same label.
|
|
263
|
-
*/ export function selectWinningBranch(branchIDs) {
|
|
264
|
-
if (branchIDs.length === 0) {
|
|
265
|
-
throw new RangeError('selectWinningBranch requires at least one branchID');
|
|
266
|
-
}
|
|
267
|
-
return branchIDs.reduce((winner, candidate)=>compareBranchIDs(candidate, winner) > 0 ? candidate : winner);
|
|
268
|
-
}
|
|
269
|
-
/**
|
|
270
|
-
* Verify an op and extract its claim. Returns `null` (never throws) when the
|
|
271
|
-
* token is unparseable, unsigned (`alg: 'none'`), or structurally malformed — an
|
|
272
|
-
* attacker cannot forge an `iss` this way.
|
|
273
|
-
*/ export async function verifyCredentialOp(token) {
|
|
274
|
-
let verified;
|
|
275
|
-
try {
|
|
276
|
-
verified = await verifyToken(token);
|
|
277
|
-
} catch {
|
|
278
|
-
return null;
|
|
279
|
-
}
|
|
280
|
-
if (!isVerifiedToken(verified)) {
|
|
281
|
-
return null;
|
|
282
|
-
}
|
|
283
|
-
const { iss, subjectKind, subjectID, keyID, keyVersion, hlc, contentDigest, deleted, branchID } = verified.payload;
|
|
284
|
-
if (!isSubjectKind(subjectKind) || typeof subjectID !== 'string' || typeof keyID !== 'string' || typeof keyVersion !== 'number' || typeof hlc !== 'string' || typeof deleted !== 'boolean' || // A live op with no digest would authenticate any bytes at all, so the two
|
|
285
|
-
// fields are checked against each other rather than one at a time.
|
|
286
|
-
(deleted ? contentDigest !== null : typeof contentDigest !== 'string') || // Optional — a key-op omits it — but a present one must be the signed string
|
|
287
|
-
// a receiver reads as the row's branch label.
|
|
288
|
-
branchID != null && typeof branchID !== 'string') {
|
|
289
|
-
return null;
|
|
290
|
-
}
|
|
291
|
-
return {
|
|
292
|
-
issuer: normalizeDID(iss),
|
|
293
|
-
claim: {
|
|
294
|
-
subjectKind,
|
|
295
|
-
subjectID,
|
|
296
|
-
keyID,
|
|
297
|
-
keyVersion,
|
|
298
|
-
hlc,
|
|
299
|
-
contentDigest: contentDigest,
|
|
300
|
-
deleted,
|
|
301
|
-
branchID: branchID
|
|
302
|
-
}
|
|
303
|
-
};
|
|
304
|
-
}
|