Skip to content

Commit

Permalink
refactor to use userKeyring
Browse files Browse the repository at this point in the history
  • Loading branch information
HerbCaudill committed Sep 24, 2024
1 parent 781c364 commit ab91c61
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 98 deletions.
30 changes: 14 additions & 16 deletions packages/auth/src/connection/Connection.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable object-shorthand */
import { EventEmitter } from '@herbcaudill/eventemitter42'
import type { DecryptFnParams, KeysetWithSecrets } from '@localfirst/crdx'
import type { DecryptFnParams } from '@localfirst/crdx'
import {
generateMessage,
headsAreEqual,
Expand All @@ -22,9 +22,9 @@ import {
NEITHER_IS_MEMBER,
SERVER_REMOVED,
TIMEOUT,
UNHANDLED,
createErrorMessage,
type ConnectionErrorType,
UNHANDLED,
} from 'connection/errors.js'
import { getDeviceUserFromGraph } from 'connection/getDeviceUserFromGraph.js'
import * as identity from 'connection/identity.js'
Expand Down Expand Up @@ -214,27 +214,25 @@ export class Connection extends EventEmitter<ConnectionEvents> {
const { device, invitationSeed } = context
assert(invitationSeed)

let user = context.user
let allUserKeys: KeysetWithSecrets[] | undefined = undefined

// If we're joining as a new device for an existing member, we won't have a user object
// and all generations of existing user keys yet, so we need to get those from the graph.
// We use the invitation seed to generate the starter keys for the new device. We can use
// these to unlock the lockboxes on the team graph that contain our user keys.
if (!user) {
const userWithKeys = getDeviceUserFromGraph({ serializedGraph, teamKeyring, invitationSeed })
user = userWithKeys.user
allUserKeys = userWithKeys.allUserKeys
}

// If we're joining as a new device for an existing member, we won't have a user object or
// user keys yet, so we need to get those from the graph. We use the invitation seed to
// generate the starter keys for the new device. We can use these to unlock the lockboxes
// on the team graph that contain our user keys.
const { user, userKeyring } =
context.user === undefined
? getDeviceUserFromGraph({ serializedGraph, teamKeyring, invitationSeed })
: { user: context.user, userKeyring: undefined }

// When admitting us, our peer added our user to the team graph. We've been given the
// serialized and encrypted graph, and the team keyring. We can now decrypt the graph and
// reconstruct the team in order to join it.
const team = new Team({ source: serializedGraph, context: { user, device }, teamKeyring })

// We join the team, which adds our device to the team graph.
team.join(teamKeyring, allUserKeys)
team.join(teamKeyring, userKeyring)

this.emit('joined', { team, user, teamKeyring })

return { user, team }
}),

