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
package/src/core/dignity-p2p.js
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
const nacl = require('tweetnacl');
|
|
2
2
|
const naclUtil = require('tweetnacl-util');
|
|
3
3
|
const EventEmitter = require('../utils/event-emitter');
|
|
4
|
-
const {
|
|
4
|
+
const {
|
|
5
|
+
MessageSecurityService,
|
|
6
|
+
stableStringify,
|
|
7
|
+
DEFAULT_APP_PASSWORD
|
|
8
|
+
} = require('../security/message-security-service');
|
|
9
|
+
const {
|
|
10
|
+
revokeAndRotateIdentity,
|
|
11
|
+
rotateIdentityPassword,
|
|
12
|
+
enrollColdRecoveryPassword
|
|
13
|
+
} = require('../security/identity-rotation');
|
|
14
|
+
const { deriveKeyPairFromCredentials } = require('../security/derive-key-pair');
|
|
5
15
|
const {
|
|
6
16
|
DEFAULT_PEER_GROUP_OPTIONS,
|
|
7
17
|
peerGroupScope,
|
|
@@ -91,6 +101,13 @@ class DignityP2P extends EventEmitter {
|
|
|
91
101
|
this.gossipIdTtlMs = security && typeof security.gossipIdTtlMs === 'number'
|
|
92
102
|
? security.gossipIdTtlMs
|
|
93
103
|
: 5 * 60 * 1000;
|
|
104
|
+
this.maxSeenGossipIds = security && typeof security.maxSeenGossipIds === 'number'
|
|
105
|
+
? security.maxSeenGossipIds
|
|
106
|
+
: 100000;
|
|
107
|
+
this.gossipPublishMinIntervalMs = security && typeof security.gossipPublishMinIntervalMs === 'number'
|
|
108
|
+
? security.gossipPublishMinIntervalMs
|
|
109
|
+
: 0;
|
|
110
|
+
this.lastGossipPublishAt = new Map(); // groupId -> timestamp
|
|
94
111
|
this.maxAppliedOperations = security && typeof security.maxAppliedOperations === 'number'
|
|
95
112
|
? security.maxAppliedOperations
|
|
96
113
|
: 50000;
|
|
@@ -103,6 +120,14 @@ class DignityP2P extends EventEmitter {
|
|
|
103
120
|
async start() {
|
|
104
121
|
this.networkAdapter.onMessage(this.boundMessageHandler);
|
|
105
122
|
await this.networkAdapter.start(this.nodeId);
|
|
123
|
+
|
|
124
|
+
const appPassword = this.securityService.options.appPassword;
|
|
125
|
+
if (!appPassword || appPassword === DEFAULT_APP_PASSWORD) {
|
|
126
|
+
this.emit('warning', {
|
|
127
|
+
type: 'default-app-password',
|
|
128
|
+
message: 'Using the default appPassword is insecure; set a strong shared secret in production.'
|
|
129
|
+
});
|
|
130
|
+
}
|
|
106
131
|
}
|
|
107
132
|
|
|
108
133
|
async stop() {
|
|
@@ -461,8 +486,154 @@ class DignityP2P extends EventEmitter {
|
|
|
461
486
|
});
|
|
462
487
|
}
|
|
463
488
|
|
|
464
|
-
registerPeerPublicKey(peerId, publicKey) {
|
|
465
|
-
this.securityService.registerPeerPublicKey(peerId, publicKey);
|
|
489
|
+
registerPeerPublicKey(peerId, publicKey, options = {}) {
|
|
490
|
+
this.securityService.registerPeerPublicKey(peerId, publicKey, options);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
getPeerIdentityGeneration(peerId) {
|
|
494
|
+
return this.securityService.getPeerIdentityGeneration(peerId);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
getPeerIdentityState(peerId) {
|
|
498
|
+
return this.securityService.getPeerIdentityState(peerId);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
applyPeerIdentityRotation(peerId, rotation) {
|
|
502
|
+
const result = this.securityService.applyIdentityRotation(peerId, rotation);
|
|
503
|
+
if (result.applied) {
|
|
504
|
+
this.emit('identityrotated', {
|
|
505
|
+
peerId,
|
|
506
|
+
username: rotation.username,
|
|
507
|
+
fromGeneration: result.fromGeneration,
|
|
508
|
+
toGeneration: result.toGeneration,
|
|
509
|
+
rotationKind: result.rotationKind
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
return result;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
async broadcastIdentityRotation(rotation, options = {}) {
|
|
516
|
+
return this.broadcastMessage('identity:rotate', rotation, options);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
async broadcastColdRecoveryEnrollment(enrollment, options = {}) {
|
|
520
|
+
return this.broadcastMessage('identity:cold-enroll', enrollment, options);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
applyPeerColdRecoveryEnrollment(peerId, enrollment) {
|
|
524
|
+
const result = this.securityService.applyColdRecoveryEnrollment(peerId, enrollment);
|
|
525
|
+
if (result.applied) {
|
|
526
|
+
this.emit('coldrecoveryenrolled', {
|
|
527
|
+
peerId,
|
|
528
|
+
username: enrollment.username,
|
|
529
|
+
recoveryPublicKey: enrollment.recoveryPublicKey
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
return result;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async enrollAndBroadcastColdRecovery({
|
|
536
|
+
username,
|
|
537
|
+
coldPassword,
|
|
538
|
+
pepper = '',
|
|
539
|
+
kdfIterations,
|
|
540
|
+
broadcastOptions = {}
|
|
541
|
+
} = {}) {
|
|
542
|
+
const result = await enrollColdRecoveryPassword({
|
|
543
|
+
username,
|
|
544
|
+
coldPassword,
|
|
545
|
+
pepper,
|
|
546
|
+
kdfIterations
|
|
547
|
+
});
|
|
548
|
+
await this.broadcastColdRecoveryEnrollment(result.enrollment, broadcastOptions);
|
|
549
|
+
return result;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
async revokeAndRotateDerivedIdentity({
|
|
553
|
+
username,
|
|
554
|
+
password,
|
|
555
|
+
coldPassword,
|
|
556
|
+
currentGeneration = 1,
|
|
557
|
+
reason = 'compromise-recovery',
|
|
558
|
+
pepper = '',
|
|
559
|
+
kdfIterations,
|
|
560
|
+
broadcast = false,
|
|
561
|
+
broadcastOptions = {}
|
|
562
|
+
} = {}) {
|
|
563
|
+
const result = await revokeAndRotateIdentity({
|
|
564
|
+
username,
|
|
565
|
+
password,
|
|
566
|
+
coldPassword,
|
|
567
|
+
currentGeneration,
|
|
568
|
+
reason,
|
|
569
|
+
pepper,
|
|
570
|
+
kdfIterations
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
if (broadcast) {
|
|
574
|
+
await this.broadcastIdentityRotation(result.rotation, broadcastOptions);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return result;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
async rotateDerivedIdentityPassword({
|
|
581
|
+
username,
|
|
582
|
+
currentPassword,
|
|
583
|
+
newPassword,
|
|
584
|
+
coldPassword,
|
|
585
|
+
currentGeneration = 1,
|
|
586
|
+
reason = 'password-change',
|
|
587
|
+
pepper = '',
|
|
588
|
+
kdfIterations,
|
|
589
|
+
broadcast = false,
|
|
590
|
+
broadcastOptions = {}
|
|
591
|
+
} = {}) {
|
|
592
|
+
const result = await rotateIdentityPassword({
|
|
593
|
+
username,
|
|
594
|
+
currentPassword,
|
|
595
|
+
newPassword,
|
|
596
|
+
coldPassword,
|
|
597
|
+
currentGeneration,
|
|
598
|
+
reason,
|
|
599
|
+
pepper,
|
|
600
|
+
kdfIterations
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
if (broadcast) {
|
|
604
|
+
await this.broadcastIdentityRotation(result.rotation, broadcastOptions);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return result;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
async adoptDerivedIdentityKeyPair(keyPair, { generation = 1 } = {}) {
|
|
611
|
+
if (!keyPair || !keyPair.signing || !keyPair.encryption) {
|
|
612
|
+
throw new Error('adoptDerivedIdentityKeyPair requires a derived keyPair');
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
this.securityService.signingSecretKey = keyPair.signing.secretKey;
|
|
616
|
+
this.securityService.signingPublicKey = keyPair.signing.publicKey;
|
|
617
|
+
this.securityService.encryptionSecretKey = keyPair.encryption.secretKey;
|
|
618
|
+
this.securityService.encryptionPublicKey = keyPair.encryption.publicKey;
|
|
619
|
+
this.securityService.publicKeyBundle = {
|
|
620
|
+
signingPublicKey: naclUtil.encodeBase64(keyPair.signing.publicKey),
|
|
621
|
+
encryptionPublicKey: naclUtil.encodeBase64(keyPair.encryption.publicKey)
|
|
622
|
+
};
|
|
623
|
+
this.securityService.options.keyPair = keyPair;
|
|
624
|
+
this.securityService.options.identityGeneration = generation;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
async deriveAndAdoptIdentity({ username, password, generation = 1, pepper = '', kdfIterations } = {}) {
|
|
628
|
+
const keyPair = await deriveKeyPairFromCredentials({
|
|
629
|
+
username,
|
|
630
|
+
password,
|
|
631
|
+
generation,
|
|
632
|
+
pepper,
|
|
633
|
+
kdfIterations
|
|
634
|
+
});
|
|
635
|
+
await this.adoptDerivedIdentityKeyPair(keyPair, { generation });
|
|
636
|
+
return keyPair;
|
|
466
637
|
}
|
|
467
638
|
|
|
468
639
|
trustPeerPublicKey(peerId, publicKey) {
|
|
@@ -624,6 +795,14 @@ class DignityP2P extends EventEmitter {
|
|
|
624
795
|
this.seenGossipIds.delete(gossipId);
|
|
625
796
|
}
|
|
626
797
|
}
|
|
798
|
+
|
|
799
|
+
while (this.seenGossipIds.size > this.maxSeenGossipIds) {
|
|
800
|
+
const oldestGossipId = this.seenGossipIds.keys().next().value;
|
|
801
|
+
if (!oldestGossipId) {
|
|
802
|
+
break;
|
|
803
|
+
}
|
|
804
|
+
this.seenGossipIds.delete(oldestGossipId);
|
|
805
|
+
}
|
|
627
806
|
}
|
|
628
807
|
|
|
629
808
|
hasSeenGossip(gossipId) {
|
|
@@ -641,6 +820,7 @@ class DignityP2P extends EventEmitter {
|
|
|
641
820
|
}
|
|
642
821
|
|
|
643
822
|
this.seenGossipIds.set(gossipId, this.now() + this.gossipIdTtlMs);
|
|
823
|
+
this.pruneSeenGossip();
|
|
644
824
|
}
|
|
645
825
|
|
|
646
826
|
listConnectedPeerIds() {
|
|
@@ -734,6 +914,16 @@ class DignityP2P extends EventEmitter {
|
|
|
734
914
|
throw new Error(`PeerGroup ${groupId} has not been joined`);
|
|
735
915
|
}
|
|
736
916
|
|
|
917
|
+
if (this.gossipPublishMinIntervalMs > 0) {
|
|
918
|
+
const lastPublishAt = this.lastGossipPublishAt.get(groupId) || 0;
|
|
919
|
+
const elapsed = this.now() - lastPublishAt;
|
|
920
|
+
if (elapsed < this.gossipPublishMinIntervalMs) {
|
|
921
|
+
const error = new Error(`Gossip publish rate limit exceeded for group ${groupId}`);
|
|
922
|
+
error.code = 'GOSSIP_RATE_LIMIT';
|
|
923
|
+
throw error;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
737
927
|
const fanout = typeof options.fanout === 'number'
|
|
738
928
|
? options.fanout
|
|
739
929
|
: (group ? group.fanout : this.defaultPeerGroupFanout);
|
|
@@ -750,6 +940,7 @@ class DignityP2P extends EventEmitter {
|
|
|
750
940
|
|
|
751
941
|
const gossipId = options.gossipId || this.idGenerator();
|
|
752
942
|
this.markSeenGossip(gossipId);
|
|
943
|
+
this.lastGossipPublishAt.set(groupId, this.now());
|
|
753
944
|
|
|
754
945
|
await this.broadcastMessage('peer-group:gossip', {
|
|
755
946
|
groupId,
|
|
@@ -1117,6 +1308,37 @@ class DignityP2P extends EventEmitter {
|
|
|
1117
1308
|
return;
|
|
1118
1309
|
}
|
|
1119
1310
|
|
|
1311
|
+
if (decrypted.messageType === 'identity:rotate') {
|
|
1312
|
+
const peerId = decrypted.senderId || decrypted.payload?.username;
|
|
1313
|
+
if (peerId && decrypted.payload) {
|
|
1314
|
+
const result = this.applyPeerIdentityRotation(peerId, decrypted.payload);
|
|
1315
|
+
if (!result.applied) {
|
|
1316
|
+
this.emit('warning', {
|
|
1317
|
+
type: 'identity-rotation-ignored',
|
|
1318
|
+
peerId,
|
|
1319
|
+
reason: result.reason
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
if (decrypted.messageType === 'identity:cold-enroll') {
|
|
1327
|
+
const peerId = decrypted.senderId || decrypted.payload?.username;
|
|
1328
|
+
if (peerId && decrypted.payload) {
|
|
1329
|
+
try {
|
|
1330
|
+
this.applyPeerColdRecoveryEnrollment(peerId, decrypted.payload);
|
|
1331
|
+
} catch (error) {
|
|
1332
|
+
this.emit('warning', {
|
|
1333
|
+
type: 'cold-recovery-enrollment-rejected',
|
|
1334
|
+
peerId,
|
|
1335
|
+
error
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1120
1342
|
if (message && message.senderId && message.senderPublicKey) {
|
|
1121
1343
|
this.trustPeerPublicKey(message.senderId, message.senderPublicKey);
|
|
1122
1344
|
}
|
|
@@ -1131,7 +1353,10 @@ class DignityP2P extends EventEmitter {
|
|
|
1131
1353
|
const { collectionName, record } = payload;
|
|
1132
1354
|
|
|
1133
1355
|
if (collectionName && record) {
|
|
1134
|
-
const applied = this.restoreRecord(collectionName, record
|
|
1356
|
+
const applied = this.restoreRecord(collectionName, record, {
|
|
1357
|
+
rejectOnHashMismatch: true,
|
|
1358
|
+
via: 'direct-mesh'
|
|
1359
|
+
});
|
|
1135
1360
|
if (applied) {
|
|
1136
1361
|
this.emit('change', {
|
|
1137
1362
|
kind: 'snapshot',
|
package/src/index.js
CHANGED
|
@@ -28,8 +28,20 @@ const VDF = require('./security/vdf');
|
|
|
28
28
|
const SlothPermutation = require('./security/sloth-vdf');
|
|
29
29
|
const {
|
|
30
30
|
MessageSecurityService,
|
|
31
|
-
DEFAULT_SECURITY_OPTIONS
|
|
31
|
+
DEFAULT_SECURITY_OPTIONS,
|
|
32
|
+
DEFAULT_APP_PASSWORD
|
|
32
33
|
} = require('./security/message-security-service');
|
|
34
|
+
const { deriveKeyPairFromCredentials, keyPairToPublicBundle, deriveColdRecoverySigningKey } = require('./security/derive-key-pair');
|
|
35
|
+
const {
|
|
36
|
+
createIdentityRotation,
|
|
37
|
+
verifyIdentityRotation,
|
|
38
|
+
revokeAndRotateIdentity,
|
|
39
|
+
rotateIdentityPassword,
|
|
40
|
+
enrollColdRecoveryPassword,
|
|
41
|
+
verifyColdRecoveryEnrollment,
|
|
42
|
+
shouldApplyIdentityRotation
|
|
43
|
+
} = require('./security/identity-rotation');
|
|
44
|
+
const parsePeerJsServerUrl = require('./signaling/parse-peerjs-url');
|
|
33
45
|
const {
|
|
34
46
|
PEER_GROUP_SCOPE_PREFIX,
|
|
35
47
|
DEFAULT_PEER_GROUP_OPTIONS,
|
|
@@ -55,6 +67,18 @@ module.exports = {
|
|
|
55
67
|
SlothPermutation,
|
|
56
68
|
MessageSecurityService,
|
|
57
69
|
DEFAULT_SECURITY_OPTIONS,
|
|
70
|
+
DEFAULT_APP_PASSWORD,
|
|
71
|
+
deriveKeyPairFromCredentials,
|
|
72
|
+
deriveColdRecoverySigningKey,
|
|
73
|
+
keyPairToPublicBundle,
|
|
74
|
+
createIdentityRotation,
|
|
75
|
+
verifyIdentityRotation,
|
|
76
|
+
revokeAndRotateIdentity,
|
|
77
|
+
rotateIdentityPassword,
|
|
78
|
+
enrollColdRecoveryPassword,
|
|
79
|
+
verifyColdRecoveryEnrollment,
|
|
80
|
+
shouldApplyIdentityRotation,
|
|
81
|
+
parsePeerJsServerUrl,
|
|
58
82
|
PEER_GROUP_SCOPE_PREFIX,
|
|
59
83
|
DEFAULT_PEER_GROUP_OPTIONS,
|
|
60
84
|
peerGroupScope,
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
const nacl = require('tweetnacl');
|
|
2
|
+
const naclUtil = require('tweetnacl-util');
|
|
3
|
+
const { deriveBroadcastKey, DEFAULT_SECURITY_OPTIONS } = require('./message-security-service');
|
|
4
|
+
|
|
5
|
+
const SIGNING_INFO = 'dignity-signing-v1';
|
|
6
|
+
const ENCRYPTION_INFO = 'dignity-encryption-v1';
|
|
7
|
+
const COLD_RECOVERY_INFO = 'dignity-cold-recovery-v1';
|
|
8
|
+
|
|
9
|
+
function utf8ToBytes(value) {
|
|
10
|
+
return naclUtil.decodeUTF8(value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function concatBytes(...parts) {
|
|
14
|
+
const total = parts.reduce((sum, part) => sum + part.length, 0);
|
|
15
|
+
const result = new Uint8Array(total);
|
|
16
|
+
let offset = 0;
|
|
17
|
+
for (const part of parts) {
|
|
18
|
+
result.set(part, offset);
|
|
19
|
+
offset += part.length;
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function buildColdRecoverySalt(username, pepper = '') {
|
|
25
|
+
if (!username || typeof username !== 'string') {
|
|
26
|
+
throw new Error('deriveColdRecoverySigningKey requires username');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const segments = ['dignity-cold-recovery-v1'];
|
|
30
|
+
if (pepper) {
|
|
31
|
+
segments.push(pepper);
|
|
32
|
+
}
|
|
33
|
+
segments.push(username, COLD_RECOVERY_INFO);
|
|
34
|
+
return utf8ToBytes(segments.join('\0'));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function deriveColdRecoverySigningKey({
|
|
38
|
+
username,
|
|
39
|
+
coldPassword,
|
|
40
|
+
pepper = '',
|
|
41
|
+
kdfIterations
|
|
42
|
+
} = {}) {
|
|
43
|
+
if (!coldPassword || typeof coldPassword !== 'string') {
|
|
44
|
+
throw new Error('deriveColdRecoverySigningKey requires coldPassword');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const salt = buildColdRecoverySalt(username, pepper);
|
|
48
|
+
const iterations = typeof kdfIterations === 'number'
|
|
49
|
+
? kdfIterations
|
|
50
|
+
: DEFAULT_SECURITY_OPTIONS.kdfIterations;
|
|
51
|
+
const seed = await deriveBroadcastKey(coldPassword, salt, iterations);
|
|
52
|
+
const signing = nacl.sign.keyPair.fromSeed(seed);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
signing,
|
|
56
|
+
recoveryPublicKey: naclUtil.encodeBase64(signing.publicKey)
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function buildIdentitySalt(username, info, pepper = '', generation = 1) {
|
|
61
|
+
if (!username || typeof username !== 'string') {
|
|
62
|
+
throw new Error('deriveKeyPairFromCredentials requires username');
|
|
63
|
+
}
|
|
64
|
+
if (!info || typeof info !== 'string') {
|
|
65
|
+
throw new Error('deriveKeyPairFromCredentials requires info label');
|
|
66
|
+
}
|
|
67
|
+
const normalizedGeneration = Number(generation);
|
|
68
|
+
if (!Number.isInteger(normalizedGeneration) || normalizedGeneration < 1) {
|
|
69
|
+
throw new Error('deriveKeyPairFromCredentials requires generation >= 1');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const segments = ['dignity-identity-v1'];
|
|
73
|
+
if (pepper) {
|
|
74
|
+
segments.push(pepper);
|
|
75
|
+
}
|
|
76
|
+
segments.push(username, `gen:${normalizedGeneration}`, info);
|
|
77
|
+
return utf8ToBytes(segments.join('\0'));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function deriveIdentitySeed({ password, username, info, pepper, generation, kdfIterations }) {
|
|
81
|
+
if (!password || typeof password !== 'string') {
|
|
82
|
+
throw new Error('deriveKeyPairFromCredentials requires password');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const salt = buildIdentitySalt(username, info, pepper, generation);
|
|
86
|
+
const iterations = typeof kdfIterations === 'number'
|
|
87
|
+
? kdfIterations
|
|
88
|
+
: DEFAULT_SECURITY_OPTIONS.kdfIterations;
|
|
89
|
+
|
|
90
|
+
return deriveBroadcastKey(password, salt, iterations);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Derive deterministic Ed25519 signing and Curve25519 box key pairs from
|
|
95
|
+
* public username + private password. Same inputs yield the same keys across
|
|
96
|
+
* runs and environments. Bump `generation` (2, 3, …) after compromise recovery;
|
|
97
|
+
* see `identity-rotation.js` for signed revocation / password-change flows.
|
|
98
|
+
*/
|
|
99
|
+
async function deriveKeyPairFromCredentials({
|
|
100
|
+
username,
|
|
101
|
+
password,
|
|
102
|
+
pepper = '',
|
|
103
|
+
generation = 1,
|
|
104
|
+
kdfIterations
|
|
105
|
+
} = {}) {
|
|
106
|
+
const signingSeed = await deriveIdentitySeed({
|
|
107
|
+
password,
|
|
108
|
+
username,
|
|
109
|
+
info: SIGNING_INFO,
|
|
110
|
+
pepper,
|
|
111
|
+
generation,
|
|
112
|
+
kdfIterations
|
|
113
|
+
});
|
|
114
|
+
const encryptionSecret = await deriveIdentitySeed({
|
|
115
|
+
password,
|
|
116
|
+
username,
|
|
117
|
+
info: ENCRYPTION_INFO,
|
|
118
|
+
pepper,
|
|
119
|
+
generation,
|
|
120
|
+
kdfIterations
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
signing: nacl.sign.keyPair.fromSeed(signingSeed),
|
|
125
|
+
encryption: nacl.box.keyPair.fromSecretKey(encryptionSecret),
|
|
126
|
+
generation
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function keyPairToPublicBundle(keyPair) {
|
|
131
|
+
return {
|
|
132
|
+
signingPublicKey: naclUtil.encodeBase64(keyPair.signing.publicKey),
|
|
133
|
+
encryptionPublicKey: naclUtil.encodeBase64(keyPair.encryption.publicKey)
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = {
|
|
138
|
+
deriveKeyPairFromCredentials,
|
|
139
|
+
deriveColdRecoverySigningKey,
|
|
140
|
+
keyPairToPublicBundle,
|
|
141
|
+
buildIdentitySalt,
|
|
142
|
+
buildColdRecoverySalt,
|
|
143
|
+
SIGNING_INFO,
|
|
144
|
+
ENCRYPTION_INFO,
|
|
145
|
+
COLD_RECOVERY_INFO,
|
|
146
|
+
concatBytes
|
|
147
|
+
};
|