Skip to content

Commit 1e8dd0f

Browse files
authored
feat(3057): Add lockbox service and create an invite lockbox on invite creation (#3060)
* Add lockbox service and create an invite lockbox on invite creation * Update changelog * Update .gitmodules * Update submodules to use feature branch * Use real salt * Update to use newer version of createLockbox method in lfa * Fix tests * Make rolename configurable when generating a lockbox * Use main auth branch * Remove unused imports * Forgot to add the correct self-assign roles to team create on this branch
1 parent 0269f91 commit 1e8dd0f

File tree

9 files changed

+126
-8
lines changed

9 files changed

+126
-8
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* Messages can now be relayed using QSS [#2805](https://github.com/TryQuiet/quiet/issues/2805)
1010
* Messages can be retrieved from QSS stores [#2806](https://github.com/TryQuiet/quiet/issues/2806)
1111
* Profile photos are now uploaded via IPFS [#3048](https://github.com/TryQuiet/quiet/issues/3048)
12+
* Create an invite lockbox when using QSS [#3057](https://github.com/TryQuiet/quiet/issues/3057)
1213
* Requests iOS notification permissions when app launches [#3079](https://github.com/TryQuiet/quiet/issues/3079)
1314

1415
### Fixes

packages/backend/src/nest/auth/services/crypto/crypto.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Handles invite-related chain operations
2+
* Handles crypto-related chain operations
33
*/
44
import * as bs58 from 'bs58'
55

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { SigChain } from '../../sigchain'
2+
import { createLogger } from '../../../common/logger'
3+
import { RoleName } from '../roles/roles'
4+
import { hash, randomBytes } from '@localfirst/crypto'
5+
import * as uint8arrays from 'uint8arrays'
6+
import { EncryptionScopeType, InviteLockboxMetadata } from './types'
7+
8+
const logger = createLogger('auth:services:lockbox.spec')
9+
10+
describe('lockbox', () => {
11+
let adminSigChain: SigChain
12+
let seed: string
13+
let salt: string
14+
let generatedKeys: InviteLockboxMetadata
15+
16+
it('should initialize a new sigchain and be admin', () => {
17+
adminSigChain = SigChain.create('test', 'user')
18+
expect(adminSigChain).toBeDefined()
19+
expect(adminSigChain.context).toBeDefined()
20+
expect(adminSigChain.team!.teamName).toBe('test')
21+
expect(adminSigChain.user.userName).toBe('user')
22+
expect(adminSigChain.roles.amIMemberOfRole(RoleName.ADMIN)).toBe(true)
23+
expect(adminSigChain.roles.amIMemberOfRole(RoleName.MEMBER)).toBe(true)
24+
})
25+
it('should create keys from seed and salt', () => {
26+
seed = uint8arrays.toString(randomBytes(32), 'hex')
27+
salt = uint8arrays.toString(randomBytes(32), 'hex')
28+
generatedKeys = adminSigChain.lockbox.generateLockboxKeys(seed, salt)
29+
expect(generatedKeys.id).toBe(hash(salt, seed))
30+
expect(generatedKeys.keys.name).toBe(generatedKeys.id)
31+
expect(generatedKeys.keys.generation).toBe(0)
32+
})
33+
it('should create a lockbox encrypted to our generated keys', () => {
34+
const lockboxes = adminSigChain.lockbox.createInviteLockboxes(seed, salt)
35+
expect(lockboxes).toHaveLength(1)
36+
const lockbox = lockboxes[0]
37+
expect(lockbox.recipient.name).toBe(generatedKeys.id)
38+
expect(lockbox.recipient.generation).toBe(0)
39+
expect(lockbox.contents.name).toBe(RoleName.MEMBER)
40+
expect(lockbox.contents.type).toBe(EncryptionScopeType.ROLE)
41+
42+
const keysFromLockbox = adminSigChain.team?.allKeys(generatedKeys.keys)
43+
expect(keysFromLockbox).toBeDefined()
44+
expect(keysFromLockbox!['ROLE'][RoleName.MEMBER].length).toBe(1)
45+
})
46+
})
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* Handles lockbox-related chain operations
3+
*/
4+
import { InviteLockboxMetadata } from './types'
5+
import { ChainServiceBase } from '../chainServiceBase'
6+
import { SigChain } from '../../sigchain'
7+
import { Lockbox, createKeyset } from '@localfirst/auth'
8+
import { createLogger } from '../../../common/logger'
9+
import { RoleName } from '../roles/roles'
10+
import { hash } from '@localfirst/crypto'
11+
12+
const logger = createLogger('auth:lockbox')
13+
14+
class LockboxService extends ChainServiceBase {
15+
constructor(sigChain: SigChain) {
16+
super(sigChain)
17+
}
18+
19+
/**
20+
* Generate a keyset from an invite seed for encrypting/decrypting a lockbox
21+
*
22+
* @param seed Invite seed generated by the owner (this is the string included in the invite link)
23+
* @param salt Random salt generated at the time of invite creation, used to create a key scope name
24+
* @returns Object containing the key scope name and the keys created using the seed
25+
*/
26+
public generateLockboxKeys(seed: string, salt: string): InviteLockboxMetadata {
27+
logger.debug('Generating keys from invite seed')
28+
const name = hash(salt, seed)
29+
const keys = createKeyset({ type: 'INVITE_LOCKBOX', name }, seed)
30+
return {
31+
id: name,
32+
keys,
33+
}
34+
}
35+
36+
/**
37+
* Create a lockbox containing role keys accessible using the invite seed
38+
*
39+
* @param seed Invite seed generated by the owner (this is the string included in the invite link)
40+
* @param salt Random salt generated at the time of invite creation, used to create a key scope name
41+
* @param roleName Role whose keys will be encrypted inside the lockbox (default = member)
42+
* @returns Lockbox containing role keys encrypted using the invite seed
43+
*/
44+
public createInviteLockboxes(seed: string, salt: string, roleName: string | RoleName = RoleName.MEMBER): Lockbox[] {
45+
logger.debug(`Creating lockbox containing ${roleName} role keys encrypted to invite-based keys`)
46+
if (this.sigChain.team == null) {
47+
throw new Error('Error while creating invite lockbox - No team')
48+
}
49+
if (!this.sigChain.roles.memberHasRole(this.sigChain.context.user.userId, roleName)) {
50+
throw new Error(`Error while creating invite lockbox - User is missing ${roleName} role`)
51+
}
52+
const inviteKeyset = this.generateLockboxKeys(seed, salt)
53+
return this.sigChain.team.createLockbox(roleName, inviteKeyset.keys)
54+
}
55+
}
56+
57+
export { LockboxService }

packages/backend/src/nest/auth/services/crypto/types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { KeyMetadata } from '@localfirst/crdx/'
2-
import { Base58 } from '@localfirst/auth'
2+
import { Base58, KeysetWithSecrets } from '@localfirst/auth'
33

44
export enum EncryptionScopeType {
55
ROLE = 'ROLE',
@@ -39,3 +39,8 @@ export type Signature = {
3939
signature: Base58
4040
author: KeyMetadata
4141
}
42+
43+
export type InviteLockboxMetadata = {
44+
id: string
45+
keys: KeysetWithSecrets
46+
}

packages/backend/src/nest/auth/sigchain.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { ServerService } from './services/members/server.service'
1212
import { RoleName } from './services/roles/roles'
1313
import { createLogger } from '../common/logger'
1414
import EventEmitter from 'events'
15+
import { LockboxService } from './services/crypto/lockbox.service'
1516

1617
const logger = createLogger('auth:sigchain')
1718

@@ -23,6 +24,7 @@ class SigChain extends EventEmitter {
2324
private _invites: InviteService | null = null
2425
private _crypto: CryptoService | null = null
2526
private _server: ServerService | null = null
27+
private _lockbox: LockboxService | null = null
2628

2729
private constructor(context: auth.MemberContext | auth.InviteeMemberContext) {
2830
super()
@@ -85,7 +87,7 @@ class SigChain extends EventEmitter {
8587
*/
8688
public static create(teamName: string, username: string, userId?: string): SigChain {
8789
const localUser = UserService.create(username, userId)
88-
const team: auth.Team = auth.createTeam(teamName, localUser)
90+
const team: auth.Team = auth.createTeam(teamName, localUser, undefined, { selfAssignableRoles: [RoleName.MEMBER] })
8991
const adminContext = {
9092
user: localUser.user,
9193
device: localUser.device,
@@ -158,6 +160,7 @@ class SigChain extends EventEmitter {
158160
this._invites = new InviteService(this)
159161
this._crypto = new CryptoService(this)
160162
this._server = new ServerService(this)
163+
this._lockbox = new LockboxService(this)
161164
}
162165

163166
public save(): Uint8Array {
@@ -191,6 +194,10 @@ class SigChain extends EventEmitter {
191194
return this._server!
192195
}
193196

197+
get lockbox(): LockboxService {
198+
return this._lockbox!
199+
}
200+
194201
static get lfa(): typeof auth {
195202
return auth
196203
}

packages/backend/src/nest/connections-manager/connections-manager.service.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,6 @@ import { privateKeyFromRaw } from '@libp2p/crypto/keys'
7070
import { SigChainService } from '../auth/sigchain.service'
7171
import { QSSService } from '../qss/qss.service'
7272
import { RoleName } from '../auth/services/roles/roles'
73-
import { SigChain } from '../auth/sigchain'
74-
import { QSSOperationResult, QSSEvents } from '../qss/qss.types'
7573

7674
/**
7775
* A monolith service that handles lots of events received from the state-manager.
@@ -721,7 +719,11 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI
721719
} else {
722720
try {
723721
const newInvite = this.sigChainService.getActiveChain().invites.createLongLivedUserInvite()
724-
722+
const qssInitStatus = await this.qssService.getQssInitStatus()
723+
// create the lockboxes using invite-based keys for users to self-assign the MEMBER role
724+
if (qssInitStatus.qssEnabled) {
725+
this.sigChainService.activeChain.lockbox.createInviteLockboxes(newInvite.seed, newInvite.salt)
726+
}
725727
await this.sigChainService.saveChain(this.sigChainService.activeChainTeamName)
726728
this.serverIoProvider.io.emit(SocketEvents.CREATED_LONG_LIVED_LFA_INVITE, newInvite)
727729
callback({ valid: false, newInvite })

0 commit comments

Comments
 (0)