dignity.js 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -1
- package/dist/dignity.cjs.js +802 -16
- package/dist/dignity.cjs.js.map +4 -4
- package/dist/dignity.esm.js +795 -9
- package/dist/dignity.esm.js.map +3 -3
- package/dist/dignity.min.js +9 -9
- package/docs/assets/dignity.esm.js +928 -30
- package/docs/assets/playground-demos.js +342 -0
- package/docs/assets/playground.css +277 -0
- package/docs/assets/playground.js +248 -0
- package/docs/assets/styles.css +18 -2
- package/docs/index.html +23 -3
- package/docs/openapi-like.json +1 -1
- package/package.json +5 -3
- package/src/core/dignity-p2p.js +229 -4
- package/src/index.js +25 -1
- package/src/security/derive-key-pair.js +147 -0
- package/src/security/identity-rotation.js +427 -0
- package/src/security/message-security-service.js +94 -4
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
const nacl = require('tweetnacl');
|
|
2
|
+
const naclUtil = require('tweetnacl-util');
|
|
3
|
+
const { stableStringify } = require('./message-security-service');
|
|
4
|
+
const {
|
|
5
|
+
deriveKeyPairFromCredentials,
|
|
6
|
+
deriveColdRecoverySigningKey,
|
|
7
|
+
keyPairToPublicBundle
|
|
8
|
+
} = require('./derive-key-pair');
|
|
9
|
+
|
|
10
|
+
const ROTATION_TYPES = new Set(['compromise-recovery', 'password-change']);
|
|
11
|
+
|
|
12
|
+
function utf8ToBytes(value) {
|
|
13
|
+
return naclUtil.decodeUTF8(value);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizePublicKeyBundle(publicKey) {
|
|
17
|
+
if (!publicKey || !publicKey.signingPublicKey || !publicKey.encryptionPublicKey) {
|
|
18
|
+
throw new Error('Public key bundle requires signingPublicKey and encryptionPublicKey');
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
signingPublicKey: publicKey.signingPublicKey,
|
|
22
|
+
encryptionPublicKey: publicKey.encryptionPublicKey
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function buildIdentityRotationPayload({
|
|
27
|
+
username,
|
|
28
|
+
fromGeneration,
|
|
29
|
+
toGeneration,
|
|
30
|
+
previousPublicKey,
|
|
31
|
+
nextPublicKey,
|
|
32
|
+
rotationKind,
|
|
33
|
+
reason,
|
|
34
|
+
timestamp
|
|
35
|
+
}) {
|
|
36
|
+
if (!username) {
|
|
37
|
+
throw new Error('Identity rotation requires username');
|
|
38
|
+
}
|
|
39
|
+
if (!ROTATION_TYPES.has(rotationKind)) {
|
|
40
|
+
throw new Error(`Identity rotation kind must be one of: ${[...ROTATION_TYPES].join(', ')}`);
|
|
41
|
+
}
|
|
42
|
+
if (toGeneration !== fromGeneration + 1) {
|
|
43
|
+
throw new Error('Identity rotation must advance generation by exactly 1');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
version: 1,
|
|
48
|
+
type: 'identity:rotate',
|
|
49
|
+
username,
|
|
50
|
+
fromGeneration,
|
|
51
|
+
toGeneration,
|
|
52
|
+
previousPublicKey: normalizePublicKeyBundle(previousPublicKey),
|
|
53
|
+
nextPublicKey: normalizePublicKeyBundle(nextPublicKey),
|
|
54
|
+
rotationKind,
|
|
55
|
+
reason: reason || '',
|
|
56
|
+
timestamp: typeof timestamp === 'number' ? timestamp : Date.now()
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function signIdentityRotationPayload(payload, signingSecretKey) {
|
|
61
|
+
const message = utf8ToBytes(stableStringify(payload));
|
|
62
|
+
const signature = nacl.sign.detached(message, signingSecretKey);
|
|
63
|
+
return naclUtil.encodeBase64(signature);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function verifyDetachedSignature(payload, signatureBase64, signingPublicKeyBase64) {
|
|
67
|
+
const message = utf8ToBytes(stableStringify(payload));
|
|
68
|
+
const signatureBytes = naclUtil.decodeBase64(signatureBase64);
|
|
69
|
+
const signingPublicKey = naclUtil.decodeBase64(signingPublicKeyBase64);
|
|
70
|
+
return nacl.sign.detached.verify(message, signatureBytes, signingPublicKey);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function createIdentityRotation({
|
|
74
|
+
username,
|
|
75
|
+
fromGeneration,
|
|
76
|
+
toGeneration,
|
|
77
|
+
previousPublicKey,
|
|
78
|
+
nextKeyPair,
|
|
79
|
+
rotationKind,
|
|
80
|
+
reason,
|
|
81
|
+
timestamp,
|
|
82
|
+
coldRecoverySigningSecretKey
|
|
83
|
+
}) {
|
|
84
|
+
if (!nextKeyPair || !nextKeyPair.signing || !nextKeyPair.signing.secretKey) {
|
|
85
|
+
throw new Error('Identity rotation requires nextKeyPair with signing secret');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const payload = buildIdentityRotationPayload({
|
|
89
|
+
username,
|
|
90
|
+
fromGeneration,
|
|
91
|
+
toGeneration,
|
|
92
|
+
previousPublicKey,
|
|
93
|
+
nextPublicKey: keyPairToPublicBundle(nextKeyPair),
|
|
94
|
+
rotationKind,
|
|
95
|
+
reason,
|
|
96
|
+
timestamp
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const rotation = {
|
|
100
|
+
...payload,
|
|
101
|
+
signature: signIdentityRotationPayload(payload, nextKeyPair.signing.secretKey)
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
if (coldRecoverySigningSecretKey) {
|
|
105
|
+
rotation.recoverySignature = signIdentityRotationPayload(
|
|
106
|
+
payload,
|
|
107
|
+
coldRecoverySigningSecretKey
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return rotation;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function buildColdRecoveryEnrollmentPayload({ username, recoveryPublicKey, timestamp }) {
|
|
115
|
+
if (!username) {
|
|
116
|
+
throw new Error('Cold recovery enrollment requires username');
|
|
117
|
+
}
|
|
118
|
+
if (!recoveryPublicKey) {
|
|
119
|
+
throw new Error('Cold recovery enrollment requires recoveryPublicKey');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
version: 1,
|
|
124
|
+
type: 'identity:cold-enroll',
|
|
125
|
+
username,
|
|
126
|
+
recoveryPublicKey,
|
|
127
|
+
timestamp: typeof timestamp === 'number' ? timestamp : Date.now()
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function createColdRecoveryEnrollment({
|
|
132
|
+
username,
|
|
133
|
+
coldRecoverySigningSecretKey,
|
|
134
|
+
recoveryPublicKey,
|
|
135
|
+
timestamp
|
|
136
|
+
}) {
|
|
137
|
+
if (!coldRecoverySigningSecretKey) {
|
|
138
|
+
throw new Error('Cold recovery enrollment requires cold recovery signing secret');
|
|
139
|
+
}
|
|
140
|
+
if (!recoveryPublicKey) {
|
|
141
|
+
throw new Error('Cold recovery enrollment requires recoveryPublicKey');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const payload = buildColdRecoveryEnrollmentPayload({
|
|
145
|
+
username,
|
|
146
|
+
recoveryPublicKey,
|
|
147
|
+
timestamp
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
...payload,
|
|
152
|
+
signature: signIdentityRotationPayload(payload, coldRecoverySigningSecretKey)
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function verifyColdRecoveryEnrollment(enrollment) {
|
|
157
|
+
if (!enrollment || enrollment.type !== 'identity:cold-enroll' || enrollment.version !== 1) {
|
|
158
|
+
return { ok: false, error: 'invalid-enrollment-shape' };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!enrollment.signature || !enrollment.recoveryPublicKey) {
|
|
162
|
+
return { ok: false, error: 'missing-enrollment-fields' };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const { signature, ...payload } = enrollment;
|
|
166
|
+
const verified = verifyDetachedSignature(payload, signature, enrollment.recoveryPublicKey);
|
|
167
|
+
|
|
168
|
+
if (!verified) {
|
|
169
|
+
return { ok: false, error: 'invalid-enrollment-signature' };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return { ok: true, enrollment };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function verifyIdentityRotation(rotation, options = {}) {
|
|
176
|
+
if (!rotation || rotation.type !== 'identity:rotate' || rotation.version !== 1) {
|
|
177
|
+
return { ok: false, error: 'invalid-rotation-shape' };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!rotation.signature || !rotation.nextPublicKey || !rotation.previousPublicKey) {
|
|
181
|
+
return { ok: false, error: 'missing-rotation-fields' };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (rotation.toGeneration !== rotation.fromGeneration + 1) {
|
|
185
|
+
return { ok: false, error: 'invalid-generation-step' };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!ROTATION_TYPES.has(rotation.rotationKind)) {
|
|
189
|
+
return { ok: false, error: 'invalid-rotation-kind' };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const { signature, recoverySignature, ...payload } = rotation;
|
|
193
|
+
const verified = verifyDetachedSignature(payload, signature, rotation.nextPublicKey.signingPublicKey);
|
|
194
|
+
|
|
195
|
+
if (!verified) {
|
|
196
|
+
return { ok: false, error: 'invalid-signature' };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const requiredRecoveryPublicKey = options.requiredRecoveryPublicKey || null;
|
|
200
|
+
if (requiredRecoveryPublicKey) {
|
|
201
|
+
if (!recoverySignature) {
|
|
202
|
+
return { ok: false, error: 'missing-recovery-signature' };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const recoveryVerified = verifyDetachedSignature(
|
|
206
|
+
payload,
|
|
207
|
+
recoverySignature,
|
|
208
|
+
requiredRecoveryPublicKey
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
if (!recoveryVerified) {
|
|
212
|
+
return { ok: false, error: 'invalid-recovery-signature' };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (rotation.previousPublicKey.signingPublicKey === rotation.nextPublicKey.signingPublicKey) {
|
|
217
|
+
return { ok: false, error: 'unchanged-signing-key' };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return { ok: true, rotation };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function resolveColdRecoverySigningSecretKey({
|
|
224
|
+
username,
|
|
225
|
+
coldPassword,
|
|
226
|
+
pepper,
|
|
227
|
+
kdfIterations
|
|
228
|
+
}) {
|
|
229
|
+
if (!coldPassword) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const coldRecovery = await deriveColdRecoverySigningKey({
|
|
234
|
+
username,
|
|
235
|
+
coldPassword,
|
|
236
|
+
pepper,
|
|
237
|
+
kdfIterations
|
|
238
|
+
});
|
|
239
|
+
return coldRecovery.signing.secretKey;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Same username + password, next generation (key #2, #3, …).
|
|
244
|
+
* Optional coldPassword co-signs the rotation so a stolen primary password
|
|
245
|
+
* cannot lock out the legitimate user after cold recovery is enrolled.
|
|
246
|
+
*/
|
|
247
|
+
async function revokeAndRotateIdentity({
|
|
248
|
+
username,
|
|
249
|
+
password,
|
|
250
|
+
coldPassword,
|
|
251
|
+
currentGeneration = 1,
|
|
252
|
+
reason = 'compromise-recovery',
|
|
253
|
+
pepper = '',
|
|
254
|
+
kdfIterations,
|
|
255
|
+
timestamp
|
|
256
|
+
} = {}) {
|
|
257
|
+
const currentKeyPair = await deriveKeyPairFromCredentials({
|
|
258
|
+
username,
|
|
259
|
+
password,
|
|
260
|
+
generation: currentGeneration,
|
|
261
|
+
pepper,
|
|
262
|
+
kdfIterations
|
|
263
|
+
});
|
|
264
|
+
const nextGeneration = currentGeneration + 1;
|
|
265
|
+
const nextKeyPair = await deriveKeyPairFromCredentials({
|
|
266
|
+
username,
|
|
267
|
+
password,
|
|
268
|
+
generation: nextGeneration,
|
|
269
|
+
pepper,
|
|
270
|
+
kdfIterations
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const coldRecoverySigningSecretKey = await resolveColdRecoverySigningSecretKey({
|
|
274
|
+
username,
|
|
275
|
+
coldPassword,
|
|
276
|
+
pepper,
|
|
277
|
+
kdfIterations
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const rotation = createIdentityRotation({
|
|
281
|
+
username,
|
|
282
|
+
fromGeneration: currentGeneration,
|
|
283
|
+
toGeneration: nextGeneration,
|
|
284
|
+
previousPublicKey: keyPairToPublicBundle(currentKeyPair),
|
|
285
|
+
nextKeyPair,
|
|
286
|
+
rotationKind: 'compromise-recovery',
|
|
287
|
+
reason,
|
|
288
|
+
timestamp,
|
|
289
|
+
coldRecoverySigningSecretKey
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
rotation,
|
|
294
|
+
currentKeyPair,
|
|
295
|
+
nextKeyPair,
|
|
296
|
+
nextGeneration,
|
|
297
|
+
coldRecoveryUsed: Boolean(coldRecoverySigningSecretKey)
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Password change: signed with new-password gen N+1 keys.
|
|
303
|
+
* Optional coldPassword prevents an attacker who stole the old primary password
|
|
304
|
+
* from rotating the identity away from the legitimate user.
|
|
305
|
+
*/
|
|
306
|
+
async function rotateIdentityPassword({
|
|
307
|
+
username,
|
|
308
|
+
currentPassword,
|
|
309
|
+
newPassword,
|
|
310
|
+
coldPassword,
|
|
311
|
+
currentGeneration = 1,
|
|
312
|
+
reason = 'password-change',
|
|
313
|
+
pepper = '',
|
|
314
|
+
kdfIterations,
|
|
315
|
+
timestamp
|
|
316
|
+
} = {}) {
|
|
317
|
+
const currentKeyPair = await deriveKeyPairFromCredentials({
|
|
318
|
+
username,
|
|
319
|
+
password: currentPassword,
|
|
320
|
+
generation: currentGeneration,
|
|
321
|
+
pepper,
|
|
322
|
+
kdfIterations
|
|
323
|
+
});
|
|
324
|
+
const nextGeneration = currentGeneration + 1;
|
|
325
|
+
const nextKeyPair = await deriveKeyPairFromCredentials({
|
|
326
|
+
username,
|
|
327
|
+
password: newPassword,
|
|
328
|
+
generation: nextGeneration,
|
|
329
|
+
pepper,
|
|
330
|
+
kdfIterations
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const coldRecoverySigningSecretKey = await resolveColdRecoverySigningSecretKey({
|
|
334
|
+
username,
|
|
335
|
+
coldPassword,
|
|
336
|
+
pepper,
|
|
337
|
+
kdfIterations
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
const rotation = createIdentityRotation({
|
|
341
|
+
username,
|
|
342
|
+
fromGeneration: currentGeneration,
|
|
343
|
+
toGeneration: nextGeneration,
|
|
344
|
+
previousPublicKey: keyPairToPublicBundle(currentKeyPair),
|
|
345
|
+
nextKeyPair,
|
|
346
|
+
rotationKind: 'password-change',
|
|
347
|
+
reason,
|
|
348
|
+
timestamp,
|
|
349
|
+
coldRecoverySigningSecretKey
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
rotation,
|
|
354
|
+
currentKeyPair,
|
|
355
|
+
nextKeyPair,
|
|
356
|
+
nextGeneration,
|
|
357
|
+
coldRecoveryUsed: Boolean(coldRecoverySigningSecretKey)
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function enrollColdRecoveryPassword({
|
|
362
|
+
username,
|
|
363
|
+
coldPassword,
|
|
364
|
+
pepper = '',
|
|
365
|
+
kdfIterations,
|
|
366
|
+
timestamp
|
|
367
|
+
} = {}) {
|
|
368
|
+
const coldRecovery = await deriveColdRecoverySigningKey({
|
|
369
|
+
username,
|
|
370
|
+
coldPassword,
|
|
371
|
+
pepper,
|
|
372
|
+
kdfIterations
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
const enrollment = createColdRecoveryEnrollment({
|
|
376
|
+
username,
|
|
377
|
+
coldRecoverySigningSecretKey: coldRecovery.signing.secretKey,
|
|
378
|
+
recoveryPublicKey: coldRecovery.recoveryPublicKey,
|
|
379
|
+
timestamp
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
enrollment,
|
|
384
|
+
recoveryPublicKey: coldRecovery.recoveryPublicKey,
|
|
385
|
+
coldRecovery
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function shouldApplyIdentityRotation(currentState, rotation, options = {}) {
|
|
390
|
+
const requiredRecoveryPublicKey = options.enrolledRecoveryPublicKey
|
|
391
|
+
|| (currentState && currentState.recoveryPublicKey)
|
|
392
|
+
|| null;
|
|
393
|
+
|
|
394
|
+
const verified = verifyIdentityRotation(rotation, { requiredRecoveryPublicKey });
|
|
395
|
+
if (!verified.ok) {
|
|
396
|
+
return { apply: false, reason: verified.error };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (currentState && rotation.toGeneration <= currentState.generation) {
|
|
400
|
+
return { apply: false, reason: 'stale-generation' };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (
|
|
404
|
+
currentState
|
|
405
|
+
&& currentState.publicKey
|
|
406
|
+
&& currentState.publicKey.signingPublicKey !== rotation.previousPublicKey.signingPublicKey
|
|
407
|
+
) {
|
|
408
|
+
return { apply: false, reason: 'previous-key-mismatch' };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return { apply: true, rotation: verified.rotation };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
module.exports = {
|
|
415
|
+
createIdentityRotation,
|
|
416
|
+
createColdRecoveryEnrollment,
|
|
417
|
+
verifyIdentityRotation,
|
|
418
|
+
verifyColdRecoveryEnrollment,
|
|
419
|
+
revokeAndRotateIdentity,
|
|
420
|
+
rotateIdentityPassword,
|
|
421
|
+
enrollColdRecoveryPassword,
|
|
422
|
+
shouldApplyIdentityRotation,
|
|
423
|
+
keyPairToPublicBundle,
|
|
424
|
+
buildIdentityRotationPayload,
|
|
425
|
+
buildColdRecoveryEnrollmentPayload,
|
|
426
|
+
signIdentityRotationPayload
|
|
427
|
+
};
|
|
@@ -2,13 +2,15 @@ const nacl = require('tweetnacl');
|
|
|
2
2
|
const naclUtil = require('tweetnacl-util');
|
|
3
3
|
const VDF = require('./vdf');
|
|
4
4
|
|
|
5
|
+
const DEFAULT_APP_PASSWORD = 'change-this-app-password';
|
|
6
|
+
|
|
5
7
|
const DEFAULT_SECURITY_OPTIONS = {
|
|
6
8
|
enabled: true,
|
|
7
9
|
signingEnabled: true,
|
|
8
10
|
encryptionEnabled: true,
|
|
9
11
|
powEnabled: true,
|
|
10
12
|
powTargetMs: 1000,
|
|
11
|
-
appPassword:
|
|
13
|
+
appPassword: DEFAULT_APP_PASSWORD,
|
|
12
14
|
broadcastPasswords: {},
|
|
13
15
|
resolveBroadcastPassword: null,
|
|
14
16
|
powSteps: 22,
|
|
@@ -123,8 +125,11 @@ class MessageSecurityService {
|
|
|
123
125
|
};
|
|
124
126
|
|
|
125
127
|
this.peerPublicKeys = new Map();
|
|
128
|
+
this.peerIdentityGenerations = new Map(); // peerId -> generation number
|
|
129
|
+
this.peerRecoveryPublicKeys = new Map(); // peerId -> cold recovery signing pubkey (base64)
|
|
126
130
|
for (const [peerId, peerKey] of Object.entries(this.options.trustedPeerKeys || {})) {
|
|
127
131
|
this.peerPublicKeys.set(peerId, normalizePeerPublicKey(peerKey));
|
|
132
|
+
this.peerIdentityGenerations.set(peerId, 1);
|
|
128
133
|
}
|
|
129
134
|
|
|
130
135
|
this.calibratedPowSteps = this.options.powSteps;
|
|
@@ -134,8 +139,92 @@ class MessageSecurityService {
|
|
|
134
139
|
return { ...this.publicKeyBundle };
|
|
135
140
|
}
|
|
136
141
|
|
|
137
|
-
registerPeerPublicKey(peerId, publicKey) {
|
|
138
|
-
|
|
142
|
+
registerPeerPublicKey(peerId, publicKey, options = {}) {
|
|
143
|
+
const normalized = normalizePeerPublicKey(publicKey);
|
|
144
|
+
const generation = typeof options.generation === 'number' ? options.generation : 1;
|
|
145
|
+
const currentGeneration = this.peerIdentityGenerations.get(peerId) || 0;
|
|
146
|
+
|
|
147
|
+
if (generation < currentGeneration && options.allowDowngrade !== true) {
|
|
148
|
+
throw new Error(`Refusing older identity generation for peer ${peerId}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
this.peerPublicKeys.set(peerId, normalized);
|
|
152
|
+
this.peerIdentityGenerations.set(peerId, Math.max(currentGeneration, generation));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
getPeerIdentityGeneration(peerId) {
|
|
156
|
+
return this.peerIdentityGenerations.get(peerId) || 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
getPeerIdentityState(peerId) {
|
|
160
|
+
const publicKey = this.peerPublicKeys.get(peerId);
|
|
161
|
+
const recoveryPublicKey = this.peerRecoveryPublicKeys.get(peerId) || null;
|
|
162
|
+
|
|
163
|
+
if (!publicKey && !recoveryPublicKey) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
publicKey: publicKey ? { ...publicKey } : null,
|
|
169
|
+
generation: this.getPeerIdentityGeneration(peerId),
|
|
170
|
+
recoveryPublicKey
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
registerPeerRecoveryPublicKey(peerId, recoveryPublicKey) {
|
|
175
|
+
if (!peerId || !recoveryPublicKey) {
|
|
176
|
+
throw new Error('registerPeerRecoveryPublicKey requires peerId and recoveryPublicKey');
|
|
177
|
+
}
|
|
178
|
+
this.peerRecoveryPublicKeys.set(peerId, recoveryPublicKey);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
getPeerRecoveryPublicKey(peerId) {
|
|
182
|
+
return this.peerRecoveryPublicKeys.get(peerId) || null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
applyColdRecoveryEnrollment(peerId, enrollment) {
|
|
186
|
+
const { verifyColdRecoveryEnrollment } = require('./identity-rotation');
|
|
187
|
+
const verified = verifyColdRecoveryEnrollment(enrollment);
|
|
188
|
+
if (!verified.ok) {
|
|
189
|
+
const error = new Error(`Invalid cold recovery enrollment: ${verified.error}`);
|
|
190
|
+
error.code = 'INVALID_COLD_RECOVERY_ENROLLMENT';
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
this.registerPeerRecoveryPublicKey(peerId, enrollment.recoveryPublicKey);
|
|
195
|
+
return { applied: true, recoveryPublicKey: enrollment.recoveryPublicKey };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
applyIdentityRotation(peerId, rotation) {
|
|
199
|
+
const { shouldApplyIdentityRotation } = require('./identity-rotation');
|
|
200
|
+
|
|
201
|
+
const currentState = this.getPeerIdentityState(peerId);
|
|
202
|
+
const decision = shouldApplyIdentityRotation(currentState, rotation, {
|
|
203
|
+
enrolledRecoveryPublicKey: this.getPeerRecoveryPublicKey(peerId)
|
|
204
|
+
});
|
|
205
|
+
if (!decision.apply) {
|
|
206
|
+
if (
|
|
207
|
+
decision.reason
|
|
208
|
+
&& decision.reason !== 'stale-generation'
|
|
209
|
+
&& decision.reason !== 'previous-key-mismatch'
|
|
210
|
+
) {
|
|
211
|
+
const error = new Error(`Invalid identity rotation: ${decision.reason}`);
|
|
212
|
+
error.code = 'INVALID_IDENTITY_ROTATION';
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
return { applied: false, reason: decision.reason };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
this.registerPeerPublicKey(peerId, rotation.nextPublicKey, {
|
|
219
|
+
generation: rotation.toGeneration
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
applied: true,
|
|
224
|
+
fromGeneration: rotation.fromGeneration,
|
|
225
|
+
toGeneration: rotation.toGeneration,
|
|
226
|
+
rotationKind: rotation.rotationKind
|
|
227
|
+
};
|
|
139
228
|
}
|
|
140
229
|
|
|
141
230
|
resolvePeerPublicKey(peerId, fallbackPublicKey) {
|
|
@@ -523,5 +612,6 @@ module.exports = {
|
|
|
523
612
|
stableStringify,
|
|
524
613
|
deriveBroadcastKey,
|
|
525
614
|
legacyBroadcastKey,
|
|
526
|
-
DEFAULT_SECURITY_OPTIONS
|
|
615
|
+
DEFAULT_SECURITY_OPTIONS,
|
|
616
|
+
DEFAULT_APP_PASSWORD
|
|
527
617
|
};
|