Expand Down
29 changes: 13 additions & 16 deletions packages/auth/src/connection/getDeviceUserFromGraph.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Keyring, KeysetWithSecrets, UserWithSecrets } from '@localfirst/crdx'
import { getLatestGeneration, type Keyring, type UserWithSecrets } from '@localfirst/crdx'
import { assert } from '@localfirst/shared'
import { generateProof } from 'invitation/generateProof.js'
import { generateStarterKeys } from 'invitation/generateStarterKeys.js'
Expand All @@ -11,9 +11,12 @@ const { USER } = KeyType
/**
* If we're joining as a new device for an existing member, we don't have a user object yet, so we
* need to get those from the graph. We use the invitation seed to generate the starter keys for the
* new device. We can use these to unlock the lockboxes on the team graph that contain our user keys.
* Because we need all user key generations to decrypt the team graph, we return all of them along
* with the user object that contains only the latest keys generation.
* new device. We can use these to unlock the lockboxes on the team graph that contain our user
* keys.
*
* Because we need all previous user keys to decrypt the team graph, we return a keyring containing
* the full history of user keys, along with a user object containing just the latest generation of
* keys.
*/
export const getDeviceUserFromGraph = ({
serializedGraph,
Expand All @@ -24,8 +27,8 @@ export const getDeviceUserFromGraph = ({
teamKeyring: Keyring
invitationSeed: string
}): {
user: UserWithSecrets,
allUserKeys: KeysetWithSecrets[]
user: UserWithSecrets
userKeyring: Keyring
} => {
const starterKeys = generateStarterKeys(invitationSeed)
const invitationId = generateProof(invitationSeed).id
Expand All @@ -37,15 +40,9 @@ export const getDeviceUserFromGraph = ({
const { userName } = select.member(state, userId)
assert(userName) // this user must exist in the team graph

const allUserKeys = select.keyMap(state, starterKeys)[USER]?.[userId]
const user = {
userName,
userId,
keys: allUserKeys.at(-1)
}
const userKeyring = select.keyring(state, { type: USER, name: userId }, starterKeys)
const keys = getLatestGeneration(userKeyring)
const user = { userName, userId, keys }

return {
user,
allUserKeys
}
return { user, userKeyring }
}
38 changes: 18 additions & 20 deletions packages/auth/src/team/Team.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
UserWithSecrets,
} from '@localfirst/crdx'
import {
createKeyring,
createKeyset,
createStore,
getLatestGeneration,
Expand Down Expand Up @@ -137,11 +138,11 @@ export class Team extends EventEmitter<TeamEvents> {
}

this.state = this.store.getState()
this.checkForNewUserKeysGeneration()
this.updateUserKeys()

// Wire up event listeners
this.on('updated', () => {
this.checkForNewUserKeysGeneration()
this.updateUserKeys()

// If we're admin, check for pending key rotations
this.checkForPendingKeyRotations()
Expand Down Expand Up @@ -505,21 +506,17 @@ export class Team extends EventEmitter<TeamEvents> {
const invitation = invitations.create({ seed, expiration, maxUses, userId: this.userId })

// In order for the invited device to be able to access the user's keys, we put the user keys in
// lockboxes that can be opened by an ephemeral keyset generated from the secret invitation
// seed.
// lockboxes that can be opened by an ephemeral keyset generated from the secret invitation seed.
const starterKeys = invitations.generateStarterKeys(seed)
const lockboxesUserKeysForDeviceStarterKeys = this.allUserKeys()
.map(keys => lockbox.create(keys, starterKeys))
const allUserKeys = Object.values(this.userKeyring())
const lockboxes = allUserKeys.map(keys => lockbox.create(keys, starterKeys))

const { id } = invitation

// Post invitation to graph
this.dispatch({
type: 'INVITE_DEVICE',
payload: {
invitation,
lockboxes: lockboxesUserKeysForDeviceStarterKeys,
},
payload: { invitation, lockboxes },
})

// Return the secret invitation seed (to pass on to invitee) and the invitation id (which could be used to revoke later)
Expand Down Expand Up @@ -570,8 +567,8 @@ export class Team extends EventEmitter<TeamEvents> {
const { id } = proof

// we know the team keys, so we can put them in lockboxes for the new member now (even if we're not an admin)
const lockboxesTeamKeysForMember = Object.values(this.teamKeyring())
.map(keys => lockbox.create(keys, memberKeys))
const allTeamKeys = Object.values(this.teamKeyring())
const lockboxes = allTeamKeys.map(keys => lockbox.create(keys, memberKeys))

// Post admission to the graph
this.dispatch({
Expand All @@ -580,7 +577,7 @@ export class Team extends EventEmitter<TeamEvents> {
id,
userName,
memberKeys: redactKeys(memberKeys),
lockboxes: lockboxesTeamKeysForMember,
lockboxes,
},
})
}
Expand Down Expand Up @@ -608,20 +605,21 @@ export class Team extends EventEmitter<TeamEvents> {
}

/** Once the new member has received the graph and can instantiate the team, they call this to add their device. */
public join = (teamKeyring: Keyring, allUserKeys = [this.context.user.keys]) => {
public join = (teamKeyring: Keyring, userKeyring = createKeyring(this.context.user.keys)) => {
assert(!this.isServer, "Can't join as member on server")

const { device } = this.context
const teamKeys = getLatestGeneration(teamKeyring)

const lockboxesUserKeysForDevice = allUserKeys.map(keys => lockbox.create(keys, device.keys))
// Create a lockbox for each generation of user keys
const lockboxes = Object.values(userKeyring).map(keys => lockbox.create(keys, device.keys))

this.dispatch(
{
type: 'ADD_DEVICE',
payload: {
device: redactDevice(device),
lockboxes: lockboxesUserKeysForDevice,
lockboxes,
},
},
teamKeys
Expand Down Expand Up @@ -767,8 +765,8 @@ export class Team extends EventEmitter<TeamEvents> {
public keys = (scope: KeyMetadata | KeyScope) =>
select.keys(this.state, this.context.device.keys, scope)

public allUserKeys = (userId = this.userId) =>
select.keyMap(this.state, this.context.device.keys)[USER]?.[userId] || []
public userKeyring = (userId = this.userId) =>
select.keyring(this.state, { type: USER, name: userId }, this.context.device.keys)

/** Returns the keys for the given role. */
public roleKeys = (roleName: string, generation?: number) =>
Expand Down Expand Up @@ -811,9 +809,9 @@ export class Team extends EventEmitter<TeamEvents> {
if (isForServer) device.keys = newKeys // (a server plays the role of both a user and a device)
}

private checkForNewUserKeysGeneration() {
private updateUserKeys() {
const { user } = this.context
const latestUserKeys = this.allUserKeys().at(-1)
const latestUserKeys = getLatestGeneration(this.userKeyring())

if (latestUserKeys && user.keys.generation < latestUserKeys.generation) {
user.keys = latestUserKeys
Expand Down
Loading

0 comments on commit ab91c61

Please sign in to comment.