diff --git a/ts/session/crypto/MessageEncrypter.ts b/ts/session/crypto/MessageEncrypter.ts index ded12577b..9898d7f92 100644 --- a/ts/session/crypto/MessageEncrypter.ts +++ b/ts/session/crypto/MessageEncrypter.ts @@ -2,6 +2,7 @@ import { EncryptionType } from '../types/EncryptionType'; import { SignalService } from '../../protobuf'; import { UserUtil } from '../../util'; import { CipherTextObject } from '../../../libtextsecure/libsignal-protocol'; +import { PubKey } from '../types'; /** * Add padding to a message buffer @@ -31,13 +32,13 @@ function getPaddedMessageLength(originalLength: number): number { /** * Encrypt `plainTextBuffer` with given `encryptionType` for `device`. * - * @param device The device to encrypt for. + * @param device The device `PubKey` to encrypt for. * @param plainTextBuffer The unpadded plaintext buffer. * @param encryptionType The type of encryption. * @returns The envelope type and the base64 encoded cipher text */ export async function encrypt( - device: string, + device: PubKey, plainTextBuffer: Uint8Array, encryptionType: EncryptionType ): Promise<{ @@ -45,7 +46,7 @@ export async function encrypt( cipherText: Uint8Array; }> { const plainText = padPlainTextBuffer(plainTextBuffer); - const address = new window.libsignal.SignalProtocolAddress(device, 1); + const address = new window.libsignal.SignalProtocolAddress(device.key, 1); if (encryptionType === EncryptionType.MediumGroup) { // TODO: Do medium group stuff here @@ -68,7 +69,7 @@ export async function encrypt( } async function encryptUsingSealedSender( - device: string, + device: PubKey, innerCipherText: CipherTextObject ): Promise<{ envelopeType: SignalService.Envelope.Type; @@ -88,7 +89,7 @@ async function encryptUsingSealedSender( window.textsecure.storage.protocol ); const cipherTextBuffer = await cipher.encrypt( - device, + device.key, certificate, innerCipherText ); diff --git a/ts/session/protocols/SessionProtocol.ts b/ts/session/protocols/SessionProtocol.ts index 8e58f465c..dcb584a19 100644 --- a/ts/session/protocols/SessionProtocol.ts +++ b/ts/session/protocols/SessionProtocol.ts @@ -276,7 +276,7 @@ export class SessionProtocol { } /** - * timestamp undefined to remove the key/value pair, otherwise updates the processed timestamp and writes to DB + * Timestamp undefined to remove the `key`/`value` pair, otherwise updates the processed timestamp and writes to database */ private static async updateProcessedSessionTimestamp( device: string, diff --git a/ts/session/sending/MessageSender.ts b/ts/session/sending/MessageSender.ts index 5af656a94..100be53ce 100644 --- a/ts/session/sending/MessageSender.ts +++ b/ts/session/sending/MessageSender.ts @@ -6,6 +6,7 @@ import { SignalService } from '../../protobuf'; import { UserUtil } from '../../util'; import { MessageEncrypter } from '../crypto'; import pRetry from 'p-retry'; +import { PubKey } from '../types'; // ================ Regular ================ @@ -31,7 +32,8 @@ export async function send( throw new Error('lokiMessageAPI is not initialized.'); } - const { device, plainTextBuffer, encryption, timestamp, ttl } = message; + const device = PubKey.cast(message.device); + const { plainTextBuffer, encryption, timestamp, ttl } = message; const { envelopeType, cipherText } = await MessageEncrypter.encrypt( device, plainTextBuffer, @@ -41,7 +43,7 @@ export async function send( const data = wrapEnvelope(envelope); return pRetry( - async () => window.lokiMessageAPI.sendMessage(device, data, timestamp, ttl), + async () => window.lokiMessageAPI.sendMessage(device.key, data, timestamp, ttl), { retries: Math.max(attempts - 1, 0), factor: 1, diff --git a/ts/session/types/PubKey.ts b/ts/session/types/PubKey.ts index b64e899dc..be4039442 100644 --- a/ts/session/types/PubKey.ts +++ b/ts/session/types/PubKey.ts @@ -12,6 +12,9 @@ export class PubKey { * @param pubkeyString The public key string. */ constructor(pubkeyString: string) { + + console.log('[vince] pubkeyString:', pubkeyString); + if (!PubKey.validate(pubkeyString)) { throw new Error(`Invalid pubkey string passed: ${pubkeyString}`); } diff --git a/ts/test/session/crypto/MessageEncrypter_test.ts b/ts/test/session/crypto/MessageEncrypter_test.ts index d46b80fae..74ccbae9c 100644 --- a/ts/test/session/crypto/MessageEncrypter_test.ts +++ b/ts/test/session/crypto/MessageEncrypter_test.ts @@ -49,7 +49,7 @@ describe('MessageEncrypter', () => { it('should throw an error', async () => { const data = crypto.randomBytes(10); const promise = MessageEncrypter.encrypt( - '1', + TestUtils.generateFakePubKey(), data, EncryptionType.MediumGroup ); @@ -66,7 +66,7 @@ describe('MessageEncrypter', () => { Stubs.FallBackSessionCipherStub.prototype, 'encrypt' ); - await MessageEncrypter.encrypt('1', data, EncryptionType.Fallback); + await MessageEncrypter.encrypt(TestUtils.generateFakePubKey(), data, EncryptionType.Fallback); expect(spy.called).to.equal( true, 'FallbackSessionCipher.encrypt should be called.' @@ -79,7 +79,7 @@ describe('MessageEncrypter', () => { Stubs.FallBackSessionCipherStub.prototype, 'encrypt' ); - await MessageEncrypter.encrypt('1', data, EncryptionType.Fallback); + await MessageEncrypter.encrypt(TestUtils.generateFakePubKey(), data, EncryptionType.Fallback); const paddedData = MessageEncrypter.padPlainTextBuffer(data); const firstArgument = new Uint8Array(spy.args[0][0]); @@ -89,7 +89,7 @@ describe('MessageEncrypter', () => { it('should return an UNIDENTIFIED SENDER envelope type', async () => { const data = crypto.randomBytes(10); const result = await MessageEncrypter.encrypt( - '1', + TestUtils.generateFakePubKey(), data, EncryptionType.Fallback ); @@ -103,7 +103,7 @@ describe('MessageEncrypter', () => { it('should call SessionCipher encrypt', async () => { const data = crypto.randomBytes(10); const spy = sandbox.spy(Stubs.SessionCipherStub.prototype, 'encrypt'); - await MessageEncrypter.encrypt('1', data, EncryptionType.Signal); + await MessageEncrypter.encrypt(TestUtils.generateFakePubKey(), data, EncryptionType.Signal); expect(spy.called).to.equal( true, 'SessionCipher.encrypt should be called.' @@ -113,7 +113,7 @@ describe('MessageEncrypter', () => { it('should pass the padded message body to encrypt', async () => { const data = crypto.randomBytes(10); const spy = sandbox.spy(Stubs.SessionCipherStub.prototype, 'encrypt'); - await MessageEncrypter.encrypt('1', data, EncryptionType.Signal); + await MessageEncrypter.encrypt(TestUtils.generateFakePubKey(), data, EncryptionType.Signal); const paddedData = MessageEncrypter.padPlainTextBuffer(data); const firstArgument = new Uint8Array(spy.args[0][0]); @@ -123,7 +123,7 @@ describe('MessageEncrypter', () => { it('should return an UNIDENTIFIED SENDER envelope type', async () => { const data = crypto.randomBytes(10); const result = await MessageEncrypter.encrypt( - '1', + TestUtils.generateFakePubKey(), data, EncryptionType.Signal ); @@ -142,7 +142,9 @@ describe('MessageEncrypter', () => { Stubs.SecretSessionCipherStub.prototype, 'encrypt' ); - await MessageEncrypter.encrypt('user', crypto.randomBytes(10), type); + + const user = TestUtils.generateFakePubKey(); + await MessageEncrypter.encrypt(user, crypto.randomBytes(10), type); const args = spy.args[0]; const [device, certificate] = args; @@ -152,7 +154,7 @@ describe('MessageEncrypter', () => { senderDevice: 1, }); - expect(device).to.equal('user'); + expect(device).to.equal(user.key); expect(certificate.toJSON()).to.deep.equal( expectedCertificate.toJSON() ); diff --git a/ts/test/session/sending/MessageSender_test.ts b/ts/test/session/sending/MessageSender_test.ts deleted file mode 100644 index b71c508a5..000000000 --- a/ts/test/session/sending/MessageSender_test.ts +++ /dev/null @@ -1,243 +0,0 @@ -import { expect } from 'chai'; -import * as crypto from 'crypto'; -import * as sinon from 'sinon'; -import { toNumber } from 'lodash'; -import { MessageSender } from '../../../session/sending'; -import LokiMessageAPI from '../../../../js/modules/loki_message_api'; -import { TestUtils } from '../../test-utils'; -import { UserUtil } from '../../../util'; -import { MessageEncrypter } from '../../../session/crypto'; -import { SignalService } from '../../../protobuf'; -import { OpenGroupMessage } from '../../../session/messages/outgoing'; -import { EncryptionType } from '../../../session/types/EncryptionType'; - -describe('MessageSender', () => { - const sandbox = sinon.createSandbox(); - - afterEach(() => { - sandbox.restore(); - TestUtils.restoreStubs(); - }); - - describe('canSendToSnode', () => { - it('should return the correct value', () => { - const stub = TestUtils.stubWindow('lokiMessageAPI', undefined); - expect(MessageSender.canSendToSnode()).to.equal( - false, - 'We cannot send if lokiMessageAPI is not set' - ); - stub.set(sandbox.createStubInstance(LokiMessageAPI)); - expect(MessageSender.canSendToSnode()).to.equal( - true, - 'We can send if lokiMessageAPI is set' - ); - }); - }); - - describe('send', () => { - const ourNumber = 'ourNumber'; - let lokiMessageAPISendStub: sinon.SinonStub< - [string, Uint8Array, number, number], - Promise - >; - let encryptStub: sinon.SinonStub<[string, Uint8Array, EncryptionType]>; - - beforeEach(() => { - // We can do this because LokiMessageAPI has a module export in it - lokiMessageAPISendStub = sandbox.stub< - [string, Uint8Array, number, number], - Promise - >(); - TestUtils.stubWindow('lokiMessageAPI', { - sendMessage: lokiMessageAPISendStub, - }); - - encryptStub = sandbox.stub(MessageEncrypter, 'encrypt').resolves({ - envelopeType: SignalService.Envelope.Type.CIPHERTEXT, - cipherText: crypto.randomBytes(10), - }); - - sandbox.stub(UserUtil, 'getCurrentDevicePubKey').resolves(ourNumber); - }); - - describe('retry', () => { - const rawMessage = { - identifier: '1', - device: '0', - plainTextBuffer: crypto.randomBytes(10), - encryption: EncryptionType.Signal, - timestamp: Date.now(), - ttl: 100, - }; - - it('should not retry if an error occurred during encryption', async () => { - encryptStub.throws(new Error('Failed to encrypt.')); - const promise = MessageSender.send(rawMessage); - await expect(promise).is.rejectedWith('Failed to encrypt.'); - expect(lokiMessageAPISendStub.callCount).to.equal(0); - }); - - it('should only call lokiMessageAPI once if no errors occured', async () => { - await MessageSender.send(rawMessage); - expect(lokiMessageAPISendStub.callCount).to.equal(1); - }); - - it('should only retry the specified amount of times before throwing', async () => { - lokiMessageAPISendStub.throws(new Error('API error')); - const attempts = 2; - const promise = MessageSender.send(rawMessage, attempts); - await expect(promise).is.rejectedWith('API error'); - expect(lokiMessageAPISendStub.callCount).to.equal(attempts); - }); - - it('should not throw error if successful send occurs within the retry limit', async () => { - lokiMessageAPISendStub.onFirstCall().throws(new Error('API error')); - await MessageSender.send(rawMessage, 3); - expect(lokiMessageAPISendStub.callCount).to.equal(2); - }); - }); - - describe('logic', () => { - let messageEncyrptReturnEnvelopeType = - SignalService.Envelope.Type.CIPHERTEXT; - - beforeEach(() => { - encryptStub.callsFake(async (_device, plainTextBuffer, _type) => ({ - envelopeType: messageEncyrptReturnEnvelopeType, - cipherText: plainTextBuffer, - })); - }); - - it('should pass the correct values to lokiMessageAPI', async () => { - const device = '0'; - const timestamp = Date.now(); - const ttl = 100; - - await MessageSender.send({ - identifier: '1', - device, - plainTextBuffer: crypto.randomBytes(10), - encryption: EncryptionType.Signal, - timestamp, - ttl, - }); - - const args = lokiMessageAPISendStub.getCall(0).args; - expect(args[0]).to.equal(device); - expect(args[2]).to.equal(timestamp); - expect(args[3]).to.equal(ttl); - }); - - it('should correctly build the envelope', async () => { - messageEncyrptReturnEnvelopeType = - SignalService.Envelope.Type.CIPHERTEXT; - - // This test assumes the encryption stub returns the plainText passed into it. - const plainTextBuffer = crypto.randomBytes(10); - const timestamp = Date.now(); - - await MessageSender.send({ - identifier: '1', - device: '0', - plainTextBuffer, - encryption: EncryptionType.Signal, - timestamp, - ttl: 1, - }); - - const data = lokiMessageAPISendStub.getCall(0).args[1]; - const webSocketMessage = SignalService.WebSocketMessage.decode(data); - expect(webSocketMessage.request?.body).to.not.equal( - undefined, - 'Request body should not be undefined' - ); - expect(webSocketMessage.request?.body).to.not.equal( - null, - 'Request body should not be null' - ); - - const envelope = SignalService.Envelope.decode( - webSocketMessage.request?.body as Uint8Array - ); - expect(envelope.type).to.equal(SignalService.Envelope.Type.CIPHERTEXT); - expect(envelope.source).to.equal(ourNumber); - expect(envelope.sourceDevice).to.equal(1); - expect(toNumber(envelope.timestamp)).to.equal(timestamp); - expect(envelope.content).to.deep.equal(plainTextBuffer); - }); - - describe('UNIDENTIFIED_SENDER', () => { - it('should set the envelope source to be empty', async () => { - messageEncyrptReturnEnvelopeType = - SignalService.Envelope.Type.UNIDENTIFIED_SENDER; - - // This test assumes the encryption stub returns the plainText passed into it. - const plainTextBuffer = crypto.randomBytes(10); - const timestamp = Date.now(); - - await MessageSender.send({ - identifier: '1', - device: '0', - plainTextBuffer, - encryption: EncryptionType.Signal, - timestamp, - ttl: 1, - }); - - const data = lokiMessageAPISendStub.getCall(0).args[1]; - const webSocketMessage = SignalService.WebSocketMessage.decode(data); - expect(webSocketMessage.request?.body).to.not.equal( - undefined, - 'Request body should not be undefined' - ); - expect(webSocketMessage.request?.body).to.not.equal( - null, - 'Request body should not be null' - ); - - const envelope = SignalService.Envelope.decode( - webSocketMessage.request?.body as Uint8Array - ); - expect(envelope.type).to.equal( - SignalService.Envelope.Type.UNIDENTIFIED_SENDER - ); - expect(envelope.source).to.equal( - '', - 'envelope source should be empty in UNIDENTIFIED_SENDER' - ); - }); - }); - }); - }); - - describe('sendToOpenGroup', () => { - it('should send the message to the correct server and channel', async () => { - // We can do this because LokiPublicChatFactoryAPI has a module export in it - const stub = sandbox.stub().resolves({ - sendMessage: sandbox.stub(), - }); - - TestUtils.stubWindow('lokiPublicChatAPI', { - findOrCreateChannel: stub, - }); - - const group = { - server: 'server', - channel: 1, - conversationId: '0', - }; - - const message = new OpenGroupMessage({ - timestamp: Date.now(), - group, - }); - - await MessageSender.sendToOpenGroup(message); - - const [server, channel, conversationId] = stub.getCall(0).args; - expect(server).to.equal(group.server); - expect(channel).to.equal(group.channel); - expect(conversationId).to.equal(group.conversationId); - }); - }); -});