diff --git a/3rd-party/auth b/3rd-party/auth index 7a055a9aa0..d7fafec5ab 160000 --- a/3rd-party/auth +++ b/3rd-party/auth @@ -1 +1 @@ -Subproject commit 7a055a9aa09d818c5bef4e28a64833ef032ac4f8 +Subproject commit d7fafec5ab122a5baece0ca7b38aedb18478beca diff --git a/3rd-party/qss b/3rd-party/qss index 0d6dd9d96b..2aaf17e8fe 160000 --- a/3rd-party/qss +++ b/3rd-party/qss @@ -1 +1 @@ -Subproject commit 0d6dd9d96bf7fe056bf96d9fbc85b5e50408871c +Subproject commit 2aaf17e8feafd28a31f1eb0c1bf070ea74e3a914 diff --git a/CHANGELOG.md b/CHANGELOG.md index 7044f53d17..e16d702468 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * Messages can now be relayed using QSS [#2805](https://github.com/TryQuiet/quiet/issues/2805) * Messages can be retrieved from QSS stores [#2806](https://github.com/TryQuiet/quiet/issues/2806) * Profile photos are now uploaded via IPFS [#3048](https://github.com/TryQuiet/quiet/issues/3048) +* Create an invite lockbox when using QSS [#3057](https://github.com/TryQuiet/quiet/issues/3057) * Requests iOS notification permissions when app launches [#3079](https://github.com/TryQuiet/quiet/issues/3079) ### Fixes diff --git a/packages/backend/src/nest/auth/services/crypto/crypto.service.ts b/packages/backend/src/nest/auth/services/crypto/crypto.service.ts index 99c5adfbde..35f2f589eb 100644 --- a/packages/backend/src/nest/auth/services/crypto/crypto.service.ts +++ b/packages/backend/src/nest/auth/services/crypto/crypto.service.ts @@ -1,5 +1,5 @@ /** - * Handles invite-related chain operations + * Handles crypto-related chain operations */ import * as bs58 from 'bs58' diff --git a/packages/backend/src/nest/auth/services/crypto/lockbox.service.spec.ts b/packages/backend/src/nest/auth/services/crypto/lockbox.service.spec.ts new file mode 100644 index 0000000000..31116ef0e8 --- /dev/null +++ b/packages/backend/src/nest/auth/services/crypto/lockbox.service.spec.ts @@ -0,0 +1,46 @@ +import { SigChain } from '../../sigchain' +import { createLogger } from '../../../common/logger' +import { RoleName } from '../roles/roles' +import { hash, randomBytes } from '@localfirst/crypto' +import * as uint8arrays from 'uint8arrays' +import { EncryptionScopeType, InviteLockboxMetadata } from './types' + +const logger = createLogger('auth:services:lockbox.spec') + +describe('lockbox', () => { + let adminSigChain: SigChain + let seed: string + let salt: string + let generatedKeys: InviteLockboxMetadata + + it('should initialize a new sigchain and be admin', () => { + adminSigChain = SigChain.create('test', 'user') + expect(adminSigChain).toBeDefined() + expect(adminSigChain.context).toBeDefined() + expect(adminSigChain.team!.teamName).toBe('test') + expect(adminSigChain.user.userName).toBe('user') + expect(adminSigChain.roles.amIMemberOfRole(RoleName.ADMIN)).toBe(true) + expect(adminSigChain.roles.amIMemberOfRole(RoleName.MEMBER)).toBe(true) + }) + it('should create keys from seed and salt', () => { + seed = uint8arrays.toString(randomBytes(32), 'hex') + salt = uint8arrays.toString(randomBytes(32), 'hex') + generatedKeys = adminSigChain.lockbox.generateLockboxKeys(seed, salt) + expect(generatedKeys.id).toBe(hash(salt, seed)) + expect(generatedKeys.keys.name).toBe(generatedKeys.id) + expect(generatedKeys.keys.generation).toBe(0) + }) + it('should create a lockbox encrypted to our generated keys', () => { + const lockboxes = adminSigChain.lockbox.createInviteLockboxes(seed, salt) + expect(lockboxes).toHaveLength(1) + const lockbox = lockboxes[0] + expect(lockbox.recipient.name).toBe(generatedKeys.id) + expect(lockbox.recipient.generation).toBe(0) + expect(lockbox.contents.name).toBe(RoleName.MEMBER) + expect(lockbox.contents.type).toBe(EncryptionScopeType.ROLE) + + const keysFromLockbox = adminSigChain.team?.allKeys(generatedKeys.keys) + expect(keysFromLockbox).toBeDefined() + expect(keysFromLockbox!['ROLE'][RoleName.MEMBER].length).toBe(1) + }) +}) diff --git a/packages/backend/src/nest/auth/services/crypto/lockbox.service.ts b/packages/backend/src/nest/auth/services/crypto/lockbox.service.ts new file mode 100644 index 0000000000..119527788f --- /dev/null +++ b/packages/backend/src/nest/auth/services/crypto/lockbox.service.ts @@ -0,0 +1,57 @@ +/** + * Handles lockbox-related chain operations + */ +import { InviteLockboxMetadata } from './types' +import { ChainServiceBase } from '../chainServiceBase' +import { SigChain } from '../../sigchain' +import { Lockbox, createKeyset } from '@localfirst/auth' +import { createLogger } from '../../../common/logger' +import { RoleName } from '../roles/roles' +import { hash } from '@localfirst/crypto' + +const logger = createLogger('auth:lockbox') + +class LockboxService extends ChainServiceBase { + constructor(sigChain: SigChain) { + super(sigChain) + } + + /** + * Generate a keyset from an invite seed for encrypting/decrypting a lockbox + * + * @param seed Invite seed generated by the owner (this is the string included in the invite link) + * @param salt Random salt generated at the time of invite creation, used to create a key scope name + * @returns Object containing the key scope name and the keys created using the seed + */ + public generateLockboxKeys(seed: string, salt: string): InviteLockboxMetadata { + logger.debug('Generating keys from invite seed') + const name = hash(salt, seed) + const keys = createKeyset({ type: 'INVITE_LOCKBOX', name }, seed) + return { + id: name, + keys, + } + } + + /** + * Create a lockbox containing role keys accessible using the invite seed + * + * @param seed Invite seed generated by the owner (this is the string included in the invite link) + * @param salt Random salt generated at the time of invite creation, used to create a key scope name + * @param roleName Role whose keys will be encrypted inside the lockbox (default = member) + * @returns Lockbox containing role keys encrypted using the invite seed + */ + public createInviteLockboxes(seed: string, salt: string, roleName: string | RoleName = RoleName.MEMBER): Lockbox[] { + logger.debug(`Creating lockbox containing ${roleName} role keys encrypted to invite-based keys`) + if (this.sigChain.team == null) { + throw new Error('Error while creating invite lockbox - No team') + } + if (!this.sigChain.roles.memberHasRole(this.sigChain.context.user.userId, roleName)) { + throw new Error(`Error while creating invite lockbox - User is missing ${roleName} role`) + } + const inviteKeyset = this.generateLockboxKeys(seed, salt) + return this.sigChain.team.createLockbox(roleName, inviteKeyset.keys) + } +} + +export { LockboxService } diff --git a/packages/backend/src/nest/auth/services/crypto/types.ts b/packages/backend/src/nest/auth/services/crypto/types.ts index a6cd08c12b..044093ddb8 100644 --- a/packages/backend/src/nest/auth/services/crypto/types.ts +++ b/packages/backend/src/nest/auth/services/crypto/types.ts @@ -1,5 +1,5 @@ import { KeyMetadata } from '@localfirst/crdx/' -import { Base58 } from '@localfirst/auth' +import { Base58, KeysetWithSecrets } from '@localfirst/auth' export enum EncryptionScopeType { ROLE = 'ROLE', @@ -39,3 +39,8 @@ export type Signature = { signature: Base58 author: KeyMetadata } + +export type InviteLockboxMetadata = { + id: string + keys: KeysetWithSecrets +} diff --git a/packages/backend/src/nest/auth/sigchain.ts b/packages/backend/src/nest/auth/sigchain.ts index 74a339c43b..261c2682f2 100644 --- a/packages/backend/src/nest/auth/sigchain.ts +++ b/packages/backend/src/nest/auth/sigchain.ts @@ -12,6 +12,7 @@ import { ServerService } from './services/members/server.service' import { RoleName } from './services/roles/roles' import { createLogger } from '../common/logger' import EventEmitter from 'events' +import { LockboxService } from './services/crypto/lockbox.service' const logger = createLogger('auth:sigchain') @@ -23,6 +24,7 @@ class SigChain extends EventEmitter { private _invites: InviteService | null = null private _crypto: CryptoService | null = null private _server: ServerService | null = null + private _lockbox: LockboxService | null = null private constructor(context: auth.MemberContext | auth.InviteeMemberContext) { super() @@ -85,7 +87,7 @@ class SigChain extends EventEmitter { */ public static create(teamName: string, username: string, userId?: string): SigChain { const localUser = UserService.create(username, userId) - const team: auth.Team = auth.createTeam(teamName, localUser) + const team: auth.Team = auth.createTeam(teamName, localUser, undefined, { selfAssignableRoles: [RoleName.MEMBER] }) const adminContext = { user: localUser.user, device: localUser.device, @@ -158,6 +160,7 @@ class SigChain extends EventEmitter { this._invites = new InviteService(this) this._crypto = new CryptoService(this) this._server = new ServerService(this) + this._lockbox = new LockboxService(this) } public save(): Uint8Array { @@ -191,6 +194,10 @@ class SigChain extends EventEmitter { return this._server! } + get lockbox(): LockboxService { + return this._lockbox! + } + static get lfa(): typeof auth { return auth } diff --git a/packages/backend/src/nest/connections-manager/connections-manager.service.ts b/packages/backend/src/nest/connections-manager/connections-manager.service.ts index 850d2da216..0ac0b01f9b 100644 --- a/packages/backend/src/nest/connections-manager/connections-manager.service.ts +++ b/packages/backend/src/nest/connections-manager/connections-manager.service.ts @@ -70,8 +70,6 @@ import { privateKeyFromRaw } from '@libp2p/crypto/keys' import { SigChainService } from '../auth/sigchain.service' import { QSSService } from '../qss/qss.service' import { RoleName } from '../auth/services/roles/roles' -import { SigChain } from '../auth/sigchain' -import { QSSOperationResult, QSSEvents } from '../qss/qss.types' /** * A monolith service that handles lots of events received from the state-manager. @@ -721,7 +719,11 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI } else { try { const newInvite = this.sigChainService.getActiveChain().invites.createLongLivedUserInvite() - + const qssInitStatus = await this.qssService.getQssInitStatus() + // create the lockboxes using invite-based keys for users to self-assign the MEMBER role + if (qssInitStatus.qssEnabled) { + this.sigChainService.activeChain.lockbox.createInviteLockboxes(newInvite.seed, newInvite.salt) + } await this.sigChainService.saveChain(this.sigChainService.activeChainTeamName) this.serverIoProvider.io.emit(SocketEvents.CREATED_LONG_LIVED_LFA_INVITE, newInvite) callback({ valid: false, newInvite })