From 36762dbbf2edca3c390b5261c70c811322a3f73c Mon Sep 17 00:00:00 2001 From: Mikunj Date: Fri, 29 May 2020 16:30:28 +1000 Subject: [PATCH 1/8] Added libsignal-protocol typings. Added MessageEncrypter. --- libloki/crypto.js | 7 +- libtextsecure/libsignal-protocol.d.ts | 120 ++++++++++++++++++++++++++ ts/session/crypto/MessageEncrypter.ts | 57 ++++++++++-- ts/window.ts | 13 ++- 4 files changed, 185 insertions(+), 12 deletions(-) create mode 100644 libtextsecure/libsignal-protocol.d.ts diff --git a/libloki/crypto.js b/libloki/crypto.js index cbbaad074..e9a9988d2 100644 --- a/libloki/crypto.js +++ b/libloki/crypto.js @@ -149,10 +149,13 @@ myPrivateKey ); const ivAndCiphertext = await DHEncrypt(symmetricKey, plaintext); + const binaryIvAndCiphertext = dcodeIO.ByteBuffer.wrap( + ivAndCiphertext + ).toString('binary'); return { type: textsecure.protobuf.Envelope.Type.FRIEND_REQUEST, - body: ivAndCiphertext, - registrationId: null, + body: binaryIvAndCiphertext, + registrationId: undefined, }; } diff --git a/libtextsecure/libsignal-protocol.d.ts b/libtextsecure/libsignal-protocol.d.ts new file mode 100644 index 000000000..d8ae0db3e --- /dev/null +++ b/libtextsecure/libsignal-protocol.d.ts @@ -0,0 +1,120 @@ +import { SignalService } from '../ts/protobuf'; + +export type BinaryString = String; + +export type CipherTextObject = { + type: SignalService.Envelope.Type; + body: BinaryString; + registrationId?: number; +}; + +export declare class SignalProtocolAddress { + constructor(hexEncodedPublicKey: string, deviceId: number); + getName(): string; + getDeviceId(): number; + toString(): string; + equals(other: SignalProtocolAddress): boolean; + static fromString(encodedAddress: string): SignalProtocolAddress; +} + +export type KeyPair = { + pubKey: ArrayBuffer; + privKey: ArrayBuffer; +}; + +interface CurveSync { + generateKeyPair(): KeyPair; + createKeyPair(privKey: ArrayBuffer): KeyPair; + calculateAgreement(pubKey: ArrayBuffer, privKey: ArrayBuffer): ArrayBuffer; + verifySignature(pubKey: ArrayBuffer, msg: ArrayBuffer, sig: ArrayBuffer); + calculateSignature(privKey: ArrayBuffer, message: ArrayBuffer): ArrayBuffer; + validatePubKeyFormat(pubKey: ArrayBuffer): ArrayBuffer; +} + +interface CurveAsync { + generateKeyPair(): Promise; + createKeyPair(privKey: ArrayBuffer): Promise; + calculateAgreement( + pubKey: ArrayBuffer, + privKey: ArrayBuffer + ): Promise; + verifySignature( + pubKey: ArrayBuffer, + msg: ArrayBuffer, + sig: ArrayBuffer + ): Promise; + calculateSignature( + privKey: ArrayBuffer, + message: ArrayBuffer + ): Promise; + validatePubKeyFormat(pubKey: ArrayBuffer): Promise; +} + +export interface CurveInterface extends CurveSync { + async: CurveAsync; +} + +export interface CryptoInterface { + encrypt( + key: ArrayBuffer, + data: ArrayBuffer, + iv: ArrayBuffer + ): Promise; + decrypt( + key: ArrayBuffer, + data: ArrayBuffer, + iv: ArrayBuffer + ): Promise; + calculateMAC(key: ArrayBuffer, data: ArrayBuffer): Promise; + verifyMAC( + data: ArrayBuffer, + key: ArrayBuffer, + mac: ArrayBuffer, + length: number + ): Promise; + getRandomBytes(size: number): ArrayBuffer; +} + +export interface KeyHelperInterface { + generateIdentityKeyPair(): Promise; + generateRegistrationId(): number; + generateSignedPreKey( + identityKeyPair: KeyPair, + signedKeyId: number + ): Promise<{ + keyId: number; + keyPair: KeyPair; + signature: ArrayBuffer; + }>; + generatePreKey( + keyId: number + ): Promise<{ + keyId: number; + keyPair: KeyPair; + }>; +} + +export declare class SessionCipher { + constructor(storage: any, remoteAddress: SignalProtocolAddress); + /** + * @returns The envelope type, registration id and binary encoded encrypted body. + */ + encrypt(buffer: ArrayBuffer | Uint8Array): Promise; + decryptPreKeyWhisperMessage( + buffer: ArrayBuffer | Uint8Array + ): Promise; + decryptWhisperMessage(buffer: ArrayBuffer | Uint8Array): Promise; + getRecord(encodedNumber: string): Promise; + getRemoteRegistrationId(): Promise; + hasOpenSession(): Promise; + closeOpenSessionForDevice(): Promise; + deleteAllSessionsForDevice(): Promise; +} + +export interface LibsignalProtocol { + SignalProtocolAddress: typeof SignalProtocolAddress; + Curve: CurveInterface; + crypto: CryptoInterface; + KeyHelper: KeyHelperInterface; + SessionCipher: typeof SessionCipher; +} diff --git a/ts/session/crypto/MessageEncrypter.ts b/ts/session/crypto/MessageEncrypter.ts index 22ce64ccc..d58a11f2f 100644 --- a/ts/session/crypto/MessageEncrypter.ts +++ b/ts/session/crypto/MessageEncrypter.ts @@ -1,5 +1,10 @@ import { EncryptionType } from '../types/EncryptionType'; import { SignalService } from '../../protobuf'; +import { libloki, libsignal, textsecure } from '../../window'; +import { + CipherTextObject, + SignalProtocolAddress, +} from '../../../libtextsecure/libsignal-protocol'; function padPlainTextBuffer(messageBuffer: Uint8Array): Uint8Array { const plaintext = new Uint8Array( @@ -22,19 +27,57 @@ function getPaddedMessageLength(originalLength: number): number { return messagePartCount * 160; } -export function encrypt( +export type Base64String = String; + +/** + * Encrypt `plainTextBuffer` with given `encryptionType` for `device`. + * + * @param device The device 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, plainTextBuffer: Uint8Array, encryptionType: EncryptionType -): { +): Promise<{ envelopeType: SignalService.Envelope.Type; - cipherText: Uint8Array; -} { + cipherText: Base64String; +}> { const plainText = padPlainTextBuffer(plainTextBuffer); - // TODO: Do encryption here? + const address = new libsignal.SignalProtocolAddress(device, 1); + + if (encryptionType === EncryptionType.MediumGroup) { + // TODO: Do medium group stuff here + throw new Error('Encryption is not yet supported'); + } + + let cipherText: CipherTextObject; + if (encryptionType === EncryptionType.SessionReset) { + const cipher = new libloki.crypto.FallBackSessionCipher(address); + cipherText = await cipher.encrypt(plainText.buffer); + } else { + const cipher = new libsignal.SessionCipher( + textsecure.storage.protocol, + address + ); + cipherText = await cipher.encrypt(plainText.buffer); + } + + return encryptUsingSealedSender(address, cipherText); +} +async function encryptUsingSealedSender( + address: SignalProtocolAddress, + cipherText: CipherTextObject +): Promise<{ + envelopeType: SignalService.Envelope.Type; + cipherText: Base64String; +}> { + // TODO: Do stuff here return { - envelopeType: SignalService.Envelope.Type.CIPHERTEXT, - cipherText: new Uint8Array(), + envelopeType: SignalService.Envelope.Type.UNIDENTIFIED_SENDER, + cipherText: 'implement me!', }; } diff --git a/ts/window.ts b/ts/window.ts index aeb19ec5d..cc5a28be3 100644 --- a/ts/window.ts +++ b/ts/window.ts @@ -1,6 +1,7 @@ import { LocalizerType } from './types/Util'; +import { LibsignalProtocol } from '../libtextsecure/libsignal-protocol'; -interface Window { +interface WindowInterface extends Window { seedNodeList: any; WebAPI: any; @@ -32,7 +33,7 @@ interface Window { shortenPubkey: any; dcodeIO: any; - libsignal: any; + libsignal: LibsignalProtocol; libloki: any; displayNameRegex: any; @@ -72,7 +73,9 @@ interface Window { resetDatabase: any; } -declare const window: Window; +declare const window: WindowInterface; + +// TODO: Is there an easier way to dynamically export these? // Utilities export const WebAPI = window.WebAPI; @@ -118,3 +121,7 @@ export const clearLocalData = window.clearLocalData; export const deleteAccount = window.deleteAccount; export const resetDatabase = window.resetDatabase; export const attemptConnection = window.attemptConnection; + +export const libloki = window.libloki; +export const libsignal = window.libsignal; +export const textsecure = window.textsecure; From b644e2a05fef6cbafbd212d14971bd166fb28513 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Mon, 1 Jun 2020 12:53:51 +1000 Subject: [PATCH 2/8] Added more types. Finalise MessageEncrypter. --- js/modules/data.d.ts | 2 +- js/modules/metadata/SecretSessionCipher.d.ts | 23 ++++++++++ js/modules/signal.d.ts | 9 ++++ ts/session/crypto/MessageEncrypter.ts | 44 +++++++++++++------- ts/util/index.ts | 2 + ts/util/user.ts | 17 ++++++++ ts/window.ts | 3 +- 7 files changed, 84 insertions(+), 16 deletions(-) create mode 100644 js/modules/metadata/SecretSessionCipher.d.ts create mode 100644 js/modules/signal.d.ts create mode 100644 ts/util/user.ts diff --git a/js/modules/data.d.ts b/js/modules/data.d.ts index 92441555f..c70fd17e1 100644 --- a/js/modules/data.d.ts +++ b/js/modules/data.d.ts @@ -179,7 +179,7 @@ export function updateGuardNodes(nodes: Array): Promise; // Storage Items export function createOrUpdateItem(data: StorageItem): Promise; -export function getItemById(id: string): Promise; +export function getItemById(id: string): Promise; export function getAlItems(): Promise>; export function bulkAddItems(array: Array): Promise; export function removeItemById(id: string): Promise; diff --git a/js/modules/metadata/SecretSessionCipher.d.ts b/js/modules/metadata/SecretSessionCipher.d.ts new file mode 100644 index 000000000..1375167fd --- /dev/null +++ b/js/modules/metadata/SecretSessionCipher.d.ts @@ -0,0 +1,23 @@ +import { SignalService } from '../../../ts/protobuf'; +import { + BinaryString, + CipherTextObject, +} from '../../../libtextsecure/libsignal-protocol'; + +export declare class SecretSessionCipher { + constructor(storage: any); + encrypt( + destinationPubkey: string, + senderCertificate: SignalService.SenderCertificate, + innerEncryptedMessage: CipherTextObject + ): Promise; + decrypt( + cipherText: ArrayBuffer, + me: { number: string; deviceId: number } + ): Promise<{ + isMe?: boolean; + sender: string; + content: ArrayBuffer; + type: SignalService.Envelope.Type; + }>; +} diff --git a/js/modules/signal.d.ts b/js/modules/signal.d.ts new file mode 100644 index 000000000..f395f30a6 --- /dev/null +++ b/js/modules/signal.d.ts @@ -0,0 +1,9 @@ +import { SecretSessionCipher } from './metadata/SecretSessionCipher'; + +interface Metadata { + SecretSessionCipher: typeof SecretSessionCipher; +} + +export interface SignalInterface { + Metadata: Metadata; +} diff --git a/ts/session/crypto/MessageEncrypter.ts b/ts/session/crypto/MessageEncrypter.ts index d58a11f2f..f19a3bbfa 100644 --- a/ts/session/crypto/MessageEncrypter.ts +++ b/ts/session/crypto/MessageEncrypter.ts @@ -1,12 +1,10 @@ import { EncryptionType } from '../types/EncryptionType'; import { SignalService } from '../../protobuf'; -import { libloki, libsignal, textsecure } from '../../window'; -import { - CipherTextObject, - SignalProtocolAddress, -} from '../../../libtextsecure/libsignal-protocol'; +import { libloki, libsignal, Signal, textsecure } from '../../window'; +import { CipherTextObject } from '../../../libtextsecure/libsignal-protocol'; +import { UserUtil } from '../../util'; -function padPlainTextBuffer(messageBuffer: Uint8Array): Uint8Array { +export function padPlainTextBuffer(messageBuffer: Uint8Array): Uint8Array { const plaintext = new Uint8Array( getPaddedMessageLength(messageBuffer.byteLength + 1) - 1 ); @@ -53,31 +51,49 @@ export async function encrypt( throw new Error('Encryption is not yet supported'); } - let cipherText: CipherTextObject; + let innerCipherText: CipherTextObject; if (encryptionType === EncryptionType.SessionReset) { const cipher = new libloki.crypto.FallBackSessionCipher(address); - cipherText = await cipher.encrypt(plainText.buffer); + innerCipherText = await cipher.encrypt(plainText.buffer); } else { const cipher = new libsignal.SessionCipher( textsecure.storage.protocol, address ); - cipherText = await cipher.encrypt(plainText.buffer); + innerCipherText = await cipher.encrypt(plainText.buffer); } - return encryptUsingSealedSender(address, cipherText); + return encryptUsingSealedSender(device, innerCipherText); } async function encryptUsingSealedSender( - address: SignalProtocolAddress, - cipherText: CipherTextObject + device: string, + innerCipherText: CipherTextObject ): Promise<{ envelopeType: SignalService.Envelope.Type; cipherText: Base64String; }> { - // TODO: Do stuff here + const ourNumber = await UserUtil.getCurrentDevicePubKey(); + if (!ourNumber) { + throw new Error('Failed to fetch current device public key.'); + } + + const certificate = SignalService.SenderCertificate.create({ + sender: ourNumber, + senderDevice: 1, + }); + + const cipher = new Signal.Metadata.SecretSessionCipher( + textsecure.storage.protocol + ); + const cipherTextBuffer = await cipher.encrypt( + device, + certificate, + innerCipherText + ); + return { envelopeType: SignalService.Envelope.Type.UNIDENTIFIED_SENDER, - cipherText: 'implement me!', + cipherText: Buffer.from(cipherTextBuffer).toString('base64'), }; } diff --git a/ts/util/index.ts b/ts/util/index.ts index ec0ca2e21..8416f8630 100644 --- a/ts/util/index.ts +++ b/ts/util/index.ts @@ -4,6 +4,7 @@ import { isFileDangerous } from './isFileDangerous'; import { missingCaseError } from './missingCaseError'; import { migrateColor } from './migrateColor'; import { makeLookup } from './makeLookup'; +import * as UserUtil from './user'; export { arrayBufferToObjectURL, @@ -12,4 +13,5 @@ export { makeLookup, migrateColor, missingCaseError, + UserUtil, }; diff --git a/ts/util/user.ts b/ts/util/user.ts new file mode 100644 index 000000000..cea991b17 --- /dev/null +++ b/ts/util/user.ts @@ -0,0 +1,17 @@ +import { getItemById } from '../../js/modules/data'; +import { KeyPair } from '../../libtextsecure/libsignal-protocol'; + +export async function getCurrentDevicePubKey(): Promise { + const item = await getItemById('number_id'); + if (!item || !item.value) { + return undefined; + } + + return item.value.split('.')[0]; +} + +export async function getIdentityKeyPair(): Promise { + const item = await getItemById('identityKey'); + + return item?.value; +} diff --git a/ts/window.ts b/ts/window.ts index cc5a28be3..1cfa81639 100644 --- a/ts/window.ts +++ b/ts/window.ts @@ -1,5 +1,6 @@ import { LocalizerType } from './types/Util'; import { LibsignalProtocol } from '../libtextsecure/libsignal-protocol'; +import { SignalInterface } from '../js/modules/signal'; interface WindowInterface extends Window { seedNodeList: any; @@ -37,7 +38,7 @@ interface WindowInterface extends Window { libloki: any; displayNameRegex: any; - Signal: any; + Signal: SignalInterface; Whisper: any; ConversationController: any; From 736cbc06da28e448fa351b71cf23fe32edd55c41 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Mon, 1 Jun 2020 15:04:09 +1000 Subject: [PATCH 3/8] Add tests --- js/modules/data.js | 76 ++++++++++--------- js/modules/signal.js | 2 + .../session/crypto/MessageEncrypter_test.ts | 47 ++++++++++++ .../utils/stubs/SignalAddressProtocolStub.ts | 27 +++++++ ts/test/utils/stubs/index.ts | 1 + ts/window.ts | 7 ++ 6 files changed, 123 insertions(+), 37 deletions(-) create mode 100644 ts/test/session/crypto/MessageEncrypter_test.ts create mode 100644 ts/test/utils/stubs/SignalAddressProtocolStub.ts create mode 100644 ts/test/utils/stubs/index.ts diff --git a/js/modules/data.js b/js/modules/data.js index 39819d5e4..cfbc3d006 100644 --- a/js/modules/data.js +++ b/js/modules/data.js @@ -1,7 +1,8 @@ /* global window, setTimeout, clearTimeout, IDBKeyRange, dcodeIO */ - const electron = require('electron'); +const { ipcRenderer } = electron; + // TODO: this results in poor readability, would be // much better to explicitly call with `_`. const { @@ -21,12 +22,6 @@ const _ = require('lodash'); const { base64ToArrayBuffer, arrayBufferToBase64 } = require('./crypto'); const MessageType = require('./types/message'); -const { ipcRenderer } = electron; - -// We listen to a lot of events on ipcRenderer, often on the same channel. This prevents -// any warnings that might be sent to the console in that case. -ipcRenderer.setMaxListeners(0); - const DATABASE_UPDATE_TIMEOUT = 2 * 60 * 1000; // two minutes const SQL_CHANNEL_KEY = 'sql-channel'; @@ -44,6 +39,7 @@ let _shutdownPromise = null; const channels = {}; module.exports = { + init, _jobs, _cleanData, @@ -212,6 +208,42 @@ module.exports = { createOrUpdateSenderKeys, }; +function init() { + // We listen to a lot of events on ipcRenderer, often on the same channel. This prevents + // any warnings that might be sent to the console in that case. + ipcRenderer.setMaxListeners(0); + + forEach(module.exports, fn => { + if (isFunction(fn) && fn.name !== 'init') { + makeChannel(fn.name); + } + }); + + ipcRenderer.on( + `${SQL_CHANNEL_KEY}-done`, + (event, jobId, errorForDisplay, result) => { + const job = _getJob(jobId); + if (!job) { + throw new Error( + `Received SQL channel reply to job ${jobId}, but did not have it in our registry!` + ); + } + + const { resolve, reject, fnName } = job; + + if (errorForDisplay) { + return reject( + new Error( + `Error received from SQL channel job ${jobId} (${fnName}): ${errorForDisplay}` + ) + ); + } + + return resolve(result); + } + ); +} + // When IPC arguments are prepared for the cross-process send, they are JSON.stringified. // We can't send ArrayBuffers or BigNumbers (what we get from proto library for dates). function _cleanData(data) { @@ -352,30 +384,6 @@ function _getJob(id) { return _jobs[id]; } -ipcRenderer.on( - `${SQL_CHANNEL_KEY}-done`, - (event, jobId, errorForDisplay, result) => { - const job = _getJob(jobId); - if (!job) { - throw new Error( - `Received SQL channel reply to job ${jobId}, but did not have it in our registry!` - ); - } - - const { resolve, reject, fnName } = job; - - if (errorForDisplay) { - return reject( - new Error( - `Error received from SQL channel job ${jobId} (${fnName}): ${errorForDisplay}` - ) - ); - } - - return resolve(result); - } -); - function makeChannel(fnName) { channels[fnName] = (...args) => { const jobId = _makeJob(fnName); @@ -398,12 +406,6 @@ function makeChannel(fnName) { }; } -forEach(module.exports, fn => { - if (isFunction(fn)) { - makeChannel(fn.name); - } -}); - function keysToArrayBuffer(keys, data) { const updated = cloneDeep(data); for (let i = 0, max = keys.length; i < max; i += 1) { diff --git a/js/modules/signal.js b/js/modules/signal.js index c676c5845..b4dad50a8 100644 --- a/js/modules/signal.js +++ b/js/modules/signal.js @@ -263,6 +263,8 @@ function initializeMigrations({ exports.setup = (options = {}) => { const { Attachments, userDataPath, getRegionCode, logger } = options; + Data.init(); + const Migrations = initializeMigrations({ userDataPath, getRegionCode, diff --git a/ts/test/session/crypto/MessageEncrypter_test.ts b/ts/test/session/crypto/MessageEncrypter_test.ts new file mode 100644 index 000000000..7a8ce125b --- /dev/null +++ b/ts/test/session/crypto/MessageEncrypter_test.ts @@ -0,0 +1,47 @@ +import { expect } from 'chai'; +import * as crypto from 'crypto'; +import * as sinon from 'sinon'; +import * as window from '../../../window'; +import { MessageEncrypter } from '../../../session/crypto'; +import { EncryptionType } from '../../../session/types/EncryptionType'; + +describe('MessageEncrypter', () => { + const sandbox = sinon.sandbox.create(); + + beforeEach(() => { + sandbox.stub(window); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('EncryptionType', () => { + describe('MediumGroup', () => { + it('should throw an error', async () => { + const data = crypto.randomBytes(10); + const promise = MessageEncrypter.encrypt('1', data, EncryptionType.MediumGroup); + await expect(promise).to.be.rejectedWith('Encryption is not yet supported'); + }); + }); + + /* + describe('SessionReset', () => { + it('should call FallbackSessionCipher', async () => { + }); + + it('should pass the padded message body to encrypt', async () => { + }); + }); + + describe('Signal', () => { + it('should call SessionCipher', async () => { + + }); + + it('should pass the padded message body to encrypt', async () => { + }); + }); + */ + }); +}); diff --git a/ts/test/utils/stubs/SignalAddressProtocolStub.ts b/ts/test/utils/stubs/SignalAddressProtocolStub.ts new file mode 100644 index 000000000..28f7697a4 --- /dev/null +++ b/ts/test/utils/stubs/SignalAddressProtocolStub.ts @@ -0,0 +1,27 @@ +import { SignalProtocolAddress } from "../../../../libtextsecure/libsignal-protocol"; + +export class SignalProtocolAddressStub extends SignalProtocolAddress { + private readonly hexEncodedPublicKey: string; + private readonly deviceId: number; + constructor(hexEncodedPublicKey: string, deviceId: number) { + super(hexEncodedPublicKey, deviceId); + this.hexEncodedPublicKey = hexEncodedPublicKey; + this.deviceId = deviceId; + } + + // tslint:disable-next-line: function-name + public static fromString(encodedAddress: string): SignalProtocolAddressStub { + const values = encodedAddress.split('.'); + + return new SignalProtocolAddressStub(values[0], Number(values[1])); + } + + public getName(): string { return this.hexEncodedPublicKey; } + public getDeviceId(): number { return this.deviceId; } + + public equals(other: SignalProtocolAddress): boolean { + return other.getName() === this.hexEncodedPublicKey; + } + + public toString(): string { return this.hexEncodedPublicKey; } +} diff --git a/ts/test/utils/stubs/index.ts b/ts/test/utils/stubs/index.ts new file mode 100644 index 000000000..8459903f1 --- /dev/null +++ b/ts/test/utils/stubs/index.ts @@ -0,0 +1 @@ +export * from './SignalAddressProtocolStub'; diff --git a/ts/window.ts b/ts/window.ts index 1cfa81639..51376df70 100644 --- a/ts/window.ts +++ b/ts/window.ts @@ -74,6 +74,13 @@ interface WindowInterface extends Window { resetDatabase: any; } +// In the case for tests +// tslint:disable-next-line: no-typeof-undefined +if (typeof(window) === 'undefined') { + const globalAny: any = global; + globalAny.window = {}; +} + declare const window: WindowInterface; // TODO: Is there an easier way to dynamically export these? From cd58e9b86e91703abfbc142f0e48116aad15dbfe Mon Sep 17 00:00:00 2001 From: Mikunj Date: Mon, 1 Jun 2020 16:00:09 +1000 Subject: [PATCH 4/8] Add utils for stubbing. Added ImportMock for easier es6 stubbing. --- package.json | 5 +- ts/session/crypto/MessageEncrypter.ts | 2 +- .../session/crypto/MessageEncrypter_test.ts | 37 ++++- ts/test/utils/index.ts | 3 + ts/test/utils/stubs/SessionCipherBasicStub.ts | 14 ++ .../utils/stubs/SignalAddressProtocolStub.ts | 27 ---- ts/test/utils/stubs/index.ts | 2 +- ts/test/utils/testUtils.ts | 18 +++ ts/util/user.ts | 2 +- ts/{window.ts => window/index.ts} | 7 +- .../window/types/SecretSessionCipher.ts | 9 +- .../window/types/libsignal-protocol.ts | 30 ++-- .../signal.d.ts => ts/window/types/signal.ts | 2 +- yarn.lock | 130 +++++++++--------- 14 files changed, 162 insertions(+), 126 deletions(-) create mode 100644 ts/test/utils/index.ts create mode 100644 ts/test/utils/stubs/SessionCipherBasicStub.ts delete mode 100644 ts/test/utils/stubs/SignalAddressProtocolStub.ts create mode 100644 ts/test/utils/testUtils.ts rename ts/{window.ts => window/index.ts} (95%) rename js/modules/metadata/SecretSessionCipher.d.ts => ts/window/types/SecretSessionCipher.ts (76%) rename libtextsecure/libsignal-protocol.d.ts => ts/window/types/libsignal-protocol.ts (77%) rename js/modules/signal.d.ts => ts/window/types/signal.ts (65%) diff --git a/package.json b/package.json index 8dda1a941..6d9df3e71 100644 --- a/package.json +++ b/package.json @@ -144,7 +144,7 @@ "@types/redux-logger": "3.0.7", "@types/rimraf": "2.0.2", "@types/semver": "5.5.0", - "@types/sinon": "4.3.1", + "@types/sinon": "9.0.4", "@types/uuid": "3.4.4", "arraybuffer-loader": "1.0.3", "asar": "0.14.0", @@ -180,9 +180,10 @@ "qs": "6.5.1", "react-docgen-typescript": "1.2.6", "react-styleguidist": "7.0.1", - "sinon": "4.4.2", + "sinon": "9.0.2", "spectron": "^10.0.0", "ts-loader": "4.1.0", + "ts-mock-imports": "^1.3.0", "tslint": "5.13.0", "tslint-microsoft-contrib": "6.0.0", "tslint-react": "3.6.0", diff --git a/ts/session/crypto/MessageEncrypter.ts b/ts/session/crypto/MessageEncrypter.ts index f19a3bbfa..89e5bd35a 100644 --- a/ts/session/crypto/MessageEncrypter.ts +++ b/ts/session/crypto/MessageEncrypter.ts @@ -1,7 +1,7 @@ import { EncryptionType } from '../types/EncryptionType'; import { SignalService } from '../../protobuf'; import { libloki, libsignal, Signal, textsecure } from '../../window'; -import { CipherTextObject } from '../../../libtextsecure/libsignal-protocol'; +import { CipherTextObject } from '../../window/types/libsignal-protocol'; import { UserUtil } from '../../util'; export function padPlainTextBuffer(messageBuffer: Uint8Array): Uint8Array { diff --git a/ts/test/session/crypto/MessageEncrypter_test.ts b/ts/test/session/crypto/MessageEncrypter_test.ts index 7a8ce125b..e89e4de0b 100644 --- a/ts/test/session/crypto/MessageEncrypter_test.ts +++ b/ts/test/session/crypto/MessageEncrypter_test.ts @@ -1,19 +1,39 @@ import { expect } from 'chai'; +import { ImportMock, MockManager } from 'ts-mock-imports'; import * as crypto from 'crypto'; import * as sinon from 'sinon'; import * as window from '../../../window'; import { MessageEncrypter } from '../../../session/crypto'; import { EncryptionType } from '../../../session/types/EncryptionType'; +import * as stubs from '../../utils/stubs'; +import { TestUtils } from '../../utils'; describe('MessageEncrypter', () => { - const sandbox = sinon.sandbox.create(); + const sandbox = sinon.createSandbox(); + let sessionCipherStub: MockManager; beforeEach(() => { - sandbox.stub(window); + sessionCipherStub = ImportMock.mockClass(stubs, 'SessionCipherBasicStub'); + ImportMock.mockOther(window, 'libsignal', { + SignalProtocolAddress: sandbox.stub(), + SessionCipher: stubs.SessionCipherBasicStub, + } as any); + + ImportMock.mockOther(window, 'textsecure', { + storage: { + protocol: sandbox.stub(), + }, + }); + + TestUtils.mockData('getItemById', undefined).resolves({ + id: 'number_id', + value: 'abc.1', + }); }); afterEach(() => { sandbox.restore(); + ImportMock.restore(); }); describe('EncryptionType', () => { @@ -33,15 +53,20 @@ describe('MessageEncrypter', () => { it('should pass the padded message body to encrypt', async () => { }); }); - + */ describe('Signal', () => { - it('should call SessionCipher', async () => { - + it('should call SessionCipher encrypt', async () => { + const data = crypto.randomBytes(10); + const stub = sessionCipherStub.mock('encrypt').resolves({ + type: 1, + body: 'body', + }); + await MessageEncrypter.encrypt('1', data, EncryptionType.Signal); + expect(stub.called).to.equal(true, 'SessionCipher.encrypt should be called.'); }); it('should pass the padded message body to encrypt', async () => { }); }); - */ }); }); diff --git a/ts/test/utils/index.ts b/ts/test/utils/index.ts new file mode 100644 index 000000000..80d6561ec --- /dev/null +++ b/ts/test/utils/index.ts @@ -0,0 +1,3 @@ +import * as TestUtils from './testUtils'; + +export { TestUtils }; diff --git a/ts/test/utils/stubs/SessionCipherBasicStub.ts b/ts/test/utils/stubs/SessionCipherBasicStub.ts new file mode 100644 index 000000000..47a22d905 --- /dev/null +++ b/ts/test/utils/stubs/SessionCipherBasicStub.ts @@ -0,0 +1,14 @@ +import { CipherTextObject } from '../../../window/types/libsignal-protocol'; + +export class SessionCipherBasicStub { + public storage: any; + public address: any; + constructor(storage: any, address: any) { + this.storage = storage; + this.address = address; + } + + public async encrypt(buffer: ArrayBuffer | Uint8Array): Promise { + throw new Error('Should stub this out'); + } +} diff --git a/ts/test/utils/stubs/SignalAddressProtocolStub.ts b/ts/test/utils/stubs/SignalAddressProtocolStub.ts deleted file mode 100644 index 28f7697a4..000000000 --- a/ts/test/utils/stubs/SignalAddressProtocolStub.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { SignalProtocolAddress } from "../../../../libtextsecure/libsignal-protocol"; - -export class SignalProtocolAddressStub extends SignalProtocolAddress { - private readonly hexEncodedPublicKey: string; - private readonly deviceId: number; - constructor(hexEncodedPublicKey: string, deviceId: number) { - super(hexEncodedPublicKey, deviceId); - this.hexEncodedPublicKey = hexEncodedPublicKey; - this.deviceId = deviceId; - } - - // tslint:disable-next-line: function-name - public static fromString(encodedAddress: string): SignalProtocolAddressStub { - const values = encodedAddress.split('.'); - - return new SignalProtocolAddressStub(values[0], Number(values[1])); - } - - public getName(): string { return this.hexEncodedPublicKey; } - public getDeviceId(): number { return this.deviceId; } - - public equals(other: SignalProtocolAddress): boolean { - return other.getName() === this.hexEncodedPublicKey; - } - - public toString(): string { return this.hexEncodedPublicKey; } -} diff --git a/ts/test/utils/stubs/index.ts b/ts/test/utils/stubs/index.ts index 8459903f1..8ec99023e 100644 --- a/ts/test/utils/stubs/index.ts +++ b/ts/test/utils/stubs/index.ts @@ -1 +1 @@ -export * from './SignalAddressProtocolStub'; +export * from './SessionCipherBasicStub'; diff --git a/ts/test/utils/testUtils.ts b/ts/test/utils/testUtils.ts new file mode 100644 index 000000000..e344462e8 --- /dev/null +++ b/ts/test/utils/testUtils.ts @@ -0,0 +1,18 @@ +import * as sinon from 'sinon'; +import { ImportMock } from 'ts-mock-imports'; +import * as Shape from '../../../js/modules/data'; + +// We have to do this in a weird way because Data uses module.exports +// which doesn't play well with sinon or ImportMock +// tslint:disable-next-line: no-require-imports no-var-requires +const Data = require('../../../js/modules/data'); +type DataFunction = typeof Shape; + +/** + * Mock a function inside Data. + * + * Note: This uses `ImportMock` so you will have to call `ImportMock.restore()` or `stub.restore()` after each test. + */ +export function mockData(fn: keyof DataFunction, returns?: any): sinon.SinonStub { + return ImportMock.mockFunction(Data, fn, returns); +} diff --git a/ts/util/user.ts b/ts/util/user.ts index cea991b17..cec8716ec 100644 --- a/ts/util/user.ts +++ b/ts/util/user.ts @@ -1,5 +1,5 @@ import { getItemById } from '../../js/modules/data'; -import { KeyPair } from '../../libtextsecure/libsignal-protocol'; +import { KeyPair } from '../window/types/libsignal-protocol'; export async function getCurrentDevicePubKey(): Promise { const item = await getItemById('number_id'); diff --git a/ts/window.ts b/ts/window/index.ts similarity index 95% rename from ts/window.ts rename to ts/window/index.ts index 51376df70..44ee56e77 100644 --- a/ts/window.ts +++ b/ts/window/index.ts @@ -1,6 +1,7 @@ -import { LocalizerType } from './types/Util'; -import { LibsignalProtocol } from '../libtextsecure/libsignal-protocol'; -import { SignalInterface } from '../js/modules/signal'; + +import { LibsignalProtocol } from './types/libsignal-protocol'; +import { SignalInterface } from './types/signal'; +import { LocalizerType } from '../types/Util'; interface WindowInterface extends Window { seedNodeList: any; diff --git a/js/modules/metadata/SecretSessionCipher.d.ts b/ts/window/types/SecretSessionCipher.ts similarity index 76% rename from js/modules/metadata/SecretSessionCipher.d.ts rename to ts/window/types/SecretSessionCipher.ts index 1375167fd..2349aafb2 100644 --- a/js/modules/metadata/SecretSessionCipher.d.ts +++ b/ts/window/types/SecretSessionCipher.ts @@ -1,17 +1,16 @@ -import { SignalService } from '../../../ts/protobuf'; +import { SignalService } from '../../protobuf'; import { - BinaryString, CipherTextObject, -} from '../../../libtextsecure/libsignal-protocol'; +} from './libsignal-protocol'; export declare class SecretSessionCipher { constructor(storage: any); - encrypt( + public encrypt( destinationPubkey: string, senderCertificate: SignalService.SenderCertificate, innerEncryptedMessage: CipherTextObject ): Promise; - decrypt( + public decrypt( cipherText: ArrayBuffer, me: { number: string; deviceId: number } ): Promise<{ diff --git a/libtextsecure/libsignal-protocol.d.ts b/ts/window/types/libsignal-protocol.ts similarity index 77% rename from libtextsecure/libsignal-protocol.d.ts rename to ts/window/types/libsignal-protocol.ts index d8ae0db3e..ffc03db0e 100644 --- a/libtextsecure/libsignal-protocol.d.ts +++ b/ts/window/types/libsignal-protocol.ts @@ -1,4 +1,4 @@ -import { SignalService } from '../ts/protobuf'; +import { SignalService } from '../../protobuf'; export type BinaryString = String; @@ -10,11 +10,11 @@ export type CipherTextObject = { export declare class SignalProtocolAddress { constructor(hexEncodedPublicKey: string, deviceId: number); - getName(): string; - getDeviceId(): number; - toString(): string; - equals(other: SignalProtocolAddress): boolean; - static fromString(encodedAddress: string): SignalProtocolAddress; + public static fromString(encodedAddress: string): SignalProtocolAddress; + public getName(): string; + public getDeviceId(): number; + public toString(): string; + public equals(other: SignalProtocolAddress): boolean; } export type KeyPair = { @@ -26,7 +26,7 @@ interface CurveSync { generateKeyPair(): KeyPair; createKeyPair(privKey: ArrayBuffer): KeyPair; calculateAgreement(pubKey: ArrayBuffer, privKey: ArrayBuffer): ArrayBuffer; - verifySignature(pubKey: ArrayBuffer, msg: ArrayBuffer, sig: ArrayBuffer); + verifySignature(pubKey: ArrayBuffer, msg: ArrayBuffer, sig: ArrayBuffer): void; calculateSignature(privKey: ArrayBuffer, message: ArrayBuffer): ArrayBuffer; validatePubKeyFormat(pubKey: ArrayBuffer): ArrayBuffer; } @@ -99,16 +99,16 @@ export declare class SessionCipher { /** * @returns The envelope type, registration id and binary encoded encrypted body. */ - encrypt(buffer: ArrayBuffer | Uint8Array): Promise; - decryptPreKeyWhisperMessage( + public encrypt(buffer: ArrayBuffer | Uint8Array): Promise; + public decryptPreKeyWhisperMessage( buffer: ArrayBuffer | Uint8Array ): Promise; - decryptWhisperMessage(buffer: ArrayBuffer | Uint8Array): Promise; - getRecord(encodedNumber: string): Promise; - getRemoteRegistrationId(): Promise; - hasOpenSession(): Promise; - closeOpenSessionForDevice(): Promise; - deleteAllSessionsForDevice(): Promise; + public decryptWhisperMessage(buffer: ArrayBuffer | Uint8Array): Promise; + public getRecord(encodedNumber: string): Promise; + public getRemoteRegistrationId(): Promise; + public hasOpenSession(): Promise; + public closeOpenSessionForDevice(): Promise; + public deleteAllSessionsForDevice(): Promise; } export interface LibsignalProtocol { diff --git a/js/modules/signal.d.ts b/ts/window/types/signal.ts similarity index 65% rename from js/modules/signal.d.ts rename to ts/window/types/signal.ts index f395f30a6..e1ed96102 100644 --- a/js/modules/signal.d.ts +++ b/ts/window/types/signal.ts @@ -1,4 +1,4 @@ -import { SecretSessionCipher } from './metadata/SecretSessionCipher'; +import { SecretSessionCipher } from './SecretSessionCipher'; interface Metadata { SecretSessionCipher: typeof SecretSessionCipher; diff --git a/yarn.lock b/yarn.lock index bf5553f64..ebb34c0e3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -113,36 +113,43 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== -"@sinonjs/commons@^1", "@sinonjs/commons@^1.3.0", "@sinonjs/commons@^1.7.0": +"@sinonjs/commons@^1", "@sinonjs/commons@^1.7.0": version "1.7.1" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.7.1.tgz#da5fd19a5f71177a53778073978873964f49acf1" integrity sha512-Debi3Baff1Qu1Unc3mjJ96MgpbwTn43S1+9yJ0llWygPwDNu2aaWBD6yc9y/Z8XDRNhx7U+u2UDg2OGQXkclUQ== dependencies: type-detect "4.0.8" -"@sinonjs/formatio@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-2.0.0.tgz#84db7e9eb5531df18a8c5e0bfb6e449e55e654b2" - integrity sha512-ls6CAMA6/5gG+O/IdsBcblvnd8qcO/l1TYoNeAzp3wcISOxlPXQEus0mLcdwazEkWjaBdaJ3TaxmNgCLWwvWzg== +"@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.2": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.0.tgz#c8d68821a854c555bba172f3b06959a0039b236d" + integrity sha512-wEj54PfsZ5jGSwMX68G8ZXFawcSglQSXqCftWX3ec8MDUzQdHgcKvw97awHbY0efQEL5iKUOAmmVtoYgmrSG4Q== dependencies: - samsam "1.3.0" + type-detect "4.0.8" -"@sinonjs/formatio@^3.2.1": - version "3.2.2" - resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-3.2.2.tgz#771c60dfa75ea7f2d68e3b94c7e888a78781372c" - integrity sha512-B8SEsgd8gArBLMD6zpRw3juQ2FVSsmdd7qlevyDqzS9WTCtvF55/gAL+h6gue8ZvPYcdiPdvueM/qm//9XzyTQ== +"@sinonjs/fake-timers@^6.0.0", "@sinonjs/fake-timers@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz#293674fccb3262ac782c7aadfdeca86b10c75c40" + integrity sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA== + dependencies: + "@sinonjs/commons" "^1.7.0" + +"@sinonjs/formatio@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-5.0.1.tgz#f13e713cb3313b1ab965901b01b0828ea6b77089" + integrity sha512-KaiQ5pBf1MpS09MuA0kp6KBQt2JUOQycqVG1NZXvzeaXe5LGFqAKueIS0bw4w0P9r7KuBSVdUk5QjXsUdu2CxQ== dependencies: "@sinonjs/commons" "^1" - "@sinonjs/samsam" "^3.1.0" + "@sinonjs/samsam" "^5.0.2" -"@sinonjs/samsam@^3.1.0": - version "3.3.3" - resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-3.3.3.tgz#46682efd9967b259b81136b9f120fd54585feb4a" - integrity sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ== +"@sinonjs/samsam@^5.0.2", "@sinonjs/samsam@^5.0.3": + version "5.0.3" + resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-5.0.3.tgz#86f21bdb3d52480faf0892a480c9906aa5a52938" + integrity sha512-QucHkc2uMJ0pFGjJUDP3F9dq5dx8QIaqISl9QgwLOh6P9yv877uONPGXh/OH/0zmM3tW1JjuJltAZV2l7zU+uQ== dependencies: - "@sinonjs/commons" "^1.3.0" - array-from "^2.1.1" - lodash "^4.17.15" + "@sinonjs/commons" "^1.6.0" + lodash.get "^4.4.2" + type-detect "^4.0.8" "@sinonjs/text-encoding@^0.7.1": version "0.7.1" @@ -411,10 +418,17 @@ dependencies: "@types/node" "*" -"@types/sinon@4.3.1": - version "4.3.1" - resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-4.3.1.tgz#32458f9b166cd44c23844eee4937814276f35199" - integrity sha512-DK4YtH30I67k4klURIBS4VAe1aBISfS9lgNlHFkibSmKem2tLQc5VkKoJreT3dCJAd+xRyCS8bx1o97iq3yUVg== +"@types/sinon@9.0.4": + version "9.0.4" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-9.0.4.tgz#e934f904606632287a6e7f7ab0ce3f08a0dad4b1" + integrity sha512-sJmb32asJZY6Z2u09bl0G2wglSxDlROlAejCjsnor+LzBMz17gu8IU7vKC/vWDnv9zEq2wqADHVXFjf4eE8Gdw== + dependencies: + "@types/sinonjs__fake-timers" "*" + +"@types/sinonjs__fake-timers@*": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz#681df970358c82836b42f989188d133e218c458e" + integrity sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA== "@types/sizzle@*": version "2.3.2" @@ -777,11 +791,6 @@ array-flatten@^2.1.0: resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099" integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== -array-from@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/array-from/-/array-from-2.1.1.tgz#cfe9d8c26628b9dc5aecc62a9f5d8f1f352c1195" - integrity sha1-z+nYwmYoudxa7MYqn12PHzUsEZU= - array-includes@^3.0.3: version "3.1.1" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.1.tgz#cdd67e6852bdf9c1215460786732255ed2459348" @@ -2749,11 +2758,16 @@ diff@3.3.1: resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.1.tgz#aa8567a6eed03c531fc89d3f711cd0e5259dec75" integrity sha512-MKPHZDMB0o6yHyDryUOScqZibp914ksXwAMYMTHj6KO8UeKsRYNJD3oNCKjTqZon+V488P7N/HzXF8t7ZR95ww== -diff@^3.1.0, diff@^3.2.0: +diff@^3.2.0: version "3.5.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== +diff@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + diffie-hellman@^5.0.0: version "5.0.3" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" @@ -5945,18 +5959,6 @@ loglevel@^1.4.1: resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.7.tgz#b3e034233188c68b889f5b862415306f565e2c56" integrity sha512-cY2eLFrQSAfVPhCgH1s7JI73tMbg9YC3v3+ZHVW67sBS7UxWzNEk/ZBbSfLykBWHp33dqqtOv82gjhKEi81T/A== -lolex@^2.2.0: - version "2.7.5" - resolved "https://registry.yarnpkg.com/lolex/-/lolex-2.7.5.tgz#113001d56bfc7e02d56e36291cc5c413d1aa0733" - integrity sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q== - -lolex@^5.0.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/lolex/-/lolex-5.1.2.tgz#953694d098ce7c07bc5ed6d0e42bc6c0c6d5a367" - integrity sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A== - dependencies: - "@sinonjs/commons" "^1.7.0" - long@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" @@ -6543,15 +6545,15 @@ neo-async@^2.5.0, neo-async@^2.6.0: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw== -nise@^1.2.0: - version "1.5.3" - resolved "https://registry.yarnpkg.com/nise/-/nise-1.5.3.tgz#9d2cfe37d44f57317766c6e9408a359c5d3ac1f7" - integrity sha512-Ymbac/94xeIrMf59REBPOv0thr+CJVFMhrlAkW/gjCIE58BGQdCj0x7KRCb3yz+Ga2Rz3E9XXSvUyyxqqhjQAQ== +nise@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/nise/-/nise-4.0.3.tgz#9f79ff02fa002ed5ffbc538ad58518fa011dc913" + integrity sha512-EGlhjm7/4KvmmE6B/UFsKh7eHykRl9VH+au8dduHLCyWUO/hr7+N+WtTvDUwc9zHuM1IaIJs/0lQ6Ag1jDkQSg== dependencies: - "@sinonjs/formatio" "^3.2.1" + "@sinonjs/commons" "^1.7.0" + "@sinonjs/fake-timers" "^6.0.0" "@sinonjs/text-encoding" "^0.7.1" just-extend "^4.0.2" - lolex "^5.0.1" path-to-regexp "^1.7.0" node-dir@^0.1.10: @@ -8932,11 +8934,6 @@ safe-regex@^1.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -samsam@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.3.0.tgz#8d1d9350e25622da30de3e44ba692b5221ab7c50" - integrity sha512-1HwIYD/8UlOtFS3QO3w7ey+SdSDFE4HRNLZoZRYVQefrOY3l17epswImeB1ijgJFQJodIaHcwkp3r/myBjFVbg== - sanitize-filename@^1.6.2, sanitize-filename@^1.6.3: version "1.6.3" resolved "https://registry.yarnpkg.com/sanitize-filename/-/sanitize-filename-1.6.3.tgz#755ebd752045931977e30b2025d340d7c9090378" @@ -9187,18 +9184,18 @@ single-line-log@^1.1.2: dependencies: string-width "^1.0.1" -sinon@4.4.2: - version "4.4.2" - resolved "https://registry.yarnpkg.com/sinon/-/sinon-4.4.2.tgz#c4c41d4bd346e1d33594daec2d5df0548334fc65" - integrity sha512-cpOHpnRyY3Dk9dTHBYMfVBB0HUCSKIpxW07X6OGW2NiYPovs4AkcL8Q8MzecbAROjbfRA9esJCmlZgikxDz7DA== - dependencies: - "@sinonjs/formatio" "^2.0.0" - diff "^3.1.0" - lodash.get "^4.4.2" - lolex "^2.2.0" - nise "^1.2.0" - supports-color "^5.1.0" - type-detect "^4.0.5" +sinon@9.0.2: + version "9.0.2" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-9.0.2.tgz#b9017e24633f4b1c98dfb6e784a5f0509f5fd85d" + integrity sha512-0uF8Q/QHkizNUmbK3LRFqx5cpTttEVXudywY9Uwzy8bTfZUhljZ7ARzSxnRHWYWtVTeh4Cw+tTb3iU21FQVO9A== + dependencies: + "@sinonjs/commons" "^1.7.2" + "@sinonjs/fake-timers" "^6.0.1" + "@sinonjs/formatio" "^5.0.1" + "@sinonjs/samsam" "^5.0.3" + diff "^4.0.2" + nise "^4.0.1" + supports-color "^7.1.0" slash@^1.0.0: version "1.0.0" @@ -10103,6 +10100,11 @@ ts-loader@4.1.0: micromatch "^3.1.4" semver "^5.0.1" +ts-mock-imports@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/ts-mock-imports/-/ts-mock-imports-1.3.0.tgz#ed9b743349f3c27346afe5b7454ffd2bcaa2302d" + integrity sha512-cCrVcRYsp84eDvPict0ZZD/D7ppQ0/JSx4ve6aEU8DjlsaWRJWV6ADMovp2sCuh6pZcduLFoIYhKTDU2LARo7Q== + tslib@^1.8.0, tslib@^1.8.1: version "1.11.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35" @@ -10184,7 +10186,7 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" -type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.5, type-detect@^4.0.8: +type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== From 2e02e7d67b2f507489567d97176e5fda8e90a6bc Mon Sep 17 00:00:00 2001 From: Mikunj Date: Tue, 2 Jun 2020 12:15:20 +1000 Subject: [PATCH 5/8] Linting --- .../session/crypto/MessageEncrypter_test.ts | 28 +++++++++++++------ ts/test/utils/stubs/SessionCipherBasicStub.ts | 4 ++- ts/test/utils/testUtils.ts | 5 +++- ts/window/index.ts | 3 +- ts/window/types/SecretSessionCipher.ts | 4 +-- ts/window/types/libsignal-protocol.ts | 11 ++++++-- 6 files changed, 37 insertions(+), 18 deletions(-) diff --git a/ts/test/session/crypto/MessageEncrypter_test.ts b/ts/test/session/crypto/MessageEncrypter_test.ts index e89e4de0b..2a62aa5e1 100644 --- a/ts/test/session/crypto/MessageEncrypter_test.ts +++ b/ts/test/session/crypto/MessageEncrypter_test.ts @@ -25,10 +25,12 @@ describe('MessageEncrypter', () => { }, }); - TestUtils.mockData('getItemById', undefined).resolves({ - id: 'number_id', - value: 'abc.1', - }); + TestUtils.mockData('getItemById', undefined) + .withArgs('number_id') + .resolves({ + id: 'number_id', + value: 'abc.1', + }); }); afterEach(() => { @@ -40,8 +42,14 @@ describe('MessageEncrypter', () => { describe('MediumGroup', () => { it('should throw an error', async () => { const data = crypto.randomBytes(10); - const promise = MessageEncrypter.encrypt('1', data, EncryptionType.MediumGroup); - await expect(promise).to.be.rejectedWith('Encryption is not yet supported'); + const promise = MessageEncrypter.encrypt( + '1', + data, + EncryptionType.MediumGroup + ); + await expect(promise).to.be.rejectedWith( + 'Encryption is not yet supported' + ); }); }); @@ -62,11 +70,13 @@ describe('MessageEncrypter', () => { body: 'body', }); await MessageEncrypter.encrypt('1', data, EncryptionType.Signal); - expect(stub.called).to.equal(true, 'SessionCipher.encrypt should be called.'); + expect(stub.called).to.equal( + true, + 'SessionCipher.encrypt should be called.' + ); }); - it('should pass the padded message body to encrypt', async () => { - }); + it('should pass the padded message body to encrypt', async () => {}); }); }); }); diff --git a/ts/test/utils/stubs/SessionCipherBasicStub.ts b/ts/test/utils/stubs/SessionCipherBasicStub.ts index 47a22d905..20586cc29 100644 --- a/ts/test/utils/stubs/SessionCipherBasicStub.ts +++ b/ts/test/utils/stubs/SessionCipherBasicStub.ts @@ -8,7 +8,9 @@ export class SessionCipherBasicStub { this.address = address; } - public async encrypt(buffer: ArrayBuffer | Uint8Array): Promise { + public async encrypt( + buffer: ArrayBuffer | Uint8Array + ): Promise { throw new Error('Should stub this out'); } } diff --git a/ts/test/utils/testUtils.ts b/ts/test/utils/testUtils.ts index e344462e8..daa08e7ee 100644 --- a/ts/test/utils/testUtils.ts +++ b/ts/test/utils/testUtils.ts @@ -13,6 +13,9 @@ type DataFunction = typeof Shape; * * Note: This uses `ImportMock` so you will have to call `ImportMock.restore()` or `stub.restore()` after each test. */ -export function mockData(fn: keyof DataFunction, returns?: any): sinon.SinonStub { +export function mockData( + fn: keyof DataFunction, + returns?: any +): sinon.SinonStub { return ImportMock.mockFunction(Data, fn, returns); } diff --git a/ts/window/index.ts b/ts/window/index.ts index 44ee56e77..5cfa408b9 100644 --- a/ts/window/index.ts +++ b/ts/window/index.ts @@ -1,4 +1,3 @@ - import { LibsignalProtocol } from './types/libsignal-protocol'; import { SignalInterface } from './types/signal'; import { LocalizerType } from '../types/Util'; @@ -77,7 +76,7 @@ interface WindowInterface extends Window { // In the case for tests // tslint:disable-next-line: no-typeof-undefined -if (typeof(window) === 'undefined') { +if (typeof window === 'undefined') { const globalAny: any = global; globalAny.window = {}; } diff --git a/ts/window/types/SecretSessionCipher.ts b/ts/window/types/SecretSessionCipher.ts index 2349aafb2..7967e2744 100644 --- a/ts/window/types/SecretSessionCipher.ts +++ b/ts/window/types/SecretSessionCipher.ts @@ -1,7 +1,5 @@ import { SignalService } from '../../protobuf'; -import { - CipherTextObject, -} from './libsignal-protocol'; +import { CipherTextObject } from './libsignal-protocol'; export declare class SecretSessionCipher { constructor(storage: any); diff --git a/ts/window/types/libsignal-protocol.ts b/ts/window/types/libsignal-protocol.ts index ffc03db0e..52f46f06e 100644 --- a/ts/window/types/libsignal-protocol.ts +++ b/ts/window/types/libsignal-protocol.ts @@ -10,6 +10,7 @@ export type CipherTextObject = { export declare class SignalProtocolAddress { constructor(hexEncodedPublicKey: string, deviceId: number); + // tslint:disable-next-line: function-name public static fromString(encodedAddress: string): SignalProtocolAddress; public getName(): string; public getDeviceId(): number; @@ -26,7 +27,11 @@ interface CurveSync { generateKeyPair(): KeyPair; createKeyPair(privKey: ArrayBuffer): KeyPair; calculateAgreement(pubKey: ArrayBuffer, privKey: ArrayBuffer): ArrayBuffer; - verifySignature(pubKey: ArrayBuffer, msg: ArrayBuffer, sig: ArrayBuffer): void; + verifySignature( + pubKey: ArrayBuffer, + msg: ArrayBuffer, + sig: ArrayBuffer + ): void; calculateSignature(privKey: ArrayBuffer, message: ArrayBuffer): ArrayBuffer; validatePubKeyFormat(pubKey: ArrayBuffer): ArrayBuffer; } @@ -103,7 +108,9 @@ export declare class SessionCipher { public decryptPreKeyWhisperMessage( buffer: ArrayBuffer | Uint8Array ): Promise; - public decryptWhisperMessage(buffer: ArrayBuffer | Uint8Array): Promise; + public decryptWhisperMessage( + buffer: ArrayBuffer | Uint8Array + ): Promise; public getRecord(encodedNumber: string): Promise; public getRemoteRegistrationId(): Promise; public hasOpenSession(): Promise; From 4704893649c9fd30782fd287f9a47490a49aba9d Mon Sep 17 00:00:00 2001 From: Mikunj Date: Tue, 2 Jun 2020 13:10:23 +1000 Subject: [PATCH 6/8] Refactor --- ts/test/session/crypto/MessageEncrypter_test.ts | 17 ++++++----------- ts/test/session/utils/JobQueue_test.ts | 2 +- ts/test/test-utils/index.ts | 5 +++++ .../stubs/SessionCipherBasicStub.ts | 0 ts/test/{utils => test-utils}/stubs/index.ts | 0 ts/test/{utils => test-utils}/testUtils.ts | 0 ts/test/{utils => test-utils}/timeout.ts | 0 ts/test/utils/index.ts | 3 --- 8 files changed, 12 insertions(+), 15 deletions(-) create mode 100644 ts/test/test-utils/index.ts rename ts/test/{utils => test-utils}/stubs/SessionCipherBasicStub.ts (100%) rename ts/test/{utils => test-utils}/stubs/index.ts (100%) rename ts/test/{utils => test-utils}/testUtils.ts (100%) rename ts/test/{utils => test-utils}/timeout.ts (100%) delete mode 100644 ts/test/utils/index.ts diff --git a/ts/test/session/crypto/MessageEncrypter_test.ts b/ts/test/session/crypto/MessageEncrypter_test.ts index 2a62aa5e1..dad0400dd 100644 --- a/ts/test/session/crypto/MessageEncrypter_test.ts +++ b/ts/test/session/crypto/MessageEncrypter_test.ts @@ -5,18 +5,18 @@ import * as sinon from 'sinon'; import * as window from '../../../window'; import { MessageEncrypter } from '../../../session/crypto'; import { EncryptionType } from '../../../session/types/EncryptionType'; -import * as stubs from '../../utils/stubs'; -import { TestUtils } from '../../utils'; +import { Stubs } from '../../test-utils'; +import { UserUtil } from '../../../util'; describe('MessageEncrypter', () => { const sandbox = sinon.createSandbox(); - let sessionCipherStub: MockManager; + let sessionCipherStub: MockManager; beforeEach(() => { - sessionCipherStub = ImportMock.mockClass(stubs, 'SessionCipherBasicStub'); + sessionCipherStub = ImportMock.mockClass(Stubs, 'SessionCipherBasicStub'); ImportMock.mockOther(window, 'libsignal', { SignalProtocolAddress: sandbox.stub(), - SessionCipher: stubs.SessionCipherBasicStub, + SessionCipher: Stubs.SessionCipherBasicStub, } as any); ImportMock.mockOther(window, 'textsecure', { @@ -25,12 +25,7 @@ describe('MessageEncrypter', () => { }, }); - TestUtils.mockData('getItemById', undefined) - .withArgs('number_id') - .resolves({ - id: 'number_id', - value: 'abc.1', - }); + ImportMock.mockFunction(UserUtil, 'getCurrentDevicePubKey', '1'); }); afterEach(() => { diff --git a/ts/test/session/utils/JobQueue_test.ts b/ts/test/session/utils/JobQueue_test.ts index 47b8a5413..88b31323c 100644 --- a/ts/test/session/utils/JobQueue_test.ts +++ b/ts/test/session/utils/JobQueue_test.ts @@ -1,7 +1,7 @@ import chai from 'chai'; import { v4 as uuid } from 'uuid'; import { JobQueue } from '../../../session/utils/JobQueue'; -import { timeout } from '../../utils/timeout'; +import { timeout } from '../../test-utils'; // tslint:disable-next-line: no-require-imports no-var-requires const chaiAsPromised = require('chai-as-promised'); diff --git a/ts/test/test-utils/index.ts b/ts/test/test-utils/index.ts new file mode 100644 index 000000000..fae05690f --- /dev/null +++ b/ts/test/test-utils/index.ts @@ -0,0 +1,5 @@ +import * as TestUtils from './testUtils'; +import * as Stubs from './stubs'; +export * from './timeout'; + +export { TestUtils, Stubs }; diff --git a/ts/test/utils/stubs/SessionCipherBasicStub.ts b/ts/test/test-utils/stubs/SessionCipherBasicStub.ts similarity index 100% rename from ts/test/utils/stubs/SessionCipherBasicStub.ts rename to ts/test/test-utils/stubs/SessionCipherBasicStub.ts diff --git a/ts/test/utils/stubs/index.ts b/ts/test/test-utils/stubs/index.ts similarity index 100% rename from ts/test/utils/stubs/index.ts rename to ts/test/test-utils/stubs/index.ts diff --git a/ts/test/utils/testUtils.ts b/ts/test/test-utils/testUtils.ts similarity index 100% rename from ts/test/utils/testUtils.ts rename to ts/test/test-utils/testUtils.ts diff --git a/ts/test/utils/timeout.ts b/ts/test/test-utils/timeout.ts similarity index 100% rename from ts/test/utils/timeout.ts rename to ts/test/test-utils/timeout.ts diff --git a/ts/test/utils/index.ts b/ts/test/utils/index.ts deleted file mode 100644 index 80d6561ec..000000000 --- a/ts/test/utils/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as TestUtils from './testUtils'; - -export { TestUtils }; From 21e2469b751a09bf1565d1d85d3c9e035d3841a9 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Tue, 2 Jun 2020 14:05:16 +1000 Subject: [PATCH 7/8] Add more tests --- ts/session/crypto/MessageEncrypter.ts | 2 +- .../session/crypto/MessageEncrypter_test.ts | 116 +++++++++++++++--- .../stubs/SessionCipherBasicStub.ts | 16 --- .../ciphers/FallBackSessionCipherStub.ts | 11 ++ .../stubs/ciphers/SecretSessionCipherStub.ts | 26 ++++ .../stubs/ciphers/SessionCipherStub.ts | 20 +++ ts/test/test-utils/stubs/ciphers/index.ts | 3 + ts/test/test-utils/stubs/index.ts | 2 +- ts/window/types/libsignal-protocol.ts | 2 +- 9 files changed, 165 insertions(+), 33 deletions(-) delete mode 100644 ts/test/test-utils/stubs/SessionCipherBasicStub.ts create mode 100644 ts/test/test-utils/stubs/ciphers/FallBackSessionCipherStub.ts create mode 100644 ts/test/test-utils/stubs/ciphers/SecretSessionCipherStub.ts create mode 100644 ts/test/test-utils/stubs/ciphers/SessionCipherStub.ts create mode 100644 ts/test/test-utils/stubs/ciphers/index.ts diff --git a/ts/session/crypto/MessageEncrypter.ts b/ts/session/crypto/MessageEncrypter.ts index 89e5bd35a..8efe6512b 100644 --- a/ts/session/crypto/MessageEncrypter.ts +++ b/ts/session/crypto/MessageEncrypter.ts @@ -25,7 +25,7 @@ function getPaddedMessageLength(originalLength: number): number { return messagePartCount * 160; } -export type Base64String = String; +export type Base64String = string; /** * Encrypt `plainTextBuffer` with given `encryptionType` for `device`. diff --git a/ts/test/session/crypto/MessageEncrypter_test.ts b/ts/test/session/crypto/MessageEncrypter_test.ts index dad0400dd..fb1956e8e 100644 --- a/ts/test/session/crypto/MessageEncrypter_test.ts +++ b/ts/test/session/crypto/MessageEncrypter_test.ts @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { ImportMock, MockManager } from 'ts-mock-imports'; +import { ImportMock } from 'ts-mock-imports'; import * as crypto from 'crypto'; import * as sinon from 'sinon'; import * as window from '../../../window'; @@ -7,16 +7,16 @@ import { MessageEncrypter } from '../../../session/crypto'; import { EncryptionType } from '../../../session/types/EncryptionType'; import { Stubs } from '../../test-utils'; import { UserUtil } from '../../../util'; +import { SignalService } from '../../../protobuf'; describe('MessageEncrypter', () => { const sandbox = sinon.createSandbox(); + const ourNumber = 'ourNumber'; - let sessionCipherStub: MockManager; beforeEach(() => { - sessionCipherStub = ImportMock.mockClass(Stubs, 'SessionCipherBasicStub'); ImportMock.mockOther(window, 'libsignal', { SignalProtocolAddress: sandbox.stub(), - SessionCipher: Stubs.SessionCipherBasicStub, + SessionCipher: Stubs.SessionCipherStub, } as any); ImportMock.mockOther(window, 'textsecure', { @@ -25,7 +25,19 @@ describe('MessageEncrypter', () => { }, }); - ImportMock.mockFunction(UserUtil, 'getCurrentDevicePubKey', '1'); + ImportMock.mockOther(window, 'Signal', { + Metadata: { + SecretSessionCipher: Stubs.SecretSessionCipherStub, + }, + }); + + ImportMock.mockOther(window, 'libloki', { + crypto: { + FallBackSessionCipher: Stubs.FallBackSessionCipherStub, + }, + }); + + ImportMock.mockFunction(UserUtil, 'getCurrentDevicePubKey', ourNumber); }); afterEach(() => { @@ -48,30 +60,106 @@ describe('MessageEncrypter', () => { }); }); - /* describe('SessionReset', () => { - it('should call FallbackSessionCipher', async () => { + it('should call FallbackSessionCipher encrypt', async () => { + const data = crypto.randomBytes(10); + const spy = sandbox.spy( + Stubs.FallBackSessionCipherStub.prototype, + 'encrypt' + ); + await MessageEncrypter.encrypt('1', data, EncryptionType.SessionReset); + expect(spy.called).to.equal( + true, + 'FallbackSessionCipher.encrypt should be called.' + ); }); it('should pass the padded message body to encrypt', async () => { + const data = crypto.randomBytes(10); + const spy = sandbox.spy( + Stubs.FallBackSessionCipherStub.prototype, + 'encrypt' + ); + await MessageEncrypter.encrypt('1', data, EncryptionType.SessionReset); + + const paddedData = MessageEncrypter.padPlainTextBuffer(data); + const firstArgument = new Uint8Array(spy.args[0][0]); + expect(firstArgument).to.deep.equal(paddedData); + }); + + it('should return an UNIDENTIFIED SENDER envelope type', async () => { + const data = crypto.randomBytes(10); + const result = await MessageEncrypter.encrypt( + '1', + data, + EncryptionType.SessionReset + ); + expect(result.envelopeType).to.deep.equal( + SignalService.Envelope.Type.UNIDENTIFIED_SENDER + ); }); }); - */ + describe('Signal', () => { it('should call SessionCipher encrypt', async () => { const data = crypto.randomBytes(10); - const stub = sessionCipherStub.mock('encrypt').resolves({ - type: 1, - body: 'body', - }); + const spy = sandbox.spy(Stubs.SessionCipherStub.prototype, 'encrypt'); await MessageEncrypter.encrypt('1', data, EncryptionType.Signal); - expect(stub.called).to.equal( + expect(spy.called).to.equal( true, 'SessionCipher.encrypt should be called.' ); }); - it('should pass the padded message body to encrypt', async () => {}); + 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); + + const paddedData = MessageEncrypter.padPlainTextBuffer(data); + const firstArgument = new Uint8Array(spy.args[0][0]); + expect(firstArgument).to.deep.equal(paddedData); + }); + + it('should return an UNIDENTIFIED SENDER envelope type', async () => { + const data = crypto.randomBytes(10); + const result = await MessageEncrypter.encrypt( + '1', + data, + EncryptionType.Signal + ); + expect(result.envelopeType).to.deep.equal( + SignalService.Envelope.Type.UNIDENTIFIED_SENDER + ); + }); + }); + }); + + describe('Sealed Sender', () => { + it('should pass the correct values to SecretSessionCipher encrypt', async () => { + const types = [EncryptionType.SessionReset, EncryptionType.Signal]; + for (const type of types) { + const spy = sandbox.spy( + Stubs.SecretSessionCipherStub.prototype, + 'encrypt' + ); + await MessageEncrypter.encrypt('user', crypto.randomBytes(10), type); + + const args = spy.args[0]; + const [device, certificate] = args; + + const expectedCertificate = SignalService.SenderCertificate.create({ + sender: ourNumber, + senderDevice: 1, + }); + + expect(device).to.equal('user'); + expect(certificate.toJSON()).to.deep.equal( + expectedCertificate.toJSON() + ); + + spy.restore(); + } }); }); }); diff --git a/ts/test/test-utils/stubs/SessionCipherBasicStub.ts b/ts/test/test-utils/stubs/SessionCipherBasicStub.ts deleted file mode 100644 index 20586cc29..000000000 --- a/ts/test/test-utils/stubs/SessionCipherBasicStub.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { CipherTextObject } from '../../../window/types/libsignal-protocol'; - -export class SessionCipherBasicStub { - public storage: any; - public address: any; - constructor(storage: any, address: any) { - this.storage = storage; - this.address = address; - } - - public async encrypt( - buffer: ArrayBuffer | Uint8Array - ): Promise { - throw new Error('Should stub this out'); - } -} diff --git a/ts/test/test-utils/stubs/ciphers/FallBackSessionCipherStub.ts b/ts/test/test-utils/stubs/ciphers/FallBackSessionCipherStub.ts new file mode 100644 index 000000000..ea30f41ad --- /dev/null +++ b/ts/test/test-utils/stubs/ciphers/FallBackSessionCipherStub.ts @@ -0,0 +1,11 @@ +import { CipherTextObject } from '../../../../window/types/libsignal-protocol'; +import { SignalService } from '../../../../protobuf'; + +export class FallBackSessionCipherStub { + public async encrypt(buffer: ArrayBuffer): Promise { + return { + type: SignalService.Envelope.Type.FRIEND_REQUEST, + body: Buffer.from(buffer).toString('binary'), + }; + } +} diff --git a/ts/test/test-utils/stubs/ciphers/SecretSessionCipherStub.ts b/ts/test/test-utils/stubs/ciphers/SecretSessionCipherStub.ts new file mode 100644 index 000000000..826ab7308 --- /dev/null +++ b/ts/test/test-utils/stubs/ciphers/SecretSessionCipherStub.ts @@ -0,0 +1,26 @@ +import { SignalService } from '../../../../protobuf'; +import { CipherTextObject } from '../../../../window/types/libsignal-protocol'; + +export class SecretSessionCipherStub { + public async encrypt( + _destinationPubkey: string, + _senderCertificate: SignalService.SenderCertificate, + innerEncryptedMessage: CipherTextObject + ): Promise { + const { body } = innerEncryptedMessage; + + return Buffer.from(body, 'binary').buffer; + } + + public async decrypt( + _cipherText: ArrayBuffer, + _me: { number: string; deviceId: number } + ): Promise<{ + isMe?: boolean; + sender: string; + content: ArrayBuffer; + type: SignalService.Envelope.Type; + }> { + throw new Error('Not implemented'); + } +} diff --git a/ts/test/test-utils/stubs/ciphers/SessionCipherStub.ts b/ts/test/test-utils/stubs/ciphers/SessionCipherStub.ts new file mode 100644 index 000000000..3d08bebfe --- /dev/null +++ b/ts/test/test-utils/stubs/ciphers/SessionCipherStub.ts @@ -0,0 +1,20 @@ +import { CipherTextObject } from '../../../../window/types/libsignal-protocol'; +import { SignalService } from '../../../../protobuf'; + +export class SessionCipherStub { + public storage: any; + public address: any; + constructor(storage: any, address: any) { + this.storage = storage; + this.address = address; + } + + public async encrypt( + buffer: ArrayBuffer | Uint8Array + ): Promise { + return { + type: SignalService.Envelope.Type.CIPHERTEXT, + body: Buffer.from(buffer).toString('binary'), + }; + } +} diff --git a/ts/test/test-utils/stubs/ciphers/index.ts b/ts/test/test-utils/stubs/ciphers/index.ts new file mode 100644 index 000000000..290785930 --- /dev/null +++ b/ts/test/test-utils/stubs/ciphers/index.ts @@ -0,0 +1,3 @@ +export * from './SessionCipherStub'; +export * from './SecretSessionCipherStub'; +export * from './FallBackSessionCipherStub'; diff --git a/ts/test/test-utils/stubs/index.ts b/ts/test/test-utils/stubs/index.ts index 8ec99023e..10ad19f0e 100644 --- a/ts/test/test-utils/stubs/index.ts +++ b/ts/test/test-utils/stubs/index.ts @@ -1 +1 @@ -export * from './SessionCipherBasicStub'; +export * from './ciphers'; diff --git a/ts/window/types/libsignal-protocol.ts b/ts/window/types/libsignal-protocol.ts index 52f46f06e..9b0e18146 100644 --- a/ts/window/types/libsignal-protocol.ts +++ b/ts/window/types/libsignal-protocol.ts @@ -1,6 +1,6 @@ import { SignalService } from '../../protobuf'; -export type BinaryString = String; +export type BinaryString = string; export type CipherTextObject = { type: SignalService.Envelope.Type; From 863c6da772bb9135a1916d701d9e9c273937842d Mon Sep 17 00:00:00 2001 From: Mikunj Date: Tue, 2 Jun 2020 16:11:40 +1000 Subject: [PATCH 8/8] Add util to wrap window stubs --- .../session/crypto/MessageEncrypter_test.ts | 16 ++++---- ts/test/test-utils/testUtils.ts | 39 ++++++++++++++----- 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/ts/test/session/crypto/MessageEncrypter_test.ts b/ts/test/session/crypto/MessageEncrypter_test.ts index fb1956e8e..5d6b4155d 100644 --- a/ts/test/session/crypto/MessageEncrypter_test.ts +++ b/ts/test/session/crypto/MessageEncrypter_test.ts @@ -1,11 +1,9 @@ import { expect } from 'chai'; -import { ImportMock } from 'ts-mock-imports'; import * as crypto from 'crypto'; import * as sinon from 'sinon'; -import * as window from '../../../window'; import { MessageEncrypter } from '../../../session/crypto'; import { EncryptionType } from '../../../session/types/EncryptionType'; -import { Stubs } from '../../test-utils'; +import { Stubs, TestUtils } from '../../test-utils'; import { UserUtil } from '../../../util'; import { SignalService } from '../../../protobuf'; @@ -14,35 +12,35 @@ describe('MessageEncrypter', () => { const ourNumber = 'ourNumber'; beforeEach(() => { - ImportMock.mockOther(window, 'libsignal', { + TestUtils.stubWindow('libsignal', { SignalProtocolAddress: sandbox.stub(), SessionCipher: Stubs.SessionCipherStub, } as any); - ImportMock.mockOther(window, 'textsecure', { + TestUtils.stubWindow('textsecure', { storage: { protocol: sandbox.stub(), }, }); - ImportMock.mockOther(window, 'Signal', { + TestUtils.stubWindow('Signal', { Metadata: { SecretSessionCipher: Stubs.SecretSessionCipherStub, }, }); - ImportMock.mockOther(window, 'libloki', { + TestUtils.stubWindow('libloki', { crypto: { FallBackSessionCipher: Stubs.FallBackSessionCipherStub, }, }); - ImportMock.mockFunction(UserUtil, 'getCurrentDevicePubKey', ourNumber); + sandbox.stub(UserUtil, 'getCurrentDevicePubKey').resolves(ourNumber); }); afterEach(() => { sandbox.restore(); - ImportMock.restore(); + TestUtils.restoreStubs(); }); describe('EncryptionType', () => { diff --git a/ts/test/test-utils/testUtils.ts b/ts/test/test-utils/testUtils.ts index daa08e7ee..317186827 100644 --- a/ts/test/test-utils/testUtils.ts +++ b/ts/test/test-utils/testUtils.ts @@ -1,21 +1,42 @@ import * as sinon from 'sinon'; import { ImportMock } from 'ts-mock-imports'; -import * as Shape from '../../../js/modules/data'; +import * as DataShape from '../../../js/modules/data'; +import * as window from '../../window'; + +const sandbox = sinon.createSandbox(); // We have to do this in a weird way because Data uses module.exports // which doesn't play well with sinon or ImportMock // tslint:disable-next-line: no-require-imports no-var-requires const Data = require('../../../js/modules/data'); -type DataFunction = typeof Shape; +type DataFunction = typeof DataShape; /** - * Mock a function inside Data. + * Stub a function inside Data. * - * Note: This uses `ImportMock` so you will have to call `ImportMock.restore()` or `stub.restore()` after each test. + * Note: This uses a custom sandbox. + * Please call `restoreStubs()` or `stub.restore()` to restore original functionality. */ -export function mockData( - fn: keyof DataFunction, - returns?: any -): sinon.SinonStub { - return ImportMock.mockFunction(Data, fn, returns); +export function stubData(fn: keyof DataFunction): sinon.SinonStub { + return sandbox.stub(Data, fn); +} + +type WindowFunction = typeof window; + +/** + * Stub a window object. + * + * Note: This uses a custom sandbox. + * Please call `restoreStubs()` or `stub.restore()` to restore original functionality. + */ +export function stubWindow( + fn: K, + replaceWith?: Partial +) { + return ImportMock.mockOther(window, fn, replaceWith); +} + +export function restoreStubs() { + ImportMock.restore(); + sandbox.restore(); }