Skip to content

Commit

Permalink
Refactor owner cert registration
Browse files Browse the repository at this point in the history
  • Loading branch information
Lucas Leblow committed Mar 8, 2024
1 parent c17cb0d commit a8db4ee
Show file tree
Hide file tree
Showing 27 changed files with 180 additions and 182 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
NetworkDataPayload,
NetworkInfo,
NetworkStats,
type SavedOwnerCertificatePayload,
PushNotificationPayload,
RegisterOwnerCertificatePayload,
RemoveDownloadStatus,
Expand Down Expand Up @@ -61,7 +62,7 @@ import { StorageEvents } from '../storage/storage.types'
import { LazyModuleLoader } from '@nestjs/core'
import Logger from '../common/logger'
import { emitError } from '../socket/socket.errors'
import { isPSKcodeValid } from '@quiet/common'
import { createLibp2pAddress, isPSKcodeValid } from '@quiet/common'
import { createRootCA } from '@quiet/identity'

@Injectable()
Expand Down Expand Up @@ -311,26 +312,50 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI
return
}

// TODO: Should we save this network info in LevelDB at this point?
return network
}

public async createCommunity(payload: InitCommunityPayload): Promise<Community> {
public async createCommunity(payload: InitCommunityPayload): Promise<Community | undefined> {
this.logger('Creating community: peers:', payload.peers)

const notBeforeDate = new Date(Date.UTC(2010, 11, 28, 10, 10, 10))
const notAfterDate = new Date(Date.UTC(2030, 11, 28, 10, 10, 10))
const CA = createRootCA(
new Time({ type: 0, value: notBeforeDate }),
new Time({ type: 0, value: notAfterDate }),
payload.name
)
if (!payload.CA || !payload.rootCa) {
this.logger.error('CA and rootCa are required to create community')
return
}

if (!payload.ownerCsr) {
this.logger.error('ownerCsr is required to create community')
return
}

const psk = Libp2pService.generateLibp2pPSK().psk
let ownerCertResult: SavedOwnerCertificatePayload

try {
this.emit(SocketActionTypes.CONNECTION_PROCESS_INFO, ConnectionProcessInfo.REGISTERING_OWNER_CERTIFICATE)
ownerCertResult = await this.registrationService.registerOwnerCertificate({
communityId: payload.id,
userCsr: payload.ownerCsr,
permsData: {
certificate: payload.CA.rootCertString,
privKey: payload.CA.rootKeyString,
},
})
} catch (e) {
this.logger.error('Failed to register owner certificate')
return
}

const localAddress = createLibp2pAddress(payload.hiddenService.onionAddress, payload.peerId.id)

const community = {
id: payload.id,
name: payload.name,
CA,
rootCa: CA.rootCertString,
CA: payload.CA,
rootCa: payload.rootCa,
peerList: [localAddress],
ownerCertificate: ownerCertResult.network.certificate,
psk: psk,
}

Expand All @@ -354,6 +379,11 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI
public async joinCommunity(payload: InitCommunityPayload): Promise<Community | undefined> {
this.logger('Joining community: peers:', payload.peers)

if (!payload.peers || payload.peers.length === 0) {
this.logger.error('Joining community: Peers required')
return
}

if (!payload.psk || !isPSKcodeValid(payload.psk)) {
this.logger.error('Joining community: Libp2p PSK is not valid')
emitError(this.serverIoProvider.io, {
Expand All @@ -374,8 +404,11 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI
return
}

const localAddress = createLibp2pAddress(payload.hiddenService.onionAddress, payload.peerId.id)

const community = {
id: payload.id,
peerList: [...new Set([localAddress, ...payload.peers])],
psk: payload.psk,
ownerOrbitDbIdentity: payload.ownerOrbitDbIdentity,
}
Expand Down Expand Up @@ -459,19 +492,16 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI
const restoredRsa = await PeerId.createFromJSON(network.peerId)
const peerId = await peerIdFromKeys(restoredRsa.marshalPubKey(), restoredRsa.marshalPrivKey())

let peers = community.peerList
const peers = community.peerList
this.logger(`Launching community ${community.id}: payload peers: ${peers}`)
if (!peers || peers.length === 0) {
peers = [this.libp2pService.createLibp2pAddress(onionAddress, peerId.toString())]
}

const params: Libp2pNodeParams = {
peerId,
listenAddresses: [this.libp2pService.createLibp2pListenAddress(onionAddress)],
agent: this.socksProxyAgent,
localAddress: this.libp2pService.createLibp2pAddress(onionAddress, peerId.toString()),
targetPort: this.ports.libp2pHiddenService,
peers,
peers: peers ?? [],
psk: Libp2pService.generateLibp2pPSK(community.psk).fullKey,
}
await this.libp2pService.createInstance(params)
Expand Down Expand Up @@ -562,7 +592,7 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI
)
this.socketService.on(
SocketActionTypes.CREATE_COMMUNITY,
async (args: InitCommunityPayload, callback: (response: Community) => void) => {
async (args: InitCommunityPayload, callback: (response: Community | undefined) => void) => {
this.logger(`socketService - ${SocketActionTypes.CREATE_COMMUNITY}`)
callback(await this.createCommunity(args))
}
Expand Down Expand Up @@ -596,12 +626,6 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI
this.logger(`socketService - ${SocketActionTypes.ADD_CSR}`)
await this.storageService?.saveCSR(payload)
})
this.socketService.on(
SocketActionTypes.REGISTER_OWNER_CERTIFICATE,
async (args: RegisterOwnerCertificatePayload) => {
await this.registrationService.registerOwnerCertificate(args)
}
)
// TODO: With the Community model on the backend, there is no need to call
// SET_COMMUNITY_CA_DATA anymore. We can call setPermsData when
// creating the community.
Expand Down
9 changes: 0 additions & 9 deletions packages/backend/src/nest/libp2p/libp2p.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,3 @@ export interface Libp2pNodeParams {
peers: string[]
psk: Uint8Array
}

export interface InitLibp2pParams {
peerId: any
address: string
addressPort: number
targetPort: number
bootstrapMultiaddrs: string[]
certs: Certificates
}
2 changes: 1 addition & 1 deletion packages/backend/src/nest/local-db/local-db.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Inject, Injectable } from '@nestjs/common'
import { Level } from 'level'
import { InitCommunityPayload, NetworkInfo, NetworkStats, Identity, Community } from '@quiet/types'
import { type Community, type Identity, InitCommunityPayload, type NetworkInfo, NetworkStats } from '@quiet/types'
import { createLibp2pAddress, filterAndSortPeers } from '@quiet/common'
import { LEVEL_DB } from '../const'
import { LocalDBKeys, LocalDbStatus } from './local-db.types'
Expand Down
28 changes: 18 additions & 10 deletions packages/backend/src/nest/registration/registration.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { Injectable, OnModuleInit } from '@nestjs/common'
import { EventEmitter } from 'events'
import { extractPendingCsrs, issueCertificate } from './registration.functions'
import { ErrorCodes, ErrorMessages, PermsData, RegisterOwnerCertificatePayload, SocketActionTypes } from '@quiet/types'
import {
ErrorCodes,
ErrorMessages,
PermsData,
RegisterOwnerCertificatePayload,
type SavedOwnerCertificatePayload,
SocketActionTypes,
} from '@quiet/types'
import { RegistrationEvents } from './registration.types'
import Logger from '../common/logger'
import { StorageService } from '../storage/storage.service'
Expand Down Expand Up @@ -93,21 +100,22 @@ export class RegistrationService extends EventEmitter implements OnModuleInit {
)
}

public async registerOwnerCertificate(payload: RegisterOwnerCertificatePayload): Promise<void> {
// TODO: This doesn't save the owner's certificate in OrbitDB, so perhaps we
// should rename it or look into refactoring so that it does save to OrbitDB.
// However, currently, this is called before the storage service is
// initialized.
public async registerOwnerCertificate(
payload: RegisterOwnerCertificatePayload
): Promise<SavedOwnerCertificatePayload> {
this.permsData = payload.permsData
const result = await issueCertificate(payload.userCsr.userCsr, this.permsData)
if (result?.cert) {
this.emit(SocketActionTypes.OWNER_CERTIFICATE_ISSUED, {
return {
communityId: payload.communityId,
network: { certificate: result.cert },
})
}
} else {
this.emit(SocketActionTypes.ERROR, {
type: SocketActionTypes.REGISTER_OWNER_CERTIFICATE,
code: ErrorCodes.SERVER_ERROR,
message: ErrorMessages.REGISTRATION_FAILED,
community: payload.communityId,
})
throw new Error('Failed to register owner certificate')
}
}

Expand Down
9 changes: 1 addition & 8 deletions packages/backend/src/nest/socket/socket.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,17 +143,10 @@ export class SocketService extends EventEmitter implements OnModuleInit {
this.emit(SocketActionTypes.ADD_CSR, payload)
})

socket.on(SocketActionTypes.REGISTER_OWNER_CERTIFICATE, async (payload: RegisterOwnerCertificatePayload) => {
this.logger(`Registering owner certificate (${payload.communityId})`)

this.emit(SocketActionTypes.REGISTER_OWNER_CERTIFICATE, payload)
this.emit(SocketActionTypes.CONNECTION_PROCESS_INFO, ConnectionProcessInfo.REGISTERING_OWNER_CERTIFICATE)
})

// ====== Community ======
socket.on(
SocketActionTypes.CREATE_COMMUNITY,
async (payload: InitCommunityPayload, callback: (response: Community) => void) => {
async (payload: InitCommunityPayload, callback: (response: Community | undefined) => void) => {
this.logger(`Creating community ${payload.id}`)
this.emit(SocketActionTypes.CREATE_COMMUNITY, payload, callback)
}
Expand Down
9 changes: 0 additions & 9 deletions packages/backend/src/nest/storage/storage.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,6 @@ export enum StorageEvents {
COMMUNITY_METADATA_STORED = 'communityMetadataStored',
}

export interface InitStorageParams {
communityId: string
peerId: any
onionAddress: string
targetPort: number
peers?: string[]
certs: Certificates
}

export interface CsrReplicatedPromiseValues {
promise: Promise<unknown>
resolveFunction: any
Expand Down
2 changes: 1 addition & 1 deletion packages/desktop/src/rtl-tests/channel.main.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -903,7 +903,7 @@ describe('Channel', () => {

const community: Community = await factory.create<
ReturnType<typeof communities.actions.addNewCommunity>['payload']
>('Community', { rootCa: 'rootCa', privateKey: 'privateKey' })
>('Community', { rootCa: 'rootCa' })

const alice = await factory.create<ReturnType<typeof identity.actions.addNewIdentity>['payload']>('Identity', {
id: community.id,
Expand Down
1 change: 0 additions & 1 deletion packages/desktop/src/rtl-tests/community.create.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,6 @@ describe('User', () => {
"Identity/registerCertificate",
"Communities/updateCommunity",
"Identity/storeUserCertificate",
"Identity/savedOwnerCertificate",
"Communities/updateCommunityData",
"Communities/sendCommunityCaData",
"Files/checkForMissingFiles",
Expand Down
2 changes: 0 additions & 2 deletions packages/desktop/src/rtl-tests/customProtocol.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@ describe('Opening app through custom protocol', () => {
rootCa: '',
peerList: [],
onionAddress: '',
privateKey: '',
port: 0,
ownerCertificate: '',
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ export const getCommunityOwnerData = (ownerStore: Store) => {
communityId: community.id,
ownerPeerId: ownerIdentityState.identities.entities[ownerIdentityState.identities.ids[0]].peerId.id,
ownerRootCA: community.rootCa,
registrarPort: community.port,
registrarPort: 0,
}
}

Expand Down
2 changes: 0 additions & 2 deletions packages/mobile/src/store/init/deepLink/deepLink.saga.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ describe('deepLinkSaga', () => {
rootCa: '',
peerList: [],
onionAddress: '',
privateKey: '',
port: 0,
ownerCertificate: '',
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { all, takeEvery } from 'typed-redux-saga'
import { communitiesActions } from './communities.slice'
import { connectionActions } from '../appConnection/connection.slice'
import { updateCommunitySaga } from './updateCommunity/updateCommunity.saga'
import { createCommunitySaga } from './createCommunity/createCommunity.saga'
import { initCommunities, launchCommunitySaga } from './launchCommunity/launchCommunity.saga'
import { createNetworkSaga } from './createNetwork/createNetwork.saga'
import { saveCommunityMetadataSaga } from './saveCommunityMetadata/saveCommunityMetadata.saga'
Expand All @@ -14,6 +15,7 @@ export function* communitiesMasterSaga(socket: Socket): Generator {
takeEvery(communitiesActions.createNetwork.type, createNetworkSaga, socket),
takeEvery(communitiesActions.updateCommunity.type, updateCommunitySaga),
takeEvery(connectionActions.torBootstrapped.type, initCommunities),
takeEvery(communitiesActions.createCommunity.type, createCommunitySaga, socket),
takeEvery(communitiesActions.launchCommunity.type, launchCommunitySaga, socket),
takeEvery(communitiesActions.saveCommunityMetadata.type, saveCommunityMetadataSaga, socket),
takeEvery(communitiesActions.sendCommunityMetadata.type, sendCommunityMetadataSaga, socket),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export const communitiesSlice = createSlice({
})
},
resetApp: (state, _action) => state,
createCommunity: (state, _action: PayloadAction<string>) => state,
launchCommunity: (state, _action: PayloadAction<string>) => state,
customProtocol: (state, _action: PayloadAction<InvitationData>) => state,
setInvitationCodes: (state, action: PayloadAction<InvitationPair[]>) => {
Expand All @@ -59,12 +60,8 @@ export const communitiesSlice = createSlice({
saveCommunityMetadata: (state, _action: PayloadAction<CommunityMetadata>) => state,
/**
* Migrate data in this store. This is necessary because we persist the
* Redux data to disk (it's not reset on each app start). Another option for
* migrations might be to migrate fields when we access them, but not sure
* how that would work with Redux since it is particular about the data
* flow. If there is an action that is called each time the app runs, that
* is another place where migrations can happen. This function is meant to
* be called once the store has been rehydrated from storage.
* Redux data to disk (it's not reset on each app start). This function is
* meant to be called once the store has been rehydrated from storage.
*/
migrate: state => {
// MIGRATION: Move CommunitiesState.psk to Community.psk
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { type Socket, applyEmitParams } from '../../../types'
import { select, apply, put } from 'typed-redux-saga'
import { type PayloadAction } from '@reduxjs/toolkit'
import { identityActions } from '../../identity/identity.slice'
import { communitiesSelectors } from '../communities.selectors'
import { communitiesActions } from '../communities.slice'
import { identitySelectors } from '../../identity/identity.selectors'
import { publicChannelsActions } from '../../publicChannels/publicChannels.slice'
import { type Community, type InitCommunityPayload, SocketActionTypes } from '@quiet/types'

export function* createCommunitySaga(
socket: Socket,
action: PayloadAction<ReturnType<typeof communitiesActions.createCommunity>['payload']>
): Generator {
let communityId: string = action.payload

if (!communityId) {
communityId = yield* select(communitiesSelectors.currentCommunityId)
}

const community = yield* select(communitiesSelectors.selectById(communityId))
const identity = yield* select(identitySelectors.selectById(communityId))

if (!identity) return

const payload: InitCommunityPayload = {
id: communityId,
name: community?.name,
peerId: identity.peerId,
hiddenService: identity.hiddenService,
CA: community?.CA,
rootCa: community?.rootCa,
// Type mismatch between `userCsr | null` in Identity and `ownerCsr?` in
// InitCommunityPayload
ownerCsr: identity.userCsr ?? undefined,
}

const createdCommunity: Community | undefined = yield* apply(
socket,
socket.emitWithAck,
applyEmitParams(SocketActionTypes.CREATE_COMMUNITY, payload)
)

if (!createdCommunity || !createdCommunity.ownerCertificate) return

yield* put(communitiesActions.updateCommunityData(createdCommunity))

yield* put(
identityActions.storeUserCertificate({
communityId: createdCommunity.id,
userCertificate: createdCommunity.ownerCertificate,
})
)

// TODO: Community metadata should already exist on the backend after creating
// the community.
yield* put(communitiesActions.sendCommunityMetadata())
yield* put(publicChannelsActions.createGeneralChannel())
// TODO: We can likely refactor this a bit. Currently, we issue the owner's
// certificate before creating the community, but then we add the owner's CSR
// to the OrbitDB store after creating the community (in the following saga).
// We can likely add the owner's CSR when creating the community or decouple
// community creation from CSR/certificate creation and create the community
// first and then add the owner's CSR and issue their certificate.
yield* put(identityActions.saveUserCsr())
}
Loading

0 comments on commit a8db4ee

Please sign in to comment.