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 CHANGED
@@ -166,6 +166,100 @@ bob.registerPeerPublicKey('alice', alice.getPublicKey());
166
166
  await alice.sendDirectMessage('bob', 'dm', { text: 'private payload' });
167
167
  ```
168
168
 
169
+ ## Credential-Derived Identity Keys
170
+
171
+ Regenerate the same signing and encryption keys from a public username plus private password (instead of persisting random keys in `localStorage`):
172
+
173
+ ```js
174
+ const { deriveKeyPairFromCredentials, DignityP2P } = require('dignity.js');
175
+
176
+ const keyPair = await deriveKeyPairFromCredentials({
177
+ username: 'alice',
178
+ password: 'user-chosen-secret'
179
+ });
180
+
181
+ const alice = new DignityP2P({
182
+ nodeId: 'alice',
183
+ networkAdapter,
184
+ security: {
185
+ appPassword: 'shared-out-of-band-password',
186
+ keyPair
187
+ }
188
+ });
189
+ ```
190
+
191
+ `appPassword` is for broadcast encryption and is separate from the identity password. Password strength affects offline brute-force resistance; compromising the identity password exposes signing keys retroactively.
192
+
193
+ ### Compromise recovery (key #2, #3, …)
194
+
195
+ Bump `generation` after suspected private-key exposure. The rotation is signed with the **next** generation key (proves password knowledge); a stolen gen-1 private key alone cannot authorize gen-2.
196
+
197
+ ```js
198
+ const { revokeAndRotateIdentity } = require('dignity.js');
199
+
200
+ // User suspects session on a public PC — rotate gen 1 → gen 2
201
+ const { rotation, nextKeyPair, nextGeneration } = await revokeAndRotateIdentity({
202
+ username: 'alice',
203
+ password: 'user-chosen-secret',
204
+ currentGeneration: 1,
205
+ reason: 'left browser open on public PC'
206
+ });
207
+
208
+ await node.adoptDerivedIdentityKeyPair(nextKeyPair, { generation: nextGeneration });
209
+ await node.broadcastIdentityRotation(rotation, { broadcastScope: 'identity:alice' });
210
+ ```
211
+
212
+ Peers verify the signed `identity:rotate` message and upgrade trusted public keys; older generations are rejected.
213
+
214
+ ### Password change
215
+
216
+ ```js
217
+ const { rotateIdentityPassword } = require('dignity.js');
218
+
219
+ const { rotation, nextKeyPair, nextGeneration } = await rotateIdentityPassword({
220
+ username: 'alice',
221
+ currentPassword: 'old-secret',
222
+ newPassword: 'new-secret',
223
+ currentGeneration: 2
224
+ });
225
+
226
+ await node.adoptDerivedIdentityKeyPair(nextKeyPair, { generation: nextGeneration });
227
+ await node.broadcastIdentityRotation(rotation);
228
+ ```
229
+
230
+ The succession is signed with keys derived from the **new** password. Attackers who only know the old password cannot forge the rotation.
231
+
232
+ ### Cold recovery password (anti-lockout)
233
+
234
+ Store a **secondary cold password** offline (password manager, paper, bank vault). After enrollment, every identity rotation requires a **co-signature** from this cold key. An attacker who steals only the **primary** password cannot rotate your identity and lock you out.
235
+
236
+ ```js
237
+ const { enrollColdRecoveryPassword } = require('dignity.js');
238
+
239
+ // One-time setup — broadcast so peers require cold co-sign on future rotations
240
+ const { enrollment } = await enrollColdRecoveryPassword({
241
+ username: 'alice',
242
+ coldPassword: 'separate-vault-secret-never-on-public-pc'
243
+ });
244
+ await node.broadcastColdRecoveryEnrollment(enrollment);
245
+
246
+ // Later: compromise recovery needs BOTH passwords
247
+ const { rotation, nextKeyPair, nextGeneration } = await revokeAndRotateIdentity({
248
+ username: 'alice',
249
+ password: 'primary-secret',
250
+ coldPassword: 'separate-vault-secret-never-on-public-pc',
251
+ currentGeneration: 1
252
+ });
253
+ ```
254
+
255
+ | Secret | Use daily? | Can rotate identity alone? |
256
+ |--------|------------|----------------------------|
257
+ | Primary password | Yes | No (after cold enroll) |
258
+ | Cold password | No — vault only | Required co-sign for rotation |
259
+ | Stolen gen-N private key | — | No |
260
+
261
+ Cold password is **stable** across primary password changes and generation bumps (separate KDF domain). If both primary and cold are compromised, rotate both.
262
+
169
263
  ## Optimistic Concurrency
170
264
 
171
265
  Updates carry a monotonic `version`. Remote peers reject stale operations when `baseVersion` does not match.
@@ -198,7 +292,7 @@ Hash details:
198
292
  - The algorithm is `sha512`, matching `tweetnacl.hash` in both browser and Node builds.
199
293
  - The digest covers only `record.data`, not `id`, `ownerId`, timestamps, collaborators, or version.
200
294
  - Data is canonicalized with `stableStringify`, so object key order does not affect the hash.
201
- - Snapshot restore recomputes the digest locally and emits a `warning` with type `content-hash-mismatch` if a remote advertised hash does not match the received `data`.
295
+ - Snapshot restore recomputes the digest locally; direct mesh and PeerGroup gossip snapshots are rejected on hash mismatch (warning `content-hash-mismatch`).
202
296
  - Deleted tombstones returned by `list(collection, { includeDeleted: true })` intentionally omit `hash`.
203
297
 
204
298
  ## IndexedDB Persistence
@@ -335,6 +429,8 @@ The joiner applies the snapshot via `restoreRecord`, then subsequent move update
335
429
  | --- | --- | --- |
336
430
  | `npm test` | Run the full Jest suite with coverage. | Standard local validation before opening a PR or publishing. |
337
431
  | `npm run test:unit` | Run the unit-test subset only. | Useful for faster local iteration. |
432
+ | `npm run test:stress-peer-group` | Run the in-memory PeerGroup scale test. | Opt-in; set `RUN_STRESS_TESTS=1` (100 subscribers). |
433
+ | `npm run stress:peer-group` | CLI stress harness with JSON metrics. | Example: `node scripts/stress-peer-group.js --subscribers 1000 --json`. |
338
434
  | `npm run test:cloudflare-live` | Run the live Cloudflare signaling integration test. | Opt-in; set `RUN_CLOUDFLARE_LIVE_TESTS=1`. |
339
435
  | `npm run test:pow-calibrate` | Run the Sloth VDF timing calibration test without coverage. | Opt-in; set `RUN_POW_CALIBRATE=1`. |
340
436
  | `npm run build` | Build the published package bundles into `dist/`. | Run after changing library source files. |