diff --git a/CHANGELOG.md b/CHANGELOG.md index cc446e0e2f..898b478ae3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,11 @@ # Changelog -## [Unreleased] +## [unreleased] ### New features +* Adds basic sigchain functions ([#2625](https://github.com/TryQuiet/quiet/issues/2625)) +* Instantiates signature chain when creating communities and reloading application ([#2626](https://github.com/TryQuiet/quiet/issues/2626)) * Added in LFA-ready invite links ([#2627](https://github.com/TryQuiet/quiet/issues/2627)) ## [2.3.2] @@ -13,10 +15,6 @@ * Moved some responsibilities of identity management to the backend ([#2602](https://github.com/TryQuiet/quiet/issues/2602)) * Added auth submodule in preparation for future encyrption work ([#2623](https://github.com/TryQuiet/quiet/issues/2623)) -### New features - -* Adds basic sigchain functions ([#2649](https://github.com/TryQuiet/quiet/pull/2649)) - ### Fixes * Fixed memory leak associated with autoUpdater ([#2606](https://github.com/TryQuiet/quiet/issues/2606)) 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 84cb39afcd..0e84d1c988 100644 --- a/packages/backend/src/nest/auth/services/crypto/crypto.service.ts +++ b/packages/backend/src/nest/auth/services/crypto/crypto.service.ts @@ -6,17 +6,8 @@ import * as bs58 from 'bs58' import { EncryptedAndSignedPayload, EncryptedPayload, EncryptionScope, EncryptionScopeType } from './types' import { ChainServiceBase } from '../chainServiceBase' import { SigChain } from '../../sigchain' -import { - asymmetric, - Base58, - Keyset, - KeysetWithSecrets, - LocalUserContext, - Member, - SignedEnvelope, -} from '@localfirst/auth' +import { asymmetric, Base58, Keyset, LocalUserContext, Member, SignedEnvelope } from '@localfirst/auth' import { DEFAULT_SEARCH_OPTIONS, MemberSearchOptions } from '../members/types' -import { ChannelService } from '../roles/channel.service' import { createLogger } from '../../../common/logger' const logger = createLogger('auth:cryptoService') diff --git a/packages/backend/src/nest/auth/services/invites/invite.service.spec.ts b/packages/backend/src/nest/auth/services/invites/invite.service.spec.ts index f98c39f3c8..0b5cbe940a 100644 --- a/packages/backend/src/nest/auth/services/invites/invite.service.spec.ts +++ b/packages/backend/src/nest/auth/services/invites/invite.service.spec.ts @@ -1,6 +1,6 @@ import { jest } from '@jest/globals' import { SigChain } from '../../sigchain' -import { SigChainManager } from '../../sigchainManager' +import { SigChainService } from '../../sigchain.service' import { createLogger } from '../../../common/logger' import { device, InviteResult, LocalUserContext } from '@localfirst/auth' import { RoleName } from '..//roles/roles' @@ -12,17 +12,15 @@ const logger = createLogger('auth:services:invite.spec') describe('invites', () => { let adminSigChain: SigChain - let adminContext: LocalUserContext let newMemberSigChain: SigChain - let newMemberContext: LocalUserContext it('should initialize a new sigchain and be admin', () => { - ;({ sigChain: adminSigChain, context: adminContext } = SigChain.create('test', 'user')) + adminSigChain = SigChain.create('test', 'user') expect(adminSigChain).toBeDefined() - expect(adminContext).toBeDefined() + expect(adminSigChain.context).toBeDefined() expect(adminSigChain.team.teamName).toBe('test') - expect(adminContext.user.userName).toBe('user') - expect(adminSigChain.roles.amIMemberOfRole(adminContext, RoleName.ADMIN)).toBe(true) - expect(adminSigChain.roles.amIMemberOfRole(adminContext, RoleName.MEMBER)).toBe(true) + expect(adminSigChain.context.user.userName).toBe('user') + expect(adminSigChain.roles.amIMemberOfRole(adminSigChain.context, RoleName.ADMIN)).toBe(true) + expect(adminSigChain.roles.amIMemberOfRole(adminSigChain.context, RoleName.MEMBER)).toBe(true) }) it('admin should generate an invite and it be added to team graph', () => { const newInvite = adminSigChain.invites.createUserInvite() @@ -38,26 +36,26 @@ describe('invites', () => { expect(inviteProof).toBeDefined() expect(adminSigChain.invites.validateProof(inviteProof)).toBe(true) expect(prospectiveMember).toBeDefined() - ;({ sigChain: newMemberSigChain, context: newMemberContext } = SigChain.join( + newMemberSigChain = SigChain.join( prospectiveMember.context, adminSigChain.team.save(), adminSigChain.team.teamKeyring() - )) + ) expect(newMemberSigChain).toBeDefined() - expect(newMemberContext).toBeDefined() - expect(newMemberContext.user.userName).toBe('user2') - expect(newMemberContext.user.userId).not.toBe(adminContext.user.userId) - expect(newMemberSigChain.roles.amIMemberOfRole(newMemberContext, RoleName.MEMBER)).toBe(false) - expect(newMemberSigChain.roles.amIMemberOfRole(newMemberContext, RoleName.ADMIN)).toBe(false) + expect(newMemberSigChain.context).toBeDefined() + expect(newMemberSigChain.context.user.userName).toBe('user2') + expect(newMemberSigChain.context.user.userId).not.toBe(adminSigChain.context.user.userId) + expect(newMemberSigChain.roles.amIMemberOfRole(newMemberSigChain.context, RoleName.MEMBER)).toBe(false) + expect(newMemberSigChain.roles.amIMemberOfRole(newMemberSigChain.context, RoleName.ADMIN)).toBe(false) expect( adminSigChain.invites.admitMemberFromInvite( inviteProof, - newMemberContext.user.userName, - newMemberContext.user.userId, - newMemberContext.user.keys + newMemberSigChain.context.user.userName, + newMemberSigChain.context.user.userId, + newMemberSigChain.context.user.keys ) ).toBeDefined() - expect(adminSigChain.roles.amIMemberOfRole(newMemberContext, RoleName.MEMBER)).toBe(true) + expect(adminSigChain.roles.amIMemberOfRole(newMemberSigChain.context, RoleName.MEMBER)).toBe(true) }) it('admin should be able to revoke an invite', () => { const inviteToRevoke = adminSigChain.invites.createUserInvite() @@ -88,7 +86,7 @@ describe('invites', () => { }).toThrowError() }) it('should invite device', () => { - const newDevice = DeviceService.generateDeviceForUser(adminContext.user.userId) + const newDevice = DeviceService.generateDeviceForUser(adminSigChain.context.user.userId) const deviceInvite = adminSigChain.invites.createDeviceInvite() const inviteProof = InviteService.generateProof(deviceInvite.seed) expect(inviteProof).toBeDefined() diff --git a/packages/backend/src/nest/auth/services/members/device.service.spec.ts b/packages/backend/src/nest/auth/services/members/device.service.spec.ts index df8a731291..8b917e9d8d 100644 --- a/packages/backend/src/nest/auth/services/members/device.service.spec.ts +++ b/packages/backend/src/nest/auth/services/members/device.service.spec.ts @@ -8,24 +8,23 @@ const logger = createLogger('auth:services:device.spec') describe('invites', () => { let adminSigChain: SigChain - let adminContext: LocalUserContext let newDevice: DeviceWithSecrets it('should initialize a new sigchain and be admin', () => { - ;({ sigChain: adminSigChain, context: adminContext } = SigChain.create('test', 'user')) + adminSigChain = SigChain.create('test', 'user') expect(adminSigChain).toBeDefined() - expect(adminContext).toBeDefined() + expect(adminSigChain.context).toBeDefined() expect(adminSigChain.team.teamName).toBe('test') - expect(adminContext.user.userName).toBe('user') - expect(adminSigChain.roles.amIMemberOfRole(adminContext, RoleName.ADMIN)).toBe(true) - expect(adminSigChain.roles.amIMemberOfRole(adminContext, RoleName.MEMBER)).toBe(true) + expect(adminSigChain.context.user.userName).toBe('user') + expect(adminSigChain.roles.amIMemberOfRole(adminSigChain.context, RoleName.ADMIN)).toBe(true) + expect(adminSigChain.roles.amIMemberOfRole(adminSigChain.context, RoleName.MEMBER)).toBe(true) }) it('sigchain should contain admin device', () => { const adminDeviceName = DeviceService.determineDeviceName() - adminSigChain.team.hasDevice(adminContext.device.deviceId) + adminSigChain.team.hasDevice(adminSigChain.context.device.deviceId) }) it('should generate a new device', () => { - newDevice = DeviceService.generateDeviceForUser(adminContext.user.userId) + newDevice = DeviceService.generateDeviceForUser(adminSigChain.context.user.userId) expect(newDevice).toBeDefined() }) it('should redactDevice', () => { diff --git a/packages/backend/src/nest/auth/services/members/user.service.spec.ts b/packages/backend/src/nest/auth/services/members/user.service.spec.ts index fc1729c0da..cf11f971b1 100644 --- a/packages/backend/src/nest/auth/services/members/user.service.spec.ts +++ b/packages/backend/src/nest/auth/services/members/user.service.spec.ts @@ -1,6 +1,6 @@ import { jest } from '@jest/globals' import { SigChain } from '../../sigchain' -import { SigChainManager } from '../../sigchainManager' +import { SigChainService } from '../../sigchain.service' import { createLogger } from '../../../common/logger' import { device, InviteResult, LocalUserContext } from '@localfirst/auth' import { RoleName } from '..//roles/roles' @@ -11,15 +11,15 @@ const logger = createLogger('auth:services:invite.spec') describe('invites', () => { let adminSigChain: SigChain - let adminContext: LocalUserContext + it('should initialize a new sigchain and be admin', () => { - ;({ sigChain: adminSigChain, context: adminContext } = SigChain.create('test', 'user')) + adminSigChain = SigChain.create('test', 'user') expect(adminSigChain).toBeDefined() - expect(adminContext).toBeDefined() + expect(adminSigChain.context).toBeDefined() expect(adminSigChain.team.teamName).toBe('test') - expect(adminContext.user.userName).toBe('user') - expect(adminSigChain.roles.amIMemberOfRole(adminContext, RoleName.ADMIN)).toBe(true) - expect(adminSigChain.roles.amIMemberOfRole(adminContext, RoleName.MEMBER)).toBe(true) + expect(adminSigChain.context.user.userName).toBe('user') + expect(adminSigChain.roles.amIMemberOfRole(adminSigChain.context, RoleName.ADMIN)).toBe(true) + expect(adminSigChain.roles.amIMemberOfRole(adminSigChain.context, RoleName.MEMBER)).toBe(true) }) it('should get keys', () => { const keys = adminSigChain.users.getKeys() @@ -30,17 +30,17 @@ describe('invites', () => { expect(users).toBeDefined() }) it('get admin member by id', () => { - const users = adminSigChain.users.getUsersById([adminContext.user.userId]) - expect(users.map(u => u.userId)).toContain(adminContext.user.userId) + const users = adminSigChain.users.getUsersById([adminSigChain.context.user.userId]) + expect(users.map(u => u.userId)).toContain(adminSigChain.context.user.userId) }) it('get admin member by name', () => { - const user = adminSigChain.users.getUserByName(adminContext.user.userName) - expect(user!.userName).toEqual(adminContext.user.userName) + const user = adminSigChain.users.getUserByName(adminSigChain.context.user.userName) + expect(user!.userName).toEqual(adminSigChain.context.user.userName) }) it('should redact user', () => { - const redactedUser = UserService.redactUser(adminContext.user) + const redactedUser = UserService.redactUser(adminSigChain.context.user) expect(redactedUser).toBeDefined() - expect(redactedUser.userId).toBe(adminContext.user.userId) - expect(redactedUser.userName).toBe(adminContext.user.userName) + expect(redactedUser.userId).toBe(adminSigChain.context.user.userId) + expect(redactedUser.userName).toBe(adminSigChain.context.user.userName) }) }) diff --git a/packages/backend/src/nest/auth/services/roles/channel.service.spec.ts b/packages/backend/src/nest/auth/services/roles/channel.service.spec.ts index fd04454299..3da5a1a312 100644 --- a/packages/backend/src/nest/auth/services/roles/channel.service.spec.ts +++ b/packages/backend/src/nest/auth/services/roles/channel.service.spec.ts @@ -11,21 +11,19 @@ const privateChannelName = 'testChannel' describe('invites', () => { let adminSigChain: SigChain - let adminContext: LocalUserContext let newMemberSigChain: SigChain - let newMemberContext: LocalUserContext it('should initialize a new sigchain and be admin', () => { - ;({ sigChain: adminSigChain, context: adminContext } = SigChain.create('test', 'user')) + adminSigChain = SigChain.create('test', 'user') expect(adminSigChain).toBeDefined() - expect(adminContext).toBeDefined() + expect(adminSigChain.context).toBeDefined() expect(adminSigChain.team.teamName).toBe('test') - expect(adminContext.user.userName).toBe('user') - expect(adminSigChain.roles.amIMemberOfRole(adminContext, RoleName.ADMIN)).toBe(true) - expect(adminSigChain.roles.amIMemberOfRole(adminContext, RoleName.MEMBER)).toBe(true) + expect(adminSigChain.context.user.userName).toBe('user') + expect(adminSigChain.roles.amIMemberOfRole(adminSigChain.context, RoleName.ADMIN)).toBe(true) + expect(adminSigChain.roles.amIMemberOfRole(adminSigChain.context, RoleName.MEMBER)).toBe(true) }) it('should create a private channel', () => { - const privateChannel = adminSigChain.channels.createPrivateChannel(privateChannelName, adminContext) + const privateChannel = adminSigChain.channels.createPrivateChannel(privateChannelName, adminSigChain.context) expect(privateChannel).toBeDefined() }) it('admin should generate an invite seed and admit a new user from it', () => { @@ -36,49 +34,56 @@ describe('invites', () => { expect(inviteProof).toBeDefined() expect(adminSigChain.invites.validateProof(inviteProof)).toBe(true) expect(prospectiveMember).toBeDefined() - ;({ sigChain: newMemberSigChain, context: newMemberContext } = SigChain.join( + newMemberSigChain = SigChain.join( prospectiveMember.context, adminSigChain.team.save(), adminSigChain.team.teamKeyring() - )) + ) expect(newMemberSigChain).toBeDefined() - expect(newMemberContext).toBeDefined() - expect(newMemberContext.user.userName).toBe('user2') - expect(newMemberContext.user.userId).not.toBe(adminContext.user.userId) - expect(newMemberSigChain.roles.amIMemberOfRole(newMemberContext, RoleName.MEMBER)).toBe(false) - expect(newMemberSigChain.roles.amIMemberOfRole(newMemberContext, RoleName.ADMIN)).toBe(false) + expect(newMemberSigChain.context).toBeDefined() + expect(newMemberSigChain.context.user.userName).toBe('user2') + expect(newMemberSigChain.context.user.userId).not.toBe(adminSigChain.context.user.userId) + expect(newMemberSigChain.roles.amIMemberOfRole(newMemberSigChain.context, RoleName.MEMBER)).toBe(false) + expect(newMemberSigChain.roles.amIMemberOfRole(newMemberSigChain.context, RoleName.ADMIN)).toBe(false) expect( adminSigChain.invites.admitMemberFromInvite( inviteProof, - newMemberContext.user.userName, - newMemberContext.user.userId, - newMemberContext.user.keys + newMemberSigChain.context.user.userName, + newMemberSigChain.context.user.userId, + newMemberSigChain.context.user.keys ) ).toBeDefined() - expect(adminSigChain.roles.amIMemberOfRole(newMemberContext, RoleName.MEMBER)).toBe(true) + expect(adminSigChain.roles.amIMemberOfRole(newMemberSigChain.context, RoleName.MEMBER)).toBe(true) }) it('should add the new member to the private channel', () => { - const privateChannel = adminSigChain.channels.getChannel(privateChannelName, adminContext) - adminSigChain.channels.addMemberToPrivateChannel(newMemberContext.user.userId, privateChannel.channelName) - expect(adminSigChain.channels.memberInChannel(newMemberContext.user.userId, privateChannel.channelName)).toBe(true) + const privateChannel = adminSigChain.channels.getChannel(privateChannelName, adminSigChain.context) + adminSigChain.channels.addMemberToPrivateChannel(newMemberSigChain.context.user.userId, privateChannel.channelName) + expect( + adminSigChain.channels.memberInChannel(newMemberSigChain.context.user.userId, privateChannel.channelName) + ).toBe(true) }) it('should remove the new member from the private channel', () => { - const privateChannel = adminSigChain.channels.getChannel(privateChannelName, adminContext) - adminSigChain.channels.revokePrivateChannelMembership(newMemberContext.user.userId, privateChannel.channelName) - expect(adminSigChain.channels.getChannels(newMemberContext, true).length).toBe(0) - expect(adminSigChain.channels.memberInChannel(newMemberContext.user.userId, privateChannel.channelName)).toBe(false) + const privateChannel = adminSigChain.channels.getChannel(privateChannelName, adminSigChain.context) + adminSigChain.channels.revokePrivateChannelMembership( + newMemberSigChain.context.user.userId, + privateChannel.channelName + ) + expect(adminSigChain.channels.getChannels(newMemberSigChain.context, true).length).toBe(0) + expect( + adminSigChain.channels.memberInChannel(newMemberSigChain.context.user.userId, privateChannel.channelName) + ).toBe(false) }) it('should delete channel', () => { - const privateChannel = adminSigChain.channels.getChannel(privateChannelName, adminContext) + const privateChannel = adminSigChain.channels.getChannel(privateChannelName, adminSigChain.context) adminSigChain.channels.deletePrivateChannel(privateChannel.channelName) - expect(adminSigChain.channels.getChannels(adminContext).length).toBe(0) + expect(adminSigChain.channels.getChannels(adminSigChain.context).length).toBe(0) }) it('should create new channel and then leave it', () => { - const channel = adminSigChain.channels.createPrivateChannel(privateChannelName, adminContext) + const channel = adminSigChain.channels.createPrivateChannel(privateChannelName, adminSigChain.context) expect(channel).toBeDefined() - adminSigChain.channels.leaveChannel(channel.channelName, adminContext) - expect(adminSigChain.channels.memberInChannel(adminContext.user.userId, channel.channelName)).toBe(false) - expect(adminSigChain.channels.getChannels(adminContext).length).toBe(1) - expect(adminSigChain.channels.getChannels(adminContext, true).length).toBe(0) + adminSigChain.channels.leaveChannel(channel.channelName, adminSigChain.context) + expect(adminSigChain.channels.memberInChannel(adminSigChain.context.user.userId, channel.channelName)).toBe(false) + expect(adminSigChain.channels.getChannels(adminSigChain.context).length).toBe(1) + expect(adminSigChain.channels.getChannels(adminSigChain.context, true).length).toBe(0) }) }) diff --git a/packages/backend/src/nest/auth/sigchain.service.module.ts b/packages/backend/src/nest/auth/sigchain.service.module.ts new file mode 100644 index 0000000000..7ed9cc0315 --- /dev/null +++ b/packages/backend/src/nest/auth/sigchain.service.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common' +import { SigChainService } from './sigchain.service' +import { LocalDbModule } from '../local-db/local-db.module' + +@Module({ + providers: [SigChainService], + exports: [SigChainService], + imports: [LocalDbModule], +}) +export class SigChainModule {} diff --git a/packages/backend/src/nest/auth/sigchain.service.spec.ts b/packages/backend/src/nest/auth/sigchain.service.spec.ts new file mode 100644 index 0000000000..be44511164 --- /dev/null +++ b/packages/backend/src/nest/auth/sigchain.service.spec.ts @@ -0,0 +1,93 @@ +import { jest } from '@jest/globals' +import { Test, TestingModule } from '@nestjs/testing' +import { SigChainService } from './sigchain.service' +import { createLogger } from '../common/logger' +import { LocalDbService } from '../local-db/local-db.service' +import { LocalDbModule } from '../local-db/local-db.module' +import { TestModule } from '../common/test.module' +import { SigChainModule } from './sigchain.service.module' + +const logger = createLogger('auth:sigchainManager.spec') + +describe('SigChainManager', () => { + let module: TestingModule + let sigChainManager: SigChainService + let localDbService: LocalDbService + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [TestModule, SigChainModule, LocalDbModule], + }).compile() + sigChainManager = await module.resolve(SigChainService) + localDbService = await module.resolve(LocalDbService) + }) + + beforeEach(async () => { + if (localDbService.getStatus() === 'closed') { + await localDbService.open() + } + }) + + afterAll(async () => { + await localDbService.close() + await module.close() + }) + + it('should throw an error when trying to get an active chain without setting one', async () => { + expect(() => sigChainManager.getActiveChain()).toThrowError() + }) + it('should throw an error when trying to set an active chain that does not exist', async () => { + expect(() => sigChainManager.setActiveChain('nonexistent')).toThrowError() + }) + it('should add a new chain and it not be active if not set to be', async () => { + const sigChain = await sigChainManager.createChain('test', 'user', false) + expect(() => sigChainManager.getActiveChain()).toThrowError() + sigChainManager.setActiveChain('test') + expect(sigChainManager.getActiveChain()).toBe(sigChain) + }) + it('should add a new chain and it be active if set to be', async () => { + const sigChain = await sigChainManager.createChain('test2', 'user2', true) + expect(sigChainManager.getActiveChain()).toBe(sigChain) + const prevSigChain = sigChainManager.getChain('test') + expect(prevSigChain).toBeDefined() + expect(prevSigChain).not.toBe(sigChain) + }) + it('should delete nonactive chain without changing active chain', async () => { + sigChainManager.setActiveChain('test2') + await sigChainManager.deleteChain('test', false) + expect(() => sigChainManager.getChain('test')).toThrowError() + expect(sigChainManager.getActiveChain()).toBeDefined() + }) + it('should delete active chain and set active chain to undefined', async () => { + await sigChainManager.deleteChain('test2', false) + expect(sigChainManager.getActiveChain).toThrowError() + }) + it('should save and load sigchain using nestjs service', async () => { + const TEAM_NAME = 'test3' + const sigChain = await sigChainManager.createChain(TEAM_NAME, 'user', true) + await sigChainManager.saveChain(TEAM_NAME) + await sigChainManager.deleteChain(TEAM_NAME, false) + const loadedSigChain = await sigChainManager.loadChain(TEAM_NAME, true) + expect(loadedSigChain).toBeDefined() + expect(sigChainManager.getActiveChain()).toBe(loadedSigChain) + }) + it('should delete sigchains from disk', async () => { + await sigChainManager.deleteChain('test3', true) + expect(() => sigChainManager.getChain('test3')).toThrowError() + await expect(sigChainManager.loadChain('test3', true)).rejects.toThrowError() + }) + it('should not allow duplicate chains to be added', async () => { + await sigChainManager.createChain('test4', 'user4', false) + await expect(sigChainManager.createChain('test4', 'user4', false)).rejects.toThrowError() + }) + it('should handle concurrent chain operations correctly', async () => { + const TEAM_NAME1 = 'test6' + const TEAM_NAME2 = 'test7' + await Promise.all([ + sigChainManager.createChain(TEAM_NAME1, 'user1', true), + sigChainManager.createChain(TEAM_NAME2, 'user2', false), + ]) + expect(sigChainManager.getChain(TEAM_NAME1)).toBeDefined() + expect(sigChainManager.getChain(TEAM_NAME2)).toBeDefined() + }) +}) diff --git a/packages/backend/src/nest/auth/sigchain.service.ts b/packages/backend/src/nest/auth/sigchain.service.ts new file mode 100644 index 0000000000..e93c6bd1b6 --- /dev/null +++ b/packages/backend/src/nest/auth/sigchain.service.ts @@ -0,0 +1,158 @@ +import { Injectable, OnModuleInit } from '@nestjs/common' +import { SigChain } from './sigchain' +import { Keyring, LocalUserContext } from '3rd-party/auth/packages/auth/dist' +import { LocalDbService } from '../local-db/local-db.service' +import { createLogger } from '../common/logger' + +@Injectable() +export class SigChainService implements OnModuleInit { + public activeChainTeamName: string | undefined + private readonly logger = createLogger(SigChainService.name) + private chains: Map = new Map() + private static _instance: SigChainService | undefined + + constructor(private readonly localDbService: LocalDbService) {} + + onModuleInit() { + if (SigChainService._instance) { + throw new Error('SigChainManagerService already initialized!') + } + SigChainService._instance = this + } + + getActiveChain(): SigChain { + if (!this.activeChainTeamName) { + throw new Error('No active chain found!') + } + return this.getChain(this.activeChainTeamName) + } + + /** + * Gets a chain by team name + * @param teamName Name of the team to get the chain for + * @returns The chain for the team + * @throws Error if the chain doesn't exist + */ + getChain(teamName: string): SigChain { + if (!this.chains.has(teamName)) { + throw new Error(`No chain found for team ${teamName}`) + } + return this.chains.get(teamName)! + } + + static get instance(): SigChainService { + if (!SigChainService._instance) { + throw new Error("SigChainManagerService hasn't been initialized yet! Run init() before accessing") + } + return SigChainService._instance + } + + setActiveChain(teamName: string): void { + if (!this.chains.has(teamName)) { + throw new Error(`No chain found for team ${teamName}, can't set to active!`) + } + this.activeChainTeamName = teamName + } + + /** + * Adds a chain to the service + * @param chain SigChain to add + * @param setActive Whether to set the chain as active + * @returns Whether the chain was set as active + */ + addChain(chain: SigChain, setActive: boolean): boolean { + if (this.chains.has(chain.team.teamName)) { + throw new Error(`Chain for team ${chain.team.teamName} already exists`) + } + this.chains.set(chain.team.teamName, chain) + if (setActive) { + this.setActiveChain(chain.team.teamName) + return true + } + return false + } + + /** + * Deletes a chain from the service + * @param teamName Name of the team to delete + * @param fromDisk Whether to delete the chain from disk as well + */ + async deleteChain(teamName: string, fromDisk: boolean): Promise { + if (fromDisk) { + this.localDbService.deleteSigChain(teamName) + } + this.chains.delete(teamName) + if (this.activeChainTeamName === teamName) { + this.activeChainTeamName = undefined + } + } + + /** + * Creates a new chain and adds it to the service + * @param teamName Name of the team to create + * @param username Name of the user to create + * @param setActive Whether to set the chain as active + * @returns The created chain + */ + async createChain(teamName: string, username: string, setActive: boolean): Promise { + if (this.chains.has(teamName)) { + throw new Error(`Chain for team ${teamName} already exists`) + } + const sigChain = SigChain.create(teamName, username) + this.addChain(sigChain, setActive) + return sigChain + } + + /** + * Deserializes a chain and adds it to the service + * @param serializedTeam Serialized chain to deserialize + * @param context User context to use for the chain + * @param teamKeyRing Keyring to use for the chain + * @param setActive Whether to set the chain as active + * @returns The SigChain instance created from the serialized chain + */ + private async deserialize( + serializedTeam: Uint8Array, + context: LocalUserContext, + teamKeyRing: Keyring, + setActive: boolean + ): Promise { + this.logger.info('Deserializing chain') + const sigChain = SigChain.load(serializedTeam, context, teamKeyRing) + this.addChain(sigChain, setActive) + return sigChain + } + + /* LevelDB methods */ + + /** + * Loads a chain from disk and adds it to the service + * @param teamName Name of the team to load + * @param setActive Whether to set the chain as active + * @returns The SigChain instance loaded from disk + * @throws Error if the chain doesn't exist + */ + async loadChain(teamName: string, setActive: boolean): Promise { + if (this.localDbService.getStatus() !== 'open') { + this.localDbService.open() + } + this.logger.info(`Loading chain for team ${teamName}`) + const chain = await this.localDbService.getSigChain(teamName) + if (!chain) { + throw new Error(`Chain for team ${teamName} not found`) + } + return await this.deserialize(chain.serializedTeam, chain.context, chain.teamKeyRing, setActive) + } + + /** + * Saves a chain to disk + * @param teamName Name of the team to save + */ + async saveChain(teamName: string): Promise { + if (this.localDbService.getStatus() !== 'open') { + this.localDbService.open() + } + const chain = await this.getChain(teamName) + await this.localDbService.setSigChain(chain) + } +} diff --git a/packages/backend/src/nest/auth/sigchain.spec.ts b/packages/backend/src/nest/auth/sigchain.spec.ts index 59ca5c0e44..2449c8de49 100644 --- a/packages/backend/src/nest/auth/sigchain.spec.ts +++ b/packages/backend/src/nest/auth/sigchain.spec.ts @@ -1,6 +1,6 @@ import { jest } from '@jest/globals' import { SigChain } from './sigchain' -import { SigChainManager } from './sigchainManager' +import { SigChainService } from './sigchain.service' import { createLogger } from '../common/logger' import { LocalUserContext } from '3rd-party/auth/packages/auth/dist' import exp from 'constants' @@ -12,19 +12,25 @@ const logger = createLogger('auth:sigchainManager.spec') describe('SigChain', () => { let sigChain: SigChain let sigChain2: SigChain - let context: LocalUserContext - let context2: LocalUserContext it('should initialize a new sigchain and be admin', () => { - ;({ sigChain, context } = SigChain.create('test', 'user')) + sigChain = SigChain.create('test', 'user') expect(sigChain).toBeDefined() - expect(context).toBeDefined() + expect(sigChain.context).toBeDefined() expect(sigChain.team.teamName).toBe('test') - expect(context.user.userName).toBe('user') - expect(sigChain.roles.amIMemberOfRole(context, RoleName.ADMIN)).toBe(true) - expect(sigChain.roles.amIMemberOfRole(context, RoleName.MEMBER)).toBe(true) + expect(sigChain.context.user.userName).toBe('user') + expect(sigChain.roles.amIMemberOfRole(sigChain.context, RoleName.ADMIN)).toBe(true) + expect(sigChain.roles.amIMemberOfRole(sigChain.context, RoleName.MEMBER)).toBe(true) }) it('admin should not have a role that does not exist', () => { - expect(sigChain.roles.amIMemberOfRole(context, 'nonexistent')).toBe(false) + expect(sigChain.roles.amIMemberOfRole(sigChain.context, 'nonexistent')).toBe(false) + }) + it('should serialize the sigchain and load it', () => { + const serializedChain = sigChain.save() + sigChain2 = SigChain.load(serializedChain, sigChain.context, sigChain.team.teamKeyring()) + expect(sigChain2).toBeDefined() + expect(sigChain2.team.teamName).toBe('test') + expect(sigChain2.roles.amIMemberOfRole(sigChain2.context, RoleName.ADMIN)).toBe(true) + expect(sigChain2.roles.amIMemberOfRole(sigChain2.context, RoleName.MEMBER)).toBe(true) }) }) diff --git a/packages/backend/src/nest/auth/sigchain.ts b/packages/backend/src/nest/auth/sigchain.ts index 6e7d22098f..f21c88069d 100644 --- a/packages/backend/src/nest/auth/sigchain.ts +++ b/packages/backend/src/nest/auth/sigchain.ts @@ -3,7 +3,6 @@ */ import * as auth from '@localfirst/auth' -import { LoadedSigChain } from './types' import { UserService } from './services/members/user.service' import { RoleService } from './services/roles/role.service' import { ChannelService } from './services/roles/channel.service' @@ -16,6 +15,7 @@ import { createLogger } from '../common/logger' const logger = createLogger('auth:sigchain') class SigChain { + private _context: auth.LocalUserContext | null = null private _team: auth.Team private _users: UserService | null = null private _devices: DeviceService | null = null @@ -24,8 +24,9 @@ class SigChain { private _invites: InviteService | null = null private _crypto: CryptoService | null = null - private constructor(team: auth.Team) { + private constructor(team: auth.Team, context: auth.LocalUserContext) { this._team = team + this._context = context } /** @@ -35,47 +36,41 @@ class SigChain { * @param username Username of the initial user we are generating * @returns LoadedSigChain instance with the new SigChain and user context */ - public static create(teamName: string, username: string): LoadedSigChain { + public static create(teamName: string, username: string): SigChain { const context = UserService.create(username) const team: auth.Team = this.lfa.createTeam(teamName, context) - const sigChain = this.init(team) + const sigChain = this.init(team, context) // sigChain.roles.createWithMembers(RoleName.ADMIN, [context.user.userId]) sigChain.roles.createWithMembers(RoleName.MEMBER, [context.user.userId]) - return { - sigChain, - context, - } + return sigChain + } + + public static createFromTeam(team: auth.Team, context: auth.LocalUserContext): SigChain { + const sigChain = this.init(team, context) + return sigChain } - public static createFromTeam(team: auth.Team, context: auth.LocalUserContext): LoadedSigChain { - const sigChain = this.init(team) - return { - context, - sigChain, - } + public static load(serializedTeam: Uint8Array, context: auth.LocalUserContext, teamKeyRing: auth.Keyring): SigChain { + const team: auth.Team = this.lfa.loadTeam(serializedTeam, context, teamKeyRing) + const sigChain = this.init(team, context) + + return sigChain } // TODO: Is this the right signature for this method? - public static join( - context: auth.LocalUserContext, - serializedTeam: Uint8Array, - teamKeyRing: auth.Keyring - ): LoadedSigChain { + public static join(context: auth.LocalUserContext, serializedTeam: Uint8Array, teamKeyRing: auth.Keyring): SigChain { const team: auth.Team = this.lfa.loadTeam(serializedTeam, context, teamKeyRing) team.join(teamKeyRing) - const sigChain = this.init(team) + const sigChain = this.init(team, context) - return { - sigChain, - context, - } + return sigChain } - private static init(team: auth.Team): SigChain { - const sigChain = new SigChain(team) + private static init(team: auth.Team, context: auth.LocalUserContext): SigChain { + const sigChain = new SigChain(team, context) sigChain.initServices() return sigChain @@ -90,11 +85,18 @@ class SigChain { this._crypto = CryptoService.init(this) } - // TODO: persist to storage - public persist(): Uint8Array { + public save(): Uint8Array { return this.team.save() // this doesn't actually do anything but create the new state to save } + get context(): auth.LocalUserContext { + return this._context! + } + + set context(context: auth.LocalUserContext) { + this._context = context + } + get team(): auth.Team { return this._team } diff --git a/packages/backend/src/nest/auth/sigchainManager.spec.ts b/packages/backend/src/nest/auth/sigchainManager.spec.ts deleted file mode 100644 index 3f86ce6f1c..0000000000 --- a/packages/backend/src/nest/auth/sigchainManager.spec.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { jest } from '@jest/globals' -import { SigChain } from './sigchain' -import { SigChainManager } from './sigchainManager' -import { createLogger } from '../common/logger' - -const logger = createLogger('auth:sigchainManager.spec') - -describe('SigChainManager', () => { - let sigChainManager: SigChainManager - - it('should initialize a new SigChainManager', () => { - sigChainManager = SigChainManager.init() - expect(sigChainManager).toBeDefined() - }) - it('should throw an error when trying to get an active chain without setting one', () => { - expect(() => sigChainManager.getActiveChain()).toThrowError() - }) - it('should throw an error when trying to set an active chain that does not exist', () => { - expect(() => sigChainManager.setActiveChain('nonexistent')).toThrowError() - }) - it('should add a new chain and it not be active if not set to be', () => { - const { context, sigChain } = sigChainManager.createChain('test', 'user', false) - expect(sigChainManager.activeChainTeamName).toBeUndefined() - expect(() => sigChainManager.getActiveChain()).toThrowError() - sigChainManager.setActiveChain('test') - expect(sigChainManager.getActiveChain()).toBe(sigChain) - }) - it('should add a new chain and it be active if set to be', () => { - const { context, sigChain } = sigChainManager.createChain('test2', 'user2', true) - expect(sigChainManager.getActiveChain()).toBe(sigChain) - const prevSigChain = sigChainManager.getChainByTeamName('test') - expect(prevSigChain).toBeDefined() - expect(prevSigChain).not.toBe(sigChain) - }) - it('should delete nonactive chain without changing active chain', () => { - sigChainManager.deleteChain('test') - expect(() => sigChainManager.getChainByTeamName('test')).toThrowError() - }) - it('should delete active chain and set active chain to undefined', () => { - sigChainManager.deleteChain('test2') - expect(sigChainManager.activeChainTeamName).toBeUndefined() - }) -}) diff --git a/packages/backend/src/nest/auth/sigchainManager.ts b/packages/backend/src/nest/auth/sigchainManager.ts deleted file mode 100644 index 372016b25b..0000000000 --- a/packages/backend/src/nest/auth/sigchainManager.ts +++ /dev/null @@ -1,96 +0,0 @@ -/** - * Manages the chain(s) and makes them accesible across the application - */ - -import { SigChain } from './sigchain' -import { createLogger } from '../common/logger' -import { LoadedSigChain } from './types' - -const logger = createLogger('auth:sigchainManager') - -class SigChainManager { - public chains: Map = new Map() - public activeChainTeamName: string | undefined - private static _instance: SigChainManager | undefined - - private constructor() {} - - public static init(): SigChainManager { - if (SigChainManager._instance !== undefined) { - throw new Error(`SigChainManager already initialized!`) - } - SigChainManager._instance = new SigChainManager() - - return SigChainManager.instance - } - - public getActiveChain(): SigChain { - if (this.activeChainTeamName == undefined) { - throw new Error(`No active chain found!`) - } - - return this.getChainByTeamName(this.activeChainTeamName) - } - - public setActiveChain(teamName: string): SigChain { - if (!this.chains.has(teamName)) { - throw new Error(`No chain found for team ${teamName}, can't set to active!`) - } - - this.activeChainTeamName = teamName - return this.getActiveChain() - } - - public addChain(chain: SigChain, setActive: boolean): boolean { - if (this.chains.has(chain.team.teamName)) { - throw new Error(`Chain for team ${chain.team.teamName} already exists`) - } - - this.chains.set(chain.team.teamName, chain) - if (setActive) { - this.setActiveChain(chain.team.teamName) - return true - } - return false - } - - public deleteChain(teamName: string): void { - if (!this.chains.has(teamName)) { - throw new Error(`No chain found for team ${teamName} to delete!`) - } - - this.chains.delete(teamName) - if (this.activeChainTeamName === teamName) { - this.activeChainTeamName = undefined - } - return - } - - public createChain(teamName: string, username: string, setActive: boolean): LoadedSigChain { - if (this.chains.has(teamName)) { - throw new Error(`Chain for team ${teamName} already exists`) - } - - const { context, sigChain } = SigChain.create(teamName, username) - this.addChain(sigChain, setActive) - return { context, sigChain } - } - - public getChainByTeamName(teamName: string): SigChain { - if (!this.chains.has(teamName)) { - throw new Error(`No chain found for team ${teamName}!`) - } - - return this.chains.get(teamName)! - } - - public static get instance(): SigChainManager { - if (SigChainManager._instance == undefined) { - throw new Error(`SigChainManager hasn't been initialized yet! Run init() before accessing`) - } - - return SigChainManager._instance - } -} - -export { SigChainManager } diff --git a/packages/backend/src/nest/auth/types.ts b/packages/backend/src/nest/auth/types.ts index 28d1aef33b..0e385f31e1 100644 --- a/packages/backend/src/nest/auth/types.ts +++ b/packages/backend/src/nest/auth/types.ts @@ -1,7 +1,13 @@ -import { LocalUserContext } from '@localfirst/auth' -import { SigChain } from './sigchain' +import { Keyring, LocalUserContext } from '@localfirst/auth' -export type LoadedSigChain = { - sigChain: SigChain +export type SigChainSaveData = { + serializedTeam: string context: LocalUserContext + teamKeyRing: Keyring +} + +export type SerializedSigChain = { + serializedTeam: Uint8Array + context: LocalUserContext + teamKeyRing: Keyring } diff --git a/packages/backend/src/nest/connections-manager/connections-manager.module.ts b/packages/backend/src/nest/connections-manager/connections-manager.module.ts index 83b0892d3a..ef599594e1 100644 --- a/packages/backend/src/nest/connections-manager/connections-manager.module.ts +++ b/packages/backend/src/nest/connections-manager/connections-manager.module.ts @@ -6,9 +6,18 @@ import { StorageModule } from '../storage/storage.module' import { TorModule } from '../tor/tor.module' import { ConnectionsManagerService } from './connections-manager.service' import { StorageServiceClientModule } from '../storageServiceClient/storageServiceClient.module' +import { SigChainModule } from '../auth/sigchain.service.module' @Module({ - imports: [RegistrationModule, StorageModule, TorModule, SocketModule, LocalDbModule, StorageServiceClientModule], + imports: [ + RegistrationModule, + StorageModule, + TorModule, + SocketModule, + LocalDbModule, + StorageServiceClientModule, + SigChainModule, + ], providers: [ConnectionsManagerService], exports: [ConnectionsManagerService], }) diff --git a/packages/backend/src/nest/connections-manager/connections-manager.service.spec.ts b/packages/backend/src/nest/connections-manager/connections-manager.service.spec.ts index b11e0224ab..3fc05061d6 100644 --- a/packages/backend/src/nest/connections-manager/connections-manager.service.spec.ts +++ b/packages/backend/src/nest/connections-manager/connections-manager.service.spec.ts @@ -1,7 +1,7 @@ import { jest } from '@jest/globals' import { LazyModuleLoader } from '@nestjs/core' import { Test, TestingModule } from '@nestjs/testing' -import { getFactory, prepareStore, type Store, type communities, type identity } from '@quiet/state-manager' +import { getFactory, identity, prepareStore, type Store, type communities } from '@quiet/state-manager' import { type Community, type Identity, type InitCommunityPayload } from '@quiet/types' import { type FactoryGirl } from 'factory-girl' import PeerId from 'peer-id' @@ -19,6 +19,12 @@ import { SocketModule } from '../socket/socket.module' import { ConnectionsManagerModule } from './connections-manager.module' import { ConnectionsManagerService } from './connections-manager.service' import { createLibp2pAddress } from '@quiet/common' +import { SigChain } from '../auth/sigchain' +import { createLogger } from '../common/logger' +import { Logger } from '@nestjs/common' +import { SigChainService } from '../auth/sigchain.service' + +const logger = createLogger('connections-manager.service.spec') describe('ConnectionsManagerService', () => { let module: TestingModule @@ -34,6 +40,7 @@ describe('ConnectionsManagerService', () => { let userIdentity: Identity let communityRootCa: string let peerId: PeerId + let sigChainService: SigChainService beforeEach(async () => { jest.clearAllMocks() @@ -58,6 +65,12 @@ describe('ConnectionsManagerService', () => { connectionsManagerService = await module.resolve(ConnectionsManagerService) localDbService = await module.resolve(LocalDbService) registrationService = await module.resolve(RegistrationService) + sigChainService = await module.resolve(SigChainService) + + // initialize sigchain on local db + sigChainService.createChain(community.name!, userIdentity.nickname, false) + sigChainService.saveChain(community.name!) + sigChainService.deleteChain(community.name!, false) lazyModuleLoader = await module.resolve(LazyModuleLoader) const { Libp2pModule: Module } = await import('../libp2p/libp2p.module') @@ -91,6 +104,7 @@ describe('ConnectionsManagerService', () => { }) it('launches community on init if its data exists in local db', async () => { + logger.info('launches community on init if its data exists in local db') const remotePeer = createLibp2pAddress( 'y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd', 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE' @@ -100,13 +114,16 @@ describe('ConnectionsManagerService', () => { // below const actualCommunity = { id: community.id, + name: community.name, peerList: [remotePeer], } + // await localDbService.setSigChain(sigChain) await localDbService.setCommunity(actualCommunity) await localDbService.setCurrentCommunityId(community.id) await localDbService.setIdentity(userIdentity) + logger.info('Closing all services') await connectionsManagerService.closeAllServices() const launchCommunitySpy = jest.spyOn(connectionsManagerService, 'launchCommunity').mockResolvedValue() @@ -116,10 +133,16 @@ describe('ConnectionsManagerService', () => { const localPeerAddress = createLibp2pAddress(userIdentity.hiddenService.onionAddress, userIdentity.peerId.id) const updatedLaunchCommunityPayload = { ...actualCommunity, peerList: [localPeerAddress, remotePeer] } - expect(launchCommunitySpy).toHaveBeenCalledWith(updatedLaunchCommunityPayload) + logger.info('updatedLaunchCommunityPayload', updatedLaunchCommunityPayload) + + // expect(launchCommunitySpy).toHaveBeenCalledWith(updatedLaunchCommunityPayload) + expect(launchCommunitySpy).toBeCalledWith(updatedLaunchCommunityPayload) + expect(sigChainService.getActiveChain()).toBeDefined() + expect(sigChainService.getActiveChain()?.team.teamName).toBe(community.name) }) it('does not launch community on init if its data does not exist in local db', async () => { + logger.info('does not launch community on init if its data does not exist in local db') await connectionsManagerService.closeAllServices() await connectionsManagerService.init() const launchCommunitySpy = jest.spyOn(connectionsManagerService, 'launchCommunity') 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 69cff21492..a1bdbd16a4 100644 --- a/packages/backend/src/nest/connections-manager/connections-manager.service.ts +++ b/packages/backend/src/nest/connections-manager/connections-manager.service.ts @@ -70,6 +70,7 @@ import { DateTime } from 'luxon' import { createLogger } from '../common/logger' import { createUserCsr, getPubKey, loadPrivateKey, pubKeyFromCsr } from '@quiet/identity' import { config } from '@quiet/state-manager' +import { SigChainService } from '../auth/sigchain.service' @Injectable() export class ConnectionsManagerService extends EventEmitter implements OnModuleInit { @@ -92,7 +93,8 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI private readonly localDbService: LocalDbService, private readonly storageService: StorageService, private readonly tor: Tor, - private readonly lazyModuleLoader: LazyModuleLoader + private readonly lazyModuleLoader: LazyModuleLoader, + private readonly sigChainService: SigChainService ) { super() } @@ -213,7 +215,7 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI public async launchCommunityFromStorage() { this.logger.info('Launching community from storage') - const community = await this.localDbService.getCurrentCommunity() + const community: Community | undefined = await this.localDbService.getCurrentCommunity() if (!community) { this.logger.info('No community found in storage') return @@ -225,6 +227,14 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI return } + if (community.name) { + try { + await this.sigChainService.loadChain(community.name, true) + } catch (e) { + this.logger.warn('Failed to load sigchain', e) + } + } + const sortedPeers = await this.localDbService.getSortedPeers(community.peerList ?? []) this.logger.info('launchCommunityFromStorage - sorted peers', sortedPeers) if (sortedPeers.length > 0) { @@ -232,10 +242,14 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI } await this.localDbService.setCommunity(community) + this.logger.info('Launching community from storage with peers', community.peerList) await this.launchCommunity(community) } public async closeAllServices(options: { saveTor: boolean } = { saveTor: false }) { + this.logger.info('Saving active sigchain') + await this.saveActiveChain() + this.logger.info('Closing services') await this.closeSocket() @@ -264,6 +278,14 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI await this.socketService.close() } + public async saveActiveChain() { + try { + await this.sigChainService.saveChain(this.sigChainService.activeChainTeamName!) + } catch (e) { + this.logger.info('Failed to save active chain', e) + } + } + public async pause() { this.logger.info('Pausing!') await this.closeSocket() @@ -561,6 +583,13 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI if (identity.userCsr?.userCsr) { await this.storageService.saveCSR({ csr: identity.userCsr.userCsr }) } + + // create sigchain + if (!community.name) { + this.logger.error('Community name is required to create sigchain') + return community + } + this.sigChainService.createChain(community.name, identity.nickname, true) return community } @@ -655,6 +684,7 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI inviteData, } + // TODO: Add initialization of sigchain from invite await this.localDbService.setCommunity(community) await this.localDbService.setCurrentCommunityId(community.id) diff --git a/packages/backend/src/nest/local-db/local-db.service.ts b/packages/backend/src/nest/local-db/local-db.service.ts index 0e02dea22c..cde65642d6 100644 --- a/packages/backend/src/nest/local-db/local-db.service.ts +++ b/packages/backend/src/nest/local-db/local-db.service.ts @@ -1,3 +1,4 @@ +import { Buffer } from 'buffer' import { Inject, Injectable } from '@nestjs/common' import { Level } from 'level' import { type Community, type NetworkInfo, NetworkStats, Identity, IdentityUpdatePayload } from '@quiet/types' @@ -5,6 +6,8 @@ import { createLibp2pAddress, filterAndSortPeers } from '@quiet/common' import { LEVEL_DB } from '../const' import { LocalDBKeys, LocalDbStatus } from './local-db.types' import { createLogger } from '../common/logger' +import { SerializedSigChain, SigChainSaveData } from '../auth/types' +import { SigChain } from '../auth/sigchain' @Injectable() export class LocalDbService { @@ -159,4 +162,39 @@ export class LocalDbService { public async getIdentities(): Promise> { return await this.get(LocalDBKeys.IDENTITIES) } + + public async setSigChain(sigChain: SigChain) { + const teamName = sigChain.team.teamName + const key = `${LocalDBKeys.SIGCHAINS}${teamName}` + const serializedSigChain: SigChainSaveData = { + serializedTeam: Buffer.from(sigChain.save()).toString('base64'), + context: sigChain.context, + teamKeyRing: sigChain.team.teamKeyring(), + } + this.logger.info('Saving sigchain', teamName) + await this.put(key, serializedSigChain) + } + + public async getSigChain(teamName: string): Promise { + const key = `${LocalDBKeys.SIGCHAINS}${teamName}` + this.logger.info('Getting sigchain', teamName, key) + const sigChainBlob = await this.get(key) + if (sigChainBlob) { + // convert serializedTeam from base64 to buffer to Uint8Array + const serializedTeamBuffer = Buffer.from(sigChainBlob.serializedTeam, 'base64') + return { + serializedTeam: new Uint8Array(serializedTeamBuffer), + context: sigChainBlob.context, + teamKeyRing: sigChainBlob.teamKeyRing, + } as SerializedSigChain + } else { + this.logger.error('Sigchain not found', teamName) + return undefined + } + } + + public async deleteSigChain(teamName: string) { + const key = `${LocalDBKeys.SIGCHAINS}${teamName}` + await this.delete(key) + } } diff --git a/packages/backend/src/nest/local-db/local-db.types.ts b/packages/backend/src/nest/local-db/local-db.types.ts index 3891a82966..819860fb15 100644 --- a/packages/backend/src/nest/local-db/local-db.types.ts +++ b/packages/backend/src/nest/local-db/local-db.types.ts @@ -17,5 +17,9 @@ export enum LocalDBKeys { // TODO: Deprecate this soon (and delete the data from LevelDB). This data // exists in the Community object. OWNER_ORBIT_DB_IDENTITY = 'ownerOrbitDbIdentity', + + SIGCHAINS = 'sigchains:', + USER_CONTEXTS = 'userContexts', + KEYRINGS = 'keyrings', } export type LocalDbStatus = 'opening' | 'open' | 'closing' | 'closed'