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/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/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/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 22ce64ccc..8efe6512b 100644 --- a/ts/session/crypto/MessageEncrypter.ts +++ b/ts/session/crypto/MessageEncrypter.ts @@ -1,7 +1,10 @@ import { EncryptionType } from '../types/EncryptionType'; import { SignalService } from '../../protobuf'; +import { libloki, libsignal, Signal, textsecure } from '../../window'; +import { CipherTextObject } from '../../window/types/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 ); @@ -22,19 +25,75 @@ 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 innerCipherText: CipherTextObject; + if (encryptionType === EncryptionType.SessionReset) { + const cipher = new libloki.crypto.FallBackSessionCipher(address); + innerCipherText = await cipher.encrypt(plainText.buffer); + } else { + const cipher = new libsignal.SessionCipher( + textsecure.storage.protocol, + address + ); + innerCipherText = await cipher.encrypt(plainText.buffer); + } + + return encryptUsingSealedSender(device, innerCipherText); +} + +async function encryptUsingSealedSender( + device: string, + innerCipherText: CipherTextObject +): Promise<{ + envelopeType: SignalService.Envelope.Type; + cipherText: Base64String; +}> { + 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.CIPHERTEXT, - cipherText: new Uint8Array(), + envelopeType: SignalService.Envelope.Type.UNIDENTIFIED_SENDER, + cipherText: Buffer.from(cipherTextBuffer).toString('base64'), }; } diff --git a/ts/test/session/crypto/MessageEncrypter_test.ts b/ts/test/session/crypto/MessageEncrypter_test.ts new file mode 100644 index 000000000..5d6b4155d --- /dev/null +++ b/ts/test/session/crypto/MessageEncrypter_test.ts @@ -0,0 +1,163 @@ +import { expect } from 'chai'; +import * as crypto from 'crypto'; +import * as sinon from 'sinon'; +import { MessageEncrypter } from '../../../session/crypto'; +import { EncryptionType } from '../../../session/types/EncryptionType'; +import { Stubs, TestUtils } from '../../test-utils'; +import { UserUtil } from '../../../util'; +import { SignalService } from '../../../protobuf'; + +describe('MessageEncrypter', () => { + const sandbox = sinon.createSandbox(); + const ourNumber = 'ourNumber'; + + beforeEach(() => { + TestUtils.stubWindow('libsignal', { + SignalProtocolAddress: sandbox.stub(), + SessionCipher: Stubs.SessionCipherStub, + } as any); + + TestUtils.stubWindow('textsecure', { + storage: { + protocol: sandbox.stub(), + }, + }); + + TestUtils.stubWindow('Signal', { + Metadata: { + SecretSessionCipher: Stubs.SecretSessionCipherStub, + }, + }); + + TestUtils.stubWindow('libloki', { + crypto: { + FallBackSessionCipher: Stubs.FallBackSessionCipherStub, + }, + }); + + sandbox.stub(UserUtil, 'getCurrentDevicePubKey').resolves(ourNumber); + }); + + afterEach(() => { + sandbox.restore(); + TestUtils.restoreStubs(); + }); + + 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 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 spy = sandbox.spy(Stubs.SessionCipherStub.prototype, 'encrypt'); + await MessageEncrypter.encrypt('1', data, EncryptionType.Signal); + expect(spy.called).to.equal( + true, + 'SessionCipher.encrypt should be called.' + ); + }); + + 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/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/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 new file mode 100644 index 000000000..10ad19f0e --- /dev/null +++ b/ts/test/test-utils/stubs/index.ts @@ -0,0 +1 @@ +export * from './ciphers'; diff --git a/ts/test/test-utils/testUtils.ts b/ts/test/test-utils/testUtils.ts new file mode 100644 index 000000000..317186827 --- /dev/null +++ b/ts/test/test-utils/testUtils.ts @@ -0,0 +1,42 @@ +import * as sinon from 'sinon'; +import { ImportMock } from 'ts-mock-imports'; +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 DataShape; + +/** + * Stub a function inside Data. + * + * Note: This uses a custom sandbox. + * Please call `restoreStubs()` or `stub.restore()` to restore original functionality. + */ +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(); +} 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/stubs/SignalAddressProtocolStub.ts b/ts/test/utils/stubs/SignalAddressProtocolStub.ts new file mode 100644 index 000000000..1493e7578 --- /dev/null +++ b/ts/test/utils/stubs/SignalAddressProtocolStub.ts @@ -0,0 +1,27 @@ +import { SignalProtocolAddress } from '../../../window/types/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/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..cec8716ec --- /dev/null +++ b/ts/util/user.ts @@ -0,0 +1,17 @@ +import { getItemById } from '../../js/modules/data'; +import { KeyPair } from '../window/types/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/index.ts similarity index 81% rename from ts/window.ts rename to ts/window/index.ts index aeb19ec5d..5cfa408b9 100644 --- a/ts/window.ts +++ b/ts/window/index.ts @@ -1,6 +1,8 @@ -import { LocalizerType } from './types/Util'; +import { LibsignalProtocol } from './types/libsignal-protocol'; +import { SignalInterface } from './types/signal'; +import { LocalizerType } from '../types/Util'; -interface Window { +interface WindowInterface extends Window { seedNodeList: any; WebAPI: any; @@ -32,11 +34,11 @@ interface Window { shortenPubkey: any; dcodeIO: any; - libsignal: any; + libsignal: LibsignalProtocol; libloki: any; displayNameRegex: any; - Signal: any; + Signal: SignalInterface; Whisper: any; ConversationController: any; @@ -72,7 +74,16 @@ interface Window { resetDatabase: any; } -declare const window: Window; +// 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? // Utilities export const WebAPI = window.WebAPI; @@ -118,3 +129,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; diff --git a/ts/window/types/SecretSessionCipher.ts b/ts/window/types/SecretSessionCipher.ts new file mode 100644 index 000000000..7967e2744 --- /dev/null +++ b/ts/window/types/SecretSessionCipher.ts @@ -0,0 +1,20 @@ +import { SignalService } from '../../protobuf'; +import { CipherTextObject } from './libsignal-protocol'; + +export declare class SecretSessionCipher { + constructor(storage: any); + public encrypt( + destinationPubkey: string, + senderCertificate: SignalService.SenderCertificate, + innerEncryptedMessage: CipherTextObject + ): Promise; + public decrypt( + cipherText: ArrayBuffer, + me: { number: string; deviceId: number } + ): Promise<{ + isMe?: boolean; + sender: string; + content: ArrayBuffer; + type: SignalService.Envelope.Type; + }>; +} diff --git a/ts/window/types/libsignal-protocol.ts b/ts/window/types/libsignal-protocol.ts new file mode 100644 index 000000000..9b0e18146 --- /dev/null +++ b/ts/window/types/libsignal-protocol.ts @@ -0,0 +1,127 @@ +import { SignalService } from '../../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); + // tslint:disable-next-line: function-name + public static fromString(encodedAddress: string): SignalProtocolAddress; + public getName(): string; + public getDeviceId(): number; + public toString(): string; + public equals(other: SignalProtocolAddress): boolean; +} + +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 + ): void; + 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. + */ + public encrypt(buffer: ArrayBuffer | Uint8Array): Promise; + public decryptPreKeyWhisperMessage( + buffer: ArrayBuffer | Uint8Array + ): 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 { + SignalProtocolAddress: typeof SignalProtocolAddress; + Curve: CurveInterface; + crypto: CryptoInterface; + KeyHelper: KeyHelperInterface; + SessionCipher: typeof SessionCipher; +} diff --git a/ts/window/types/signal.ts b/ts/window/types/signal.ts new file mode 100644 index 000000000..e1ed96102 --- /dev/null +++ b/ts/window/types/signal.ts @@ -0,0 +1,9 @@ +import { SecretSessionCipher } from './SecretSessionCipher'; + +interface Metadata { + SecretSessionCipher: typeof SecretSessionCipher; +} + +export interface SignalInterface { + Metadata: Metadata; +} 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==