diff --git a/app/attachments.ts b/app/attachments.ts index 87ed87845..4b9f9c5f2 100644 --- a/app/attachments.ts +++ b/app/attachments.ts @@ -2,19 +2,17 @@ // SPDX-License-Identifier: AGPL-3.0-only import { randomBytes } from 'crypto'; -import { basename, extname, join, normalize, relative } from 'path'; +import { basename, join, normalize, relative } from 'path'; import { app, dialog, shell, remote } from 'electron'; import fastGlob from 'fast-glob'; import glob from 'glob'; import pify from 'pify'; import fse from 'fs-extra'; -import { map, isArrayBuffer, isString } from 'lodash'; +import { map, isTypedArray, isString } from 'lodash'; import normalizePath from 'normalize-path'; -import sanitizeFilename from 'sanitize-filename'; import getGuid from 'uuid/v4'; -import { typedArrayToArrayBuffer } from '../ts/Crypto'; import { isPathInside } from '../ts/util/isPathInside'; import { isWindows } from '../ts/OS'; import { writeWindowsZoneIdentifier } from '../ts/util/windowsZoneIdentifier'; @@ -123,12 +121,12 @@ export const clearTempPath = (userDataPath: string): Promise => { export const createReader = ( root: string -): ((relativePath: string) => Promise) => { +): ((relativePath: string) => Promise) => { if (!isString(root)) { throw new TypeError("'root' must be a path"); } - return async (relativePath: string): Promise => { + return async (relativePath: string): Promise => { if (!isString(relativePath)) { throw new TypeError("'relativePath' must be a string"); } @@ -138,8 +136,7 @@ export const createReader = ( if (!isPathInside(normalized, root)) { throw new Error('Invalid relative path'); } - const buffer = await fse.readFile(normalized); - return typedArrayToArrayBuffer(buffer); + return fse.readFile(normalized); }; }; @@ -203,48 +200,9 @@ export const copyIntoAttachmentsDirectory = ( }; }; -export const writeToDownloads = async ({ - data, - name, -}: { - data: ArrayBuffer; - name: string; -}): Promise<{ fullPath: string; name: string }> => { - const appToUse = getApp(); - const downloadsPath = - appToUse.getPath('downloads') || appToUse.getPath('home'); - const sanitized = sanitizeFilename(name); - - const extension = extname(sanitized); - const fileBasename = basename(sanitized, extension); - const getCandidateName = (count: number) => - `${fileBasename} (${count})${extension}`; - - const existingFiles = await fse.readdir(downloadsPath); - let candidateName = sanitized; - let count = 0; - while (existingFiles.includes(candidateName)) { - count += 1; - candidateName = getCandidateName(count); - } - - const target = join(downloadsPath, candidateName); - const normalized = normalize(target); - if (!isPathInside(normalized, downloadsPath)) { - throw new Error('Invalid filename!'); - } - - await writeWithAttributes(normalized, data); - - return { - fullPath: normalized, - name: candidateName, - }; -}; - async function writeWithAttributes( target: string, - data: ArrayBuffer + data: Uint8Array ): Promise { await fse.writeFile(target, Buffer.from(data)); @@ -292,7 +250,7 @@ export const saveAttachmentToDisk = async ({ data, name, }: { - data: ArrayBuffer; + data: Uint8Array; name: string; }): Promise => { const dialogToUse = dialog || remote.dialog; @@ -327,20 +285,20 @@ export const openFileInFolder = async (target: string): Promise => { export const createWriterForNew = ( root: string -): ((arrayBuffer: ArrayBuffer) => Promise) => { +): ((bytes: Uint8Array) => Promise) => { if (!isString(root)) { throw new TypeError("'root' must be a path"); } - return async (arrayBuffer: ArrayBuffer) => { - if (!isArrayBuffer(arrayBuffer)) { - throw new TypeError("'arrayBuffer' must be an array buffer"); + return async (bytes: Uint8Array) => { + if (!isTypedArray(bytes)) { + throw new TypeError("'bytes' must be a typed array"); } const name = createName(); const relativePath = getRelativePath(name); return createWriterForExisting(root)({ - data: arrayBuffer, + data: bytes, path: relativePath, }); }; @@ -348,27 +306,27 @@ export const createWriterForNew = ( export const createWriterForExisting = ( root: string -): ((options: { data: ArrayBuffer; path: string }) => Promise) => { +): ((options: { data: Uint8Array; path: string }) => Promise) => { if (!isString(root)) { throw new TypeError("'root' must be a path"); } return async ({ - data: arrayBuffer, + data: bytes, path: relativePath, }: { - data: ArrayBuffer; + data: Uint8Array; path: string; }): Promise => { if (!isString(relativePath)) { throw new TypeError("'relativePath' must be a path"); } - if (!isArrayBuffer(arrayBuffer)) { + if (!isTypedArray(bytes)) { throw new TypeError("'arrayBuffer' must be an array buffer"); } - const buffer = Buffer.from(arrayBuffer); + const buffer = Buffer.from(bytes); const absolutePath = join(root, relativePath); const normalized = normalize(absolutePath); if (!isPathInside(normalized, root)) { diff --git a/js/modules/signal.js b/js/modules/signal.js index f496bcd2d..e56210109 100644 --- a/js/modules/signal.js +++ b/js/modules/signal.js @@ -130,13 +130,8 @@ const searchSelectors = require('../../ts/state/selectors/search'); // Types const AttachmentType = require('../../ts/types/Attachment'); -const VisualAttachment = require('./types/visual_attachment'); -const EmbeddedContact = require('../../ts/types/EmbeddedContact'); -const Conversation = require('./types/conversation'); -const Errors = require('../../ts/types/errors'); +const VisualAttachment = require('../../ts/types/VisualAttachment'); const MessageType = require('./types/message'); -const MIME = require('../../ts/types/MIME'); -const SettingsType = require('../../ts/types/Settings'); const { UUID } = require('../../ts/types/UUID'); const { Address } = require('../../ts/types/Address'); const { QualifiedAddress } = require('../../ts/types/QualifiedAddress'); @@ -417,14 +412,9 @@ exports.setup = (options = {}) => { }; const Types = { - Attachment: AttachmentType, - EmbeddedContact, - Conversation, - Errors, Message: MessageType, - MIME, - Settings: SettingsType, - VisualAttachment, + + // Mostly for debugging UUID, Address, QualifiedAddress, diff --git a/js/modules/types/conversation.js b/js/modules/types/conversation.js deleted file mode 100644 index b222167e9..000000000 --- a/js/modules/types/conversation.js +++ /dev/null @@ -1,167 +0,0 @@ -// Copyright 2018-2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -/* global window */ - -const { isFunction, isNumber } = require('lodash'); -const { - arrayBufferToBase64, - base64ToArrayBuffer, - computeHash, -} = require('../../../ts/Crypto'); - -function buildAvatarUpdater({ field }) { - return async (conversation, data, options = {}) => { - if (!conversation) { - return conversation; - } - - const avatar = conversation[field]; - const { - deleteAttachmentData, - doesAttachmentExist, - writeNewAttachmentData, - } = options; - if (!isFunction(deleteAttachmentData)) { - throw new Error( - 'Conversation.buildAvatarUpdater: deleteAttachmentData must be a function' - ); - } - if (!isFunction(doesAttachmentExist)) { - throw new Error( - 'Conversation.buildAvatarUpdater: deleteAttachmentData must be a function' - ); - } - if (!isFunction(writeNewAttachmentData)) { - throw new Error( - 'Conversation.buildAvatarUpdater: writeNewAttachmentData must be a function' - ); - } - - const newHash = await computeHash(data); - - if (!avatar || !avatar.hash) { - return { - ...conversation, - [field]: { - hash: newHash, - path: await writeNewAttachmentData(data), - }, - }; - } - - const { hash, path } = avatar; - const exists = await doesAttachmentExist(path); - if (!exists) { - window.SignalWindow.log.warn( - `Conversation.buildAvatarUpdater: attachment ${path} did not exist` - ); - } - - if (exists && hash === newHash) { - return conversation; - } - - await deleteAttachmentData(path); - - return { - ...conversation, - [field]: { - hash: newHash, - path: await writeNewAttachmentData(data), - }, - }; - }; -} - -const maybeUpdateAvatar = buildAvatarUpdater({ field: 'avatar' }); -const maybeUpdateProfileAvatar = buildAvatarUpdater({ - field: 'profileAvatar', -}); - -async function upgradeToVersion2(conversation, options) { - if (conversation.version >= 2) { - return conversation; - } - - const { writeNewAttachmentData } = options; - if (!isFunction(writeNewAttachmentData)) { - throw new Error( - 'Conversation.upgradeToVersion2: writeNewAttachmentData must be a function' - ); - } - - let { avatar, profileAvatar, profileKey } = conversation; - - if (avatar && avatar.data) { - avatar = { - hash: await computeHash(avatar.data), - path: await writeNewAttachmentData(avatar.data), - }; - } - - if (profileAvatar && profileAvatar.data) { - profileAvatar = { - hash: await computeHash(profileAvatar.data), - path: await writeNewAttachmentData(profileAvatar.data), - }; - } - - if (profileKey && profileKey.byteLength) { - profileKey = arrayBufferToBase64(profileKey); - } - - return { - ...conversation, - version: 2, - avatar, - profileAvatar, - profileKey, - }; -} - -async function migrateConversation(conversation, options = {}) { - if (!conversation) { - return conversation; - } - if (!isNumber(conversation.version)) { - // eslint-disable-next-line no-param-reassign - conversation.version = 1; - } - - return upgradeToVersion2(conversation, options); -} - -async function deleteExternalFiles(conversation, options = {}) { - if (!conversation) { - return; - } - - const { deleteAttachmentData } = options; - if (!isFunction(deleteAttachmentData)) { - throw new Error( - 'Conversation.buildAvatarUpdater: deleteAttachmentData must be a function' - ); - } - - const { avatar, profileAvatar } = conversation; - - if (avatar && avatar.path) { - await deleteAttachmentData(avatar.path); - } - - if (profileAvatar && profileAvatar.path) { - await deleteAttachmentData(profileAvatar.path); - } -} - -module.exports = { - arrayBufferToBase64, - base64ToArrayBuffer, - computeHash, - - deleteExternalFiles, - maybeUpdateAvatar, - maybeUpdateProfileAvatar, - migrateConversation, -}; diff --git a/js/modules/types/message.js b/js/modules/types/message.js index 334baf43b..cfeece093 100644 --- a/js/modules/types/message.js +++ b/js/modules/types/message.js @@ -6,7 +6,7 @@ const { isFunction, isObject, isString, omit } = require('lodash'); const Contact = require('../../../ts/types/EmbeddedContact'); const Attachment = require('../../../ts/types/Attachment'); const Errors = require('../../../ts/types/errors'); -const SchemaVersion = require('./schema_version'); +const SchemaVersion = require('../../../ts/types/SchemaVersion'); const { initializeAttachmentMetadata, } = require('../../../ts/types/message/initializeAttachmentMetadata'); diff --git a/js/modules/types/schema_version.js b/js/modules/types/schema_version.js deleted file mode 100644 index ad3277cb3..000000000 --- a/js/modules/types/schema_version.js +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright 2018-2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -const { isNumber } = require('lodash'); - -exports.isValid = value => isNumber(value) && value >= 0; diff --git a/libtextsecure/test/.eslintrc.js b/libtextsecure/test/.eslintrc.js index 6b71e3f3e..de9f8080f 100644 --- a/libtextsecure/test/.eslintrc.js +++ b/libtextsecure/test/.eslintrc.js @@ -16,9 +16,6 @@ module.exports = { }, globals: { assert: true, - assertEqualArrayBuffers: true, getString: true, - hexToArrayBuffer: true, - stringToArrayBuffer: true, }, }; diff --git a/libtextsecure/test/_test.js b/libtextsecure/test/_test.js index 9b5b5a0db..b58f2f6cb 100644 --- a/libtextsecure/test/_test.js +++ b/libtextsecure/test/_test.js @@ -46,18 +46,6 @@ mocha.reporter(SauceReporter); /* * global helpers for tests */ -window.assertEqualArrayBuffers = (ab1, ab2) => { - assert.deepEqual(new Uint8Array(ab1), new Uint8Array(ab2)); -}; - -window.hexToArrayBuffer = str => { - const ret = new ArrayBuffer(str.length / 2); - const array = new Uint8Array(ret); - for (let i = 0; i < str.length / 2; i += 1) { - array[i] = parseInt(str.substr(i * 2, 2), 16); - } - return ret; -}; window.Whisper = window.Whisper || {}; window.Whisper.events = { diff --git a/libtextsecure/test/helpers_test.js b/libtextsecure/test/helpers_test.js deleted file mode 100644 index ec678d99c..000000000 --- a/libtextsecure/test/helpers_test.js +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2015-2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -describe('Helpers', () => { - describe('ArrayBuffer->String conversion', () => { - it('works', () => { - const b = new ArrayBuffer(3); - const a = new Uint8Array(b); - a[0] = 0; - a[1] = 255; - a[2] = 128; - assert.equal(window.textsecure.utils.getString(b), '\x00\xff\x80'); - }); - }); - - describe('stringToArrayBuffer', () => { - it('returns ArrayBuffer when passed string', () => { - const anArrayBuffer = new ArrayBuffer(1); - const typedArray = new Uint8Array(anArrayBuffer); - typedArray[0] = 'a'.charCodeAt(0); - assertEqualArrayBuffers( - window.textsecure.utils.stringToArrayBuffer('a'), - anArrayBuffer - ); - }); - it('throws an error when passed a non string', () => { - const notStringable = [{}, undefined, null, new ArrayBuffer()]; - notStringable.forEach(notString => { - assert.throw(() => { - window.textsecure.utils.stringToArrayBuffer(notString); - }, Error); - }); - }); - }); -}); diff --git a/libtextsecure/test/index.html b/libtextsecure/test/index.html index 4e4decc36..e4b831ce4 100644 --- a/libtextsecure/test/index.html +++ b/libtextsecure/test/index.html @@ -31,7 +31,6 @@ data-cover > - diff --git a/preload.js b/preload.js index d980fa000..0c0ba70b2 100644 --- a/preload.js +++ b/preload.js @@ -367,7 +367,6 @@ try { window.Backbone = require('backbone'); window.textsecure = require('./ts/textsecure').default; - window.synchronousCrypto = require('./ts/util/synchronousCrypto'); window.WebAPI = window.textsecure.WebAPI.initialize({ url: config.serverUrl, diff --git a/sticker-creator/preload.js b/sticker-creator/preload.js index 908cab5bc..1da684db7 100644 --- a/sticker-creator/preload.js +++ b/sticker-creator/preload.js @@ -9,7 +9,6 @@ const { readFile } = require('fs'); const config = require('url').parse(window.location.toString(), true).query; const { noop, uniqBy } = require('lodash'); const pMap = require('p-map'); -const client = require('@signalapp/signal-client'); // It is important to call this as early as possible require('../ts/windows/context'); @@ -55,20 +54,6 @@ const Signal = require('../js/modules/signal'); window.Signal = Signal.setup({}); window.textsecure = require('../ts/textsecure').default; -window.libsignal = window.libsignal || {}; -window.libsignal.HKDF = {}; -window.libsignal.HKDF.deriveSecrets = (input, salt, info) => { - const hkdf = client.HKDF.new(3); - const output = hkdf.deriveSecrets( - 3 * 32, - Buffer.from(input), - Buffer.from(info), - Buffer.from(salt) - ); - return [output.slice(0, 32), output.slice(32, 64), output.slice(64, 96)]; -}; -window.synchronousCrypto = require('../ts/util/synchronousCrypto'); - const { initialize: initializeWebAPI } = require('../ts/textsecure/WebAPI'); const { getAnimatedPngDataIfExists, @@ -206,7 +191,7 @@ window.encryptAndUpload = async ( const { value: password } = passwordItem; const packKey = window.Signal.Crypto.getRandomBytes(32); - const encryptionKey = await deriveStickerPackKey(packKey); + const encryptionKey = deriveStickerPackKey(packKey); const iv = window.Signal.Crypto.getRandomBytes(16); const server = WebAPI.connect({ @@ -265,9 +250,7 @@ window.encryptAndUpload = async ( async function encrypt(data, key, iv) { const { ciphertext } = await window.textsecure.crypto.encryptAttachment( - data instanceof ArrayBuffer - ? data - : window.Signal.Crypto.typedArrayToArrayBuffer(data), + data, key, iv ); diff --git a/test/.eslintrc.js b/test/.eslintrc.js index c22636780..b2a0af3c9 100644 --- a/test/.eslintrc.js +++ b/test/.eslintrc.js @@ -11,10 +11,7 @@ module.exports = { globals: { assert: true, - assertEqualArrayBuffers: true, getString: true, - hexToArrayBuffer: true, - stringToArrayBuffer: true, }, parserOptions: { diff --git a/test/_test.js b/test/_test.js index a12497f98..20c8bc55e 100644 --- a/test/_test.js +++ b/test/_test.js @@ -51,18 +51,6 @@ Whisper.Database.id = 'test'; /* * global helpers for tests */ -window.assertEqualArrayBuffers = (ab1, ab2) => { - assert.deepEqual(new Uint8Array(ab1), new Uint8Array(ab2)); -}; - -window.hexToArrayBuffer = str => { - const ret = new ArrayBuffer(str.length / 2); - const array = new Uint8Array(ret); - for (let i = 0; i < str.length / 2; i += 1) { - array[i] = parseInt(str.substr(i * 2, 2), 16); - } - return ret; -}; function deleteIndexedDB() { return new Promise((resolve, reject) => { diff --git a/test/app/attachments_test.js b/test/app/attachments_test.js index 9e99af1c9..1e454e9d1 100644 --- a/test/app/attachments_test.js +++ b/test/app/attachments_test.js @@ -9,7 +9,7 @@ const { assert } = require('chai'); const { app } = require('electron'); const Attachments = require('../../app/attachments'); -const { stringToArrayBuffer } = require('../../ts/util/stringToArrayBuffer'); +const Bytes = require('../../ts/Bytes'); const PREFIX_LENGTH = 2; const NUM_SEPARATORS = 1; @@ -28,7 +28,7 @@ describe('Attachments', () => { }); it('should write file to disk and return path', async () => { - const input = stringToArrayBuffer('test string'); + const input = Bytes.fromString('test string'); const tempDirectory = path.join( tempRootDirectory, 'Attachments_createWriterForNew' @@ -57,7 +57,7 @@ describe('Attachments', () => { }); it('should write file to disk on given path and return path', async () => { - const input = stringToArrayBuffer('test string'); + const input = Bytes.fromString('test string'); const tempDirectory = path.join( tempRootDirectory, 'Attachments_createWriterForExisting' @@ -82,7 +82,7 @@ describe('Attachments', () => { }); it('throws if relative path goes higher than root', async () => { - const input = stringToArrayBuffer('test string'); + const input = Bytes.fromString('test string'); const tempDirectory = path.join( tempRootDirectory, 'Attachments_createWriterForExisting' @@ -124,7 +124,7 @@ describe('Attachments', () => { Attachments.createName() ); const fullPath = path.join(tempDirectory, relativePath); - const input = stringToArrayBuffer('test string'); + const input = Bytes.fromString('test string'); const inputBuffer = Buffer.from(input); await fse.ensureFile(fullPath); @@ -260,7 +260,7 @@ describe('Attachments', () => { Attachments.createName() ); const fullPath = path.join(tempDirectory, relativePath); - const input = stringToArrayBuffer('test string'); + const input = Bytes.fromString('test string'); const inputBuffer = Buffer.from(input); await fse.ensureFile(fullPath); diff --git a/test/modules/types/message_test.js b/test/modules/types/message_test.js index 36418c1ce..4c7709c52 100644 --- a/test/modules/types/message_test.js +++ b/test/modules/types/message_test.js @@ -6,7 +6,7 @@ const sinon = require('sinon'); const Message = require('../../../js/modules/types/message'); const { SignalService } = require('../../../ts/protobuf'); -const { stringToArrayBuffer } = require('../../../ts/util/stringToArrayBuffer'); +const Bytes = require('../../../ts/Bytes'); describe('Message', () => { const logger = { @@ -60,7 +60,7 @@ describe('Message', () => { attachments: [ { path: 'ab/abcdefghi', - data: stringToArrayBuffer('It’s easy if you try'), + data: Bytes.fromString('It’s easy if you try'), }, ], }; @@ -78,9 +78,9 @@ describe('Message', () => { const writeExistingAttachmentData = attachment => { assert.equal(attachment.path, 'ab/abcdefghi'); - assert.deepEqual( - attachment.data, - stringToArrayBuffer('It’s easy if you try') + assert.strictEqual( + Bytes.toString(attachment.data), + 'It’s easy if you try' ); }; @@ -101,7 +101,7 @@ describe('Message', () => { { thumbnail: { path: 'ab/abcdefghi', - data: stringToArrayBuffer('It’s easy if you try'), + data: Bytes.fromString('It’s easy if you try'), }, }, ], @@ -126,9 +126,9 @@ describe('Message', () => { const writeExistingAttachmentData = attachment => { assert.equal(attachment.path, 'ab/abcdefghi'); - assert.deepEqual( - attachment.data, - stringToArrayBuffer('It’s easy if you try') + assert.strictEqual( + Bytes.toString(attachment.data), + 'It’s easy if you try' ); }; @@ -151,7 +151,7 @@ describe('Message', () => { isProfile: false, avatar: { path: 'ab/abcdefghi', - data: stringToArrayBuffer('It’s easy if you try'), + data: Bytes.fromString('It’s easy if you try'), }, }, }, @@ -177,9 +177,9 @@ describe('Message', () => { const writeExistingAttachmentData = attachment => { assert.equal(attachment.path, 'ab/abcdefghi'); - assert.deepEqual( - attachment.data, - stringToArrayBuffer('It’s easy if you try') + assert.strictEqual( + Bytes.toString(attachment.data), + 'It’s easy if you try' ); }; @@ -268,7 +268,7 @@ describe('Message', () => { { contentType: 'audio/aac', flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE, - data: stringToArrayBuffer('It’s easy if you try'), + data: Bytes.fromString('It’s easy if you try'), fileName: 'test\u202Dfig.exe', size: 1111, }, @@ -292,12 +292,13 @@ describe('Message', () => { contact: [], }; - const expectedAttachmentData = stringToArrayBuffer( - 'It’s easy if you try' - ); + const expectedAttachmentData = 'It’s easy if you try'; const context = { writeNewAttachmentData: async attachmentData => { - assert.deepEqual(attachmentData, expectedAttachmentData); + assert.strictEqual( + Bytes.toString(attachmentData), + expectedAttachmentData + ); return 'abc/abcdefg'; }, getRegionCode: () => 'US', diff --git a/test/modules/types/schema_version_test.js b/test/modules/types/schema_version_test.js deleted file mode 100644 index 7e21f4b8f..000000000 --- a/test/modules/types/schema_version_test.js +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2018-2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -require('mocha-testcheck').install(); -const { assert } = require('chai'); - -const SchemaVersion = require('../../../js/modules/types/schema_version'); - -describe('SchemaVersion', () => { - describe('isValid', () => { - check.it('should return true for positive integers', gen.posInt, input => { - assert.isTrue(SchemaVersion.isValid(input)); - }); - - check.it( - 'should return false for any other value', - gen.primitive.suchThat(value => typeof value !== 'number' || value < 0), - input => { - assert.isFalse(SchemaVersion.isValid(input)); - } - ); - }); -}); diff --git a/ts/ConversationController.ts b/ts/ConversationController.ts index a6887d01d..fa3329214 100644 --- a/ts/ConversationController.ts +++ b/ts/ConversationController.ts @@ -245,7 +245,7 @@ export class ConversationController { try { if (isGroupV1(conversation.attributes)) { - await maybeDeriveGroupV2Id(conversation); + maybeDeriveGroupV2Id(conversation); } await saveConversation(conversation.attributes); } catch (error) { @@ -577,8 +577,7 @@ export class ConversationController { let groupV2Id: undefined | string; if (isGroupV1(conversation.attributes)) { - // eslint-disable-next-line no-await-in-loop - await maybeDeriveGroupV2Id(conversation); + maybeDeriveGroupV2Id(conversation); groupV2Id = conversation.get('derivedGroupV2Id'); assert( groupV2Id, @@ -836,7 +835,7 @@ export class ConversationController { // Hydrate contactCollection, now that initial fetch is complete conversation.fetchContacts(); - const isChanged = await maybeDeriveGroupV2Id(conversation); + const isChanged = maybeDeriveGroupV2Id(conversation); if (isChanged) { updateConversation(conversation.attributes); } diff --git a/ts/Crypto.ts b/ts/Crypto.ts index d3328d684..0aaaa7110 100644 --- a/ts/Crypto.ts +++ b/ts/Crypto.ts @@ -7,17 +7,29 @@ import { chunk } from 'lodash'; import Long from 'long'; import { HKDF } from '@signalapp/signal-client'; +import * as Bytes from './Bytes'; import { calculateAgreement, generateKeyPair } from './Curve'; import * as log from './logging/log'; +import { HashType, CipherType } from './types/Crypto'; +import { ProfileDecryptError } from './types/errors'; -import { - CipherType, - encrypt, - decrypt, - HashType, - hash, - sign, -} from './util/synchronousCrypto'; +export { HashType, CipherType }; + +const PROFILE_IV_LENGTH = 12; // bytes +const PROFILE_KEY_LENGTH = 32; // bytes + +// bytes +export const PaddedLengths = { + Name: [53, 257], + About: [128, 254, 512], + AboutEmoji: [32], + PaymentAddress: [554], +}; + +export type EncryptedAttachment = { + ciphertext: Uint8Array; + digest: Uint8Array; +}; // Generate a number between zero and 16383 export function generateRegistrationId(): number { @@ -27,72 +39,20 @@ export function generateRegistrationId(): number { return id & 0x3fff; } -export function typedArrayToArrayBuffer(typedArray: Uint8Array): ArrayBuffer { - const ab = new ArrayBuffer(typedArray.length); - // Create a new Uint8Array backed by the ArrayBuffer and copy all values from - // the `typedArray` into it by calling `.set()` method. Note that raw - // ArrayBuffer doesn't offer this API, because it is supposed to be used with - // concrete data view (i.e. Uint8Array, Float64Array, and so on.) - new Uint8Array(ab).set(typedArray, 0); - return ab; -} - -export function arrayBufferToBase64(arrayBuffer: ArrayBuffer): string { - // NOTE: We can't use `Bytes.toBase64` here because this runs in both - // node and electron contexts. - return Buffer.from(arrayBuffer).toString('base64'); -} - -export function arrayBufferToHex(arrayBuffer: ArrayBuffer): string { - return Buffer.from(arrayBuffer).toString('hex'); -} - -export function base64ToArrayBuffer(base64string: string): ArrayBuffer { - return typedArrayToArrayBuffer(Buffer.from(base64string, 'base64')); -} - -export function hexToArrayBuffer(hexString: string): ArrayBuffer { - return typedArrayToArrayBuffer(Buffer.from(hexString, 'hex')); -} - -export function fromEncodedBinaryToArrayBuffer(key: string): ArrayBuffer { - return typedArrayToArrayBuffer(Buffer.from(key, 'binary')); -} - -export function arrayBufferToEncodedBinary(arrayBuffer: ArrayBuffer): string { - return Buffer.from(arrayBuffer).toString('binary'); -} - -export function bytesFromString(string: string): ArrayBuffer { - return typedArrayToArrayBuffer(Buffer.from(string)); -} -export function stringFromBytes(buffer: ArrayBuffer): string { - return Buffer.from(buffer).toString(); -} -export function hexFromBytes(buffer: ArrayBuffer): string { - return Buffer.from(buffer).toString('hex'); -} - -export function bytesFromHexString(string: string): ArrayBuffer { - return typedArrayToArrayBuffer(Buffer.from(string, 'hex')); -} - -export async function deriveStickerPackKey( - packKey: ArrayBuffer -): Promise { +export function deriveStickerPackKey(packKey: Uint8Array): Uint8Array { const salt = getZeroes(32); - const info = bytesFromString('Sticker Pack'); + const info = Bytes.fromString('Sticker Pack'); - const [part1, part2] = await deriveSecrets(packKey, salt, info); + const [part1, part2] = deriveSecrets(packKey, salt, info); - return concatenateBytes(part1, part2); + return Bytes.concatenate([part1, part2]); } export function deriveSecrets( - input: ArrayBuffer, - salt: ArrayBuffer, - info: ArrayBuffer -): [ArrayBuffer, ArrayBuffer, ArrayBuffer] { + input: Uint8Array, + salt: Uint8Array, + info: Uint8Array +): [Uint8Array, Uint8Array, Uint8Array] { const hkdf = HKDF.new(3); const output = hkdf.deriveSecrets( 3 * 32, @@ -100,56 +60,49 @@ export function deriveSecrets( Buffer.from(info), Buffer.from(salt) ); - return [ - typedArrayToArrayBuffer(output.slice(0, 32)), - typedArrayToArrayBuffer(output.slice(32, 64)), - typedArrayToArrayBuffer(output.slice(64, 96)), - ]; + return [output.slice(0, 32), output.slice(32, 64), output.slice(64, 96)]; } -export async function deriveMasterKeyFromGroupV1( - groupV1Id: ArrayBuffer -): Promise { +export function deriveMasterKeyFromGroupV1(groupV1Id: Uint8Array): Uint8Array { const salt = getZeroes(32); - const info = bytesFromString('GV2 Migration'); + const info = Bytes.fromString('GV2 Migration'); - const [part1] = await deriveSecrets(groupV1Id, salt, info); + const [part1] = deriveSecrets(groupV1Id, salt, info); return part1; } -export async function computeHash(data: ArrayBuffer): Promise { - const digest = await crypto.subtle.digest({ name: 'SHA-512' }, data); - return arrayBufferToBase64(digest); +export function computeHash(data: Uint8Array): string { + return Bytes.toBase64(hash(HashType.size512, data)); } // High-level Operations export type EncryptedDeviceName = { - ephemeralPublic: ArrayBuffer; - syntheticIv: ArrayBuffer; - ciphertext: ArrayBuffer; + ephemeralPublic: Uint8Array; + syntheticIv: Uint8Array; + ciphertext: Uint8Array; }; -export async function encryptDeviceName( +export function encryptDeviceName( deviceName: string, - identityPublic: ArrayBuffer -): Promise { - const plaintext = bytesFromString(deviceName); + identityPublic: Uint8Array +): EncryptedDeviceName { + const plaintext = Bytes.fromString(deviceName); const ephemeralKeyPair = generateKeyPair(); const masterSecret = calculateAgreement( identityPublic, ephemeralKeyPair.privKey ); - const key1 = await hmacSha256(masterSecret, bytesFromString('auth')); - const syntheticIv = getFirstBytes(await hmacSha256(key1, plaintext), 16); + const key1 = hmacSha256(masterSecret, Bytes.fromString('auth')); + const syntheticIv = getFirstBytes(hmacSha256(key1, plaintext), 16); - const key2 = await hmacSha256(masterSecret, bytesFromString('cipher')); - const cipherKey = await hmacSha256(key2, syntheticIv); + const key2 = hmacSha256(masterSecret, Bytes.fromString('cipher')); + const cipherKey = hmacSha256(key2, syntheticIv); const counter = getZeroes(16); - const ciphertext = await encryptAesCtr(cipherKey, plaintext, counter); + const ciphertext = encryptAesCtr(cipherKey, plaintext, counter); return { ephemeralPublic: ephemeralKeyPair.pubKey, @@ -158,124 +111,61 @@ export async function encryptDeviceName( }; } -export async function decryptDeviceName( +export function decryptDeviceName( { ephemeralPublic, syntheticIv, ciphertext }: EncryptedDeviceName, - identityPrivate: ArrayBuffer -): Promise { + identityPrivate: Uint8Array +): string { const masterSecret = calculateAgreement(ephemeralPublic, identityPrivate); - const key2 = await hmacSha256(masterSecret, bytesFromString('cipher')); - const cipherKey = await hmacSha256(key2, syntheticIv); + const key2 = hmacSha256(masterSecret, Bytes.fromString('cipher')); + const cipherKey = hmacSha256(key2, syntheticIv); const counter = getZeroes(16); - const plaintext = await decryptAesCtr(cipherKey, ciphertext, counter); + const plaintext = decryptAesCtr(cipherKey, ciphertext, counter); - const key1 = await hmacSha256(masterSecret, bytesFromString('auth')); - const ourSyntheticIv = getFirstBytes(await hmacSha256(key1, plaintext), 16); + const key1 = hmacSha256(masterSecret, Bytes.fromString('auth')); + const ourSyntheticIv = getFirstBytes(hmacSha256(key1, plaintext), 16); if (!constantTimeEqual(ourSyntheticIv, syntheticIv)) { throw new Error('decryptDeviceName: synthetic IV did not match'); } - return stringFromBytes(plaintext); + return Bytes.toString(plaintext); } -// Path structure: 'fa/facdf99c22945b1c9393345599a276f4b36ad7ccdc8c2467f5441b742c2d11fa' -export function getAttachmentLabel(path: string): ArrayBuffer { - const filename = path.slice(3); - - return base64ToArrayBuffer(filename); -} - -const PUB_KEY_LENGTH = 32; -export async function encryptAttachment( - staticPublicKey: ArrayBuffer, - path: string, - plaintext: ArrayBuffer -): Promise { - const uniqueId = getAttachmentLabel(path); - - return encryptFile(staticPublicKey, uniqueId, plaintext); -} - -export async function decryptAttachment( - staticPrivateKey: ArrayBuffer, - path: string, - data: ArrayBuffer -): Promise { - const uniqueId = getAttachmentLabel(path); - - return decryptFile(staticPrivateKey, uniqueId, data); -} - -export async function encryptFile( - staticPublicKey: ArrayBuffer, - uniqueId: ArrayBuffer, - plaintext: ArrayBuffer -): Promise { - const ephemeralKeyPair = generateKeyPair(); - const agreement = calculateAgreement( - staticPublicKey, - ephemeralKeyPair.privKey - ); - const key = await hmacSha256(agreement, uniqueId); - - const prefix = ephemeralKeyPair.pubKey.slice(1); - - return concatenateBytes(prefix, await encryptSymmetric(key, plaintext)); -} - -export async function decryptFile( - staticPrivateKey: ArrayBuffer, - uniqueId: ArrayBuffer, - data: ArrayBuffer -): Promise { - const ephemeralPublicKey = getFirstBytes(data, PUB_KEY_LENGTH); - const ciphertext = getBytes(data, PUB_KEY_LENGTH, data.byteLength); - const agreement = calculateAgreement(ephemeralPublicKey, staticPrivateKey); - - const key = await hmacSha256(agreement, uniqueId); - - return decryptSymmetric(key, ciphertext); -} - -export async function deriveStorageManifestKey( - storageServiceKey: ArrayBuffer, +export function deriveStorageManifestKey( + storageServiceKey: Uint8Array, version: number -): Promise { - return hmacSha256(storageServiceKey, bytesFromString(`Manifest_${version}`)); +): Uint8Array { + return hmacSha256(storageServiceKey, Bytes.fromString(`Manifest_${version}`)); } -export async function deriveStorageItemKey( - storageServiceKey: ArrayBuffer, +export function deriveStorageItemKey( + storageServiceKey: Uint8Array, itemID: string -): Promise { - return hmacSha256(storageServiceKey, bytesFromString(`Item_${itemID}`)); +): Uint8Array { + return hmacSha256(storageServiceKey, Bytes.fromString(`Item_${itemID}`)); } -export async function deriveAccessKey( - profileKey: ArrayBuffer -): Promise { +export function deriveAccessKey(profileKey: Uint8Array): Uint8Array { const iv = getZeroes(12); const plaintext = getZeroes(16); - const accessKey = await encryptAesGcm(profileKey, iv, plaintext); + const accessKey = encryptAesGcm(profileKey, iv, plaintext); return getFirstBytes(accessKey, 16); } -export async function getAccessKeyVerifier( - accessKey: ArrayBuffer -): Promise { +export function getAccessKeyVerifier(accessKey: Uint8Array): Uint8Array { const plaintext = getZeroes(32); return hmacSha256(accessKey, plaintext); } -export async function verifyAccessKey( - accessKey: ArrayBuffer, - theirVerifier: ArrayBuffer -): Promise { - const ourVerifier = await getAccessKeyVerifier(accessKey); +export function verifyAccessKey( + accessKey: Uint8Array, + theirVerifier: Uint8Array +): boolean { + const ourVerifier = getAccessKeyVerifier(accessKey); if (constantTimeEqual(ourVerifier, theirVerifier)) { return true; @@ -288,30 +178,26 @@ const IV_LENGTH = 16; const MAC_LENGTH = 16; const NONCE_LENGTH = 16; -export async function encryptSymmetric( - key: ArrayBuffer, - plaintext: ArrayBuffer -): Promise { +export function encryptSymmetric( + key: Uint8Array, + plaintext: Uint8Array +): Uint8Array { const iv = getZeroes(IV_LENGTH); const nonce = getRandomBytes(NONCE_LENGTH); - const cipherKey = await hmacSha256(key, nonce); - const macKey = await hmacSha256(key, cipherKey); + const cipherKey = hmacSha256(key, nonce); + const macKey = hmacSha256(key, cipherKey); - const ciphertext = await encryptAes256CbcPkcsPadding( - cipherKey, - plaintext, - iv - ); - const mac = getFirstBytes(await hmacSha256(macKey, ciphertext), MAC_LENGTH); + const ciphertext = encryptAes256CbcPkcsPadding(cipherKey, plaintext, iv); + const mac = getFirstBytes(hmacSha256(macKey, ciphertext), MAC_LENGTH); - return concatenateBytes(nonce, ciphertext, mac); + return Bytes.concatenate([nonce, ciphertext, mac]); } -export async function decryptSymmetric( - key: ArrayBuffer, - data: ArrayBuffer -): Promise { +export function decryptSymmetric( + key: Uint8Array, + data: Uint8Array +): Uint8Array { const iv = getZeroes(IV_LENGTH); const nonce = getFirstBytes(data, NONCE_LENGTH); @@ -322,13 +208,10 @@ export async function decryptSymmetric( ); const theirMac = getBytes(data, data.byteLength - MAC_LENGTH, MAC_LENGTH); - const cipherKey = await hmacSha256(key, nonce); - const macKey = await hmacSha256(key, cipherKey); + const cipherKey = hmacSha256(key, nonce); + const macKey = hmacSha256(key, cipherKey); - const ourMac = getFirstBytes( - await hmacSha256(macKey, ciphertext), - MAC_LENGTH - ); + const ourMac = getFirstBytes(hmacSha256(macKey, ciphertext), MAC_LENGTH); if (!constantTimeEqual(theirMac, ourMac)) { throw new Error( 'decryptSymmetric: Failed to decrypt; MAC verification failed' @@ -338,190 +221,122 @@ export async function decryptSymmetric( return decryptAes256CbcPkcsPadding(cipherKey, ciphertext, iv); } -export function constantTimeEqual( - left: ArrayBuffer, - right: ArrayBuffer -): boolean { - if (left.byteLength !== right.byteLength) { - return false; - } - let result = 0; - const ta1 = new Uint8Array(left); - const ta2 = new Uint8Array(right); - const max = left.byteLength; - for (let i = 0; i < max; i += 1) { - // eslint-disable-next-line no-bitwise - result |= ta1[i] ^ ta2[i]; - } - - return result === 0; -} - // Encryption -export async function hmacSha256( - key: ArrayBuffer, - plaintext: ArrayBuffer -): Promise { +export function hmacSha256(key: Uint8Array, plaintext: Uint8Array): Uint8Array { return sign(key, plaintext); } // We use part of the constantTimeEqual algorithm from below here, but we allow ourMac // to be longer than the passed-in length. This allows easy comparisons against // arbitrary MAC lengths. -export async function verifyHmacSha256( - plaintext: ArrayBuffer, - key: ArrayBuffer, - theirMac: ArrayBuffer, +export function verifyHmacSha256( + plaintext: Uint8Array, + key: Uint8Array, + theirMac: Uint8Array, length: number -): Promise { - const ourMac = await hmacSha256(key, plaintext); +): void { + const ourMac = hmacSha256(key, plaintext); if (theirMac.byteLength !== length || ourMac.byteLength < length) { throw new Error('Bad MAC length'); } - const a = new Uint8Array(theirMac); - const b = new Uint8Array(ourMac); let result = 0; for (let i = 0; i < theirMac.byteLength; i += 1) { // eslint-disable-next-line no-bitwise - result |= a[i] ^ b[i]; + result |= ourMac[i] ^ theirMac[i]; } if (result !== 0) { throw new Error('Bad MAC'); } } -export async function encryptAes256CbcPkcsPadding( - key: ArrayBuffer, - plaintext: ArrayBuffer, - iv: ArrayBuffer -): Promise { - const algorithm = { - name: 'AES-CBC', - iv, - }; - const extractable = false; - - const cryptoKey = await window.crypto.subtle.importKey( - 'raw', +export function encryptAes256CbcPkcsPadding( + key: Uint8Array, + plaintext: Uint8Array, + iv: Uint8Array +): Uint8Array { + return encrypt(CipherType.AES256CBC, { key, - algorithm, - extractable, - ['encrypt'] - ); - - return window.crypto.subtle.encrypt(algorithm, cryptoKey, plaintext); -} - -export async function decryptAes256CbcPkcsPadding( - key: ArrayBuffer, - ciphertext: ArrayBuffer, - iv: ArrayBuffer -): Promise { - const algorithm = { - name: 'AES-CBC', + plaintext, iv, - }; - const extractable = false; + }); +} - const cryptoKey = await window.crypto.subtle.importKey( - 'raw', +export function decryptAes256CbcPkcsPadding( + key: Uint8Array, + ciphertext: Uint8Array, + iv: Uint8Array +): Uint8Array { + return decrypt(CipherType.AES256CBC, { key, - algorithm, - extractable, - ['decrypt'] - ); - - return window.crypto.subtle.decrypt(algorithm, cryptoKey, ciphertext); -} - -export async function encryptAesCtr( - key: ArrayBuffer, - plaintext: ArrayBuffer, - counter: ArrayBuffer -): Promise { - return encrypt(key, plaintext, counter, CipherType.AES256CTR); -} - -export async function decryptAesCtr( - key: ArrayBuffer, - ciphertext: ArrayBuffer, - counter: ArrayBuffer -): Promise { - return decrypt(key, ciphertext, counter, CipherType.AES256CTR); -} - -export async function encryptAesGcm( - key: ArrayBuffer, - iv: ArrayBuffer, - plaintext: ArrayBuffer, - additionalData?: ArrayBuffer -): Promise { - const algorithm = { - name: 'AES-GCM', + ciphertext, iv, - ...(additionalData ? { additionalData } : {}), - }; - - const extractable = false; - - const cryptoKey = await crypto.subtle.importKey( - 'raw', - key, - algorithm, - extractable, - ['encrypt'] - ); - - return crypto.subtle.encrypt(algorithm, cryptoKey, plaintext); + }); } -export async function decryptAesGcm( - key: ArrayBuffer, - iv: ArrayBuffer, - ciphertext: ArrayBuffer, - additionalData?: ArrayBuffer -): Promise { - const algorithm = { - name: 'AES-GCM', - iv, - ...(additionalData ? { additionalData } : {}), - tagLength: 128, - }; - - const extractable = false; - const cryptoKey = await crypto.subtle.importKey( - 'raw', +export function encryptAesCtr( + key: Uint8Array, + plaintext: Uint8Array, + counter: Uint8Array +): Uint8Array { + return encrypt(CipherType.AES256CTR, { key, - algorithm, - extractable, - ['decrypt'] - ); + plaintext, + iv: counter, + }); +} - return crypto.subtle.decrypt(algorithm, cryptoKey, ciphertext); +export function decryptAesCtr( + key: Uint8Array, + ciphertext: Uint8Array, + counter: Uint8Array +): Uint8Array { + return decrypt(CipherType.AES256CTR, { + key, + ciphertext, + iv: counter, + }); +} + +export function encryptAesGcm( + key: Uint8Array, + iv: Uint8Array, + plaintext: Uint8Array, + aad?: Uint8Array +): Uint8Array { + return encrypt(CipherType.AES256GCM, { + key, + plaintext, + iv, + aad, + }); +} + +export function decryptAesGcm( + key: Uint8Array, + iv: Uint8Array, + ciphertext: Uint8Array +): Uint8Array { + return decrypt(CipherType.AES256GCM, { + key, + ciphertext, + iv, + }); } // Hashing -export function sha256(data: ArrayBuffer): ArrayBuffer { +export function sha256(data: Uint8Array): Uint8Array { return hash(HashType.size256, data); } // Utility -export function getRandomBytes(n: number): ArrayBuffer { - const bytes = new Uint8Array(n); - window.crypto.getRandomValues(bytes); - - return typedArrayToArrayBuffer(bytes); -} - export function getRandomValue(low: number, high: number): number { const diff = high - low; - const bytes = new Uint32Array(1); - window.crypto.getRandomValues(bytes); + const bytes = getRandomBytes(1); // Because high and low are inclusive const mod = diff + 1; @@ -529,15 +344,8 @@ export function getRandomValue(low: number, high: number): number { return (bytes[0] % mod) + low; } -export function getZeroes(n: number): ArrayBuffer { - const result = new Uint8Array(n); - - const value = 0; - const startIndex = 0; - const endExclusive = n; - result.fill(value, startIndex, endExclusive); - - return typedArrayToArrayBuffer(result); +export function getZeroes(n: number): Uint8Array { + return new Uint8Array(n); } export function highBitsToInt(byte: number): number { @@ -553,92 +361,19 @@ export function intsToByteHighAndLow( return ((highValue << 4) | lowValue) & 0xff; } -export function trimBytes(buffer: ArrayBuffer, length: number): ArrayBuffer { - return getFirstBytes(buffer, length); -} - -export function getViewOfArrayBuffer( - buffer: ArrayBuffer, - start: number, - finish: number -): ArrayBuffer | SharedArrayBuffer { - const source = new Uint8Array(buffer); - const result = source.slice(start, finish); - - return window.Signal.Crypto.typedArrayToArrayBuffer(result); -} - -export function concatenateBytes( - ...elements: Array -): ArrayBuffer { - const length = elements.reduce( - (total, element) => total + element.byteLength, - 0 - ); - - const result = new Uint8Array(length); - let position = 0; - - const max = elements.length; - for (let i = 0; i < max; i += 1) { - const element = new Uint8Array(elements[i]); - result.set(element, position); - position += element.byteLength; - } - if (position !== result.length) { - throw new Error('problem concatenating!'); - } - - return typedArrayToArrayBuffer(result); -} - -export function splitBytes( - buffer: ArrayBuffer, - ...lengths: Array -): Array { - const total = lengths.reduce((acc, length) => acc + length, 0); - - if (total !== buffer.byteLength) { - throw new Error( - `Requested lengths total ${total} does not match source total ${buffer.byteLength}` - ); - } - - const source = new Uint8Array(buffer); - const results = []; - let position = 0; - - const max = lengths.length; - for (let i = 0; i < max; i += 1) { - const length = lengths[i]; - const result = new Uint8Array(length); - const section = source.slice(position, position + length); - result.set(section); - position += result.byteLength; - - results.push(typedArrayToArrayBuffer(result)); - } - - return results; -} - -export function getFirstBytes(data: ArrayBuffer, n: number): ArrayBuffer { - const source = new Uint8Array(data); - - return typedArrayToArrayBuffer(source.subarray(0, n)); +export function getFirstBytes(data: Uint8Array, n: number): Uint8Array { + return data.subarray(0, n); } export function getBytes( - data: ArrayBuffer | Uint8Array, + data: Uint8Array, start: number, n: number -): ArrayBuffer { - const source = new Uint8Array(data); - - return typedArrayToArrayBuffer(source.subarray(start, start + n)); +): Uint8Array { + return data.subarray(start, start + n); } -function _getMacAndData(ciphertext: ArrayBuffer) { +function _getMacAndData(ciphertext: Uint8Array) { const dataLength = ciphertext.byteLength - MAC_LENGTH; const data = getBytes(ciphertext, 0, dataLength); const mac = getBytes(ciphertext, dataLength, MAC_LENGTH); @@ -648,7 +383,7 @@ function _getMacAndData(ciphertext: ArrayBuffer) { export async function encryptCdsDiscoveryRequest( attestations: { - [key: string]: { clientKey: ArrayBuffer; requestId: ArrayBuffer }; + [key: string]: { clientKey: Uint8Array; requestId: Uint8Array }; }, phoneNumbers: ReadonlyArray ): Promise> { @@ -661,16 +396,13 @@ export async function encryptCdsDiscoveryRequest( ); // We've written to the array, so offset === byteLength; we need to reset it. Then we'll - // have access to everything in the array when we generate an ArrayBuffer from it. - const queryDataPlaintext = concatenateBytes( - nonce, - typedArrayToArrayBuffer(numbersArray) - ); + // have access to everything in the array when we generate an Uint8Array from it. + const queryDataPlaintext = Bytes.concatenate([nonce, numbersArray]); const queryDataKey = getRandomBytes(32); const commitment = sha256(queryDataPlaintext); const iv = getRandomBytes(12); - const queryDataCiphertext = await encryptAesGcm( + const queryDataCiphertext = encryptAesGcm( queryDataKey, iv, queryDataPlaintext @@ -684,7 +416,7 @@ export async function encryptCdsDiscoveryRequest( attestations, async ({ clientKey, requestId }) => { const envelopeIv = getRandomBytes(12); - const ciphertext = await encryptAesGcm( + const ciphertext = encryptAesGcm( clientKey, envelopeIv, queryDataKey, @@ -693,61 +425,59 @@ export async function encryptCdsDiscoveryRequest( const { data, mac } = _getMacAndData(ciphertext); return { - requestId: arrayBufferToBase64(requestId), - data: arrayBufferToBase64(data), - iv: arrayBufferToBase64(envelopeIv), - mac: arrayBufferToBase64(mac), + requestId: Bytes.toBase64(requestId), + data: Bytes.toBase64(data), + iv: Bytes.toBase64(envelopeIv), + mac: Bytes.toBase64(mac), }; } ); return { addressCount: phoneNumbers.length, - commitment: arrayBufferToBase64(commitment), - data: arrayBufferToBase64(queryDataCiphertextData), - iv: arrayBufferToBase64(iv), - mac: arrayBufferToBase64(queryDataCiphertextMac), + commitment: Bytes.toBase64(commitment), + data: Bytes.toBase64(queryDataCiphertextData), + iv: Bytes.toBase64(iv), + mac: Bytes.toBase64(queryDataCiphertextMac), envelopes, }; } -export function uuidToArrayBuffer(uuid: string): ArrayBuffer { +export function uuidToBytes(uuid: string): Uint8Array { if (uuid.length !== 36) { log.warn( - 'uuidToArrayBuffer: received a string of invalid length. Returning an empty ArrayBuffer' + 'uuidToBytes: received a string of invalid length. ' + + 'Returning an empty Uint8Array' ); - return new ArrayBuffer(0); + return new Uint8Array(0); } - return typedArrayToArrayBuffer( - Uint8Array.from( - chunk(uuid.replace(/-/g, ''), 2).map(pair => parseInt(pair.join(''), 16)) - ) + return Uint8Array.from( + chunk(uuid.replace(/-/g, ''), 2).map(pair => parseInt(pair.join(''), 16)) ); } -export function arrayBufferToUuid( - arrayBuffer: ArrayBuffer -): undefined | string { - if (arrayBuffer.byteLength !== 16) { +export function bytesToUuid(bytes: Uint8Array): undefined | string { + if (bytes.byteLength !== 16) { log.warn( - 'arrayBufferToUuid: received an ArrayBuffer of invalid length. Returning undefined' + 'bytesToUuid: received an Uint8Array of invalid length. ' + + 'Returning undefined' ); return undefined; } - const uuids = splitUuids(arrayBuffer); + const uuids = splitUuids(bytes); if (uuids.length === 1) { return uuids[0] || undefined; } return undefined; } -export function splitUuids(arrayBuffer: ArrayBuffer): Array { +export function splitUuids(buffer: Uint8Array): Array { const uuids = []; - for (let i = 0; i < arrayBuffer.byteLength; i += 16) { - const bytes = getBytes(arrayBuffer, i, 16); - const hex = arrayBufferToHex(bytes); + for (let i = 0; i < buffer.byteLength; i += 16) { + const bytes = getBytes(buffer, i, 16); + const hex = Bytes.toHex(bytes); const chunks = [ hex.substring(0, 8), hex.substring(8, 12), @@ -765,14 +495,209 @@ export function splitUuids(arrayBuffer: ArrayBuffer): Array { return uuids; } -export function trimForDisplay(arrayBuffer: ArrayBuffer): ArrayBuffer { - const padded = new Uint8Array(arrayBuffer); - +export function trimForDisplay(padded: Uint8Array): Uint8Array { let paddingEnd = 0; for (paddingEnd; paddingEnd < padded.length; paddingEnd += 1) { if (padded[paddingEnd] === 0x00) { break; } } - return typedArrayToArrayBuffer(padded.slice(0, paddingEnd)); + return padded.slice(0, paddingEnd); +} + +function verifyDigest(data: Uint8Array, theirDigest: Uint8Array): void { + const ourDigest = sha256(data); + let result = 0; + for (let i = 0; i < theirDigest.byteLength; i += 1) { + // eslint-disable-next-line no-bitwise + result |= ourDigest[i] ^ theirDigest[i]; + } + if (result !== 0) { + throw new Error('Bad digest'); + } +} + +export function decryptAttachment( + encryptedBin: Uint8Array, + keys: Uint8Array, + theirDigest?: Uint8Array +): Uint8Array { + if (keys.byteLength !== 64) { + throw new Error('Got invalid length attachment keys'); + } + if (encryptedBin.byteLength < 16 + 32) { + throw new Error('Got invalid length attachment'); + } + + const aesKey = keys.slice(0, 32); + const macKey = keys.slice(32, 64); + + const iv = encryptedBin.slice(0, 16); + const ciphertext = encryptedBin.slice(16, encryptedBin.byteLength - 32); + const ivAndCiphertext = encryptedBin.slice(0, encryptedBin.byteLength - 32); + const mac = encryptedBin.slice( + encryptedBin.byteLength - 32, + encryptedBin.byteLength + ); + + verifyHmacSha256(ivAndCiphertext, macKey, mac, 32); + + if (theirDigest) { + verifyDigest(encryptedBin, theirDigest); + } + + return decryptAes256CbcPkcsPadding(aesKey, ciphertext, iv); +} + +export function encryptAttachment( + plaintext: Uint8Array, + keys: Uint8Array, + iv: Uint8Array +): EncryptedAttachment { + if (!(plaintext instanceof Uint8Array)) { + throw new TypeError( + `\`plaintext\` must be an \`Uint8Array\`; got: ${typeof plaintext}` + ); + } + + if (keys.byteLength !== 64) { + throw new Error('Got invalid length attachment keys'); + } + if (iv.byteLength !== 16) { + throw new Error('Got invalid length attachment iv'); + } + const aesKey = keys.slice(0, 32); + const macKey = keys.slice(32, 64); + + const ciphertext = encryptAes256CbcPkcsPadding(aesKey, plaintext, iv); + + const ivAndCiphertext = Bytes.concatenate([iv, ciphertext]); + + const mac = hmacSha256(macKey, ivAndCiphertext); + + const encryptedBin = Bytes.concatenate([ivAndCiphertext, mac]); + const digest = sha256(encryptedBin); + + return { + ciphertext: encryptedBin, + digest, + }; +} + +export function encryptProfile(data: Uint8Array, key: Uint8Array): Uint8Array { + const iv = getRandomBytes(PROFILE_IV_LENGTH); + if (key.byteLength !== PROFILE_KEY_LENGTH) { + throw new Error('Got invalid length profile key'); + } + if (iv.byteLength !== PROFILE_IV_LENGTH) { + throw new Error('Got invalid length profile iv'); + } + const ciphertext = encryptAesGcm(key, iv, data); + return Bytes.concatenate([iv, ciphertext]); +} + +export function decryptProfile(data: Uint8Array, key: Uint8Array): Uint8Array { + if (data.byteLength < 12 + 16 + 1) { + throw new Error(`Got too short input: ${data.byteLength}`); + } + const iv = data.slice(0, PROFILE_IV_LENGTH); + const ciphertext = data.slice(PROFILE_IV_LENGTH, data.byteLength); + if (key.byteLength !== PROFILE_KEY_LENGTH) { + throw new Error('Got invalid length profile key'); + } + if (iv.byteLength !== PROFILE_IV_LENGTH) { + throw new Error('Got invalid length profile iv'); + } + + try { + return decryptAesGcm(key, iv, ciphertext); + } catch (_) { + throw new ProfileDecryptError( + 'Failed to decrypt profile data. ' + + 'Most likely the profile key has changed.' + ); + } +} + +export function encryptProfileItemWithPadding( + item: Uint8Array, + profileKey: Uint8Array, + paddedLengths: typeof PaddedLengths[keyof typeof PaddedLengths] +): Uint8Array { + const paddedLength = paddedLengths.find( + (length: number) => item.byteLength <= length + ); + if (!paddedLength) { + throw new Error('Oversized value'); + } + const padded = new Uint8Array(paddedLength); + padded.set(new Uint8Array(item)); + return encryptProfile(padded, profileKey); +} + +export function decryptProfileName( + encryptedProfileName: string, + key: Uint8Array +): { given: Uint8Array; family: Uint8Array | null } { + const data = Bytes.fromBase64(encryptedProfileName); + const padded = decryptProfile(data, key); + + // Given name is the start of the string to the first null character + let givenEnd; + for (givenEnd = 0; givenEnd < padded.length; givenEnd += 1) { + if (padded[givenEnd] === 0x00) { + break; + } + } + + // Family name is the next chunk of non-null characters after that first null + let familyEnd; + for (familyEnd = givenEnd + 1; familyEnd < padded.length; familyEnd += 1) { + if (padded[familyEnd] === 0x00) { + break; + } + } + const foundFamilyName = familyEnd > givenEnd + 1; + + return { + given: padded.slice(0, givenEnd), + family: foundFamilyName ? padded.slice(givenEnd + 1, familyEnd) : null, + }; +} + +// +// SignalContext APIs +// + +const { crypto } = window.SignalContext; + +export function sign(key: Uint8Array, data: Uint8Array): Uint8Array { + return crypto.sign(key, data); +} + +export function hash(type: HashType, data: Uint8Array): Uint8Array { + return crypto.hash(type, data); +} + +export function encrypt( + ...args: Parameters +): Uint8Array { + return crypto.encrypt(...args); +} + +export function decrypt( + ...args: Parameters +): Uint8Array { + return crypto.decrypt(...args); +} + +export function getRandomBytes(size: number): Uint8Array { + return crypto.getRandomBytes(size); +} + +export function constantTimeEqual( + left: Uint8Array, + right: Uint8Array +): boolean { + return crypto.constantTimeEqual(left, right); } diff --git a/ts/Curve.ts b/ts/Curve.ts index a73a15abf..246d9ef3b 100644 --- a/ts/Curve.ts +++ b/ts/Curve.ts @@ -3,7 +3,8 @@ import * as client from '@signalapp/signal-client'; -import { constantTimeEqual, typedArrayToArrayBuffer } from './Crypto'; +import * as Bytes from './Bytes'; +import { constantTimeEqual } from './Crypto'; import { KeyPairType, CompatPreKeyType, @@ -26,9 +27,9 @@ export function generateSignedPreKey( } if ( - !(identityKeyPair.privKey instanceof ArrayBuffer) || + !(identityKeyPair.privKey instanceof Uint8Array) || identityKeyPair.privKey.byteLength !== 32 || - !(identityKeyPair.pubKey instanceof ArrayBuffer) || + !(identityKeyPair.pubKey instanceof Uint8Array) || identityKeyPair.pubKey.byteLength !== 33 ) { throw new TypeError( @@ -63,24 +64,13 @@ export function generateKeyPair(): KeyPairType { const pubKey = privKey.getPublicKey(); return { - privKey: typedArrayToArrayBuffer(privKey.serialize()), - pubKey: typedArrayToArrayBuffer(pubKey.serialize()), + privKey: privKey.serialize(), + pubKey: pubKey.serialize(), }; } -export function copyArrayBuffer(source: ArrayBuffer): ArrayBuffer { - const sourceArray = new Uint8Array(source); - - const target = new ArrayBuffer(source.byteLength); - const targetArray = new Uint8Array(target); - - targetArray.set(sourceArray, 0); - - return target; -} - -export function createKeyPair(incomingKey: ArrayBuffer): KeyPairType { - const copy = copyArrayBuffer(incomingKey); +export function createKeyPair(incomingKey: Uint8Array): KeyPairType { + const copy = new Uint8Array(incomingKey); clampPrivateKey(copy); if (!constantTimeEqual(copy, incomingKey)) { log.warn('createKeyPair: incoming private key was not clamped!'); @@ -96,32 +86,31 @@ export function createKeyPair(incomingKey: ArrayBuffer): KeyPairType { const pubKey = privKey.getPublicKey(); return { - privKey: typedArrayToArrayBuffer(privKey.serialize()), - pubKey: typedArrayToArrayBuffer(pubKey.serialize()), + privKey: privKey.serialize(), + pubKey: pubKey.serialize(), }; } export function calculateAgreement( - pubKey: ArrayBuffer, - privKey: ArrayBuffer -): ArrayBuffer { + pubKey: Uint8Array, + privKey: Uint8Array +): Uint8Array { const privKeyBuffer = Buffer.from(privKey); const pubKeyObj = client.PublicKey.deserialize( - Buffer.concat([ - Buffer.from([0x05]), - Buffer.from(validatePubKeyFormat(pubKey)), - ]) + Buffer.from( + Bytes.concatenate([new Uint8Array([0x05]), validatePubKeyFormat(pubKey)]) + ) ); const privKeyObj = client.PrivateKey.deserialize(privKeyBuffer); const sharedSecret = privKeyObj.agree(pubKeyObj); - return typedArrayToArrayBuffer(sharedSecret); + return sharedSecret; } export function verifySignature( - pubKey: ArrayBuffer, - message: ArrayBuffer, - signature: ArrayBuffer + pubKey: Uint8Array, + message: Uint8Array, + signature: Uint8Array ): boolean { const pubKeyBuffer = Buffer.from(pubKey); const messageBuffer = Buffer.from(message); @@ -134,22 +123,21 @@ export function verifySignature( } export function calculateSignature( - privKey: ArrayBuffer, - plaintext: ArrayBuffer -): ArrayBuffer { + privKey: Uint8Array, + plaintext: Uint8Array +): Uint8Array { const privKeyBuffer = Buffer.from(privKey); const plaintextBuffer = Buffer.from(plaintext); const privKeyObj = client.PrivateKey.deserialize(privKeyBuffer); const signature = privKeyObj.sign(plaintextBuffer); - return typedArrayToArrayBuffer(signature); + return signature; } -export function validatePubKeyFormat(pubKey: ArrayBuffer): ArrayBuffer { +function validatePubKeyFormat(pubKey: Uint8Array): Uint8Array { if ( pubKey === undefined || - ((pubKey.byteLength !== 33 || new Uint8Array(pubKey)[0] !== 5) && - pubKey.byteLength !== 32) + ((pubKey.byteLength !== 33 || pubKey[0] !== 5) && pubKey.byteLength !== 32) ) { throw new Error('Invalid public key'); } @@ -160,18 +148,16 @@ export function validatePubKeyFormat(pubKey: ArrayBuffer): ArrayBuffer { return pubKey; } -export function setPublicKeyTypeByte(publicKey: ArrayBuffer): void { - const byteArray = new Uint8Array(publicKey); - byteArray[0] = 5; +export function setPublicKeyTypeByte(publicKey: Uint8Array): void { + // eslint-disable-next-line no-param-reassign + publicKey[0] = 5; } -export function clampPrivateKey(privateKey: ArrayBuffer): void { - const byteArray = new Uint8Array(privateKey); - - // eslint-disable-next-line no-bitwise - byteArray[0] &= 248; - // eslint-disable-next-line no-bitwise - byteArray[31] &= 127; - // eslint-disable-next-line no-bitwise - byteArray[31] |= 64; +export function clampPrivateKey(privateKey: Uint8Array): void { + // eslint-disable-next-line no-bitwise, no-param-reassign + privateKey[0] &= 248; + // eslint-disable-next-line no-bitwise, no-param-reassign + privateKey[31] &= 127; + // eslint-disable-next-line no-bitwise, no-param-reassign + privateKey[31] |= 64; } diff --git a/ts/LibSignalStores.ts b/ts/LibSignalStores.ts index e21afc5b4..b7206198b 100644 --- a/ts/LibSignalStores.ts +++ b/ts/LibSignalStores.ts @@ -27,8 +27,6 @@ import { Address } from './types/Address'; import { QualifiedAddress } from './types/QualifiedAddress'; import type { UUID } from './types/UUID'; -import { typedArrayToArrayBuffer } from './Crypto'; - import { Zone } from './util/Zone'; function encodeAddress(address: ProtocolAddress): Address { @@ -148,7 +146,7 @@ export class IdentityKeys extends IdentityKeyStore { async saveIdentity(name: ProtocolAddress, key: PublicKey): Promise { const encodedAddress = encodeAddress(name); - const publicKey = typedArrayToArrayBuffer(key.serialize()); + const publicKey = key.serialize(); // Pass `zone` to let `saveIdentity` archive sibling sessions when identity // key changes. @@ -166,7 +164,7 @@ export class IdentityKeys extends IdentityKeyStore { direction: Direction ): Promise { const encodedAddress = encodeAddress(name); - const publicKey = typedArrayToArrayBuffer(key.serialize()); + const publicKey = key.serialize(); return window.textsecure.storage.protocol.isTrustedIdentity( encodedAddress, diff --git a/ts/SignalProtocolStore.ts b/ts/SignalProtocolStore.ts index cdabf1987..1fa7ea781 100644 --- a/ts/SignalProtocolStore.ts +++ b/ts/SignalProtocolStore.ts @@ -17,12 +17,8 @@ import { SignedPreKeyRecord, } from '@signalapp/signal-client'; -import { - constantTimeEqual, - fromEncodedBinaryToArrayBuffer, - typedArrayToArrayBuffer, - base64ToArrayBuffer, -} from './Crypto'; +import * as Bytes from './Bytes'; +import { constantTimeEqual } from './Crypto'; import { assert, strictAssert } from './util/assert'; import { handleMessageSend } from './util/handleMessageSend'; import { isNotNil } from './util/isNotNil'; @@ -30,7 +26,7 @@ import { Zone } from './util/Zone'; import { isMoreRecentThan } from './util/timestamp'; import { sessionRecordToProtobuf, - sessionStructureToArrayBuffer, + sessionStructureToBytes, } from './util/sessionTranslation'; import { DeviceType, @@ -81,7 +77,7 @@ function validateVerifiedStatus(status: number): boolean { const identityKeySchema = z.object({ id: z.string(), - publicKey: z.instanceof(ArrayBuffer), + publicKey: z.instanceof(Uint8Array), firstUse: z.boolean(), timestamp: z.number().refine((value: number) => value % 1 === 0 && value > 0), verified: z.number().refine(validateVerifiedStatus), @@ -171,13 +167,13 @@ export function hydrateSignedPreKey( export function freezeSession(session: SessionRecord): string { return session.serialize().toString('base64'); } -export function freezePublicKey(publicKey: PublicKey): ArrayBuffer { - return typedArrayToArrayBuffer(publicKey.serialize()); +export function freezePublicKey(publicKey: PublicKey): Uint8Array { + return publicKey.serialize(); } export function freezePreKey(preKey: PreKeyRecord): KeyPairType { const keyPair = { - pubKey: typedArrayToArrayBuffer(preKey.publicKey().serialize()), - privKey: typedArrayToArrayBuffer(preKey.privateKey().serialize()), + pubKey: preKey.publicKey().serialize(), + privKey: preKey.privateKey().serialize(), }; return keyPair; } @@ -185,8 +181,8 @@ export function freezeSignedPreKey( signedPreKey: SignedPreKeyRecord ): KeyPairType { const keyPair = { - pubKey: typedArrayToArrayBuffer(signedPreKey.publicKey().serialize()), - privKey: typedArrayToArrayBuffer(signedPreKey.privateKey().serialize()), + pubKey: signedPreKey.publicKey().serialize(), + privKey: signedPreKey.privateKey().serialize(), }; return keyPair; } @@ -260,8 +256,8 @@ export class SignalProtocolStore extends EventsMixin { for (const key of Object.keys(map.value)) { const { privKey, pubKey } = map.value[key]; this.ourIdentityKeys.set(new UUID(key).toString(), { - privKey: base64ToArrayBuffer(privKey), - pubKey: base64ToArrayBuffer(pubKey), + privKey: Bytes.fromBase64(privKey), + pubKey: Bytes.fromBase64(pubKey), }); } })(), @@ -607,7 +603,7 @@ export class SignalProtocolStore extends EventsMixin { return entry.item; } - const item = SenderKeyRecord.deserialize(entry.fromDB.data); + const item = SenderKeyRecord.deserialize(Buffer.from(entry.fromDB.data)); this.senderKeys.set(id, { hydrated: true, item, @@ -960,7 +956,7 @@ export class SignalProtocolStore extends EventsMixin { localUserData ); const record = SessionRecord.deserialize( - Buffer.from(sessionStructureToArrayBuffer(sessionProto)) + Buffer.from(sessionStructureToBytes(sessionProto)) ); await this.storeSession(QualifiedAddress.parse(session.id), record, { @@ -1425,7 +1421,7 @@ export class SignalProtocolStore extends EventsMixin { async isTrustedIdentity( encodedAddress: Address, - publicKey: ArrayBuffer, + publicKey: Uint8Array, direction: number ): Promise { if (!this.identityKeys) { @@ -1463,7 +1459,7 @@ export class SignalProtocolStore extends EventsMixin { } isTrustedForSending( - publicKey: ArrayBuffer, + publicKey: Uint8Array, identityRecord?: IdentityKeyType ): boolean { if (!identityRecord) { @@ -1493,7 +1489,7 @@ export class SignalProtocolStore extends EventsMixin { return true; } - async loadIdentityKey(uuid: UUID): Promise { + async loadIdentityKey(uuid: UUID): Promise { if (uuid === null || uuid === undefined) { throw new Error('loadIdentityKey: uuid was undefined/null'); } @@ -1522,7 +1518,7 @@ export class SignalProtocolStore extends EventsMixin { async saveIdentity( encodedAddress: Address, - publicKey: ArrayBuffer, + publicKey: Uint8Array, nonblockingApproval = false, { zone }: SessionTransactionOptions = {} ): Promise { @@ -1533,9 +1529,9 @@ export class SignalProtocolStore extends EventsMixin { if (encodedAddress === null || encodedAddress === undefined) { throw new Error('saveIdentity: encodedAddress was undefined/null'); } - if (!(publicKey instanceof ArrayBuffer)) { + if (!(publicKey instanceof Uint8Array)) { // eslint-disable-next-line no-param-reassign - publicKey = fromEncodedBinaryToArrayBuffer(publicKey); + publicKey = Bytes.fromBinary(publicKey); } if (typeof nonblockingApproval !== 'boolean') { // eslint-disable-next-line no-param-reassign @@ -1668,7 +1664,7 @@ export class SignalProtocolStore extends EventsMixin { async setVerified( uuid: UUID, verifiedStatus: number, - publicKey?: ArrayBuffer + publicKey?: Uint8Array ): Promise { if (uuid === null || uuid === undefined) { throw new Error('setVerified: uuid was undefined/null'); @@ -1676,7 +1672,7 @@ export class SignalProtocolStore extends EventsMixin { if (!validateVerifiedStatus(verifiedStatus)) { throw new Error('setVerified: Invalid verified status'); } - if (arguments.length > 2 && !(publicKey instanceof ArrayBuffer)) { + if (arguments.length > 2 && !(publicKey instanceof Uint8Array)) { throw new Error('setVerified: Invalid public key'); } @@ -1719,7 +1715,7 @@ export class SignalProtocolStore extends EventsMixin { processContactSyncVerificationState( uuid: UUID, verifiedStatus: number, - publicKey: ArrayBuffer + publicKey: Uint8Array ): Promise { if (verifiedStatus === VerifiedStatus.UNVERIFIED) { return this.processUnverifiedMessage(uuid, verifiedStatus, publicKey); @@ -1733,12 +1729,12 @@ export class SignalProtocolStore extends EventsMixin { async processUnverifiedMessage( uuid: UUID, verifiedStatus: number, - publicKey?: ArrayBuffer + publicKey?: Uint8Array ): Promise { if (uuid === null || uuid === undefined) { throw new Error('processUnverifiedMessage: uuid was undefined/null'); } - if (publicKey !== undefined && !(publicKey instanceof ArrayBuffer)) { + if (publicKey !== undefined && !(publicKey instanceof Uint8Array)) { throw new Error('processUnverifiedMessage: Invalid public key'); } @@ -1796,7 +1792,7 @@ export class SignalProtocolStore extends EventsMixin { async processVerifiedMessage( uuid: UUID, verifiedStatus: number, - publicKey?: ArrayBuffer + publicKey?: Uint8Array ): Promise { if (uuid === null || uuid === undefined) { throw new Error('processVerifiedMessage: uuid was undefined/null'); @@ -1804,7 +1800,7 @@ export class SignalProtocolStore extends EventsMixin { if (!validateVerifiedStatus(verifiedStatus)) { throw new Error('processVerifiedMessage: Invalid verified status'); } - if (publicKey !== undefined && !(publicKey instanceof ArrayBuffer)) { + if (publicKey !== undefined && !(publicKey instanceof Uint8Array)) { throw new Error('processVerifiedMessage: Invalid public key'); } diff --git a/ts/background.ts b/ts/background.ts index 447fa7941..1ae6d9104 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -14,7 +14,6 @@ import { ConversationAttributesType, } from './model-types.d'; import * as Bytes from './Bytes'; -import { typedArrayToArrayBuffer } from './Crypto'; import { WhatIsThis, DeliveryReceiptBatcherItemType } from './window.d'; import { getTitleBarVisibility, TitleBarVisibility } from './types/Settings'; import { SocketStatus } from './types/SocketStatus'; @@ -93,7 +92,9 @@ import { } from './messages/MessageSendState'; import * as AttachmentDownloads from './messageModifiers/AttachmentDownloads'; import * as preferredReactions from './state/ducks/preferredReactions'; +import * as Conversation from './types/Conversation'; import * as Stickers from './types/Stickers'; +import * as Errors from './types/errors'; import { SignalService as Proto } from './protobuf'; import { onRetryRequest, onDecryptionError } from './util/handleRetry'; import { themeChanged } from './shims/themeChanged'; @@ -497,7 +498,7 @@ export async function startApp(): Promise { removeDatabase: removeIndexedDB, doesDatabaseExist, } = window.Signal.IndexedDB; - const { Errors, Message } = window.Signal.Types; + const { Message } = window.Signal.Types; const { upgradeMessageSchema, writeNewAttachmentData, @@ -2559,7 +2560,7 @@ export async function startApp(): Promise { // special case for syncing details about ourselves if (details.profileKey) { log.info('Got sync message with our own profile key'); - ourProfileKeyService.set(typedArrayToArrayBuffer(details.profileKey)); + ourProfileKeyService.set(details.profileKey); } } @@ -2607,7 +2608,7 @@ export async function startApp(): Promise { // Update the conversation avatar only if new avatar exists and hash differs const { avatar } = details; if (avatar && avatar.data) { - const newAttributes = await window.Signal.Types.Conversation.maybeUpdateAvatar( + const newAttributes = await Conversation.maybeUpdateAvatar( conversation.attributes, avatar.data, { @@ -2650,9 +2651,7 @@ export async function startApp(): Promise { state: dropNull(verified.state), destination: dropNull(verified.destination), destinationUuid: dropNull(verified.destinationUuid), - identityKey: verified.identityKey - ? typedArrayToArrayBuffer(verified.identityKey) - : undefined, + identityKey: dropNull(verified.identityKey), viaContactSync: true, }, noop @@ -2720,7 +2719,7 @@ export async function startApp(): Promise { // Update the conversation avatar only if new avatar exists and hash differs const { avatar } = details; if (avatar && avatar.data) { - const newAttributes = await window.Signal.Types.Conversation.maybeUpdateAvatar( + const newAttributes = await Conversation.maybeUpdateAvatar( conversation.attributes, avatar.data, { @@ -3478,9 +3477,7 @@ export async function startApp(): Promise { if (storageServiceKey) { log.info('onKeysSync: received keys'); - const storageServiceKeyBase64 = window.Signal.Crypto.arrayBufferToBase64( - storageServiceKey - ); + const storageServiceKeyBase64 = Bytes.toBase64(storageServiceKey); window.storage.put('storageKey', storageServiceKeyBase64); await window.Signal.Services.runStorageServiceSyncJob(); diff --git a/ts/components/AvatarEditor.tsx b/ts/components/AvatarEditor.tsx index 62e1624c7..839be31a0 100644 --- a/ts/components/AvatarEditor.tsx +++ b/ts/components/AvatarEditor.tsx @@ -17,7 +17,7 @@ import { AvatarTextEditor } from './AvatarTextEditor'; import { AvatarUploadButton } from './AvatarUploadButton'; import { BetterAvatar } from './BetterAvatar'; import { LocalizerType } from '../types/Util'; -import { avatarDataToArrayBuffer } from '../util/avatarDataToArrayBuffer'; +import { avatarDataToBytes } from '../util/avatarDataToBytes'; import { createAvatarData } from '../util/createAvatarData'; import { isSameAvatarData } from '../util/isSameAvatarData'; import { missingCaseError } from '../util/missingCaseError'; @@ -25,14 +25,14 @@ import { missingCaseError } from '../util/missingCaseError'; export type PropsType = { avatarColor?: AvatarColorType; avatarPath?: string; - avatarValue?: ArrayBuffer; + avatarValue?: Uint8Array; conversationId?: string; conversationTitle?: string; deleteAvatarFromDisk: DeleteAvatarFromDiskActionType; i18n: LocalizerType; isGroup?: boolean; onCancel: () => unknown; - onSave: (buffer: ArrayBuffer | undefined) => unknown; + onSave: (buffer: Uint8Array | undefined) => unknown; userAvatarData: ReadonlyArray; replaceAvatar: ReplaceAvatarActionType; saveAvatarToDisk: SaveAvatarToDiskActionType; @@ -62,10 +62,10 @@ export const AvatarEditor = ({ const [provisionalSelectedAvatar, setProvisionalSelectedAvatar] = useState< AvatarDataType | undefined >(); - const [avatarPreview, setAvatarPreview] = useState( + const [avatarPreview, setAvatarPreview] = useState( avatarValue ); - const [initialAvatar, setInitialAvatar] = useState( + const [initialAvatar, setInitialAvatar] = useState( avatarValue ); const [localAvatarData, setLocalAvatarData] = useState>( @@ -84,7 +84,7 @@ export const AvatarEditor = ({ const selectedAvatar = getSelectedAvatar(provisionalSelectedAvatar); - // Caching the ArrayBuffer produced into avatarData as buffer because + // Caching the Uint8Array produced into avatarData as buffer because // that function is a little expensive to run and so we don't flicker the UI. useEffect(() => { let shouldCancel = false; @@ -95,7 +95,7 @@ export const AvatarEditor = ({ if (avatarData.buffer) { return avatarData; } - const buffer = await avatarDataToArrayBuffer(avatarData); + const buffer = await avatarDataToBytes(avatarData); return { ...avatarData, buffer, diff --git a/ts/components/AvatarIconEditor.tsx b/ts/components/AvatarIconEditor.tsx index ef88b4d88..c214b0ce2 100644 --- a/ts/components/AvatarIconEditor.tsx +++ b/ts/components/AvatarIconEditor.tsx @@ -9,7 +9,7 @@ import { AvatarDataType } from '../types/Avatar'; import { AvatarModalButtons } from './AvatarModalButtons'; import { AvatarPreview } from './AvatarPreview'; import { LocalizerType } from '../types/Util'; -import { avatarDataToArrayBuffer } from '../util/avatarDataToArrayBuffer'; +import { avatarDataToBytes } from '../util/avatarDataToBytes'; export type PropsType = { avatarData: AvatarDataType; @@ -22,7 +22,7 @@ export const AvatarIconEditor = ({ i18n, onClose, }: PropsType): JSX.Element => { - const [avatarBuffer, setAvatarBuffer] = useState(); + const [avatarBuffer, setAvatarBuffer] = useState(); const [avatarData, setAvatarData] = useState( initialAvatarData ); @@ -41,7 +41,7 @@ export const AvatarIconEditor = ({ let shouldCancel = false; async function loadAvatar() { - const buffer = await avatarDataToArrayBuffer(avatarData); + const buffer = await avatarDataToBytes(avatarData); if (!shouldCancel) { setAvatarBuffer(buffer); } diff --git a/ts/components/AvatarPreview.stories.tsx b/ts/components/AvatarPreview.stories.tsx index 40f46ed57..debc7079c 100644 --- a/ts/components/AvatarPreview.stories.tsx +++ b/ts/components/AvatarPreview.stories.tsx @@ -19,7 +19,7 @@ const TEST_IMAGE = new Uint8Array( '89504e470d0a1a0a0000000d4948445200000008000000080103000000fec12cc800000006504c5445ff00ff00ff000c82e9800000001849444154085b633061a8638863a867f8c720c760c12000001a4302f4d81dd9870000000049454e44ae426082', 2 ).map(bytePair => parseInt(bytePair.join(''), 16)) -).buffer; +); const createProps = (overrideProps: Partial = {}): PropsType => ({ avatarColor: overrideProps.avatarColor, diff --git a/ts/components/AvatarPreview.tsx b/ts/components/AvatarPreview.tsx index 2b45d371b..efe9c48f7 100644 --- a/ts/components/AvatarPreview.tsx +++ b/ts/components/AvatarPreview.tsx @@ -9,17 +9,17 @@ import { LocalizerType } from '../types/Util'; import { Spinner } from './Spinner'; import { AvatarColors, AvatarColorType } from '../types/Colors'; import { getInitials } from '../util/getInitials'; -import { imagePathToArrayBuffer } from '../util/imagePathToArrayBuffer'; +import { imagePathToBytes } from '../util/imagePathToBytes'; export type PropsType = { avatarColor?: AvatarColorType; avatarPath?: string; - avatarValue?: ArrayBuffer; + avatarValue?: Uint8Array; conversationTitle?: string; i18n: LocalizerType; isEditable?: boolean; isGroup?: boolean; - onAvatarLoaded?: (avatarBuffer: ArrayBuffer) => unknown; + onAvatarLoaded?: (avatarBuffer: Uint8Array) => unknown; onClear?: () => unknown; onClick?: () => unknown; style?: CSSProperties; @@ -48,7 +48,7 @@ export const AvatarPreview = ({ avatarValue ? undefined : avatarPath ); - const [avatarPreview, setAvatarPreview] = useState(); + const [avatarPreview, setAvatarPreview] = useState(); // Loads the initial avatarPath if one is provided. useEffect(() => { @@ -61,7 +61,7 @@ export const AvatarPreview = ({ (async () => { try { - const buffer = await imagePathToArrayBuffer(startingAvatarPath); + const buffer = await imagePathToBytes(startingAvatarPath); if (shouldCancel) { return; } @@ -95,7 +95,7 @@ export const AvatarPreview = ({ } }, [avatarValue]); - // Creates the object URL to render the ArrayBuffer image + // Creates the object URL to render the Uint8Array image const [objectUrl, setObjectUrl] = useState(); useEffect(() => { diff --git a/ts/components/AvatarTextEditor.tsx b/ts/components/AvatarTextEditor.tsx index 9a8218702..5b0561687 100644 --- a/ts/components/AvatarTextEditor.tsx +++ b/ts/components/AvatarTextEditor.tsx @@ -19,7 +19,7 @@ import { AvatarDataType } from '../types/Avatar'; import { AvatarModalButtons } from './AvatarModalButtons'; import { BetterAvatarBubble } from './BetterAvatarBubble'; import { LocalizerType } from '../types/Util'; -import { avatarDataToArrayBuffer } from '../util/avatarDataToArrayBuffer'; +import { avatarDataToBytes } from '../util/avatarDataToBytes'; import { createAvatarData } from '../util/createAvatarData'; import { getFittedFontSize, @@ -27,7 +27,7 @@ import { } from '../util/avatarTextSizeCalculator'; type DoneHandleType = ( - avatarBuffer: ArrayBuffer, + avatarBuffer: Uint8Array, avatarData: AvatarDataType ) => unknown; @@ -111,7 +111,7 @@ export const AvatarTextEditor = ({ text: inputText, }); - const buffer = await avatarDataToArrayBuffer(newAvatarData); + const buffer = await avatarDataToBytes(newAvatarData); onDoneRef.current(buffer, newAvatarData); }, [inputText, selectedColor]); diff --git a/ts/components/AvatarUploadButton.tsx b/ts/components/AvatarUploadButton.tsx index d20154008..252c5d1fb 100644 --- a/ts/components/AvatarUploadButton.tsx +++ b/ts/components/AvatarUploadButton.tsx @@ -10,7 +10,7 @@ import { processImageFile } from '../util/processImageFile'; export type PropsType = { className: string; i18n: LocalizerType; - onChange: (avatar: ArrayBuffer) => unknown; + onChange: (avatar: Uint8Array) => unknown; }; export const AvatarUploadButton = ({ @@ -30,7 +30,7 @@ export const AvatarUploadButton = ({ let shouldCancel = false; (async () => { - let newAvatar: ArrayBuffer; + let newAvatar: Uint8Array; try { newAvatar = await processImageFile(processingFile); } catch (err) { diff --git a/ts/components/BetterAvatar.tsx b/ts/components/BetterAvatar.tsx index 8ffd5a945..10c353129 100644 --- a/ts/components/BetterAvatar.tsx +++ b/ts/components/BetterAvatar.tsx @@ -7,7 +7,7 @@ import { AvatarDataType } from '../types/Avatar'; import { BetterAvatarBubble } from './BetterAvatarBubble'; import { LocalizerType } from '../types/Util'; import { Spinner } from './Spinner'; -import { avatarDataToArrayBuffer } from '../util/avatarDataToArrayBuffer'; +import { avatarDataToBytes } from '../util/avatarDataToBytes'; type AvatarSize = 48 | 80; @@ -15,7 +15,7 @@ export type PropsType = { avatarData: AvatarDataType; i18n: LocalizerType; isSelected?: boolean; - onClick: (avatarBuffer: ArrayBuffer | undefined) => unknown; + onClick: (avatarBuffer: Uint8Array | undefined) => unknown; onDelete: () => unknown; size?: AvatarSize; }; @@ -28,7 +28,7 @@ export const BetterAvatar = ({ onDelete, size = 48, }: PropsType): JSX.Element => { - const [avatarBuffer, setAvatarBuffer] = useState( + const [avatarBuffer, setAvatarBuffer] = useState( avatarData.buffer ); const [avatarURL, setAvatarURL] = useState(undefined); @@ -37,7 +37,7 @@ export const BetterAvatar = ({ let shouldCancel = false; async function makeAvatar() { - const buffer = await avatarDataToArrayBuffer(avatarData); + const buffer = await avatarDataToBytes(avatarData); if (!shouldCancel) { setAvatarBuffer(buffer); } @@ -56,7 +56,7 @@ export const BetterAvatar = ({ }; }, [avatarBuffer, avatarData]); - // Convert avatar's ArrayBuffer to a URL object + // Convert avatar's Uint8Array to a URL object useEffect(() => { if (avatarBuffer) { const url = URL.createObjectURL(new Blob([avatarBuffer])); diff --git a/ts/components/LeftPane.tsx b/ts/components/LeftPane.tsx index 39a6d0316..7f3323e18 100644 --- a/ts/components/LeftPane.tsx +++ b/ts/components/LeftPane.tsx @@ -102,7 +102,7 @@ export type PropsType = { switchToAssociatedView?: boolean; }) => void; setComposeSearchTerm: (composeSearchTerm: string) => void; - setComposeGroupAvatar: (_: undefined | ArrayBuffer) => void; + setComposeGroupAvatar: (_: undefined | Uint8Array) => void; setComposeGroupName: (_: string) => void; setComposeGroupExpireTimer: (_: number) => void; showArchivedConversations: () => void; diff --git a/ts/components/ProfileEditor.tsx b/ts/components/ProfileEditor.tsx index 2b4ba7d87..b76904128 100644 --- a/ts/components/ProfileEditor.tsx +++ b/ts/components/ProfileEditor.tsx @@ -37,7 +37,7 @@ type PropsExternalType = { onEditStateChanged: (editState: EditState) => unknown; onProfileChanged: ( profileData: ProfileDataType, - avatarBuffer?: ArrayBuffer + avatarBuffer?: Uint8Array ) => unknown; }; @@ -126,7 +126,7 @@ export const ProfileEditor = ({ aboutText, }); - const [avatarBuffer, setAvatarBuffer] = useState( + const [avatarBuffer, setAvatarBuffer] = useState( undefined ); const [stagedProfile, setStagedProfile] = useState({ @@ -153,7 +153,7 @@ export const ProfileEditor = ({ ); const handleAvatarChanged = useCallback( - (avatar: ArrayBuffer | undefined) => { + (avatar: Uint8Array | undefined) => { setAvatarBuffer(avatar); setEditState(EditState.None); onProfileChanged(stagedProfile, avatar); diff --git a/ts/components/ProfileEditorModal.tsx b/ts/components/ProfileEditorModal.tsx index 937bcdce1..6fd090c32 100644 --- a/ts/components/ProfileEditorModal.tsx +++ b/ts/components/ProfileEditorModal.tsx @@ -18,7 +18,7 @@ export type PropsDataType = { type PropsType = { myProfileChanged: ( profileData: ProfileDataType, - avatarBuffer?: ArrayBuffer + avatarBuffer?: Uint8Array ) => unknown; toggleProfileEditor: () => unknown; toggleProfileEditorHasError: () => unknown; diff --git a/ts/components/conversation/conversation-details/ConversationDetails.tsx b/ts/components/conversation/conversation-details/ConversationDetails.tsx index dc0f6ca2e..b55ec30c4 100644 --- a/ts/components/conversation/conversation-details/ConversationDetails.tsx +++ b/ts/components/conversation/conversation-details/ConversationDetails.tsx @@ -72,7 +72,7 @@ export type StateProps = { showConversationNotificationsSettings: () => void; updateGroupAttributes: ( _: Readonly<{ - avatar?: undefined | ArrayBuffer; + avatar?: undefined | Uint8Array; description?: string; title?: string; }> @@ -169,7 +169,7 @@ export const ConversationDetails: React.ComponentType = ({ } makeRequest={async ( options: Readonly<{ - avatar?: undefined | ArrayBuffer; + avatar?: undefined | Uint8Array; description?: string; title?: string; }> diff --git a/ts/components/conversation/conversation-details/EditConversationAttributesModal.tsx b/ts/components/conversation/conversation-details/EditConversationAttributesModal.tsx index c5c09bbc6..6e2095533 100644 --- a/ts/components/conversation/conversation-details/EditConversationAttributesModal.tsx +++ b/ts/components/conversation/conversation-details/EditConversationAttributesModal.tsx @@ -34,7 +34,7 @@ type PropsType = { initiallyFocusDescription: boolean; makeRequest: ( _: Readonly<{ - avatar?: undefined | ArrayBuffer; + avatar?: undefined | Uint8Array; description?: string; title?: undefined | string; }> @@ -73,7 +73,7 @@ export const EditConversationAttributesModal: FunctionComponent = ({ const startingAvatarPathRef = useRef(externalAvatarPath); const [editingAvatar, setEditingAvatar] = useState(false); - const [avatar, setAvatar] = useState(); + const [avatar, setAvatar] = useState(); const [rawTitle, setRawTitle] = useState(externalTitle); const [rawGroupDescription, setRawGroupDescription] = useState( externalGroupDescription @@ -111,7 +111,7 @@ export const EditConversationAttributesModal: FunctionComponent = ({ event.preventDefault(); const request: { - avatar?: undefined | ArrayBuffer; + avatar?: undefined | Uint8Array; description?: string; title?: string; } = {}; diff --git a/ts/components/leftPane/LeftPaneHelper.tsx b/ts/components/leftPane/LeftPaneHelper.tsx index 92168ccd0..130bbcaa6 100644 --- a/ts/components/leftPane/LeftPaneHelper.tsx +++ b/ts/components/leftPane/LeftPaneHelper.tsx @@ -56,7 +56,7 @@ export abstract class LeftPaneHelper { composeSaveAvatarToDisk: SaveAvatarToDiskActionType; createGroup: () => unknown; i18n: LocalizerType; - setComposeGroupAvatar: (_: undefined | ArrayBuffer) => unknown; + setComposeGroupAvatar: (_: undefined | Uint8Array) => unknown; setComposeGroupName: (_: string) => unknown; setComposeGroupExpireTimer: (_: number) => void; onChangeComposeSearchTerm: ( diff --git a/ts/components/leftPane/LeftPaneSetGroupMetadataHelper.tsx b/ts/components/leftPane/LeftPaneSetGroupMetadataHelper.tsx index 066d13e4e..946bda797 100644 --- a/ts/components/leftPane/LeftPaneSetGroupMetadataHelper.tsx +++ b/ts/components/leftPane/LeftPaneSetGroupMetadataHelper.tsx @@ -24,7 +24,7 @@ import { import { AvatarColors } from '../../types/Colors'; export type LeftPaneSetGroupMetadataPropsType = { - groupAvatar: undefined | ArrayBuffer; + groupAvatar: undefined | Uint8Array; groupName: string; groupExpireTimer: number; hasError: boolean; @@ -37,7 +37,7 @@ export type LeftPaneSetGroupMetadataPropsType = { /* eslint-disable class-methods-use-this */ export class LeftPaneSetGroupMetadataHelper extends LeftPaneHelper { - private readonly groupAvatar: undefined | ArrayBuffer; + private readonly groupAvatar: undefined | Uint8Array; private readonly groupName: string; @@ -127,7 +127,7 @@ export class LeftPaneSetGroupMetadataHelper extends LeftPaneHelper unknown; i18n: LocalizerType; - setComposeGroupAvatar: (_: undefined | ArrayBuffer) => unknown; + setComposeGroupAvatar: (_: undefined | Uint8Array) => unknown; setComposeGroupExpireTimer: (_: number) => void; setComposeGroupName: (_: string) => unknown; toggleComposeEditingAvatar: () => unknown; diff --git a/ts/context/Crypto.ts b/ts/context/Crypto.ts new file mode 100644 index 000000000..5925b2a5b --- /dev/null +++ b/ts/context/Crypto.ts @@ -0,0 +1,129 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +/* eslint-disable class-methods-use-this */ + +import { Buffer } from 'buffer'; +import crypto, { Decipher } from 'crypto'; + +import { strictAssert } from '../util/assert'; +import { HashType, CipherType } from '../types/Crypto'; + +const AUTH_TAG_SIZE = 16; + +export class Crypto { + public sign(key: Uint8Array, data: Uint8Array): Uint8Array { + return crypto + .createHmac('sha256', Buffer.from(key)) + .update(Buffer.from(data)) + .digest(); + } + + public hash(type: HashType, data: Uint8Array): Uint8Array { + return crypto.createHash(type).update(Buffer.from(data)).digest(); + } + + public encrypt( + cipherType: CipherType, + { + key, + plaintext, + iv, + aad, + }: Readonly<{ + key: Uint8Array; + plaintext: Uint8Array; + iv: Uint8Array; + aad?: Uint8Array; + }> + ): Uint8Array { + if (cipherType === CipherType.AES256GCM) { + const gcm = crypto.createCipheriv( + cipherType, + Buffer.from(key), + Buffer.from(iv) + ); + + if (aad) { + gcm.setAAD(aad); + } + + const first = gcm.update(Buffer.from(plaintext)); + const last = gcm.final(); + const tag = gcm.getAuthTag(); + strictAssert(tag.length === AUTH_TAG_SIZE, 'Invalid auth tag size'); + + return Buffer.concat([first, last, tag]); + } + + strictAssert(aad === undefined, `AAD is not supported for: ${cipherType}`); + const cipher = crypto.createCipheriv( + cipherType, + Buffer.from(key), + Buffer.from(iv) + ); + return Buffer.concat([ + cipher.update(Buffer.from(plaintext)), + cipher.final(), + ]); + } + + public decrypt( + cipherType: CipherType, + { + key, + ciphertext, + iv, + aad, + }: Readonly<{ + key: Uint8Array; + ciphertext: Uint8Array; + iv: Uint8Array; + aad?: Uint8Array; + }> + ): Uint8Array { + let decipher: Decipher; + let input = Buffer.from(ciphertext); + if (cipherType === CipherType.AES256GCM) { + const gcm = crypto.createDecipheriv( + cipherType, + Buffer.from(key), + Buffer.from(iv) + ); + + if (input.length < AUTH_TAG_SIZE) { + throw new Error('Invalid GCM ciphertext'); + } + + const tag = input.slice(input.length - AUTH_TAG_SIZE); + input = input.slice(0, input.length - AUTH_TAG_SIZE); + + gcm.setAuthTag(tag); + + if (aad) { + gcm.setAAD(aad); + } + + decipher = gcm; + } else { + strictAssert( + aad === undefined, + `AAD is not supported for: ${cipherType}` + ); + decipher = crypto.createDecipheriv( + cipherType, + Buffer.from(key), + Buffer.from(iv) + ); + } + return Buffer.concat([decipher.update(input), decipher.final()]); + } + + public getRandomBytes(size: number): Uint8Array { + return crypto.randomBytes(size); + } + + public constantTimeEqual(left: Uint8Array, right: Uint8Array): boolean { + return crypto.timingSafeEqual(Buffer.from(left), Buffer.from(right)); + } +} diff --git a/ts/context/index.ts b/ts/context/index.ts index 50e70670f..9be7acf20 100644 --- a/ts/context/index.ts +++ b/ts/context/index.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import { Bytes } from './Bytes'; +import { Crypto } from './Crypto'; import { createNativeThemeListener, MinimalIPC, @@ -10,6 +11,8 @@ import { export class Context { public readonly bytes = new Bytes(); + public readonly crypto = new Crypto(); + public readonly nativeThemeListener; constructor(ipc: MinimalIPC) { diff --git a/ts/groups.ts b/ts/groups.ts index 8e069f56d..09be22266 100644 --- a/ts/groups.ts +++ b/ts/groups.ts @@ -53,9 +53,7 @@ import { import { computeHash, deriveMasterKeyFromGroupV1, - fromEncodedBinaryToArrayBuffer, getRandomBytes, - typedArrayToArrayBuffer, } from './Crypto'; import { GroupCredentialsType, @@ -224,9 +222,6 @@ export type GroupFields = { readonly publicParams: Uint8Array; }; -// TODO: remove once we move away from ArrayBuffers -const FIXMEU8 = Uint8Array; - const MAX_CACHED_GROUP_FIELDS = 100; const groupFieldsCache = new LRU({ @@ -256,7 +251,7 @@ type UpdatesResultType = { }; type UploadedAvatarType = { - data: ArrayBuffer; + data: Uint8Array; hash: string; key: string; }; @@ -277,7 +272,7 @@ const GROUP_INVITE_LINK_PASSWORD_LENGTH = 16; // Group Links -export function generateGroupInviteLinkPassword(): ArrayBuffer { +export function generateGroupInviteLinkPassword(): Uint8Array { return getRandomBytes(GROUP_INVITE_LINK_PASSWORD_LENGTH); } @@ -366,24 +361,24 @@ async function uploadAvatar( logId: string; publicParams: string; secretParams: string; - } & ({ path: string } | { data: ArrayBuffer }) + } & ({ path: string } | { data: Uint8Array }) ): Promise { const { logId, publicParams, secretParams } = options; try { const clientZkGroupCipher = getClientZkGroupCipher(secretParams); - let data: ArrayBuffer; + let data: Uint8Array; if ('data' in options) { ({ data } = options); } else { data = await window.Signal.Migrations.readAttachmentData(options.path); } - const hash = await computeHash(data); + const hash = computeHash(data); const blobPlaintext = Proto.GroupAttributeBlob.encode({ - avatar: new FIXMEU8(data), + avatar: data, }).finish(); const ciphertext = encryptGroupBlob(clientZkGroupCipher, blobPlaintext); @@ -731,7 +726,7 @@ export async function buildUpdateAttributesChange( 'id' | 'revision' | 'publicParams' | 'secretParams' >, attributes: Readonly<{ - avatar?: undefined | ArrayBuffer; + avatar?: undefined | Uint8Array; description?: string; title?: string; }> @@ -1309,7 +1304,7 @@ export async function modifyGroupV2({ window.Signal.Util.sendToGroup({ groupSendOptions: { groupV2: conversation.getGroupV2Info({ - groupChange: typedArrayToArrayBuffer(groupChangeBuffer), + groupChange: groupChangeBuffer, includePendingMembers: true, extraConversationsForSend, }), @@ -1497,7 +1492,7 @@ export async function createGroupV2({ avatars, }: Readonly<{ name: string; - avatar: undefined | ArrayBuffer; + avatar: undefined | Uint8Array; expireTimer: undefined | number; conversationIds: Array; avatars?: Array; @@ -1514,7 +1509,7 @@ export async function createGroupV2({ const ACCESS_ENUM = Proto.AccessControl.AccessRequired; const MEMBER_ROLE_ENUM = Proto.Member.Role; - const masterKeyBuffer = new FIXMEU8(getRandomBytes(32)); + const masterKeyBuffer = getRandomBytes(32); const fields = deriveGroupFields(masterKeyBuffer); const groupId = Bytes.toBase64(fields.id); @@ -1763,10 +1758,8 @@ export async function hasV1GroupBeenMigrated( throw new Error(`checkForGV2Existence/${logId}: No groupId!`); } - const idBuffer = fromEncodedBinaryToArrayBuffer(groupId); - const masterKeyBuffer = new FIXMEU8( - await deriveMasterKeyFromGroupV1(idBuffer) - ); + const idBuffer = Bytes.fromBinary(groupId); + const masterKeyBuffer = deriveMasterKeyFromGroupV1(idBuffer); const fields = deriveGroupFields(masterKeyBuffer); try { @@ -1783,9 +1776,7 @@ export async function hasV1GroupBeenMigrated( } } -export async function maybeDeriveGroupV2Id( - conversation: ConversationModel -): Promise { +export function maybeDeriveGroupV2Id(conversation: ConversationModel): boolean { const isGroupV1 = getIsGroupV1(conversation.attributes); const groupV1Id = conversation.get('groupId'); const derived = conversation.get('derivedGroupV2Id'); @@ -1794,10 +1785,8 @@ export async function maybeDeriveGroupV2Id( return false; } - const v1IdBuffer = fromEncodedBinaryToArrayBuffer(groupV1Id); - const masterKeyBuffer = new FIXMEU8( - await deriveMasterKeyFromGroupV1(v1IdBuffer) - ); + const v1IdBuffer = Bytes.fromBinary(groupV1Id); + const masterKeyBuffer = deriveMasterKeyFromGroupV1(v1IdBuffer); const fields = deriveGroupFields(masterKeyBuffer); const derivedGroupV2Id = Bytes.toBase64(fields.id); @@ -2050,10 +2039,8 @@ export async function initiateMigrationToGroupV2( ); } - const groupV1IdBuffer = fromEncodedBinaryToArrayBuffer(previousGroupV1Id); - const masterKeyBuffer = new FIXMEU8( - await deriveMasterKeyFromGroupV1(groupV1IdBuffer) - ); + const groupV1IdBuffer = Bytes.fromBinary(previousGroupV1Id); + const masterKeyBuffer = deriveMasterKeyFromGroupV1(groupV1IdBuffer); const fields = deriveGroupFields(masterKeyBuffer); const groupId = Bytes.toBase64(fields.id); @@ -2219,9 +2206,7 @@ export async function initiateMigrationToGroupV2( const logId = conversation.idForLogging(); const timestamp = Date.now(); - const ourProfileKey: - | ArrayBuffer - | undefined = await ourProfileKeyService.get(); + const ourProfileKey = await ourProfileKeyService.get(); const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; const sendOptions = await getSendOptions(conversation.attributes); @@ -2408,10 +2393,8 @@ export async function joinGroupV2ViaLinkAndMigrate({ } // Derive GroupV2 fields - const groupV1IdBuffer = fromEncodedBinaryToArrayBuffer(previousGroupV1Id); - const masterKeyBuffer = new FIXMEU8( - await deriveMasterKeyFromGroupV1(groupV1IdBuffer) - ); + const groupV1IdBuffer = Bytes.fromBinary(previousGroupV1Id); + const masterKeyBuffer = deriveMasterKeyFromGroupV1(groupV1IdBuffer); const fields = deriveGroupFields(masterKeyBuffer); const groupId = Bytes.toBase64(fields.id); @@ -2506,10 +2489,8 @@ export async function respondToGroupV2Migration({ conversation.hasMember(ourConversationId); // Derive GroupV2 fields - const groupV1IdBuffer = fromEncodedBinaryToArrayBuffer(previousGroupV1Id); - const masterKeyBuffer = new FIXMEU8( - await deriveMasterKeyFromGroupV1(groupV1IdBuffer) - ); + const groupV1IdBuffer = Bytes.fromBinary(previousGroupV1Id); + const masterKeyBuffer = deriveMasterKeyFromGroupV1(groupV1IdBuffer); const fields = deriveGroupFields(masterKeyBuffer); const groupId = Bytes.toBase64(fields.id); @@ -3421,7 +3402,7 @@ async function integrateGroupChange({ if (groupChange) { groupChangeActions = Proto.GroupChange.Actions.decode( - groupChange.actions || new FIXMEU8(0) + groupChange.actions || new Uint8Array(0) ); if ( @@ -4541,7 +4522,7 @@ async function applyGroupChange({ export async function decryptGroupAvatar( avatarKey: string, secretParamsBase64: string -): Promise { +): Promise { const sender = window.textsecure.messaging; if (!sender) { throw new Error( @@ -4549,7 +4530,7 @@ export async function decryptGroupAvatar( ); } - const ciphertext = new FIXMEU8(await sender.getGroupAvatar(avatarKey)); + const ciphertext = await sender.getGroupAvatar(avatarKey); const clientZkGroupCipher = getClientZkGroupCipher(secretParamsBase64); const plaintext = decryptGroupBlob(clientZkGroupCipher, ciphertext); const blob = Proto.GroupAttributeBlob.decode(plaintext); @@ -4559,7 +4540,7 @@ export async function decryptGroupAvatar( ); } - return typedArrayToArrayBuffer(blob.avatar); + return blob.avatar; } // Ovewriting result.avatar as part of functionality @@ -4583,7 +4564,7 @@ export async function applyNewAvatar( } const data = await decryptGroupAvatar(newAvatar, result.secretParams); - const hash = await computeHash(data); + const hash = computeHash(data); if (result.avatar && result.avatar.path && result.avatar.hash !== hash) { await window.Signal.Migrations.deleteAttachmentData(result.avatar.path); diff --git a/ts/jobs/normalMessageSendJobQueue.ts b/ts/jobs/normalMessageSendJobQueue.ts index 48936d13b..b73113078 100644 --- a/ts/jobs/normalMessageSendJobQueue.ts +++ b/ts/jobs/normalMessageSendJobQueue.ts @@ -465,7 +465,7 @@ async function getMessageSendData({ mentions: undefined | BodyRangesType; messageTimestamp: number; preview: Array; - profileKey: undefined | ArrayBuffer; + profileKey: undefined | Uint8Array; quote: WhatIsThis; sticker: WhatIsThis; }> { diff --git a/ts/linkPreviews/linkPreviewFetch.ts b/ts/linkPreviews/linkPreviewFetch.ts index 85d64a916..80d9017d9 100644 --- a/ts/linkPreviews/linkPreviewFetch.ts +++ b/ts/linkPreviews/linkPreviewFetch.ts @@ -64,7 +64,7 @@ export type LinkPreviewMetadata = { }; export type LinkPreviewImage = { - data: ArrayBuffer; + data: Uint8Array; contentType: MIMEType; }; @@ -290,7 +290,7 @@ const getHtmlDocument = async ( let result: HTMLDocument = emptyHtmlDocument(); const maxHtmlBytesToLoad = Math.min(contentLength, MAX_HTML_BYTES_TO_LOAD); - const buffer = new Uint8Array(new ArrayBuffer(maxHtmlBytesToLoad)); + const buffer = new Uint8Array(maxHtmlBytesToLoad); let bytesLoadedSoFar = 0; try { @@ -578,9 +578,9 @@ export async function fetchLinkPreviewImage( return null; } - let data: ArrayBuffer; + let data: Uint8Array; try { - data = await response.arrayBuffer(); + data = await response.buffer(); } catch (err) { log.warn('fetchLinkPreviewImage: failed to read body; bailing'); return null; diff --git a/ts/messageModifiers/AttachmentDownloads.ts b/ts/messageModifiers/AttachmentDownloads.ts index b8c888fd1..a8e9e7a62 100644 --- a/ts/messageModifiers/AttachmentDownloads.ts +++ b/ts/messageModifiers/AttachmentDownloads.ts @@ -7,7 +7,7 @@ import { v4 as getGuid } from 'uuid'; import dataInterface from '../sql/Client'; import * as durations from '../util/durations'; import { downloadAttachment } from '../util/downloadAttachment'; -import { stringFromBytes } from '../Crypto'; +import * as Bytes from '../Bytes'; import { AttachmentDownloadJobType, AttachmentDownloadJobTypeType, @@ -319,7 +319,7 @@ async function _addAttachmentToMessage( attachment ); message.set({ - body: attachment.error ? message.get('body') : stringFromBytes(data), + body: attachment.error ? message.get('body') : Bytes.toString(data), bodyPending: false, }); } finally { diff --git a/ts/model-types.d.ts b/ts/model-types.d.ts index bcf024acf..e090ff486 100644 --- a/ts/model-types.d.ts +++ b/ts/model-types.d.ts @@ -94,7 +94,7 @@ export type MessageAttributesType = { bodyRanges?: BodyRangesType; callHistoryDetails?: CallHistoryDetailsFromDiskType; changedId?: string; - dataMessage?: ArrayBuffer | null; + dataMessage?: Uint8Array | null; decrypted_at?: number; deletedForEveryone?: boolean; deletedForEveryoneTimestamp?: number; @@ -355,7 +355,7 @@ export type GroupV2PendingAdminApprovalType = { }; export type VerificationOptions = { - key?: null | ArrayBuffer; + key?: null | Uint8Array; viaContactSync?: boolean; viaStorageServiceSync?: boolean; viaSyncMessage?: boolean; diff --git a/ts/models/conversations.ts b/ts/models/conversations.ts index 8ad9a47c4..a7b263a07 100644 --- a/ts/models/conversations.ts +++ b/ts/models/conversations.ts @@ -16,6 +16,8 @@ import { } from '../model-types.d'; import { AttachmentType, isGIF } from '../types/Attachment'; import { CallMode, CallHistoryDetailsType } from '../types/Calling'; +import * as EmbeddedContact from '../types/EmbeddedContact'; +import * as Conversation from '../types/Conversation'; import * as Stickers from '../types/Stickers'; import { CapabilityError } from '../types/errors'; import type { @@ -40,16 +42,10 @@ import { sniffImageMimeType } from '../util/sniffImageMimeType'; import { isValidE164 } from '../util/isValidE164'; import { MIMEType, IMAGE_JPEG, IMAGE_GIF, IMAGE_WEBP } from '../types/MIME'; import { UUID } from '../types/UUID'; -import { - arrayBufferToBase64, - base64ToArrayBuffer, - deriveAccessKey, - fromEncodedBinaryToArrayBuffer, - stringFromBytes, -} from '../Crypto'; +import { deriveAccessKey, decryptProfileName, decryptProfile } from '../Crypto'; import * as Bytes from '../Bytes'; import { BodyRangesType } from '../types/Util'; -import { getTextWithMentions } from '../util'; +import { getTextWithMentions } from '../util/getTextWithMentions'; import { migrateColor } from '../util/migrateColor'; import { isNotNil } from '../util/isNotNil'; import { dropNull } from '../util/dropNull'; @@ -98,14 +94,11 @@ import { getAvatarData } from '../util/getAvatarData'; import { createIdenticon } from '../util/createIdenticon'; import * as log from '../logging/log'; -// TODO: remove once we move away from ArrayBuffers -const FIXMEU8 = Uint8Array; - /* eslint-disable more/no-then */ window.Whisper = window.Whisper || {}; const { Services, Util } = window.Signal; -const { EmbeddedContact, Message } = window.Signal.Types; +const { Message } = window.Signal.Types; const { deleteAttachmentData, doesAttachmentExist, @@ -1125,7 +1118,7 @@ export class ConversationModel extends window.Backbone includePendingMembers, extraConversationsForSend, }: { - groupChange?: ArrayBuffer; + groupChange?: Uint8Array; includePendingMembers?: boolean; extraConversationsForSend?: Array; } = {}): GroupV2InfoType | undefined { @@ -1143,7 +1136,7 @@ export class ConversationModel extends window.Backbone includePendingMembers, extraConversationsForSend, }), - groupChange: groupChange ? new FIXMEU8(groupChange) : undefined, + groupChange, }; } @@ -1165,7 +1158,7 @@ export class ConversationModel extends window.Backbone }; } - getGroupIdBuffer(): ArrayBuffer | undefined { + getGroupIdBuffer(): Uint8Array | undefined { const groupIdString = this.get('groupId'); if (!groupIdString) { @@ -1173,10 +1166,10 @@ export class ConversationModel extends window.Backbone } if (isGroupV1(this.attributes)) { - return fromEncodedBinaryToArrayBuffer(groupIdString); + return Bytes.fromBinary(groupIdString); } if (isGroupV2(this.attributes)) { - return base64ToArrayBuffer(groupIdString); + return Bytes.fromBase64(groupIdString); } return undefined; @@ -1253,12 +1246,9 @@ export class ConversationModel extends window.Backbone } async cleanup(): Promise { - await window.Signal.Types.Conversation.deleteExternalFiles( - this.attributes, - { - deleteAttachmentData, - } - ); + await Conversation.deleteExternalFiles(this.attributes, { + deleteAttachmentData, + }); } async onNewMessage(message: MessageModel): Promise { @@ -1838,7 +1828,7 @@ export class ConversationModel extends window.Backbone if (!error.response) { throw error; } else { - const errorDetails = stringFromBytes(error.response); + const errorDetails = Bytes.toString(error.response); if (errorDetails !== ALREADY_REQUESTED_TO_JOIN) { throw error; } else { @@ -1914,7 +1904,7 @@ export class ConversationModel extends window.Backbone async updateGroupAttributesV2( attributes: Readonly<{ - avatar?: undefined | ArrayBuffer; + avatar?: undefined | Uint8Array; description?: string; title?: string; }> @@ -3340,7 +3330,7 @@ export class ConversationModel extends window.Backbone const sendOptions = await getSendOptions(this.attributes); const promise = (async () => { - let profileKey: ArrayBuffer | undefined; + let profileKey: Uint8Array | undefined; if (this.get('profileSharing')) { profileKey = await ourProfileKeyService.get(); } @@ -3474,7 +3464,7 @@ export class ConversationModel extends window.Backbone throw new Error('Cannot send reaction while offline!'); } - let profileKey: ArrayBuffer | undefined; + let profileKey: Uint8Array | undefined; if (this.get('profileSharing')) { profileKey = await ourProfileKeyService.get(); } @@ -3905,7 +3895,7 @@ export class ConversationModel extends window.Backbone return; } - const groupInviteLinkPassword = arrayBufferToBase64( + const groupInviteLinkPassword = Bytes.toBase64( window.Signal.Groups.generateGroupInviteLinkPassword() ); @@ -3932,9 +3922,7 @@ export class ConversationModel extends window.Backbone value && !this.get('groupInviteLinkPassword'); const groupInviteLinkPassword = this.get('groupInviteLinkPassword') || - arrayBufferToBase64( - window.Signal.Groups.generateGroupInviteLinkPassword() - ); + Bytes.toBase64(window.Signal.Groups.generateGroupInviteLinkPassword()); log.info('toggleGroupLink for conversation', this.idForLogging(), value); @@ -4410,24 +4398,21 @@ export class ConversationModel extends window.Backbone if (!encryptedName) { return; } - // isn't this already an ArrayBuffer? + // isn't this already an Uint8Array? const key = (this.get('profileKey') as unknown) as string; if (!key) { return; } // decode - const keyBuffer = base64ToArrayBuffer(key); + const keyBuffer = Bytes.fromBase64(key); // decrypt - const { given, family } = await window.textsecure.crypto.decryptProfileName( - encryptedName, - keyBuffer - ); + const { given, family } = decryptProfileName(encryptedName, keyBuffer); // encode - const profileName = given ? stringFromBytes(given) : undefined; - const profileFamilyName = family ? stringFromBytes(family) : undefined; + const profileName = given ? Bytes.toString(given) : undefined; + const profileFamilyName = family ? Bytes.toString(family) : undefined; // set then check for changes const oldName = this.getProfileName(); @@ -4467,22 +4452,19 @@ export class ConversationModel extends window.Backbone } const avatar = await window.textsecure.messaging.getAvatar(avatarPath); - // isn't this already an ArrayBuffer? + // isn't this already an Uint8Array? const key = (this.get('profileKey') as unknown) as string; if (!key) { return; } - const keyBuffer = base64ToArrayBuffer(key); + const keyBuffer = Bytes.fromBase64(key); // decrypt - const decrypted = await window.textsecure.crypto.decryptProfile( - avatar, - keyBuffer - ); + const decrypted = decryptProfile(avatar, keyBuffer); // update the conversation avatar only if hash differs if (decrypted) { - const newAttributes = await window.Signal.Types.Conversation.maybeUpdateProfileAvatar( + const newAttributes = await Conversation.maybeUpdateProfileAvatar( this.attributes, decrypted, { @@ -4540,9 +4522,9 @@ export class ConversationModel extends window.Backbone return; } - const profileKeyBuffer = base64ToArrayBuffer(profileKey); - const accessKeyBuffer = await deriveAccessKey(profileKeyBuffer); - const accessKey = arrayBufferToBase64(accessKeyBuffer); + const profileKeyBuffer = Bytes.fromBase64(profileKey); + const accessKeyBuffer = deriveAccessKey(profileKeyBuffer); + const accessKey = Bytes.toBase64(accessKeyBuffer); this.set({ accessKey }); } diff --git a/ts/models/messages.ts b/ts/models/messages.ts index 19792d1cd..bbcba4831 100644 --- a/ts/models/messages.ts +++ b/ts/models/messages.ts @@ -41,8 +41,12 @@ import { getStickerPackStatus, } from '../types/Stickers'; import * as Stickers from '../types/Stickers'; +import * as Errors from '../types/errors'; +import * as EmbeddedContact from '../types/EmbeddedContact'; import { AttachmentType, isImage, isVideo } from '../types/Attachment'; +import * as Attachment from '../types/Attachment'; import { IMAGE_WEBP, stringToMIMEType } from '../types/MIME'; +import * as MIME from '../types/MIME'; import { ReadStatus } from '../messages/MessageReadStatus'; import { SendActionType, @@ -115,6 +119,8 @@ import { normalMessageSendJobQueue } from '../jobs/normalMessageSendJobQueue'; import { notificationService } from '../services/notifications'; import type { PreviewType as OutgoingPreviewType } from '../textsecure/SendMessage'; import * as log from '../logging/log'; +import * as Bytes from '../Bytes'; +import { computeHash } from '../Crypto'; /* eslint-disable camelcase */ /* eslint-disable more/no-then */ @@ -128,13 +134,7 @@ declare const _: typeof window._; window.Whisper = window.Whisper || {}; -const { - Message: TypedMessage, - Attachment, - MIME, - EmbeddedContact, - Errors, -} = window.Signal.Types; +const { Message: TypedMessage } = window.Signal.Types; const { deleteExternalMessageFiles, upgradeMessageSchema, @@ -142,7 +142,6 @@ const { const { getTextWithMentions, GoogleChrome } = window.Signal.Util; const { addStickerPackReference, getMessageBySender } = window.Signal.Data; -const { bytesFromString } = window.Signal.Crypto; export function isQuoteAMatch( message: MessageModel | null | undefined, @@ -1560,7 +1559,7 @@ export class MessageModel extends window.Backbone.Model { } async sendSyncMessageOnly( - dataMessage: ArrayBuffer, + dataMessage: Uint8Array, saveErrors?: (errors: Array) => void ): Promise { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -2691,9 +2690,7 @@ export class MessageModel extends window.Backbone.Model { downloadedAvatar ); - hash = await window.Signal.Types.Conversation.computeHash( - loadedAttachment.data - ); + hash = computeHash(loadedAttachment.data); } } catch (err) { log.info('handleDataMessage: group avatar download failed'); @@ -2719,7 +2716,7 @@ export class MessageModel extends window.Backbone.Model { let avatar = null; if (downloadedAvatar && avatarAttachment !== null) { - const onDiskAttachment = await window.Signal.Types.Attachment.migrateDataToFileSystem( + const onDiskAttachment = await Attachment.migrateDataToFileSystem( downloadedAvatar, { writeNewAttachmentData: @@ -3328,7 +3325,7 @@ window.Whisper.Message.getLongMessageAttachment = ({ }; } - const data = bytesFromString(body); + const data = Bytes.fromString(body); const attachment = { contentType: MIME.LONG_MESSAGE, fileName: `long-message-${now}.txt`, diff --git a/ts/services/calling.ts b/ts/services/calling.ts index d2b631602..3a21fd0fb 100644 --- a/ts/services/calling.ts +++ b/ts/services/calling.ts @@ -56,11 +56,7 @@ import { LocalizerType } from '../types/Util'; import { UUID } from '../types/UUID'; import { ConversationModel } from '../models/conversations'; import * as Bytes from '../Bytes'; -import { - uuidToArrayBuffer, - arrayBufferToUuid, - typedArrayToArrayBuffer, -} from '../Crypto'; +import { uuidToBytes, bytesToUuid } from '../Crypto'; import { dropNull, shallowDropNull } from '../util/dropNull'; import { getOwn } from '../util/getOwn'; import { isNormalNumber } from '../util/isNormalNumber'; @@ -270,7 +266,7 @@ export class CallingClass { return; } - RingRTC.setSelfUuid(Buffer.from(uuidToArrayBuffer(ourUuid))); + RingRTC.setSelfUuid(Buffer.from(uuidToBytes(ourUuid))); } async startCallingLobby( @@ -480,7 +476,7 @@ export class CallingClass { return getMembershipList(conversationId).map( member => new GroupMemberInfo( - Buffer.from(uuidToArrayBuffer(member.uuid)), + Buffer.from(uuidToBytes(member.uuid)), Buffer.from(member.uuidCiphertext) ) ); @@ -772,18 +768,16 @@ export class CallingClass { ): GroupCallPeekInfoType { return { uuids: peekInfo.joinedMembers.map(uuidBuffer => { - let uuid = arrayBufferToUuid(typedArrayToArrayBuffer(uuidBuffer)); + let uuid = bytesToUuid(uuidBuffer); if (!uuid) { log.error( - 'Calling.formatGroupCallPeekInfoForRedux: could not convert peek UUID ArrayBuffer to string; using fallback UUID' + 'Calling.formatGroupCallPeekInfoForRedux: could not convert peek UUID Uint8Array to string; using fallback UUID' ); uuid = '00000000-0000-0000-0000-000000000000'; } return uuid; }), - creatorUuid: - peekInfo.creator && - arrayBufferToUuid(typedArrayToArrayBuffer(peekInfo.creator)), + creatorUuid: peekInfo.creator && bytesToUuid(peekInfo.creator), eraId: peekInfo.eraId, maxDevices: peekInfo.maxDevices ?? Infinity, deviceCount: peekInfo.deviceCount, @@ -820,12 +814,10 @@ export class CallingClass { ? this.formatGroupCallPeekInfoForRedux(peekInfo) : undefined, remoteParticipants: remoteDeviceStates.map(remoteDeviceState => { - let uuid = arrayBufferToUuid( - typedArrayToArrayBuffer(remoteDeviceState.userId) - ); + let uuid = bytesToUuid(remoteDeviceState.userId); if (!uuid) { log.error( - 'Calling.formatGroupCallForRedux: could not convert remote participant UUID ArrayBuffer to string; using fallback UUID' + 'Calling.formatGroupCallForRedux: could not convert remote participant UUID Uint8Array to string; using fallback UUID' ); uuid = '00000000-0000-0000-0000-000000000000'; } @@ -1441,7 +1433,7 @@ export class CallingClass { } const sourceUuid = envelope.sourceUuid - ? uuidToArrayBuffer(envelope.sourceUuid) + ? uuidToBytes(envelope.sourceUuid) : null; const messageAgeSec = envelope.messageAgeSec ? envelope.messageAgeSec : 0; @@ -1531,7 +1523,7 @@ export class CallingClass { data: Uint8Array, urgency: CallMessageUrgency ): Promise { - const userId = arrayBufferToUuid(typedArrayToArrayBuffer(recipient)); + const userId = bytesToUuid(recipient); if (!userId) { log.error('handleSendCallMessage(): bad recipient UUID'); return false; @@ -1594,7 +1586,7 @@ export class CallingClass { const groupId = groupIdBytes.toString('base64'); - const ringerUuid = arrayBufferToUuid(typedArrayToArrayBuffer(ringerBytes)); + const ringerUuid = bytesToUuid(ringerBytes); if (!ringerUuid) { log.error('handleGroupCallRingUpdate(): ringerUuid was invalid'); return; @@ -1862,7 +1854,7 @@ export class CallingClass { url, httpMethod, headers, - body ? typedArrayToArrayBuffer(body) : undefined + body ); } catch (err) { if (err.code !== -1) { @@ -1925,7 +1917,10 @@ export class CallingClass { const isContactUnknown = !conversation.isFromOrAddedByTrustedContact(); return { - iceServer, + iceServer: { + ...iceServer, + urls: iceServer.urls.slice(), + }, hideIp: shouldRelayCalls || isContactUnknown, bandwidthMode: BandwidthMode.Normal, }; @@ -2005,9 +2000,7 @@ export class CallingClass { if (!peekInfo || !peekInfo.eraId || !peekInfo.creator) { return; } - const creatorUuid = arrayBufferToUuid( - typedArrayToArrayBuffer(peekInfo.creator) - ); + const creatorUuid = bytesToUuid(peekInfo.creator); if (!creatorUuid) { log.error('updateCallHistoryForGroupCall(): bad creator UUID'); return; diff --git a/ts/services/ourProfileKey.ts b/ts/services/ourProfileKey.ts index 234ea9919..f2eebd81d 100644 --- a/ts/services/ourProfileKey.ts +++ b/ts/services/ourProfileKey.ts @@ -7,7 +7,7 @@ import * as log from '../logging/log'; import { StorageInterface } from '../types/Storage.d'; export class OurProfileKeyService { - private getPromise: undefined | Promise; + private getPromise: undefined | Promise; private promisesBlockingGet: Array> = []; @@ -26,7 +26,7 @@ export class OurProfileKeyService { this.storage = storage; } - get(): Promise { + get(): Promise { if (this.getPromise) { log.info( 'Our profile key service: was already fetching. Piggybacking off of that' @@ -38,7 +38,7 @@ export class OurProfileKeyService { return this.getPromise; } - async set(newValue: undefined | ArrayBuffer): Promise { + async set(newValue: undefined | Uint8Array): Promise { log.info('Our profile key service: updating profile key'); assert(this.storage, 'OurProfileKeyService was not initialized'); if (newValue) { @@ -52,7 +52,7 @@ export class OurProfileKeyService { this.promisesBlockingGet.push(promise); } - private async doGet(): Promise { + private async doGet(): Promise { log.info( `Our profile key service: waiting for ${this.promisesBlockingGet.length} promises before fetching` ); @@ -66,13 +66,13 @@ export class OurProfileKeyService { log.info('Our profile key service: fetching profile key from storage'); const result = this.storage.get('profileKey'); - if (result === undefined || result instanceof ArrayBuffer) { + if (result === undefined || result instanceof Uint8Array) { return result; } assert( false, - 'Profile key in storage was defined, but not an ArrayBuffer. Returning undefined' + 'Profile key in storage was defined, but not an Uint8Array. Returning undefined' ); return undefined; } diff --git a/ts/services/senderCertificate.ts b/ts/services/senderCertificate.ts index b87ed8812..c11b44ce6 100644 --- a/ts/services/senderCertificate.ts +++ b/ts/services/senderCertificate.ts @@ -7,7 +7,6 @@ import { SerializedCertificateType, } from '../textsecure/OutgoingMessage'; import * as Bytes from '../Bytes'; -import { typedArrayToArrayBuffer } from '../Crypto'; import { assert } from '../util/assert'; import { missingCaseError } from '../util/missingCaseError'; import { normalizeNumber } from '../util/normalizeNumber'; @@ -177,7 +176,7 @@ export class SenderCertificateService { const serializedCertificate = { expires: expires - CLOCK_SKEW_THRESHOLD, - serialized: typedArrayToArrayBuffer(certificate), + serialized: certificate, }; await storage.put(modeToStorageKey(mode), serializedCertificate); diff --git a/ts/services/storage.ts b/ts/services/storage.ts index 08948a404..23a4fac6b 100644 --- a/ts/services/storage.ts +++ b/ts/services/storage.ts @@ -4,15 +4,14 @@ import { debounce, isNumber } from 'lodash'; import pMap from 'p-map'; -import Crypto from '../textsecure/Crypto'; import dataInterface from '../sql/Client'; import * as Bytes from '../Bytes'; import { - arrayBufferToBase64, - base64ToArrayBuffer, + getRandomBytes, deriveStorageItemKey, deriveStorageManifestKey, - typedArrayToArrayBuffer, + encryptProfile, + decryptProfile, } from '../Crypto'; import { mergeAccountRecord, @@ -44,9 +43,6 @@ import * as log from '../logging/log'; type IManifestRecordIdentifier = Proto.ManifestRecord.IIdentifier; -// TODO: remove once we move away from ArrayBuffers -const FIXMEU8 = Uint8Array; - const { eraseStorageServiceStateFromConversations, updateConversation, @@ -94,32 +90,32 @@ async function encryptRecord( const storageItem = new Proto.StorageItem(); const storageKeyBuffer = storageID - ? base64ToArrayBuffer(String(storageID)) + ? Bytes.fromBase64(String(storageID)) : generateStorageID(); const storageKeyBase64 = window.storage.get('storageKey'); if (!storageKeyBase64) { throw new Error('No storage key'); } - const storageKey = base64ToArrayBuffer(storageKeyBase64); - const storageItemKey = await deriveStorageItemKey( + const storageKey = Bytes.fromBase64(storageKeyBase64); + const storageItemKey = deriveStorageItemKey( storageKey, - arrayBufferToBase64(storageKeyBuffer) + Bytes.toBase64(storageKeyBuffer) ); - const encryptedRecord = await Crypto.encryptProfile( - typedArrayToArrayBuffer(Proto.StorageRecord.encode(storageRecord).finish()), + const encryptedRecord = encryptProfile( + Proto.StorageRecord.encode(storageRecord).finish(), storageItemKey ); - storageItem.key = new FIXMEU8(storageKeyBuffer); - storageItem.value = new FIXMEU8(encryptedRecord); + storageItem.key = storageKeyBuffer; + storageItem.value = encryptedRecord; return storageItem; } -function generateStorageID(): ArrayBuffer { - return Crypto.getRandomBytes(16); +function generateStorageID(): Uint8Array { + return getRandomBytes(16); } type GeneratedManifestType = { @@ -127,7 +123,7 @@ type GeneratedManifestType = { conversation: ConversationModel; storageID: string | undefined; }>; - deleteKeys: Array; + deleteKeys: Array; newItems: Set; storageManifest: Proto.IStorageManifest; }; @@ -149,7 +145,7 @@ async function generateManifest( const conversationsToUpdate = []; const insertKeys: Array = []; - const deleteKeys: Array = []; + const deleteKeys: Array = []; const manifestRecordKeys: Set = new Set(); const newItems: Set = new Set(); @@ -202,7 +198,7 @@ async function generateManifest( !currentStorageID; const storageID = isNewItem - ? arrayBufferToBase64(generateStorageID()) + ? Bytes.toBase64(generateStorageID()) : currentStorageID; let storageItem; @@ -243,7 +239,7 @@ async function generateManifest( 'storageService.generateManifest: deleting key', redactStorageID(oldStorageID) ); - deleteKeys.push(base64ToArrayBuffer(oldStorageID)); + deleteKeys.push(Bytes.fromBase64(oldStorageID)); } conversationsToUpdate.push({ @@ -323,7 +319,7 @@ async function generateManifest( // Ensure all deletes are not present in the manifest const hasDeleteKey = deleteKeys.find( - key => arrayBufferToBase64(key) === storageID + key => Bytes.toBase64(key) === storageID ); if (hasDeleteKey) { log.info( @@ -400,7 +396,7 @@ async function generateManifest( if (deleteKeys.length !== pendingDeletes.size) { const localDeletes = deleteKeys.map(key => - redactStorageID(arrayBufferToBase64(key)) + redactStorageID(Bytes.toBase64(key)) ); const remoteDeletes: Array = []; pendingDeletes.forEach(id => remoteDeletes.push(redactStorageID(id))); @@ -417,7 +413,7 @@ async function generateManifest( throw new Error('invalid write insert items length do not match'); } deleteKeys.forEach(key => { - const storageID = arrayBufferToBase64(key); + const storageID = Bytes.toBase64(key); if (!pendingDeletes.has(storageID)) { throw new Error( 'invalid write delete key missing from pending deletes' @@ -441,21 +437,16 @@ async function generateManifest( if (!storageKeyBase64) { throw new Error('No storage key'); } - const storageKey = base64ToArrayBuffer(storageKeyBase64); - const storageManifestKey = await deriveStorageManifestKey( - storageKey, - version - ); - const encryptedManifest = await Crypto.encryptProfile( - typedArrayToArrayBuffer( - Proto.ManifestRecord.encode(manifestRecord).finish() - ), + const storageKey = Bytes.fromBase64(storageKeyBase64); + const storageManifestKey = deriveStorageManifestKey(storageKey, version); + const encryptedManifest = encryptProfile( + Proto.ManifestRecord.encode(manifestRecord).finish(), storageManifestKey ); const storageManifest = new Proto.StorageManifest(); storageManifest.version = version; - storageManifest.value = new FIXMEU8(encryptedManifest); + storageManifest.value = encryptedManifest; return { conversationsToUpdate, @@ -494,13 +485,11 @@ async function uploadManifest( const writeOperation = new Proto.WriteOperation(); writeOperation.manifest = storageManifest; writeOperation.insertItem = Array.from(newItems); - writeOperation.deleteKey = deleteKeys.map(key => new FIXMEU8(key)); + writeOperation.deleteKey = deleteKeys; log.info('storageService.uploadManifest: uploading...', version); await window.textsecure.messaging.modifyStorageRecords( - typedArrayToArrayBuffer( - Proto.WriteOperation.encode(writeOperation).finish() - ), + Proto.WriteOperation.encode(writeOperation).finish(), { credentials, } @@ -626,19 +615,16 @@ async function decryptManifest( if (!storageKeyBase64) { throw new Error('No storage key'); } - const storageKey = base64ToArrayBuffer(storageKeyBase64); - const storageManifestKey = await deriveStorageManifestKey( + const storageKey = Bytes.fromBase64(storageKeyBase64); + const storageManifestKey = deriveStorageManifestKey( storageKey, normalizeNumber(version ?? 0) ); strictAssert(value, 'StorageManifest has no value field'); - const decryptedManifest = await Crypto.decryptProfile( - typedArrayToArrayBuffer(value), - storageManifestKey - ); + const decryptedManifest = decryptProfile(value, storageManifestKey); - return Proto.ManifestRecord.decode(new FIXMEU8(decryptedManifest)); + return Proto.ManifestRecord.decode(decryptedManifest); } async function fetchManifest( @@ -660,9 +646,7 @@ async function fetchManifest( greaterThanVersion: manifestVersion, } ); - const encryptedManifest = Proto.StorageManifest.decode( - new FIXMEU8(manifestBinary) - ); + const encryptedManifest = Proto.StorageManifest.decode(manifestBinary); // if we don't get a value we're assuming that there's no newer manifest if (!encryptedManifest.value || !encryptedManifest.version) { @@ -855,7 +839,7 @@ async function processRemoteRecords( if (!storageKeyBase64) { throw new Error('No storage key'); } - const storageKey = base64ToArrayBuffer(storageKeyBase64); + const storageKey = Bytes.fromBase64(storageKeyBase64); log.info( 'storageService.processRemoteRecords: remote only keys', @@ -869,15 +853,13 @@ async function processRemoteRecords( const credentials = window.storage.get('storageCredentials'); const storageItemsBuffer = await window.textsecure.messaging.getStorageRecords( - typedArrayToArrayBuffer(Proto.ReadOperation.encode(readOperation).finish()), + Proto.ReadOperation.encode(readOperation).finish(), { credentials, } ); - const storageItems = Proto.StorageItems.decode( - new FIXMEU8(storageItemsBuffer) - ); + const storageItems = Proto.StorageItems.decode(storageItemsBuffer); if (!storageItems.items) { log.info('storageService.processRemoteRecords: No storage items retrieved'); @@ -903,15 +885,12 @@ async function processRemoteRecords( const base64ItemID = Bytes.toBase64(key); - const storageItemKey = await deriveStorageItemKey( - storageKey, - base64ItemID - ); + const storageItemKey = deriveStorageItemKey(storageKey, base64ItemID); let storageItemPlaintext; try { - storageItemPlaintext = await Crypto.decryptProfile( - typedArrayToArrayBuffer(storageItemCiphertext), + storageItemPlaintext = decryptProfile( + storageItemCiphertext, storageItemKey ); } catch (err) { @@ -922,9 +901,7 @@ async function processRemoteRecords( throw err; } - const storageRecord = Proto.StorageRecord.decode( - new FIXMEU8(storageItemPlaintext) - ); + const storageRecord = Proto.StorageRecord.decode(storageItemPlaintext); const remoteRecord = remoteOnlyRecords.get(base64ItemID); if (!remoteRecord) { diff --git a/ts/services/storageRecordOps.ts b/ts/services/storageRecordOps.ts index 8a987e874..cdc5f3a1c 100644 --- a/ts/services/storageRecordOps.ts +++ b/ts/services/storageRecordOps.ts @@ -4,7 +4,7 @@ import { isEqual, isNumber } from 'lodash'; import Long from 'long'; -import { deriveMasterKeyFromGroupV1, typedArrayToArrayBuffer } from '../Crypto'; +import { deriveMasterKeyFromGroupV1 } from '../Crypto'; import * as Bytes from '../Bytes'; import dataInterface from '../sql/Client'; import { @@ -43,9 +43,6 @@ import * as log from '../logging/log'; const { updateConversation } = dataInterface; -// TODO: remove once we move away from ArrayBuffers -const FIXMEU8 = Uint8Array; - type RecordClass = | Proto.IAccountRecord | Proto.IContactRecord @@ -134,7 +131,7 @@ export async function toContactRecord( ? await window.textsecure.storage.protocol.loadIdentityKey(maybeUuid) : undefined; if (identityKey) { - contactRecord.identityKey = new FIXMEU8(identityKey); + contactRecord.identityKey = identityKey; } const verified = conversation.get('verified'); if (verified) { @@ -397,13 +394,13 @@ function doRecordsConflict( const localValue = localRecord[key]; const remoteValue = remoteRecord[key]; - // Sometimes we have a ByteBuffer and an ArrayBuffer, this ensures that we + // Sometimes we have a ByteBuffer and an Uint8Array, this ensures that we // are comparing them both equally by converting them into base64 string. if (localValue instanceof Uint8Array) { const areEqual = Bytes.areEqual(localValue, remoteValue); if (!areEqual) { log.info( - 'storageService.doRecordsConflict: Conflict found for ArrayBuffer', + 'storageService.doRecordsConflict: Conflict found for Uint8Array', key, idForLogging ); @@ -525,10 +522,8 @@ export async function mergeGroupV1Record( // It's possible this group was migrated to a GV2 if so we attempt to // retrieve the master key and find the conversation locally. If we // are successful then we continue setting and applying state. - const masterKeyBuffer = await deriveMasterKeyFromGroupV1( - typedArrayToArrayBuffer(groupV1Record.id) - ); - const fields = deriveGroupFields(new FIXMEU8(masterKeyBuffer)); + const masterKeyBuffer = deriveMasterKeyFromGroupV1(groupV1Record.id); + const fields = deriveGroupFields(masterKeyBuffer); const derivedGroupV2Id = Bytes.toBase64(fields.id); log.info( @@ -771,9 +766,7 @@ export async function mergeContactRecord( const storageServiceVerified = contactRecord.identityState || 0; if (verified !== storageServiceVerified) { const verifiedOptions = { - key: contactRecord.identityKey - ? typedArrayToArrayBuffer(contactRecord.identityKey) - : undefined, + key: contactRecord.identityKey, viaStorageServiceSync: true, }; const STATE_ENUM = Proto.ContactRecord.IdentityState; @@ -900,7 +893,7 @@ export async function mergeAccountRecord( window.storage.put('phoneNumberDiscoverability', discoverability); if (profileKey) { - ourProfileKeyService.set(typedArrayToArrayBuffer(profileKey)); + ourProfileKeyService.set(profileKey); } if (pinnedConversations) { diff --git a/ts/services/writeProfile.ts b/ts/services/writeProfile.ts index 8693a5537..1a34042b2 100644 --- a/ts/services/writeProfile.ts +++ b/ts/services/writeProfile.ts @@ -10,7 +10,7 @@ import { handleMessageSend } from '../util/handleMessageSend'; export async function writeProfile( conversation: ConversationType, - avatarBuffer?: ArrayBuffer + avatarBuffer?: Uint8Array ): Promise { // Before we write anything we request the user's profile so that we can // have an up-to-date paymentAddress to be able to include it when we write diff --git a/ts/sql/Client.ts b/ts/sql/Client.ts index 7d8afe0c0..a42b1b012 100644 --- a/ts/sql/Client.ts +++ b/ts/sql/Client.ts @@ -24,7 +24,7 @@ import { uniq, } from 'lodash'; -import { arrayBufferToBase64, base64ToArrayBuffer } from '../Crypto'; +import * as Bytes from '../Bytes'; import { CURRENT_SCHEMA_VERSION } from '../../js/modules/types/message'; import { createBatcher } from '../util/batcher'; import { assert } from '../util/assert'; @@ -577,7 +577,7 @@ function makeChannel(fnName: string) { }; } -function keysToArrayBuffer(keys: Array, data: any) { +function keysToBytes(keys: Array, data: any) { const updated = cloneDeep(data); const max = keys.length; @@ -586,14 +586,14 @@ function keysToArrayBuffer(keys: Array, data: any) { const value = get(data, key); if (value) { - set(updated, key, base64ToArrayBuffer(value)); + set(updated, key, Bytes.fromBase64(value)); } } return updated; } -function keysFromArrayBuffer(keys: Array, data: any) { +function keysFromBytes(keys: Array, data: any) { const updated = cloneDeep(data); const max = keys.length; @@ -602,7 +602,7 @@ function keysFromArrayBuffer(keys: Array, data: any) { const value = get(data, key); if (value) { - set(updated, key, arrayBufferToBase64(value)); + set(updated, key, Bytes.toBase64(value)); } } @@ -637,18 +637,16 @@ async function removeIndexedDBFiles() { const IDENTITY_KEY_KEYS = ['publicKey']; async function createOrUpdateIdentityKey(data: IdentityKeyType) { - const updated = keysFromArrayBuffer(IDENTITY_KEY_KEYS, data); + const updated = keysFromBytes(IDENTITY_KEY_KEYS, data); await channels.createOrUpdateIdentityKey(updated); } async function getIdentityKeyById(id: IdentityKeyIdType) { const data = await channels.getIdentityKeyById(id); - return keysToArrayBuffer(IDENTITY_KEY_KEYS, data); + return keysToBytes(IDENTITY_KEY_KEYS, data); } async function bulkAddIdentityKeys(array: Array) { - const updated = map(array, data => - keysFromArrayBuffer(IDENTITY_KEY_KEYS, data) - ); + const updated = map(array, data => keysFromBytes(IDENTITY_KEY_KEYS, data)); await channels.bulkAddIdentityKeys(updated); } async function removeIdentityKeyById(id: IdentityKeyIdType) { @@ -660,22 +658,22 @@ async function removeAllIdentityKeys() { async function getAllIdentityKeys() { const keys = await channels.getAllIdentityKeys(); - return keys.map(key => keysToArrayBuffer(IDENTITY_KEY_KEYS, key)); + return keys.map(key => keysToBytes(IDENTITY_KEY_KEYS, key)); } // Pre Keys async function createOrUpdatePreKey(data: PreKeyType) { - const updated = keysFromArrayBuffer(PRE_KEY_KEYS, data); + const updated = keysFromBytes(PRE_KEY_KEYS, data); await channels.createOrUpdatePreKey(updated); } async function getPreKeyById(id: PreKeyIdType) { const data = await channels.getPreKeyById(id); - return keysToArrayBuffer(PRE_KEY_KEYS, data); + return keysToBytes(PRE_KEY_KEYS, data); } async function bulkAddPreKeys(array: Array) { - const updated = map(array, data => keysFromArrayBuffer(PRE_KEY_KEYS, data)); + const updated = map(array, data => keysFromBytes(PRE_KEY_KEYS, data)); await channels.bulkAddPreKeys(updated); } async function removePreKeyById(id: PreKeyIdType) { @@ -687,30 +685,28 @@ async function removeAllPreKeys() { async function getAllPreKeys() { const keys = await channels.getAllPreKeys(); - return keys.map(key => keysToArrayBuffer(PRE_KEY_KEYS, key)); + return keys.map(key => keysToBytes(PRE_KEY_KEYS, key)); } // Signed Pre Keys const PRE_KEY_KEYS = ['privateKey', 'publicKey']; async function createOrUpdateSignedPreKey(data: SignedPreKeyType) { - const updated = keysFromArrayBuffer(PRE_KEY_KEYS, data); + const updated = keysFromBytes(PRE_KEY_KEYS, data); await channels.createOrUpdateSignedPreKey(updated); } async function getSignedPreKeyById(id: SignedPreKeyIdType) { const data = await channels.getSignedPreKeyById(id); - return keysToArrayBuffer(PRE_KEY_KEYS, data); + return keysToBytes(PRE_KEY_KEYS, data); } async function getAllSignedPreKeys() { const keys = await channels.getAllSignedPreKeys(); - return keys.map((key: SignedPreKeyType) => - keysToArrayBuffer(PRE_KEY_KEYS, key) - ); + return keys.map((key: SignedPreKeyType) => keysToBytes(PRE_KEY_KEYS, key)); } async function bulkAddSignedPreKeys(array: Array) { - const updated = map(array, data => keysFromArrayBuffer(PRE_KEY_KEYS, data)); + const updated = map(array, data => keysFromBytes(PRE_KEY_KEYS, data)); await channels.bulkAddSignedPreKeys(updated); } async function removeSignedPreKeyById(id: SignedPreKeyIdType) { @@ -736,7 +732,7 @@ async function createOrUpdateItem(data: ItemType) { } const keys = ITEM_KEYS[id]; - const updated = Array.isArray(keys) ? keysFromArrayBuffer(keys, data) : data; + const updated = Array.isArray(keys) ? keysFromBytes(keys, data) : data; await channels.createOrUpdateItem(updated); } @@ -746,7 +742,7 @@ async function getItemById( const keys = ITEM_KEYS[id]; const data = await channels.getItemById(id); - return Array.isArray(keys) ? keysToArrayBuffer(keys, data) : data; + return Array.isArray(keys) ? keysToBytes(keys, data) : data; } async function getAllItems() { const items = await channels.getAllItems(); @@ -760,7 +756,7 @@ async function getAllItems() { const keys = ITEM_KEYS[key]; const deserializedValue = Array.isArray(keys) - ? keysToArrayBuffer(keys, { value }).value + ? keysToBytes(keys, { value }).value : value; result[key] = deserializedValue; diff --git a/ts/sql/Interface.ts b/ts/sql/Interface.ts index 52f1a34e8..dc9b0c9c9 100644 --- a/ts/sql/Interface.ts +++ b/ts/sql/Interface.ts @@ -65,7 +65,7 @@ export type IdentityKeyType = { firstUse: boolean; id: UUIDStringType | `conversation:${UUIDStringType}`; nonblockingApproval: boolean; - publicKey: ArrayBuffer; + publicKey: Uint8Array; timestamp: number; verified: number; }; @@ -85,8 +85,8 @@ export type PreKeyType = { id: `${UUIDStringType}:${number}`; keyId: number; ourUuid: UUIDStringType; - privateKey: ArrayBuffer; - publicKey: ArrayBuffer; + privateKey: Uint8Array; + publicKey: Uint8Array; }; export type PreKeyIdType = PreKeyType['id']; export type SearchResultMessageType = { @@ -101,7 +101,7 @@ export type ClientSearchResultMessageType = MessageType & { export type SentProtoType = { contentHint: number; - proto: Buffer; + proto: Uint8Array; timestamp: number; }; export type SentProtoWithMessageIdsType = SentProtoType & { @@ -128,7 +128,7 @@ export type SenderKeyType = { senderId: string; distributionId: string; // Raw data to serialize/deserialize into signal-client SenderKeyRecord - data: Buffer; + data: Uint8Array; lastUpdatedDate: number; }; export type SenderKeyIdType = SenderKeyType['id']; @@ -149,8 +149,8 @@ export type SignedPreKeyType = { ourUuid: UUIDStringType; id: `${UUIDStringType}:${number}`; keyId: number; - privateKey: ArrayBuffer; - publicKey: ArrayBuffer; + privateKey: Uint8Array; + publicKey: Uint8Array; }; export type SignedPreKeyIdType = SignedPreKeyType['id']; diff --git a/ts/sql/cleanDataForIpc.ts b/ts/sql/cleanDataForIpc.ts index 8b3a38294..0afaa793d 100644 --- a/ts/sql/cleanDataForIpc.ts +++ b/ts/sql/cleanDataForIpc.ts @@ -37,7 +37,7 @@ type CleanedDataValue = | boolean | null | undefined - | Buffer + | Uint8Array | CleanedObject | CleanedArray; /* eslint-disable no-restricted-syntax */ @@ -111,7 +111,7 @@ function cleanDataInner( return undefined; } - if (data instanceof Buffer) { + if (data instanceof Uint8Array) { return data; } diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index d89e787e6..f1919ba58 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -255,7 +255,7 @@ export enum OneTimeModalState { } type ComposerGroupCreationState = { - groupAvatar: undefined | ArrayBuffer; + groupAvatar: undefined | Uint8Array; groupName: string; groupExpireTimer: number; maximumGroupSizeModalState: OneTimeModalState; @@ -632,7 +632,7 @@ export type ShowArchivedConversationsActionType = { }; type SetComposeGroupAvatarActionType = { type: 'SET_COMPOSE_GROUP_AVATAR'; - payload: { groupAvatar: undefined | ArrayBuffer }; + payload: { groupAvatar: undefined | Uint8Array }; }; type SetComposeGroupNameActionType = { type: 'SET_COMPOSE_GROUP_NAME'; @@ -931,7 +931,7 @@ function saveAvatarToDisk( ): ThunkAction { return async (dispatch, getState) => { if (!avatarData.buffer) { - throw new Error('No avatar ArrayBuffer provided'); + throw new Error('No avatar Uint8Array provided'); } strictAssert(conversationId, 'conversationId not provided'); @@ -966,7 +966,7 @@ function saveAvatarToDisk( function myProfileChanged( profileData: ProfileDataType, - avatarBuffer?: ArrayBuffer + avatarBuffer?: Uint8Array ): ThunkAction< void, RootStateType, @@ -1138,7 +1138,7 @@ function composeSaveAvatarToDisk( ): ThunkAction { return async dispatch => { if (!avatarData.buffer) { - throw new Error('No avatar ArrayBuffer provided'); + throw new Error('No avatar Uint8Array provided'); } const imagePath = await window.Signal.Migrations.writeNewAvatarData( @@ -1601,7 +1601,7 @@ function scrollToMessage( } function setComposeGroupAvatar( - groupAvatar: undefined | ArrayBuffer + groupAvatar: undefined | Uint8Array ): SetComposeGroupAvatarActionType { return { type: 'SET_COMPOSE_GROUP_AVATAR', @@ -2884,7 +2884,7 @@ export function reducer( let recommendedGroupSizeModalState: OneTimeModalState; let maximumGroupSizeModalState: OneTimeModalState; let groupName: string; - let groupAvatar: undefined | ArrayBuffer; + let groupAvatar: undefined | Uint8Array; let groupExpireTimer: number; let userAvatarData = getDefaultAvatars(true); diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 576efbe21..ea11ec40b 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -541,7 +541,7 @@ const getGroupCreationComposerState = createSelector( composerState ): { groupName: string; - groupAvatar: undefined | ArrayBuffer; + groupAvatar: undefined | Uint8Array; groupExpireTimer: number; selectedConversationIds: Array; } => { @@ -566,7 +566,7 @@ const getGroupCreationComposerState = createSelector( export const getComposeGroupAvatar = createSelector( getGroupCreationComposerState, - (composerState): undefined | ArrayBuffer => composerState.groupAvatar + (composerState): undefined | Uint8Array => composerState.groupAvatar ); export const getComposeGroupName = createSelector( diff --git a/ts/state/smart/ConversationDetails.tsx b/ts/state/smart/ConversationDetails.tsx index af6e8e175..f83c160ba 100644 --- a/ts/state/smart/ConversationDetails.tsx +++ b/ts/state/smart/ConversationDetails.tsx @@ -36,7 +36,7 @@ export type SmartConversationDetailsProps = { ) => void; updateGroupAttributes: ( _: Readonly<{ - avatar?: undefined | ArrayBuffer; + avatar?: undefined | Uint8Array; title?: string; }> ) => Promise; diff --git a/ts/test-both/ContactsParser_test.ts b/ts/test-both/ContactsParser_test.ts index 883fcaa5a..83a70603e 100644 --- a/ts/test-both/ContactsParser_test.ts +++ b/ts/test-both/ContactsParser_test.ts @@ -5,7 +5,6 @@ import { assert } from 'chai'; import { Writer } from 'protobufjs'; import * as Bytes from '../Bytes'; -import { typedArrayToArrayBuffer } from '../Crypto'; import { SignalService as Proto } from '../protobuf'; import { ContactBuffer, GroupBuffer } from '../textsecure/ContactsParser'; @@ -19,7 +18,7 @@ describe('ContactsParser', () => { } describe('ContactBuffer', () => { - function getTestBuffer(): ArrayBuffer { + function getTestBuffer(): Uint8Array { const avatarBuffer = generateAvatar(); const contactInfoBuffer = Proto.ContactDetails.encode({ @@ -39,12 +38,12 @@ describe('ContactsParser', () => { chunks.push(avatarBuffer); } - return typedArrayToArrayBuffer(Bytes.concatenate(chunks)); + return Bytes.concatenate(chunks); } it('parses an array buffer of contacts', () => { - const arrayBuffer = getTestBuffer(); - const contactBuffer = new ContactBuffer(arrayBuffer); + const bytes = getTestBuffer(); + const contactBuffer = new ContactBuffer(bytes); let contact = contactBuffer.next(); let count = 0; while (contact !== undefined) { @@ -59,7 +58,7 @@ describe('ContactsParser', () => { assert.strictEqual(contact.avatar?.length, 255); assert.strictEqual(contact.avatar?.data.byteLength, 255); const avatarBytes = new Uint8Array( - contact.avatar?.data || new ArrayBuffer(0) + contact.avatar?.data || new Uint8Array(0) ); for (let j = 0; j < 255; j += 1) { assert.strictEqual(avatarBytes[j], j); @@ -71,7 +70,7 @@ describe('ContactsParser', () => { }); describe('GroupBuffer', () => { - function getTestBuffer(): ArrayBuffer { + function getTestBuffer(): Uint8Array { const avatarBuffer = generateAvatar(); const groupInfoBuffer = Proto.GroupDetails.encode({ @@ -91,12 +90,12 @@ describe('ContactsParser', () => { chunks.push(avatarBuffer); } - return typedArrayToArrayBuffer(Bytes.concatenate(chunks)); + return Bytes.concatenate(chunks); } it('parses an array buffer of groups', () => { - const arrayBuffer = getTestBuffer(); - const groupBuffer = new GroupBuffer(arrayBuffer); + const bytes = getTestBuffer(); + const groupBuffer = new GroupBuffer(bytes); let group = groupBuffer.next(); let count = 0; while (group !== undefined) { @@ -113,7 +112,7 @@ describe('ContactsParser', () => { assert.strictEqual(group.avatar?.length, 255); assert.strictEqual(group.avatar?.data.byteLength, 255); const avatarBytes = new Uint8Array( - group.avatar?.data || new ArrayBuffer(0) + group.avatar?.data || new Uint8Array(0) ); for (let j = 0; j < 255; j += 1) { assert.strictEqual(avatarBytes[j], j); diff --git a/ts/test-both/services/ourProfileKey_test.ts b/ts/test-both/services/ourProfileKey_test.ts index 5d460a618..6252106bc 100644 --- a/ts/test-both/services/ourProfileKey_test.ts +++ b/ts/test-both/services/ourProfileKey_test.ts @@ -6,6 +6,7 @@ import * as sinon from 'sinon'; import { noop } from 'lodash'; import { sleep } from '../../util/sleep'; +import { constantTimeEqual } from '../../Crypto'; import { OurProfileKeyService } from '../../services/ourProfileKey'; describe('"our profile key" service', () => { @@ -18,14 +19,17 @@ describe('"our profile key" service', () => { describe('get', () => { it("fetches the key from storage if it's there", async () => { - const fakeProfileKey = new ArrayBuffer(2); + const fakeProfileKey = new Uint8Array(2); const fakeStorage = createFakeStorage(); fakeStorage.get.withArgs('profileKey').returns(fakeProfileKey); const service = new OurProfileKeyService(); service.initialize(fakeStorage); - assert.strictEqual(await service.get(), fakeProfileKey); + const profileKey = await service.get(); + assert.isTrue( + profileKey && constantTimeEqual(profileKey, fakeProfileKey) + ); }); it('resolves with undefined if the key is not in storage', async () => { @@ -39,7 +43,7 @@ describe('"our profile key" service', () => { let onReadyCallback = noop; const fakeStorage = { ...createFakeStorage(), - get: sinon.stub().returns(new ArrayBuffer(2)), + get: sinon.stub().returns(new Uint8Array(2)), onready: sinon.stub().callsFake(callback => { onReadyCallback = callback; }), @@ -106,7 +110,7 @@ describe('"our profile key" service', () => { it("if there are blocking promises, doesn't grab the profile key from storage more than once (in other words, subsequent calls piggyback)", async () => { const fakeStorage = createFakeStorage(); - fakeStorage.get.returns(new ArrayBuffer(2)); + fakeStorage.get.returns(new Uint8Array(2)); const service = new OurProfileKeyService(); service.initialize(fakeStorage); @@ -153,7 +157,7 @@ describe('"our profile key" service', () => { describe('set', () => { it('updates the key in storage', async () => { - const fakeProfileKey = new ArrayBuffer(2); + const fakeProfileKey = new Uint8Array(2); const fakeStorage = createFakeStorage(); const service = new OurProfileKeyService(); diff --git a/ts/test-both/sql/cleanDataForIpc_test.ts b/ts/test-both/sql/cleanDataForIpc_test.ts index 9be0bd1a3..47614ee07 100644 --- a/ts/test-both/sql/cleanDataForIpc_test.ts +++ b/ts/test-both/sql/cleanDataForIpc_test.ts @@ -62,7 +62,7 @@ describe('cleanDataForIpc', () => { }); it('keeps Buffers in a field', () => { - const buffer = Buffer.from('AABBCC', 'hex'); + const buffer = new Uint8Array([0xaa, 0xbb, 0xcc]); assert.deepEqual(cleanDataForIpc(buffer), { cleaned: buffer, @@ -85,11 +85,6 @@ describe('cleanDataForIpc', () => { }); it('converts other iterables to arrays', () => { - assert.deepEqual(cleanDataForIpc(new Uint8Array([1, 2, 3])), { - cleaned: [1, 2, 3], - pathsChanged: ['root'], - }); - assert.deepEqual(cleanDataForIpc(new Float32Array([1, 2, 3])), { cleaned: [1, 2, 3], pathsChanged: ['root'], diff --git a/ts/test-both/state/selectors/conversations_test.ts b/ts/test-both/state/selectors/conversations_test.ts index d82c916f6..e323e8921 100644 --- a/ts/test-both/state/selectors/conversations_test.ts +++ b/ts/test-both/state/selectors/conversations_test.ts @@ -1680,14 +1680,11 @@ describe('both/state/selectors/conversations', () => { ...getEmptyState(), composer: { ...defaultSetGroupMetadataComposerState, - groupAvatar: new Uint8Array([1, 2, 3]).buffer, + groupAvatar: new Uint8Array([1, 2, 3]), }, }, }; - assert.deepEqual( - getComposeGroupAvatar(state), - new Uint8Array([1, 2, 3]).buffer - ); + assert.deepEqual(getComposeGroupAvatar(state), new Uint8Array([1, 2, 3])); }); }); diff --git a/ts/test-both/types/SchemaVersion_test.ts b/ts/test-both/types/SchemaVersion_test.ts new file mode 100644 index 000000000..33d2717fc --- /dev/null +++ b/ts/test-both/types/SchemaVersion_test.ts @@ -0,0 +1,22 @@ +// Copyright 2018-2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { assert } from 'chai'; + +import { isValid } from '../../types/SchemaVersion'; + +describe('SchemaVersion', () => { + describe('isValid', () => { + it('should return true for positive integers', () => { + assert.isTrue(isValid(0)); + assert.isTrue(isValid(1)); + assert.isTrue(isValid(2)); + }); + + it('should return false for any other value', () => { + assert.isFalse(isValid(null)); + assert.isFalse(isValid(-1)); + assert.isFalse(isValid('')); + }); + }); +}); diff --git a/ts/test-both/util/sessionTranslation_test.ts b/ts/test-both/util/sessionTranslation_test.ts index 7ab1fb27b..1b5208424 100644 --- a/ts/test-both/util/sessionTranslation_test.ts +++ b/ts/test-both/util/sessionTranslation_test.ts @@ -5,11 +5,11 @@ import { assert } from 'chai'; +import * as Bytes from '../../Bytes'; import { LocalUserDataType, sessionRecordToProtobuf, } from '../../util/sessionTranslation'; -import { base64ToArrayBuffer } from '../../Crypto'; const getRecordCopy = (record: any): any => JSON.parse(JSON.stringify(record)); @@ -18,7 +18,7 @@ describe('sessionTranslation', () => { beforeEach(() => { ourData = { - identityKeyPublic: base64ToArrayBuffer( + identityKeyPublic: Bytes.fromBase64( 'Baioqfzc/5JD6b+GNqapPouf6eHK7xr9ynLJHnvl+444' ), registrationId: 3554, diff --git a/ts/test-electron/Crypto_test.ts b/ts/test-electron/Crypto_test.ts index 9b45b0941..fcc7a6a9d 100644 --- a/ts/test-electron/Crypto_test.ts +++ b/ts/test-electron/Crypto_test.ts @@ -3,141 +3,160 @@ import { assert } from 'chai'; +import * as Bytes from '../Bytes'; import * as Curve from '../Curve'; -import * as Crypto from '../Crypto'; -import TSCrypto, { PaddedLengths } from '../textsecure/Crypto'; +import { + PaddedLengths, + encryptProfileItemWithPadding, + decryptProfileName, + encryptProfile, + decryptProfile, + getRandomBytes, + constantTimeEqual, + generateRegistrationId, + deriveSecrets, + encryptDeviceName, + decryptDeviceName, + deriveAccessKey, + getAccessKeyVerifier, + verifyAccessKey, + deriveMasterKeyFromGroupV1, + encryptSymmetric, + decryptSymmetric, + hmacSha256, + verifyHmacSha256, + uuidToBytes, + bytesToUuid, +} from '../Crypto'; describe('Crypto', () => { describe('encrypting and decrypting profile data', () => { const NAME_PADDED_LENGTH = 53; describe('encrypting and decrypting profile names', () => { - it('pads, encrypts, decrypts, and unpads a short string', async () => { + it('pads, encrypts, decrypts, and unpads a short string', () => { const name = 'Alice'; - const buffer = Crypto.bytesFromString(name); - const key = Crypto.getRandomBytes(32); + const buffer = Bytes.fromString(name); + const key = getRandomBytes(32); - const encrypted = await TSCrypto.encryptProfileItemWithPadding( + const encrypted = encryptProfileItemWithPadding( buffer, key, PaddedLengths.Name ); assert.equal(encrypted.byteLength, NAME_PADDED_LENGTH + 16 + 12); - const { given, family } = await TSCrypto.decryptProfileName( - Crypto.arrayBufferToBase64(encrypted), + const { given, family } = decryptProfileName( + Bytes.toBase64(encrypted), key ); assert.strictEqual(family, null); - assert.strictEqual(Crypto.stringFromBytes(given), name); + assert.strictEqual(Bytes.toString(given), name); }); - it('handles a given name of the max, 53 characters', async () => { + it('handles a given name of the max, 53 characters', () => { const name = '01234567890123456789012345678901234567890123456789123'; - const buffer = Crypto.bytesFromString(name); - const key = Crypto.getRandomBytes(32); + const buffer = Bytes.fromString(name); + const key = getRandomBytes(32); - const encrypted = await TSCrypto.encryptProfileItemWithPadding( + const encrypted = encryptProfileItemWithPadding( buffer, key, PaddedLengths.Name ); assert.equal(encrypted.byteLength, NAME_PADDED_LENGTH + 16 + 12); - const { given, family } = await TSCrypto.decryptProfileName( - Crypto.arrayBufferToBase64(encrypted), + const { given, family } = decryptProfileName( + Bytes.toBase64(encrypted), key ); - assert.strictEqual(Crypto.stringFromBytes(given), name); + assert.strictEqual(Bytes.toString(given), name); assert.strictEqual(family, null); }); - it('handles family/given name of the max, 53 characters', async () => { + it('handles family/given name of the max, 53 characters', () => { const name = '01234567890123456789\u000001234567890123456789012345678912'; - const buffer = Crypto.bytesFromString(name); - const key = Crypto.getRandomBytes(32); + const buffer = Bytes.fromString(name); + const key = getRandomBytes(32); - const encrypted = await TSCrypto.encryptProfileItemWithPadding( + const encrypted = encryptProfileItemWithPadding( buffer, key, PaddedLengths.Name ); assert.equal(encrypted.byteLength, NAME_PADDED_LENGTH + 16 + 12); - const { given, family } = await TSCrypto.decryptProfileName( - Crypto.arrayBufferToBase64(encrypted), + const { given, family } = decryptProfileName( + Bytes.toBase64(encrypted), key ); + assert.strictEqual(Bytes.toString(given), '01234567890123456789'); assert.strictEqual( - Crypto.stringFromBytes(given), - '01234567890123456789' - ); - assert.strictEqual( - family && Crypto.stringFromBytes(family), + family && Bytes.toString(family), '01234567890123456789012345678912' ); }); - it('handles a string with family/given name', async () => { + it('handles a string with family/given name', () => { const name = 'Alice\0Jones'; - const buffer = Crypto.bytesFromString(name); - const key = Crypto.getRandomBytes(32); + const buffer = Bytes.fromString(name); + const key = getRandomBytes(32); - const encrypted = await TSCrypto.encryptProfileItemWithPadding( + const encrypted = encryptProfileItemWithPadding( buffer, key, PaddedLengths.Name ); assert.equal(encrypted.byteLength, NAME_PADDED_LENGTH + 16 + 12); - const { given, family } = await TSCrypto.decryptProfileName( - Crypto.arrayBufferToBase64(encrypted), + const { given, family } = decryptProfileName( + Bytes.toBase64(encrypted), key ); - assert.strictEqual(Crypto.stringFromBytes(given), 'Alice'); - assert.strictEqual(family && Crypto.stringFromBytes(family), 'Jones'); + assert.strictEqual(Bytes.toString(given), 'Alice'); + assert.strictEqual(family && Bytes.toString(family), 'Jones'); }); - it('works for empty string', async () => { - const name = Crypto.bytesFromString(''); - const key = Crypto.getRandomBytes(32); + it('works for empty string', () => { + const name = Bytes.fromString(''); + const key = getRandomBytes(32); - const encrypted = await TSCrypto.encryptProfileItemWithPadding( + const encrypted = encryptProfileItemWithPadding( name, key, PaddedLengths.Name ); assert.equal(encrypted.byteLength, NAME_PADDED_LENGTH + 16 + 12); - const { given, family } = await TSCrypto.decryptProfileName( - Crypto.arrayBufferToBase64(encrypted), + const { given, family } = decryptProfileName( + Bytes.toBase64(encrypted), key ); assert.strictEqual(family, null); assert.strictEqual(given.byteLength, 0); - assert.strictEqual(Crypto.stringFromBytes(given), ''); + assert.strictEqual(Bytes.toString(given), ''); }); }); describe('encrypting and decrypting profile avatars', () => { it('encrypts and decrypts', async () => { - const buffer = Crypto.bytesFromString('This is an avatar'); - const key = Crypto.getRandomBytes(32); + const buffer = Bytes.fromString('This is an avatar'); + const key = getRandomBytes(32); - const encrypted = await TSCrypto.encryptProfile(buffer, key); + const encrypted = encryptProfile(buffer, key); assert(encrypted.byteLength === buffer.byteLength + 16 + 12); - const decrypted = await TSCrypto.decryptProfile(encrypted, key); - assert(Crypto.constantTimeEqual(buffer, decrypted)); + const decrypted = decryptProfile(encrypted, key); + assert(constantTimeEqual(buffer, decrypted)); }); - it('throws when decrypting with the wrong key', async () => { - const buffer = Crypto.bytesFromString('This is an avatar'); - const key = Crypto.getRandomBytes(32); - const badKey = Crypto.getRandomBytes(32); + it('throws when decrypting with the wrong key', () => { + const buffer = Bytes.fromString('This is an avatar'); + const key = getRandomBytes(32); + const badKey = getRandomBytes(32); - const encrypted = await TSCrypto.encryptProfile(buffer, key); + const encrypted = encryptProfile(buffer, key); assert(encrypted.byteLength === buffer.byteLength + 16 + 12); - await assert.isRejected( - TSCrypto.decryptProfile(encrypted, badKey), + assert.throws( + () => decryptProfile(encrypted, badKey), 'Failed to decrypt profile data. Most likely the profile key has changed.' ); }); @@ -147,7 +166,7 @@ describe('Crypto', () => { describe('generateRegistrationId', () => { it('generates an integer between 0 and 16383 (inclusive)', () => { for (let i = 0; i < 100; i += 1) { - const id = Crypto.generateRegistrationId(); + const id = generateRegistrationId(); assert.isAtLeast(id, 0); assert.isAtMost(id, 16383); assert(Number.isInteger(id)); @@ -157,27 +176,27 @@ describe('Crypto', () => { describe('deriveSecrets', () => { it('derives key parts via HKDF', () => { - const input = Crypto.getRandomBytes(32); - const salt = Crypto.getRandomBytes(32); - const info = Crypto.bytesFromString('Hello world'); - const result = Crypto.deriveSecrets(input, salt, info); + const input = getRandomBytes(32); + const salt = getRandomBytes(32); + const info = Bytes.fromString('Hello world'); + const result = deriveSecrets(input, salt, info); assert.lengthOf(result, 3); result.forEach(part => { // This is a smoke test; HKDF is tested as part of @signalapp/signal-client. - assert.instanceOf(part, ArrayBuffer); + assert.instanceOf(part, Uint8Array); assert.strictEqual(part.byteLength, 32); }); }); }); describe('accessKey/profileKey', () => { - it('verification roundtrips', async () => { - const profileKey = await Crypto.getRandomBytes(32); - const accessKey = await Crypto.deriveAccessKey(profileKey); + it('verification roundtrips', () => { + const profileKey = getRandomBytes(32); + const accessKey = deriveAccessKey(profileKey); - const verifier = await Crypto.getAccessKeyVerifier(accessKey); + const verifier = getAccessKeyVerifier(accessKey); - const correct = await Crypto.verifyAccessKey(accessKey, verifier); + const correct = verifyAccessKey(accessKey, verifier); assert.strictEqual(correct, true); }); @@ -208,12 +227,12 @@ describe('Crypto', () => { ]; vectors.forEach((vector, index) => { - it(`vector ${index}`, async () => { - const gv1 = Crypto.hexToArrayBuffer(vector.gv1); + it(`vector ${index}`, () => { + const gv1 = Bytes.fromHex(vector.gv1); const expectedHex = vector.masterKey; - const actual = await Crypto.deriveMasterKeyFromGroupV1(gv1); - const actualHex = Crypto.arrayBufferToHex(actual); + const actual = deriveMasterKeyFromGroupV1(gv1); + const actualHex = Bytes.toHex(actual); assert.strictEqual(actualHex, expectedHex); }); @@ -221,34 +240,30 @@ describe('Crypto', () => { }); describe('symmetric encryption', () => { - it('roundtrips', async () => { + it('roundtrips', () => { const message = 'this is my message'; - const plaintext = Crypto.bytesFromString(message); - const key = Crypto.getRandomBytes(32); + const plaintext = Bytes.fromString(message); + const key = getRandomBytes(32); - const encrypted = await Crypto.encryptSymmetric(key, plaintext); - const decrypted = await Crypto.decryptSymmetric(key, encrypted); + const encrypted = encryptSymmetric(key, plaintext); + const decrypted = decryptSymmetric(key, encrypted); - const equal = Crypto.constantTimeEqual(plaintext, decrypted); + const equal = constantTimeEqual(plaintext, decrypted); if (!equal) { throw new Error('The output and input did not match!'); } }); - it('roundtrip fails if nonce is modified', async () => { + it('roundtrip fails if nonce is modified', () => { const message = 'this is my message'; - const plaintext = Crypto.bytesFromString(message); - const key = Crypto.getRandomBytes(32); + const plaintext = Bytes.fromString(message); + const key = getRandomBytes(32); - const encrypted = await Crypto.encryptSymmetric(key, plaintext); - const uintArray = new Uint8Array(encrypted); - uintArray[2] += 2; + const encrypted = encryptSymmetric(key, plaintext); + encrypted[2] += 2; try { - await Crypto.decryptSymmetric( - key, - Crypto.typedArrayToArrayBuffer(uintArray) - ); + decryptSymmetric(key, encrypted); } catch (error) { assert.strictEqual( error.message, @@ -260,20 +275,16 @@ describe('Crypto', () => { throw new Error('Expected error to be thrown'); }); - it('roundtrip fails if mac is modified', async () => { + it('roundtrip fails if mac is modified', () => { const message = 'this is my message'; - const plaintext = Crypto.bytesFromString(message); - const key = Crypto.getRandomBytes(32); + const plaintext = Bytes.fromString(message); + const key = getRandomBytes(32); - const encrypted = await Crypto.encryptSymmetric(key, plaintext); - const uintArray = new Uint8Array(encrypted); - uintArray[uintArray.length - 3] += 2; + const encrypted = encryptSymmetric(key, plaintext); + encrypted[encrypted.length - 3] += 2; try { - await Crypto.decryptSymmetric( - key, - Crypto.typedArrayToArrayBuffer(uintArray) - ); + decryptSymmetric(key, encrypted); } catch (error) { assert.strictEqual( error.message, @@ -285,20 +296,16 @@ describe('Crypto', () => { throw new Error('Expected error to be thrown'); }); - it('roundtrip fails if encrypted contents are modified', async () => { + it('roundtrip fails if encrypted contents are modified', () => { const message = 'this is my message'; - const plaintext = Crypto.bytesFromString(message); - const key = Crypto.getRandomBytes(32); + const plaintext = Bytes.fromString(message); + const key = getRandomBytes(32); - const encrypted = await Crypto.encryptSymmetric(key, plaintext); - const uintArray = new Uint8Array(encrypted); - uintArray[35] += 9; + const encrypted = encryptSymmetric(key, plaintext); + encrypted[35] += 9; try { - await Crypto.decryptSymmetric( - key, - Crypto.typedArrayToArrayBuffer(uintArray) - ); + decryptSymmetric(key, encrypted); } catch (error) { assert.strictEqual( error.message, @@ -312,33 +319,24 @@ describe('Crypto', () => { }); describe('encrypted device name', () => { - it('roundtrips', async () => { + it('roundtrips', () => { const deviceName = 'v1.19.0 on Windows 10'; const identityKey = Curve.generateKeyPair(); - const encrypted = await Crypto.encryptDeviceName( - deviceName, - identityKey.pubKey - ); - const decrypted = await Crypto.decryptDeviceName( - encrypted, - identityKey.privKey - ); + const encrypted = encryptDeviceName(deviceName, identityKey.pubKey); + const decrypted = decryptDeviceName(encrypted, identityKey.privKey); assert.strictEqual(decrypted, deviceName); }); - it('fails if iv is changed', async () => { + it('fails if iv is changed', () => { const deviceName = 'v1.19.0 on Windows 10'; const identityKey = Curve.generateKeyPair(); - const encrypted = await Crypto.encryptDeviceName( - deviceName, - identityKey.pubKey - ); - encrypted.syntheticIv = Crypto.getRandomBytes(16); + const encrypted = encryptDeviceName(deviceName, identityKey.pubKey); + encrypted.syntheticIv = getRandomBytes(16); try { - await Crypto.decryptDeviceName(encrypted, identityKey.privKey); + decryptDeviceName(encrypted, identityKey.privKey); } catch (error) { assert.strictEqual( error.message, @@ -348,46 +346,15 @@ describe('Crypto', () => { }); }); - describe('attachment encryption', () => { - it('roundtrips', async () => { - const staticKeyPair = Curve.generateKeyPair(); - const message = 'this is my message'; - const plaintext = Crypto.bytesFromString(message); - const path = - 'fa/facdf99c22945b1c9393345599a276f4b36ad7ccdc8c2467f5441b742c2d11fa'; - - const encrypted = await Crypto.encryptAttachment( - staticKeyPair.pubKey.slice(1), - path, - plaintext - ); - const decrypted = await Crypto.decryptAttachment( - staticKeyPair.privKey, - path, - encrypted - ); - - const equal = Crypto.constantTimeEqual(plaintext, decrypted); - if (!equal) { - throw new Error('The output and input did not match!'); - } - }); - }); - describe('verifyHmacSha256', () => { - it('rejects if their MAC is too short', async () => { - const key = Crypto.getRandomBytes(32); - const plaintext = Crypto.bytesFromString('Hello world'); - const ourMac = await Crypto.hmacSha256(key, plaintext); + it('rejects if their MAC is too short', () => { + const key = getRandomBytes(32); + const plaintext = Bytes.fromString('Hello world'); + const ourMac = hmacSha256(key, plaintext); const theirMac = ourMac.slice(0, -1); let error; try { - await Crypto.verifyHmacSha256( - plaintext, - key, - theirMac, - ourMac.byteLength - ); + verifyHmacSha256(plaintext, key, theirMac, ourMac.byteLength); } catch (err) { error = err; } @@ -395,19 +362,14 @@ describe('Crypto', () => { assert.strictEqual(error.message, 'Bad MAC length'); }); - it('rejects if their MAC is too long', async () => { - const key = Crypto.getRandomBytes(32); - const plaintext = Crypto.bytesFromString('Hello world'); - const ourMac = await Crypto.hmacSha256(key, plaintext); - const theirMac = Crypto.concatenateBytes(ourMac, new Uint8Array([0xff])); + it('rejects if their MAC is too long', () => { + const key = getRandomBytes(32); + const plaintext = Bytes.fromString('Hello world'); + const ourMac = hmacSha256(key, plaintext); + const theirMac = Bytes.concatenate([ourMac, new Uint8Array([0xff])]); let error; try { - await Crypto.verifyHmacSha256( - plaintext, - key, - theirMac, - ourMac.byteLength - ); + verifyHmacSha256(plaintext, key, theirMac, ourMac.byteLength); } catch (err) { error = err; } @@ -415,19 +377,14 @@ describe('Crypto', () => { assert.strictEqual(error.message, 'Bad MAC length'); }); - it('rejects if our MAC is shorter than the specified length', async () => { - const key = Crypto.getRandomBytes(32); - const plaintext = Crypto.bytesFromString('Hello world'); - const ourMac = await Crypto.hmacSha256(key, plaintext); + it('rejects if our MAC is shorter than the specified length', () => { + const key = getRandomBytes(32); + const plaintext = Bytes.fromString('Hello world'); + const ourMac = hmacSha256(key, plaintext); const theirMac = ourMac; let error; try { - await Crypto.verifyHmacSha256( - plaintext, - key, - theirMac, - ourMac.byteLength + 1 - ); + verifyHmacSha256(plaintext, key, theirMac, ourMac.byteLength + 1); } catch (err) { error = err; } @@ -435,20 +392,15 @@ describe('Crypto', () => { assert.strictEqual(error.message, 'Bad MAC length'); }); - it("rejects if the MACs don't match", async () => { - const plaintext = Crypto.bytesFromString('Hello world'); - const ourKey = Crypto.getRandomBytes(32); - const ourMac = await Crypto.hmacSha256(ourKey, plaintext); - const theirKey = Crypto.getRandomBytes(32); - const theirMac = await Crypto.hmacSha256(theirKey, plaintext); + it("rejects if the MACs don't match", () => { + const plaintext = Bytes.fromString('Hello world'); + const ourKey = getRandomBytes(32); + const ourMac = hmacSha256(ourKey, plaintext); + const theirKey = getRandomBytes(32); + const theirMac = hmacSha256(theirKey, plaintext); let error; try { - await Crypto.verifyHmacSha256( - plaintext, - ourKey, - theirMac, - ourMac.byteLength - ); + verifyHmacSha256(plaintext, ourKey, theirMac, ourMac.byteLength); } catch (err) { error = err; } @@ -456,11 +408,11 @@ describe('Crypto', () => { assert.strictEqual(error.message, 'Bad MAC'); }); - it('resolves with undefined if the MACs match exactly', async () => { - const key = Crypto.getRandomBytes(32); - const plaintext = Crypto.bytesFromString('Hello world'); - const theirMac = await Crypto.hmacSha256(key, plaintext); - const result = await Crypto.verifyHmacSha256( + it('resolves with undefined if the MACs match exactly', () => { + const key = getRandomBytes(32); + const plaintext = Bytes.fromString('Hello world'); + const theirMac = hmacSha256(key, plaintext); + const result = verifyHmacSha256( plaintext, key, theirMac, @@ -469,11 +421,11 @@ describe('Crypto', () => { assert.isUndefined(result); }); - it('resolves with undefined if the first `length` bytes of the MACs match', async () => { - const key = Crypto.getRandomBytes(32); - const plaintext = Crypto.bytesFromString('Hello world'); - const theirMac = (await Crypto.hmacSha256(key, plaintext)).slice(0, -5); - const result = await Crypto.verifyHmacSha256( + it('resolves with undefined if the first `length` bytes of the MACs match', () => { + const key = getRandomBytes(32); + const plaintext = Bytes.fromString('Hello world'); + const theirMac = hmacSha256(key, plaintext).slice(0, -5); + const result = verifyHmacSha256( plaintext, key, theirMac, @@ -483,102 +435,86 @@ describe('Crypto', () => { }); }); - describe('uuidToArrayBuffer', () => { - const { uuidToArrayBuffer } = Crypto; - - it('converts valid UUIDs to ArrayBuffers', () => { - const expectedResult = Crypto.typedArrayToArrayBuffer( - new Uint8Array([ - 0x22, - 0x6e, - 0x44, - 0x02, - 0x7f, - 0xfc, - 0x45, - 0x43, - 0x85, - 0xc9, - 0x46, - 0x22, - 0xc5, - 0x0a, - 0x5b, - 0x14, - ]) - ); + describe('uuidToBytes', () => { + it('converts valid UUIDs to Uint8Arrays', () => { + const expectedResult = new Uint8Array([ + 0x22, + 0x6e, + 0x44, + 0x02, + 0x7f, + 0xfc, + 0x45, + 0x43, + 0x85, + 0xc9, + 0x46, + 0x22, + 0xc5, + 0x0a, + 0x5b, + 0x14, + ]); assert.deepEqual( - uuidToArrayBuffer('226e4402-7ffc-4543-85c9-4622c50a5b14'), + uuidToBytes('226e4402-7ffc-4543-85c9-4622c50a5b14'), expectedResult ); assert.deepEqual( - uuidToArrayBuffer('226E4402-7FFC-4543-85C9-4622C50A5B14'), + uuidToBytes('226E4402-7FFC-4543-85C9-4622C50A5B14'), expectedResult ); }); - it('returns an empty ArrayBuffer for strings of the wrong length', () => { - assert.deepEqual(uuidToArrayBuffer(''), new ArrayBuffer(0)); - assert.deepEqual(uuidToArrayBuffer('abc'), new ArrayBuffer(0)); + it('returns an empty Uint8Array for strings of the wrong length', () => { + assert.deepEqual(uuidToBytes(''), new Uint8Array(0)); + assert.deepEqual(uuidToBytes('abc'), new Uint8Array(0)); assert.deepEqual( - uuidToArrayBuffer('032deadf0d5e4ee78da28e75b1dfb284'), - new ArrayBuffer(0) + uuidToBytes('032deadf0d5e4ee78da28e75b1dfb284'), + new Uint8Array(0) ); assert.deepEqual( - uuidToArrayBuffer('deaed5eb-d983-456a-a954-9ad7a006b271aaaaaaaaaa'), - new ArrayBuffer(0) + uuidToBytes('deaed5eb-d983-456a-a954-9ad7a006b271aaaaaaaaaa'), + new Uint8Array(0) ); }); }); - describe('arrayBufferToUuid', () => { - const { arrayBufferToUuid } = Crypto; - - it('converts valid ArrayBuffers to UUID strings', () => { - const buf = Crypto.typedArrayToArrayBuffer( - new Uint8Array([ - 0x22, - 0x6e, - 0x44, - 0x02, - 0x7f, - 0xfc, - 0x45, - 0x43, - 0x85, - 0xc9, - 0x46, - 0x22, - 0xc5, - 0x0a, - 0x5b, - 0x14, - ]) - ); + describe('bytesToUuid', () => { + it('converts valid Uint8Arrays to UUID strings', () => { + const buf = new Uint8Array([ + 0x22, + 0x6e, + 0x44, + 0x02, + 0x7f, + 0xfc, + 0x45, + 0x43, + 0x85, + 0xc9, + 0x46, + 0x22, + 0xc5, + 0x0a, + 0x5b, + 0x14, + ]); assert.deepEqual( - arrayBufferToUuid(buf), + bytesToUuid(buf), '226e4402-7ffc-4543-85c9-4622c50a5b14' ); }); it('returns undefined if passed an all-zero buffer', () => { - assert.isUndefined(arrayBufferToUuid(new ArrayBuffer(16))); + assert.isUndefined(bytesToUuid(new Uint8Array(16))); }); it('returns undefined if passed the wrong number of bytes', () => { - assert.isUndefined(arrayBufferToUuid(new ArrayBuffer(0))); - assert.isUndefined( - arrayBufferToUuid( - Crypto.typedArrayToArrayBuffer(new Uint8Array([0x22])) - ) - ); - assert.isUndefined( - arrayBufferToUuid( - Crypto.typedArrayToArrayBuffer(new Uint8Array(Array(17).fill(0x22))) - ) - ); + assert.isUndefined(bytesToUuid(new Uint8Array(0))); + assert.isUndefined(bytesToUuid(new Uint8Array([0x22]))); + assert.isUndefined(bytesToUuid(new Uint8Array(Array(17).fill(0x22)))); }); }); }); diff --git a/ts/test-electron/Curve_test.ts b/ts/test-electron/Curve_test.ts index e4e368680..c550ff6a9 100644 --- a/ts/test-electron/Curve_test.ts +++ b/ts/test-electron/Curve_test.ts @@ -3,18 +3,12 @@ import { assert } from 'chai'; -import { - arrayBufferToHex, - constantTimeEqual, - getRandomBytes, - hexToArrayBuffer, - typedArrayToArrayBuffer, -} from '../Crypto'; +import * as Bytes from '../Bytes'; +import { constantTimeEqual } from '../Crypto'; import { calculateSignature, clampPrivateKey, createKeyPair, - copyArrayBuffer, calculateAgreement, generateKeyPair, generatePreKey, @@ -25,7 +19,7 @@ import { describe('Curve', () => { it('verifySignature roundtrip', () => { - const message = typedArrayToArrayBuffer(Buffer.from('message')); + const message = Buffer.from('message'); const { pubKey, privKey } = generateKeyPair(); const signature = calculateSignature(privKey, message); const verified = verifySignature(pubKey, message, signature); @@ -87,42 +81,12 @@ describe('Curve', () => { }); }); - describe('#copyArrayBuffer', () => { - it('copy matches original', () => { - const data = getRandomBytes(200); - const dataHex = arrayBufferToHex(data); - const copy = copyArrayBuffer(data); - - assert.equal(data.byteLength, copy.byteLength); - assert.isTrue(constantTimeEqual(data, copy)); - - assert.equal(dataHex, arrayBufferToHex(data)); - assert.equal(dataHex, arrayBufferToHex(copy)); - }); - - it('copies into new memory location', () => { - const data = getRandomBytes(200); - const dataHex = arrayBufferToHex(data); - const copy = copyArrayBuffer(data); - - const view = new Uint8Array(copy); - view[0] += 1; - view[1] -= 1; - - assert.equal(data.byteLength, copy.byteLength); - assert.isFalse(constantTimeEqual(data, copy)); - - assert.equal(dataHex, arrayBufferToHex(data)); - assert.notEqual(dataHex, arrayBufferToHex(copy)); - }); - }); - describe('#createKeyPair', () => { it('does not modify unclamped private key', () => { const initialHex = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; - const privateKey = hexToArrayBuffer(initialHex); - const copyOfPrivateKey = copyArrayBuffer(privateKey); + const privateKey = Bytes.fromHex(initialHex); + const copyOfPrivateKey = new Uint8Array(privateKey); assert.isTrue( constantTimeEqual(privateKey, copyOfPrivateKey), @@ -143,7 +107,7 @@ describe('Curve', () => { // But the keypair that comes out has indeed been updated assert.notEqual( initialHex, - arrayBufferToHex(keyPair.privKey), + Bytes.toHex(keyPair.privKey), 'keypair check' ); assert.isFalse( @@ -155,10 +119,10 @@ describe('Curve', () => { it('does not modify clamped private key', () => { const initialHex = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; - const privateKey = hexToArrayBuffer(initialHex); + const privateKey = Bytes.fromHex(initialHex); clampPrivateKey(privateKey); - const postClampHex = arrayBufferToHex(privateKey); - const copyOfPrivateKey = copyArrayBuffer(privateKey); + const postClampHex = Bytes.toHex(privateKey); + const copyOfPrivateKey = new Uint8Array(privateKey); assert.notEqual(postClampHex, initialHex, 'initial clamp check'); assert.isTrue( @@ -178,11 +142,7 @@ describe('Curve', () => { ); // The keypair that comes out hasn't been updated either - assert.equal( - postClampHex, - arrayBufferToHex(keyPair.privKey), - 'keypair check' - ); + assert.equal(postClampHex, Bytes.toHex(keyPair.privKey), 'keypair check'); assert.isTrue( constantTimeEqual(privateKey, keyPair.privKey), 'keypair vs incoming value' diff --git a/ts/test-electron/MessageReceiver_test.ts b/ts/test-electron/MessageReceiver_test.ts index ee1b9d288..bd90c6a55 100644 --- a/ts/test-electron/MessageReceiver_test.ts +++ b/ts/test-electron/MessageReceiver_test.ts @@ -15,9 +15,6 @@ import { DecryptionErrorEvent } from '../textsecure/messageReceiverEvents'; import { SignalService as Proto } from '../protobuf'; import * as Crypto from '../Crypto'; -// TODO: remove once we move away from ArrayBuffers -const FIXMEU8 = Uint8Array; - describe('MessageReceiver', () => { const number = '+19999999999'; const uuid = 'aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee'; @@ -54,7 +51,7 @@ describe('MessageReceiver', () => { sourceUuid: uuid, sourceDevice: deviceId, timestamp: Date.now(), - content: new FIXMEU8(Crypto.getRandomBytes(200)), + content: Crypto.getRandomBytes(200), }).finish(); messageReceiver.handleRequest( diff --git a/ts/test-electron/SignalProtocolStore_test.ts b/ts/test-electron/SignalProtocolStore_test.ts index 83defbf6b..c4f6a3f01 100644 --- a/ts/test-electron/SignalProtocolStore_test.ts +++ b/ts/test-electron/SignalProtocolStore_test.ts @@ -13,15 +13,11 @@ import { import { v4 as getGuid } from 'uuid'; import { signal } from '../protobuf/compiled'; -import { sessionStructureToArrayBuffer } from '../util/sessionTranslation'; +import { sessionStructureToBytes } from '../util/sessionTranslation'; import { Zone } from '../util/Zone'; -import { - getRandomBytes, - constantTimeEqual, - typedArrayToArrayBuffer as toArrayBuffer, - arrayBufferToBase64 as toBase64, -} from '../Crypto'; +import * as Bytes from '../Bytes'; +import { getRandomBytes, constantTimeEqual } from '../Crypto'; import { clampPrivateKey, setPublicKeyTypeByte } from '../Curve'; import { GLOBAL_ZONE, SignalProtocolStore } from '../SignalProtocolStore'; import { Address } from '../types/Address'; @@ -53,20 +49,20 @@ describe('SignalProtocolStore', () => { if (isOpen) { proto.currentSession = new SessionStructure(); - proto.currentSession.aliceBaseKey = toUint8Array(getPublicKey()); - proto.currentSession.localIdentityPublic = toUint8Array(getPublicKey()); + proto.currentSession.aliceBaseKey = getPublicKey(); + proto.currentSession.localIdentityPublic = getPublicKey(); proto.currentSession.localRegistrationId = 435; proto.currentSession.previousCounter = 1; - proto.currentSession.remoteIdentityPublic = toUint8Array(getPublicKey()); + proto.currentSession.remoteIdentityPublic = getPublicKey(); proto.currentSession.remoteRegistrationId = 243; - proto.currentSession.rootKey = toUint8Array(getPrivateKey()); + proto.currentSession.rootKey = getPrivateKey(); proto.currentSession.sessionVersion = 3; } return SessionRecord.deserialize( - Buffer.from(sessionStructureToArrayBuffer(proto)) + Buffer.from(sessionStructureToBytes(proto)) ); } @@ -80,19 +76,19 @@ describe('SignalProtocolStore', () => { const senderChainKey = new SenderKeyStateStructure.SenderChainKey(); senderChainKey.iteration = 10; - senderChainKey.seed = toUint8Array(getPublicKey()); + senderChainKey.seed = getPublicKey(); state.senderChainKey = senderChainKey; const senderSigningKey = new SenderKeyStateStructure.SenderSigningKey(); - senderSigningKey.public = toUint8Array(getPublicKey()); - senderSigningKey.private = toUint8Array(getPrivateKey()); + senderSigningKey.public = getPublicKey(); + senderSigningKey.private = getPrivateKey(); state.senderSigningKey = senderSigningKey; state.senderMessageKeys = []; const messageKey = new SenderKeyStateStructure.SenderMessageKey(); messageKey.iteration = 234; - messageKey.seed = toUint8Array(getPublicKey()); + messageKey.seed = getPublicKey(); state.senderMessageKeys.push(messageKey); proto.senderKeyStates = []; @@ -105,10 +101,6 @@ describe('SignalProtocolStore', () => { ); } - function toUint8Array(buffer: ArrayBuffer): Uint8Array { - return new Uint8Array(buffer); - } - function getPrivateKey() { const key = getRandomBytes(32); clampPrivateKey(key); @@ -141,8 +133,8 @@ describe('SignalProtocolStore', () => { window.storage.put('registrationIdMap', { [ourUuid.toString()]: 1337 }); window.storage.put('identityKeyMap', { [ourUuid.toString()]: { - privKey: toBase64(identityKey.privKey), - pubKey: toBase64(identityKey.pubKey), + privKey: Bytes.toBase64(identityKey.privKey), + pubKey: Bytes.toBase64(identityKey.pubKey), }, }); await window.storage.fetch(); @@ -194,10 +186,7 @@ describe('SignalProtocolStore', () => { } assert.isTrue( - constantTimeEqual( - toArrayBuffer(expected.serialize()), - toArrayBuffer(actual.serialize()) - ) + constantTimeEqual(expected.serialize(), actual.serialize()) ); await store.removeSenderKey(qualifiedAddress, distributionId); @@ -230,10 +219,7 @@ describe('SignalProtocolStore', () => { } assert.isTrue( - constantTimeEqual( - toArrayBuffer(expected.serialize()), - toArrayBuffer(actual.serialize()) - ) + constantTimeEqual(expected.serialize(), actual.serialize()) ); await store.removeSenderKey(qualifiedAddress, distributionId); @@ -1254,12 +1240,8 @@ describe('SignalProtocolStore', () => { } const keyPair = { - pubKey: window.Signal.Crypto.typedArrayToArrayBuffer( - key.publicKey().serialize() - ), - privKey: window.Signal.Crypto.typedArrayToArrayBuffer( - key.privateKey().serialize() - ), + pubKey: key.publicKey().serialize(), + privKey: key.privateKey().serialize(), }; assert.isTrue(constantTimeEqual(keyPair.pubKey, testKey.pubKey)); @@ -1286,12 +1268,8 @@ describe('SignalProtocolStore', () => { } const keyPair = { - pubKey: window.Signal.Crypto.typedArrayToArrayBuffer( - key.publicKey().serialize() - ), - privKey: window.Signal.Crypto.typedArrayToArrayBuffer( - key.privateKey().serialize() - ), + pubKey: key.publicKey().serialize(), + privKey: key.privateKey().serialize(), }; assert.isTrue(constantTimeEqual(keyPair.pubKey, testKey.pubKey)); diff --git a/ts/test-both/util/synchronousCrypto_test.ts b/ts/test-electron/context/Crypto_test.ts similarity index 57% rename from ts/test-both/util/synchronousCrypto_test.ts rename to ts/test-electron/context/Crypto_test.ts index 91ace2773..874335272 100644 --- a/ts/test-both/util/synchronousCrypto_test.ts +++ b/ts/test-electron/context/Crypto_test.ts @@ -4,22 +4,19 @@ import { assert } from 'chai'; import crypto from 'crypto'; -import { typedArrayToArrayBuffer as toArrayBuffer } from '../../Crypto'; import { + CipherType, HashType, hash, sign, encrypt, decrypt, -} from '../../util/synchronousCrypto'; +} from '../../Crypto'; -describe('synchronousCrypto', () => { +describe('SignalContext.Crypto', () => { describe('hash', () => { it('returns SHA512 hash of the input', () => { - const result = hash( - HashType.size512, - toArrayBuffer(Buffer.from('signal')) - ); + const result = hash(HashType.size512, Buffer.from('signal')); assert.strictEqual( Buffer.from(result).toString('base64'), 'WxneQjrfSlY95Bi+SAzDAr2cf3mxUXePeNYn6DILN4a8NFr9VelTbP5tGHdthi+' + @@ -30,10 +27,7 @@ describe('synchronousCrypto', () => { describe('sign', () => { it('returns hmac SHA256 hash of the input', () => { - const result = sign( - toArrayBuffer(Buffer.from('secret')), - toArrayBuffer(Buffer.from('signal')) - ); + const result = sign(Buffer.from('secret'), Buffer.from('signal')); assert.strictEqual( Buffer.from(result).toString('base64'), @@ -44,12 +38,20 @@ describe('synchronousCrypto', () => { describe('encrypt+decrypt', () => { it('returns original input', () => { - const iv = toArrayBuffer(crypto.randomBytes(16)); - const key = toArrayBuffer(crypto.randomBytes(32)); - const input = toArrayBuffer(Buffer.from('plaintext')); + const iv = crypto.randomBytes(16); + const key = crypto.randomBytes(32); + const input = Buffer.from('plaintext'); - const ciphertext = encrypt(key, input, iv); - const plaintext = decrypt(key, ciphertext, iv); + const ciphertext = encrypt(CipherType.AES256CBC, { + key, + iv, + plaintext: input, + }); + const plaintext = decrypt(CipherType.AES256CBC, { + key, + iv, + ciphertext, + }); assert.strictEqual(Buffer.from(plaintext).toString(), 'plaintext'); }); diff --git a/ts/test-electron/linkPreviews/linkPreviewFetch_test.ts b/ts/test-electron/linkPreviews/linkPreviewFetch_test.ts index d822ce33a..55a1084bd 100644 --- a/ts/test-electron/linkPreviews/linkPreviewFetch_test.ts +++ b/ts/test-electron/linkPreviews/linkPreviewFetch_test.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import { assert } from 'chai'; +import { Response } from 'node-fetch'; import * as sinon from 'sinon'; import * as fs from 'fs'; import * as path from 'path'; @@ -9,8 +10,6 @@ import AbortController from 'abort-controller'; import { IMAGE_JPEG, stringToMIMEType } from '../../types/MIME'; import * as log from '../../logging/log'; -import { typedArrayToArrayBuffer } from '../../Crypto'; - import { fetchLinkPreviewImage, fetchLinkPreviewMetadata, @@ -1155,7 +1154,7 @@ describe('link preview fetching', () => { new AbortController().signal ), { - data: typedArrayToArrayBuffer(fixture), + data: fixture, contentType: stringToMIMEType(contentType), } ); @@ -1217,7 +1216,7 @@ describe('link preview fetching', () => { const fakeFetch = stub(); fakeFetch.onFirstCall().resolves( - new Response(null, { + new Response(Buffer.from(''), { status: 301, headers: { Location: '/result.jpg', @@ -1240,7 +1239,7 @@ describe('link preview fetching', () => { new AbortController().signal ), { - data: typedArrayToArrayBuffer(fixture), + data: fixture, contentType: IMAGE_JPEG, } ); @@ -1336,7 +1335,7 @@ describe('link preview fetching', () => { }); it('sends WhatsApp as the User-Agent for compatibility', async () => { - const fakeFetch = stub().resolves(new Response(null)); + const fakeFetch = stub().resolves(new Response(Buffer.from(''))); await fetchLinkPreviewImage( fakeFetch, @@ -1368,7 +1367,7 @@ describe('link preview fetching', () => { }, }); sinon - .stub(response, 'arrayBuffer') + .stub(response, 'buffer') .rejects(new Error('Should not be called')); sinon.stub(response, 'blob').rejects(new Error('Should not be called')); sinon.stub(response, 'text').rejects(new Error('Should not be called')); @@ -1402,9 +1401,9 @@ describe('link preview fetching', () => { 'Content-Length': fixture.length.toString(), }, }); - const oldArrayBufferMethod = response.arrayBuffer.bind(response); - sinon.stub(response, 'arrayBuffer').callsFake(async () => { - const data = await oldArrayBufferMethod(); + const oldBufferMethod = response.buffer.bind(response); + sinon.stub(response, 'buffer').callsFake(async () => { + const data = await oldBufferMethod(); abortController.abort(); return data; }); diff --git a/ts/test-electron/models/messages_test.ts b/ts/test-electron/models/messages_test.ts index 696cf12fb..894317445 100644 --- a/ts/test-electron/models/messages_test.ts +++ b/ts/test-electron/models/messages_test.ts @@ -136,7 +136,7 @@ describe('Message', () => { }, }); - const fakeDataMessage = new ArrayBuffer(0); + const fakeDataMessage = new Uint8Array(0); const conversation1Uuid = conversation1.get('uuid'); const ignoredUuid = window.getGuid(); diff --git a/ts/test-electron/services/senderCertificate_test.ts b/ts/test-electron/services/senderCertificate_test.ts index 1915bda13..4c80fd639 100644 --- a/ts/test-electron/services/senderCertificate_test.ts +++ b/ts/test-electron/services/senderCertificate_test.ts @@ -10,7 +10,6 @@ import { v4 as uuid } from 'uuid'; import Long from 'long'; import * as durations from '../../util/durations'; import * as Bytes from '../../Bytes'; -import { typedArrayToArrayBuffer } from '../../Crypto'; import { SenderCertificateMode } from '../../textsecure/OutgoingMessage'; import { SignalService as Proto } from '../../protobuf'; @@ -22,6 +21,7 @@ describe('SenderCertificateService', () => { const FIFTEEN_MINUTES = 15 * durations.MINUTE; let fakeValidCertificate: SenderCertificate; + let fakeValidEncodedCertificate: Uint8Array; let fakeValidCertificateExpiry: number; let fakeServer: any; let fakeNavigator: { onLine: boolean }; @@ -47,12 +47,13 @@ describe('SenderCertificateService', () => { fakeValidCertificate.certificate = SenderCertificate.Certificate.encode( certificate ).finish(); + fakeValidEncodedCertificate = SenderCertificate.encode( + fakeValidCertificate + ).finish(); fakeServer = { getSenderCertificate: sinon.stub().resolves({ - certificate: Bytes.toBase64( - SenderCertificate.encode(fakeValidCertificate).finish() - ), + certificate: Bytes.toBase64(fakeValidEncodedCertificate), }), }; @@ -77,7 +78,7 @@ describe('SenderCertificateService', () => { it('returns valid yes-E164 certificates from storage if they exist', async () => { const cert = { expires: Date.now() + 123456, - serialized: new ArrayBuffer(2), + serialized: new Uint8Array(2), }; fakeStorage.get.withArgs('senderCertificate').returns(cert); @@ -94,7 +95,7 @@ describe('SenderCertificateService', () => { it('returns valid no-E164 certificates from storage if they exist', async () => { const cert = { expires: Date.now() + 123456, - serialized: new ArrayBuffer(2), + serialized: new Uint8Array(2), }; fakeStorage.get.withArgs('senderCertificateNoE164').returns(cert); @@ -113,16 +114,12 @@ describe('SenderCertificateService', () => { assert.deepEqual(await service.get(SenderCertificateMode.WithE164), { expires: fakeValidCertificateExpiry - FIFTEEN_MINUTES, - serialized: typedArrayToArrayBuffer( - SenderCertificate.encode(fakeValidCertificate).finish() - ), + serialized: fakeValidEncodedCertificate, }); sinon.assert.calledWithMatch(fakeStorage.put, 'senderCertificate', { expires: fakeValidCertificateExpiry - FIFTEEN_MINUTES, - serialized: typedArrayToArrayBuffer( - SenderCertificate.encode(fakeValidCertificate).finish() - ), + serialized: Buffer.from(fakeValidEncodedCertificate), }); sinon.assert.calledWith(fakeServer.getSenderCertificate, false); @@ -133,16 +130,12 @@ describe('SenderCertificateService', () => { assert.deepEqual(await service.get(SenderCertificateMode.WithoutE164), { expires: fakeValidCertificateExpiry - FIFTEEN_MINUTES, - serialized: typedArrayToArrayBuffer( - SenderCertificate.encode(fakeValidCertificate).finish() - ), + serialized: fakeValidEncodedCertificate, }); sinon.assert.calledWithMatch(fakeStorage.put, 'senderCertificateNoE164', { expires: fakeValidCertificateExpiry - FIFTEEN_MINUTES, - serialized: typedArrayToArrayBuffer( - SenderCertificate.encode(fakeValidCertificate).finish() - ), + serialized: Buffer.from(fakeValidEncodedCertificate), }); sinon.assert.calledWith(fakeServer.getSenderCertificate, true); @@ -153,7 +146,7 @@ describe('SenderCertificateService', () => { fakeStorage.get.withArgs('senderCertificate').returns({ expires: Date.now() - 1000, - serialized: new ArrayBuffer(2), + serialized: new Uint8Array(2), }); await service.get(SenderCertificateMode.WithE164); @@ -165,7 +158,7 @@ describe('SenderCertificateService', () => { const service = initializeTestService(); fakeStorage.get.withArgs('senderCertificate').returns({ - serialized: 'not an arraybuffer', + serialized: 'not an uint8array', }); await service.get(SenderCertificateMode.WithE164); diff --git a/ts/test-electron/sql/sendLog_test.ts b/ts/test-electron/sql/sendLog_test.ts index d0bc4ec3f..d59b92ea1 100644 --- a/ts/test-electron/sql/sendLog_test.ts +++ b/ts/test-electron/sql/sendLog_test.ts @@ -6,11 +6,7 @@ import { v4 as getGuid } from 'uuid'; import { assert } from 'chai'; import dataInterface from '../../sql/Client'; -import { - constantTimeEqual, - getRandomBytes, - typedArrayToArrayBuffer, -} from '../../Crypto'; +import { constantTimeEqual, getRandomBytes } from '../../Crypto'; const { _getAllSentProtoMessageIds, @@ -33,7 +29,7 @@ describe('sendLog', () => { }); it('roundtrips with insertSentProto/getAllSentProtos', async () => { - const bytes = Buffer.from(getRandomBytes(128)); + const bytes = getRandomBytes(128); const timestamp = Date.now(); const proto = { contentHint: 1, @@ -52,12 +48,7 @@ describe('sendLog', () => { const actual = allProtos[0]; assert.strictEqual(actual.contentHint, proto.contentHint); - assert.isTrue( - constantTimeEqual( - typedArrayToArrayBuffer(actual.proto), - typedArrayToArrayBuffer(proto.proto) - ) - ); + assert.isTrue(constantTimeEqual(actual.proto, proto.proto)); assert.strictEqual(actual.timestamp, proto.timestamp); await removeAllSentProtos(); @@ -70,7 +61,7 @@ describe('sendLog', () => { assert.lengthOf(await _getAllSentProtoMessageIds(), 0); assert.lengthOf(await _getAllSentProtoRecipients(), 0); - const bytes = Buffer.from(getRandomBytes(128)); + const bytes = getRandomBytes(128); const timestamp = Date.now(); const proto = { contentHint: 1, @@ -114,7 +105,7 @@ describe('sendLog', () => { { forceSave: true } ); - const bytes = Buffer.from(getRandomBytes(128)); + const bytes = getRandomBytes(128); const proto = { contentHint: 1, proto: bytes, @@ -148,12 +139,12 @@ describe('sendLog', () => { }; const proto1 = { contentHint: 7, - proto: Buffer.from(getRandomBytes(128)), + proto: getRandomBytes(128), timestamp, }; const proto2 = { contentHint: 9, - proto: Buffer.from(getRandomBytes(128)), + proto: getRandomBytes(128), timestamp, }; @@ -182,7 +173,7 @@ describe('sendLog', () => { const messageIds = [getGuid()]; const proto = { contentHint: 1, - proto: Buffer.from(getRandomBytes(128)), + proto: getRandomBytes(128), timestamp, }; @@ -220,17 +211,17 @@ describe('sendLog', () => { const proto1 = { contentHint: 1, - proto: Buffer.from(getRandomBytes(128)), + proto: getRandomBytes(128), timestamp: timestamp + 10, }; const proto2 = { contentHint: 2, - proto: Buffer.from(getRandomBytes(128)), + proto: getRandomBytes(128), timestamp, }; const proto3 = { contentHint: 0, - proto: Buffer.from(getRandomBytes(128)), + proto: getRandomBytes(128), timestamp: timestamp - 15, }; await insertSentProto(proto1, { @@ -261,22 +252,12 @@ describe('sendLog', () => { const actual1 = allProtos[0]; assert.strictEqual(actual1.contentHint, proto1.contentHint); - assert.isTrue( - constantTimeEqual( - typedArrayToArrayBuffer(actual1.proto), - typedArrayToArrayBuffer(proto1.proto) - ) - ); + assert.isTrue(constantTimeEqual(actual1.proto, proto1.proto)); assert.strictEqual(actual1.timestamp, proto1.timestamp); const actual2 = allProtos[1]; assert.strictEqual(actual2.contentHint, proto2.contentHint); - assert.isTrue( - constantTimeEqual( - typedArrayToArrayBuffer(actual2.proto), - typedArrayToArrayBuffer(proto2.proto) - ) - ); + assert.isTrue(constantTimeEqual(actual2.proto, proto2.proto)); assert.strictEqual(actual2.timestamp, proto2.timestamp); }); }); @@ -291,17 +272,17 @@ describe('sendLog', () => { const timestamp = Date.now(); const proto1 = { contentHint: 1, - proto: Buffer.from(getRandomBytes(128)), + proto: getRandomBytes(128), timestamp, }; const proto2 = { contentHint: 1, - proto: Buffer.from(getRandomBytes(128)), + proto: getRandomBytes(128), timestamp: timestamp - 10, }; const proto3 = { contentHint: 1, - proto: Buffer.from(getRandomBytes(128)), + proto: getRandomBytes(128), timestamp: timestamp - 20, }; await insertSentProto(proto1, { @@ -344,7 +325,7 @@ describe('sendLog', () => { const recipientUuid2 = getGuid(); const proto = { contentHint: 1, - proto: Buffer.from(getRandomBytes(128)), + proto: getRandomBytes(128), timestamp, }; await insertSentProto(proto, { @@ -375,7 +356,7 @@ describe('sendLog', () => { const recipientUuid2 = getGuid(); const proto = { contentHint: 1, - proto: Buffer.from(getRandomBytes(128)), + proto: getRandomBytes(128), timestamp, }; await insertSentProto(proto, { @@ -424,7 +405,7 @@ describe('sendLog', () => { const recipientUuid2 = getGuid(); const proto = { contentHint: 1, - proto: Buffer.from(getRandomBytes(128)), + proto: getRandomBytes(128), timestamp, }; await insertSentProto(proto, { @@ -469,7 +450,7 @@ describe('sendLog', () => { const messageIds = [getGuid(), getGuid()]; const proto = { contentHint: 1, - proto: Buffer.from(getRandomBytes(128)), + proto: getRandomBytes(128), timestamp, }; await insertSentProto(proto, { @@ -493,12 +474,7 @@ describe('sendLog', () => { throw new Error('Failed to fetch proto!'); } assert.strictEqual(actual.contentHint, proto.contentHint); - assert.isTrue( - constantTimeEqual( - typedArrayToArrayBuffer(actual.proto), - typedArrayToArrayBuffer(proto.proto) - ) - ); + assert.isTrue(constantTimeEqual(actual.proto, proto.proto)); assert.strictEqual(actual.timestamp, proto.timestamp); assert.sameMembers(actual.messageIds, messageIds); }); @@ -509,7 +485,7 @@ describe('sendLog', () => { const recipientUuid = getGuid(); const proto = { contentHint: 1, - proto: Buffer.from(getRandomBytes(128)), + proto: getRandomBytes(128), timestamp, }; await insertSentProto(proto, { @@ -533,12 +509,7 @@ describe('sendLog', () => { throw new Error('Failed to fetch proto!'); } assert.strictEqual(actual.contentHint, proto.contentHint); - assert.isTrue( - constantTimeEqual( - typedArrayToArrayBuffer(actual.proto), - typedArrayToArrayBuffer(proto.proto) - ) - ); + assert.isTrue(constantTimeEqual(actual.proto, proto.proto)); assert.strictEqual(actual.timestamp, proto.timestamp); assert.deepEqual(actual.messageIds, []); }); @@ -549,7 +520,7 @@ describe('sendLog', () => { const recipientUuid = getGuid(); const proto = { contentHint: 1, - proto: Buffer.from(getRandomBytes(128)), + proto: getRandomBytes(128), timestamp, }; await insertSentProto(proto, { @@ -577,7 +548,7 @@ describe('sendLog', () => { const recipientUuid = getGuid(); const proto = { contentHint: 1, - proto: Buffer.from(getRandomBytes(128)), + proto: getRandomBytes(128), timestamp, }; await insertSentProto(proto, { @@ -606,7 +577,7 @@ describe('sendLog', () => { const recipientUuid = getGuid(); const proto = { contentHint: 1, - proto: Buffer.from(getRandomBytes(128)), + proto: getRandomBytes(128), timestamp, }; await insertSentProto(proto, { diff --git a/ts/test-electron/state/ducks/conversations_test.ts b/ts/test-electron/state/ducks/conversations_test.ts index 72fb4db7b..acb3a2554 100644 --- a/ts/test-electron/state/ducks/conversations_test.ts +++ b/ts/test-electron/state/ducks/conversations_test.ts @@ -645,7 +645,7 @@ describe('both/state/ducks/conversations', () => { ...defaultSetGroupMetadataComposerState, selectedConversationIds: ['abc123'], groupName: 'Foo Bar Group', - groupAvatar: new Uint8Array([1, 2, 3]).buffer, + groupAvatar: new Uint8Array([1, 2, 3]), }, }; @@ -688,7 +688,7 @@ describe('both/state/ducks/conversations', () => { sinon.assert.calledOnce(createGroupStub); sinon.assert.calledWith(createGroupStub, { name: 'Foo Bar Group', - avatar: new Uint8Array([1, 2, 3]).buffer, + avatar: new Uint8Array([1, 2, 3]), avatars: [], expireTimer: 0, conversationIds: ['abc123'], @@ -1172,7 +1172,7 @@ describe('both/state/ducks/conversations', () => { ...getEmptyState(), composer: { ...defaultSetGroupMetadataComposerState, - groupAvatar: new ArrayBuffer(2), + groupAvatar: new Uint8Array(2), }, }; const action = setComposeGroupAvatar(undefined); @@ -1185,7 +1185,7 @@ describe('both/state/ducks/conversations', () => { }); it("can set the composer's group avatar", () => { - const avatar = new Uint8Array([1, 2, 3]).buffer; + const avatar = new Uint8Array([1, 2, 3]); const state = { ...getEmptyState(), @@ -1450,7 +1450,7 @@ describe('both/state/ducks/conversations', () => { composer: { ...defaultSetGroupMetadataComposerState, groupName: 'Foo Bar Group', - groupAvatar: new Uint8Array([4, 2]).buffer, + groupAvatar: new Uint8Array([4, 2]), }, }; const action = showChooseGroupMembers(); @@ -1460,7 +1460,7 @@ describe('both/state/ducks/conversations', () => { assert.deepEqual(result.composer, { ...defaultChooseGroupMembersComposerState, groupName: 'Foo Bar Group', - groupAvatar: new Uint8Array([4, 2]).buffer, + groupAvatar: new Uint8Array([4, 2]), }); }); @@ -1518,7 +1518,7 @@ describe('both/state/ducks/conversations', () => { searchTerm: 'foo bar', selectedConversationIds: ['abc', 'def'], groupName: 'Foo Bar Group', - groupAvatar: new Uint8Array([6, 9]).buffer, + groupAvatar: new Uint8Array([6, 9]), }, }; const action = startSettingGroupMetadata(); @@ -1528,7 +1528,7 @@ describe('both/state/ducks/conversations', () => { ...defaultSetGroupMetadataComposerState, selectedConversationIds: ['abc', 'def'], groupName: 'Foo Bar Group', - groupAvatar: new Uint8Array([6, 9]).buffer, + groupAvatar: new Uint8Array([6, 9]), }); }); diff --git a/ts/test-electron/textsecure/AccountManager_test.ts b/ts/test-electron/textsecure/AccountManager_test.ts index 1e15d7f94..6e466b0f8 100644 --- a/ts/test-electron/textsecure/AccountManager_test.ts +++ b/ts/test-electron/textsecure/AccountManager_test.ts @@ -59,7 +59,7 @@ describe('AccountManager', () => { describe('encrypted device name', () => { it('roundtrips', async () => { const deviceName = 'v2.5.0 on Ubunto 20.04'; - const encrypted = await accountManager.encryptDeviceName( + const encrypted = accountManager.encryptDeviceName( deviceName, identityKey ); @@ -72,11 +72,8 @@ describe('AccountManager', () => { assert.strictEqual(decrypted, deviceName); }); - it('handles falsey deviceName', async () => { - const encrypted = await accountManager.encryptDeviceName( - '', - identityKey - ); + it('handles falsey deviceName', () => { + const encrypted = accountManager.encryptDeviceName('', identityKey); assert.strictEqual(encrypted, null); }); }); diff --git a/ts/test-electron/textsecure/generate_keys_test.ts b/ts/test-electron/textsecure/generate_keys_test.ts index 5823c2a5c..ae4607dde 100644 --- a/ts/test-electron/textsecure/generate_keys_test.ts +++ b/ts/test-electron/textsecure/generate_keys_test.ts @@ -3,11 +3,8 @@ import { assert } from 'chai'; -import { - typedArrayToArrayBuffer as toArrayBuffer, - arrayBufferToBase64 as toBase64, - constantTimeEqual, -} from '../../Crypto'; +import { toBase64 } from '../../Bytes'; +import { constantTimeEqual } from '../../Crypto'; import { generateKeyPair } from '../../Curve'; import AccountManager, { GeneratedKeysType, @@ -17,7 +14,7 @@ import { UUID } from '../../types/UUID'; const { textsecure } = window; -const assertEqualArrayBuffers = (a: ArrayBuffer, b: ArrayBuffer) => { +const assertEqualBuffers = (a: Uint8Array, b: Uint8Array) => { assert.isTrue(constantTimeEqual(a, b)); }; @@ -54,10 +51,7 @@ describe('Key generation', function thisNeeded() { if (!keyPair) { throw new Error(`PreKey ${resultKey.keyId} not found`); } - assertEqualArrayBuffers( - resultKey.publicKey, - toArrayBuffer(keyPair.publicKey().serialize()) - ); + assertEqualBuffers(resultKey.publicKey, keyPair.publicKey().serialize()); } async function validateResultSignedKey( resultSignedKey: Pick @@ -69,9 +63,9 @@ describe('Key generation', function thisNeeded() { if (!keyPair) { throw new Error(`SignedPreKey ${resultSignedKey.keyId} not found`); } - assertEqualArrayBuffers( + assertEqualBuffers( resultSignedKey.publicKey, - toArrayBuffer(keyPair.publicKey().serialize()) + keyPair.publicKey().serialize() ); } @@ -123,7 +117,7 @@ describe('Key generation', function thisNeeded() { }); it('returns a signed prekey', () => { assert.strictEqual(result.signedPreKey.keyId, 1); - assert.instanceOf(result.signedPreKey.signature, ArrayBuffer); + assert.instanceOf(result.signedPreKey.signature, Uint8Array); return validateResultSignedKey(result.signedPreKey); }); }); @@ -157,7 +151,7 @@ describe('Key generation', function thisNeeded() { }); it('returns a signed prekey', () => { assert.strictEqual(result.signedPreKey.keyId, 2); - assert.instanceOf(result.signedPreKey.signature, ArrayBuffer); + assert.instanceOf(result.signedPreKey.signature, Uint8Array); return validateResultSignedKey(result.signedPreKey); }); }); @@ -191,7 +185,7 @@ describe('Key generation', function thisNeeded() { }); it('result contains a signed prekey', () => { assert.strictEqual(result.signedPreKey.keyId, 3); - assert.instanceOf(result.signedPreKey.signature, ArrayBuffer); + assert.instanceOf(result.signedPreKey.signature, Uint8Array); return validateResultSignedKey(result.signedPreKey); }); }); diff --git a/ts/test-electron/util/canvasToBlob_test.ts b/ts/test-electron/util/canvasToBlob_test.ts index 8e7af9a95..744da6ee9 100644 --- a/ts/test-electron/util/canvasToBlob_test.ts +++ b/ts/test-electron/util/canvasToBlob_test.ts @@ -26,7 +26,7 @@ describe('canvasToBlob', () => { const result = await canvasToBlob(canvas); assert.strictEqual( - sniffImageMimeType(await result.arrayBuffer()), + sniffImageMimeType(new Uint8Array(await result.arrayBuffer())), IMAGE_JPEG ); @@ -39,7 +39,7 @@ describe('canvasToBlob', () => { const result = await canvasToBlob(canvas, IMAGE_PNG); assert.strictEqual( - sniffImageMimeType(await result.arrayBuffer()), + sniffImageMimeType(new Uint8Array(await result.arrayBuffer())), IMAGE_PNG ); }); diff --git a/ts/test-electron/util/canvasToArrayBuffer_test.ts b/ts/test-electron/util/canvasToBytes_test.ts similarity index 67% rename from ts/test-electron/util/canvasToArrayBuffer_test.ts rename to ts/test-electron/util/canvasToBytes_test.ts index b1f533a2e..27720000d 100644 --- a/ts/test-electron/util/canvasToArrayBuffer_test.ts +++ b/ts/test-electron/util/canvasToBytes_test.ts @@ -5,9 +5,9 @@ import { assert } from 'chai'; import { IMAGE_JPEG, IMAGE_PNG } from '../../types/MIME'; import { sniffImageMimeType } from '../../util/sniffImageMimeType'; -import { canvasToArrayBuffer } from '../../util/canvasToArrayBuffer'; +import { canvasToBytes } from '../../util/canvasToBytes'; -describe('canvasToArrayBuffer', () => { +describe('canvasToBytes', () => { let canvas: HTMLCanvasElement; beforeEach(() => { canvas = document.createElement('canvas'); @@ -22,18 +22,18 @@ describe('canvasToArrayBuffer', () => { context.fillRect(10, 10, 20, 20); }); - it('converts a canvas to an ArrayBuffer, JPEG by default', async () => { - const result = await canvasToArrayBuffer(canvas); + it('converts a canvas to an Uint8Array, JPEG by default', async () => { + const result = await canvasToBytes(canvas); assert.strictEqual(sniffImageMimeType(result), IMAGE_JPEG); // These are just smoke tests. - assert.instanceOf(result, ArrayBuffer); + assert.instanceOf(result, Uint8Array); assert.isAtLeast(result.byteLength, 50); }); - it('can convert a canvas to a PNG ArrayBuffer', async () => { - const result = await canvasToArrayBuffer(canvas, IMAGE_PNG); + it('can convert a canvas to a PNG Uint8Array', async () => { + const result = await canvasToBytes(canvas, IMAGE_PNG); assert.strictEqual(sniffImageMimeType(result), IMAGE_PNG); }); diff --git a/ts/test-electron/util/encryptProfileData_test.ts b/ts/test-electron/util/encryptProfileData_test.ts index b112ba0e0..88018643c 100644 --- a/ts/test-electron/util/encryptProfileData_test.ts +++ b/ts/test-electron/util/encryptProfileData_test.ts @@ -4,24 +4,24 @@ import { assert } from 'chai'; import { v4 as uuid } from 'uuid'; -import Crypto from '../../textsecure/Crypto'; +import * as Bytes from '../../Bytes'; import { - arrayBufferToBase64, - base64ToArrayBuffer, - stringFromBytes, trimForDisplay, + getRandomBytes, + decryptProfileName, + decryptProfile, } from '../../Crypto'; import { encryptProfileData } from '../../util/encryptProfileData'; describe('encryptProfileData', () => { it('encrypts and decrypts properly', async () => { - const keyBuffer = Crypto.getRandomBytes(32); + const keyBuffer = getRandomBytes(32); const conversation = { aboutEmoji: '🐢', aboutText: 'I like turtles', familyName: 'Kid', firstName: 'Zombie', - profileKey: arrayBufferToBase64(keyBuffer), + profileKey: Bytes.toBase64(keyBuffer), uuid: uuid(), // To satisfy TS @@ -39,17 +39,17 @@ describe('encryptProfileData', () => { assert.isDefined(encrypted.name); assert.isDefined(encrypted.commitment); - const decryptedProfileNameBytes = await Crypto.decryptProfileName( + const decryptedProfileNameBytes = decryptProfileName( encrypted.name, keyBuffer ); assert.equal( - stringFromBytes(decryptedProfileNameBytes.given), + Bytes.toString(decryptedProfileNameBytes.given), conversation.firstName ); if (decryptedProfileNameBytes.family) { assert.equal( - stringFromBytes(decryptedProfileNameBytes.family), + Bytes.toString(decryptedProfileNameBytes.family), conversation.familyName ); } else { @@ -57,12 +57,12 @@ describe('encryptProfileData', () => { } if (encrypted.about) { - const decryptedAboutBytes = await Crypto.decryptProfile( - base64ToArrayBuffer(encrypted.about), + const decryptedAboutBytes = decryptProfile( + Bytes.fromBase64(encrypted.about), keyBuffer ); assert.equal( - stringFromBytes(trimForDisplay(decryptedAboutBytes)), + Bytes.toString(trimForDisplay(decryptedAboutBytes)), conversation.aboutText ); } else { @@ -70,12 +70,12 @@ describe('encryptProfileData', () => { } if (encrypted.aboutEmoji) { - const decryptedAboutEmojiBytes = await Crypto.decryptProfile( - base64ToArrayBuffer(encrypted.aboutEmoji), + const decryptedAboutEmojiBytes = await decryptProfile( + Bytes.fromBase64(encrypted.aboutEmoji), keyBuffer ); assert.equal( - stringFromBytes(trimForDisplay(decryptedAboutEmojiBytes)), + Bytes.toString(trimForDisplay(decryptedAboutEmojiBytes)), conversation.aboutEmoji ); } else { diff --git a/ts/test-electron/util/imagePathToArrayBuffer_test.ts b/ts/test-electron/util/imagePathToBytes_test.ts similarity index 50% rename from ts/test-electron/util/imagePathToArrayBuffer_test.ts rename to ts/test-electron/util/imagePathToBytes_test.ts index 295390092..d00c098af 100644 --- a/ts/test-electron/util/imagePathToArrayBuffer_test.ts +++ b/ts/test-electron/util/imagePathToBytes_test.ts @@ -4,16 +4,16 @@ import { assert } from 'chai'; import path from 'path'; -import { imagePathToArrayBuffer } from '../../util/imagePathToArrayBuffer'; +import { imagePathToBytes } from '../../util/imagePathToBytes'; -describe('imagePathToArrayBuffer', () => { - it('converts an image to an ArrayBuffer', async () => { +describe('imagePathToBytes', () => { + it('converts an image to an Bytes', async () => { const avatarPath = path.join( __dirname, '../../../fixtures/kitten-3-64-64.jpg' ); - const buffer = await imagePathToArrayBuffer(avatarPath); + const buffer = await imagePathToBytes(avatarPath); assert.isDefined(buffer); - assert(buffer instanceof ArrayBuffer); + assert(buffer instanceof Uint8Array); }); }); diff --git a/ts/test-node/types/Attachment_test.ts b/ts/test-node/types/Attachment_test.ts index fffc136b7..8f02c3b74 100644 --- a/ts/test-node/types/Attachment_test.ts +++ b/ts/test-node/types/Attachment_test.ts @@ -6,7 +6,7 @@ import { assert } from 'chai'; import * as Attachment from '../../types/Attachment'; import * as MIME from '../../types/MIME'; import { SignalService } from '../../protobuf'; -import { stringToArrayBuffer } from '../../util/stringToArrayBuffer'; +import * as Bytes from '../../Bytes'; import * as logger from '../../logging/log'; describe('Attachment', () => { @@ -38,7 +38,7 @@ describe('Attachment', () => { describe('getFileExtension', () => { it('should return file extension from content type', () => { const input: Attachment.AttachmentType = { - data: stringToArrayBuffer('foo'), + data: Bytes.fromString('foo'), contentType: MIME.IMAGE_GIF, }; assert.strictEqual(Attachment.getFileExtension(input), 'gif'); @@ -46,7 +46,7 @@ describe('Attachment', () => { it('should return file extension for QuickTime videos', () => { const input: Attachment.AttachmentType = { - data: stringToArrayBuffer('foo'), + data: Bytes.fromString('foo'), contentType: MIME.VIDEO_QUICKTIME, }; assert.strictEqual(Attachment.getFileExtension(input), 'mov'); @@ -58,7 +58,7 @@ describe('Attachment', () => { it('should return existing filename if present', () => { const attachment: Attachment.AttachmentType = { fileName: 'funny-cat.mov', - data: stringToArrayBuffer('foo'), + data: Bytes.fromString('foo'), contentType: MIME.VIDEO_QUICKTIME, }; const actual = Attachment.getSuggestedFilename({ attachment }); @@ -69,7 +69,7 @@ describe('Attachment', () => { context('for attachment without filename', () => { it('should generate a filename based on timestamp', () => { const attachment: Attachment.AttachmentType = { - data: stringToArrayBuffer('foo'), + data: Bytes.fromString('foo'), contentType: MIME.VIDEO_QUICKTIME, }; const timestamp = new Date(new Date(0).getTimezoneOffset() * 60 * 1000); @@ -84,7 +84,7 @@ describe('Attachment', () => { context('for attachment with index', () => { it('should generate a filename based on timestamp', () => { const attachment: Attachment.AttachmentType = { - data: stringToArrayBuffer('foo'), + data: Bytes.fromString('foo'), contentType: MIME.VIDEO_QUICKTIME, }; const timestamp = new Date(new Date(0).getTimezoneOffset() * 60 * 1000); @@ -103,7 +103,7 @@ describe('Attachment', () => { it('should return true for images', () => { const attachment: Attachment.AttachmentType = { fileName: 'meme.gif', - data: stringToArrayBuffer('gif'), + data: Bytes.fromString('gif'), contentType: MIME.IMAGE_GIF, }; assert.isTrue(Attachment.isVisualMedia(attachment)); @@ -112,7 +112,7 @@ describe('Attachment', () => { it('should return true for videos', () => { const attachment: Attachment.AttachmentType = { fileName: 'meme.mp4', - data: stringToArrayBuffer('mp4'), + data: Bytes.fromString('mp4'), contentType: MIME.VIDEO_MP4, }; assert.isTrue(Attachment.isVisualMedia(attachment)); @@ -122,7 +122,7 @@ describe('Attachment', () => { const attachment: Attachment.AttachmentType = { fileName: 'Voice Message.aac', flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE, - data: stringToArrayBuffer('voice message'), + data: Bytes.fromString('voice message'), contentType: MIME.AUDIO_AAC, }; assert.isFalse(Attachment.isVisualMedia(attachment)); @@ -131,7 +131,7 @@ describe('Attachment', () => { it('should return false for other attachments', () => { const attachment: Attachment.AttachmentType = { fileName: 'foo.json', - data: stringToArrayBuffer('{"foo": "bar"}'), + data: Bytes.fromString('{"foo": "bar"}'), contentType: MIME.APPLICATION_JSON, }; assert.isFalse(Attachment.isVisualMedia(attachment)); @@ -142,7 +142,7 @@ describe('Attachment', () => { it('should return true for JSON', () => { const attachment: Attachment.AttachmentType = { fileName: 'foo.json', - data: stringToArrayBuffer('{"foo": "bar"}'), + data: Bytes.fromString('{"foo": "bar"}'), contentType: MIME.APPLICATION_JSON, }; assert.isTrue(Attachment.isFile(attachment)); @@ -151,7 +151,7 @@ describe('Attachment', () => { it('should return false for images', () => { const attachment: Attachment.AttachmentType = { fileName: 'meme.gif', - data: stringToArrayBuffer('gif'), + data: Bytes.fromString('gif'), contentType: MIME.IMAGE_GIF, }; assert.isFalse(Attachment.isFile(attachment)); @@ -160,7 +160,7 @@ describe('Attachment', () => { it('should return false for videos', () => { const attachment: Attachment.AttachmentType = { fileName: 'meme.mp4', - data: stringToArrayBuffer('mp4'), + data: Bytes.fromString('mp4'), contentType: MIME.VIDEO_MP4, }; assert.isFalse(Attachment.isFile(attachment)); @@ -170,7 +170,7 @@ describe('Attachment', () => { const attachment: Attachment.AttachmentType = { fileName: 'Voice Message.aac', flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE, - data: stringToArrayBuffer('voice message'), + data: Bytes.fromString('voice message'), contentType: MIME.AUDIO_AAC, }; assert.isFalse(Attachment.isFile(attachment)); @@ -182,7 +182,7 @@ describe('Attachment', () => { const attachment: Attachment.AttachmentType = { fileName: 'Voice Message.aac', flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE, - data: stringToArrayBuffer('voice message'), + data: Bytes.fromString('voice message'), contentType: MIME.AUDIO_AAC, }; assert.isTrue(Attachment.isVoiceMessage(attachment)); @@ -190,7 +190,7 @@ describe('Attachment', () => { it('should return true for legacy Android voice message attachment', () => { const attachment: Attachment.AttachmentType = { - data: stringToArrayBuffer('voice message'), + data: Bytes.fromString('voice message'), contentType: MIME.AUDIO_MP3, }; assert.isTrue(Attachment.isVoiceMessage(attachment)); @@ -199,7 +199,7 @@ describe('Attachment', () => { it('should return false for other attachments', () => { const attachment: Attachment.AttachmentType = { fileName: 'foo.gif', - data: stringToArrayBuffer('foo'), + data: Bytes.fromString('foo'), contentType: MIME.IMAGE_GIF, }; assert.isFalse(Attachment.isVoiceMessage(attachment)); @@ -359,7 +359,7 @@ describe('Attachment', () => { it('should write data to disk and store relative path to it', async () => { const input = { contentType: MIME.IMAGE_JPEG, - data: stringToArrayBuffer('Above us only sky'), + data: Bytes.fromString('Above us only sky'), fileName: 'foo.jpg', size: 1111, }; @@ -371,8 +371,8 @@ describe('Attachment', () => { size: 1111, }; - const expectedAttachmentData = stringToArrayBuffer('Above us only sky'); - const writeNewAttachmentData = async (attachmentData: ArrayBuffer) => { + const expectedAttachmentData = Bytes.fromString('Above us only sky'); + const writeNewAttachmentData = async (attachmentData: Uint8Array) => { assert.deepEqual(attachmentData, expectedAttachmentData); return 'abc/abcdefgh123456789'; }; @@ -419,7 +419,7 @@ describe('Attachment', () => { Attachment.migrateDataToFileSystem(input, { writeNewAttachmentData, }), - 'Expected `attachment.data` to be an array buffer; got: number' + 'Expected `attachment.data` to be a typed array; got: number' ); }); }); diff --git a/ts/test-node/types/EmbeddedContact_test.ts b/ts/test-node/types/EmbeddedContact_test.ts index 85188f239..81e870ea8 100644 --- a/ts/test-node/types/EmbeddedContact_test.ts +++ b/ts/test-node/types/EmbeddedContact_test.ts @@ -6,7 +6,6 @@ import * as sinon from 'sinon'; import { IMAGE_GIF, IMAGE_PNG } from '../../types/MIME'; import { MessageAttributesType } from '../../model-types.d'; -import { stringToArrayBuffer } from '../../util/stringToArrayBuffer'; import { Avatar, Email, @@ -400,7 +399,7 @@ describe('Contact', () => { otherKey: 'otherValue', avatar: { contentType: 'image/png', - data: stringToArrayBuffer('It’s easy if you try'), + data: Buffer.from('It’s easy if you try'), }, } as unknown) as Avatar, }, diff --git a/ts/test-node/types/message/initializeAttachmentMetadata_test.ts b/ts/test-node/types/message/initializeAttachmentMetadata_test.ts index f8f2bc273..479ade56f 100644 --- a/ts/test-node/types/message/initializeAttachmentMetadata_test.ts +++ b/ts/test-node/types/message/initializeAttachmentMetadata_test.ts @@ -7,7 +7,7 @@ import * as Message from '../../../types/message/initializeAttachmentMetadata'; import { IncomingMessage } from '../../../types/Message'; import { SignalService } from '../../../protobuf'; import * as MIME from '../../../types/MIME'; -import { stringToArrayBuffer } from '../../../util/stringToArrayBuffer'; +import * as Bytes from '../../../Bytes'; describe('Message', () => { describe('initializeAttachmentMetadata', () => { @@ -22,7 +22,7 @@ describe('Message', () => { attachments: [ { contentType: MIME.IMAGE_JPEG, - data: stringToArrayBuffer('foo'), + data: Bytes.fromString('foo'), fileName: 'foo.jpg', size: 1111, }, @@ -38,7 +38,7 @@ describe('Message', () => { attachments: [ { contentType: MIME.IMAGE_JPEG, - data: stringToArrayBuffer('foo'), + data: Bytes.fromString('foo'), fileName: 'foo.jpg', size: 1111, }, @@ -63,7 +63,7 @@ describe('Message', () => { attachments: [ { contentType: MIME.APPLICATION_OCTET_STREAM, - data: stringToArrayBuffer('foo'), + data: Bytes.fromString('foo'), fileName: 'foo.bin', size: 1111, }, @@ -79,7 +79,7 @@ describe('Message', () => { attachments: [ { contentType: MIME.APPLICATION_OCTET_STREAM, - data: stringToArrayBuffer('foo'), + data: Bytes.fromString('foo'), fileName: 'foo.bin', size: 1111, }, @@ -105,7 +105,7 @@ describe('Message', () => { { contentType: MIME.AUDIO_AAC, flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE, - data: stringToArrayBuffer('foo'), + data: Bytes.fromString('foo'), fileName: 'Voice Message.aac', size: 1111, }, @@ -122,7 +122,7 @@ describe('Message', () => { { contentType: MIME.AUDIO_AAC, flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE, - data: stringToArrayBuffer('foo'), + data: Bytes.fromString('foo'), fileName: 'Voice Message.aac', size: 1111, }, @@ -147,7 +147,7 @@ describe('Message', () => { attachments: [ { contentType: MIME.LONG_MESSAGE, - data: stringToArrayBuffer('foo'), + data: Bytes.fromString('foo'), fileName: 'message.txt', size: 1111, }, @@ -163,7 +163,7 @@ describe('Message', () => { attachments: [ { contentType: MIME.LONG_MESSAGE, - data: stringToArrayBuffer('foo'), + data: Bytes.fromString('foo'), fileName: 'message.txt', size: 1111, }, diff --git a/ts/test-node/util/getProvisioningUrl_test.ts b/ts/test-node/util/getProvisioningUrl_test.ts index 2de6a89fb..478d741bd 100644 --- a/ts/test-node/util/getProvisioningUrl_test.ts +++ b/ts/test-node/util/getProvisioningUrl_test.ts @@ -4,8 +4,6 @@ import { assert } from 'chai'; import { size } from '../../util/iterables'; -import { typedArrayToArrayBuffer } from '../../Crypto'; - import { getProvisioningUrl } from '../../util/getProvisioningUrl'; // It'd be nice to run these tests in the renderer, too, but [Chromium's `URL` doesn't @@ -17,7 +15,7 @@ describe('getProvisioningUrl', () => { const uuid = 'a08bf1fd-1799-427f-a551-70af747e3956'; const publicKey = new Uint8Array([9, 8, 7, 6, 5, 4, 3]); - const result = getProvisioningUrl(uuid, typedArrayToArrayBuffer(publicKey)); + const result = getProvisioningUrl(uuid, publicKey); const resultUrl = new URL(result); assert.strictEqual(resultUrl.protocol, 'sgnl:'); diff --git a/ts/test-node/util/sniffImageMimeType_test.ts b/ts/test-node/util/sniffImageMimeType_test.ts index c58e57fad..2115f0be0 100644 --- a/ts/test-node/util/sniffImageMimeType_test.ts +++ b/ts/test-node/util/sniffImageMimeType_test.ts @@ -13,8 +13,6 @@ import { IMAGE_WEBP, } from '../../types/MIME'; -import { typedArrayToArrayBuffer } from '../../Crypto'; - import { sniffImageMimeType } from '../../util/sniffImageMimeType'; describe('sniffImageMimeType', () => { @@ -89,11 +87,4 @@ describe('sniffImageMimeType', () => { IMAGE_JPEG ); }); - - it('handles ArrayBuffers', async () => { - const arrayBuffer = typedArrayToArrayBuffer( - await fixture('kitten-1-64-64.jpg') - ); - assert.strictEqual(sniffImageMimeType(arrayBuffer), IMAGE_JPEG); - }); }); diff --git a/ts/textsecure.d.ts b/ts/textsecure.d.ts index 5417441ac..b717256ec 100644 --- a/ts/textsecure.d.ts +++ b/ts/textsecure.d.ts @@ -3,7 +3,6 @@ import { UnidentifiedSenderMessageContent } from '@signalapp/signal-client'; -import Crypto from './textsecure/Crypto'; import MessageSender from './textsecure/SendMessage'; import SyncRequest from './textsecure/SyncRequest'; import EventTarget from './textsecure/EventTarget'; @@ -37,7 +36,6 @@ export type UnprocessedType = { export { StorageServiceCallOptionsType, StorageServiceCredentials }; export type TextSecureType = { - crypto: typeof Crypto; storage: Storage; server: WebAPIType; messageSender: MessageSender; diff --git a/ts/textsecure/AccountManager.ts b/ts/textsecure/AccountManager.ts index 8308cca2c..65279fad8 100644 --- a/ts/textsecure/AccountManager.ts +++ b/ts/textsecure/AccountManager.ts @@ -13,7 +13,6 @@ import EventTarget from './EventTarget'; import type { WebAPIType } from './WebAPI'; import { HTTPError } from './Errors'; import { KeyPairType, CompatSignedPreKeyType } from './Types.d'; -import utils from './Helpers'; import ProvisioningCipher from './ProvisioningCipher'; import { IncomingWebSocketRequest } from './WebsocketResources'; import createTaskWithTimeout from './TaskWithTimeout'; @@ -22,8 +21,8 @@ import { deriveAccessKey, generateRegistrationId, getRandomBytes, - typedArrayToArrayBuffer, - arrayBufferToBase64, + decryptDeviceName, + encryptDeviceName, } from '../Crypto'; import { generateKeyPair, @@ -45,9 +44,6 @@ const PREKEY_ROTATION_AGE = DAY * 1.5; const PROFILE_KEY_LENGTH = 32; const SIGNED_KEY_GEN_BATCH_SIZE = 100; -// TODO: remove once we move away from ArrayBuffers -const FIXMEU8 = Uint8Array; - function getIdentifier(id: string | undefined) { if (!id || !id.length) { return id; @@ -64,15 +60,15 @@ function getIdentifier(id: string | undefined) { export type GeneratedKeysType = { preKeys: Array<{ keyId: number; - publicKey: ArrayBuffer; + publicKey: Uint8Array; }>; signedPreKey: { keyId: number; - publicKey: ArrayBuffer; - signature: ArrayBuffer; + publicKey: Uint8Array; + signature: Uint8Array; keyPair: KeyPairType; }; - identityKey: ArrayBuffer; + identityKey: Uint8Array; }; export default class AccountManager extends EventTarget { @@ -94,19 +90,16 @@ export default class AccountManager extends EventTarget { return this.server.requestVerificationSMS(number); } - async encryptDeviceName(name: string, identityKey: KeyPairType) { + encryptDeviceName(name: string, identityKey: KeyPairType) { if (!name) { return null; } - const encrypted = await window.Signal.Crypto.encryptDeviceName( - name, - identityKey.pubKey - ); + const encrypted = encryptDeviceName(name, identityKey.pubKey); const proto = new Proto.DeviceName(); - proto.ephemeralPublic = new FIXMEU8(encrypted.ephemeralPublic); - proto.syntheticIv = new FIXMEU8(encrypted.syntheticIv); - proto.ciphertext = new FIXMEU8(encrypted.ciphertext); + proto.ephemeralPublic = encrypted.ephemeralPublic; + proto.syntheticIv = encrypted.syntheticIv; + proto.ciphertext = encrypted.ciphertext; const bytes = Proto.DeviceName.encode(proto).finish(); return Bytes.toBase64(bytes); @@ -127,16 +120,8 @@ export default class AccountManager extends EventTarget { proto.ephemeralPublic && proto.syntheticIv && proto.ciphertext, 'Missing required fields in DeviceName' ); - const encrypted = { - ephemeralPublic: typedArrayToArrayBuffer(proto.ephemeralPublic), - syntheticIv: typedArrayToArrayBuffer(proto.syntheticIv), - ciphertext: typedArrayToArrayBuffer(proto.ciphertext), - }; - const name = await window.Signal.Crypto.decryptDeviceName( - encrypted, - identityKey.privKey - ); + const name = decryptDeviceName(proto, identityKey.privKey); return name; } @@ -155,10 +140,7 @@ export default class AccountManager extends EventTarget { identityKeyPair !== undefined, "Can't encrypt device name without identity key pair" ); - const base64 = await this.encryptDeviceName( - deviceName || '', - identityKeyPair - ); + const base64 = this.encryptDeviceName(deviceName || '', identityKeyPair); if (base64) { await this.server.updateDeviceName(base64); @@ -173,7 +155,7 @@ export default class AccountManager extends EventTarget { return this.queueTask(async () => { const identityKeyPair = generateKeyPair(); const profileKey = getRandomBytes(PROFILE_KEY_LENGTH); - const accessKey = await deriveAccessKey(profileKey); + const accessKey = deriveAccessKey(profileKey); await this.createAccount( number, @@ -469,15 +451,15 @@ export default class AccountManager extends EventTarget { number: string, verificationCode: string, identityKeyPair: KeyPairType, - profileKey: ArrayBuffer | undefined, + profileKey: Uint8Array | undefined, deviceName: string | null, userAgent?: string | null, readReceipts?: boolean | null, - options: { accessKey?: ArrayBuffer; uuid?: string } = {} + options: { accessKey?: Uint8Array; uuid?: string } = {} ): Promise { const { storage } = window.textsecure; const { accessKey, uuid } = options; - let password = btoa(utils.getString(getRandomBytes(16))); + let password = Bytes.toBase64(getRandomBytes(16)); password = password.substring(0, password.length - 2); const registrationId = generateRegistrationId(); @@ -486,10 +468,7 @@ export default class AccountManager extends EventTarget { let encryptedDeviceName; if (deviceName) { - encryptedDeviceName = await this.encryptDeviceName( - deviceName, - identityKeyPair - ); + encryptedDeviceName = this.encryptDeviceName(deviceName, identityKeyPair); await this.deviceNameIsEncrypted(); } @@ -601,8 +580,8 @@ export default class AccountManager extends EventTarget { const identityKeyMap = { ...(storage.get('identityKeyMap') || {}), [ourUuid]: { - pubKey: arrayBufferToBase64(identityKeyPair.pubKey), - privKey: arrayBufferToBase64(identityKeyPair.privKey), + pubKey: Bytes.toBase64(identityKeyPair.pubKey), + privKey: Bytes.toBase64(identityKeyPair.privKey), }, }; const registrationIdMap = { diff --git a/ts/textsecure/ContactsParser.ts b/ts/textsecure/ContactsParser.ts index 84634641c..d89cbf83f 100644 --- a/ts/textsecure/ContactsParser.ts +++ b/ts/textsecure/ContactsParser.ts @@ -7,7 +7,6 @@ import { Reader } from 'protobufjs'; import { SignalService as Proto } from '../protobuf'; import { normalizeUuid } from '../util/normalizeUuid'; -import { typedArrayToArrayBuffer } from '../Crypto'; import * as log from '../logging/log'; import Avatar = Proto.ContactDetails.IAvatar; @@ -22,24 +21,21 @@ export type MessageWithAvatar = Omit< Message, 'avatar' > & { - avatar?: (Avatar & { data: ArrayBuffer }) | null; + avatar?: (Avatar & { data: Uint8Array }) | null; }; export type ModifiedGroupDetails = MessageWithAvatar; export type ModifiedContactDetails = MessageWithAvatar; -// TODO: remove once we move away from ArrayBuffers -const FIXMEU8 = Uint8Array; - class ParserBase< Message extends OptionalAvatar, Decoder extends DecoderBase > { protected readonly reader: Reader; - constructor(arrayBuffer: ArrayBuffer, private readonly decoder: Decoder) { - this.reader = new Reader(new FIXMEU8(arrayBuffer)); + constructor(bytes: Uint8Array, private readonly decoder: Decoder) { + this.reader = new Reader(bytes); } protected decodeDelimited(): MessageWithAvatar | undefined { @@ -74,7 +70,7 @@ class ParserBase< avatar: { ...proto.avatar, - data: typedArrayToArrayBuffer(avatarData), + data: avatarData, }, }; } catch (error) { @@ -91,7 +87,7 @@ export class GroupBuffer extends ParserBase< Proto.GroupDetails, typeof Proto.GroupDetails > { - constructor(arrayBuffer: ArrayBuffer) { + constructor(arrayBuffer: Uint8Array) { super(arrayBuffer, Proto.GroupDetails); } @@ -124,7 +120,7 @@ export class ContactBuffer extends ParserBase< Proto.ContactDetails, typeof Proto.ContactDetails > { - constructor(arrayBuffer: ArrayBuffer) { + constructor(arrayBuffer: Uint8Array) { super(arrayBuffer, Proto.ContactDetails); } diff --git a/ts/textsecure/Crypto.ts b/ts/textsecure/Crypto.ts deleted file mode 100644 index 5d4a9e6d8..000000000 --- a/ts/textsecure/Crypto.ts +++ /dev/null @@ -1,255 +0,0 @@ -// Copyright 2020-2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable no-bitwise */ -/* eslint-disable more/no-then */ -import { - decryptAes256CbcPkcsPadding, - encryptAes256CbcPkcsPadding, - getRandomBytes as outerGetRandomBytes, - hmacSha256, - sha256, - verifyHmacSha256, - base64ToArrayBuffer, - typedArrayToArrayBuffer, -} from '../Crypto'; - -const PROFILE_IV_LENGTH = 12; // bytes -const PROFILE_KEY_LENGTH = 32; // bytes -const PROFILE_TAG_LENGTH = 128; // bits - -// bytes -export const PaddedLengths = { - Name: [53, 257], - About: [128, 254, 512], - AboutEmoji: [32], - PaymentAddress: [554], -}; - -type EncryptedAttachment = { - ciphertext: ArrayBuffer; - digest: ArrayBuffer; -}; - -async function verifyDigest( - data: ArrayBuffer, - theirDigest: ArrayBuffer -): Promise { - return window.crypto.subtle - .digest({ name: 'SHA-256' }, data) - .then(ourDigest => { - const a = new Uint8Array(ourDigest); - const b = new Uint8Array(theirDigest); - let result = 0; - for (let i = 0; i < theirDigest.byteLength; i += 1) { - result |= a[i] ^ b[i]; - } - if (result !== 0) { - throw new Error('Bad digest'); - } - }); -} - -const Crypto = { - async decryptAttachment( - encryptedBin: ArrayBuffer, - keys: ArrayBuffer, - theirDigest?: ArrayBuffer - ): Promise { - if (keys.byteLength !== 64) { - throw new Error('Got invalid length attachment keys'); - } - if (encryptedBin.byteLength < 16 + 32) { - throw new Error('Got invalid length attachment'); - } - - const aesKey = keys.slice(0, 32); - const macKey = keys.slice(32, 64); - - const iv = encryptedBin.slice(0, 16); - const ciphertext = encryptedBin.slice(16, encryptedBin.byteLength - 32); - const ivAndCiphertext = encryptedBin.slice(0, encryptedBin.byteLength - 32); - const mac = encryptedBin.slice( - encryptedBin.byteLength - 32, - encryptedBin.byteLength - ); - - await verifyHmacSha256(ivAndCiphertext, macKey, mac, 32); - - if (theirDigest) { - await verifyDigest(encryptedBin, theirDigest); - } - - return decryptAes256CbcPkcsPadding(aesKey, ciphertext, iv); - }, - - async encryptAttachment( - plaintext: ArrayBuffer, - keys: ArrayBuffer, - iv: ArrayBuffer - ): Promise { - if (!(plaintext instanceof ArrayBuffer) && !ArrayBuffer.isView(plaintext)) { - throw new TypeError( - `\`plaintext\` must be an \`ArrayBuffer\` or \`ArrayBufferView\`; got: ${typeof plaintext}` - ); - } - - if (keys.byteLength !== 64) { - throw new Error('Got invalid length attachment keys'); - } - if (iv.byteLength !== 16) { - throw new Error('Got invalid length attachment iv'); - } - const aesKey = keys.slice(0, 32); - const macKey = keys.slice(32, 64); - - const ciphertext = await encryptAes256CbcPkcsPadding(aesKey, plaintext, iv); - - const ivAndCiphertext = new Uint8Array(16 + ciphertext.byteLength); - ivAndCiphertext.set(new Uint8Array(iv)); - ivAndCiphertext.set(new Uint8Array(ciphertext), 16); - - const mac = await hmacSha256(macKey, ivAndCiphertext.buffer as ArrayBuffer); - - const encryptedBin = new Uint8Array(16 + ciphertext.byteLength + 32); - encryptedBin.set(ivAndCiphertext); - encryptedBin.set(new Uint8Array(mac), 16 + ciphertext.byteLength); - const digest = await sha256(encryptedBin.buffer as ArrayBuffer); - - return { - ciphertext: encryptedBin.buffer, - digest, - }; - }, - - async encryptProfile( - data: ArrayBuffer, - key: ArrayBuffer - ): Promise { - const iv = outerGetRandomBytes(PROFILE_IV_LENGTH); - if (key.byteLength !== PROFILE_KEY_LENGTH) { - throw new Error('Got invalid length profile key'); - } - if (iv.byteLength !== PROFILE_IV_LENGTH) { - throw new Error('Got invalid length profile iv'); - } - return window.crypto.subtle - .importKey('raw', key, { name: 'AES-GCM' } as any, false, ['encrypt']) - .then(async keyForEncryption => - window.crypto.subtle - .encrypt( - { name: 'AES-GCM', iv, tagLength: PROFILE_TAG_LENGTH }, - keyForEncryption, - data - ) - .then(ciphertext => { - const ivAndCiphertext = new Uint8Array( - PROFILE_IV_LENGTH + ciphertext.byteLength - ); - ivAndCiphertext.set(new Uint8Array(iv)); - ivAndCiphertext.set(new Uint8Array(ciphertext), PROFILE_IV_LENGTH); - return ivAndCiphertext.buffer; - }) - ); - }, - - async decryptProfile( - data: ArrayBuffer, - key: ArrayBuffer - ): Promise { - if (data.byteLength < 12 + 16 + 1) { - throw new Error(`Got too short input: ${data.byteLength}`); - } - const iv = data.slice(0, PROFILE_IV_LENGTH); - const ciphertext = data.slice(PROFILE_IV_LENGTH, data.byteLength); - if (key.byteLength !== PROFILE_KEY_LENGTH) { - throw new Error('Got invalid length profile key'); - } - if (iv.byteLength !== PROFILE_IV_LENGTH) { - throw new Error('Got invalid length profile iv'); - } - const error = new Error(); // save stack - return window.crypto.subtle - .importKey('raw', key, { name: 'AES-GCM' } as any, false, ['decrypt']) - .then(async keyForEncryption => - window.crypto.subtle - .decrypt( - { name: 'AES-GCM', iv, tagLength: PROFILE_TAG_LENGTH }, - keyForEncryption, - ciphertext - ) - .catch((e: Error) => { - if (e.name === 'OperationError') { - // bad mac, basically. - error.message = - 'Failed to decrypt profile data. Most likely the profile key has changed.'; - error.name = 'ProfileDecryptError'; - throw error; - } - - return (undefined as unknown) as ArrayBuffer; // uses of this function are not guarded - }) - ); - }, - - async encryptProfileItemWithPadding( - item: ArrayBuffer, - profileKey: ArrayBuffer, - paddedLengths: typeof PaddedLengths[keyof typeof PaddedLengths] - ): Promise { - const paddedLength = paddedLengths.find( - (length: number) => item.byteLength <= length - ); - if (!paddedLength) { - throw new Error('Oversized value'); - } - const padded = new Uint8Array(paddedLength); - padded.set(new Uint8Array(item)); - return Crypto.encryptProfile(padded.buffer as ArrayBuffer, profileKey); - }, - - async decryptProfileName( - encryptedProfileName: string, - key: ArrayBuffer - ): Promise<{ given: ArrayBuffer; family: ArrayBuffer | null }> { - const data = base64ToArrayBuffer(encryptedProfileName); - return Crypto.decryptProfile(data, key).then(decrypted => { - const padded = new Uint8Array(decrypted); - - // Given name is the start of the string to the first null character - let givenEnd; - for (givenEnd = 0; givenEnd < padded.length; givenEnd += 1) { - if (padded[givenEnd] === 0x00) { - break; - } - } - - // Family name is the next chunk of non-null characters after that first null - let familyEnd; - for ( - familyEnd = givenEnd + 1; - familyEnd < padded.length; - familyEnd += 1 - ) { - if (padded[familyEnd] === 0x00) { - break; - } - } - const foundFamilyName = familyEnd > givenEnd + 1; - - return { - given: typedArrayToArrayBuffer(padded.slice(0, givenEnd)), - family: foundFamilyName - ? typedArrayToArrayBuffer(padded.slice(givenEnd + 1, familyEnd)) - : null, - }; - }); - }, - - getRandomBytes(size: number): ArrayBuffer { - return outerGetRandomBytes(size); - }, -}; - -export default Crypto; diff --git a/ts/textsecure/Errors.ts b/ts/textsecure/Errors.ts index b1bb27c1d..1bff46505 100644 --- a/ts/textsecure/Errors.ts +++ b/ts/textsecure/Errors.ts @@ -77,14 +77,14 @@ export class ReplayableError extends Error { export class OutgoingIdentityKeyError extends ReplayableError { identifier: string; - identityKey: ArrayBuffer; + identityKey: Uint8Array; // Note: Data to resend message is no longer captured constructor( incomingIdentifier: string, - _m: ArrayBuffer, + _m: Uint8Array, _t: number, - identityKey: ArrayBuffer + identityKey: Uint8Array ) { const identifier = incomingIdentifier.split('.')[0]; @@ -188,7 +188,7 @@ export class SendMessageProtoError extends Error implements CallbackResultType { public readonly unidentifiedDeliveries?: Array; - public readonly dataMessage?: ArrayBuffer; + public readonly dataMessage?: Uint8Array; // Fields necesary for send log save public readonly contentHint?: number; diff --git a/ts/textsecure/Helpers.ts b/ts/textsecure/Helpers.ts index 3cbc042bd..e7337e9f7 100644 --- a/ts/textsecure/Helpers.ts +++ b/ts/textsecure/Helpers.ts @@ -1,8 +1,6 @@ // Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import { stringToArrayBuffer } from '../util/stringToArrayBuffer'; - /* eslint-disable guard-for-in */ /* eslint-disable no-restricted-syntax */ /* eslint-disable no-proto */ @@ -71,7 +69,6 @@ const utils = { number[0] === '+' && /^[0-9]+$/.test(number.substring(1)), // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types jsonThing: (thing: unknown) => JSON.stringify(ensureStringed(thing)), - stringToArrayBuffer, unencodeNumber: (number: string): Array => number.split('.'), }; diff --git a/ts/textsecure/MessageReceiver.ts b/ts/textsecure/MessageReceiver.ts index 2dedb5571..1bf645dff 100644 --- a/ts/textsecure/MessageReceiver.ts +++ b/ts/textsecure/MessageReceiver.ts @@ -43,7 +43,7 @@ import { normalizeUuid } from '../util/normalizeUuid'; import { normalizeNumber } from '../util/normalizeNumber'; import { parseIntOrThrow } from '../util/parseIntOrThrow'; import { Zone } from '../util/Zone'; -import { deriveMasterKeyFromGroupV1, typedArrayToArrayBuffer } from '../Crypto'; +import { deriveMasterKeyFromGroupV1 } from '../Crypto'; import { DownloadedAttachmentType } from '../types/Attachment'; import { Address } from '../types/Address'; import { QualifiedAddress } from '../types/QualifiedAddress'; @@ -103,9 +103,6 @@ import { } from './messageReceiverEvents'; import * as log from '../logging/log'; -// TODO: remove once we move away from ArrayBuffers -const FIXMEU8 = Uint8Array; - const GROUPV1_ID_LENGTH = 16; const GROUPV2_ID_LENGTH = 32; const RETRY_TIMEOUT = 2 * 60 * 1000; @@ -1137,9 +1134,9 @@ export default class MessageReceiver if ( !verifySignature( - typedArrayToArrayBuffer(this.serverTrustRoot), - typedArrayToArrayBuffer(serverCertificate.certificateData()), - typedArrayToArrayBuffer(serverCertificate.signature()) + this.serverTrustRoot, + serverCertificate.certificateData(), + serverCertificate.signature() ) ) { throw new Error( @@ -1150,9 +1147,9 @@ export default class MessageReceiver if ( !verifySignature( - typedArrayToArrayBuffer(serverCertificate.key().serialize()), - typedArrayToArrayBuffer(certificate.certificate()), - typedArrayToArrayBuffer(certificate.signature()) + serverCertificate.key().serialize(), + certificate.certificate(), + certificate.signature() ) ) { throw new Error( @@ -1448,9 +1445,7 @@ export default class MessageReceiver ciphertext: Uint8Array ): Promise { try { - const plaintext = await this.innerDecrypt(stores, envelope, ciphertext); - - return new FIXMEU8(plaintext); + return await this.innerDecrypt(stores, envelope, ciphertext); } catch (error) { this.removeFromCache(envelope); const uuid = envelope.sourceUuid; @@ -1486,9 +1481,7 @@ export default class MessageReceiver if (uuid && deviceId) { const { usmc } = envelope; const event = new DecryptionErrorEvent({ - cipherTextBytes: usmc - ? typedArrayToArrayBuffer(usmc.contents()) - : undefined, + cipherTextBytes: usmc ? usmc.contents() : undefined, cipherTextType: usmc ? usmc.msgType() : undefined, contentHint: envelope.contentHint, groupId: envelope.groupId, @@ -1955,7 +1948,7 @@ export default class MessageReceiver if (groupId && groupId.byteLength > 0) { if (groupId.byteLength === GROUPV1_ID_LENGTH) { groupIdString = Bytes.toBinary(groupId); - groupV2IdString = await this.deriveGroupV2FromV1(groupId); + groupV2IdString = this.deriveGroupV2FromV1(groupId); } else if (groupId.byteLength === GROUPV2_ID_LENGTH) { groupV2IdString = Bytes.toBase64(groupId); } else { @@ -2024,16 +2017,14 @@ export default class MessageReceiver return false; } - private async deriveGroupV2FromV1(groupId: Uint8Array): Promise { + private deriveGroupV2FromV1(groupId: Uint8Array): string { if (groupId.byteLength !== GROUPV1_ID_LENGTH) { throw new Error( `deriveGroupV2FromV1: had id with wrong byteLength: ${groupId.byteLength}` ); } - const masterKey = await deriveMasterKeyFromGroupV1( - typedArrayToArrayBuffer(groupId) - ); - const data = deriveGroupFields(new FIXMEU8(masterKey)); + const masterKey = deriveMasterKeyFromGroupV1(groupId); + const data = deriveGroupFields(masterKey); return Bytes.toBase64(data.id); } @@ -2246,7 +2237,7 @@ export default class MessageReceiver if (groupId && groupId.byteLength > 0) { if (groupId.byteLength === GROUPV1_ID_LENGTH) { groupIdString = Bytes.toBinary(groupId); - groupV2IdString = await this.deriveGroupV2FromV1(groupId); + groupV2IdString = this.deriveGroupV2FromV1(groupId); } else if (groupId.byteLength === GROUPV2_ID_LENGTH) { groupV2IdString = Bytes.toBase64(groupId); } else { @@ -2300,7 +2291,7 @@ export default class MessageReceiver } const ev = new KeysEvent( - typedArrayToArrayBuffer(sync.storageService), + sync.storageService, this.removeFromCache.bind(this, envelope) ); @@ -2343,9 +2334,7 @@ export default class MessageReceiver 'handleVerified.destinationUuid' ) : undefined, - identityKey: verified.identityKey - ? typedArrayToArrayBuffer(verified.identityKey) - : undefined, + identityKey: verified.identityKey ? verified.identityKey : undefined, }, this.removeFromCache.bind(this, envelope) ); diff --git a/ts/textsecure/OutgoingMessage.ts b/ts/textsecure/OutgoingMessage.ts index 5d25d7646..3dac943ae 100644 --- a/ts/textsecure/OutgoingMessage.ts +++ b/ts/textsecure/OutgoingMessage.ts @@ -37,7 +37,6 @@ import { Address } from '../types/Address'; import { QualifiedAddress } from '../types/QualifiedAddress'; import { UUID } from '../types/UUID'; import { Sessions, IdentityKeys } from '../LibSignalStores'; -import { typedArrayToArrayBuffer as toArrayBuffer } from '../Crypto'; import { updateConversationsWithUuidLookup } from '../updateConversationsWithUuidLookup'; import { getKeysForIdentifier } from './getKeysForIdentifier'; import { SignalService as Proto } from '../protobuf'; @@ -63,7 +62,7 @@ type SendMetadata = { export const serializedCertificateSchema = z .object({ expires: z.number().optional(), - serialized: z.instanceof(ArrayBuffer), + serialized: z.instanceof(Uint8Array), }) .nonstrict(); @@ -331,7 +330,7 @@ export default class OutgoingMessage { }); } - getPlaintext(): ArrayBuffer { + getPlaintext(): Uint8Array { if (!this.plaintext) { const { message } = this; @@ -341,7 +340,7 @@ export default class OutgoingMessage { this.plaintext = message.serialize(); } } - return toArrayBuffer(this.plaintext); + return this.plaintext; } getContentProtoBytes(): Uint8Array | undefined { @@ -720,7 +719,7 @@ export default class OutgoingMessage { identifier, error.originalMessage, error.timestamp, - error.identityKey + error.identityKey && new Uint8Array(error.identityKey) ); this.registerError(identifier, 'Untrusted identity', newError); } else { diff --git a/ts/textsecure/ProvisioningCipher.ts b/ts/textsecure/ProvisioningCipher.ts index 1831a9d27..9a7a26c76 100644 --- a/ts/textsecure/ProvisioningCipher.ts +++ b/ts/textsecure/ProvisioningCipher.ts @@ -5,21 +5,17 @@ /* eslint-disable max-classes-per-file */ import { KeyPairType } from './Types.d'; +import * as Bytes from '../Bytes'; import { decryptAes256CbcPkcsPadding, deriveSecrets, - bytesFromString, verifyHmacSha256, - typedArrayToArrayBuffer, } from '../Crypto'; import { calculateAgreement, createKeyPair, generateKeyPair } from '../Curve'; import { SignalService as Proto } from '../protobuf'; import { strictAssert } from '../util/assert'; import { normalizeUuid } from '../util/normalizeUuid'; -// TODO: remove once we move away from ArrayBuffers -const FIXMEU8 = Uint8Array; - type ProvisionDecryptResult = { identityKeyPair: KeyPairType; number?: string; @@ -27,7 +23,7 @@ type ProvisionDecryptResult = { provisioningCode?: string; userAgent?: string; readReceipts?: boolean; - profileKey?: ArrayBuffer; + profileKey?: Uint8Array; }; class ProvisioningCipherInner { @@ -55,34 +51,20 @@ class ProvisioningCipherInner { throw new Error('ProvisioningCipher.decrypt: No keypair!'); } - const ecRes = calculateAgreement( - typedArrayToArrayBuffer(masterEphemeral), - this.keyPair.privKey - ); + const ecRes = calculateAgreement(masterEphemeral, this.keyPair.privKey); const keys = deriveSecrets( ecRes, - new ArrayBuffer(32), - bytesFromString('TextSecure Provisioning Message') - ); - await verifyHmacSha256( - typedArrayToArrayBuffer(ivAndCiphertext), - keys[1], - typedArrayToArrayBuffer(mac), - 32 + new Uint8Array(32), + Bytes.fromString('TextSecure Provisioning Message') ); + verifyHmacSha256(ivAndCiphertext, keys[1], mac, 32); - const plaintext = await decryptAes256CbcPkcsPadding( - keys[0], - typedArrayToArrayBuffer(ciphertext), - typedArrayToArrayBuffer(iv) - ); - const provisionMessage = Proto.ProvisionMessage.decode( - new FIXMEU8(plaintext) - ); + const plaintext = decryptAes256CbcPkcsPadding(keys[0], ciphertext, iv); + const provisionMessage = Proto.ProvisionMessage.decode(plaintext); const privKey = provisionMessage.identityKeyPrivate; strictAssert(privKey, 'Missing identityKeyPrivate in ProvisionMessage'); - const keyPair = createKeyPair(typedArrayToArrayBuffer(privKey)); + const keyPair = createKeyPair(privKey); const { uuid } = provisionMessage; strictAssert(uuid, 'Missing uuid in provisioning message'); @@ -96,12 +78,12 @@ class ProvisioningCipherInner { readReceipts: provisionMessage.readReceipts, }; if (provisionMessage.profileKey) { - ret.profileKey = typedArrayToArrayBuffer(provisionMessage.profileKey); + ret.profileKey = provisionMessage.profileKey; } return ret; } - async getPublicKey(): Promise { + async getPublicKey(): Promise { if (!this.keyPair) { this.keyPair = generateKeyPair(); } @@ -126,5 +108,5 @@ export default class ProvisioningCipher { provisionEnvelope: Proto.ProvisionEnvelope ) => Promise; - getPublicKey: () => Promise; + getPublicKey: () => Promise; } diff --git a/ts/textsecure/SendMessage.ts b/ts/textsecure/SendMessage.ts index 67179324e..26c810215 100644 --- a/ts/textsecure/SendMessage.ts +++ b/ts/textsecure/SendMessage.ts @@ -39,14 +39,8 @@ import OutgoingMessage, { SerializedCertificateType, SendLogCallbackType, } from './OutgoingMessage'; -import Crypto from './Crypto'; import * as Bytes from '../Bytes'; -import { - concatenateBytes, - getRandomBytes, - getZeroes, - typedArrayToArrayBuffer, -} from '../Crypto'; +import { getRandomBytes, getZeroes, encryptAttachment } from '../Crypto'; import { StorageServiceCallOptionsType, StorageServiceCredentials, @@ -111,7 +105,7 @@ type GroupCallUpdateType = { export type AttachmentType = { size: number; - data: ArrayBuffer; + data: Uint8Array; contentType: string; fileName: string; @@ -137,7 +131,7 @@ export type MessageOptionsType = { groupV2?: GroupV2InfoType; needsSync?: boolean; preview?: ReadonlyArray | null; - profileKey?: ArrayBuffer; + profileKey?: Uint8Array; quote?: any; recipients: ReadonlyArray; sticker?: any; @@ -154,7 +148,7 @@ export type GroupSendOptionsType = { groupV1?: GroupV1InfoType; messageText?: string; preview?: any; - profileKey?: ArrayBuffer; + profileKey?: Uint8Array; quote?: any; reaction?: any; sticker?: any; @@ -164,9 +158,6 @@ export type GroupSendOptionsType = { groupCallUpdate?: GroupCallUpdateType; }; -// TODO: remove once we move away from ArrayBuffers -const FIXMEU8 = Uint8Array; - class Message { attachments: ReadonlyArray; @@ -187,7 +178,7 @@ class Message { preview: any; - profileKey?: ArrayBuffer; + profileKey?: Uint8Array; quote?: { id?: number; @@ -210,7 +201,7 @@ class Message { timestamp: number; - dataMessage: any; + dataMessage?: Proto.DataMessage; attachmentPointers: Array = []; @@ -298,7 +289,7 @@ class Message { } toProto(): Proto.DataMessage { - if (this.dataMessage instanceof Proto.DataMessage) { + if (this.dataMessage) { return this.dataMessage; } const proto = new Proto.DataMessage(); @@ -405,7 +396,7 @@ class Message { proto.expireTimer = this.expireTimer; } if (this.profileKey) { - proto.profileKey = new FIXMEU8(this.profileKey); + proto.profileKey = this.profileKey; } if (this.deletedForEveryoneTimestamp) { proto.delete = { @@ -437,10 +428,8 @@ class Message { return proto; } - toArrayBuffer() { - return typedArrayToArrayBuffer( - Proto.DataMessage.encode(this.toProto()).finish() - ); + encode() { + return Proto.DataMessage.encode(this.toProto()).finish(); } } @@ -489,15 +478,15 @@ export default class MessageSender { const paddingLength = (new Uint16Array(buffer)[0] & 0x1ff) + 1; // Generate a random padding buffer of the chosen size - return new FIXMEU8(getRandomBytes(paddingLength)); + return getRandomBytes(paddingLength); } - getPaddedAttachment(data: Readonly): ArrayBuffer { + getPaddedAttachment(data: Readonly): Uint8Array { const size = data.byteLength; const paddedSize = this._getAttachmentSizeBucket(size); const padding = getZeroes(paddedSize - size); - return concatenateBytes(data, padding); + return Bytes.concatenate([data, padding]); } async makeAttachmentPointer( @@ -509,9 +498,9 @@ export default class MessageSender { ); const { data, size } = attachment; - if (!(data instanceof ArrayBuffer) && !ArrayBuffer.isView(data)) { + if (!(data instanceof Uint8Array)) { throw new Error( - `makeAttachmentPointer: data was a '${typeof data}' instead of ArrayBuffer/ArrayBufferView` + `makeAttachmentPointer: data was a '${typeof data}' instead of Uint8Array` ); } if (data.byteLength !== size) { @@ -524,15 +513,15 @@ export default class MessageSender { const key = getRandomBytes(64); const iv = getRandomBytes(16); - const result = await Crypto.encryptAttachment(padded, key, iv); + const result = encryptAttachment(padded, key, iv); const id = await this.server.putAttachment(result.ciphertext); const proto = new Proto.AttachmentPointer(); proto.cdnId = Long.fromString(id); proto.contentType = attachment.contentType; - proto.key = new FIXMEU8(key); + proto.key = key; proto.size = attachment.size; - proto.digest = new FIXMEU8(result.digest); + proto.digest = result.digest; if (attachment.fileName) { proto.fileName = attachment.fileName; @@ -651,9 +640,9 @@ export default class MessageSender { async getDataMessage( options: Readonly - ): Promise { + ): Promise { const message = await this.getHydratedMessage(options); - return message.toArrayBuffer(); + return message.encode(); } async getContentMessage( @@ -685,7 +674,7 @@ export default class MessageSender { getTypingContentMessage( options: Readonly<{ recipientId?: string; - groupId?: ArrayBuffer; + groupId?: Uint8Array; groupMembers: ReadonlyArray; isTyping: boolean; timestamp?: number; @@ -705,7 +694,7 @@ export default class MessageSender { const typingMessage = new Proto.TypingMessage(); if (groupId) { - typingMessage.groupId = new FIXMEU8(groupId); + typingMessage.groupId = groupId; } typingMessage.action = action; typingMessage.timestamp = finalTimestamp; @@ -823,7 +812,7 @@ export default class MessageSender { new Promise((resolve, reject) => { this.sendMessageProto({ callback: (res: CallbackResultType) => { - res.dataMessage = message.toArrayBuffer(); + res.dataMessage = message.encode(); if (res.errors && res.errors.length > 0) { reject(new SendMessageProtoError(res)); } else { @@ -988,7 +977,7 @@ export default class MessageSender { expireTimer: number | undefined; contentHint: number; groupId: string | undefined; - profileKey?: ArrayBuffer; + profileKey?: Uint8Array; options?: SendOptionsType; }>): Promise { return this.sendMessage({ @@ -1026,7 +1015,7 @@ export default class MessageSender { isUpdate, options, }: Readonly<{ - encodedDataMessage: ArrayBuffer; + encodedDataMessage: Uint8Array; timestamp: number; destination: string | undefined; destinationUuid: string | null | undefined; @@ -1038,9 +1027,7 @@ export default class MessageSender { }>): Promise { const myUuid = window.textsecure.storage.user.getCheckedUuid(); - const dataMessage = Proto.DataMessage.decode( - new FIXMEU8(encodedDataMessage) - ); + const dataMessage = Proto.DataMessage.decode(encodedDataMessage); const sentMessage = new Proto.SyncMessage.Sent(); sentMessage.timestamp = timestamp; sentMessage.message = dataMessage; @@ -1356,7 +1343,7 @@ export default class MessageSender { responseArgs: Readonly<{ threadE164?: string; threadUuid?: string; - groupId?: ArrayBuffer; + groupId?: Uint8Array; type: number; }>, options?: Readonly @@ -1373,7 +1360,7 @@ export default class MessageSender { response.threadUuid = responseArgs.threadUuid; } if (responseArgs.groupId) { - response.groupId = new FIXMEU8(responseArgs.groupId); + response.groupId = responseArgs.groupId; } response.type = responseArgs.type; syncMessage.messageRequestResponse = response; @@ -1435,7 +1422,7 @@ export default class MessageSender { destinationE164: string | undefined, destinationUuid: string | undefined, state: number, - identityKey: Readonly, + identityKey: Readonly, options?: Readonly ): Promise { const myUuid = window.textsecure.storage.user.getCheckedUuid(); @@ -1468,7 +1455,7 @@ export default class MessageSender { if (destinationUuid) { verified.destinationUuid = destinationUuid; } - verified.identityKey = new FIXMEU8(identityKey); + verified.identityKey = identityKey; verified.nullMessage = padding; const syncMessage = this.createSyncMessage(); @@ -1491,7 +1478,7 @@ export default class MessageSender { // Sending messages to contacts async sendProfileKeyUpdate( - profileKey: Readonly, + profileKey: Readonly, recipients: ReadonlyArray, options: Readonly, groupId?: string @@ -1712,11 +1699,9 @@ export default class MessageSender { return sendToContactPromise; } - const buffer = typedArrayToArrayBuffer( - Proto.DataMessage.encode(proto).finish() - ); + const encodedDataMessage = Proto.DataMessage.encode(proto).finish(); const sendSyncPromise = this.sendSyncMessage({ - encodedDataMessage: buffer, + encodedDataMessage, timestamp, destination: e164, destinationUuid: uuid, @@ -1738,7 +1723,7 @@ export default class MessageSender { identifier: string, expireTimer: number | undefined, timestamp: number, - profileKey?: Readonly, + profileKey?: Readonly, options?: Readonly ): Promise { const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; @@ -1869,9 +1854,7 @@ export default class MessageSender { timestamp: number; }>): Promise { const dataMessage = proto.dataMessage - ? typedArrayToArrayBuffer( - Proto.DataMessage.encode(proto.dataMessage).finish() - ) + ? Proto.DataMessage.encode(proto.dataMessage).finish() : undefined; const myE164 = window.textsecure.storage.user.getNumber(); @@ -2034,7 +2017,7 @@ export default class MessageSender { groupIdentifiers: ReadonlyArray, expireTimer: number | undefined, timestamp: number, - profileKey?: Readonly, + profileKey?: Readonly, options?: Readonly ): Promise { const myNumber = window.textsecure.storage.user.getNumber(); @@ -2163,7 +2146,7 @@ export default class MessageSender { return this.server.getGroupLog(startVersion, options); } - async getGroupAvatar(key: string): Promise { + async getGroupAvatar(key: string): Promise { return this.server.getGroupAvatar(key); } @@ -2176,8 +2159,8 @@ export default class MessageSender { } async sendWithSenderKey( - data: Readonly, - accessKeys: Readonly, + data: Readonly, + accessKeys: Readonly, timestamp: number, online?: boolean ): Promise { @@ -2211,21 +2194,21 @@ export default class MessageSender { async getStorageManifest( options: Readonly - ): Promise { + ): Promise { return this.server.getStorageManifest(options); } async getStorageRecords( - data: Readonly, + data: Readonly, options: Readonly - ): Promise { + ): Promise { return this.server.getStorageRecords(data, options); } async modifyStorageRecords( - data: Readonly, + data: Readonly, options: Readonly - ): Promise { + ): Promise { return this.server.modifyStorageRecords(data, options); } @@ -2249,7 +2232,7 @@ export default class MessageSender { async uploadAvatar( requestHeaders: Readonly, - avatarData: Readonly + avatarData: Readonly ): Promise { return this.server.uploadAvatar(requestHeaders, avatarData); } diff --git a/ts/textsecure/SocketManager.ts b/ts/textsecure/SocketManager.ts index 7b505d1b6..ce74897c0 100644 --- a/ts/textsecure/SocketManager.ts +++ b/ts/textsecure/SocketManager.ts @@ -28,9 +28,6 @@ import { ConnectTimeoutError, HTTPError } from './Errors'; import { handleStatusCode, translateError } from './Utils'; import { WebAPICredentials, IRequestHandler } from './Types.d'; -// TODO: remove once we move away from ArrayBuffers -const FIXMEU8 = Uint8Array; - const TEN_SECONDS = 10 * durations.SECOND; const FIVE_MINUTES = 5 * durations.MINUTE; @@ -289,7 +286,7 @@ export class SocketManager extends EventListener { } else if (body instanceof Uint8Array) { bodyBytes = body; } else if (body instanceof ArrayBuffer) { - bodyBytes = new FIXMEU8(body); + throw new Error('Unsupported body type: ArrayBuffer'); } else if (typeof body === 'string') { bodyBytes = Bytes.fromString(body); } else { diff --git a/ts/textsecure/StringView.ts b/ts/textsecure/StringView.ts deleted file mode 100644 index 1f8db0641..000000000 --- a/ts/textsecure/StringView.ts +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright 2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -/* eslint-disable no-bitwise */ -/* eslint-disable no-nested-ternary */ - -const StringView = { - /* - * These functions from the Mozilla Developer Network - * and have been placed in the public domain. - * https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding - * https://developer.mozilla.org/en-US/docs/MDN/About#Copyrights_and_licenses - */ - - // prettier-ignore - b64ToUint6(nChr: number): number { - return nChr > 64 && nChr < 91 - ? nChr - 65 - : nChr > 96 && nChr < 123 - ? nChr - 71 - : nChr > 47 && nChr < 58 - ? nChr + 4 - : nChr === 43 - ? 62 - : nChr === 47 - ? 63 - : 0; - }, - - base64ToBytes(sBase64: string, nBlocksSize: number): ArrayBuffer { - const sB64Enc = sBase64.replace(/[^A-Za-z0-9+/]/g, ''); - const nInLen = sB64Enc.length; - const nOutLen = nBlocksSize - ? Math.ceil(((nInLen * 3 + 1) >> 2) / nBlocksSize) * nBlocksSize - : (nInLen * 3 + 1) >> 2; - const aBBytes = new ArrayBuffer(nOutLen); - const taBytes = new Uint8Array(aBBytes); - - let nMod3; - let nMod4; - let nOutIdx = 0; - let nInIdx = 0; - for (let nUint24 = 0; nInIdx < nInLen; nInIdx += 1) { - nMod4 = nInIdx & 3; - nUint24 |= - StringView.b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << (18 - 6 * nMod4); - if (nMod4 === 3 || nInLen - nInIdx === 1) { - for ( - nMod3 = 0; - nMod3 < 3 && nOutIdx < nOutLen; - nMod3 += 1, nOutIdx += 1 - ) { - taBytes[nOutIdx] = (nUint24 >>> ((16 >>> nMod3) & 24)) & 255; - } - nUint24 = 0; - } - } - return aBBytes; - }, - - // prettier-ignore - uint6ToB64(nUint6: number): number { - return nUint6 < 26 - ? nUint6 + 65 - : nUint6 < 52 - ? nUint6 + 71 - : nUint6 < 62 - ? nUint6 - 4 - : nUint6 === 62 - ? 43 - : nUint6 === 63 - ? 47 - : 65; - }, - - bytesToBase64(aBytes: Uint8Array): string { - let nMod3; - let sB64Enc = ''; - let nUint24 = 0; - const nLen = aBytes.length; - for (let nIdx = 0; nIdx < nLen; nIdx += 1) { - nMod3 = nIdx % 3; - if (nIdx > 0 && ((nIdx * 4) / 3) % 76 === 0) { - sB64Enc += '\r\n'; - } - nUint24 |= aBytes[nIdx] << ((16 >>> nMod3) & 24); - if (nMod3 === 2 || aBytes.length - nIdx === 1) { - sB64Enc += String.fromCharCode( - StringView.uint6ToB64((nUint24 >>> 18) & 63), - StringView.uint6ToB64((nUint24 >>> 12) & 63), - StringView.uint6ToB64((nUint24 >>> 6) & 63), - StringView.uint6ToB64(nUint24 & 63) - ); - nUint24 = 0; - } - } - return sB64Enc.replace(/A(?=A$|$)/g, '='); - }, -}; - -export default StringView; diff --git a/ts/textsecure/Types.d.ts b/ts/textsecure/Types.d.ts index b85063c3a..8393c458f 100644 --- a/ts/textsecure/Types.d.ts +++ b/ts/textsecure/Types.d.ts @@ -45,7 +45,7 @@ export type DeviceType = { export type CompatSignedPreKeyType = { keyId: number; keyPair: KeyPairType; - signature: ArrayBuffer; + signature: Uint8Array; }; export type CompatPreKeyType = { @@ -56,8 +56,8 @@ export type CompatPreKeyType = { // How we work with these types thereafter export type KeyPairType = { - privKey: ArrayBuffer; - pubKey: ArrayBuffer; + privKey: Uint8Array; + pubKey: Uint8Array; }; export type OuterSignedPrekeyType = { @@ -65,8 +65,8 @@ export type OuterSignedPrekeyType = { // eslint-disable-next-line camelcase created_at: number; keyId: number; - privKey: ArrayBuffer; - pubKey: ArrayBuffer; + privKey: Uint8Array; + pubKey: Uint8Array; }; export type SessionResetsType = Record; @@ -231,7 +231,7 @@ export interface CallbackResultType { failoverIdentifiers?: Array; errors?: Array; unidentifiedDeliveries?: Array; - dataMessage?: ArrayBuffer; + dataMessage?: Uint8Array; // Fields necesary for send log save contentHint?: number; diff --git a/ts/textsecure/WebAPI.ts b/ts/textsecure/WebAPI.ts index d81efa68d..23644169b 100644 --- a/ts/textsecure/WebAPI.ts +++ b/ts/textsecure/WebAPI.ts @@ -2,7 +2,6 @@ // SPDX-License-Identifier: AGPL-3.0-only /* eslint-disable no-param-reassign */ -/* eslint-disable more/no-then */ /* eslint-disable no-bitwise */ /* eslint-disable guard-for-in */ /* eslint-disable no-restricted-syntax */ @@ -34,20 +33,16 @@ import * as durations from '../util/durations'; import { getUserAgent } from '../util/getUserAgent'; import { toWebSafeBase64 } from '../util/webSafeBase64'; import { SocketStatus } from '../types/SocketStatus'; +import { toLogFormat } from '../types/errors'; import { isPackIdValid, redactPackId } from '../types/Stickers'; import * as Bytes from '../Bytes'; import { - arrayBufferToBase64, - base64ToArrayBuffer, - bytesFromString, - concatenateBytes, constantTimeEqual, decryptAesGcm, deriveSecrets, encryptCdsDiscoveryRequest, getRandomValue, splitUuids, - typedArrayToArrayBuffer, } from '../Crypto'; import { calculateAgreement, generateKeyPair } from '../Curve'; import * as linkPreviewFetch from '../linkPreviews/linkPreviewFetch'; @@ -66,9 +61,6 @@ import { WebAPICredentials, IRequestHandler } from './Types.d'; import { handleStatusCode, translateError } from './Utils'; import * as log from '../logging/log'; -// TODO: remove once we move away from ArrayBuffers -const FIXMEU8 = Uint8Array; - // Note: this will break some code that expects to be able to use err.response when a // web request fails, because it will force it to text. But it is very useful for // debugging failed requests. @@ -111,20 +103,9 @@ function getSgxConstants() { return sgxConstantCache; } -function _btoa(str: any) { - let buffer; - - if (str instanceof Buffer) { - buffer = str; - } else { - buffer = Buffer.from(str.toString(), 'binary'); - } - - return buffer.toString('base64'); -} - const _call = (object: any) => Object.prototype.toString.call(object); +// TODO: DESKTOP-2424 const ArrayBufferToString = _call(new ArrayBuffer(0)); const Uint8ArrayToString = _call(new Uint8Array()); @@ -141,21 +122,6 @@ function _getString(thing: any): string { return thing; } -// prettier-ignore -function _b64ToUint6(nChr: number) { - return nChr > 64 && nChr < 91 - ? nChr - 65 - : nChr > 96 && nChr < 123 - ? nChr - 71 - : nChr > 47 && nChr < 58 - ? nChr + 4 - : nChr === 43 - ? 62 - : nChr === 47 - ? 63 - : 0; -} - function _getStringable(thing: any) { return ( typeof thing === 'string' || @@ -196,42 +162,10 @@ function _ensureStringed(thing: any): any { throw new Error(`unsure of how to jsonify object of type ${typeof thing}`); } -function _jsonThing(thing: any) { +function _jsonThing(thing: any): string { return JSON.stringify(_ensureStringed(thing)); } -function _base64ToBytes(sBase64: string, nBlocksSize?: number) { - const sB64Enc = sBase64.replace(/[^A-Za-z0-9+/]/g, ''); - const nInLen = sB64Enc.length; - const nOutLen = nBlocksSize - ? Math.ceil(((nInLen * 3 + 1) >> 2) / nBlocksSize) * nBlocksSize - : (nInLen * 3 + 1) >> 2; - const aBBytes = new ArrayBuffer(nOutLen); - const taBytes = new Uint8Array(aBBytes); - - let nMod3 = 0; - let nMod4 = 0; - let nUint24 = 0; - let nOutIdx = 0; - - for (let nInIdx = 0; nInIdx < nInLen; nInIdx += 1) { - nMod4 = nInIdx & 3; - nUint24 |= _b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << (18 - 6 * nMod4); - if (nMod4 === 3 || nInLen - nInIdx === 1) { - for ( - nMod3 = 0; - nMod3 < 3 && nOutIdx < nOutLen; - nMod3 += 1, nOutIdx += 1 - ) { - taBytes[nOutIdx] = (nUint24 >>> ((16 >>> nMod3) & 24)) & 255; - } - nUint24 = 0; - } - } - - return aBBytes; -} - function _createRedactor( ...toReplace: ReadonlyArray ): RedactUrl { @@ -298,7 +232,7 @@ type PromiseAjaxOptionsType = { basicAuth?: string; certificateAuthority?: string; contentType?: string; - data?: ArrayBuffer | Buffer | string; + data?: Uint8Array | string; headers?: HeaderListType; host?: string; password?: string; @@ -306,11 +240,7 @@ type PromiseAjaxOptionsType = { proxyUrl?: string; redactUrl?: RedactUrl; redirect?: 'error' | 'follow' | 'manual'; - responseType?: - | 'json' - | 'jsonwithdetails' - | 'arraybuffer' - | 'arraybufferwithdetails'; + responseType?: 'json' | 'jsonwithdetails' | 'bytes' | 'byteswithdetails'; serverUrl?: string; stack?: string; timeout?: number; @@ -322,12 +252,12 @@ type PromiseAjaxOptionsType = { }; type JSONWithDetailsType = { - data: any; + data: unknown; contentType: string | null; response: Response; }; -type ArrayBufferWithDetailsType = { - data: ArrayBuffer; +type BytesWithDetailsType = { + data: Uint8Array; contentType: string | null; response: Response; }; @@ -387,13 +317,7 @@ function getHostname(url: string): string { async function _promiseAjax( providedUrl: string | null, options: PromiseAjaxOptionsType -): Promise< - | string - | ArrayBuffer - | unknown - | JSONWithDetailsType - | ArrayBufferWithDetailsType -> { +): Promise { const url = providedUrl || `${options.host}/${options.path}`; const unauthLabel = options.unauthenticated ? ' (unauth)' : ''; @@ -437,8 +361,8 @@ async function _promiseAjax( timeout, }; - if (fetchOptions.body instanceof ArrayBuffer) { - // node-fetch doesn't support ArrayBuffer, only node Buffer + if (fetchOptions.body instanceof Uint8Array) { + // node-fetch doesn't support Uint8Array, only node Buffer const contentLength = fetchOptions.body.byteLength; fetchOptions.body = Buffer.from(fetchOptions.body); @@ -458,9 +382,9 @@ async function _promiseAjax( // Access key is already a Base64 string fetchOptions.headers['Unidentified-Access-Key'] = accessKey; } else if (options.user && options.password) { - const user = _getString(options.user); - const password = _getString(options.password); - const auth = _btoa(`${user}:${password}`); + const auth = Bytes.toBase64( + Bytes.fromString(`${options.user}:${options.password}`) + ); fetchOptions.headers.Authorization = `Basic ${auth}`; } @@ -469,7 +393,7 @@ async function _promiseAjax( } let response: Response; - let result: string | ArrayBuffer | unknown; + let result: string | Uint8Array | unknown; try { response = socketManager ? await socketManager.fetch(url, fetchOptions) @@ -498,10 +422,10 @@ async function _promiseAjax( ) { result = await response.json(); } else if ( - options.responseType === 'arraybuffer' || - options.responseType === 'arraybufferwithdetails' + options.responseType === 'bytes' || + options.responseType === 'byteswithdetails' ) { - result = await response.arrayBuffer(); + result = await response.buffer(); } else { result = await response.textConverted(); } @@ -564,9 +488,9 @@ async function _promiseAjax( log.info(options.type, url, response.status, 'Success'); } - if (options.responseType === 'arraybufferwithdetails') { - assert(result instanceof ArrayBuffer, 'Expected ArrayBuffer result'); - const fullResult: ArrayBufferWithDetailsType = { + if (options.responseType === 'byteswithdetails') { + assert(result instanceof Uint8Array, 'Expected Uint8Array result'); + const fullResult: BytesWithDetailsType = { data: result, contentType: getContentType(response), response, @@ -593,11 +517,13 @@ async function _retryAjax( options: PromiseAjaxOptionsType, providedLimit?: number, providedCount?: number -) { +): Promise { const count = (providedCount || 0) + 1; const limit = providedLimit || 3; - return _promiseAjax(url, options).catch(async (e: Error) => { + try { + return await _promiseAjax(url, options); + } catch (e) { if (e instanceof HTTPError && e.code === -1 && count < limit) { return new Promise(resolve => { setTimeout(() => { @@ -606,10 +532,34 @@ async function _retryAjax( }); } throw e; - }); + } } -async function _outerAjax(url: string | null, options: PromiseAjaxOptionsType) { +function _outerAjax( + providedUrl: string | null, + options: PromiseAjaxOptionsType & { responseType: 'json' } +): Promise; +function _outerAjax( + providedUrl: string | null, + options: PromiseAjaxOptionsType & { responseType: 'jsonwithdetails' } +): Promise; +function _outerAjax( + providedUrl: string | null, + options: PromiseAjaxOptionsType & { responseType?: 'bytes' } +): Promise; +function _outerAjax( + providedUrl: string | null, + options: PromiseAjaxOptionsType & { responseType: 'byteswithdetails' } +): Promise; +function _outerAjax( + providedUrl: string | null, + options: PromiseAjaxOptionsType +): Promise; + +async function _outerAjax( + url: string | null, + options: PromiseAjaxOptionsType +): Promise { options.stack = new Error().stack; // just in case, save stack here. return _retryAjax(url, options); @@ -619,7 +569,7 @@ function makeHTTPError( message: string, providedCode: number, headers: HeaderListType, - response: any, + response: unknown, stack?: string ) { return new HTTPError(message, { @@ -718,14 +668,14 @@ type AjaxOptionsType = { basicAuth?: string; call: keyof typeof URL_CALLS; contentType?: string; - data?: ArrayBuffer | Buffer | Uint8Array | string; + data?: Uint8Array | Buffer | Uint8Array | string; headers?: HeaderListType; host?: string; httpType: HTTPCodeType; - jsonData?: any; + jsonData?: unknown; password?: string; redactUrl?: RedactUrl; - responseType?: 'json' | 'arraybuffer' | 'arraybufferwithdetails'; + responseType?: 'json' | 'bytes' | 'byteswithdetails'; schema?: unknown; timeout?: number; unauthenticated?: boolean; @@ -757,7 +707,7 @@ export type CapabilitiesUploadType = { changeNumber: true; }; -type StickerPackManifestType = ArrayBuffer; +type StickerPackManifestType = Uint8Array; export type GroupCredentialType = { credential: string; @@ -811,6 +761,35 @@ export type ProfileType = Readonly<{ capabilities?: unknown; }>; +export type GetIceServersResultType = Readonly<{ + username: string; + password: string; + urls: ReadonlyArray; +}>; + +export type GetDevicesResultType = ReadonlyArray< + Readonly<{ + id: number; + name: string; + lastSeen: number; + created: number; + }> +>; + +export type GetSenderCertificateResultType = Readonly<{ certificate: string }>; + +export type MakeProxiedRequestResultType = + | Uint8Array + | { + result: BytesWithDetailsType; + totalSize: number; + }; + +export type WhoamiResultType = Readonly<{ + uuid?: string; + number?: string; +}>; + export type WebAPIType = { confirmCode: ( number: string, @@ -818,28 +797,21 @@ export type WebAPIType = { newPassword: string, registrationId: number, deviceName?: string | null, - options?: { accessKey?: ArrayBuffer; uuid?: string } - ) => Promise<{ uuid?: string; deviceId: number }>; + options?: { accessKey?: Uint8Array; uuid?: string } + ) => Promise<{ uuid?: string; deviceId?: number }>; createGroup: ( group: Proto.IGroup, options: GroupCredentialsType ) => Promise; - getAttachment: (cdnKey: string, cdnNumber?: number) => Promise; - getAvatar: (path: string) => Promise; - getDevices: () => Promise< - Array<{ - id: number; - name: string; - lastSeen: number; - created: number; - }> - >; + getAttachment: (cdnKey: string, cdnNumber?: number) => Promise; + getAvatar: (path: string) => Promise; + getDevices: () => Promise; getGroup: (options: GroupCredentialsType) => Promise; getGroupFromLink: ( inviteLinkPassword: string, auth: GroupCredentialsType ) => Promise; - getGroupAvatar: (key: string) => Promise; + getGroupAvatar: (key: string) => Promise; getGroupCredentials: ( startDay: number, endDay: number @@ -851,11 +823,7 @@ export type WebAPIType = { startVersion: number, options: GroupCredentialsType ) => Promise; - getIceServers: () => Promise<{ - username: string; - password: string; - urls: Array; - }>; + getIceServers: () => Promise; getKeysForIdentifier: ( identifier: string, deviceId?: number @@ -886,8 +854,8 @@ export type WebAPIType = { ) => Promise; getSenderCertificate: ( withUuid?: boolean - ) => Promise<{ certificate: string }>; - getSticker: (packId: string, stickerId: number) => Promise; + ) => Promise; + getSticker: (packId: string, stickerId: number) => Promise; getStickerPackManifest: (packId: string) => Promise; getStorageCredentials: MessageSender['getStorageCredentials']; getStorageManifest: MessageSender['getStorageManifest']; @@ -906,33 +874,27 @@ export type WebAPIType = { makeProxiedRequest: ( targetUrl: string, options?: ProxiedRequestOptionsType - ) => Promise< - | ArrayBufferWithDetailsType - | { - result: ArrayBufferWithDetailsType; - totalSize: number; - } - >; + ) => Promise; makeSfuRequest: ( targetUrl: string, type: HTTPCodeType, headers: HeaderListType, - body: ArrayBuffer | undefined - ) => Promise; + body: Uint8Array | undefined + ) => Promise; modifyGroup: ( changes: Proto.GroupChange.IActions, options: GroupCredentialsType, inviteLinkBase64?: string ) => Promise; modifyStorageRecords: MessageSender['modifyStorageRecords']; - putAttachment: (encryptedBin: ArrayBuffer) => Promise; + putAttachment: (encryptedBin: Uint8Array) => Promise; putProfile: ( jsonData: ProfileRequestDataType ) => Promise; registerCapabilities: (capabilities: CapabilitiesUploadType) => Promise; putStickers: ( - encryptedManifest: ArrayBuffer, - encryptedStickers: Array, + encryptedManifest: Uint8Array, + encryptedStickers: Array, onProgress?: () => void ) => Promise; registerKeys: (genKeys: KeysType) => Promise; @@ -954,8 +916,8 @@ export type WebAPIType = { options?: { accessKey?: string } ) => Promise; sendWithSenderKey: ( - payload: ArrayBuffer, - accessKeys: ArrayBuffer, + payload: Uint8Array, + accessKeys: Uint8Array, timestamp: number, online?: boolean ) => Promise; @@ -963,16 +925,13 @@ export type WebAPIType = { updateDeviceName: (deviceName: string) => Promise; uploadAvatar: ( uploadAvatarRequestHeaders: UploadAvatarHeadersType, - avatarData: ArrayBuffer + avatarData: Uint8Array ) => Promise; uploadGroupAvatar: ( avatarData: Uint8Array, options: GroupCredentialsType ) => Promise; - whoami: () => Promise<{ - uuid?: string; - number?: string; - }>; + whoami: () => Promise; sendChallengeResponse: (challengeResponse: ChallengeType) => Promise; getConfig: () => Promise< Array<{ name: string; enabled: boolean; value: string | null }> @@ -989,16 +948,16 @@ export type WebAPIType = { export type SignedPreKeyType = { keyId: number; - publicKey: ArrayBuffer; - signature: ArrayBuffer; + publicKey: Uint8Array; + signature: Uint8Array; }; export type KeysType = { - identityKey: ArrayBuffer; + identityKey: Uint8Array; signedPreKey: SignedPreKeyType; preKeys: Array<{ keyId: number; - publicKey: ArrayBuffer; + publicKey: Uint8Array; }>; }; @@ -1008,15 +967,15 @@ export type ServerKeysType = { registrationId: number; signedPreKey: { keyId: number; - publicKey: ArrayBuffer; - signature: ArrayBuffer; + publicKey: Uint8Array; + signature: Uint8Array; }; preKey?: { keyId: number; - publicKey: ArrayBuffer; + publicKey: Uint8Array; }; }>; - identityKey: ArrayBuffer; + identityKey: Uint8Array; }; export type ChallengeType = { @@ -1026,7 +985,7 @@ export type ChallengeType = { }; export type ProxiedRequestOptionsType = { - returnArrayBuffer?: boolean; + returnUint8Array?: boolean; start?: number; end?: number; }; @@ -1181,7 +1140,17 @@ export function initialize({ sendChallengeResponse, }; - async function _ajax(param: AjaxOptionsType): Promise { + function _ajax( + param: AjaxOptionsType & { responseType?: 'bytes' } + ): Promise; + function _ajax( + param: AjaxOptionsType & { responseType: 'byteswithdetails' } + ): Promise; + function _ajax( + param: AjaxOptionsType & { responseType: 'json' } + ): Promise; + + async function _ajax(param: AjaxOptionsType): Promise { if (!param.urlParameters) { param.urlParameters = ''; } @@ -1189,12 +1158,14 @@ export function initialize({ const useWebSocketForEndpoint = useWebSocket && WEBSOCKET_CALLS.has(param.call); - return _outerAjax(null, { + const outerParams = { socketManager: useWebSocketForEndpoint ? socketManager : undefined, basicAuth: param.basicAuth, certificateAuthority, contentType: param.contentType || 'application/json; charset=utf-8', - data: param.data || (param.jsonData && _jsonThing(param.jsonData)), + data: + param.data || + (param.jsonData ? _jsonThing(param.jsonData) : undefined), headers: param.headers, host: param.host || url, password: param.password || password, @@ -1210,7 +1181,11 @@ export function initialize({ version, unauthenticated: param.unauthenticated, accessKey: param.accessKey, - }).catch((e: Error) => { + }; + + try { + return await _outerAjax(null, outerParams); + } catch (e) { if (!(e instanceof HTTPError)) { throw e; } @@ -1218,19 +1193,20 @@ export function initialize({ if (translatedError) { throw translatedError; } - }); + throw e; + } } async function whoami() { - return _ajax({ + return (await _ajax({ call: 'whoami', httpType: 'GET', responseType: 'json', - }); + })) as WhoamiResultType; } async function sendChallengeResponse(challengeResponse: ChallengeType) { - return _ajax({ + await _ajax({ call: 'challenge', httpType: 'PUT', jsonData: challengeResponse, @@ -1287,11 +1263,11 @@ export function initialize({ type ResType = { config: Array<{ name: string; enabled: boolean; value: string | null }>; }; - const res: ResType = await _ajax({ + const res = (await _ajax({ call: 'config', httpType: 'GET', responseType: 'json', - }); + })) as ResType; return res.config.filter( ({ name }: { name: string }) => @@ -1300,27 +1276,27 @@ export function initialize({ } async function getSenderCertificate(omitE164?: boolean) { - return _ajax({ + return (await _ajax({ call: 'deliveryCert', httpType: 'GET', responseType: 'json', validateResponse: { certificate: 'string' }, ...(omitE164 ? { urlParameters: '?includeE164=false' } : {}), - }); + })) as GetSenderCertificateResultType; } async function getStorageCredentials(): Promise { - return _ajax({ + return (await _ajax({ call: 'storageToken', httpType: 'GET', responseType: 'json', schema: { username: 'string', password: 'string' }, - }); + })) as StorageServiceCredentials; } async function getStorageManifest( options: StorageServiceCallOptionsType = {} - ): Promise { + ): Promise { const { credentials, greaterThanVersion } = options; return _ajax({ @@ -1328,7 +1304,7 @@ export function initialize({ contentType: 'application/x-protobuf', host: storageUrl, httpType: 'GET', - responseType: 'arraybuffer', + responseType: 'bytes', urlParameters: greaterThanVersion ? `/version/${greaterThanVersion}` : '', @@ -1337,9 +1313,9 @@ export function initialize({ } async function getStorageRecords( - data: ArrayBuffer, + data: Uint8Array, options: StorageServiceCallOptionsType = {} - ): Promise { + ): Promise { const { credentials } = options; return _ajax({ @@ -1348,15 +1324,15 @@ export function initialize({ data, host: storageUrl, httpType: 'PUT', - responseType: 'arraybuffer', + responseType: 'bytes', ...credentials, }); } async function modifyStorageRecords( - data: ArrayBuffer, + data: Uint8Array, options: StorageServiceCallOptionsType = {} - ): Promise { + ): Promise { const { credentials } = options; return _ajax({ @@ -1366,14 +1342,14 @@ export function initialize({ host: storageUrl, httpType: 'PUT', // If we run into a conflict, the current manifest is returned - - // it will will be an ArrayBuffer at the response key on the Error - responseType: 'arraybuffer', + // it will will be an Uint8Array at the response key on the Error + responseType: 'bytes', ...credentials, }); } async function registerSupportForUnauthenticatedDelivery() { - return _ajax({ + await _ajax({ call: 'supportUnauthenticatedDelivery', httpType: 'PUT', responseType: 'json', @@ -1381,7 +1357,7 @@ export function initialize({ } async function registerCapabilities(capabilities: CapabilitiesUploadType) { - return _ajax({ + await _ajax({ call: 'registerCapabilities', httpType: 'PUT', jsonData: capabilities, @@ -1414,7 +1390,7 @@ export function initialize({ ) { const { profileKeyVersion, profileKeyCredentialRequest } = options; - return _ajax({ + return (await _ajax({ call: 'profile', httpType: 'GET', urlParameters: getProfileUrl( @@ -1428,7 +1404,7 @@ export function initialize({ profileKeyVersion, profileKeyCredentialRequest ), - }); + })) as ProfileType; } async function putProfile( @@ -1437,6 +1413,7 @@ export function initialize({ const res = await _ajax({ call: 'profile', httpType: 'PUT', + responseType: 'json', jsonData, }); @@ -1444,8 +1421,7 @@ export function initialize({ return; } - const parsed = JSON.parse(res); - return uploadAvatarHeadersZod.parse(parsed); + return uploadAvatarHeadersZod.parse(res); } async function getProfileUnauth( @@ -1462,7 +1438,7 @@ export function initialize({ profileKeyCredentialRequest, } = options; - return _ajax({ + return (await _ajax({ call: 'profile', httpType: 'GET', urlParameters: getProfileUrl( @@ -1478,17 +1454,17 @@ export function initialize({ profileKeyVersion, profileKeyCredentialRequest ), - }); + })) as ProfileType; } async function getAvatar(path: string) { // Using _outerAJAX, since it's not hardcoded to the Signal Server. Unlike our // attachment CDN, it uses our self-signed certificate, so we pass it in. - return (await _outerAjax(`${cdnUrlObject['0']}/${path}`, { + return _outerAjax(`${cdnUrlObject['0']}/${path}`, { certificateAuthority, contentType: 'application/octet-stream', proxyUrl, - responseType: 'arraybuffer', + responseType: 'bytes', timeout: 0, type: 'GET', redactUrl: (href: string) => { @@ -1496,7 +1472,7 @@ export function initialize({ return href.replace(pattern, `[REDACTED]${path.slice(-3)}`); }, version, - })) as ArrayBuffer; + }); } async function reportMessage( @@ -1507,12 +1483,12 @@ export function initialize({ call: 'reportMessage', httpType: 'POST', urlParameters: `/${senderE164}/${serverGuid}`, - responseType: 'arraybuffer', + responseType: 'bytes', }); } async function requestVerificationSMS(number: string) { - return _ajax({ + await _ajax({ call: 'accounts', httpType: 'GET', urlParameters: `/sms/code/${number}`, @@ -1520,7 +1496,7 @@ export function initialize({ } async function requestVerificationVoice(number: string) { - return _ajax({ + await _ajax({ call: 'accounts', httpType: 'GET', urlParameters: `/voice/code/${number}`, @@ -1533,7 +1509,7 @@ export function initialize({ newPassword: string, registrationId: number, deviceName?: string | null, - options: { accessKey?: ArrayBuffer; uuid?: string } = {} + options: { accessKey?: Uint8Array; uuid?: string } = {} ) { const capabilities: CapabilitiesUploadType = { announcementGroup: true, @@ -1544,14 +1520,14 @@ export function initialize({ }; const { accessKey, uuid } = options; - const jsonData: any = { + const jsonData = { capabilities, fetchesMessages: true, name: deviceName || undefined, registrationId, supportsSms: false, unidentifiedAccessKey: accessKey - ? _btoa(_getString(accessKey)) + ? Bytes.toBase64(accessKey) : undefined, unrestrictedUnidentifiedAccess: false, }; @@ -1568,13 +1544,13 @@ export function initialize({ username = number; password = newPassword; - const response = await _ajax({ + const response = (await _ajax({ call, httpType: 'PUT', responseType: 'json', urlParameters: urlPrefix + code, jsonData, - }); + })) as { uuid?: string; deviceId?: number }; // Set final REST credentials to let `registerKeys` succeed. username = `${uuid || response.uuid || number}.${response.deviceId || 1}`; @@ -1584,7 +1560,7 @@ export function initialize({ } async function updateDeviceName(deviceName: string) { - return _ajax({ + await _ajax({ call: 'updateDeviceName', httpType: 'PUT', jsonData: { @@ -1594,18 +1570,19 @@ export function initialize({ } async function getIceServers() { - return _ajax({ + return (await _ajax({ call: 'getIceServers', httpType: 'GET', responseType: 'json', - }); + })) as GetIceServersResultType; } async function getDevices() { - return _ajax({ + return (await _ajax({ call: 'devices', httpType: 'GET', - }); + responseType: 'json', + })) as GetDevicesResultType; } type JSONSignedPreKeyType = { @@ -1621,34 +1598,25 @@ export function initialize({ keyId: number; publicKey: string; }>; - lastResortKey: { - keyId: number; - publicKey: string; - }; }; async function registerKeys(genKeys: KeysType) { const preKeys = genKeys.preKeys.map(key => ({ keyId: key.keyId, - publicKey: _btoa(_getString(key.publicKey)), + publicKey: Bytes.toBase64(key.publicKey), })); const keys: JSONKeysType = { - identityKey: _btoa(_getString(genKeys.identityKey)), + identityKey: Bytes.toBase64(genKeys.identityKey), signedPreKey: { keyId: genKeys.signedPreKey.keyId, - publicKey: _btoa(_getString(genKeys.signedPreKey.publicKey)), - signature: _btoa(_getString(genKeys.signedPreKey.signature)), + publicKey: Bytes.toBase64(genKeys.signedPreKey.publicKey), + signature: Bytes.toBase64(genKeys.signedPreKey.signature), }, preKeys, - // This is just to make the server happy (v2 clients should choke on publicKey) - lastResortKey: { - keyId: 0x7fffffff, - publicKey: _btoa('42'), - }, }; - return _ajax({ + await _ajax({ call: 'keys', httpType: 'PUT', jsonData: keys, @@ -1656,13 +1624,13 @@ export function initialize({ } async function setSignedPreKey(signedPreKey: SignedPreKeyType) { - return _ajax({ + await _ajax({ call: 'signed', httpType: 'PUT', jsonData: { keyId: signedPreKey.keyId, - publicKey: _btoa(_getString(signedPreKey.publicKey)), - signature: _btoa(_getString(signedPreKey.signature)), + publicKey: Bytes.toBase64(signedPreKey.publicKey), + signature: Bytes.toBase64(signedPreKey.signature), }, }); } @@ -1672,12 +1640,12 @@ export function initialize({ }; async function getMyKeys(): Promise { - const result: ServerKeyCountType = await _ajax({ + const result = (await _ajax({ call: 'keys', httpType: 'GET', responseType: 'json', validateResponse: { count: 'number' }, - }); + })) as ServerKeyCountType; return result.count; } @@ -1726,7 +1694,7 @@ export function initialize({ preKey = { keyId: device.preKey.keyId, - publicKey: _base64ToBytes(device.preKey.publicKey), + publicKey: Bytes.fromBase64(device.preKey.publicKey), }; } @@ -1736,26 +1704,27 @@ export function initialize({ preKey, signedPreKey: { keyId: device.signedPreKey.keyId, - publicKey: _base64ToBytes(device.signedPreKey.publicKey), - signature: _base64ToBytes(device.signedPreKey.signature), + publicKey: Bytes.fromBase64(device.signedPreKey.publicKey), + signature: Bytes.fromBase64(device.signedPreKey.signature), }, }; }); return { devices, - identityKey: _base64ToBytes(res.identityKey), + identityKey: Bytes.fromBase64(res.identityKey), }; } async function getKeysForIdentifier(identifier: string, deviceId?: number) { - return _ajax({ + const keys = (await _ajax({ call: 'keys', httpType: 'GET', urlParameters: `/${identifier}/${deviceId || '*'}`, responseType: 'json', validateResponse: { identityKey: 'string', devices: 'object' }, - }).then(handleKeys); + })) as ServerKeyResponseType; + return handleKeys(keys); } async function getKeysForIdentifierUnauth( @@ -1763,7 +1732,7 @@ export function initialize({ deviceId?: number, { accessKey }: { accessKey?: string } = {} ) { - return _ajax({ + const keys = (await _ajax({ call: 'keys', httpType: 'GET', urlParameters: `/${identifier}/${deviceId || '*'}`, @@ -1771,7 +1740,8 @@ export function initialize({ validateResponse: { identityKey: 'string', devices: 'object' }, unauthenticated: true, accessKey, - }).then(handleKeys); + })) as ServerKeyResponseType; + return handleKeys(keys); } function validateMessages(messages: Array): void { @@ -1782,20 +1752,21 @@ export function initialize({ async function sendMessagesUnauth( destination: string, - messageArray: Array, + messages: Array, timestamp: number, online?: boolean, { accessKey }: { accessKey?: string } = {} ) { - const jsonData: any = { messages: messageArray, timestamp }; - + let jsonData; if (online) { - jsonData.online = true; + jsonData = { messages, timestamp, online: true }; + } else { + jsonData = { messages, timestamp }; } - validateMessages(messageArray); + validateMessages(messages); - return _ajax({ + await _ajax({ call: 'messages', httpType: 'PUT', urlParameters: `/${destination}`, @@ -1808,19 +1779,20 @@ export function initialize({ async function sendMessages( destination: string, - messageArray: Array, + messages: Array, timestamp: number, online?: boolean ) { - const jsonData: any = { messages: messageArray, timestamp }; - + let jsonData; if (online) { - jsonData.online = true; + jsonData = { messages, timestamp, online: true }; + } else { + jsonData = { messages, timestamp }; } - validateMessages(messageArray); + validateMessages(messages); - return _ajax({ + await _ajax({ call: 'messages', httpType: 'PUT', urlParameters: `/${destination}`, @@ -1830,12 +1802,12 @@ export function initialize({ } async function sendWithSenderKey( - data: ArrayBuffer, - accessKeys: ArrayBuffer, + data: Uint8Array, + accessKeys: Uint8Array, timestamp: number, online?: boolean ): Promise { - return _ajax({ + const response = await _ajax({ call: 'multiRecipient', httpType: 'PUT', contentType: 'application/vnd.signal-messenger.mrm', @@ -1843,8 +1815,18 @@ export function initialize({ urlParameters: `?ts=${timestamp}&online=${online ? 'true' : 'false'}`, responseType: 'json', unauthenticated: true, - accessKey: arrayBufferToBase64(accessKeys), + accessKey: Bytes.toBase64(accessKeys), }); + const parseResult = multiRecipient200ResponseSchema.safeParse(response); + if (parseResult.success) { + return parseResult.data; + } + + log.warn( + 'WebAPI: invalid response from sendWithSenderKey', + toLogFormat(parseResult.error) + ); + return response as MultiRecipient200ResponseType; } function redactStickerUrl(stickerUrl: string) { @@ -1859,34 +1841,34 @@ export function initialize({ if (!isPackIdValid(packId)) { throw new Error('getSticker: pack ID was invalid'); } - return (await _outerAjax( + return _outerAjax( `${cdnUrlObject['0']}/stickers/${packId}/full/${stickerId}`, { certificateAuthority, proxyUrl, - responseType: 'arraybuffer', + responseType: 'bytes', type: 'GET', redactUrl: redactStickerUrl, version, } - )) as ArrayBuffer; + ); } async function getStickerPackManifest(packId: string) { if (!isPackIdValid(packId)) { throw new Error('getStickerPackManifest: pack ID was invalid'); } - return (await _outerAjax( + return _outerAjax( `${cdnUrlObject['0']}/stickers/${packId}/manifest.proto`, { certificateAuthority, proxyUrl, - responseType: 'arraybuffer', + responseType: 'bytes', type: 'GET', redactUrl: redactStickerUrl, version, } - )) as ArrayBuffer; + ); } type ServerAttachmentType = { @@ -1909,7 +1891,7 @@ export function initialize({ policy, signature, }: ServerAttachmentType, - encryptedBin: ArrayBuffer + encryptedBin: Uint8Array ) { // Note: when using the boundary string in the POST body, it needs to be prefixed by // an extra --, and the final boundary string at the end gets a -- prefix and a -- @@ -1959,17 +1941,21 @@ export function initialize({ } async function putStickers( - encryptedManifest: ArrayBuffer, - encryptedStickers: Array, + encryptedManifest: Uint8Array, + encryptedStickers: Array, onProgress?: () => void ) { // Get manifest and sticker upload parameters - const { packId, manifest, stickers } = await _ajax({ + const { packId, manifest, stickers } = (await _ajax({ call: 'getStickerPackUpload', responseType: 'json', httpType: 'GET', urlParameters: `/${encryptedStickers.length}`, - }); + })) as { + packId: string; + manifest: ServerAttachmentType; + stickers: ReadonlyArray; + }; // Upload manifest const manifestParams = makePutParams(manifest, encryptedManifest); @@ -2016,23 +2002,27 @@ export function initialize({ ? cdnUrlObject[cdnNumber] || cdnUrlObject['0'] : cdnUrlObject['0']; // This is going to the CDN, not the service, so we use _outerAjax - return (await _outerAjax(`${cdnUrl}/attachments/${cdnKey}`, { + return _outerAjax(`${cdnUrl}/attachments/${cdnKey}`, { certificateAuthority, proxyUrl, - responseType: 'arraybuffer', + responseType: 'bytes', timeout: 0, type: 'GET', redactUrl: _createRedactor(cdnKey), version, - })) as ArrayBuffer; + }); } - async function putAttachment(encryptedBin: ArrayBuffer) { - const response = await _ajax({ + type PutAttachmentResponseType = ServerAttachmentType & { + attachmentIdString: string; + }; + + async function putAttachment(encryptedBin: Uint8Array) { + const response = (await _ajax({ call: 'attachmentId', httpType: 'GET', responseType: 'json', - }); + })) as PutAttachmentResponseType; const { attachmentIdString } = response; @@ -2083,8 +2073,8 @@ export function initialize({ async function makeProxiedRequest( targetUrl: string, options: ProxiedRequestOptionsType = {} - ) { - const { returnArrayBuffer, start, end } = options; + ): Promise { + const { returnUint8Array, start, end } = options; const headers: HeaderListType = { 'X-SignalPadding': getHeaderPadding(), }; @@ -2093,21 +2083,21 @@ export function initialize({ headers.Range = `bytes=${start}-${end}`; } - const result = (await _outerAjax(targetUrl, { - responseType: returnArrayBuffer ? 'arraybufferwithdetails' : undefined, + const result = await _outerAjax(targetUrl, { + responseType: returnUint8Array ? 'byteswithdetails' : undefined, proxyUrl: contentProxyUrl, type: 'GET', redirect: 'follow', redactUrl: () => '[REDACTED_URL]', headers, version, - })) as ArrayBufferWithDetailsType; + }); - if (!returnArrayBuffer) { - return result; + if (!returnUint8Array) { + return result as Uint8Array; } - const { response } = result as ArrayBufferWithDetailsType; + const { response } = result as BytesWithDetailsType; if (!response.headers || !response.headers.get) { throw new Error('makeProxiedRequest: Problem retrieving header value'); } @@ -2125,7 +2115,7 @@ export function initialize({ return { totalSize, - result, + result: result as BytesWithDetailsType, }; } @@ -2133,18 +2123,18 @@ export function initialize({ targetUrl: string, type: HTTPCodeType, headers: HeaderListType, - body: ArrayBuffer | undefined - ): Promise { + body: Uint8Array | undefined + ): Promise { return _outerAjax(targetUrl, { certificateAuthority, data: body, headers, proxyUrl, - responseType: 'arraybufferwithdetails', + responseType: 'byteswithdetails', timeout: 0, type, version, - }) as Promise; + }); } // Groups @@ -2153,7 +2143,11 @@ export function initialize({ groupPublicParamsHex: string, authCredentialPresentationHex: string ) { - return _btoa(`${groupPublicParamsHex}:${authCredentialPresentationHex}`); + return Bytes.toBase64( + Bytes.fromString( + `${groupPublicParamsHex}:${authCredentialPresentationHex}` + ) + ); } type CredentialResponseType = { @@ -2164,12 +2158,12 @@ export function initialize({ startDay: number, endDay: number ): Promise> { - const response: CredentialResponseType = await _ajax({ + const response = (await _ajax({ call: 'getGroupCredentials', urlParameters: `/${startDay}/${endDay}`, httpType: 'GET', responseType: 'json', - }); + })) as CredentialResponseType; return response.credentials; } @@ -2182,16 +2176,16 @@ export function initialize({ options.authCredentialPresentationHex ); - const response: ArrayBuffer = await _ajax({ + const response = await _ajax({ basicAuth, call: 'groupToken', httpType: 'GET', contentType: 'application/x-protobuf', - responseType: 'arraybuffer', + responseType: 'bytes', host: storageUrl, }); - return Proto.GroupExternalCredential.decode(new FIXMEU8(response)); + return Proto.GroupExternalCredential.decode(response); } function verifyAttributes(attributes: Proto.IAvatarUploadAttributes) { @@ -2232,7 +2226,7 @@ export function initialize({ async function uploadAvatar( uploadAvatarRequestHeaders: UploadAvatarHeadersType, - avatarData: ArrayBuffer + avatarData: Uint8Array ): Promise { const verified = verifyAttributes(uploadAvatarRequestHeaders); const { key } = verified; @@ -2260,24 +2254,19 @@ export function initialize({ options.authCredentialPresentationHex ); - const response: ArrayBuffer = await _ajax({ + const response = await _ajax({ basicAuth, call: 'getGroupAvatarUpload', httpType: 'GET', - responseType: 'arraybuffer', + responseType: 'bytes', host: storageUrl, }); - const attributes = Proto.AvatarUploadAttributes.decode( - new FIXMEU8(response) - ); + const attributes = Proto.AvatarUploadAttributes.decode(response); const verified = verifyAttributes(attributes); const { key } = verified; - const manifestParams = makePutParams( - verified, - typedArrayToArrayBuffer(avatarData) - ); + const manifestParams = makePutParams(verified, avatarData); await _outerAjax(`${cdnUrlObject['0']}/`, { ...manifestParams, @@ -2291,15 +2280,15 @@ export function initialize({ return key; } - async function getGroupAvatar(key: string): Promise { + async function getGroupAvatar(key: string): Promise { return _outerAjax(`${cdnUrlObject['0']}/${key}`, { certificateAuthority, proxyUrl, - responseType: 'arraybuffer', + responseType: 'bytes', timeout: 0, type: 'GET', version, - }) as Promise; + }); } async function createGroup( @@ -2330,16 +2319,16 @@ export function initialize({ options.authCredentialPresentationHex ); - const response: ArrayBuffer = await _ajax({ + const response = await _ajax({ basicAuth, call: 'groups', contentType: 'application/x-protobuf', host: storageUrl, httpType: 'GET', - responseType: 'arraybuffer', + responseType: 'bytes', }); - return Proto.Group.decode(new FIXMEU8(response)); + return Proto.Group.decode(response); } async function getGroupFromLink( @@ -2352,18 +2341,18 @@ export function initialize({ ); const safeInviteLinkPassword = toWebSafeBase64(inviteLinkPassword); - const response: ArrayBuffer = await _ajax({ + const response = await _ajax({ basicAuth, call: 'groupsViaLink', contentType: 'application/x-protobuf', host: storageUrl, httpType: 'GET', - responseType: 'arraybuffer', + responseType: 'bytes', urlParameters: `/${safeInviteLinkPassword}`, redactUrl: _createRedactor(safeInviteLinkPassword), }); - return Proto.GroupJoinInfo.decode(new FIXMEU8(response)); + return Proto.GroupJoinInfo.decode(response); } async function modifyGroup( @@ -2380,14 +2369,14 @@ export function initialize({ ? toWebSafeBase64(inviteLinkBase64) : undefined; - const response: ArrayBuffer = await _ajax({ + const response = await _ajax({ basicAuth, call: 'groups', contentType: 'application/x-protobuf', data, host: storageUrl, httpType: 'PATCH', - responseType: 'arraybuffer', + responseType: 'bytes', urlParameters: safeInviteLinkPassword ? `?inviteLinkPassword=${safeInviteLinkPassword}` : undefined, @@ -2396,7 +2385,7 @@ export function initialize({ : undefined, }); - return Proto.GroupChange.decode(new FIXMEU8(response)); + return Proto.GroupChange.decode(response); } async function getGroupLog( @@ -2408,17 +2397,17 @@ export function initialize({ options.authCredentialPresentationHex ); - const withDetails: ArrayBufferWithDetailsType = await _ajax({ + const withDetails = await _ajax({ basicAuth, call: 'groupLog', contentType: 'application/x-protobuf', host: storageUrl, httpType: 'GET', - responseType: 'arraybufferwithdetails', + responseType: 'byteswithdetails', urlParameters: `/${startVersion}`, }); const { data, response } = withDetails; - const changes = Proto.GroupChanges.decode(new FIXMEU8(data)); + const changes = Proto.GroupChanges.decode(data); if (response && response.status === 206) { const range = response.headers.get('Content-Range'); @@ -2458,22 +2447,22 @@ export function initialize({ username: string; password: string; }> { - return _ajax({ + return (await _ajax({ call: 'directoryAuth', httpType: 'GET', responseType: 'json', - }); + })) as { username: string; password: string }; } function validateAttestationQuote({ serverStaticPublic, - quote: quoteArrayBuffer, + quote: quoteBytes, }: { - serverStaticPublic: ArrayBuffer; - quote: ArrayBuffer; + serverStaticPublic: Uint8Array; + quote: Uint8Array; }) { const SGX_CONSTANTS = getSgxConstants(); - const quote = Buffer.from(quoteArrayBuffer); + const quote = Buffer.from(quoteBytes); const quoteVersion = quote.readInt16LE(0) & 0xffff; if (quoteVersion < 0 || quoteVersion > 2) { @@ -2520,7 +2509,7 @@ export function initialize({ } const reportData = quote.slice(368, 368 + 64); - const serverStaticPublicBytes = new Uint8Array(serverStaticPublic); + const serverStaticPublicBytes = serverStaticPublic; if ( !reportData.every((byte, index) => { if (index >= 32) { @@ -2582,7 +2571,7 @@ export function initialize({ } async function validateAttestationSignature( - signature: ArrayBuffer, + signature: Uint8Array, signatureBody: string, certificates: string ) { @@ -2603,7 +2592,7 @@ export function initialize({ } const verify = createVerify('RSA-SHA256'); - verify.update(Buffer.from(bytesFromString(signatureBody))); + verify.update(Buffer.from(Bytes.fromString(signatureBody))); const isValid = verify.verify(pem[0], Buffer.from(signature)); if (!isValid) { throw new Error('Validation of signature across signatureBody failed!'); @@ -2650,7 +2639,7 @@ export function initialize({ const { privKey, pubKey } = keyPair; // Remove first "key type" byte from public key const slicedPubKey = pubKey.slice(1); - const pubKeyBase64 = arrayBufferToBase64(slicedPubKey); + const pubKeyBase64 = Bytes.toBase64(slicedPubKey); // Do request const data = JSON.stringify({ clientPublic: pubKeyBase64 }); const result: JSONWithDetailsType = (await _outerAjax(null, { @@ -2667,7 +2656,25 @@ export function initialize({ version, })) as JSONWithDetailsType; - const { data: responseBody, response } = result; + const { data: responseBody, response } = result as { + data: { + attestations: Record< + string, + { + ciphertext: string; + iv: string; + quote: string; + serverEphemeralPublic: string; + serverStaticPublic: string; + signature: string; + signatureBody: string; + tag: string; + certificates: string; + } + >; + }; + response: Response; + }; const attestationsLength = Object.keys(responseBody.attestations).length; if (attestationsLength > 3) { @@ -2689,19 +2696,20 @@ export function initialize({ attestations: await pProps( responseBody.attestations, async attestation => { - const decoded = { ...attestation }; - - [ - 'ciphertext', - 'iv', - 'quote', - 'serverEphemeralPublic', - 'serverStaticPublic', - 'signature', - 'tag', - ].forEach(prop => { - decoded[prop] = base64ToArrayBuffer(decoded[prop]); - }); + const decoded = { + ...attestation, + ciphertext: Bytes.fromBase64(attestation.ciphertext), + iv: Bytes.fromBase64(attestation.iv), + quote: Bytes.fromBase64(attestation.quote), + serverEphemeralPublic: Bytes.fromBase64( + attestation.serverEphemeralPublic + ), + serverStaticPublic: Bytes.fromBase64( + attestation.serverStaticPublic + ), + signature: Bytes.fromBase64(attestation.signature), + tag: Bytes.fromBase64(attestation.tag), + }; // Validate response validateAttestationQuote(decoded); @@ -2724,29 +2732,33 @@ export function initialize({ decoded.serverStaticPublic, privKey ); - const masterSecret = concatenateBytes( + const masterSecret = Bytes.concatenate([ ephemeralToEphemeral, - ephemeralToStatic - ); - const publicKeys = concatenateBytes( + ephemeralToStatic, + ]); + const publicKeys = Bytes.concatenate([ slicedPubKey, decoded.serverEphemeralPublic, - decoded.serverStaticPublic - ); - const [clientKey, serverKey] = await deriveSecrets( + decoded.serverStaticPublic, + ]); + const [clientKey, serverKey] = deriveSecrets( masterSecret, publicKeys, - new ArrayBuffer(0) + new Uint8Array(0) ); // Decrypt ciphertext into requestId - const requestId = await decryptAesGcm( + const requestId = decryptAesGcm( serverKey, decoded.iv, - concatenateBytes(decoded.ciphertext, decoded.tag) + Bytes.concatenate([decoded.ciphertext, decoded.tag]) ); - return { clientKey, serverKey, requestId }; + return { + clientKey, + serverKey, + requestId, + }; } ), }; @@ -2766,12 +2778,7 @@ export function initialize({ const { cookie } = attestationResult; // Send discovery request - const discoveryResponse: { - requestId: string; - iv: string; - data: string; - mac: string; - } = (await _outerAjax(null, { + const discoveryResponse = (await _outerAjax(null, { certificateAuthority, type: 'PUT', headers: cookie @@ -2788,14 +2795,19 @@ export function initialize({ timeout: 30000, data: JSON.stringify(data), version, - })) as any; + })) as { + requestId: string; + iv: string; + data: string; + mac: string; + }; // Decode discovery request response - const decodedDiscoveryResponse: { - [K in keyof typeof discoveryResponse]: ArrayBuffer; - } = mapValues(discoveryResponse, value => { - return base64ToArrayBuffer(value); - }) as any; + const decodedDiscoveryResponse = (mapValues(discoveryResponse, value => { + return Bytes.fromBase64(value); + }) as unknown) as { + [K in keyof typeof discoveryResponse]: Uint8Array; + }; const returnedAttestation = Object.values( attestationResult.attestations @@ -2807,13 +2819,13 @@ export function initialize({ } // Decrypt discovery response - const decryptedDiscoveryData = await decryptAesGcm( + const decryptedDiscoveryData = decryptAesGcm( returnedAttestation.serverKey, decodedDiscoveryResponse.iv, - concatenateBytes( + Bytes.concatenate([ decodedDiscoveryResponse.data, - decodedDiscoveryResponse.mac - ) + decodedDiscoveryResponse.mac, + ]) ); // Process and return result diff --git a/ts/textsecure/downloadAttachment.ts b/ts/textsecure/downloadAttachment.ts index 6a4399e1f..2ffb68063 100644 --- a/ts/textsecure/downloadAttachment.ts +++ b/ts/textsecure/downloadAttachment.ts @@ -8,9 +8,8 @@ import { dropNull } from '../util/dropNull'; import { DownloadedAttachmentType } from '../types/Attachment'; import * as MIME from '../types/MIME'; import * as Bytes from '../Bytes'; -import { typedArrayToArrayBuffer } from '../Crypto'; +import { getFirstBytes, decryptAttachment } from '../Crypto'; -import Crypto from './Crypto'; import { ProcessedAttachment } from './Types.d'; import type { WebAPIType } from './WebAPI'; @@ -36,10 +35,10 @@ export async function downloadAttachment( strictAssert(key, 'attachment has no key'); strictAssert(digest, 'attachment has no digest'); - const paddedData = await Crypto.decryptAttachment( + const paddedData = decryptAttachment( encrypted, - typedArrayToArrayBuffer(Bytes.fromBase64(key)), - typedArrayToArrayBuffer(Bytes.fromBase64(digest)) + Bytes.fromBase64(key), + Bytes.fromBase64(digest) ); if (!isNumber(size)) { @@ -48,7 +47,7 @@ export async function downloadAttachment( ); } - const data = window.Signal.Crypto.getFirstBytes(paddedData, size); + const data = getFirstBytes(paddedData, size); return { ...omit(attachment, 'digest', 'key'), diff --git a/ts/textsecure/index.ts b/ts/textsecure/index.ts index 08d110304..6822b27e9 100644 --- a/ts/textsecure/index.ts +++ b/ts/textsecure/index.ts @@ -5,17 +5,14 @@ import EventTarget from './EventTarget'; import AccountManager from './AccountManager'; import MessageReceiver from './MessageReceiver'; import utils from './Helpers'; -import Crypto from './Crypto'; import { ContactBuffer, GroupBuffer } from './ContactsParser'; import SyncRequest from './SyncRequest'; import MessageSender from './SendMessage'; -import StringView from './StringView'; import { Storage } from './Storage'; import * as WebAPI from './WebAPI'; import WebSocketResource from './WebsocketResources'; export const textsecure = { - crypto: Crypto, utils, storage: new Storage(), @@ -26,7 +23,6 @@ export const textsecure = { MessageReceiver, MessageSender, SyncRequest, - StringView, WebAPI, WebSocketResource, }; diff --git a/ts/textsecure/messageReceiverEvents.ts b/ts/textsecure/messageReceiverEvents.ts index 69e481943..a12e32605 100644 --- a/ts/textsecure/messageReceiverEvents.ts +++ b/ts/textsecure/messageReceiverEvents.ts @@ -79,7 +79,7 @@ export class ErrorEvent extends Event { } export type DecryptionErrorEventData = Readonly<{ - cipherTextBytes?: ArrayBuffer; + cipherTextBytes?: Uint8Array; cipherTextType?: number; contentHint?: number; groupId?: string; @@ -342,7 +342,7 @@ export class FetchLatestEvent extends ConfirmableEvent { export class KeysEvent extends ConfirmableEvent { constructor( - public readonly storageServiceKey: ArrayBuffer, + public readonly storageServiceKey: Uint8Array, confirm: ConfirmCallback ) { super('keys', confirm); @@ -369,7 +369,7 @@ export type VerifiedEventData = Readonly<{ state: Proto.IVerified['state']; destination?: string; destinationUuid?: string; - identityKey?: ArrayBuffer; + identityKey?: Uint8Array; // Used in `ts/background.ts` viaContactSync?: boolean; diff --git a/ts/textsecure/processDataMessage.ts b/ts/textsecure/processDataMessage.ts index 1b72b05e3..562708d62 100644 --- a/ts/textsecure/processDataMessage.ts +++ b/ts/textsecure/processDataMessage.ts @@ -9,7 +9,7 @@ import { normalizeNumber } from '../util/normalizeNumber'; import { SignalService as Proto } from '../protobuf'; import { deriveGroupFields } from '../groups'; import * as Bytes from '../Bytes'; -import { deriveMasterKeyFromGroupV1, typedArrayToArrayBuffer } from '../Crypto'; +import { deriveMasterKeyFromGroupV1 } from '../Crypto'; import { ProcessedAttachment, @@ -25,9 +25,6 @@ import { } from './Types.d'; import { WarnOnlyError } from './Errors'; -// TODO: remove once we move away from ArrayBuffers -const FIXMEU8 = Uint8Array; - const FLAGS = Proto.DataMessage.Flags; export const ATTACHMENT_MAX = 32; @@ -57,9 +54,9 @@ export function processAttachment( }; } -async function processGroupContext( +function processGroupContext( group?: Proto.IGroupContext | null -): Promise { +): ProcessedGroupContext | undefined { if (!group) { return undefined; } @@ -70,10 +67,8 @@ async function processGroupContext( 'group context without type' ); - const masterKey = await deriveMasterKeyFromGroupV1( - typedArrayToArrayBuffer(group.id) - ); - const data = deriveGroupFields(new FIXMEU8(masterKey)); + const masterKey = deriveMasterKeyFromGroupV1(group.id); + const data = deriveGroupFields(masterKey); const derivedGroupV2Id = Bytes.toBase64(data.id); @@ -266,7 +261,7 @@ export async function processDataMessage( ).map((attachment: Proto.IAttachmentPointer) => processAttachment(attachment) ), - group: await processGroupContext(message.group), + group: processGroupContext(message.group), groupV2: processGroupV2Context(message.groupV2), flags: message.flags ?? 0, expireTimer: message.expireTimer ?? 0, diff --git a/ts/types/Attachment.ts b/ts/types/Attachment.ts index b3fc4c6a8..5f1ec9fc6 100644 --- a/ts/types/Attachment.ts +++ b/ts/types/Attachment.ts @@ -6,12 +6,12 @@ import moment from 'moment'; import { isNumber, padStart, - isArrayBuffer, + isTypedArray, isFunction, isUndefined, omit, } from 'lodash'; -import { arrayBufferToBlob, blobToArrayBuffer } from 'blob-util'; +import { blobToArrayBuffer } from 'blob-util'; import { LoggerType } from './Logging'; import * as MIME from './MIME'; @@ -63,14 +63,14 @@ export type AttachmentType = { cdnNumber?: number; cdnId?: string; cdnKey?: string; - data?: ArrayBuffer; + data?: Uint8Array; /** Legacy field. Used only for downloading old attachments */ id?: number; }; export type DownloadedAttachmentType = AttachmentType & { - data: ArrayBuffer; + data: Uint8Array; }; export type BaseAttachmentDraftType = { @@ -85,9 +85,9 @@ export type BaseAttachmentDraftType = { export type InMemoryAttachmentDraftType = | ({ - data?: ArrayBuffer; + data?: Uint8Array; pending: false; - screenshotData?: ArrayBuffer; + screenshotData?: Uint8Array; } & BaseAttachmentDraftType) | { contentType: MIME.MIMEType; @@ -138,7 +138,7 @@ export async function migrateDataToFileSystem( { writeNewAttachmentData, }: { - writeNewAttachmentData: (data: ArrayBuffer) => Promise; + writeNewAttachmentData: (data: Uint8Array) => Promise; } ): Promise { if (!isFunction(writeNewAttachmentData)) { @@ -152,9 +152,9 @@ export async function migrateDataToFileSystem( return attachment; } - if (!isArrayBuffer(data)) { + if (!isTypedArray(data)) { throw new TypeError( - 'Expected `attachment.data` to be an array buffer;' + + 'Expected `attachment.data` to be a typed array;' + ` got: ${typeof attachment.data}` ); } @@ -169,19 +169,19 @@ export async function migrateDataToFileSystem( // { // id: string // contentType: MIMEType -// data: ArrayBuffer -// digest: ArrayBuffer +// data: Uint8Array +// digest: Uint8Array // fileName?: string // flags: null -// key: ArrayBuffer +// key: Uint8Array // size: integer -// thumbnail: ArrayBuffer +// thumbnail: Uint8Array // } // // Outgoing message attachment fields // { // contentType: MIMEType -// data: ArrayBuffer +// data: Uint8Array // fileName: string // size: integer // } @@ -232,10 +232,9 @@ export async function autoOrientJPEG( return attachment; } - const dataBlob = await arrayBufferToBlob( - attachment.data, - attachment.contentType - ); + const dataBlob = new Blob([attachment.data], { + type: attachment.contentType, + }); const { blob: xcodedDataBlob } = await scaleImageToLevel( dataBlob, attachment.contentType, @@ -243,7 +242,7 @@ export async function autoOrientJPEG( ); const xcodedDataArrayBuffer = await blobToArrayBuffer(xcodedDataBlob); - // IMPORTANT: We overwrite the existing `data` `ArrayBuffer` losing the original + // IMPORTANT: We overwrite the existing `data` `Uint8Array` losing the original // image data. Ideally, we’d preserve the original image data for users who want to // retain it but due to reports of data loss, we don’t want to overburden IndexedDB // by potentially doubling stored image data. @@ -251,7 +250,7 @@ export async function autoOrientJPEG( const xcodedAttachment = { // `digest` is no longer valid for auto-oriented image data, so we discard it: ...omit(attachment, 'digest'), - data: xcodedDataArrayBuffer, + data: new Uint8Array(xcodedDataArrayBuffer), size: xcodedDataArrayBuffer.byteLength, }; @@ -335,14 +334,11 @@ export function removeSchemaVersion({ } export function hasData(attachment: AttachmentType): boolean { - return ( - attachment.data instanceof ArrayBuffer || - ArrayBuffer.isView(attachment.data) - ); + return attachment.data instanceof Uint8Array; } export function loadData( - readAttachmentData: (path: string) => Promise + readAttachmentData: (path: string) => Promise ): (attachment?: AttachmentType) => Promise { if (!is.function_(readAttachmentData)) { throw new TypeError("'readAttachmentData' must be a function"); @@ -400,9 +396,12 @@ const THUMBNAIL_CONTENT_TYPE = MIME.IMAGE_PNG; export async function captureDimensionsAndScreenshot( attachment: AttachmentType, params: { - writeNewAttachmentData: (data: ArrayBuffer) => Promise; + writeNewAttachmentData: (data: Uint8Array) => Promise; getAbsoluteAttachmentPath: (path: string) => Promise; - makeObjectUrl: (data: ArrayBuffer, contentType: MIME.MIMEType) => string; + makeObjectUrl: ( + data: Uint8Array | ArrayBuffer, + contentType: MIME.MIMEType + ) => string; revokeObjectUrl: (path: string) => void; getImageDimensions: (params: { objectUrl: string; @@ -464,7 +463,9 @@ export async function captureDimensionsAndScreenshot( }) ); - const thumbnailPath = await writeNewAttachmentData(thumbnailBuffer); + const thumbnailPath = await writeNewAttachmentData( + new Uint8Array(thumbnailBuffer) + ); return { ...attachment, width, @@ -503,7 +504,9 @@ export async function captureDimensionsAndScreenshot( objectUrl: screenshotObjectUrl, logger, }); - const screenshotPath = await writeNewAttachmentData(screenshotBuffer); + const screenshotPath = await writeNewAttachmentData( + new Uint8Array(screenshotBuffer) + ); const thumbnailBuffer = await blobToArrayBuffer( await makeImageThumbnail({ @@ -514,7 +517,9 @@ export async function captureDimensionsAndScreenshot( }) ); - const thumbnailPath = await writeNewAttachmentData(thumbnailBuffer); + const thumbnailPath = await writeNewAttachmentData( + new Uint8Array(thumbnailBuffer) + ); return { ...attachment, @@ -876,14 +881,14 @@ export const save = async ({ }: { attachment: AttachmentType; index?: number; - readAttachmentData: (relativePath: string) => Promise; + readAttachmentData: (relativePath: string) => Promise; saveAttachmentToDisk: (options: { - data: ArrayBuffer; + data: Uint8Array; name: string; - }) => Promise<{ name: string; fullPath: string }>; + }) => Promise<{ name: string; fullPath: string } | null>; timestamp?: number; }): Promise => { - let data: ArrayBuffer; + let data: Uint8Array; if (attachment.path) { data = await readAttachmentData(attachment.path); } else if (attachment.data) { diff --git a/ts/types/Avatar.ts b/ts/types/Avatar.ts index da1a289ef..73b4016ba 100644 --- a/ts/types/Avatar.ts +++ b/ts/types/Avatar.ts @@ -42,7 +42,7 @@ export type AvatarIconType = GroupAvatarIconType | PersonalAvatarIconType; export type AvatarDataType = { id: number | string; - buffer?: ArrayBuffer; + buffer?: Uint8Array; color?: AvatarColorType; icon?: AvatarIconType; imagePath?: string; diff --git a/ts/types/Conversation.ts b/ts/types/Conversation.ts new file mode 100644 index 000000000..082517ae7 --- /dev/null +++ b/ts/types/Conversation.ts @@ -0,0 +1,89 @@ +// Copyright 2018-2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { computeHash } from '../Crypto'; +import { ConversationAttributesType } from '../model-types.d'; + +export type BuildAvatarUpdaterOptions = Readonly<{ + deleteAttachmentData: (path: string) => Promise; + doesAttachmentExist: (path: string) => Promise; + writeNewAttachmentData: (data: Uint8Array) => Promise; +}>; + +function buildAvatarUpdater({ field }: { field: 'avatar' | 'profileAvatar' }) { + return async ( + conversation: Readonly, + data: Uint8Array, + { + deleteAttachmentData, + doesAttachmentExist, + writeNewAttachmentData, + }: BuildAvatarUpdaterOptions + ): Promise => { + if (!conversation) { + return conversation; + } + + const avatar = conversation[field]; + + const newHash = computeHash(data); + + if (!avatar || !avatar.hash) { + return { + ...conversation, + [field]: { + hash: newHash, + path: await writeNewAttachmentData(data), + }, + }; + } + + const { hash, path } = avatar; + const exists = await doesAttachmentExist(path); + if (!exists) { + window.SignalWindow.log.warn( + `Conversation.buildAvatarUpdater: attachment ${path} did not exist` + ); + } + + if (exists && hash === newHash) { + return conversation; + } + + await deleteAttachmentData(path); + + return { + ...conversation, + [field]: { + hash: newHash, + path: await writeNewAttachmentData(data), + }, + }; + }; +} + +export const maybeUpdateAvatar = buildAvatarUpdater({ field: 'avatar' }); +export const maybeUpdateProfileAvatar = buildAvatarUpdater({ + field: 'profileAvatar', +}); + +export async function deleteExternalFiles( + conversation: ConversationAttributesType, + { + deleteAttachmentData, + }: Pick +): Promise { + if (!conversation) { + return; + } + + const { avatar, profileAvatar } = conversation; + + if (avatar && avatar.path) { + await deleteAttachmentData(avatar.path); + } + + if (profileAvatar && profileAvatar.path) { + await deleteAttachmentData(profileAvatar.path); + } +} diff --git a/ts/types/Crypto.ts b/ts/types/Crypto.ts new file mode 100644 index 000000000..5bd856c3b --- /dev/null +++ b/ts/types/Crypto.ts @@ -0,0 +1,13 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +export enum HashType { + size256 = 'sha256', + size512 = 'sha512', +} + +export enum CipherType { + AES256CBC = 'aes-256-cbc', + AES256CTR = 'aes-256-ctr', + AES256GCM = 'aes-256-gcm', +} diff --git a/ts/types/EmbeddedContact.ts b/ts/types/EmbeddedContact.ts index b082242f6..0797dfd94 100644 --- a/ts/types/EmbeddedContact.ts +++ b/ts/types/EmbeddedContact.ts @@ -152,7 +152,7 @@ export function parseAndWriteAvatar( message: MessageAttributesType; regionCode: string; logger: Pick; - writeNewAttachmentData: (data: ArrayBuffer) => Promise; + writeNewAttachmentData: (data: Uint8Array) => Promise; } ): Promise => { const { message, regionCode, logger } = context; diff --git a/ts/types/LinkPreview.ts b/ts/types/LinkPreview.ts index 697cb709d..e41a4df8b 100644 --- a/ts/types/LinkPreview.ts +++ b/ts/types/LinkPreview.ts @@ -11,7 +11,7 @@ import { replaceEmojiWithSpaces } from '../util/emoji'; import { AttachmentType } from './Attachment'; export type LinkPreviewImage = AttachmentType & { - data: ArrayBuffer; + data: Uint8Array; }; export type LinkPreviewResult = { diff --git a/ts/types/SchemaVersion.ts b/ts/types/SchemaVersion.ts new file mode 100644 index 000000000..e771ccdde --- /dev/null +++ b/ts/types/SchemaVersion.ts @@ -0,0 +1,8 @@ +// Copyright 2018-2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { isNumber } from 'lodash'; + +export const isValid = (value: unknown): boolean => { + return Boolean(isNumber(value) && value >= 0); +}; diff --git a/ts/types/Stickers.ts b/ts/types/Stickers.ts index 2271d3e3e..318bc4e04 100644 --- a/ts/types/Stickers.ts +++ b/ts/types/Stickers.ts @@ -9,7 +9,8 @@ import { strictAssert } from '../util/assert'; import { dropNull } from '../util/dropNull'; import { makeLookup } from '../util/makeLookup'; import { maybeParseUrl } from '../util/url'; -import { base64ToArrayBuffer, deriveStickerPackKey } from '../Crypto'; +import * as Bytes from '../Bytes'; +import { deriveStickerPackKey, decryptAttachment } from '../Crypto'; import type { StickerType, StickerPackType, @@ -41,9 +42,6 @@ export type DownloadMap = Record< } >; -// TODO: remove once we move away from ArrayBuffers -const FIXMEU8 = Uint8Array; - export const BLESSED_PACKS: Record = { '9acc9e8aba563d26a4994e69263e3b25': { key: 'Wm3/OUjCjvubeq+T7MN1xp/DFueAd+0mhnoU0QoPahI=', @@ -287,16 +285,10 @@ function getReduxStickerActions() { return actions.stickers; } -async function decryptSticker( - packKey: string, - ciphertext: ArrayBuffer -): Promise { - const binaryKey = base64ToArrayBuffer(packKey); - const derivedKey = await deriveStickerPackKey(binaryKey); - const plaintext = await window.textsecure.crypto.decryptAttachment( - ciphertext, - derivedKey - ); +function decryptSticker(packKey: string, ciphertext: Uint8Array): Uint8Array { + const binaryKey = Bytes.fromBase64(packKey); + const derivedKey = deriveStickerPackKey(binaryKey); + const plaintext = decryptAttachment(ciphertext, derivedKey); return plaintext; } @@ -311,7 +303,7 @@ async function downloadSticker( strictAssert(id !== undefined && id !== null, "Sticker id can't be null"); const ciphertext = await window.textsecure.messaging.getSticker(packId, id); - const plaintext = await decryptSticker(packKey, ciphertext); + const plaintext = decryptSticker(packKey, ciphertext); const sticker = ephemeral ? await window.Signal.Migrations.processNewEphemeralSticker(plaintext) @@ -413,8 +405,8 @@ export async function downloadEphemeralPack( const ciphertext = await window.textsecure.messaging.getStickerPackManifest( packId ); - const plaintext = await decryptSticker(packKey, ciphertext); - const proto = Proto.StickerPack.decode(new FIXMEU8(plaintext)); + const plaintext = decryptSticker(packKey, ciphertext); + const proto = Proto.StickerPack.decode(plaintext); const firstStickerProto = proto.stickers ? proto.stickers[0] : null; const stickerCount = proto.stickers.length; @@ -594,8 +586,8 @@ async function doDownloadStickerPack( const ciphertext = await window.textsecure.messaging.getStickerPackManifest( packId ); - const plaintext = await decryptSticker(packKey, ciphertext); - const proto = Proto.StickerPack.decode(new FIXMEU8(plaintext)); + const plaintext = decryptSticker(packKey, ciphertext); + const proto = Proto.StickerPack.decode(plaintext); const firstStickerProto = proto.stickers ? proto.stickers[0] : undefined; const stickerCount = proto.stickers.length; diff --git a/ts/types/Storage.d.ts b/ts/types/Storage.d.ts index 4ea05a62e..37d876a72 100644 --- a/ts/types/Storage.d.ts +++ b/ts/types/Storage.d.ts @@ -21,7 +21,7 @@ import type { export type SerializedCertificateType = { expires: number; - serialized: ArrayBuffer; + serialized: Uint8Array; }; export type ZoomFactorType = 0.75 | 1 | 1.25 | 1.5 | 2 | number; @@ -70,7 +70,7 @@ export type StorageAccessType = { maxPreKeyId: number; number_id: string; password: string; - profileKey: ArrayBuffer; + profileKey: Uint8Array; regionCode: string; registrationIdMap: Record; remoteBuildExpiration: number; diff --git a/js/modules/types/visual_attachment.js b/ts/types/VisualAttachment.ts similarity index 55% rename from js/modules/types/visual_attachment.js rename to ts/types/VisualAttachment.ts index d876ce596..54396aab4 100644 --- a/js/modules/types/visual_attachment.js +++ b/ts/types/VisualAttachment.ts @@ -1,20 +1,27 @@ // Copyright 2018-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -/* global document, URL, Blob */ +import loadImage from 'blueimp-load-image'; +import { blobToArrayBuffer } from 'blob-util'; +import { toLogFormat } from './errors'; +import { MIMEType, IMAGE_PNG } from './MIME'; +import { LoggerType } from './Logging'; +import { arrayBufferToObjectURL } from '../util/arrayBufferToObjectURL'; +import { strictAssert } from '../util/assert'; +import { canvasToBlob } from '../util/canvasToBlob'; -const loadImage = require('blueimp-load-image'); -const { blobToArrayBuffer } = require('blob-util'); -const { toLogFormat } = require('../../../ts/types/errors'); -const { - arrayBufferToObjectURL, -} = require('../../../ts/util/arrayBufferToObjectURL'); -const { canvasToBlob } = require('../../../ts/util/canvasToBlob'); +export { blobToArrayBuffer }; -exports.blobToArrayBuffer = blobToArrayBuffer; +export type GetImageDimensionsOptionsType = Readonly<{ + objectUrl: string; + logger: Pick; +}>; -exports.getImageDimensions = ({ objectUrl, logger }) => - new Promise((resolve, reject) => { +export function getImageDimensions({ + objectUrl, + logger, +}: GetImageDimensionsOptionsType): Promise<{ width: number; height: number }> { + return new Promise((resolve, reject) => { const image = document.createElement('img'); image.addEventListener('load', () => { @@ -30,14 +37,22 @@ exports.getImageDimensions = ({ objectUrl, logger }) => image.src = objectUrl; }); +} -exports.makeImageThumbnail = ({ +export type MakeImageThumbnailOptionsType = Readonly<{ + size: number; + objectUrl: string; + contentType?: MIMEType; + logger: Pick; +}>; + +export function makeImageThumbnail({ size, objectUrl, - contentType = 'image/png', + contentType = IMAGE_PNG, logger, -}) => - new Promise((resolve, reject) => { +}: MakeImageThumbnailOptionsType): Promise { + return new Promise((resolve, reject) => { const image = document.createElement('img'); image.addEventListener('load', async () => { @@ -63,6 +78,11 @@ exports.makeImageThumbnail = ({ minHeight: size, }); + strictAssert( + canvas instanceof HTMLCanvasElement, + 'loadImage must produce canvas' + ); + try { const blob = await canvasToBlob(canvas, contentType); resolve(blob); @@ -78,13 +98,20 @@ exports.makeImageThumbnail = ({ image.src = objectUrl; }); +} -exports.makeVideoScreenshot = ({ +export type MakeVideoScreenshotOptionsType = Readonly<{ + objectUrl: string; + contentType?: MIMEType; + logger: Pick; +}>; + +export function makeVideoScreenshot({ objectUrl, - contentType = 'image/png', + contentType = IMAGE_PNG, logger, -}) => - new Promise((resolve, reject) => { +}: MakeVideoScreenshotOptionsType): Promise { + return new Promise((resolve, reject) => { const video = document.createElement('video'); function seek() { @@ -95,9 +122,9 @@ exports.makeVideoScreenshot = ({ const canvas = document.createElement('canvas'); canvas.width = video.videoWidth; canvas.height = video.videoHeight; - canvas - .getContext('2d') - .drawImage(video, 0, 0, canvas.width, canvas.height); + const context = canvas.getContext('2d'); + strictAssert(context, 'Failed to get canvas context'); + context.drawImage(video, 0, 0, canvas.width, canvas.height); video.addEventListener('loadeddata', seek); video.removeEventListener('seeked', capture); @@ -120,16 +147,24 @@ exports.makeVideoScreenshot = ({ video.src = objectUrl; }); +} -exports.makeVideoThumbnail = async ({ +export type MakeVideoThumbnailOptionsType = Readonly<{ + size: number; + videoObjectUrl: string; + logger: Pick; + contentType: MIMEType; +}>; + +export async function makeVideoThumbnail({ size, videoObjectUrl, logger, contentType, -}) => { - let screenshotObjectUrl; +}: MakeVideoThumbnailOptionsType): Promise { + let screenshotObjectUrl: string | undefined; try { - const blob = await exports.makeVideoScreenshot({ + const blob = await makeVideoScreenshot({ objectUrl: videoObjectUrl, contentType, logger, @@ -141,7 +176,7 @@ exports.makeVideoThumbnail = async ({ }); // We need to wait for this, otherwise the finally below will run first - const resultBlob = await exports.makeImageThumbnail({ + const resultBlob = await makeImageThumbnail({ size, objectUrl: screenshotObjectUrl, contentType, @@ -150,18 +185,23 @@ exports.makeVideoThumbnail = async ({ return resultBlob; } finally { - exports.revokeObjectUrl(screenshotObjectUrl); + if (screenshotObjectUrl !== undefined) { + revokeObjectUrl(screenshotObjectUrl); + } } -}; +} -exports.makeObjectUrl = (data, contentType) => { +export function makeObjectUrl( + data: Uint8Array | ArrayBuffer, + contentType: MIMEType +): string { const blob = new Blob([data], { type: contentType, }); return URL.createObjectURL(blob); -}; +} -exports.revokeObjectUrl = objectUrl => { +export function revokeObjectUrl(objectUrl: string): void { URL.revokeObjectURL(objectUrl); -}; +} diff --git a/ts/types/errors.ts b/ts/types/errors.ts index 4f7ecbf72..c41afe842 100644 --- a/ts/types/errors.ts +++ b/ts/types/errors.ts @@ -1,6 +1,8 @@ // Copyright 2018-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +/* eslint-disable max-classes-per-file */ + export function toLogFormat(error: unknown): string { if (error instanceof Error && error.stack) { return error.stack; @@ -10,3 +12,5 @@ export function toLogFormat(error: unknown): string { } export class CapabilityError extends Error {} + +export class ProfileDecryptError extends Error {} diff --git a/ts/updateConversationsWithUuidLookup.ts b/ts/updateConversationsWithUuidLookup.ts index 67a806a6b..579875fbd 100644 --- a/ts/updateConversationsWithUuidLookup.ts +++ b/ts/updateConversationsWithUuidLookup.ts @@ -1,9 +1,9 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import { ConversationController } from './ConversationController'; -import { ConversationModel } from './models/conversations'; -import SendMessage from './textsecure/SendMessage'; +import type { ConversationController } from './ConversationController'; +import type { ConversationModel } from './models/conversations'; +import type SendMessage from './textsecure/SendMessage'; import { assert } from './util/assert'; import { getOwn } from './util/getOwn'; import { isNotNil } from './util/isNotNil'; diff --git a/ts/util/avatarDataToArrayBuffer.ts b/ts/util/avatarDataToBytes.ts similarity index 93% rename from ts/util/avatarDataToArrayBuffer.ts rename to ts/util/avatarDataToBytes.ts index 3878f1feb..c6a7fe809 100644 --- a/ts/util/avatarDataToArrayBuffer.ts +++ b/ts/util/avatarDataToBytes.ts @@ -3,7 +3,7 @@ import { AvatarColorMap, AvatarColorType } from '../types/Colors'; import { AvatarDataType } from '../types/Avatar'; -import { canvasToArrayBuffer } from './canvasToArrayBuffer'; +import { canvasToBytes } from './canvasToBytes'; import { getFittedFontSize } from './avatarTextSizeCalculator'; const CANVAS_SIZE = 1024; @@ -74,9 +74,9 @@ async function getFont(text: string): Promise { return `${fontSize}px Inter`; } -export async function avatarDataToArrayBuffer( +export async function avatarDataToBytes( avatarData: AvatarDataType -): Promise { +): Promise { const canvas = document.createElement('canvas'); canvas.width = CANVAS_SIZE; canvas.height = CANVAS_SIZE; @@ -84,7 +84,7 @@ export async function avatarDataToArrayBuffer( if (!context) { throw new Error( - 'avatarDataToArrayBuffer: could not get canvas rendering context' + 'avatarDataToBytes: could not get canvas rendering context' ); } @@ -117,5 +117,5 @@ export async function avatarDataToArrayBuffer( setCanvasBackground(bg, context, canvas); } - return canvasToArrayBuffer(canvas); + return canvasToBytes(canvas); } diff --git a/ts/util/callingMessageToProto.ts b/ts/util/callingMessageToProto.ts index 3f67175eb..b94db068e 100644 --- a/ts/util/callingMessageToProto.ts +++ b/ts/util/callingMessageToProto.ts @@ -6,9 +6,6 @@ import { SignalService as Proto } from '../protobuf'; import * as log from '../logging/log'; import { missingCaseError } from './missingCaseError'; -// TODO: remove once we move away from ArrayBuffers -const FIXMEU8 = Uint8Array; - export function callingMessageToProto( { offer, @@ -88,7 +85,7 @@ function bufferToProto( return value; } - return new FIXMEU8(value.toArrayBuffer()); + return new Uint8Array(value.toArrayBuffer()); } function urgencyToProto( diff --git a/ts/util/canvasToArrayBuffer.ts b/ts/util/canvasToBytes.ts similarity index 73% rename from ts/util/canvasToArrayBuffer.ts rename to ts/util/canvasToBytes.ts index 06bccb244..f970cf588 100644 --- a/ts/util/canvasToArrayBuffer.ts +++ b/ts/util/canvasToBytes.ts @@ -4,11 +4,11 @@ import { canvasToBlob } from './canvasToBlob'; import { MIMEType } from '../types/MIME'; -export async function canvasToArrayBuffer( +export async function canvasToBytes( canvas: HTMLCanvasElement, mimeType?: MIMEType, quality?: number -): Promise { +): Promise { const blob = await canvasToBlob(canvas, mimeType, quality); - return blob.arrayBuffer(); + return new Uint8Array(await blob.arrayBuffer()); } diff --git a/ts/util/encryptProfileData.ts b/ts/util/encryptProfileData.ts index e9fe9ee76..043796aa1 100644 --- a/ts/util/encryptProfileData.ts +++ b/ts/util/encryptProfileData.ts @@ -1,23 +1,21 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import Crypto, { PaddedLengths } from '../textsecure/Crypto'; import { ConversationType } from '../state/ducks/conversations'; import { ProfileRequestDataType } from '../textsecure/WebAPI'; import { assert } from './assert'; +import * as Bytes from '../Bytes'; import { - arrayBufferToBase64, - base64ToArrayBuffer, - bytesFromString, + PaddedLengths, + encryptProfile, + encryptProfileItemWithPadding, } from '../Crypto'; import { deriveProfileKeyCommitment, deriveProfileKeyVersion } from './zkgroup'; -const { encryptProfile, encryptProfileItemWithPadding } = Crypto; - export async function encryptProfileData( conversation: ConversationType, - avatarBuffer?: ArrayBuffer -): Promise<[ProfileRequestDataType, ArrayBuffer | undefined]> { + avatarBuffer?: Uint8Array +): Promise<[ProfileRequestDataType, Uint8Array | undefined]> { const { aboutEmoji, aboutText, @@ -30,43 +28,41 @@ export async function encryptProfileData( assert(profileKey, 'profileKey'); assert(uuid, 'uuid'); - const keyBuffer = base64ToArrayBuffer(profileKey); + const keyBuffer = Bytes.fromBase64(profileKey); const fullName = [firstName, familyName].filter(Boolean).join('\0'); - const [ - bytesName, - bytesAbout, - bytesAboutEmoji, - encryptedAvatarData, - ] = await Promise.all([ - encryptProfileItemWithPadding( - bytesFromString(fullName), - keyBuffer, - PaddedLengths.Name - ), - aboutText - ? encryptProfileItemWithPadding( - bytesFromString(aboutText), - keyBuffer, - PaddedLengths.About - ) - : null, - aboutEmoji - ? encryptProfileItemWithPadding( - bytesFromString(aboutEmoji), - keyBuffer, - PaddedLengths.AboutEmoji - ) - : null, - avatarBuffer ? encryptProfile(avatarBuffer, keyBuffer) : undefined, - ]); + const bytesName = encryptProfileItemWithPadding( + Bytes.fromString(fullName), + keyBuffer, + PaddedLengths.Name + ); + + const bytesAbout = aboutText + ? encryptProfileItemWithPadding( + Bytes.fromString(aboutText), + keyBuffer, + PaddedLengths.About + ) + : null; + + const bytesAboutEmoji = aboutEmoji + ? encryptProfileItemWithPadding( + Bytes.fromString(aboutEmoji), + keyBuffer, + PaddedLengths.AboutEmoji + ) + : null; + + const encryptedAvatarData = avatarBuffer + ? encryptProfile(avatarBuffer, keyBuffer) + : undefined; const profileData = { version: deriveProfileKeyVersion(profileKey, uuid), - name: arrayBufferToBase64(bytesName), - about: bytesAbout ? arrayBufferToBase64(bytesAbout) : null, - aboutEmoji: bytesAboutEmoji ? arrayBufferToBase64(bytesAboutEmoji) : null, + name: Bytes.toBase64(bytesName), + about: bytesAbout ? Bytes.toBase64(bytesAbout) : null, + aboutEmoji: bytesAboutEmoji ? Bytes.toBase64(bytesAboutEmoji) : null, paymentAddress: window.storage.get('paymentAddress') || null, avatar: Boolean(avatarBuffer), commitment: deriveProfileKeyCommitment(profileKey, uuid), diff --git a/ts/util/getProfile.ts b/ts/util/getProfile.ts index b313090df..6deb5429b 100644 --- a/ts/util/getProfile.ts +++ b/ts/util/getProfile.ts @@ -6,12 +6,8 @@ import { SEALED_SENDER } from '../types/SealedSender'; import { Address } from '../types/Address'; import { QualifiedAddress } from '../types/QualifiedAddress'; import { UUID } from '../types/UUID'; -import { - base64ToArrayBuffer, - stringFromBytes, - trimForDisplay, - verifyAccessKey, -} from '../Crypto'; +import * as Bytes from '../Bytes'; +import { trimForDisplay, verifyAccessKey, decryptProfile } from '../Crypto'; import { generateProfileKeyCredentialRequest, getClientZkProfileOperations, @@ -118,7 +114,7 @@ export async function getProfile( }); } - const identityKey = base64ToArrayBuffer(profile.identityKey); + const identityKey = Bytes.fromBase64(profile.identityKey); const changed = await window.textsecure.storage.protocol.saveIdentity( new Address(targetUuid, 1), identityKey, @@ -142,9 +138,9 @@ export async function getProfile( sealedSender: SEALED_SENDER.UNRESTRICTED, }); } else if (accessKey && profile.unidentifiedAccess) { - const haveCorrectKey = await verifyAccessKey( - base64ToArrayBuffer(accessKey), - base64ToArrayBuffer(profile.unidentifiedAccess) + const haveCorrectKey = verifyAccessKey( + Bytes.fromBase64(accessKey), + Bytes.fromBase64(profile.unidentifiedAccess) ); if (haveCorrectKey) { @@ -174,12 +170,12 @@ export async function getProfile( if (profile.about) { const key = c.get('profileKey'); if (key) { - const keyBuffer = base64ToArrayBuffer(key); - const decrypted = await window.textsecure.crypto.decryptProfile( - base64ToArrayBuffer(profile.about), + const keyBuffer = Bytes.fromBase64(key); + const decrypted = decryptProfile( + Bytes.fromBase64(profile.about), keyBuffer ); - c.set('about', stringFromBytes(trimForDisplay(decrypted))); + c.set('about', Bytes.toString(trimForDisplay(decrypted))); } } else { c.unset('about'); @@ -188,12 +184,12 @@ export async function getProfile( if (profile.aboutEmoji) { const key = c.get('profileKey'); if (key) { - const keyBuffer = base64ToArrayBuffer(key); - const decrypted = await window.textsecure.crypto.decryptProfile( - base64ToArrayBuffer(profile.aboutEmoji), + const keyBuffer = Bytes.fromBase64(key); + const decrypted = decryptProfile( + Bytes.fromBase64(profile.aboutEmoji), keyBuffer ); - c.set('aboutEmoji', stringFromBytes(trimForDisplay(decrypted))); + c.set('aboutEmoji', Bytes.toString(trimForDisplay(decrypted))); } } else { c.unset('aboutEmoji'); diff --git a/ts/util/getProvisioningUrl.ts b/ts/util/getProvisioningUrl.ts index 7f677d13f..113ce9431 100644 --- a/ts/util/getProvisioningUrl.ts +++ b/ts/util/getProvisioningUrl.ts @@ -1,14 +1,14 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import { arrayBufferToBase64 } from '../Crypto'; +import * as Bytes from '../Bytes'; export function getProvisioningUrl( uuid: string, - publicKey: ArrayBuffer + publicKey: Uint8Array ): string { const url = new URL('sgnl://linkdevice'); url.searchParams.set('uuid', uuid); - url.searchParams.set('pub_key', arrayBufferToBase64(publicKey)); + url.searchParams.set('pub_key', Bytes.toBase64(publicKey)); return url.toString(); } diff --git a/ts/util/getSendOptions.ts b/ts/util/getSendOptions.ts index 19e556042..68f688020 100644 --- a/ts/util/getSendOptions.ts +++ b/ts/util/getSendOptions.ts @@ -3,7 +3,8 @@ import { ConversationAttributesType } from '../model-types.d'; import { SendMetadataType, SendOptionsType } from '../textsecure/SendMessage'; -import { arrayBufferToBase64, getRandomBytes } from '../Crypto'; +import * as Bytes from '../Bytes'; +import { getRandomBytes } from '../Crypto'; import { getConversationMembers } from './getConversationMembers'; import { isDirectConversation, isMe } from './whatTypeOfConversation'; import { isInSystemContacts } from './isInSystemContacts'; @@ -68,7 +69,7 @@ export async function getSendOptions( // If we've never fetched user's profile, we default to what we have if (sealedSender === SEALED_SENDER.UNKNOWN) { const identifierData = { - accessKey: accessKey || arrayBufferToBase64(getRandomBytes(16)), + accessKey: accessKey || Bytes.toBase64(getRandomBytes(16)), senderCertificate, }; return { @@ -89,7 +90,7 @@ export async function getSendOptions( accessKey: accessKey && sealedSender === SEALED_SENDER.ENABLED ? accessKey - : arrayBufferToBase64(getRandomBytes(16)), + : Bytes.toBase64(getRandomBytes(16)), senderCertificate, }; diff --git a/ts/util/handleImageAttachment.ts b/ts/util/handleImageAttachment.ts index 655eb09c4..df6844388 100644 --- a/ts/util/handleImageAttachment.ts +++ b/ts/util/handleImageAttachment.ts @@ -1,10 +1,11 @@ -// Copyright 2021 Signal Messenger, LLC +// Copyright 2020-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import path from 'path'; import { ipcRenderer } from 'electron'; import { v4 as genUuid } from 'uuid'; +import { blobToArrayBuffer } from '../types/VisualAttachment'; import { IMAGE_JPEG, MIMEType, isHeic, stringToMIMEType } from '../types/MIME'; import { InMemoryAttachmentDraftType, @@ -20,7 +21,7 @@ export async function handleImageAttachment( if (isHeic(file.type)) { const uuid = genUuid(); - const arrayBuffer = await file.arrayBuffer(); + const bytes = new Uint8Array(await file.arrayBuffer()); const convertedFile = await new Promise((resolve, reject) => { ipcRenderer.once(`convert-image:${uuid}`, (_, { error, response }) => { @@ -30,7 +31,7 @@ export async function handleImageAttachment( reject(error); } }); - ipcRenderer.send('convert-image', uuid, arrayBuffer); + ipcRenderer.send('convert-image', uuid, bytes); }); processedFile = new Blob([convertedFile]); @@ -42,15 +43,13 @@ export async function handleImageAttachment( file: processedFile, }); - const data = await window.Signal.Types.VisualAttachment.blobToArrayBuffer( - resizedBlob - ); + const data = await blobToArrayBuffer(resizedBlob); const blurHash = await imageToBlurHash(resizedBlob); return { blurHash, contentType, - data, + data: new Uint8Array(data), fileName: fileName || file.name, path: file.name, pending: false, diff --git a/ts/util/imagePathToArrayBuffer.ts b/ts/util/imagePathToBytes.ts similarity index 68% rename from ts/util/imagePathToArrayBuffer.ts rename to ts/util/imagePathToBytes.ts index 757aaec4a..817c9bc62 100644 --- a/ts/util/imagePathToArrayBuffer.ts +++ b/ts/util/imagePathToBytes.ts @@ -1,11 +1,9 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import { canvasToArrayBuffer } from './canvasToArrayBuffer'; +import { canvasToBytes } from './canvasToBytes'; -export async function imagePathToArrayBuffer( - src: string -): Promise { +export async function imagePathToBytes(src: string): Promise { const image = new Image(); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); @@ -23,6 +21,5 @@ export async function imagePathToArrayBuffer( context.drawImage(image, 0, 0); - const result = await canvasToArrayBuffer(canvas); - return result; + return canvasToBytes(canvas); } diff --git a/ts/util/index.ts b/ts/util/index.ts index f2b23dcc0..213404268 100644 --- a/ts/util/index.ts +++ b/ts/util/index.ts @@ -34,7 +34,7 @@ import { toWebSafeBase64, fromWebSafeBase64 } from './webSafeBase64'; import { mapToSupportLocale } from './mapToSupportLocale'; import { sessionRecordToProtobuf, - sessionStructureToArrayBuffer, + sessionStructureToBytes, } from './sessionTranslation'; import * as zkgroup from './zkgroup'; import { StartupQueue } from './StartupQueue'; @@ -78,7 +78,7 @@ export { sendToGroup, setBatchingStrategy, sessionRecordToProtobuf, - sessionStructureToArrayBuffer, + sessionStructureToBytes, sleep, toWebSafeBase64, zkgroup, diff --git a/ts/util/isAttachmentSizeOkay.ts b/ts/util/isAttachmentSizeOkay.ts index df4c8caf4..a8bd1a1da 100644 --- a/ts/util/isAttachmentSizeOkay.ts +++ b/ts/util/isAttachmentSizeOkay.ts @@ -1,16 +1,14 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import { AttachmentType } from '../types/Attachment'; +import { AttachmentType, getUploadSizeLimitKb } from '../types/Attachment'; import { showToast } from './showToast'; import { ToastFileSize } from '../components/ToastFileSize'; export function isAttachmentSizeOkay( attachment: Readonly ): boolean { - const limitKb = window.Signal.Types.Attachment.getUploadSizeLimitKb( - attachment.contentType - ); + const limitKb = getUploadSizeLimitKb(attachment.contentType); // this needs to be cast properly // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index c44242bf8..337c96677 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -13057,14 +13057,14 @@ }, { "rule": "jQuery-load(", - "path": "ts/util/avatarDataToArrayBuffer.js", + "path": "ts/util/avatarDataToBytes.js", "line": " await font.load();", "reasonCategory": "usageTrusted", "updated": "2021-08-03T21:17:38.615Z" }, { "rule": "jQuery-load(", - "path": "ts/util/avatarDataToArrayBuffer.ts", + "path": "ts/util/avatarDataToBytes.ts", "line": " await font.load();", "reasonCategory": "usageTrusted", "updated": "2021-08-03T21:17:38.615Z" diff --git a/ts/util/processImageFile.ts b/ts/util/processImageFile.ts index b6ad7737e..fb5528aa1 100644 --- a/ts/util/processImageFile.ts +++ b/ts/util/processImageFile.ts @@ -2,9 +2,9 @@ // SPDX-License-Identifier: AGPL-3.0-only import loadImage, { LoadImageOptions } from 'blueimp-load-image'; -import { canvasToArrayBuffer } from './canvasToArrayBuffer'; +import { canvasToBytes } from './canvasToBytes'; -export async function processImageFile(file: File): Promise { +export async function processImageFile(file: File): Promise { const { image } = await loadImage(file, { canvas: true, cover: true, @@ -26,5 +26,5 @@ export async function processImageFile(file: File): Promise { throw new Error('Loaded image was not a canvas'); } - return canvasToArrayBuffer(image); + return canvasToBytes(image); } diff --git a/ts/util/safetyNumber.ts b/ts/util/safetyNumber.ts index 1f4a0a03b..ab0f79b80 100644 --- a/ts/util/safetyNumber.ts +++ b/ts/util/safetyNumber.ts @@ -10,9 +10,9 @@ import * as log from '../logging/log'; export async function generateSecurityNumber( ourNumber: string, - ourKey: ArrayBuffer, + ourKey: Uint8Array, theirNumber: string, - theirKey: ArrayBuffer + theirKey: Uint8Array ): Promise { const ourNumberBuf = Buffer.from(ourNumber); const ourKeyObj = PublicKey.deserialize(Buffer.from(ourKey)); diff --git a/ts/util/sendToGroup.ts b/ts/util/sendToGroup.ts index 6a5928f44..156b71f23 100644 --- a/ts/util/sendToGroup.ts +++ b/ts/util/sendToGroup.ts @@ -12,7 +12,6 @@ import { SenderCertificate, UnidentifiedSenderMessageContent, } from '@signalapp/signal-client'; -import { typedArrayToArrayBuffer as toArrayBuffer } from '../Crypto'; import * as Bytes from '../Bytes'; import { senderCertificateService } from '../services/senderCertificate'; import { @@ -69,9 +68,6 @@ const MAX_RECURSION = 10; const ACCESS_KEY_LENGTH = 16; const ZERO_ACCESS_KEY = Bytes.toBase64(new Uint8Array(ACCESS_KEY_LENGTH)); -// TODO: remove once we move away from ArrayBuffers -const FIXMEU8 = Uint8Array; - // Public API: export async function sendToGroup({ @@ -453,16 +449,14 @@ export async function sendToGroupViaSenderKey(options: { contentHint, devices: devicesForSenderKey, distributionId, - contentMessage: toArrayBuffer( - Proto.Content.encode(contentMessage).finish() - ), + contentMessage: Proto.Content.encode(contentMessage).finish(), groupId, }); const accessKeys = getXorOfAccessKeys(devicesForSenderKey); const result = await window.textsecure.messaging.sendWithSenderKey( - toArrayBuffer(messageBuffer), - toArrayBuffer(accessKeys), + messageBuffer, + accessKeys, timestamp, online ); @@ -554,9 +548,7 @@ export async function sendToGroupViaSenderKey(options: { if (normalSendRecipients.length === 0) { return { dataMessage: contentMessage.dataMessage - ? toArrayBuffer( - Proto.DataMessage.encode(contentMessage.dataMessage).finish() - ) + ? Proto.DataMessage.encode(contentMessage.dataMessage).finish() : undefined, successfulIdentifiers: senderKeyRecipients, unidentifiedDeliveries: senderKeyRecipients, @@ -617,9 +609,7 @@ export async function sendToGroupViaSenderKey(options: { return { dataMessage: contentMessage.dataMessage - ? toArrayBuffer( - Proto.DataMessage.encode(contentMessage.dataMessage).finish() - ) + ? Proto.DataMessage.encode(contentMessage.dataMessage).finish() : undefined, errors: normalSendResult.errors, failoverIdentifiers: normalSendResult.failoverIdentifiers, @@ -838,7 +828,7 @@ async function encryptForSenderKey({ groupId, }: { contentHint: number; - contentMessage: ArrayBuffer; + contentMessage: Uint8Array; devices: Array; distributionId: string; groupId: string; @@ -857,7 +847,7 @@ async function encryptForSenderKey({ ); const ourAddress = getOurAddress(); const senderKeyStore = new SenderKeys({ ourUuid }); - const message = Buffer.from(padMessage(new FIXMEU8(contentMessage))); + const message = Buffer.from(padMessage(contentMessage)); const ciphertextMessage = await window.textsecure.storage.protocol.enqueueSenderKeyJob( new QualifiedAddress(ourUuid, ourAddress), diff --git a/ts/util/sessionTranslation.ts b/ts/util/sessionTranslation.ts index bc655b44e..e577ca72e 100644 --- a/ts/util/sessionTranslation.ts +++ b/ts/util/sessionTranslation.ts @@ -4,12 +4,8 @@ import { get, isFinite, isInteger, isString } from 'lodash'; import { signal } from '../protobuf/compiled'; -import { - bytesFromString, - deriveSecrets, - fromEncodedBinaryToArrayBuffer, - typedArrayToArrayBuffer, -} from '../Crypto'; +import * as Bytes from '../Bytes'; +import { deriveSecrets } from '../Crypto'; const { RecordStructure, SessionStructure } = signal.proto.storage; const { Chain } = SessionStructure; @@ -75,16 +71,14 @@ type SessionRecordType = { }; export type LocalUserDataType = { - identityKeyPublic: ArrayBuffer; + identityKeyPublic: Uint8Array; registrationId: number; }; -export function sessionStructureToArrayBuffer( +export function sessionStructureToBytes( recordStructure: signal.proto.storage.RecordStructure -): ArrayBuffer { - return typedArrayToArrayBuffer( - signal.proto.storage.RecordStructure.encode(recordStructure).finish() - ); +): Uint8Array { + return signal.proto.storage.RecordStructure.encode(recordStructure).finish(); } export function sessionRecordToProtobuf( @@ -139,7 +133,7 @@ function toProtobufSession( // Core Fields proto.aliceBaseKey = binaryToUint8Array(session, 'indexInfo.baseKey', 33); - proto.localIdentityPublic = new Uint8Array(ourData.identityKeyPublic); + proto.localIdentityPublic = ourData.identityKeyPublic; proto.localRegistrationId = ourData.registrationId; proto.previousCounter = @@ -306,9 +300,9 @@ function toProtobufChain( const { cipherKey, macKey, iv } = translateMessageKey(key); - protoMessageKey.cipherKey = new Uint8Array(cipherKey); - protoMessageKey.macKey = new Uint8Array(macKey); - protoMessageKey.iv = new Uint8Array(iv); + protoMessageKey.cipherKey = cipherKey; + protoMessageKey.macKey = macKey; + protoMessageKey.iv = iv; return protoMessageKey; }); @@ -321,9 +315,9 @@ function toProtobufChain( const WHISPER_MESSAGE_KEYS = 'WhisperMessageKeys'; function translateMessageKey(key: Uint8Array) { - const input = key.buffer; - const salt = new ArrayBuffer(32); - const info = bytesFromString(WHISPER_MESSAGE_KEYS); + const input = key; + const salt = new Uint8Array(32); + const info = Bytes.fromString(WHISPER_MESSAGE_KEYS); const [cipherKey, macKey, ivContainer] = deriveSecrets(input, salt, info); @@ -349,14 +343,14 @@ function binaryToUint8Array( throw new Error(`binaryToUint8Array: String not found at path ${path}`); } - const buffer = fromEncodedBinaryToArrayBuffer(target); + const buffer = Bytes.fromBinary(target); if (length && buffer.byteLength !== length) { throw new Error( `binaryToUint8Array: Got unexpected length ${buffer.byteLength} instead of ${length} at path ${path}` ); } - return new Uint8Array(buffer); + return buffer; } // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/ts/util/sniffImageMimeType.ts b/ts/util/sniffImageMimeType.ts index de5357a57..20e15c059 100644 --- a/ts/util/sniffImageMimeType.ts +++ b/ts/util/sniffImageMimeType.ts @@ -16,13 +16,10 @@ import { * * [0]: https://mimesniff.spec.whatwg.org/#matching-an-image-type-pattern */ -export function sniffImageMimeType( - bytes: ArrayBuffer | Uint8Array -): undefined | MIMEType { - const asTypedArray = new Uint8Array(bytes); +export function sniffImageMimeType(bytes: Uint8Array): undefined | MIMEType { for (let i = 0; i < TYPES.length; i += 1) { const type = TYPES[i]; - if (matchesType(asTypedArray, type)) { + if (matchesType(bytes, type)) { return type.mimeType; } } diff --git a/ts/util/stringToArrayBuffer.ts b/ts/util/stringToArrayBuffer.ts deleted file mode 100644 index 74c799ddc..000000000 --- a/ts/util/stringToArrayBuffer.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2018-2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -export function stringToArrayBuffer(string: string): ArrayBuffer { - if (typeof string !== 'string') { - throw new TypeError("'string' must be a string"); - } - - const array = new Uint8Array(string.length); - for (let i = 0; i < string.length; i += 1) { - array[i] = string.charCodeAt(i); - } - return array.buffer; -} diff --git a/ts/util/synchronousCrypto.ts b/ts/util/synchronousCrypto.ts deleted file mode 100644 index be17bb5b1..000000000 --- a/ts/util/synchronousCrypto.ts +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import crypto from 'crypto'; - -import { typedArrayToArrayBuffer as toArrayBuffer } from '../Crypto'; - -export function sign(key: ArrayBuffer, data: ArrayBuffer): ArrayBuffer { - return toArrayBuffer( - crypto - .createHmac('sha256', Buffer.from(key)) - .update(Buffer.from(data)) - .digest() - ); -} - -export enum HashType { - size256 = 'sha256', - size512 = 'sha512', -} - -export function hash(type: HashType, data: ArrayBuffer): ArrayBuffer { - return toArrayBuffer( - crypto.createHash(type).update(Buffer.from(data)).digest() - ); -} - -export enum CipherType { - AES256CBC = 'aes-256-cbc', - AES256CTR = 'aes-256-ctr', -} - -export function encrypt( - key: ArrayBuffer, - data: ArrayBuffer, - iv: ArrayBuffer, - cipherType: CipherType = CipherType.AES256CBC -): ArrayBuffer { - const cipher = crypto.createCipheriv( - cipherType, - Buffer.from(key), - Buffer.from(iv) - ); - const encrypted = Buffer.concat([ - cipher.update(Buffer.from(data)), - cipher.final(), - ]); - - return toArrayBuffer(encrypted); -} - -export function decrypt( - key: ArrayBuffer, - data: ArrayBuffer, - iv: ArrayBuffer, - cipherType: CipherType = CipherType.AES256CBC -): ArrayBuffer { - const cipher = crypto.createDecipheriv( - cipherType, - Buffer.from(key), - Buffer.from(iv) - ); - const decrypted = Buffer.concat([ - cipher.update(Buffer.from(data)), - cipher.final(), - ]); - - return toArrayBuffer(decrypted); -} diff --git a/ts/util/whatTypeOfConversation.ts b/ts/util/whatTypeOfConversation.ts index 06a60ddf4..a0c9c531b 100644 --- a/ts/util/whatTypeOfConversation.ts +++ b/ts/util/whatTypeOfConversation.ts @@ -3,7 +3,7 @@ import { ConversationAttributesType } from '../model-types.d'; import { ConversationType } from '../state/ducks/conversations'; -import { base64ToArrayBuffer, fromEncodedBinaryToArrayBuffer } from '../Crypto'; +import * as Bytes from '../Bytes'; import * as log from '../logging/log'; export enum ConversationTypes { @@ -38,7 +38,7 @@ export function isGroupV1( return false; } - const buffer = fromEncodedBinaryToArrayBuffer(groupId); + const buffer = Bytes.fromBinary(groupId); return buffer.byteLength === window.Signal.Groups.ID_V1_LENGTH; } @@ -56,7 +56,7 @@ export function isGroupV2( try { return ( groupVersion === 2 && - base64ToArrayBuffer(groupId).byteLength === window.Signal.Groups.ID_LENGTH + Bytes.fromBase64(groupId).byteLength === window.Signal.Groups.ID_LENGTH ); } catch (error) { log.error('isGroupV2: Failed to process groupId in base64!'); diff --git a/ts/views/conversation_view.ts b/ts/views/conversation_view.ts index 2eb44ccef..1efa127b2 100644 --- a/ts/views/conversation_view.ts +++ b/ts/views/conversation_view.ts @@ -13,12 +13,14 @@ import { OnDiskAttachmentDraftType, isGIF, } from '../types/Attachment'; +import * as Attachment from '../types/Attachment'; import type { StickerPackType as StickerPackDBType } from '../sql/Interface'; import * as Stickers from '../types/Stickers'; import { BodyRangeType, BodyRangesType } from '../types/Util'; import { IMAGE_JPEG, IMAGE_WEBP, + IMAGE_PNG, isHeic, MIMEType, stringToMIMEType, @@ -79,6 +81,8 @@ import { markViewed } from '../services/MessageUpdater'; import { viewedReceiptsJobQueue } from '../jobs/viewedReceiptsJobQueue'; import { viewSyncJobQueue } from '../jobs/viewSyncJobQueue'; import type { EmbeddedContactType } from '../types/EmbeddedContact'; +import * as VisualAttachment from '../types/VisualAttachment'; +import * as MIME from '../types/MIME'; import type { AnyViewClass, BasicReactWrapperViewClass } from '../window.d'; import { isNotNil } from '../util/isNotNil'; import { dropNull } from '../util/dropNull'; @@ -126,7 +130,7 @@ const LINK_PREVIEW_TIMEOUT = 60 * 1000; window.Whisper = window.Whisper || {}; const { Whisper } = window; -const { Message, MIME, VisualAttachment } = window.Signal.Types; +const { Message } = window.Signal.Types; const { copyIntoTempDirectory, @@ -1707,15 +1711,15 @@ export class ConversationView extends window.Backbone.View { } // eslint-disable-next-line class-methods-use-this - arrayBufferFromFile(file: Blob): Promise { + bytesFromFile(file: Blob): Promise { return new Promise((resolve, rejectPromise) => { const FR = new FileReader(); FR.onload = () => { if (!FR.result || typeof FR.result === 'string') { - rejectPromise(new Error('arrayBufferFromFile: No result!')); + rejectPromise(new Error('bytesFromFile: No result!')); return; } - resolve(FR.result); + resolve(new Uint8Array(FR.result)); }; FR.onerror = rejectPromise; FR.onabort = rejectPromise; @@ -1843,7 +1847,7 @@ export class ConversationView extends window.Backbone.View { ) { attachment = await this.handleVideoAttachment(file); } else { - const data = await this.arrayBufferFromFile(file); + const data = await this.bytesFromFile(file); attachment = { contentType: fileType, data, @@ -1858,7 +1862,7 @@ export class ConversationView extends window.Backbone.View { `Was unable to generate thumbnail for fileType ${fileType}`, e && e.stack ? e.stack : e ); - const data = await this.arrayBufferFromFile(file); + const data = await this.bytesFromFile(file); attachment = { contentType: fileType, data, @@ -1904,7 +1908,7 @@ export class ConversationView extends window.Backbone.View { throw new Error('Failed to create object url for video!'); } try { - const screenshotContentType = 'image/png'; + const screenshotContentType = IMAGE_PNG; const screenshotBlob = await VisualAttachment.makeVideoScreenshot({ objectUrl, contentType: screenshotContentType, @@ -1913,7 +1917,7 @@ export class ConversationView extends window.Backbone.View { const screenshotData = await VisualAttachment.blobToArrayBuffer( screenshotBlob ); - const data = await this.arrayBufferFromFile(file); + const data = await this.bytesFromFile(file); return { contentType: stringToMIMEType(file.type), @@ -1922,7 +1926,7 @@ export class ConversationView extends window.Backbone.View { path: file.name, pending: false, screenshotContentType, - screenshotData, + screenshotData: new Uint8Array(screenshotData), screenshotSize: screenshotData.byteLength, size: data.byteLength, }; @@ -2015,7 +2019,7 @@ export class ConversationView extends window.Backbone.View { throw new Error('A voice note cannot be sent with other attachments'); } - const data = await this.arrayBufferFromFile(blob); + const data = await this.bytesFromFile(blob); // These aren't persisted to disk; they are meant to be sent immediately this.voiceNoteAttachment = { @@ -2431,7 +2435,7 @@ export class ConversationView extends window.Backbone.View { message: Pick; }) => { const timestamp = message.sent_at; - const fullPath = await window.Signal.Types.Attachment.save({ + const fullPath = await Attachment.save({ attachment, readAttachmentData, saveAttachmentToDisk, @@ -2627,7 +2631,7 @@ export class ConversationView extends window.Backbone.View { return; } - const fullPath = await window.Signal.Types.Attachment.save({ + const fullPath = await Attachment.save({ attachment, readAttachmentData, saveAttachmentToDisk, @@ -2836,7 +2840,7 @@ export class ConversationView extends window.Backbone.View { message: MediaItemMessageType; index: number; }) => { - const fullPath = await window.Signal.Types.Attachment.save({ + const fullPath = await Attachment.save({ attachment, index: index + 1, readAttachmentData, @@ -3900,8 +3904,8 @@ export class ConversationView extends window.Backbone.View { const { id, key } = dataFromLink; try { - const keyBytes = window.Signal.Crypto.bytesFromHexString(key); - const keyBase64 = window.Signal.Crypto.arrayBufferToBase64(keyBytes); + const keyBytes = Bytes.fromHex(key); + const keyBase64 = Bytes.toBase64(keyBytes); const existing = Stickers.getStickerPack(id); if (!isPackDownloaded(existing)) { @@ -4093,7 +4097,7 @@ export class ConversationView extends window.Backbone.View { fileName: title, }); - const data = await this.arrayBufferFromFile(withBlob.file); + const data = await this.bytesFromFile(withBlob.file); objectUrl = URL.createObjectURL(withBlob.file); const blurHash = await window.imageToBlurHash(withBlob.file); @@ -4107,7 +4111,7 @@ export class ConversationView extends window.Backbone.View { data, size: data.byteLength, ...dimensions, - contentType: withBlob.file.type, + contentType: stringToMIMEType(withBlob.file.type), blurHash, }; } catch (error) { diff --git a/ts/window.d.ts b/ts/window.d.ts index 1518c3bb5..1daf9f530 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -33,8 +33,6 @@ import * as OS from './OS'; import { getEnvironment } from './environment'; import * as zkgroup from './util/zkgroup'; import { LocalizerType, BodyRangesType, BodyRangeType } from './types/Util'; -import * as Attachment from './types/Attachment'; -import * as MIME from './types/MIME'; import * as EmbeddedContact from './types/EmbeddedContact'; import * as Errors from './types/errors'; import { ConversationController } from './ConversationController'; @@ -103,7 +101,6 @@ import { ElectronLocaleType } from './util/mapToSupportLocale'; import { SignalProtocolStore } from './SignalProtocolStore'; import { Context as SignalContext } from './context'; import { StartupQueue } from './util/StartupQueue'; -import * as synchronousCrypto from './util/synchronousCrypto'; import { SocketStatus } from './types/SocketStatus'; import SyncRequest from './textsecure/SyncRequest'; import { ConversationColorType, CustomColorType } from './types/Colors'; @@ -250,7 +247,6 @@ declare global { storage: Storage; systemTheme: WhatIsThis; textsecure: TextSecureType; - synchronousCrypto: typeof synchronousCrypto; titleBarDoubleClick: () => void; unregisterForActive: (handler: () => void) => void; updateTrayIcon: (count: number) => void; @@ -286,28 +282,28 @@ declare global { storageServiceUploadJob: () => void; }; Migrations: { - readTempData: any; + readTempData: (path: string) => Promise; deleteAttachmentData: (path: string) => Promise; - doesAttachmentExist: () => unknown; - writeNewAttachmentData: (data: ArrayBuffer) => Promise; + doesAttachmentExist: (path: string) => Promise; + writeNewAttachmentData: (data: Uint8Array) => Promise; deleteExternalMessageFiles: (attributes: unknown) => Promise; getAbsoluteAttachmentPath: (path: string) => string; loadAttachmentData: (attachment: WhatIsThis) => WhatIsThis; loadQuoteData: (quote: unknown) => WhatIsThis; loadPreviewData: (preview: unknown) => WhatIsThis; loadStickerData: (sticker: unknown) => WhatIsThis; - readStickerData: (path: string) => Promise; + readStickerData: (path: string) => Promise; deleteSticker: (path: string) => Promise; getAbsoluteStickerPath: (path: string) => string; processNewEphemeralSticker: ( - stickerData: ArrayBuffer + stickerData: Uint8Array ) => { path: string; width: number; height: number; }; processNewSticker: ( - stickerData: ArrayBuffer + stickerData: Uint8Array ) => { path: string; width: number; @@ -325,36 +321,18 @@ declare global { getAbsoluteDraftPath: any; getAbsoluteTempPath: any; openFileInFolder: any; - readAttachmentData: any; - readDraftData: any; - saveAttachmentToDisk: any; - writeNewDraftData: any; + readAttachmentData: (path: string) => Promise; + readDraftData: (path: string) => Promise; + saveAttachmentToDisk: (options: { + data: Uint8Array; + name: string; + }) => Promise; + writeNewDraftData: (data: Uint8Array) => Promise; deleteAvatar: (path: string) => Promise; getAbsoluteAvatarPath: (src: string) => string; - writeNewAvatarData: (data: ArrayBuffer) => Promise; + writeNewAvatarData: (data: Uint8Array) => Promise; }; Types: { - Attachment: typeof Attachment; - MIME: typeof MIME; - EmbeddedContact: typeof EmbeddedContact; - Conversation: { - computeHash: (data: string) => Promise; - deleteExternalFiles: ( - attributes: unknown, - options: unknown - ) => Promise; - maybeUpdateProfileAvatar: ( - attributes: unknown, - decrypted: unknown, - options: unknown - ) => Promise>; - maybeUpdateAvatar: ( - attributes: unknown, - data: unknown, - options: unknown - ) => Promise; - }; - Errors: typeof Errors; Message: { CURRENT_SCHEMA_VERSION: number; VERSION_NEEDED_FOR_DISPLAY: number; @@ -382,7 +360,6 @@ declare global { height: number; path: string; }; - VisualAttachment: any; UUID: typeof UUID; Address: typeof Address; QualifiedAddress: typeof QualifiedAddress; diff --git a/ts/workers/heicConverterMain.ts b/ts/workers/heicConverterMain.ts index 46bc8cda4..d900e681b 100644 --- a/ts/workers/heicConverterMain.ts +++ b/ts/workers/heicConverterMain.ts @@ -6,7 +6,7 @@ import { Worker } from 'worker_threads'; export type WrappedWorkerRequest = { readonly uuid: string; - readonly data: ArrayBuffer; + readonly data: Uint8Array; }; export type WrappedWorkerResponse = { @@ -19,7 +19,7 @@ const ASAR_PATTERN = /app\.asar$/; export function getHeicConverter(): ( uuid: string, - data: ArrayBuffer + data: Uint8Array ) => Promise { let appDir = join(__dirname, '..', '..'); let isBundled = false;