diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f8f4279fd..edc0685f61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +[unreleased] + +* Backward incompatible change: use pre shared key as connection protector in libp2p. Add libp2p psk to invitation link + [2.0.3-alpha.1] * Temporarily hiding leave community button from Possible impersonation attack diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index 0665f2f495..850cd66787 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -3,6 +3,22 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [2.0.3-alpha.5](https://github.com/TryQuiet/backend/compare/@quiet/backend@2.0.3-alpha.4...@quiet/backend@2.0.3-alpha.5) (2023-11-14) + +**Note:** Version bump only for package @quiet/backend + + + + + +## [2.0.3-alpha.4](https://github.com/TryQuiet/backend/compare/@quiet/backend@2.0.3-alpha.3...@quiet/backend@2.0.3-alpha.4) (2023-11-14) + +**Note:** Version bump only for package @quiet/backend + + + + + ## [2.0.3-alpha.3](https://github.com/TryQuiet/backend/compare/@quiet/backend@2.0.3-alpha.2...@quiet/backend@2.0.3-alpha.3) (2023-11-13) **Note:** Version bump only for package @quiet/backend diff --git a/packages/backend/package-lock.json b/packages/backend/package-lock.json index 15500283f1..4323d96834 100644 --- a/packages/backend/package-lock.json +++ b/packages/backend/package-lock.json @@ -1,12 +1,12 @@ { "name": "@quiet/backend", - "version": "2.0.3-alpha.3", + "version": "2.0.3-alpha.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@quiet/backend", - "version": "2.0.3-alpha.3", + "version": "2.0.3-alpha.5", "license": "MIT", "dependencies": { "@chainsafe/libp2p-gossipsub": "6.1.0", @@ -56,7 +56,7 @@ "socks-proxy-agent": "^5.0.0", "string-replace-loader": "3.1.0", "ts-jest-resolver": "^2.0.0", - "validator": "^13.6.0" + "validator": "^13.11.0" }, "devDependencies": { "@nestjs/cli": "^10.0.0", @@ -72,7 +72,7 @@ "@types/orbit-db": "git+https://github.com/orbitdb/orbit-db-types.git", "@types/supertest": "^2.0.11", "@types/tmp": "^0.2.3", - "@types/validator": "^13.1.4", + "@types/validator": "^13.11.5", "@types/ws": "8.5.3", "babel-jest": "^29.3.1", "cross-env": "^5.2.0", @@ -6580,8 +6580,9 @@ "license": "MIT" }, "node_modules/@types/validator": { - "version": "13.1.4", - "license": "MIT" + "version": "13.11.6", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.6.tgz", + "integrity": "sha512-HUgHujPhKuNzgNXBRZKYexwoG+gHKU+tnfPqjWXFghZAnn73JElicMkuSKJyLGr9JgyA8IgK7fj88IyA9rwYeQ==" }, "node_modules/@types/ws": { "version": "8.5.3", @@ -21995,8 +21996,9 @@ } }, "node_modules/validator": { - "version": "13.7.0", - "license": "MIT", + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==", "engines": { "node": ">= 0.10" } @@ -26934,7 +26936,9 @@ "dev": true }, "@types/validator": { - "version": "13.1.4" + "version": "13.11.6", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.6.tgz", + "integrity": "sha512-HUgHujPhKuNzgNXBRZKYexwoG+gHKU+tnfPqjWXFghZAnn73JElicMkuSKJyLGr9JgyA8IgK7fj88IyA9rwYeQ==" }, "@types/ws": { "version": "8.5.3", @@ -36673,7 +36677,9 @@ } }, "validator": { - "version": "13.7.0" + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==" }, "varint": { "version": "6.0.0" diff --git a/packages/backend/package.json b/packages/backend/package.json index d167a3615d..0f5b46f61e 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "@quiet/backend", - "version": "2.0.3-alpha.3", + "version": "2.0.3-alpha.5", "description": "tlg-manager", "types": "lib/index.d.ts", "type": "module", @@ -54,7 +54,7 @@ "@nestjs/schematics": "^10.0.0", "@nestjs/testing": "^10.0.0", "@quiet/eslint-config": "^2.0.2-alpha.0", - "@quiet/state-manager": "^2.0.2-alpha.1", + "@quiet/state-manager": "^2.0.2-alpha.3", "@types/crypto-js": "^4.0.2", "@types/express": "^4.17.9", "@types/jest": "28.1.8", @@ -65,7 +65,7 @@ "@types/orbit-db": "git+https://github.com/orbitdb/orbit-db-types.git", "@types/supertest": "^2.0.11", "@types/tmp": "^0.2.3", - "@types/validator": "^13.1.4", + "@types/validator": "^13.11.5", "@types/ws": "8.5.3", "babel-jest": "^29.3.1", "cross-env": "^5.2.0", @@ -89,10 +89,10 @@ "@nestjs/core": "^10.0.0", "@nestjs/platform-express": "^10.0.0", "@peculiar/webcrypto": "1.4.3", - "@quiet/common": "^2.0.2-alpha.0", - "@quiet/identity": "^2.0.2-alpha.0", + "@quiet/common": "^2.0.2-alpha.1", + "@quiet/identity": "^2.0.2-alpha.2", "@quiet/logger": "^2.0.2-alpha.0", - "@quiet/types": "^2.0.2-alpha.0", + "@quiet/types": "^2.0.2-alpha.1", "abortable-iterator": "^3.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.13.1", @@ -134,7 +134,7 @@ "socks-proxy-agent": "^5.0.0", "string-replace-loader": "3.1.0", "ts-jest-resolver": "^2.0.0", - "validator": "^13.6.0" + "validator": "^13.11.0" }, "overrides": { "level": "$level", diff --git a/packages/backend/src/nest/common/utils.ts b/packages/backend/src/nest/common/utils.ts index 23d0e875ed..eeff2ad29c 100644 --- a/packages/backend/src/nest/common/utils.ts +++ b/packages/backend/src/nest/common/utils.ts @@ -10,9 +10,10 @@ import crypto from 'crypto' import { type PermsData } from '@quiet/types' import { TestConfig } from '../const' import logger from './logger' -import { createCertificatesTestHelper } from './client-server' import { Libp2pNodeParams } from '../libp2p/libp2p.types' import { createLibp2pAddress, createLibp2pListenAddress } from '@quiet/common' +import { Libp2pService } from '../libp2p/libp2p.service' + const log = logger('test') export interface Ports { @@ -189,13 +190,12 @@ export const testBootstrapMultiaddrs = [ ] export const libp2pInstanceParams = async (): Promise => { - const pems = await createCertificatesTestHelper('address1.onion', 'address2.onion') const port = await getPort() const peerId = await createPeerId() const address = '0.0.0.0' const peerIdRemote = await createPeerId() const remoteAddress = createLibp2pAddress(address, peerIdRemote.toString()) - + const libp2pKey = Libp2pService.generateLibp2pPSK().fullKey return { peerId, listenAddresses: [createLibp2pListenAddress('localhost')], @@ -203,6 +203,7 @@ export const libp2pInstanceParams = async (): Promise => { localAddress: createLibp2pAddress('localhost', peerId.toString()), targetPort: port, peers: [remoteAddress], + psk: libp2pKey, } } diff --git a/packages/backend/src/nest/connections-manager/connections-manager.service.tor.spec.ts b/packages/backend/src/nest/connections-manager/connections-manager.service.tor.spec.ts index 8c759f8571..d2b9f28bd7 100644 --- a/packages/backend/src/nest/connections-manager/connections-manager.service.tor.spec.ts +++ b/packages/backend/src/nest/connections-manager/connections-manager.service.tor.spec.ts @@ -87,8 +87,6 @@ beforeEach(async () => { localDbService = await module.resolve(LocalDbService) registrationService = await module.resolve(RegistrationService) tor = await module.resolve(Tor) - - console.log('tor ', tor) await tor.init() const torPassword = crypto.randomBytes(16).toString('hex') @@ -106,6 +104,9 @@ beforeEach(async () => { connectionsManagerService.libp2pService = libp2pService quietDir = await module.resolve(QUIET_DIR) + + const pskBase64 = Libp2pService.generateLibp2pPSK().psk + await localDbService.put(LocalDBKeys.PSK, pskBase64) }) afterEach(async () => { @@ -117,11 +118,6 @@ afterEach(async () => { }) describe('Connections manager', () => { - it('runs tor by default', async () => { - await connectionsManagerService.init() - console.log(connectionsManagerService.isTorInit) - }) - it('saves peer stats when peer has been disconnected', async () => { class RemotePeerEventDetail { peerId: string @@ -135,7 +131,6 @@ describe('Connections manager', () => { } } const emitSpy = jest.spyOn(libp2pService, 'emit') - // const emitSpy = jest.spyOn(libp2pService, 'emit') const launchCommunityPayload: InitCommunityPayload = { id: community.id, 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 dcef3bdc41..41f61c694d 100644 --- a/packages/backend/src/nest/connections-manager/connections-manager.service.ts +++ b/packages/backend/src/nest/connections-manager/connections-manager.service.ts @@ -9,6 +9,7 @@ import { EventEmitter } from 'events' import getPort from 'get-port' import PeerId from 'peer-id' import { removeFilesFromDir } from '../common/utils' + import { AskForMessagesPayload, ChannelMessagesIdsResponse, @@ -59,6 +60,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' @Injectable() export class ConnectionsManagerService extends EventEmitter implements OnModuleInit { @@ -155,7 +157,7 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI this.logger('launchCommunityFromStorage') const community: InitCommunityPayload = await this.localDbService.get(LocalDBKeys.COMMUNITY) - console.log('launchCommunityFromStorage - community', community) + this.logger('launchCommunityFromStorage - community:', community?.id) if (community) { const sortedPeers = await this.localDbService.getSortedPeers(community.peers) if (sortedPeers.length > 0) { @@ -164,8 +166,6 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI await this.localDbService.put(LocalDBKeys.COMMUNITY, community) if ([ServiceState.LAUNCHING, ServiceState.LAUNCHED].includes(this.communityState)) return this.communityState = ServiceState.LAUNCHING - } - if (community) { await this.launchCommunity(community) } } @@ -276,18 +276,43 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI }, network, } + const psk = community.psk + if (psk) { + this.logger('Creating network: received Libp2p PSK') + if (!isPSKcodeValid(psk)) { + this.logger.error('Creating network: received Libp2p PSK is not valid') + emitError(this.serverIoProvider.io, { + type: SocketActionTypes.NETWORK, + message: ErrorMessages.NETWORK_SETUP_FAILED, + community: community.id, + }) + return + } + await this.localDbService.put(LocalDBKeys.PSK, psk) + } + this.serverIoProvider.io.emit(SocketActionTypes.NETWORK, payload) } + private async generatePSK() { + const pskBase64 = Libp2pService.generateLibp2pPSK().psk + await this.localDbService.put(LocalDBKeys.PSK, pskBase64) + this.logger('Generated Libp2p PSK') + this.serverIoProvider.io.emit(SocketActionTypes.LIBP2P_PSK_SAVED, { psk: pskBase64 }) + } + public async createCommunity(payload: InitCommunityPayload) { - console.log('ConnectionsManager.createCommunity peers:', payload.peers) + this.logger('Creating community: peers:', payload.peers) + + await this.generatePSK() + await this.launchCommunity(payload) this.logger(`Created and launched community ${payload.id}`) this.serverIoProvider.io.emit(SocketActionTypes.NEW_COMMUNITY, { id: payload.id }) } public async launchCommunity(payload: InitCommunityPayload) { - console.log('ConnectionsManager.launchCommunity peers:', payload.peers) + this.logger('Launching community: peers:', payload.peers) this.communityState = ServiceState.LAUNCHING const communityData: InitCommunityPayload = await this.localDbService.get(LocalDBKeys.COMMUNITY) if (!communityData) { @@ -331,7 +356,7 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI targetPort: this.ports.libp2pHiddenService, privKey: payload.hiddenService.privateKey, }) - this.logger(`Launching community ${payload.id}, peer: ${payload.peerId.id}`) + this.logger(`Launching community ${payload.id}: peer: ${payload.peerId.id}`) const { Libp2pModule } = await import('../libp2p/libp2p.module') const moduleRef = await this.lazyModuleLoader.load(() => Libp2pModule) @@ -343,11 +368,17 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI const _peerId = await peerIdFromKeys(restoredRsa.marshalPubKey(), restoredRsa.marshalPrivKey()) let peers = payload.peers - console.log(`Launching community ${payload.id}, payload peers: ${peers}`) + this.logger(`Launching community ${payload.id}: payload peers: ${peers}`) if (!peers || peers.length === 0) { peers = [this.libp2pService.createLibp2pAddress(onionAddress, _peerId.toString())] } + const pskValue: string = await this.localDbService.get(LocalDBKeys.PSK) + if (!pskValue) { + throw new Error('No psk in local db') + } + this.logger(`Launching community ${payload.id}: retrieved Libp2p PSK`) + const libp2pPSK = Libp2pService.generateLibp2pPSK(pskValue).fullKey const params: Libp2pNodeParams = { peerId: _peerId, listenAddresses: [this.libp2pService.createLibp2pListenAddress(onionAddress)], @@ -355,16 +386,15 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI localAddress: this.libp2pService.createLibp2pAddress(onionAddress, _peerId.toString()), targetPort: this.ports.libp2pHiddenService, peers, + psk: libp2pPSK, } await this.libp2pService.createInstance(params) - // KACPER // Libp2p event listeners this.libp2pService.on(Libp2pEvents.PEER_CONNECTED, (payload: { peers: string[] }) => { this.serverIoProvider.io.emit(SocketActionTypes.PEER_CONNECTED, payload) }) this.libp2pService.on(Libp2pEvents.PEER_DISCONNECTED, async (payload: NetworkDataPayload) => { - console.log(' this.libp2pService.on(Libp2pEvents.PEER_DISCONNECTED') const peerPrevStats = await this.localDbService.find(LocalDBKeys.PEERS, payload.peer) const prev = peerPrevStats?.connectionTime || 0 @@ -381,7 +411,7 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI this.serverIoProvider.io.emit(SocketActionTypes.PEER_DISCONNECTED, payload) }) await this.storageService.init(_peerId) - console.log('storage initialized') + this.logger('storage initialized') } private attachTorEventsListeners() { this.logger('attachTorEventsListeners') @@ -428,6 +458,7 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI } }) this.socketService.on(SocketActionTypes.CREATE_NETWORK, async (args: Community) => { + this.logger(`socketService - ${SocketActionTypes.CREATE_NETWORK}`) await this.createNetwork(args) }) this.socketService.on(SocketActionTypes.CREATE_COMMUNITY, async (args: InitCommunityPayload) => { @@ -451,7 +482,7 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI await this.storageService?.updateCommunityMetadata(payload) }) this.socketService.on(SocketActionTypes.SAVE_USER_CSR, async (payload: SaveCSRPayload) => { - console.log(`On ${SocketActionTypes.SAVE_USER_CSR}`) + this.logger(`socketService - ${SocketActionTypes.SAVE_USER_CSR}`) await this.storageService?.saveCSR(payload) this.serverIoProvider.io.emit(SocketActionTypes.SAVED_USER_CSR, payload) }) @@ -500,7 +531,7 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI this.socketService.on( SocketActionTypes.DELETE_FILES_FROM_CHANNEL, async (payload: DeleteFilesFromChannelSocketPayload) => { - this.logger('DELETE_FILES_FROM_CHANNEL : payload', payload) + this.logger(`socketService - ${SocketActionTypes.DELETE_FILES_FROM_CHANNEL}`, payload) await this.storageService?.deleteFilesFromChannel(payload) // await this.deleteFilesFromTemporaryDir() //crashes on mobile, will be fixes in next versions } @@ -533,7 +564,7 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI this.serverIoProvider.io.emit(SocketActionTypes.CHANNEL_SUBSCRIBED, payload) }) this.storageService.on(StorageEvents.CREATED_CHANNEL, (payload: CreatedChannelResponse) => { - console.log('created channel in services') + this.logger(`Storage - ${StorageEvents.CREATED_CHANNEL}: ${payload.channel.name}`) this.serverIoProvider.io.emit(SocketActionTypes.CREATED_CHANNEL, payload) }) this.storageService.on(StorageEvents.REMOVE_DOWNLOAD_STATUS, (payload: RemoveDownloadStatus) => { @@ -564,19 +595,19 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI this.serverIoProvider.io.emit(SocketActionTypes.CHECK_FOR_MISSING_FILES, payload) }) this.storageService.on(StorageEvents.CHANNEL_DELETION_RESPONSE, (payload: { channelId: string }) => { - console.log('emitting deleted channel event back to state manager') + this.logger(`Storage - ${StorageEvents.CHANNEL_DELETION_RESPONSE}`) this.serverIoProvider.io.emit(SocketActionTypes.CHANNEL_DELETION_RESPONSE, payload) }) this.storageService.on( StorageEvents.REPLICATED_CSR, async (payload: { csrs: string[]; certificates: string[]; id: string }) => { - console.log(`On ${StorageEvents.REPLICATED_CSR}`) + console.log(`Storage - ${StorageEvents.REPLICATED_CSR}`) this.serverIoProvider.io.emit(SocketActionTypes.RESPONSE_GET_CSRS, { csrs: payload.csrs }) this.registrationService.emit(RegistrationEvents.REGISTER_USER_CERTIFICATE, payload) } ) this.storageService.on(StorageEvents.REPLICATED_COMMUNITY_METADATA, (payload: CommunityMetadata) => { - console.log(`On ${StorageEvents.REPLICATED_COMMUNITY_METADATA}: ${payload}`) + this.logger(`Storage - ${StorageEvents.REPLICATED_COMMUNITY_METADATA}: ${payload}`) const communityMetadataPayload: CommunityMetadataPayload = { rootCa: payload.rootCa, ownerCertificate: payload.ownerCertificate, diff --git a/packages/backend/src/nest/libp2p/libp2p.service.spec.ts b/packages/backend/src/nest/libp2p/libp2p.service.spec.ts index 6775f9a2e2..5fc4e370aa 100644 --- a/packages/backend/src/nest/libp2p/libp2p.service.spec.ts +++ b/packages/backend/src/nest/libp2p/libp2p.service.spec.ts @@ -2,8 +2,10 @@ import { Test, TestingModule } from '@nestjs/testing' import { TestModule } from '../common/test.module' import { libp2pInstanceParams } from '../common/utils' import { Libp2pModule } from './libp2p.module' -import { Libp2pService } from './libp2p.service' +import { LIBP2P_PSK_METADATA, Libp2pService } from './libp2p.service' import { Libp2pNodeParams } from './libp2p.types' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import validator from 'validator' describe('Libp2pService', () => { let module: TestingModule @@ -45,4 +47,15 @@ describe('Libp2pService', () => { const libp2pListenAddress = libp2pService.createLibp2pListenAddress('onionAddress') expect(libp2pListenAddress).toStrictEqual(`/dns4/onionAddress.onion/tcp/80/ws`) }) + + it('Generated libp2p psk matches psk composed from existing key', () => { + const generatedKey = Libp2pService.generateLibp2pPSK() + const retrievedKey = Libp2pService.generateLibp2pPSK(generatedKey.psk) + expect(generatedKey).toEqual(retrievedKey) + expect(validator.isBase64(generatedKey.psk)).toBeTruthy() + + const generatedPskBuffer = Buffer.from(generatedKey.psk, 'base64') + const expectedFullKeyString = LIBP2P_PSK_METADATA + uint8ArrayToString(generatedPskBuffer, 'base16') + expect(uint8ArrayToString(generatedKey.fullKey)).toEqual(expectedFullKeyString) + }) }) diff --git a/packages/backend/src/nest/libp2p/libp2p.service.ts b/packages/backend/src/nest/libp2p/libp2p.service.ts index 311b8f2b47..68a4fd6c7c 100644 --- a/packages/backend/src/nest/libp2p/libp2p.service.ts +++ b/packages/backend/src/nest/libp2p/libp2p.service.ts @@ -1,5 +1,4 @@ import { Inject, Injectable } from '@nestjs/common' - import { Agent } from 'https' import { createLibp2p, Libp2p } from 'libp2p' import { noise } from '@chainsafe/libp2p-noise' @@ -19,6 +18,13 @@ import Logger from '../common/logger' import { webSockets } from '../websocketOverTor' import { all } from '../websocketOverTor/filters' import { createLibp2pAddress, createLibp2pListenAddress } from '@quiet/common' +import { preSharedKey } from 'libp2p/pnet' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import crypto from 'crypto' + +const KEY_LENGTH = 32 +export const LIBP2P_PSK_METADATA = '/key/swarm/psk/1.0.0/\n/base16/\n' @Injectable() export class Libp2pService extends EventEmitter { @@ -44,6 +50,24 @@ export class Libp2pService extends EventEmitter { return createLibp2pListenAddress(address) } + public static generateLibp2pPSK(key?: string) { + /** + * Based on 'libp2p/pnet' generateKey + * + * @param key: base64 encoded psk + */ + let psk + if (key) { + psk = Buffer.from(key, 'base64') + } else { + psk = crypto.randomBytes(KEY_LENGTH) + } + + const base16StringKey = uint8ArrayToString(psk, 'base16') + const fullKey = uint8ArrayFromString(LIBP2P_PSK_METADATA + base16StringKey) + return { psk: psk.toString('base64'), fullKey } + } + public async createInstance(params: Libp2pNodeParams): Promise { if (this.libp2pInstance) { return this.libp2pInstance @@ -64,6 +88,9 @@ export class Libp2pService extends EventEmitter { addresses: { listen: params.listenAddresses, }, + connectionProtector: preSharedKey({ + psk: params.psk, + }), streamMuxers: [mplex()], connectionEncryption: [noise()], relay: { diff --git a/packages/backend/src/nest/libp2p/libp2p.types.ts b/packages/backend/src/nest/libp2p/libp2p.types.ts index d32cc0230f..8b80845033 100644 --- a/packages/backend/src/nest/libp2p/libp2p.types.ts +++ b/packages/backend/src/nest/libp2p/libp2p.types.ts @@ -14,6 +14,7 @@ export interface Libp2pNodeParams { localAddress: string targetPort: number peers: string[] + psk: Uint8Array } export interface InitLibp2pParams { 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 a419575730..1834f0a062 100644 --- a/packages/backend/src/nest/local-db/local-db.types.ts +++ b/packages/backend/src/nest/local-db/local-db.types.ts @@ -2,5 +2,6 @@ export enum LocalDBKeys { COMMUNITY = 'community', REGISTRAR = 'registrar', PEERS = 'peers', + PSK = 'psk', } export type LocalDbStatus = 'opening' | 'open' | 'closing' | 'closed' diff --git a/packages/backend/src/nest/socket/socket.service.ts b/packages/backend/src/nest/socket/socket.service.ts index 5d687fe2a2..b1218cb7ec 100644 --- a/packages/backend/src/nest/socket/socket.service.ts +++ b/packages/backend/src/nest/socket/socket.service.ts @@ -207,6 +207,10 @@ export class SocketService extends EventEmitter implements OnModuleInit { this.logger('Leaving community') this.emit(SocketActionTypes.LEAVE_COMMUNITY) }) + socket.on(SocketActionTypes.LIBP2P_PSK_SAVED, payload => { + this.logger('Saving PSK', payload) + this.emit(SocketActionTypes.LIBP2P_PSK_SAVED, payload) + }) }) } diff --git a/packages/backend/src/nest/storage/storage.service.ts b/packages/backend/src/nest/storage/storage.service.ts index 0068130d95..fdef474a25 100644 --- a/packages/backend/src/nest/storage/storage.service.ts +++ b/packages/backend/src/nest/storage/storage.service.ts @@ -382,7 +382,6 @@ export class StorageService extends EventEmitter { }) this.certificates.events.on('write', async (_address, entry) => { this.logger('Saved certificate locally') - this.logger(entry.payload.value) this.emit(StorageEvents.LOAD_CERTIFICATES, { certificates: this.getAllEventLogEntries(this.certificates), }) @@ -526,9 +525,7 @@ export class StorageService extends EventEmitter { // @ts-expect-error - OrbitDB's type declaration of `load` lacks 'options' await this.channels.load({ fetchEntryTimeout: 2000 }) - const channels = Object.values(this.channels.all).map(channel => { - return this.transformChannel(channel) - }) + const channels = Object.values(this.channels.all) const keyValueChannels: { [key: string]: PublicChannel @@ -549,10 +546,9 @@ export class StorageService extends EventEmitter { // @ts-expect-error - OrbitDB's type declaration of `load` lacks 'options' await this.channels.load({ fetchEntryTimeout: 1000 }) - this.logger('ALL CHANNELS COUNT:', Object.keys(this.channels.all).length) - this.logger('ALL CHANNELS COUNT:', Object.keys(this.channels.all)) + this.logger('Channels count:', Object.keys(this.channels.all).length) + this.logger('Channels names:', Object.keys(this.channels.all)) Object.values(this.channels.all).forEach(async (channel: PublicChannel) => { - channel = this.transformChannel(channel) await this.subscribeToChannel(channel) }) this.logger('STORAGE: Finished createDbForChannels') @@ -629,7 +625,7 @@ export class StorageService extends EventEmitter { db.events.on('replicate.progress', async (address, _hash, entry, progress, total) => { this.logger(`progress ${progress as string}/${total as string}. Address: ${address as string}`) - const messages = this.transformMessages([entry.payload.value]) + const messages = [entry.payload.value] const verified = await this.verifyMessage(messages[0]) @@ -691,48 +687,14 @@ export class StorageService extends EventEmitter { }) } - public transformMessages(msgs: ChannelMessage[]) { - console.log('---------------- TRANSFORMING MESSAGES ----------------------') - const messages = msgs.map(msg => { - console.log('processing message ', msg.id) - // @ts-ignore - if (msg.channelAddress) { - console.log('message before transformation ', msg) - // @ts-ignore - msg.channelId = msg.channelAddress - // @ts-ignore - delete msg.channelAddress - console.log('transformed message to new format ', msg) - return msg - } - return msg - }) - return messages - } - - public transformChannel(channel: PublicChannel) { - // @ts-ignore - if (channel.address) { - console.log('channel before transformation ', channel) - // @ts-ignore - channel.id = channel.address - // @ts-ignore - delete channel.address - console.log('transformed channel to new format ', channel) - return channel - } - return channel - } - public async askForMessages(channelId: string, ids: string[]) { const repo = this.publicChannelsRepos.get(channelId) if (!repo) return const messages = this.getAllEventLogEntries(repo.db) - let filteredMessages: ChannelMessage[] = [] + const filteredMessages: ChannelMessage[] = [] for (const id of ids) { filteredMessages.push(...messages.filter(i => i.id === id)) } - filteredMessages = this.transformMessages(filteredMessages) this.emit(StorageEvents.LOAD_MESSAGES, { messages: filteredMessages, isVerified: true, @@ -749,8 +711,7 @@ export class StorageService extends EventEmitter { } this.logger(`Creating channel ${data.id}`) - // @ts-ignore - const channelId = data.id || data.address + const channelId = data.id const db: EventStore = await this.orbitDb.log(`channels.${channelId}`, { replicate: options.replicate, @@ -956,7 +917,7 @@ export class StorageService extends EventEmitter { public getAllUsers(): UserData[] { const csrs = this.getAllEventLogEntries(this.certificatesRequests) - console.log('csrs count:', csrs.length) + this.logger('CSRs count:', csrs.length) const allUsers: UserData[] = [] for (const csr of csrs) { const parsedCert = parseCertificationRequest(csr) @@ -1044,6 +1005,10 @@ export class StorageService extends EventEmitter { this.messageThreads = undefined // @ts-ignore this.certificates = undefined + // @ts-ignore + this.certificatesRequests = undefined + // @ts-ignore + this.communityMetadata = undefined this.publicChannelsRepos = new Map() this.directMessagesRepos = new Map() this.publicKeysMap = new Map() diff --git a/packages/backend/src/nest/tor/tor.service.ts b/packages/backend/src/nest/tor/tor.service.ts index 8fbbe162de..be7183103e 100644 --- a/packages/backend/src/nest/tor/tor.service.ts +++ b/packages/backend/src/nest/tor/tor.service.ts @@ -205,21 +205,6 @@ export class Tor extends EventEmitter implements OnModuleInit { return } - console.log([ - '--SocksPort', - this.socksPort.toString(), - '--HTTPTunnelPort', - this.configOptions.httpTunnelPort?.toString(), - '--ControlPort', - this.controlPort.toString(), - '--PidFile', - this.torPidPath, - '--DataDirectory', - this.torDataDirectory, - '--HashedControlPassword', - this.torPasswordProvider.torHashedPassword, - // ...this.torProcessParams - ]) this.process = child_process.spawn( this.torParamsProvider.torPath, [ diff --git a/packages/common/CHANGELOG.md b/packages/common/CHANGELOG.md index de505e8aa5..58019f1a5d 100644 --- a/packages/common/CHANGELOG.md +++ b/packages/common/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [2.0.2-alpha.1](https://github.com/TryQuiet/quiet/compare/@quiet/common@2.0.2-alpha.0...@quiet/common@2.0.2-alpha.1) (2023-11-14) + +**Note:** Version bump only for package @quiet/common + + + + + ## [2.0.2-alpha.0](https://github.com/TryQuiet/quiet/compare/@quiet/common@2.0.1-alpha.4...@quiet/common@2.0.2-alpha.0) (2023-10-26) **Note:** Version bump only for package @quiet/common diff --git a/packages/common/package-lock.json b/packages/common/package-lock.json index 67dbf3e87e..8e6302db19 100644 --- a/packages/common/package-lock.json +++ b/packages/common/package-lock.json @@ -1,20 +1,22 @@ { "name": "@quiet/common", - "version": "2.0.2-alpha.0", + "version": "2.0.2-alpha.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@quiet/common", - "version": "2.0.2-alpha.0", + "version": "2.0.2-alpha.1", "license": "ISC", "dependencies": { "cross-env": "^5.2.0", - "debug": "^4.3.1" + "debug": "^4.3.1", + "validator": "^13.11.0" }, "devDependencies": { "@types/jest": "^26.0.23", "@types/node": "^17.0.21", + "@types/validator": "^13.11.5", "jest": "^26.6.3", "ts-jest": "^26.5.2", "typescript": "^4.9.3" @@ -1116,6 +1118,12 @@ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "dev": true }, + "node_modules/@types/validator": { + "version": "13.11.5", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.5.tgz", + "integrity": "sha512-xW4qsT4UIYILu+7ZrBnfQdBYniZrMLYYK3wN9M/NdeIHgBN5pZI2/8Q7UfdWIcr5RLJv/OGENsx91JIpUUoC7Q==", + "dev": true + }, "node_modules/@types/yargs": { "version": "15.0.15", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.15.tgz", @@ -5989,6 +5997,14 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/validator": { + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", @@ -7098,6 +7114,12 @@ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "dev": true }, + "@types/validator": { + "version": "13.11.5", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.5.tgz", + "integrity": "sha512-xW4qsT4UIYILu+7ZrBnfQdBYniZrMLYYK3wN9M/NdeIHgBN5pZI2/8Q7UfdWIcr5RLJv/OGENsx91JIpUUoC7Q==", + "dev": true + }, "@types/yargs": { "version": "15.0.15", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.15.tgz", @@ -10885,6 +10907,11 @@ "spdx-expression-parse": "^3.0.0" } }, + "validator": { + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==" + }, "w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", diff --git a/packages/common/package.json b/packages/common/package.json index 1bd4e547ac..d62236d210 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -1,6 +1,6 @@ { "name": "@quiet/common", - "version": "2.0.2-alpha.0", + "version": "2.0.2-alpha.1", "description": "Common monorepo utils", "main": "lib/index.js", "types": "lib/index.d.ts", @@ -21,14 +21,17 @@ "@quiet/eslint-config": "^2.0.2-alpha.0", "@types/jest": "^26.0.23", "@types/node": "^17.0.21", + "@types/validator": "^13.11.5", "jest": "^26.6.3", "ts-jest": "^26.5.2", "typescript": "^4.9.3" }, "dependencies": { - "@quiet/types": "^2.0.2-alpha.0", + "@quiet/logger": "^2.0.2-alpha.0", + "@quiet/types": "^2.0.2-alpha.1", "cross-env": "^5.2.0", - "debug": "^4.3.1" + "debug": "^4.3.1", + "validator": "^13.11.0" }, "jest": { "transform": { diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 67ce8a7b5b..bd92483eb7 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -8,3 +8,4 @@ export * from './channelAddress' export * from './naming' export * from './fileData' export * from './libp2p' +export * from './tests' diff --git a/packages/common/src/invitationCode.test.ts b/packages/common/src/invitationCode.test.ts index 28254563bc..7ef34257cb 100644 --- a/packages/common/src/invitationCode.test.ts +++ b/packages/common/src/invitationCode.test.ts @@ -1,9 +1,11 @@ +import { InvitationData } from '@quiet/types' import { argvInvitationCode, - invitationDeepUrl, + composeInvitationDeepUrl, invitationShareUrl, - pairsToInvitationShareUrl, - retrieveInvitationCode, + composeInvitationShareUrl, + parseInvitationCodeDeepUrl, + PSK_PARAM_KEY, } from './invitationCode' import { QUIET_JOIN_PAGE } from './static' @@ -12,64 +14,102 @@ describe('Invitation code helper', () => { const address1 = 'gloao6h5plwjy4tdlze24zzgcxll6upq2ex2fmu2ohhyu4gtys4nrjad' const peerId2 = 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE' const address2 = 'y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd' + const psk = 'BNlxfE2WBF7LrlpIX0CvECN5o1oZtA16PkAb7GYiwYw%3D' + const pskDecoded = 'BNlxfE2WBF7LrlpIX0CvECN5o1oZtA16PkAb7GYiwYw=' it('retrieves invitation code from argv', () => { - const expectedCodes = [ - { peerId: peerId1, onionAddress: address1 }, - { peerId: peerId2, onionAddress: address2 }, - ] + const expectedCodes: InvitationData = { + pairs: [ + { peerId: peerId1, onionAddress: address1 }, + { peerId: peerId2, onionAddress: address2 }, + ], + psk: pskDecoded, + } const result = argvInvitationCode([ 'something', 'quiet:/invalid', 'zbay://invalid', 'quiet://invalid', 'quiet://?param=invalid', - invitationDeepUrl(expectedCodes), + composeInvitationDeepUrl(expectedCodes), ]) expect(result).toEqual(expectedCodes) }) - it('builds proper invitation deep url', () => { + it('returns null if argv do not contain any valid invitation code', () => { + const result = argvInvitationCode([ + 'something', + 'quiet:/invalid', + 'zbay://invalid', + 'quiet://invalid', + 'quiet://?param=invalid', + ]) + expect(result).toBeNull() + }) + + it('composes proper invitation deep url', () => { expect( - invitationDeepUrl([ - { peerId: 'peerID1', onionAddress: 'address1' }, - { peerId: 'peerID2', onionAddress: 'address2' }, - ]) - ).toEqual('quiet://?peerID1=address1&peerID2=address2') + composeInvitationDeepUrl({ + pairs: [ + { peerId: 'peerID1', onionAddress: 'address1' }, + { peerId: 'peerID2', onionAddress: 'address2' }, + ], + psk: pskDecoded, + }) + ).toEqual(`quiet://?peerID1=address1&peerID2=address2&${PSK_PARAM_KEY}=${psk}`) }) - it('creates invitation share url based on invitation pairs', () => { - const pairs = [ - { peerId: 'peerID1', onionAddress: 'address1' }, - { peerId: 'peerID2', onionAddress: 'address2' }, - ] - const expected = `${QUIET_JOIN_PAGE}#peerID1=address1&peerID2=address2` - expect(pairsToInvitationShareUrl(pairs)).toEqual(expected) + it('creates invitation share url based on invitation data', () => { + const pairs: InvitationData = { + pairs: [ + { peerId: 'peerID1', onionAddress: 'address1' }, + { peerId: 'peerID2', onionAddress: 'address2' }, + ], + psk: pskDecoded, + } + const expected = `${QUIET_JOIN_PAGE}#peerID1=address1&peerID2=address2&${PSK_PARAM_KEY}=${psk}` + expect(composeInvitationShareUrl(pairs)).toEqual(expected) }) - it('builds proper invitation share url', () => { + it('builds proper invitation share url from peers addresses', () => { const peerList = [ '/dns4/gloao6h5plwjy4tdlze24zzgcxll6upq2ex2fmu2ohhyu4gtys4nrjad.onion/tcp/443/wss/p2p/QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE', 'invalidAddress', '/dns4/somethingElse.onion/tcp/443/wss/p2p/QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSA', ] - expect(invitationShareUrl(peerList)).toEqual( - `${QUIET_JOIN_PAGE}#QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE=gloao6h5plwjy4tdlze24zzgcxll6upq2ex2fmu2ohhyu4gtys4nrjad&QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSA=somethingElse` + expect(invitationShareUrl(peerList, pskDecoded)).toEqual( + `${QUIET_JOIN_PAGE}#QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE=gloao6h5plwjy4tdlze24zzgcxll6upq2ex2fmu2ohhyu4gtys4nrjad&QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSA=somethingElse&${PSK_PARAM_KEY}=${psk}` ) }) it('retrieves invitation codes from deep url', () => { - const codes = retrieveInvitationCode(`quiet://?${peerId1}=${address1}&${peerId2}=${address2}`) - expect(codes).toEqual([ - { peerId: peerId1, onionAddress: address1 }, - { peerId: peerId2, onionAddress: address2 }, - ]) + const codes = parseInvitationCodeDeepUrl( + `quiet://?${peerId1}=${address1}&${peerId2}=${address2}&${PSK_PARAM_KEY}=${psk}` + ) + expect(codes).toEqual({ + pairs: [ + { peerId: peerId1, onionAddress: address1 }, + { peerId: peerId2, onionAddress: address2 }, + ], + psk: pskDecoded, + }) }) + it.each([['12345'], ['a2FzemE='], 'a2FycGllIHcgZ2FsYXJlY2llIGVjaWUgcGVjaWUgYWxlIGkgdGFrIHpqZWNpZQ=='])( + 'parsing invitation code throws error if psk is invalid: (%s)', + (psk: string) => { + expect(() => { + parseInvitationCodeDeepUrl(`quiet://?${peerId1}=${address1}&${peerId2}=${address2}&${PSK_PARAM_KEY}=${psk}`) + }).toThrow() + } + ) + it('retrieves invitation codes from deep url with partly invalid codes', () => { const peerId2 = 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLs' const address2 = 'y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd' - const codes = retrieveInvitationCode(`quiet://?${peerId1}=${address1}&${peerId2}=${address2}}`) - expect(codes).toEqual([{ peerId: peerId1, onionAddress: address1 }]) + const parsed = parseInvitationCodeDeepUrl( + `quiet://?${peerId1}=${address1}&${peerId2}=${address2}&${PSK_PARAM_KEY}=${psk}` + ) + expect(parsed).toEqual({ pairs: [{ peerId: peerId1, onionAddress: address1 }], psk: pskDecoded }) }) }) diff --git a/packages/common/src/invitationCode.ts b/packages/common/src/invitationCode.ts index e5e6af26f7..10ecf79dd9 100644 --- a/packages/common/src/invitationCode.ts +++ b/packages/common/src/invitationCode.ts @@ -1,38 +1,86 @@ -import { InvitationPair } from '@quiet/types' -import { ONION_ADDRESS_REGEX, Site } from './static' -import { createLibp2pAddress } from './libp2p' -export const retrieveInvitationCode = (url: string): InvitationPair[] => { - /** - * Extract invitation codes from deep url. - * Valid format: quiet://?=&= - */ - let data: URL +import { InvitationData, InvitationPair } from '@quiet/types' +import { QUIET_JOIN_PAGE } from './static' +import { createLibp2pAddress, isPSKcodeValid } from './libp2p' +import Logger from './logger' +const logger = Logger('invite') + +export const PSK_PARAM_KEY = 'k' +const DEEP_URL_SCHEME_WITH_SEPARATOR = 'quiet://' +const DEEP_URL_SCHEME = 'quiet' +const ONION_ADDRESS_REGEX = /^[a-z0-9]{56}$/g +const PEER_ID_REGEX = /^[a-zA-Z0-9]{46}$/g + +interface ParseDeepUrlParams { + url: string + expectedProtocol?: string +} + +const parseDeepUrl = ({ url, expectedProtocol = `${DEEP_URL_SCHEME}:` }: ParseDeepUrlParams): InvitationData => { + let _url = url + let validUrl: URL | null = null + + if (!expectedProtocol) { + // Create a full url to be able to use the same URL parsing mechanism + expectedProtocol = `${DEEP_URL_SCHEME}:` + _url = `${DEEP_URL_SCHEME}://?${url}` + } + try { - data = new URL(url) + validUrl = new URL(_url) } catch (e) { - return [] + logger.error(`Could not retrieve invitation code from deep url '${url}'. Reason: ${e.message}`) + throw e } - if (!data || data.protocol !== 'quiet:') return [] - const params = data.searchParams + if (!validUrl || validUrl.protocol !== expectedProtocol) { + logger.error(`Could not retrieve invitation code from deep url '${url}'`) + throw new Error(`Invalid url`) + } + const params = validUrl.searchParams const codes: InvitationPair[] = [] - for (const [peerId, onionAddress] of params.entries()) { - if (!invitationCodeValid(peerId, onionAddress)) continue + let psk = params.get(PSK_PARAM_KEY) + if (!psk) throw new Error(`No psk found in invitation code '${url}'`) + + psk = decodeURIComponent(psk) + if (!isPSKcodeValid(psk)) throw new Error(`Invalid psk in invitation code '${url}'`) + + params.delete(PSK_PARAM_KEY) + + params.forEach((onionAddress, peerId) => { + if (!peerDataValid({ peerId, onionAddress })) return codes.push({ peerId, onionAddress, }) + }) + logger('Retrieved data:', codes) + return { + pairs: codes, + psk: psk, } - console.log('Retrieved codes:', codes) - return codes } -export const invitationShareUrl = (peers: string[] = []): string => { - /** - * @arg {string[]} peers - List of peer's p2p addresses - * @returns {string} - Complete shareable invitation link, e.g. https://tryquiet.org/join/#=&= - */ - console.log('Invitation share url, peers:', peers) - const pairs = [] +/** + * Extract invitation data from deep url. + * Valid format: quiet://?=&=&k= + */ +export const parseInvitationCodeDeepUrl = (url: string): InvitationData => { + return parseDeepUrl({ url }) +} + +/** + * @param code =&=&k= + */ +export const parseInvitationCode = (code: string): InvitationData => { + return parseDeepUrl({ url: code, expectedProtocol: '' }) +} + +/** + * @arg {string[]} peers - List of peer's p2p addresses + * @arg psk - Pre shared key in base64 + * @returns {string} - Complete shareable invitation link, e.g. https://tryquiet.org/join/#=&=&k= + */ +export const invitationShareUrl = (peers: string[] = [], psk: string): string => { + const pairs: InvitationPair[] = [] for (const peerAddress of peers) { let peerId: string let onionAddress: string @@ -54,12 +102,10 @@ export const invitationShareUrl = (peers: string[] = []): string => { continue } const rawAddress = onionAddress.endsWith('.onion') ? onionAddress.split('.')[0] : onionAddress - pairs.push(`${peerId}=${rawAddress}`) + pairs.push({ peerId: peerId, onionAddress: rawAddress }) } - console.log('invitationShareUrl', pairs.join('&')) - const url = new URL(`${Site.MAIN_PAGE}${Site.JOIN_PAGE}#${pairs.join('&')}`) - return url.href + return composeInvitationShareUrl({ pairs: pairs, psk: psk }) } export const pairsToP2pAddresses = (pairs: InvitationPair[]): string[] => { @@ -70,63 +116,52 @@ export const pairsToP2pAddresses = (pairs: InvitationPair[]): string[] => { return addresses } -export const pairsToInvitationShareUrl = (pairs: InvitationPair[]) => { - const url = new URL(`${Site.MAIN_PAGE}${Site.JOIN_PAGE}`) - for (const pair of pairs) { - url.searchParams.append(pair.peerId, pair.onionAddress) - } - return url.href.replace('?', '#') +export const composeInvitationShareUrl = (data: InvitationData) => { + return composeInvitationUrl(`${QUIET_JOIN_PAGE}`, data).replace('?', '#') } -export const invitationDeepUrl = (pairs: InvitationPair[] = []): string => { - const url = new URL('quiet://') - for (const pair of pairs) { +export const composeInvitationDeepUrl = (data: InvitationData): string => { + return composeInvitationUrl(`${DEEP_URL_SCHEME_WITH_SEPARATOR}`, data) +} + +const composeInvitationUrl = (baseUrl: string, data: InvitationData): string => { + const url = new URL(baseUrl) + for (const pair of data.pairs) { url.searchParams.append(pair.peerId, pair.onionAddress) } + url.searchParams.append(PSK_PARAM_KEY, data.psk) return url.href } -export const argvInvitationCode = (argv: string[]): InvitationPair[] => { - /** - * Extract invitation codes from deep url if url is present in argv - */ - let invitationCodes: InvitationPair[] = [] +/** + * Extract invitation codes from deep url if url is present in argv + */ +export const argvInvitationCode = (argv: string[]): InvitationData | null => { + let invitationData: InvitationData | null = null for (const arg of argv) { - invitationCodes = retrieveInvitationCode(arg) - if (invitationCodes.length > 0) { + try { + invitationData = parseInvitationCodeDeepUrl(arg) + } catch (e) { + continue + } + if (invitationData.pairs.length > 0) { break + } else { + invitationData = null } } - return invitationCodes + return invitationData } -export const invitationCodeValid = (peerId: string, onionAddress: string): boolean => { - if (!peerId.match(/^[a-zA-Z0-9]{46}$/g)) { +const peerDataValid = ({ peerId, onionAddress }: { peerId: string; onionAddress: string }): boolean => { + if (!peerId.match(PEER_ID_REGEX)) { // TODO: test it more properly e.g with PeerId.createFromB58String(peerId.trim()) - console.log(`PeerId ${peerId} is not valid`) + logger(`PeerId ${peerId} is not valid`) return false } if (!onionAddress.trim().match(ONION_ADDRESS_REGEX)) { - console.log(`Onion address ${onionAddress} is not valid`) + logger(`Onion address ${onionAddress} is not valid`) return false } return true } - -export const getInvitationPairs = (code: string) => { - /** - * @param code =&= - */ - const pairs = code.split('&') - const codes: InvitationPair[] = [] - for (const pair of pairs) { - const [peerId, address] = pair.split('=') - if (!peerId || !address) continue - if (!invitationCodeValid(peerId, address)) continue - codes.push({ - peerId: peerId, - onionAddress: address, - }) - } - return codes -} diff --git a/packages/common/src/libp2p.ts b/packages/common/src/libp2p.ts index e86ee508e2..b2fb70e83f 100644 --- a/packages/common/src/libp2p.ts +++ b/packages/common/src/libp2p.ts @@ -1,3 +1,6 @@ +import validator from 'validator' +export const PSK_LENGTH = 44 // PSK is 256 bits/8 = 32 bytes which encodes to 44 characters base64 + const ONION = '.onion' export const createLibp2pAddress = (address: string, peerId: string) => { @@ -9,3 +12,8 @@ export const createLibp2pListenAddress = (address: string) => { if (!address.endsWith(ONION)) address += ONION return `/dns4/${address}/tcp/80/ws` } + +export const isPSKcodeValid = (psk: string): boolean => { + const _psk = psk.trim() + return validator.isBase64(_psk) && _psk.length === PSK_LENGTH +} diff --git a/packages/common/src/logger.ts b/packages/common/src/logger.ts new file mode 100644 index 0000000000..31360daef5 --- /dev/null +++ b/packages/common/src/logger.ts @@ -0,0 +1,7 @@ +import { logger } from '@quiet/logger' + +const createLogger = (name: string) => { + return logger('utils')(name) +} + +export default createLogger diff --git a/packages/common/src/static.ts b/packages/common/src/static.ts index 05b1f35f0a..270bcdee89 100644 --- a/packages/common/src/static.ts +++ b/packages/common/src/static.ts @@ -1,5 +1,3 @@ -export const ONION_ADDRESS_REGEX = /^[a-z0-9]{56}$/g - export enum Site { DOMAIN = 'tryquiet.org', MAIN_PAGE = 'https://tryquiet.org/', diff --git a/packages/common/src/tests.ts b/packages/common/src/tests.ts new file mode 100644 index 0000000000..2cd38af1cf --- /dev/null +++ b/packages/common/src/tests.ts @@ -0,0 +1,20 @@ +import { InvitationData } from '@quiet/types' +import { composeInvitationDeepUrl, composeInvitationShareUrl } from './invitationCode' +import { QUIET_JOIN_PAGE } from './static' + +const validInvitationCodeTestData: InvitationData = { + pairs: [ + { + onionAddress: 'y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd', + peerId: 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE', + }, + ], + psk: 'BNlxfE2WBF7LrlpIX0CvECN5o1oZtA16PkAb7GYiwYw=', +} + +export const validInvitationUrlTestData = { + shareUrl: () => composeInvitationShareUrl(validInvitationCodeTestData), + deepUrl: () => composeInvitationDeepUrl(validInvitationCodeTestData), + code: () => composeInvitationShareUrl(validInvitationCodeTestData).split(QUIET_JOIN_PAGE + '#')[1], + data: validInvitationCodeTestData, +} diff --git a/packages/desktop/CHANGELOG.md b/packages/desktop/CHANGELOG.md index 7508693170..78943be782 100644 --- a/packages/desktop/CHANGELOG.md +++ b/packages/desktop/CHANGELOG.md @@ -3,6 +3,25 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [2.0.3-alpha.5](https://github.com/TryQuiet/quiet/compare/@quiet/desktop@2.0.3-alpha.4...@quiet/desktop@2.0.3-alpha.5) (2023-11-14) + +**Note:** Version bump only for package @quiet/desktop + + + + + +## [2.0.3-alpha.4](https://github.com/TryQuiet/quiet/compare/@quiet/desktop@2.0.3-alpha.3...@quiet/desktop@2.0.3-alpha.4) (2023-11-14) + + +### Features + +* trigger desktop ([713e1b8](https://github.com/TryQuiet/quiet/commit/713e1b822b266c218f71742ed68616b8b1056c75)) + + + + + ## [2.0.3-alpha.3](https://github.com/TryQuiet/quiet/compare/@quiet/desktop@2.0.3-alpha.2...@quiet/desktop@2.0.3-alpha.3) (2023-11-13) diff --git a/packages/desktop/package-lock.json b/packages/desktop/package-lock.json index a412d51019..62347f7ab7 100644 --- a/packages/desktop/package-lock.json +++ b/packages/desktop/package-lock.json @@ -1,12 +1,12 @@ { "name": "@quiet/desktop", - "version": "2.0.3-alpha.3", + "version": "2.0.3-alpha.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@quiet/desktop", - "version": "2.0.3-alpha.3", + "version": "2.0.3-alpha.5", "license": "ISC", "dependencies": { "@electron/remote": "^2.0.8", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index c15fe02713..42dd73f5ff 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -80,7 +80,7 @@ }, "homepage": "https://github.com/TryQuiet", "@comment version": "To build new version for specific platform, just replace platform in version tag to one of following linux, mac, windows", - "version": "2.0.3-alpha.3", + "version": "2.0.3-alpha.5", "description": "Decentralized team chat", "main": "dist/main/main.js", "scripts": { @@ -111,7 +111,7 @@ "build:renderer:prod": "webpack --config webpack/webpack.config.renderer.prod.js", "postBuild": "node scripts/postBuild.js", "prestart": "npm run build:main", - "start": "cross-env DEBUG='backend*,quiet*,state-manager*,desktop*,libp2p:websockets:listener:backend' npm run start:renderer", + "start": "cross-env DEBUG='backend*,quiet*,state-manager*,desktop*,utils*,libp2p:websockets:listener:backend' npm run start:renderer", "start:main": "cross-env NODE_ENV=development electron .", "start:renderer": "cross-env NODE_ENV=development webpack-dev-server --config webpack/webpack.config.renderer.dev.js", "storybook": "export NODE_OPTIONS=--openssl-legacy-provider && start-storybook -p 6006", @@ -125,9 +125,9 @@ "dependencies": { "@electron/remote": "^2.0.8", "@peculiar/webcrypto": "1.4.3", - "@quiet/common": "^2.0.2-alpha.0", + "@quiet/common": "^2.0.2-alpha.1", "@quiet/logger": "^2.0.2-alpha.0", - "@quiet/types": "^2.0.2-alpha.0", + "@quiet/types": "^2.0.2-alpha.1", "@sentry/electron": "^2.5.4", "backend-bundle": "^2.0.1-alpha.4", "electron-debug": "^3.0.1", @@ -157,8 +157,8 @@ "@mui/lab": "^5.0.0-alpha.109", "@mui/material": "~5.10.15", "@quiet/eslint-config": "^2.0.2-alpha.0", - "@quiet/identity": "^2.0.2-alpha.0", - "@quiet/state-manager": "^2.0.2-alpha.1", + "@quiet/identity": "^2.0.2-alpha.2", + "@quiet/state-manager": "^2.0.2-alpha.3", "@redux-saga/types": "^1.1.0", "@reduxjs/toolkit": "^1.9.1", "@sentry/browser": "^6.19.7", diff --git a/packages/desktop/src/main/invitation.ts b/packages/desktop/src/main/invitation.ts index 39bf432962..2616059683 100644 --- a/packages/desktop/src/main/invitation.ts +++ b/packages/desktop/src/main/invitation.ts @@ -3,15 +3,15 @@ import path from 'path' import os from 'os' import { execSync } from 'child_process' import { BrowserWindow } from 'electron' -import { InvitationPair } from '@quiet/types' +import { InvitationData, InvitationPair } from '@quiet/types' -export const processInvitationCode = (mainWindow: BrowserWindow, codes: InvitationPair[]) => { - if (codes.length === 0) { +export const processInvitationCode = (mainWindow: BrowserWindow, data: InvitationData | null) => { + if (!data || data?.pairs.length === 0) { console.log('No valid invitation codes, not processing') return } mainWindow.webContents.send('invitation', { - codes, + data, }) } diff --git a/packages/desktop/src/main/main.test.ts b/packages/desktop/src/main/main.test.ts index 0239a05cd0..8f136a3e0b 100644 --- a/packages/desktop/src/main/main.test.ts +++ b/packages/desktop/src/main/main.test.ts @@ -5,7 +5,8 @@ import { autoUpdater } from 'electron-updater' import { BrowserWindow, app, ipcMain, Menu } from 'electron' import { waitFor } from '@testing-library/dom' import path from 'path' -import { invitationDeepUrl } from '@quiet/common' +import { composeInvitationDeepUrl, validInvitationUrlTestData } from '@quiet/common' +import { InvitationData } from '@quiet/types' // eslint-disable-next-line const remote = require('@electron/remote/main') @@ -237,33 +238,39 @@ describe('other electron app events ', () => { }) describe('Invitation code', () => { + let codes: InvitationData + + beforeEach(() => { + codes = { ...validInvitationUrlTestData.data } + }) + it('handles invitation code on open-url event (on macos)', async () => { expect(mockAppOnCalls[2][0]).toBe('ready') await mockAppOnCalls[2][1]() - const codes = [ - { - peerId: 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE', - onionAddress: 'y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd', - }, - ] + + expect(mockAppOnCalls[1][0]).toBe('open-url') + const event = { preventDefault: () => {} } + mockAppOnCalls[1][1](event, composeInvitationDeepUrl(codes)) + expect(mockWindowWebContentsSend).toHaveBeenCalledWith('invitation', { data: codes }) + }) + + it('do not process invitation code on open-url event (on macos) if url is invalid', async () => { + codes['psk'] = '12345' + expect(mockAppOnCalls[2][0]).toBe('ready') + await mockAppOnCalls[2][1]() + expect(mockAppOnCalls[1][0]).toBe('open-url') const event = { preventDefault: () => {} } - mockAppOnCalls[1][1](event, invitationDeepUrl(codes)) - expect(mockWindowWebContentsSend).toHaveBeenCalledWith('invitation', { codes }) + mockAppOnCalls[1][1](event, composeInvitationDeepUrl(codes)) + expect(mockWindowWebContentsSend).not.toHaveBeenCalledWith('invitation', { data: codes }) }) it('process invitation code on second-instance event', async () => { - const codes = [ - { - peerId: 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE', - onionAddress: 'y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd', - }, - ] await mockAppOnCalls[2][1]() - const commandLine = ['/tmp/.mount_Quiet-TVQc6s/quiet', invitationDeepUrl(codes)] + const commandLine = ['/tmp/.mount_Quiet-TVQc6s/quiet', composeInvitationDeepUrl(codes)] expect(mockAppOnCalls[0][0]).toBe('second-instance') const event = { preventDefault: () => {} } mockAppOnCalls[0][1](event, commandLine) - expect(mockWindowWebContentsSend).toHaveBeenCalledWith('invitation', { codes }) + expect(mockWindowWebContentsSend).toHaveBeenCalledWith('invitation', { data: codes }) }) }) diff --git a/packages/desktop/src/main/main.ts b/packages/desktop/src/main/main.ts index b8c20310f4..c6c74db976 100644 --- a/packages/desktop/src/main/main.ts +++ b/packages/desktop/src/main/main.ts @@ -11,7 +11,7 @@ import { Crypto } from '@peculiar/webcrypto' import logger from './logger' import { DATA_DIR, DEV_DATA_DIR } from '../shared/static' import { fork, ChildProcess } from 'child_process' -import { argvInvitationCode, getFilesData, retrieveInvitationCode } from '@quiet/common' +import { argvInvitationCode, getFilesData, parseInvitationCodeDeepUrl } from '@quiet/common' import { updateDesktopFile, processInvitationCode } from './invitation' const ElectronStore = require('electron-store') ElectronStore.initRenderer() @@ -147,8 +147,12 @@ app.on('open-url', (event, url) => { event.preventDefault() if (mainWindow) { invitationUrl = null - const invitationCode = retrieveInvitationCode(url) - processInvitationCode(mainWindow, invitationCode) + try { + const invitationCode = parseInvitationCodeDeepUrl(url) + processInvitationCode(mainWindow, invitationCode) + } catch (e) { + console.warn(e.message) + } } }) @@ -474,13 +478,22 @@ app.on('ready', async () => { throw new Error(`mainWindow is on unexpected type ${mainWindow}`) } if (process.platform === 'darwin' && invitationUrl) { - const invitationCode = retrieveInvitationCode(invitationUrl) - processInvitationCode(mainWindow, invitationCode) - invitationUrl = null + try { + const invitationCode = parseInvitationCodeDeepUrl(invitationUrl) + processInvitationCode(mainWindow, invitationCode) + } catch (e) { + console.warn(e.message) + } finally { + invitationUrl = null + } } if (process.platform !== 'darwin' && process.argv) { - const invitationCode = argvInvitationCode(process.argv) - processInvitationCode(mainWindow, invitationCode) + try { + const invitationCode = argvInvitationCode(process.argv) + processInvitationCode(mainWindow, invitationCode) + } catch (e) { + console.warn(e.message) + } } await checkForUpdate(mainWindow) diff --git a/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.test.tsx b/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.test.tsx index a9752d46f8..db777c9a6b 100644 --- a/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.test.tsx +++ b/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.test.tsx @@ -17,17 +17,12 @@ import PerformCommunityActionComponent from '../PerformCommunityActionComponent' import { inviteLinkField } from '../../../forms/fields/communityFields' import { InviteLinkErrors } from '../../../forms/fieldsErrors' import { CommunityOwnership } from '@quiet/types' -import { Site, QUIET_JOIN_PAGE } from '@quiet/common' +import { Site, QUIET_JOIN_PAGE, validInvitationUrlTestData, PSK_PARAM_KEY } from '@quiet/common' describe('join community', () => { - const validCode = - 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE=y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd' - const validPair = [ - { - onionAddress: 'y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd', - peerId: 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE', - }, - ] + const validCode = validInvitationUrlTestData.code() + const validData = validInvitationUrlTestData.data + const psk = validInvitationUrlTestData.data.psk it('users switches from join to create', async () => { const { store } = await prepareStore({ @@ -106,9 +101,6 @@ describe('join community', () => { }) it('joins community on submit if connection is ready and registrar url is correct', async () => { - const registrarUrl = - 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE=y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd' - const handleCommunityAction = jest.fn() const component = ( @@ -129,20 +121,13 @@ describe('join community', () => { const textInput = result.queryByPlaceholderText(inviteLinkField().fieldProps.placeholder) expect(textInput).not.toBeNull() // @ts-expect-error - await userEvent.type(textInput, registrarUrl) + await userEvent.type(textInput, validCode) const submitButton = result.getByText('Continue') expect(submitButton).toBeEnabled() await userEvent.click(submitButton) - await waitFor(() => - expect(handleCommunityAction).toBeCalledWith([ - { - peerId: 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE', - onionAddress: 'y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd', - }, - ]) - ) + await waitFor(() => expect(handleCommunityAction).toBeCalledWith(validData)) }) it.each([[`${QUIET_JOIN_PAGE}#${validCode}`], [`${QUIET_JOIN_PAGE}/#${validCode}`]])( @@ -176,13 +161,12 @@ describe('join community', () => { expect(submitButton).toBeEnabled() await userEvent.click(submitButton) - await waitFor(() => expect(handleCommunityAction).toBeCalledWith(validPair)) + await waitFor(() => expect(handleCommunityAction).toBeCalledWith(validData)) } ) it('trims whitespaces from registrar url', async () => { - const registrarUrl = - 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE=y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd ' + const registrarUrl = validCode + ' ' const handleCommunityAction = jest.fn() @@ -210,19 +194,12 @@ describe('join community', () => { expect(submitButton).toBeEnabled() await userEvent.click(submitButton) - await waitFor(() => - expect(handleCommunityAction).toBeCalledWith([ - { - peerId: 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE', - onionAddress: 'y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd', - }, - ]) - ) + await waitFor(() => expect(handleCommunityAction).toBeCalledWith(validData)) }) it.each([ [`http://${validCode}`, InviteLinkErrors.InvalidCode], - ['QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE=bbb', InviteLinkErrors.InvalidCode], + [`QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE=bbb&${PSK_PARAM_KEY}=${psk}`, InviteLinkErrors.InvalidCode], ['bbb=y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd', InviteLinkErrors.InvalidCode], ['QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE= ', InviteLinkErrors.InvalidCode], ['nqnw4kc4c77fb47lk52m5l57h4tc', InviteLinkErrors.InvalidCode], diff --git a/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.tsx b/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.tsx index 590c156805..9dd23ee7aa 100644 --- a/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.tsx +++ b/packages/desktop/src/renderer/components/CreateJoinCommunity/JoinCommunity/JoinCommunity.tsx @@ -1,8 +1,8 @@ import React, { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { socketSelectors } from '../../../sagas/socket/socket.selectors' -import { CommunityOwnership, CreateNetworkPayload, InvitationPair } from '@quiet/types' -import { communities, identity, connection } from '@quiet/state-manager' +import { CommunityOwnership, CreateNetworkPayload, InvitationData, InvitationPair } from '@quiet/types' +import { communities, identity, connection, network } from '@quiet/state-manager' import PerformCommunityActionComponent from '../../../components/CreateJoinCommunity/PerformCommunityActionComponent' import { ModalName } from '../../../sagas/modals/modals.types' import { useModal } from '../../../containers/hooks' @@ -15,7 +15,9 @@ const JoinCommunity = () => { const currentCommunity = useSelector(communities.selectors.currentCommunity) const currentIdentity = useSelector(identity.selectors.currentIdentity) - const invitationCode = useSelector(communities.selectors.invitationCodes) + // Invitation link data should be already available if user joined via deep link + const invitationCodes = useSelector(communities.selectors.invitationCodes) + const psk = useSelector(communities.selectors.psk) const joinCommunityModal = useModal(ModalName.joinCommunityModal) const createCommunityModal = useModal(ModalName.createCommunityModal) @@ -36,10 +38,11 @@ const JoinCommunity = () => { } }, [currentCommunity]) - const handleCommunityAction = (address: InvitationPair[]) => { + const handleCommunityAction = (data: InvitationData) => { const payload: CreateNetworkPayload = { ownership: CommunityOwnership.User, - peers: address, + peers: data.pairs, + psk: data.psk, } dispatch(communities.actions.createNetwork(payload)) } @@ -68,7 +71,8 @@ const JoinCommunity = () => { hasReceivedResponse={Boolean(currentIdentity && !currentIdentity.userCertificate)} revealInputValue={revealInputValue} handleClickInputReveal={handleClickInputReveal} - invitationCode={invitationCode} + invitationCode={invitationCodes} + psk={psk} /> ) } diff --git a/packages/desktop/src/renderer/components/CreateJoinCommunity/PerformCommunityActionComponent.tsx b/packages/desktop/src/renderer/components/CreateJoinCommunity/PerformCommunityActionComponent.tsx index 3ec4ac037f..144a3b2a9e 100644 --- a/packages/desktop/src/renderer/components/CreateJoinCommunity/PerformCommunityActionComponent.tsx +++ b/packages/desktop/src/renderer/components/CreateJoinCommunity/PerformCommunityActionComponent.tsx @@ -12,7 +12,7 @@ import { LoadingButton } from '../ui/LoadingButton/LoadingButton' import { CreateCommunityDictionary, JoinCommunityDictionary } from '../CreateJoinCommunity/community.dictionary' -import { CommunityOwnership, InvitationPair } from '@quiet/types' +import { CommunityOwnership, InvitationData, InvitationPair } from '@quiet/types' import { Controller, useForm } from 'react-hook-form' import { TextInput } from '../../forms/components/textInput' @@ -20,7 +20,7 @@ import { InviteLinkErrors } from '../../forms/fieldsErrors' import { IconButton, InputAdornment } from '@mui/material' import VisibilityOff from '@mui/icons-material/VisibilityOff' import Visibility from '@mui/icons-material/Visibility' -import { ONION_ADDRESS_REGEX, pairsToInvitationShareUrl, parseName } from '@quiet/common' +import { composeInvitationShareUrl, parseName } from '@quiet/common' import { getInvitationCodes } from '@quiet/state-manager' const PREFIX = 'PerformCommunityActionComponent' @@ -138,6 +138,7 @@ export interface PerformCommunityActionProps { revealInputValue?: boolean handleClickInputReveal?: () => void invitationCode?: InvitationPair[] + psk?: string } export const PerformCommunityActionComponent: React.FC = ({ @@ -152,6 +153,7 @@ export const PerformCommunityActionComponent: React.FC { const [formSent, setFormSent] = useState(false) @@ -190,14 +192,20 @@ export const PerformCommunityActionComponent: React.FC { - if (communityOwnership === CommunityOwnership.User && invitationCode?.length) { + if (communityOwnership === CommunityOwnership.User && invitationCode?.length && psk) { setFormSent(true) - setValue('name', pairsToInvitationShareUrl(invitationCode)) + setValue('name', composeInvitationShareUrl({ pairs: invitationCode, psk: psk })) } }, [communityOwnership, invitationCode]) diff --git a/packages/desktop/src/renderer/components/Settings/Settings.stories.tsx b/packages/desktop/src/renderer/components/Settings/Settings.stories.tsx index 4b3e96a4bc..80e8663143 100644 --- a/packages/desktop/src/renderer/components/Settings/Settings.stories.tsx +++ b/packages/desktop/src/renderer/components/Settings/Settings.stories.tsx @@ -10,18 +10,21 @@ import { InviteComponent } from './Tabs/Invite/Invite.component' import { LeaveCommunityComponent } from './Tabs/LeaveCommunity/LeaveCommunityComponent' import { Typography } from '@mui/material' import { QRCodeComponent } from './Tabs/QRCode/QRCode.component' -import { pairsToInvitationShareUrl } from '@quiet/common' - -const invitationLink = pairsToInvitationShareUrl([ - { - peerId: 'QmVTkUad2Gq3MkCa8gf12R1gsWDfk2yiTEqb6YGXDG2iQ3', - onionAddress: 'p3oqdr53dkgg3n5nuezlzyawhxvit5efxzlunvzp7n7lmva6fj3i43ad', - }, - { - peerId: 'Qmd2Un9AynokZrcZGsMuaqgupTtidHGQnUkNVfFFAef97C', - onionAddress: 'vnywuiyl7p7ig2murcscdyzksko53e4k3dpdm2yoopvvu25p6wwjqbad', - }, -]) +import { composeInvitationShareUrl } from '@quiet/common' + +const invitationLink = composeInvitationShareUrl({ + pairs: [ + { + peerId: 'QmVTkUad2Gq3MkCa8gf12R1gsWDfk2yiTEqb6YGXDG2iQ3', + onionAddress: 'p3oqdr53dkgg3n5nuezlzyawhxvit5efxzlunvzp7n7lmva6fj3i43ad', + }, + { + peerId: 'Qmd2Un9AynokZrcZGsMuaqgupTtidHGQnUkNVfFFAef97C', + onionAddress: 'vnywuiyl7p7ig2murcscdyzksko53e4k3dpdm2yoopvvu25p6wwjqbad', + }, + ], + psk: '12345', +}) const Template: ComponentStory = args => { return diff --git a/packages/desktop/src/renderer/components/Settings/Tabs/Invite/Invite.component.test.tsx b/packages/desktop/src/renderer/components/Settings/Tabs/Invite/Invite.component.test.tsx index 926d0e39fe..a258ee8331 100644 --- a/packages/desktop/src/renderer/components/Settings/Tabs/Invite/Invite.component.test.tsx +++ b/packages/desktop/src/renderer/components/Settings/Tabs/Invite/Invite.component.test.tsx @@ -2,28 +2,31 @@ import '@testing-library/jest-dom' import React from 'react' import { renderComponent } from '../../../../testUtils/renderComponent' import { InviteComponent } from './Invite.component' -import { pairsToInvitationShareUrl } from '@quiet/common' +import { composeInvitationShareUrl } from '@quiet/common' describe('CopyLink', () => { it('renderComponent - hidden long link', () => { - const invitationLink = pairsToInvitationShareUrl([ - { - peerId: 'QmVTkUad2Gq3MkCa8gf12R1gsWDfk2yiTEqb6YGXDG2iQ3', - onionAddress: 'p3oqdr53dkgg3n5nuezlzyawhxvit5efxzlunvzp7n7lmva6fj3i43ad', - }, - { - peerId: 'Qmd2Un9AynokZrcZGsMuaqgupTtidHGQnUkNVfFFAef97C', - onionAddress: 'vnywuiyl7p7ig2murcscdyzksko53e4k3dpdm2yoopvvu25p6wwjqbad', - }, - { - peerId: 'QmXRY4rhAx8Muq8dMGkr9qknJdE6UHZDdGaDRTQEbwFN5b', - onionAddress: '6vu2bxki777it3cpayv6fq6vpl4ke3kzj7gxicfygm55dhhtphyfdvyd', - }, - { - peerId: 'QmT18UvnUBkseMc3SqnfPxpHwN8nzLrJeNSLZtc8rAFXhz', - onionAddress: 'y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd', - }, - ]) + const invitationLink = composeInvitationShareUrl({ + pairs: [ + { + peerId: 'QmVTkUad2Gq3MkCa8gf12R1gsWDfk2yiTEqb6YGXDG2iQ3', + onionAddress: 'p3oqdr53dkgg3n5nuezlzyawhxvit5efxzlunvzp7n7lmva6fj3i43ad', + }, + { + peerId: 'Qmd2Un9AynokZrcZGsMuaqgupTtidHGQnUkNVfFFAef97C', + onionAddress: 'vnywuiyl7p7ig2murcscdyzksko53e4k3dpdm2yoopvvu25p6wwjqbad', + }, + { + peerId: 'QmXRY4rhAx8Muq8dMGkr9qknJdE6UHZDdGaDRTQEbwFN5b', + onionAddress: '6vu2bxki777it3cpayv6fq6vpl4ke3kzj7gxicfygm55dhhtphyfdvyd', + }, + { + peerId: 'QmT18UvnUBkseMc3SqnfPxpHwN8nzLrJeNSLZtc8rAFXhz', + onionAddress: 'y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd', + }, + ], + psk: '123435', + }) const result = renderComponent( ) @@ -124,12 +127,15 @@ describe('CopyLink', () => { }) it('renderComponent - revealed short link', () => { - const invitationLink = pairsToInvitationShareUrl([ - { - peerId: 'QmVTkUad2Gq3MkCa8gf12R1gsWDfk2yiTEqb6YGXDG2iQ3', - onionAddress: 'p3oqdr53dkgg3n5nuezlzyawhxvit5efxzlunvzp7n7lmva6fj3i43ad', - }, - ]) + const invitationLink = composeInvitationShareUrl({ + pairs: [ + { + peerId: 'QmVTkUad2Gq3MkCa8gf12R1gsWDfk2yiTEqb6YGXDG2iQ3', + onionAddress: 'p3oqdr53dkgg3n5nuezlzyawhxvit5efxzlunvzp7n7lmva6fj3i43ad', + }, + ], + psk: '12345', + }) const result = renderComponent( ) @@ -182,7 +188,7 @@ describe('CopyLink', () => { class="MuiTypography-root MuiTypography-body2 InviteToCommunitylink css-16d47hw-MuiTypography-root" data-testid="invitation-link" > - https://tryquiet.org/join#QmVTkUad2Gq3MkCa8gf12R1gsWDfk2yiTEqb6YGXDG2iQ3=p3oqdr53dkgg3n5nuezlzyawhxvit5efxzlunvzp7n7lmva6fj3i43ad + https://tryquiet.org/join#QmVTkUad2Gq3MkCa8gf12R1gsWDfk2yiTEqb6YGXDG2iQ3=p3oqdr53dkgg3n5nuezlzyawhxvit5efxzlunvzp7n7lmva6fj3i43ad&k=12345