|
1 | 1 | import { createKeyset, type UnixTimestamp } from '@localfirst/crdx' |
2 | 2 | import { signatures } from '@localfirst/crypto' |
3 | | -import { redactDevice, type FirstUseDevice } from 'index.js' |
| 3 | +import { redactDevice, Team, type FirstUseDevice } from 'index.js' |
4 | 4 | import { generateProof } from 'invitation/index.js' |
5 | 5 | import * as teams from 'team/index.js' |
6 | 6 | import { KeyType } from 'util/index.js' |
@@ -287,7 +287,7 @@ describe('Team', () => { |
287 | 287 |
|
288 | 288 | // To do that, she uses the invitation seed to generate starter keys, which she can use to |
289 | 289 | // unlock a lockbox stored on the graph containing her user keys. |
290 | | - const aliceUser = teams.getDeviceUserFromGraph({ |
| 290 | + const { user: aliceUser } = teams.getDeviceUserFromGraph({ |
291 | 291 | serializedGraph, |
292 | 292 | teamKeyring, |
293 | 293 | invitationSeed: seed, |
@@ -353,6 +353,94 @@ describe('Team', () => { |
353 | 353 | // 🦹♀️ GRRR I would've got away with it too, if it weren't for you meddling cryptographic algorithms! |
354 | 354 | expect(submitBadProof).toThrow('Signature provided is not valid') |
355 | 355 | }) |
| 356 | + |
| 357 | + it('an invited device needs access to all generations of user and team keys', () => { |
| 358 | + const { alice: aliceLaptop } = setup('alice') |
| 359 | + const alicePhone = aliceLaptop.phone! |
| 360 | + |
| 361 | + const changeKeys = () => { |
| 362 | + const newKeys = { type: KeyType.USER, name: aliceLaptop.userId } |
| 363 | + aliceLaptop.team.changeKeys(createKeyset(newKeys)) |
| 364 | + } |
| 365 | + |
| 366 | + // Alice rotates her keys two times |
| 367 | + changeKeys() |
| 368 | + changeKeys() |
| 369 | + |
| 370 | + // key rotation results in two new keys generations for team keys, admin keys and alice user keys |
| 371 | + expect(aliceLaptop.team.teamKeys().generation).toBe(2) |
| 372 | + expect(aliceLaptop.team.adminKeys().generation).toBe(2) |
| 373 | + expect(aliceLaptop.team.members(aliceLaptop.userId).keys.generation).toBe(2) |
| 374 | + expect(aliceLaptop.user.keys.generation).toBe(2) |
| 375 | + expect(Object.values(aliceLaptop.team.teamKeyring())).toHaveLength(3) |
| 376 | + expect(aliceLaptop.team.allUserKeys()).toHaveLength(3) |
| 377 | + // 3 times 3 generations of team keys, admin keys, alice user keys |
| 378 | + expect(aliceLaptop.team.state.lockboxes.length).toBe(9) |
| 379 | + |
| 380 | + // 💻 on her laptop, Alice generates an invitation for her phone |
| 381 | + const { seed } = aliceLaptop.team.inviteDevice() |
| 382 | + |
| 383 | + // 📱 Alice's phone uses the seed to generate her proof of invitation and sends it to the laptop |
| 384 | + const proofOfInvitation = generateProof(seed) |
| 385 | + |
| 386 | + // 💻 Alice's laptop verifies the proof |
| 387 | + aliceLaptop.team.admitDevice(proofOfInvitation, redactDevice(alicePhone)) |
| 388 | + |
| 389 | + // 👍 The proof was good, so the laptop sends the phone the team's graph and keyring |
| 390 | + const serializedGraph = aliceLaptop.team.save() |
| 391 | + const teamKeyring = aliceLaptop.team.teamKeyring() |
| 392 | + |
| 393 | + // 📱 Alice's phone needs to get her user keys. |
| 394 | + |
| 395 | + // Alice's laptop also sends all generations of user keys in encrypted lockboxes for each key, |
| 396 | + // which Alice's phone decrypts using her starter keys generated from the invitation seed. |
| 397 | + // Alice's phone needs every generation of user keys to unlock every generation of team keys so |
| 398 | + // the phone can decrypt the whole team graph using all the secret keys of the team keys generations. |
| 399 | + const { user: aliceUser, allUserKeys } = teams.getDeviceUserFromGraph({ |
| 400 | + serializedGraph, |
| 401 | + teamKeyring, |
| 402 | + invitationSeed: seed, |
| 403 | + }) |
| 404 | + |
| 405 | + // on invitation creation, Alice's laptop added 3 lockboxes to send 3 generations of user keys |
| 406 | + // to Alice's phone using the starter keys for encryption |
| 407 | + expect(aliceLaptop.team.state.lockboxes.length).toBe(12) |
| 408 | + |
| 409 | + const phoneTeam = new Team({ |
| 410 | + source: serializedGraph, |
| 411 | + context: { user: aliceUser, device: alicePhone }, |
| 412 | + teamKeyring |
| 413 | + }) |
| 414 | + phoneTeam.join(teamKeyring, allUserKeys) |
| 415 | + |
| 416 | + // ✅ Now Alice has 💻📱 two devices on the signature chain |
| 417 | + expect(phoneTeam.members(aliceLaptop.userId).devices).toHaveLength(2) |
| 418 | + expect(aliceLaptop.team.members(aliceLaptop.userId).devices).toHaveLength(2) |
| 419 | + |
| 420 | + // Alice's phone added 3 more lockboxes for 3 generations of user keys while joining, |
| 421 | + // this time using it's own secret device keys for encryption |
| 422 | + expect(phoneTeam.state.lockboxes.length).toBe(15) |
| 423 | + |
| 424 | + // Alice's phone has all user keys and team keys generations, the latest admin keys, |
| 425 | + // and the latest user keys generation stored on it's member object |
| 426 | + expect(Object.values(phoneTeam.teamKeyring())).toHaveLength(3) |
| 427 | + expect(phoneTeam.allUserKeys()).toHaveLength(3) |
| 428 | + expect(phoneTeam.adminKeys().generation).toBe(2) |
| 429 | + expect(phoneTeam.members(aliceLaptop.userId).keys.generation).toBe(2) |
| 430 | + |
| 431 | + const serializedPhoneTeam = phoneTeam.save() |
| 432 | + |
| 433 | + // In case some keys went missing while serializing and deserializing |
| 434 | + // the phone's local context, some required keys wouldn't be available to decrypt, |
| 435 | + // resulting in the error "Can't decrypt link: don't have the correct keyset" |
| 436 | + expect(() => |
| 437 | + new Team({ |
| 438 | + source: serializedPhoneTeam, |
| 439 | + context: { user: aliceUser, device: alicePhone }, |
| 440 | + teamKeyring: phoneTeam.teamKeyring() |
| 441 | + }) |
| 442 | + ).not.toThrow() |
| 443 | + }) |
356 | 444 | }) |
357 | 445 | }) |
358 | 446 | }) |
|
0 commit comments