diff --git a/about_preload.js b/about_preload.js index c39456347..828f4a77a 100644 --- a/about_preload.js +++ b/about_preload.js @@ -8,6 +8,9 @@ const config = url.parse(window.location.toString(), true).query; const { locale } = config; const localeMessages = ipcRenderer.sendSync('locale-data'); +global.dcodeIO = global.dcodeIO || {}; +global.dcodeIO.ByteBuffer = require('bytebuffer'); + window.getEnvironment = () => config.environment; window.getVersion = () => config.version; window.getCommitHash = () => config.commitHash; diff --git a/debug_log_preload.js b/debug_log_preload.js index 407f6f963..8121af5d6 100644 --- a/debug_log_preload.js +++ b/debug_log_preload.js @@ -8,6 +8,9 @@ const config = url.parse(window.location.toString(), true).query; const { locale } = config; const localeMessages = ipcRenderer.sendSync('locale-data'); +global.dcodeIO = global.dcodeIO || {}; +global.dcodeIO.ByteBuffer = require('bytebuffer'); + window.getVersion = () => config.version; window.theme = config.theme; window.i18n = i18n.setup(locale, localeMessages); diff --git a/js/models/conversations.js b/js/models/conversations.js index 9b012e74d..ba457b522 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -417,13 +417,13 @@ libsession .getMessageQueue() .sendUsingMultiDevice(device, typingMessage) - .ignore(); + .catch(log.error); } else { // the recipients on the case of a group are found by the messageQueue using message.groupId libsession .getMessageQueue() .sendToGroup(typingMessage) - .ignore(); + .catch(log.error); } }, @@ -1931,7 +1931,7 @@ libsession .getMessageQueue() .sendToGroup(groupUpdateMessage) - .ignore(); + .catch(log.error); }, sendGroupInfo(recipient) { @@ -1955,7 +1955,7 @@ libsession .getMessageQueue() .send(recipientPubKey, groupUpdateMessage) - .ignore(); + .catch(log.error); } }, diff --git a/libloki/crypto.d.ts b/libloki/crypto.d.ts new file mode 100644 index 000000000..d928decb9 --- /dev/null +++ b/libloki/crypto.d.ts @@ -0,0 +1,27 @@ +import { PairingAuthorisation } from '../js/modules/data'; + +declare enum PairingTypeEnum { + REQUEST = 1, + GRANT, +} + +export interface CryptoInterface { + DHDecrypt: any; + DHEncrypt: any; + DecryptGCM: any; // AES-GCM + EncryptGCM: any; // AES-GCM + FallBackDecryptionError: any; + FallBackSessionCipher: any; + LokiSessionCipher: any; + PairingType: PairingTypeEnum; + _decodeSnodeAddressToPubKey: any; + decryptForPubkey: any; + decryptToken: any; + encryptForPubkey: any; + generateEphemeralKeyPair: any; + generateSignatureForPairing: any; + sha512: any; + validateAuthorisation: any; + verifyAuthorisation(authorisation: PairingAuthorisation): Promise; + verifyPairingSignature: any; +} diff --git a/libloki/index.d.ts b/libloki/index.d.ts new file mode 100644 index 000000000..82ad4d784 --- /dev/null +++ b/libloki/index.d.ts @@ -0,0 +1,8 @@ +import { CryptoInterface } from './crypto'; + +export interface Libloki { + api: any; + crypto: CryptoInterface; + storage: any; + serviceNodes: any; +} diff --git a/password_preload.js b/password_preload.js index a90393ecb..e0cc712ef 100644 --- a/password_preload.js +++ b/password_preload.js @@ -8,6 +8,9 @@ const config = url.parse(window.location.toString(), true).query; const { locale } = config; const localeMessages = ipcRenderer.sendSync('locale-data'); +global.dcodeIO = global.dcodeIO || {}; +global.dcodeIO.ByteBuffer = require('bytebuffer'); + window.React = require('react'); window.ReactDOM = require('react-dom'); diff --git a/permissions_popup_preload.js b/permissions_popup_preload.js index 621a5fc7c..c591700bf 100644 --- a/permissions_popup_preload.js +++ b/permissions_popup_preload.js @@ -8,6 +8,9 @@ const config = url.parse(window.location.toString(), true).query; const { locale } = config; const localeMessages = ipcRenderer.sendSync('locale-data'); +global.dcodeIO = global.dcodeIO || {}; +global.dcodeIO.ByteBuffer = require('bytebuffer'); + window.getVersion = () => config.version; window.theme = config.theme; window.i18n = i18n.setup(locale, localeMessages); diff --git a/test/models/conversations_test.js b/test/models/conversations_test.js index 97d0ed1bd..b4aadef27 100644 --- a/test/models/conversations_test.js +++ b/test/models/conversations_test.js @@ -96,7 +96,7 @@ describe('Conversation', () => { it('adds conversation to message collection upon leaving group', async () => { const convo = new Whisper.ConversationCollection().add({ type: 'group', - id: 'a random string', + id: '052d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94ab', }); await convo.leaveGroup(); assert.notEqual(convo.messageCollection.length, 0); diff --git a/ts/session/messages/outgoing/content/TypingMessage.ts b/ts/session/messages/outgoing/content/TypingMessage.ts index 371d0739f..3c1f247bd 100644 --- a/ts/session/messages/outgoing/content/TypingMessage.ts +++ b/ts/session/messages/outgoing/content/TypingMessage.ts @@ -3,23 +3,26 @@ import { SignalService } from '../../../../protobuf'; import { TextEncoder } from 'util'; import { MessageParams } from '../Message'; import { StringUtils } from '../../../utils'; +import { PubKey } from '../../../types'; interface TypingMessageParams extends MessageParams { isTyping: boolean; typingTimestamp?: number; - groupId?: string; + groupId?: string | PubKey; } export class TypingMessage extends ContentMessage { - private readonly isTyping: boolean; - private readonly typingTimestamp?: number; - private readonly groupId?: string; + public readonly isTyping: boolean; + public readonly typingTimestamp?: number; + public readonly groupId?: PubKey; constructor(params: TypingMessageParams) { super({ timestamp: params.timestamp, identifier: params.identifier }); this.isTyping = params.isTyping; this.typingTimestamp = params.typingTimestamp; - this.groupId = params.groupId; + + const { groupId } = params; + this.groupId = groupId ? PubKey.cast(groupId) : undefined; } public ttl(): number { @@ -41,7 +44,7 @@ export class TypingMessage extends ContentMessage { const typingMessage = new SignalService.TypingMessage(); if (this.groupId) { typingMessage.groupId = new Uint8Array( - StringUtils.encode(this.groupId, 'utf8') + StringUtils.encode(this.groupId.key, 'utf8') ); } typingMessage.action = action; diff --git a/ts/session/messages/outgoing/content/data/ExpirationTimerUpdateMessage.ts b/ts/session/messages/outgoing/content/data/ExpirationTimerUpdateMessage.ts index 7b2044487..6329ec32b 100644 --- a/ts/session/messages/outgoing/content/data/ExpirationTimerUpdateMessage.ts +++ b/ts/session/messages/outgoing/content/data/ExpirationTimerUpdateMessage.ts @@ -3,23 +3,26 @@ import { SignalService } from '../../../../../protobuf'; import { MessageParams } from '../../Message'; import { StringUtils } from '../../../../utils'; import { DataMessage } from './DataMessage'; +import { PubKey } from '../../../../types'; interface ExpirationTimerUpdateMessageParams extends MessageParams { - groupId?: string; + groupId?: string | PubKey; expireTimer: number | null; profileKey?: Uint8Array; } export class ExpirationTimerUpdateMessage extends DataMessage { - private readonly groupId?: string; - private readonly expireTimer: number | null; - private readonly profileKey?: Uint8Array; + public readonly groupId?: PubKey; + public readonly expireTimer: number | null; + public readonly profileKey?: Uint8Array; constructor(params: ExpirationTimerUpdateMessageParams) { super({ timestamp: params.timestamp, identifier: params.identifier }); - this.groupId = params.groupId; this.expireTimer = params.expireTimer; this.profileKey = params.profileKey; + + const { groupId } = params; + this.groupId = groupId ? PubKey.cast(groupId) : undefined; } public ttl(): number { @@ -32,7 +35,7 @@ export class ExpirationTimerUpdateMessage extends DataMessage { const groupMessage = new SignalService.GroupContext(); if (this.groupId) { groupMessage.id = new Uint8Array( - StringUtils.encode(this.groupId, 'utf8') + StringUtils.encode(this.groupId.key, 'utf8') ); groupMessage.type = SignalService.GroupContext.Type.DELIVER; } diff --git a/ts/session/messages/outgoing/content/data/group/ClosedGroupMessage.ts b/ts/session/messages/outgoing/content/data/group/ClosedGroupMessage.ts index 5fe9b561c..91bb9a874 100644 --- a/ts/session/messages/outgoing/content/data/group/ClosedGroupMessage.ts +++ b/ts/session/messages/outgoing/content/data/group/ClosedGroupMessage.ts @@ -16,8 +16,7 @@ export abstract class ClosedGroupMessage extends DataMessage { timestamp: params.timestamp, identifier: params.identifier, }); - const { groupId } = params; - this.groupId = typeof groupId === 'string' ? new PubKey(groupId) : groupId; + this.groupId = PubKey.cast(params.groupId); } public ttl(): number { diff --git a/ts/session/messages/outgoing/content/data/index.ts b/ts/session/messages/outgoing/content/data/index.ts index 76abca7ee..a8022724b 100644 --- a/ts/session/messages/outgoing/content/data/index.ts +++ b/ts/session/messages/outgoing/content/data/index.ts @@ -3,3 +3,4 @@ export * from './DeviceUnlinkMessage'; export * from './GroupInvitationMessage'; export * from './ChatMessage'; export * from './group'; +export * from './ExpirationTimerUpdateMessage'; diff --git a/ts/session/protocols/MultiDeviceProtocol.ts b/ts/session/protocols/MultiDeviceProtocol.ts index 3958eeec3..140e394b8 100644 --- a/ts/session/protocols/MultiDeviceProtocol.ts +++ b/ts/session/protocols/MultiDeviceProtocol.ts @@ -82,25 +82,45 @@ export class MultiDeviceProtocol { const mapping = await window.lokiFileServerAPI.getUserDeviceMapping( device.key ); - // TODO: Filter out invalid authorisations if (!mapping || !mapping.authorisations) { return []; } - return mapping.authorisations.map( - ({ - primaryDevicePubKey, - secondaryDevicePubKey, - requestSignature, - grantSignature, - }) => ({ - primaryDevicePubKey, - secondaryDevicePubKey, - requestSignature: StringUtils.encode(requestSignature, 'base64'), - grantSignature: StringUtils.encode(grantSignature, 'base64'), - }) - ); + try { + const authorisations = mapping.authorisations.map( + ({ + primaryDevicePubKey, + secondaryDevicePubKey, + requestSignature, + grantSignature, + }) => ({ + primaryDevicePubKey, + secondaryDevicePubKey, + requestSignature: StringUtils.encode(requestSignature, 'base64'), + grantSignature: StringUtils.encode(grantSignature, 'base64'), + }) + ); + + const validAuthorisations = await Promise.all( + authorisations.map(async authorisation => { + const valid = await window.libloki.crypto.verifyAuthorisation( + authorisation + ); + return valid ? authorisation : undefined; + }) + ); + + return validAuthorisations.filter(a => !!a) as Array< + PairingAuthorisation + >; + } catch (e) { + console.warn( + `MultiDeviceProtocol::fetchPairingAuthorisation: Failed to map authorisations for ${device.key}.`, + e + ); + return []; + } } /** @@ -120,7 +140,7 @@ export class MultiDeviceProtocol { public static async getPairingAuthorisations( device: PubKey | string ): Promise> { - const pubKey = typeof device === 'string' ? new PubKey(device) : device; + const pubKey = PubKey.cast(device); await this.fetchPairingAuthorisationsIfNeeded(pubKey); return getPairingAuthorisationsFor(pubKey.key); @@ -133,7 +153,7 @@ export class MultiDeviceProtocol { public static async removePairingAuthorisations( device: PubKey | string ): Promise { - const pubKey = typeof device === 'string' ? new PubKey(device) : device; + const pubKey = PubKey.cast(device); return removePairingAuthorisationsFor(pubKey.key); } @@ -146,7 +166,7 @@ export class MultiDeviceProtocol { public static async getAllDevices( user: PubKey | string ): Promise> { - const pubKey = typeof user === 'string' ? new PubKey(user) : user; + const pubKey = PubKey.cast(user); const authorisations = await this.getPairingAuthorisations(pubKey); if (authorisations.length === 0) { return [pubKey]; @@ -170,7 +190,7 @@ export class MultiDeviceProtocol { public static async getPrimaryDevice( user: PubKey | string ): Promise { - const pubKey = typeof user === 'string' ? new PubKey(user) : user; + const pubKey = PubKey.cast(user); const authorisations = await this.getPairingAuthorisations(pubKey); if (authorisations.length === 0) { return pubKey; @@ -217,7 +237,7 @@ export class MultiDeviceProtocol { * @param device The device to check. */ public static async isOurDevice(device: PubKey | string): Promise { - const pubKey = typeof device === 'string' ? new PubKey(device) : device; + const pubKey = PubKey.cast(device); try { const ourDevices = await this.getOurDevices(); diff --git a/ts/session/sending/MessageQueue.ts b/ts/session/sending/MessageQueue.ts index 60e812104..cbb2e817e 100644 --- a/ts/session/sending/MessageQueue.ts +++ b/ts/session/sending/MessageQueue.ts @@ -6,9 +6,11 @@ import { import { ClosedGroupMessage, ContentMessage, + ExpirationTimerUpdateMessage, OpenGroupMessage, SessionRequestMessage, SyncMessage, + TypingMessage, } from '../messages/outgoing'; import { PendingMessageCache } from './PendingMessageCache'; import { @@ -24,7 +26,7 @@ import { UserUtil } from '../../util'; export class MessageQueue implements MessageQueueInterface { public readonly events: TypedEventEmitter; - private readonly jobQueues: Map = new Map(); + private readonly jobQueues: Map = new Map(); private readonly pendingMessageCache: PendingMessageCache; constructor(cache?: PendingMessageCache) { @@ -33,13 +35,16 @@ export class MessageQueue implements MessageQueueInterface { void this.processAllPending(); } - public async sendUsingMultiDevice(user: PubKey, message: ContentMessage) { + public async sendUsingMultiDevice( + user: PubKey, + message: ContentMessage + ): Promise { const userDevices = await MultiDeviceProtocol.getAllDevices(user.key); await this.sendMessageToDevices(userDevices, message); } - public async send(device: PubKey, message: ContentMessage) { + public async send(device: PubKey, message: ContentMessage): Promise { await this.sendMessageToDevices([device], message); } @@ -75,26 +80,8 @@ export class MessageQueue implements MessageQueueInterface { } public async sendToGroup( - message: OpenGroupMessage | ClosedGroupMessage - ): Promise { - // Closed groups - if (message instanceof ClosedGroupMessage) { - // Get devices in closed group - const recipients = await GroupUtils.getGroupMembers(message.groupId); - if (recipients.length === 0) { - return false; - } - - // Send to all devices of members - await Promise.all( - recipients.map(async recipient => - this.sendUsingMultiDevice(recipient, message) - ) - ); - - return true; - } - + message: OpenGroupMessage | ContentMessage + ): Promise { // Open groups if (message instanceof OpenGroupMessage) { // No queue needed for Open Groups; send directly @@ -108,20 +95,42 @@ export class MessageQueue implements MessageQueueInterface { } else { this.events.emit('fail', message, error); } - - return result; } catch (e) { console.warn( `Failed to send message to open group: ${message.group.server}`, e ); this.events.emit('fail', message, error); - - return false; } + + return; + } + + let groupId: PubKey | undefined; + if (message instanceof ClosedGroupMessage) { + groupId = message.groupId; + } else if (message instanceof TypingMessage) { + groupId = message.groupId; + } else if (message instanceof ExpirationTimerUpdateMessage) { + groupId = message.groupId; + } + + if (!groupId) { + throw new Error('Invalid group message passed in sendToGroup.'); } - return false; + // Get devices in group + const recipients = await GroupUtils.getGroupMembers(groupId); + if (recipients.length === 0) { + return; + } + + // Send to all devices of members + await Promise.all( + recipients.map(async recipient => + this.sendUsingMultiDevice(recipient, message) + ) + ); } public async sendSyncMessage(message: SyncMessage | undefined): Promise { @@ -153,17 +162,19 @@ export class MessageQueue implements MessageQueueInterface { const messageId = String(message.timestamp); if (!jobQueue.has(messageId)) { - try { - await jobQueue.addWithId(messageId, async () => - MessageSender.send(message) - ); - this.events.emit('success', message); - } catch (e) { - this.events.emit('fail', message, e); - } finally { - // Remove from the cache because retrying is done in the sender - void this.pendingMessageCache.remove(message); - } + // We put the event handling inside this job to avoid sending duplicate events + const job = async () => { + try { + await MessageSender.send(message); + this.events.emit('success', message); + } catch (e) { + this.events.emit('fail', message, e); + } finally { + // Remove from the cache because retrying is done in the sender + void this.pendingMessageCache.remove(message); + } + }; + await jobQueue.addWithId(messageId, job); } }); } @@ -194,10 +205,10 @@ export class MessageQueue implements MessageQueueInterface { } private getJobQueue(device: PubKey): JobQueue { - let queue = this.jobQueues.get(device); + let queue = this.jobQueues.get(device.key); if (!queue) { queue = new JobQueue(); - this.jobQueues.set(device, queue); + this.jobQueues.set(device.key, queue); } return queue; diff --git a/ts/session/sending/MessageQueueInterface.ts b/ts/session/sending/MessageQueueInterface.ts index 5bed428ca..2cd58354e 100644 --- a/ts/session/sending/MessageQueueInterface.ts +++ b/ts/session/sending/MessageQueueInterface.ts @@ -17,8 +17,8 @@ export interface MessageQueueInterfaceEvents { export interface MessageQueueInterface { events: TypedEventEmitter; - sendUsingMultiDevice(user: PubKey, message: ContentMessage): void; - send(device: PubKey, message: ContentMessage): void; - sendToGroup(message: GroupMessageType): void; + sendUsingMultiDevice(user: PubKey, message: ContentMessage): Promise; + send(device: PubKey, message: ContentMessage): Promise; + sendToGroup(message: GroupMessageType): Promise; sendSyncMessage(message: SyncMessage | undefined): Promise; } diff --git a/ts/session/types/PubKey.ts b/ts/session/types/PubKey.ts index a6c0a2077..a76e4bf6d 100644 --- a/ts/session/types/PubKey.ts +++ b/ts/session/types/PubKey.ts @@ -5,11 +5,35 @@ export class PubKey { ); public readonly key: string; + /** + * A PubKey object. + * If `pubKeyString` is not valid then this will throw an `Error`. + * + * @param pubkeyString The public key string. + */ constructor(pubkeyString: string) { - PubKey.validate(pubkeyString); - this.key = pubkeyString; + if (!PubKey.validate(pubkeyString)) { + throw new Error(`Invalid pubkey string passed: ${pubkeyString}`); + } + this.key = pubkeyString.toLowerCase(); + } + + /** + * Cast a `value` to a `PubKey`. + * If `value` is not valid then this will throw. + * + * @param value The value to cast. + */ + public static cast(value: string | PubKey): PubKey { + return typeof value === 'string' ? new PubKey(value) : value; } + /** + * Try convert `pubKeyString` to `PubKey`. + * + * @param pubkeyString The public key string. + * @returns `PubKey` if valid otherwise returns `undefined`. + */ public static from(pubkeyString: string): PubKey | undefined { // Returns a new instance if the pubkey is valid if (PubKey.validate(pubkeyString)) { @@ -30,7 +54,7 @@ export class PubKey { public isEqual(comparator: PubKey | string) { return comparator instanceof PubKey ? this.key === comparator.key - : this.key === comparator; + : this.key === comparator.toLowerCase(); } } diff --git a/ts/test/session/crypto/MessageEncrypter_test.ts b/ts/test/session/crypto/MessageEncrypter_test.ts index 6f16e77c7..7612f3c37 100644 --- a/ts/test/session/crypto/MessageEncrypter_test.ts +++ b/ts/test/session/crypto/MessageEncrypter_test.ts @@ -33,7 +33,7 @@ describe('MessageEncrypter', () => { TestUtils.stubWindow('libloki', { crypto: { FallBackSessionCipher: Stubs.FallBackSessionCipherStub, - }, + } as any, }); sandbox.stub(UserUtil, 'getCurrentDevicePubKey').resolves(ourNumber); diff --git a/ts/test/session/messages/ClosedGroupChatMessage_test.ts b/ts/test/session/messages/ClosedGroupChatMessage_test.ts index 4215824d0..b7b93115e 100644 --- a/ts/test/session/messages/ClosedGroupChatMessage_test.ts +++ b/ts/test/session/messages/ClosedGroupChatMessage_test.ts @@ -6,22 +6,32 @@ import { } from '../../../session/messages/outgoing'; import { SignalService } from '../../../protobuf'; import { TextEncoder } from 'util'; +import { TestUtils } from '../../test-utils'; +import { StringUtils } from '../../../session/utils'; +import { PubKey } from '../../../session/types'; describe('ClosedGroupChatMessage', () => { + let groupId: PubKey; + beforeEach(() => { + groupId = TestUtils.generateFakePubKey(); + }); it('can create empty message with timestamp, groupId and chatMessage', () => { const chatMessage = new ChatMessage({ timestamp: Date.now(), body: 'body', }); const message = new ClosedGroupChatMessage({ - groupId: '12', + groupId, chatMessage, }); const plainText = message.plainTextBuffer(); const decoded = SignalService.Content.decode(plainText); expect(decoded.dataMessage) .to.have.property('group') - .to.have.deep.property('id', new TextEncoder().encode('12')); + .to.have.deep.property( + 'id', + new Uint8Array(StringUtils.encode(groupId.key, 'utf8')) + ); expect(decoded.dataMessage) .to.have.property('group') .to.have.deep.property('type', SignalService.GroupContext.Type.DELIVER); @@ -39,7 +49,7 @@ describe('ClosedGroupChatMessage', () => { timestamp: Date.now(), }); const message = new ClosedGroupChatMessage({ - groupId: '12', + groupId, chatMessage, }); expect(message.ttl()).to.equal(24 * 60 * 60 * 1000); @@ -50,7 +60,7 @@ describe('ClosedGroupChatMessage', () => { timestamp: Date.now(), }); const message = new ClosedGroupChatMessage({ - groupId: '12', + groupId, chatMessage, }); expect(message.identifier).to.not.equal(null, 'identifier cannot be null'); @@ -67,7 +77,7 @@ describe('ClosedGroupChatMessage', () => { identifier: 'chatMessage', }); const message = new ClosedGroupChatMessage({ - groupId: '12', + groupId, chatMessage, identifier: 'closedGroupMessage', }); @@ -81,7 +91,7 @@ describe('ClosedGroupChatMessage', () => { identifier: 'chatMessage', }); const message = new ClosedGroupChatMessage({ - groupId: '12', + groupId, chatMessage, }); expect(message.identifier).to.be.equal('chatMessage'); diff --git a/ts/test/session/messages/TypingMessage_test.ts b/ts/test/session/messages/TypingMessage_test.ts index 8753f1408..ad14f4961 100644 --- a/ts/test/session/messages/TypingMessage_test.ts +++ b/ts/test/session/messages/TypingMessage_test.ts @@ -5,6 +5,8 @@ import { SignalService } from '../../../protobuf'; import { TextEncoder } from 'util'; import Long from 'long'; import { toNumber } from 'lodash'; +import { StringUtils } from '../../../session/utils'; +import { TestUtils } from '../../test-utils'; describe('TypingMessage', () => { it('has Action.STARTED if isTyping = true', () => { @@ -60,7 +62,7 @@ describe('TypingMessage', () => { }); it('has groupId set if a value given', () => { - const groupId = '6666666666'; + const groupId = TestUtils.generateFakePubKey(); const message = new TypingMessage({ timestamp: Date.now(), isTyping: true, @@ -68,7 +70,9 @@ describe('TypingMessage', () => { }); const plainText = message.plainTextBuffer(); const decoded = SignalService.Content.decode(plainText); - const manuallyEncodedGroupId = new TextEncoder().encode(groupId); + const manuallyEncodedGroupId = new Uint8Array( + StringUtils.encode(groupId.key, 'utf8') + ); expect(decoded.typingMessage?.groupId).to.be.deep.equal( manuallyEncodedGroupId diff --git a/ts/test/session/protocols/MultiDeviceProtocol_test.ts b/ts/test/session/protocols/MultiDeviceProtocol_test.ts index ad287a3dc..e1d248121 100644 --- a/ts/test/session/protocols/MultiDeviceProtocol_test.ts +++ b/ts/test/session/protocols/MultiDeviceProtocol_test.ts @@ -71,6 +71,21 @@ describe('MultiDeviceProtocol', () => { }); describe('fetchPairingAuthorisations', () => { + let verifyAuthorisationStub: sinon.SinonStub< + [PairingAuthorisation], + Promise + >; + beforeEach(() => { + verifyAuthorisationStub = sandbox + .stub<[PairingAuthorisation], Promise>() + .resolves(true); + TestUtils.stubWindow('libloki', { + crypto: { + verifyAuthorisation: verifyAuthorisationStub, + } as any, + }); + }); + it('should throw if lokiFileServerAPI does not exist', async () => { TestUtils.stubWindow('lokiFileServerAPI', undefined); expect( @@ -125,9 +140,96 @@ describe('MultiDeviceProtocol', () => { networkAuth.grantSignature ); }); + + it('should not return invalid authorisations', async () => { + const networkAuth = { + primaryDevicePubKey: + '05caa6310a490415df45f8f4ad1b3655ad7a11e722257887a30cf71601d679720b', + secondaryDevicePubKey: + '051296b9588641eea268d60ad6636eecb53a95150e91c0531a00203e01a2c16a39', + requestSignature: + '+knEdlenTV+MooRqlFsZRPWW8s9pcjKwB40fY5o0GJmAi2RPZtaVGRTqgApTIn2zPBTE4GQlmPD7uxcczHDjAg==', + grantSignature: + 'eKzcOWMEVetybkuiVK2u18B9en5pywohn2Hn25/VOVTMrIsKSCW4xXpqwipfqvgvi62WtUt6SA9bCEB5Ngcyiw==', + }; + + const stub = sinon.stub().resolves({ + isPrimary: false, + authorisations: [networkAuth], + }); + TestUtils.stubWindow('lokiFileServerAPI', { + getUserDeviceMapping: stub, + }); + + verifyAuthorisationStub.resolves(false); + + const authorisations = await MultiDeviceProtocol.fetchPairingAuthorisations( + TestUtils.generateFakePubKey() + ); + expect(verifyAuthorisationStub.callCount).to.equal(1); + expect(authorisations.length).to.equal(0); + }); + + it('should handle incorrect pairing authorisations from the file server', async () => { + const invalidAuth = { + primaryDevicePubKey: + '05caa6310a490415df45f8f4ad1b3655ad7a11e722257887a30cf71601d679720b', + secondaryDevicePubKey: + '051296b9588641eea268d60ad6636eecb53a95150e91c0531a00203e01a2c16a39', + requestSignatures: + '+knEdlenTV+MooRqlFsZRPWW8s9pcjKwB40fY5o0GJmAi2RPZtaVGRTqgApTIn2zPBTE4GQlmPD7uxcczHDjAg==', + }; + + const stub = sinon.stub().resolves({ + isPrimary: false, + authorisations: [invalidAuth], + }); + TestUtils.stubWindow('lokiFileServerAPI', { + getUserDeviceMapping: stub, + }); + const authorisations = await MultiDeviceProtocol.fetchPairingAuthorisations( + TestUtils.generateFakePubKey() + ); + expect(authorisations.length).to.equal(0); + }); + + it('should return empty array if mapping is null', async () => { + const stub = sinon.stub().resolves(null); + TestUtils.stubWindow('lokiFileServerAPI', { + getUserDeviceMapping: stub, + }); + + const authorisations = await MultiDeviceProtocol.fetchPairingAuthorisations( + TestUtils.generateFakePubKey() + ); + expect(authorisations.length).to.equal(0); + }); + + it('should return empty array if authorisations in mapping are null', async () => { + const stub = sinon.stub().resolves({ + isPrimary: false, + authorisations: null, + }); + TestUtils.stubWindow('lokiFileServerAPI', { + getUserDeviceMapping: stub, + }); + + const authorisations = await MultiDeviceProtocol.fetchPairingAuthorisations( + TestUtils.generateFakePubKey() + ); + expect(authorisations.length).to.equal(0); + }); }); describe('fetchPairingAuthorisationIfNeeded', () => { + beforeEach(() => { + TestUtils.stubWindow('libloki', { + crypto: { + verifyAuthorisation: async () => true, + } as any, + }); + }); + let fetchPairingAuthorisationStub: sinon.SinonStub< [PubKey], Promise> @@ -250,6 +352,16 @@ describe('MultiDeviceProtocol', () => { expect(allDevicePubKeys).to.have.same.members(devices.map(d => d.key)); } }); + + it('should return the passed in user device if no pairing authorisations are found', async () => { + const pubKey = TestUtils.generateFakePubKey(); + sandbox + .stub(MultiDeviceProtocol, 'getPairingAuthorisations') + .resolves([]); + const allDevices = await MultiDeviceProtocol.getAllDevices(pubKey); + expect(allDevices).to.have.length(1); + expect(allDevices[0].key).to.equal(pubKey.key); + }); }); describe('getPrimaryDevice', () => { diff --git a/ts/test/session/protocols/SessionProtocol_test.ts b/ts/test/session/protocols/SessionProtocol_test.ts index 712088aa2..5f87a99b9 100644 --- a/ts/test/session/protocols/SessionProtocol_test.ts +++ b/ts/test/session/protocols/SessionProtocol_test.ts @@ -12,7 +12,7 @@ import { PubKey } from '../../../session/types'; describe('SessionProtocol', () => { const sandbox = sinon.createSandbox(); const ourNumber = 'ourNumber'; - const pubkey = new PubKey('deviceid'); + const pubkey = TestUtils.generateFakePubKey(); let getItemById: sinon.SinonStub; let send: sinon.SinonStub; @@ -88,14 +88,14 @@ describe('SessionProtocol', () => { it('protocol: sendSessionRequest should add the deviceID to the sentMap', async () => { expect(SessionProtocol.getSentSessionsTimestamp()) - .to.have.property('deviceid') + .to.have.property(pubkey.key) .to.be.approximately(Date.now(), 100); }); it('protocol: sendSessionRequest should not have pendingSend set after', async () => { expect( SessionProtocol.getPendingSendSessionTimestamp() - ).to.not.have.property('deviceid'); + ).to.not.have.property(pubkey.key); }); }); @@ -107,34 +107,32 @@ describe('SessionProtocol', () => { it('protocol: onSessionEstablished should remove the device in sentTimestamps', async () => { expect(SessionProtocol.getSentSessionsTimestamp()).to.have.property( - 'deviceid' + pubkey.key ); await SessionProtocol.onSessionEstablished(pubkey); expect(SessionProtocol.getSentSessionsTimestamp()).to.not.have.property( - 'deviceid' + pubkey.key ); }); it('protocol: onSessionEstablished should remove the device in sentTimestamps and ONLY that one', async () => { // add a second item to the map - await SessionProtocol.sendSessionRequest( - resetMessage, - new PubKey('deviceid2') - ); + const anotherPubKey = TestUtils.generateFakePubKey(); + await SessionProtocol.sendSessionRequest(resetMessage, anotherPubKey); expect(SessionProtocol.getSentSessionsTimestamp()).to.have.property( - 'deviceid' + pubkey.key ); expect(SessionProtocol.getSentSessionsTimestamp()).to.have.property( - 'deviceid2' + anotherPubKey.key ); await SessionProtocol.onSessionEstablished(pubkey); expect(SessionProtocol.getSentSessionsTimestamp()).to.not.have.property( - 'deviceid' + pubkey.key ); expect(SessionProtocol.getSentSessionsTimestamp()).to.have.property( - 'deviceid2' + anotherPubKey.key ); }); }); @@ -144,7 +142,7 @@ describe('SessionProtocol', () => { const hasSent = await SessionProtocol.hasSentSessionRequest(pubkey); expect(hasSent).to.be.equal( false, - 'hasSent should be false for `deviceid`' + `hasSent should be false for ${pubkey.key}` ); }); @@ -154,7 +152,7 @@ describe('SessionProtocol', () => { const hasSent = await SessionProtocol.hasSentSessionRequest(pubkey); expect(hasSent).to.be.equal( true, - 'hasSent should be true for `deviceid`' + `hasSent should be true for ${pubkey.key}` ); }); @@ -174,7 +172,7 @@ describe('SessionProtocol', () => { const hasSent = await SessionProtocol.hasSentSessionRequest(pubkey); expect(hasSent).to.be.equal( true, - 'hasSent should be true for `deviceid`' + `hasSent should be true for ${pubkey.key}` ); }); @@ -189,7 +187,7 @@ describe('SessionProtocol', () => { const hasSent = await SessionProtocol.hasSentSessionRequest(pubkey); expect(hasSent).to.be.equal( true, - 'hasSent should be true for `deviceid`' + `hasSent should be true for ${pubkey.key}` ); send.resetHistory(); @@ -207,7 +205,7 @@ describe('SessionProtocol', () => { // trigger the requestProcessed and check the map is updated await SessionProtocol.onSessionRequestProcessed(pubkey); expect(SessionProtocol.getProcessedSessionsTimestamp()) - .to.have.property('deviceid') + .to.have.property(pubkey.key) .to.be.approximately(Date.now(), 5); }); @@ -217,14 +215,15 @@ describe('SessionProtocol', () => { await SessionProtocol.onSessionRequestProcessed(pubkey); expect(SessionProtocol.getProcessedSessionsTimestamp()) - .to.have.property('deviceid') + .to.have.property(pubkey.key) .to.be.approximately(Date.now(), 5); await TestUtils.timeout(5); - const oldTimestamp = SessionProtocol.getProcessedSessionsTimestamp() - .deviceid; + const oldTimestamp = SessionProtocol.getProcessedSessionsTimestamp()[ + pubkey.key + ]; await SessionProtocol.onSessionRequestProcessed(pubkey); expect(SessionProtocol.getProcessedSessionsTimestamp()) - .to.have.property('deviceid') + .to.have.property(pubkey.key) .to.be.approximately(Date.now(), 5) .to.not.be.equal(oldTimestamp); }); diff --git a/ts/test/session/sending/MessageQueue_test.ts b/ts/test/session/sending/MessageQueue_test.ts index 7d3bf5fc3..3f1a4cb72 100644 --- a/ts/test/session/sending/MessageQueue_test.ts +++ b/ts/test/session/sending/MessageQueue_test.ts @@ -312,6 +312,13 @@ describe('MessageQueue', () => { }); describe('sendToGroup', () => { + it('should throw an error if invalid non-group message was passed', async () => { + const chatMessage = TestUtils.generateChatMessage(); + await expect( + messageQueueStub.sendToGroup(chatMessage) + ).to.be.rejectedWith('Invalid group message passed in sendToGroup.'); + }); + describe('closed groups', async () => { it('can send to closed group', async () => { const members = TestUtils.generateFakePubKeys(4).map( @@ -324,8 +331,7 @@ describe('MessageQueue', () => { .resolves(); const message = TestUtils.generateClosedGroupMessage(); - const success = await messageQueueStub.sendToGroup(message); - expect(success).to.equal(true, 'sending to group failed'); + await messageQueueStub.sendToGroup(message); expect(sendUsingMultiDeviceStub.callCount).to.equal(members.length); const arg = sendUsingMultiDeviceStub.getCall(0).args; @@ -342,12 +348,7 @@ describe('MessageQueue', () => { .resolves(); const message = TestUtils.generateClosedGroupMessage(); - const response = await messageQueueStub.sendToGroup(message); - - expect(response).to.equal( - false, - 'sendToGroup sent a message to an empty group' - ); + await messageQueueStub.sendToGroup(message); expect(sendUsingMultiDeviceStub.callCount).to.equal(0); }); }); @@ -365,9 +366,8 @@ describe('MessageQueue', () => { it('can send to open group', async () => { const message = TestUtils.generateOpenGroupMessage(); - const success = await messageQueueStub.sendToGroup(message); + await messageQueueStub.sendToGroup(message); expect(sendToOpenGroupStub.callCount).to.equal(1); - expect(success).to.equal(true, 'Sending to open group failed'); }); it('should emit a success event when send was successful', async () => { diff --git a/ts/window.d.ts b/ts/window.d.ts index ca938e632..be0f5e570 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -3,6 +3,7 @@ import LokiMessageAPI from '../../js/modules/loki_message_api'; import LokiPublicChatFactoryAPI from '../../js/modules/loki_public_chat_api'; import { LibsignalProtocol } from '../../libtextsecure/libsignal-protocol'; import { SignalInterface } from '../../js/modules/signal'; +import { Libloki } from '../libloki'; /* We declare window stuff here instead of global.d.ts because we are importing other declarations. @@ -42,7 +43,7 @@ declare global { getFriendsFromContacts: any; getSettingValue: any; i18n: LocalizerType; - libloki: any; + libloki: Libloki; libsignal: LibsignalProtocol; log: any; lokiFeatureFlags: any;