Uint8Array migration

This commit is contained in:
Fedor Indutny 2021-09-23 17:49:05 -07:00 committed by GitHub
parent daf75190b8
commit 4ef0bf96cc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
137 changed files with 2202 additions and 3170 deletions

View file

@ -2,19 +2,17 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { randomBytes } from 'crypto'; 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 { app, dialog, shell, remote } from 'electron';
import fastGlob from 'fast-glob'; import fastGlob from 'fast-glob';
import glob from 'glob'; import glob from 'glob';
import pify from 'pify'; import pify from 'pify';
import fse from 'fs-extra'; import fse from 'fs-extra';
import { map, isArrayBuffer, isString } from 'lodash'; import { map, isTypedArray, isString } from 'lodash';
import normalizePath from 'normalize-path'; import normalizePath from 'normalize-path';
import sanitizeFilename from 'sanitize-filename';
import getGuid from 'uuid/v4'; import getGuid from 'uuid/v4';
import { typedArrayToArrayBuffer } from '../ts/Crypto';
import { isPathInside } from '../ts/util/isPathInside'; import { isPathInside } from '../ts/util/isPathInside';
import { isWindows } from '../ts/OS'; import { isWindows } from '../ts/OS';
import { writeWindowsZoneIdentifier } from '../ts/util/windowsZoneIdentifier'; import { writeWindowsZoneIdentifier } from '../ts/util/windowsZoneIdentifier';
@ -123,12 +121,12 @@ export const clearTempPath = (userDataPath: string): Promise<void> => {
export const createReader = ( export const createReader = (
root: string root: string
): ((relativePath: string) => Promise<ArrayBuffer>) => { ): ((relativePath: string) => Promise<Uint8Array>) => {
if (!isString(root)) { if (!isString(root)) {
throw new TypeError("'root' must be a path"); throw new TypeError("'root' must be a path");
} }
return async (relativePath: string): Promise<ArrayBuffer> => { return async (relativePath: string): Promise<Uint8Array> => {
if (!isString(relativePath)) { if (!isString(relativePath)) {
throw new TypeError("'relativePath' must be a string"); throw new TypeError("'relativePath' must be a string");
} }
@ -138,8 +136,7 @@ export const createReader = (
if (!isPathInside(normalized, root)) { if (!isPathInside(normalized, root)) {
throw new Error('Invalid relative path'); throw new Error('Invalid relative path');
} }
const buffer = await fse.readFile(normalized); return fse.readFile(normalized);
return typedArrayToArrayBuffer(buffer);
}; };
}; };
@ -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( async function writeWithAttributes(
target: string, target: string,
data: ArrayBuffer data: Uint8Array
): Promise<void> { ): Promise<void> {
await fse.writeFile(target, Buffer.from(data)); await fse.writeFile(target, Buffer.from(data));
@ -292,7 +250,7 @@ export const saveAttachmentToDisk = async ({
data, data,
name, name,
}: { }: {
data: ArrayBuffer; data: Uint8Array;
name: string; name: string;
}): Promise<null | { fullPath: string; name: string }> => { }): Promise<null | { fullPath: string; name: string }> => {
const dialogToUse = dialog || remote.dialog; const dialogToUse = dialog || remote.dialog;
@ -327,20 +285,20 @@ export const openFileInFolder = async (target: string): Promise<void> => {
export const createWriterForNew = ( export const createWriterForNew = (
root: string root: string
): ((arrayBuffer: ArrayBuffer) => Promise<string>) => { ): ((bytes: Uint8Array) => Promise<string>) => {
if (!isString(root)) { if (!isString(root)) {
throw new TypeError("'root' must be a path"); throw new TypeError("'root' must be a path");
} }
return async (arrayBuffer: ArrayBuffer) => { return async (bytes: Uint8Array) => {
if (!isArrayBuffer(arrayBuffer)) { if (!isTypedArray(bytes)) {
throw new TypeError("'arrayBuffer' must be an array buffer"); throw new TypeError("'bytes' must be a typed array");
} }
const name = createName(); const name = createName();
const relativePath = getRelativePath(name); const relativePath = getRelativePath(name);
return createWriterForExisting(root)({ return createWriterForExisting(root)({
data: arrayBuffer, data: bytes,
path: relativePath, path: relativePath,
}); });
}; };
@ -348,27 +306,27 @@ export const createWriterForNew = (
export const createWriterForExisting = ( export const createWriterForExisting = (
root: string root: string
): ((options: { data: ArrayBuffer; path: string }) => Promise<string>) => { ): ((options: { data: Uint8Array; path: string }) => Promise<string>) => {
if (!isString(root)) { if (!isString(root)) {
throw new TypeError("'root' must be a path"); throw new TypeError("'root' must be a path");
} }
return async ({ return async ({
data: arrayBuffer, data: bytes,
path: relativePath, path: relativePath,
}: { }: {
data: ArrayBuffer; data: Uint8Array;
path: string; path: string;
}): Promise<string> => { }): Promise<string> => {
if (!isString(relativePath)) { if (!isString(relativePath)) {
throw new TypeError("'relativePath' must be a path"); throw new TypeError("'relativePath' must be a path");
} }
if (!isArrayBuffer(arrayBuffer)) { if (!isTypedArray(bytes)) {
throw new TypeError("'arrayBuffer' must be an array buffer"); 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 absolutePath = join(root, relativePath);
const normalized = normalize(absolutePath); const normalized = normalize(absolutePath);
if (!isPathInside(normalized, root)) { if (!isPathInside(normalized, root)) {

View file

@ -130,13 +130,8 @@ const searchSelectors = require('../../ts/state/selectors/search');
// Types // Types
const AttachmentType = require('../../ts/types/Attachment'); const AttachmentType = require('../../ts/types/Attachment');
const VisualAttachment = require('./types/visual_attachment'); const VisualAttachment = require('../../ts/types/VisualAttachment');
const EmbeddedContact = require('../../ts/types/EmbeddedContact');
const Conversation = require('./types/conversation');
const Errors = require('../../ts/types/errors');
const MessageType = require('./types/message'); const MessageType = require('./types/message');
const MIME = require('../../ts/types/MIME');
const SettingsType = require('../../ts/types/Settings');
const { UUID } = require('../../ts/types/UUID'); const { UUID } = require('../../ts/types/UUID');
const { Address } = require('../../ts/types/Address'); const { Address } = require('../../ts/types/Address');
const { QualifiedAddress } = require('../../ts/types/QualifiedAddress'); const { QualifiedAddress } = require('../../ts/types/QualifiedAddress');
@ -417,14 +412,9 @@ exports.setup = (options = {}) => {
}; };
const Types = { const Types = {
Attachment: AttachmentType,
EmbeddedContact,
Conversation,
Errors,
Message: MessageType, Message: MessageType,
MIME,
Settings: SettingsType, // Mostly for debugging
VisualAttachment,
UUID, UUID,
Address, Address,
QualifiedAddress, QualifiedAddress,

View file

@ -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,
};

View file

@ -6,7 +6,7 @@ const { isFunction, isObject, isString, omit } = require('lodash');
const Contact = require('../../../ts/types/EmbeddedContact'); const Contact = require('../../../ts/types/EmbeddedContact');
const Attachment = require('../../../ts/types/Attachment'); const Attachment = require('../../../ts/types/Attachment');
const Errors = require('../../../ts/types/errors'); const Errors = require('../../../ts/types/errors');
const SchemaVersion = require('./schema_version'); const SchemaVersion = require('../../../ts/types/SchemaVersion');
const { const {
initializeAttachmentMetadata, initializeAttachmentMetadata,
} = require('../../../ts/types/message/initializeAttachmentMetadata'); } = require('../../../ts/types/message/initializeAttachmentMetadata');

View file

@ -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;

View file

@ -16,9 +16,6 @@ module.exports = {
}, },
globals: { globals: {
assert: true, assert: true,
assertEqualArrayBuffers: true,
getString: true, getString: true,
hexToArrayBuffer: true,
stringToArrayBuffer: true,
}, },
}; };

View file

@ -46,18 +46,6 @@ mocha.reporter(SauceReporter);
/* /*
* global helpers for tests * 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 = window.Whisper || {};
window.Whisper.events = { window.Whisper.events = {

View file

@ -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);
});
});
});
});

View file

@ -31,7 +31,6 @@
data-cover data-cover
></script> ></script>
<script type="text/javascript" src="helpers_test.js"></script>
<script type="text/javascript" src="task_with_timeout_test.js"></script> <script type="text/javascript" src="task_with_timeout_test.js"></script>
<script type="text/javascript" src="account_manager_test.js"></script> <script type="text/javascript" src="account_manager_test.js"></script>
<script type="text/javascript" src="sendmessage_test.js"></script> <script type="text/javascript" src="sendmessage_test.js"></script>

View file

@ -367,7 +367,6 @@ try {
window.Backbone = require('backbone'); window.Backbone = require('backbone');
window.textsecure = require('./ts/textsecure').default; window.textsecure = require('./ts/textsecure').default;
window.synchronousCrypto = require('./ts/util/synchronousCrypto');
window.WebAPI = window.textsecure.WebAPI.initialize({ window.WebAPI = window.textsecure.WebAPI.initialize({
url: config.serverUrl, url: config.serverUrl,

View file

@ -9,7 +9,6 @@ const { readFile } = require('fs');
const config = require('url').parse(window.location.toString(), true).query; const config = require('url').parse(window.location.toString(), true).query;
const { noop, uniqBy } = require('lodash'); const { noop, uniqBy } = require('lodash');
const pMap = require('p-map'); const pMap = require('p-map');
const client = require('@signalapp/signal-client');
// It is important to call this as early as possible // It is important to call this as early as possible
require('../ts/windows/context'); require('../ts/windows/context');
@ -55,20 +54,6 @@ const Signal = require('../js/modules/signal');
window.Signal = Signal.setup({}); window.Signal = Signal.setup({});
window.textsecure = require('../ts/textsecure').default; 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 { initialize: initializeWebAPI } = require('../ts/textsecure/WebAPI');
const { const {
getAnimatedPngDataIfExists, getAnimatedPngDataIfExists,
@ -206,7 +191,7 @@ window.encryptAndUpload = async (
const { value: password } = passwordItem; const { value: password } = passwordItem;
const packKey = window.Signal.Crypto.getRandomBytes(32); const packKey = window.Signal.Crypto.getRandomBytes(32);
const encryptionKey = await deriveStickerPackKey(packKey); const encryptionKey = deriveStickerPackKey(packKey);
const iv = window.Signal.Crypto.getRandomBytes(16); const iv = window.Signal.Crypto.getRandomBytes(16);
const server = WebAPI.connect({ const server = WebAPI.connect({
@ -265,9 +250,7 @@ window.encryptAndUpload = async (
async function encrypt(data, key, iv) { async function encrypt(data, key, iv) {
const { ciphertext } = await window.textsecure.crypto.encryptAttachment( const { ciphertext } = await window.textsecure.crypto.encryptAttachment(
data instanceof ArrayBuffer data,
? data
: window.Signal.Crypto.typedArrayToArrayBuffer(data),
key, key,
iv iv
); );

View file

@ -11,10 +11,7 @@ module.exports = {
globals: { globals: {
assert: true, assert: true,
assertEqualArrayBuffers: true,
getString: true, getString: true,
hexToArrayBuffer: true,
stringToArrayBuffer: true,
}, },
parserOptions: { parserOptions: {

View file

@ -51,18 +51,6 @@ Whisper.Database.id = 'test';
/* /*
* global helpers for tests * 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() { function deleteIndexedDB() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

View file

@ -9,7 +9,7 @@ const { assert } = require('chai');
const { app } = require('electron'); const { app } = require('electron');
const Attachments = require('../../app/attachments'); const Attachments = require('../../app/attachments');
const { stringToArrayBuffer } = require('../../ts/util/stringToArrayBuffer'); const Bytes = require('../../ts/Bytes');
const PREFIX_LENGTH = 2; const PREFIX_LENGTH = 2;
const NUM_SEPARATORS = 1; const NUM_SEPARATORS = 1;
@ -28,7 +28,7 @@ describe('Attachments', () => {
}); });
it('should write file to disk and return path', async () => { 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( const tempDirectory = path.join(
tempRootDirectory, tempRootDirectory,
'Attachments_createWriterForNew' 'Attachments_createWriterForNew'
@ -57,7 +57,7 @@ describe('Attachments', () => {
}); });
it('should write file to disk on given path and return path', async () => { 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( const tempDirectory = path.join(
tempRootDirectory, tempRootDirectory,
'Attachments_createWriterForExisting' 'Attachments_createWriterForExisting'
@ -82,7 +82,7 @@ describe('Attachments', () => {
}); });
it('throws if relative path goes higher than root', async () => { 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( const tempDirectory = path.join(
tempRootDirectory, tempRootDirectory,
'Attachments_createWriterForExisting' 'Attachments_createWriterForExisting'
@ -124,7 +124,7 @@ describe('Attachments', () => {
Attachments.createName() Attachments.createName()
); );
const fullPath = path.join(tempDirectory, relativePath); const fullPath = path.join(tempDirectory, relativePath);
const input = stringToArrayBuffer('test string'); const input = Bytes.fromString('test string');
const inputBuffer = Buffer.from(input); const inputBuffer = Buffer.from(input);
await fse.ensureFile(fullPath); await fse.ensureFile(fullPath);
@ -260,7 +260,7 @@ describe('Attachments', () => {
Attachments.createName() Attachments.createName()
); );
const fullPath = path.join(tempDirectory, relativePath); const fullPath = path.join(tempDirectory, relativePath);
const input = stringToArrayBuffer('test string'); const input = Bytes.fromString('test string');
const inputBuffer = Buffer.from(input); const inputBuffer = Buffer.from(input);
await fse.ensureFile(fullPath); await fse.ensureFile(fullPath);

View file

@ -6,7 +6,7 @@ const sinon = require('sinon');
const Message = require('../../../js/modules/types/message'); const Message = require('../../../js/modules/types/message');
const { SignalService } = require('../../../ts/protobuf'); const { SignalService } = require('../../../ts/protobuf');
const { stringToArrayBuffer } = require('../../../ts/util/stringToArrayBuffer'); const Bytes = require('../../../ts/Bytes');
describe('Message', () => { describe('Message', () => {
const logger = { const logger = {
@ -60,7 +60,7 @@ describe('Message', () => {
attachments: [ attachments: [
{ {
path: 'ab/abcdefghi', path: 'ab/abcdefghi',
data: stringToArrayBuffer('Its easy if you try'), data: Bytes.fromString('Its easy if you try'),
}, },
], ],
}; };
@ -78,9 +78,9 @@ describe('Message', () => {
const writeExistingAttachmentData = attachment => { const writeExistingAttachmentData = attachment => {
assert.equal(attachment.path, 'ab/abcdefghi'); assert.equal(attachment.path, 'ab/abcdefghi');
assert.deepEqual( assert.strictEqual(
attachment.data, Bytes.toString(attachment.data),
stringToArrayBuffer('Its easy if you try') 'Its easy if you try'
); );
}; };
@ -101,7 +101,7 @@ describe('Message', () => {
{ {
thumbnail: { thumbnail: {
path: 'ab/abcdefghi', path: 'ab/abcdefghi',
data: stringToArrayBuffer('Its easy if you try'), data: Bytes.fromString('Its easy if you try'),
}, },
}, },
], ],
@ -126,9 +126,9 @@ describe('Message', () => {
const writeExistingAttachmentData = attachment => { const writeExistingAttachmentData = attachment => {
assert.equal(attachment.path, 'ab/abcdefghi'); assert.equal(attachment.path, 'ab/abcdefghi');
assert.deepEqual( assert.strictEqual(
attachment.data, Bytes.toString(attachment.data),
stringToArrayBuffer('Its easy if you try') 'Its easy if you try'
); );
}; };
@ -151,7 +151,7 @@ describe('Message', () => {
isProfile: false, isProfile: false,
avatar: { avatar: {
path: 'ab/abcdefghi', path: 'ab/abcdefghi',
data: stringToArrayBuffer('Its easy if you try'), data: Bytes.fromString('Its easy if you try'),
}, },
}, },
}, },
@ -177,9 +177,9 @@ describe('Message', () => {
const writeExistingAttachmentData = attachment => { const writeExistingAttachmentData = attachment => {
assert.equal(attachment.path, 'ab/abcdefghi'); assert.equal(attachment.path, 'ab/abcdefghi');
assert.deepEqual( assert.strictEqual(
attachment.data, Bytes.toString(attachment.data),
stringToArrayBuffer('Its easy if you try') 'Its easy if you try'
); );
}; };
@ -268,7 +268,7 @@ describe('Message', () => {
{ {
contentType: 'audio/aac', contentType: 'audio/aac',
flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE, flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE,
data: stringToArrayBuffer('Its easy if you try'), data: Bytes.fromString('Its easy if you try'),
fileName: 'test\u202Dfig.exe', fileName: 'test\u202Dfig.exe',
size: 1111, size: 1111,
}, },
@ -292,12 +292,13 @@ describe('Message', () => {
contact: [], contact: [],
}; };
const expectedAttachmentData = stringToArrayBuffer( const expectedAttachmentData = 'Its easy if you try';
'Its easy if you try'
);
const context = { const context = {
writeNewAttachmentData: async attachmentData => { writeNewAttachmentData: async attachmentData => {
assert.deepEqual(attachmentData, expectedAttachmentData); assert.strictEqual(
Bytes.toString(attachmentData),
expectedAttachmentData
);
return 'abc/abcdefg'; return 'abc/abcdefg';
}, },
getRegionCode: () => 'US', getRegionCode: () => 'US',

View file

@ -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));
}
);
});
});

View file

@ -245,7 +245,7 @@ export class ConversationController {
try { try {
if (isGroupV1(conversation.attributes)) { if (isGroupV1(conversation.attributes)) {
await maybeDeriveGroupV2Id(conversation); maybeDeriveGroupV2Id(conversation);
} }
await saveConversation(conversation.attributes); await saveConversation(conversation.attributes);
} catch (error) { } catch (error) {
@ -577,8 +577,7 @@ export class ConversationController {
let groupV2Id: undefined | string; let groupV2Id: undefined | string;
if (isGroupV1(conversation.attributes)) { if (isGroupV1(conversation.attributes)) {
// eslint-disable-next-line no-await-in-loop maybeDeriveGroupV2Id(conversation);
await maybeDeriveGroupV2Id(conversation);
groupV2Id = conversation.get('derivedGroupV2Id'); groupV2Id = conversation.get('derivedGroupV2Id');
assert( assert(
groupV2Id, groupV2Id,
@ -836,7 +835,7 @@ export class ConversationController {
// Hydrate contactCollection, now that initial fetch is complete // Hydrate contactCollection, now that initial fetch is complete
conversation.fetchContacts(); conversation.fetchContacts();
const isChanged = await maybeDeriveGroupV2Id(conversation); const isChanged = maybeDeriveGroupV2Id(conversation);
if (isChanged) { if (isChanged) {
updateConversation(conversation.attributes); updateConversation(conversation.attributes);
} }

File diff suppressed because it is too large Load diff

View file

@ -3,7 +3,8 @@
import * as client from '@signalapp/signal-client'; import * as client from '@signalapp/signal-client';
import { constantTimeEqual, typedArrayToArrayBuffer } from './Crypto'; import * as Bytes from './Bytes';
import { constantTimeEqual } from './Crypto';
import { import {
KeyPairType, KeyPairType,
CompatPreKeyType, CompatPreKeyType,
@ -26,9 +27,9 @@ export function generateSignedPreKey(
} }
if ( if (
!(identityKeyPair.privKey instanceof ArrayBuffer) || !(identityKeyPair.privKey instanceof Uint8Array) ||
identityKeyPair.privKey.byteLength !== 32 || identityKeyPair.privKey.byteLength !== 32 ||
!(identityKeyPair.pubKey instanceof ArrayBuffer) || !(identityKeyPair.pubKey instanceof Uint8Array) ||
identityKeyPair.pubKey.byteLength !== 33 identityKeyPair.pubKey.byteLength !== 33
) { ) {
throw new TypeError( throw new TypeError(
@ -63,24 +64,13 @@ export function generateKeyPair(): KeyPairType {
const pubKey = privKey.getPublicKey(); const pubKey = privKey.getPublicKey();
return { return {
privKey: typedArrayToArrayBuffer(privKey.serialize()), privKey: privKey.serialize(),
pubKey: typedArrayToArrayBuffer(pubKey.serialize()), pubKey: pubKey.serialize(),
}; };
} }
export function copyArrayBuffer(source: ArrayBuffer): ArrayBuffer { export function createKeyPair(incomingKey: Uint8Array): KeyPairType {
const sourceArray = new Uint8Array(source); const copy = new Uint8Array(incomingKey);
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);
clampPrivateKey(copy); clampPrivateKey(copy);
if (!constantTimeEqual(copy, incomingKey)) { if (!constantTimeEqual(copy, incomingKey)) {
log.warn('createKeyPair: incoming private key was not clamped!'); log.warn('createKeyPair: incoming private key was not clamped!');
@ -96,32 +86,31 @@ export function createKeyPair(incomingKey: ArrayBuffer): KeyPairType {
const pubKey = privKey.getPublicKey(); const pubKey = privKey.getPublicKey();
return { return {
privKey: typedArrayToArrayBuffer(privKey.serialize()), privKey: privKey.serialize(),
pubKey: typedArrayToArrayBuffer(pubKey.serialize()), pubKey: pubKey.serialize(),
}; };
} }
export function calculateAgreement( export function calculateAgreement(
pubKey: ArrayBuffer, pubKey: Uint8Array,
privKey: ArrayBuffer privKey: Uint8Array
): ArrayBuffer { ): Uint8Array {
const privKeyBuffer = Buffer.from(privKey); const privKeyBuffer = Buffer.from(privKey);
const pubKeyObj = client.PublicKey.deserialize( const pubKeyObj = client.PublicKey.deserialize(
Buffer.concat([ Buffer.from(
Buffer.from([0x05]), Bytes.concatenate([new Uint8Array([0x05]), validatePubKeyFormat(pubKey)])
Buffer.from(validatePubKeyFormat(pubKey)), )
])
); );
const privKeyObj = client.PrivateKey.deserialize(privKeyBuffer); const privKeyObj = client.PrivateKey.deserialize(privKeyBuffer);
const sharedSecret = privKeyObj.agree(pubKeyObj); const sharedSecret = privKeyObj.agree(pubKeyObj);
return typedArrayToArrayBuffer(sharedSecret); return sharedSecret;
} }
export function verifySignature( export function verifySignature(
pubKey: ArrayBuffer, pubKey: Uint8Array,
message: ArrayBuffer, message: Uint8Array,
signature: ArrayBuffer signature: Uint8Array
): boolean { ): boolean {
const pubKeyBuffer = Buffer.from(pubKey); const pubKeyBuffer = Buffer.from(pubKey);
const messageBuffer = Buffer.from(message); const messageBuffer = Buffer.from(message);
@ -134,22 +123,21 @@ export function verifySignature(
} }
export function calculateSignature( export function calculateSignature(
privKey: ArrayBuffer, privKey: Uint8Array,
plaintext: ArrayBuffer plaintext: Uint8Array
): ArrayBuffer { ): Uint8Array {
const privKeyBuffer = Buffer.from(privKey); const privKeyBuffer = Buffer.from(privKey);
const plaintextBuffer = Buffer.from(plaintext); const plaintextBuffer = Buffer.from(plaintext);
const privKeyObj = client.PrivateKey.deserialize(privKeyBuffer); const privKeyObj = client.PrivateKey.deserialize(privKeyBuffer);
const signature = privKeyObj.sign(plaintextBuffer); const signature = privKeyObj.sign(plaintextBuffer);
return typedArrayToArrayBuffer(signature); return signature;
} }
export function validatePubKeyFormat(pubKey: ArrayBuffer): ArrayBuffer { function validatePubKeyFormat(pubKey: Uint8Array): Uint8Array {
if ( if (
pubKey === undefined || pubKey === undefined ||
((pubKey.byteLength !== 33 || new Uint8Array(pubKey)[0] !== 5) && ((pubKey.byteLength !== 33 || pubKey[0] !== 5) && pubKey.byteLength !== 32)
pubKey.byteLength !== 32)
) { ) {
throw new Error('Invalid public key'); throw new Error('Invalid public key');
} }
@ -160,18 +148,16 @@ export function validatePubKeyFormat(pubKey: ArrayBuffer): ArrayBuffer {
return pubKey; return pubKey;
} }
export function setPublicKeyTypeByte(publicKey: ArrayBuffer): void { export function setPublicKeyTypeByte(publicKey: Uint8Array): void {
const byteArray = new Uint8Array(publicKey); // eslint-disable-next-line no-param-reassign
byteArray[0] = 5; publicKey[0] = 5;
} }
export function clampPrivateKey(privateKey: ArrayBuffer): void { export function clampPrivateKey(privateKey: Uint8Array): void {
const byteArray = new Uint8Array(privateKey); // eslint-disable-next-line no-bitwise, no-param-reassign
privateKey[0] &= 248;
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise, no-param-reassign
byteArray[0] &= 248; privateKey[31] &= 127;
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise, no-param-reassign
byteArray[31] &= 127; privateKey[31] |= 64;
// eslint-disable-next-line no-bitwise
byteArray[31] |= 64;
} }

View file

@ -27,8 +27,6 @@ import { Address } from './types/Address';
import { QualifiedAddress } from './types/QualifiedAddress'; import { QualifiedAddress } from './types/QualifiedAddress';
import type { UUID } from './types/UUID'; import type { UUID } from './types/UUID';
import { typedArrayToArrayBuffer } from './Crypto';
import { Zone } from './util/Zone'; import { Zone } from './util/Zone';
function encodeAddress(address: ProtocolAddress): Address { function encodeAddress(address: ProtocolAddress): Address {
@ -148,7 +146,7 @@ export class IdentityKeys extends IdentityKeyStore {
async saveIdentity(name: ProtocolAddress, key: PublicKey): Promise<boolean> { async saveIdentity(name: ProtocolAddress, key: PublicKey): Promise<boolean> {
const encodedAddress = encodeAddress(name); const encodedAddress = encodeAddress(name);
const publicKey = typedArrayToArrayBuffer(key.serialize()); const publicKey = key.serialize();
// Pass `zone` to let `saveIdentity` archive sibling sessions when identity // Pass `zone` to let `saveIdentity` archive sibling sessions when identity
// key changes. // key changes.
@ -166,7 +164,7 @@ export class IdentityKeys extends IdentityKeyStore {
direction: Direction direction: Direction
): Promise<boolean> { ): Promise<boolean> {
const encodedAddress = encodeAddress(name); const encodedAddress = encodeAddress(name);
const publicKey = typedArrayToArrayBuffer(key.serialize()); const publicKey = key.serialize();
return window.textsecure.storage.protocol.isTrustedIdentity( return window.textsecure.storage.protocol.isTrustedIdentity(
encodedAddress, encodedAddress,

View file

@ -17,12 +17,8 @@ import {
SignedPreKeyRecord, SignedPreKeyRecord,
} from '@signalapp/signal-client'; } from '@signalapp/signal-client';
import { import * as Bytes from './Bytes';
constantTimeEqual, import { constantTimeEqual } from './Crypto';
fromEncodedBinaryToArrayBuffer,
typedArrayToArrayBuffer,
base64ToArrayBuffer,
} from './Crypto';
import { assert, strictAssert } from './util/assert'; import { assert, strictAssert } from './util/assert';
import { handleMessageSend } from './util/handleMessageSend'; import { handleMessageSend } from './util/handleMessageSend';
import { isNotNil } from './util/isNotNil'; import { isNotNil } from './util/isNotNil';
@ -30,7 +26,7 @@ import { Zone } from './util/Zone';
import { isMoreRecentThan } from './util/timestamp'; import { isMoreRecentThan } from './util/timestamp';
import { import {
sessionRecordToProtobuf, sessionRecordToProtobuf,
sessionStructureToArrayBuffer, sessionStructureToBytes,
} from './util/sessionTranslation'; } from './util/sessionTranslation';
import { import {
DeviceType, DeviceType,
@ -81,7 +77,7 @@ function validateVerifiedStatus(status: number): boolean {
const identityKeySchema = z.object({ const identityKeySchema = z.object({
id: z.string(), id: z.string(),
publicKey: z.instanceof(ArrayBuffer), publicKey: z.instanceof(Uint8Array),
firstUse: z.boolean(), firstUse: z.boolean(),
timestamp: z.number().refine((value: number) => value % 1 === 0 && value > 0), timestamp: z.number().refine((value: number) => value % 1 === 0 && value > 0),
verified: z.number().refine(validateVerifiedStatus), verified: z.number().refine(validateVerifiedStatus),
@ -171,13 +167,13 @@ export function hydrateSignedPreKey(
export function freezeSession(session: SessionRecord): string { export function freezeSession(session: SessionRecord): string {
return session.serialize().toString('base64'); return session.serialize().toString('base64');
} }
export function freezePublicKey(publicKey: PublicKey): ArrayBuffer { export function freezePublicKey(publicKey: PublicKey): Uint8Array {
return typedArrayToArrayBuffer(publicKey.serialize()); return publicKey.serialize();
} }
export function freezePreKey(preKey: PreKeyRecord): KeyPairType { export function freezePreKey(preKey: PreKeyRecord): KeyPairType {
const keyPair = { const keyPair = {
pubKey: typedArrayToArrayBuffer(preKey.publicKey().serialize()), pubKey: preKey.publicKey().serialize(),
privKey: typedArrayToArrayBuffer(preKey.privateKey().serialize()), privKey: preKey.privateKey().serialize(),
}; };
return keyPair; return keyPair;
} }
@ -185,8 +181,8 @@ export function freezeSignedPreKey(
signedPreKey: SignedPreKeyRecord signedPreKey: SignedPreKeyRecord
): KeyPairType { ): KeyPairType {
const keyPair = { const keyPair = {
pubKey: typedArrayToArrayBuffer(signedPreKey.publicKey().serialize()), pubKey: signedPreKey.publicKey().serialize(),
privKey: typedArrayToArrayBuffer(signedPreKey.privateKey().serialize()), privKey: signedPreKey.privateKey().serialize(),
}; };
return keyPair; return keyPair;
} }
@ -260,8 +256,8 @@ export class SignalProtocolStore extends EventsMixin {
for (const key of Object.keys(map.value)) { for (const key of Object.keys(map.value)) {
const { privKey, pubKey } = map.value[key]; const { privKey, pubKey } = map.value[key];
this.ourIdentityKeys.set(new UUID(key).toString(), { this.ourIdentityKeys.set(new UUID(key).toString(), {
privKey: base64ToArrayBuffer(privKey), privKey: Bytes.fromBase64(privKey),
pubKey: base64ToArrayBuffer(pubKey), pubKey: Bytes.fromBase64(pubKey),
}); });
} }
})(), })(),
@ -607,7 +603,7 @@ export class SignalProtocolStore extends EventsMixin {
return entry.item; return entry.item;
} }
const item = SenderKeyRecord.deserialize(entry.fromDB.data); const item = SenderKeyRecord.deserialize(Buffer.from(entry.fromDB.data));
this.senderKeys.set(id, { this.senderKeys.set(id, {
hydrated: true, hydrated: true,
item, item,
@ -960,7 +956,7 @@ export class SignalProtocolStore extends EventsMixin {
localUserData localUserData
); );
const record = SessionRecord.deserialize( const record = SessionRecord.deserialize(
Buffer.from(sessionStructureToArrayBuffer(sessionProto)) Buffer.from(sessionStructureToBytes(sessionProto))
); );
await this.storeSession(QualifiedAddress.parse(session.id), record, { await this.storeSession(QualifiedAddress.parse(session.id), record, {
@ -1425,7 +1421,7 @@ export class SignalProtocolStore extends EventsMixin {
async isTrustedIdentity( async isTrustedIdentity(
encodedAddress: Address, encodedAddress: Address,
publicKey: ArrayBuffer, publicKey: Uint8Array,
direction: number direction: number
): Promise<boolean> { ): Promise<boolean> {
if (!this.identityKeys) { if (!this.identityKeys) {
@ -1463,7 +1459,7 @@ export class SignalProtocolStore extends EventsMixin {
} }
isTrustedForSending( isTrustedForSending(
publicKey: ArrayBuffer, publicKey: Uint8Array,
identityRecord?: IdentityKeyType identityRecord?: IdentityKeyType
): boolean { ): boolean {
if (!identityRecord) { if (!identityRecord) {
@ -1493,7 +1489,7 @@ export class SignalProtocolStore extends EventsMixin {
return true; return true;
} }
async loadIdentityKey(uuid: UUID): Promise<ArrayBuffer | undefined> { async loadIdentityKey(uuid: UUID): Promise<Uint8Array | undefined> {
if (uuid === null || uuid === undefined) { if (uuid === null || uuid === undefined) {
throw new Error('loadIdentityKey: uuid was undefined/null'); throw new Error('loadIdentityKey: uuid was undefined/null');
} }
@ -1522,7 +1518,7 @@ export class SignalProtocolStore extends EventsMixin {
async saveIdentity( async saveIdentity(
encodedAddress: Address, encodedAddress: Address,
publicKey: ArrayBuffer, publicKey: Uint8Array,
nonblockingApproval = false, nonblockingApproval = false,
{ zone }: SessionTransactionOptions = {} { zone }: SessionTransactionOptions = {}
): Promise<boolean> { ): Promise<boolean> {
@ -1533,9 +1529,9 @@ export class SignalProtocolStore extends EventsMixin {
if (encodedAddress === null || encodedAddress === undefined) { if (encodedAddress === null || encodedAddress === undefined) {
throw new Error('saveIdentity: encodedAddress was undefined/null'); throw new Error('saveIdentity: encodedAddress was undefined/null');
} }
if (!(publicKey instanceof ArrayBuffer)) { if (!(publicKey instanceof Uint8Array)) {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
publicKey = fromEncodedBinaryToArrayBuffer(publicKey); publicKey = Bytes.fromBinary(publicKey);
} }
if (typeof nonblockingApproval !== 'boolean') { if (typeof nonblockingApproval !== 'boolean') {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
@ -1668,7 +1664,7 @@ export class SignalProtocolStore extends EventsMixin {
async setVerified( async setVerified(
uuid: UUID, uuid: UUID,
verifiedStatus: number, verifiedStatus: number,
publicKey?: ArrayBuffer publicKey?: Uint8Array
): Promise<void> { ): Promise<void> {
if (uuid === null || uuid === undefined) { if (uuid === null || uuid === undefined) {
throw new Error('setVerified: uuid was undefined/null'); throw new Error('setVerified: uuid was undefined/null');
@ -1676,7 +1672,7 @@ export class SignalProtocolStore extends EventsMixin {
if (!validateVerifiedStatus(verifiedStatus)) { if (!validateVerifiedStatus(verifiedStatus)) {
throw new Error('setVerified: Invalid verified status'); 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'); throw new Error('setVerified: Invalid public key');
} }
@ -1719,7 +1715,7 @@ export class SignalProtocolStore extends EventsMixin {
processContactSyncVerificationState( processContactSyncVerificationState(
uuid: UUID, uuid: UUID,
verifiedStatus: number, verifiedStatus: number,
publicKey: ArrayBuffer publicKey: Uint8Array
): Promise<boolean> { ): Promise<boolean> {
if (verifiedStatus === VerifiedStatus.UNVERIFIED) { if (verifiedStatus === VerifiedStatus.UNVERIFIED) {
return this.processUnverifiedMessage(uuid, verifiedStatus, publicKey); return this.processUnverifiedMessage(uuid, verifiedStatus, publicKey);
@ -1733,12 +1729,12 @@ export class SignalProtocolStore extends EventsMixin {
async processUnverifiedMessage( async processUnverifiedMessage(
uuid: UUID, uuid: UUID,
verifiedStatus: number, verifiedStatus: number,
publicKey?: ArrayBuffer publicKey?: Uint8Array
): Promise<boolean> { ): Promise<boolean> {
if (uuid === null || uuid === undefined) { if (uuid === null || uuid === undefined) {
throw new Error('processUnverifiedMessage: uuid was undefined/null'); 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'); throw new Error('processUnverifiedMessage: Invalid public key');
} }
@ -1796,7 +1792,7 @@ export class SignalProtocolStore extends EventsMixin {
async processVerifiedMessage( async processVerifiedMessage(
uuid: UUID, uuid: UUID,
verifiedStatus: number, verifiedStatus: number,
publicKey?: ArrayBuffer publicKey?: Uint8Array
): Promise<boolean> { ): Promise<boolean> {
if (uuid === null || uuid === undefined) { if (uuid === null || uuid === undefined) {
throw new Error('processVerifiedMessage: uuid was undefined/null'); throw new Error('processVerifiedMessage: uuid was undefined/null');
@ -1804,7 +1800,7 @@ export class SignalProtocolStore extends EventsMixin {
if (!validateVerifiedStatus(verifiedStatus)) { if (!validateVerifiedStatus(verifiedStatus)) {
throw new Error('processVerifiedMessage: Invalid verified status'); 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'); throw new Error('processVerifiedMessage: Invalid public key');
} }

View file

@ -14,7 +14,6 @@ import {
ConversationAttributesType, ConversationAttributesType,
} from './model-types.d'; } from './model-types.d';
import * as Bytes from './Bytes'; import * as Bytes from './Bytes';
import { typedArrayToArrayBuffer } from './Crypto';
import { WhatIsThis, DeliveryReceiptBatcherItemType } from './window.d'; import { WhatIsThis, DeliveryReceiptBatcherItemType } from './window.d';
import { getTitleBarVisibility, TitleBarVisibility } from './types/Settings'; import { getTitleBarVisibility, TitleBarVisibility } from './types/Settings';
import { SocketStatus } from './types/SocketStatus'; import { SocketStatus } from './types/SocketStatus';
@ -93,7 +92,9 @@ import {
} from './messages/MessageSendState'; } from './messages/MessageSendState';
import * as AttachmentDownloads from './messageModifiers/AttachmentDownloads'; import * as AttachmentDownloads from './messageModifiers/AttachmentDownloads';
import * as preferredReactions from './state/ducks/preferredReactions'; import * as preferredReactions from './state/ducks/preferredReactions';
import * as Conversation from './types/Conversation';
import * as Stickers from './types/Stickers'; import * as Stickers from './types/Stickers';
import * as Errors from './types/errors';
import { SignalService as Proto } from './protobuf'; import { SignalService as Proto } from './protobuf';
import { onRetryRequest, onDecryptionError } from './util/handleRetry'; import { onRetryRequest, onDecryptionError } from './util/handleRetry';
import { themeChanged } from './shims/themeChanged'; import { themeChanged } from './shims/themeChanged';
@ -497,7 +498,7 @@ export async function startApp(): Promise<void> {
removeDatabase: removeIndexedDB, removeDatabase: removeIndexedDB,
doesDatabaseExist, doesDatabaseExist,
} = window.Signal.IndexedDB; } = window.Signal.IndexedDB;
const { Errors, Message } = window.Signal.Types; const { Message } = window.Signal.Types;
const { const {
upgradeMessageSchema, upgradeMessageSchema,
writeNewAttachmentData, writeNewAttachmentData,
@ -2559,7 +2560,7 @@ export async function startApp(): Promise<void> {
// special case for syncing details about ourselves // special case for syncing details about ourselves
if (details.profileKey) { if (details.profileKey) {
log.info('Got sync message with our own profile key'); 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<void> {
// Update the conversation avatar only if new avatar exists and hash differs // Update the conversation avatar only if new avatar exists and hash differs
const { avatar } = details; const { avatar } = details;
if (avatar && avatar.data) { if (avatar && avatar.data) {
const newAttributes = await window.Signal.Types.Conversation.maybeUpdateAvatar( const newAttributes = await Conversation.maybeUpdateAvatar(
conversation.attributes, conversation.attributes,
avatar.data, avatar.data,
{ {
@ -2650,9 +2651,7 @@ export async function startApp(): Promise<void> {
state: dropNull(verified.state), state: dropNull(verified.state),
destination: dropNull(verified.destination), destination: dropNull(verified.destination),
destinationUuid: dropNull(verified.destinationUuid), destinationUuid: dropNull(verified.destinationUuid),
identityKey: verified.identityKey identityKey: dropNull(verified.identityKey),
? typedArrayToArrayBuffer(verified.identityKey)
: undefined,
viaContactSync: true, viaContactSync: true,
}, },
noop noop
@ -2720,7 +2719,7 @@ export async function startApp(): Promise<void> {
// Update the conversation avatar only if new avatar exists and hash differs // Update the conversation avatar only if new avatar exists and hash differs
const { avatar } = details; const { avatar } = details;
if (avatar && avatar.data) { if (avatar && avatar.data) {
const newAttributes = await window.Signal.Types.Conversation.maybeUpdateAvatar( const newAttributes = await Conversation.maybeUpdateAvatar(
conversation.attributes, conversation.attributes,
avatar.data, avatar.data,
{ {
@ -3478,9 +3477,7 @@ export async function startApp(): Promise<void> {
if (storageServiceKey) { if (storageServiceKey) {
log.info('onKeysSync: received keys'); log.info('onKeysSync: received keys');
const storageServiceKeyBase64 = window.Signal.Crypto.arrayBufferToBase64( const storageServiceKeyBase64 = Bytes.toBase64(storageServiceKey);
storageServiceKey
);
window.storage.put('storageKey', storageServiceKeyBase64); window.storage.put('storageKey', storageServiceKeyBase64);
await window.Signal.Services.runStorageServiceSyncJob(); await window.Signal.Services.runStorageServiceSyncJob();

View file

@ -17,7 +17,7 @@ import { AvatarTextEditor } from './AvatarTextEditor';
import { AvatarUploadButton } from './AvatarUploadButton'; import { AvatarUploadButton } from './AvatarUploadButton';
import { BetterAvatar } from './BetterAvatar'; import { BetterAvatar } from './BetterAvatar';
import { LocalizerType } from '../types/Util'; import { LocalizerType } from '../types/Util';
import { avatarDataToArrayBuffer } from '../util/avatarDataToArrayBuffer'; import { avatarDataToBytes } from '../util/avatarDataToBytes';
import { createAvatarData } from '../util/createAvatarData'; import { createAvatarData } from '../util/createAvatarData';
import { isSameAvatarData } from '../util/isSameAvatarData'; import { isSameAvatarData } from '../util/isSameAvatarData';
import { missingCaseError } from '../util/missingCaseError'; import { missingCaseError } from '../util/missingCaseError';
@ -25,14 +25,14 @@ import { missingCaseError } from '../util/missingCaseError';
export type PropsType = { export type PropsType = {
avatarColor?: AvatarColorType; avatarColor?: AvatarColorType;
avatarPath?: string; avatarPath?: string;
avatarValue?: ArrayBuffer; avatarValue?: Uint8Array;
conversationId?: string; conversationId?: string;
conversationTitle?: string; conversationTitle?: string;
deleteAvatarFromDisk: DeleteAvatarFromDiskActionType; deleteAvatarFromDisk: DeleteAvatarFromDiskActionType;
i18n: LocalizerType; i18n: LocalizerType;
isGroup?: boolean; isGroup?: boolean;
onCancel: () => unknown; onCancel: () => unknown;
onSave: (buffer: ArrayBuffer | undefined) => unknown; onSave: (buffer: Uint8Array | undefined) => unknown;
userAvatarData: ReadonlyArray<AvatarDataType>; userAvatarData: ReadonlyArray<AvatarDataType>;
replaceAvatar: ReplaceAvatarActionType; replaceAvatar: ReplaceAvatarActionType;
saveAvatarToDisk: SaveAvatarToDiskActionType; saveAvatarToDisk: SaveAvatarToDiskActionType;
@ -62,10 +62,10 @@ export const AvatarEditor = ({
const [provisionalSelectedAvatar, setProvisionalSelectedAvatar] = useState< const [provisionalSelectedAvatar, setProvisionalSelectedAvatar] = useState<
AvatarDataType | undefined AvatarDataType | undefined
>(); >();
const [avatarPreview, setAvatarPreview] = useState<ArrayBuffer | undefined>( const [avatarPreview, setAvatarPreview] = useState<Uint8Array | undefined>(
avatarValue avatarValue
); );
const [initialAvatar, setInitialAvatar] = useState<ArrayBuffer | undefined>( const [initialAvatar, setInitialAvatar] = useState<Uint8Array | undefined>(
avatarValue avatarValue
); );
const [localAvatarData, setLocalAvatarData] = useState<Array<AvatarDataType>>( const [localAvatarData, setLocalAvatarData] = useState<Array<AvatarDataType>>(
@ -84,7 +84,7 @@ export const AvatarEditor = ({
const selectedAvatar = getSelectedAvatar(provisionalSelectedAvatar); 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. // that function is a little expensive to run and so we don't flicker the UI.
useEffect(() => { useEffect(() => {
let shouldCancel = false; let shouldCancel = false;
@ -95,7 +95,7 @@ export const AvatarEditor = ({
if (avatarData.buffer) { if (avatarData.buffer) {
return avatarData; return avatarData;
} }
const buffer = await avatarDataToArrayBuffer(avatarData); const buffer = await avatarDataToBytes(avatarData);
return { return {
...avatarData, ...avatarData,
buffer, buffer,

View file

@ -9,7 +9,7 @@ import { AvatarDataType } from '../types/Avatar';
import { AvatarModalButtons } from './AvatarModalButtons'; import { AvatarModalButtons } from './AvatarModalButtons';
import { AvatarPreview } from './AvatarPreview'; import { AvatarPreview } from './AvatarPreview';
import { LocalizerType } from '../types/Util'; import { LocalizerType } from '../types/Util';
import { avatarDataToArrayBuffer } from '../util/avatarDataToArrayBuffer'; import { avatarDataToBytes } from '../util/avatarDataToBytes';
export type PropsType = { export type PropsType = {
avatarData: AvatarDataType; avatarData: AvatarDataType;
@ -22,7 +22,7 @@ export const AvatarIconEditor = ({
i18n, i18n,
onClose, onClose,
}: PropsType): JSX.Element => { }: PropsType): JSX.Element => {
const [avatarBuffer, setAvatarBuffer] = useState<ArrayBuffer | undefined>(); const [avatarBuffer, setAvatarBuffer] = useState<Uint8Array | undefined>();
const [avatarData, setAvatarData] = useState<AvatarDataType>( const [avatarData, setAvatarData] = useState<AvatarDataType>(
initialAvatarData initialAvatarData
); );
@ -41,7 +41,7 @@ export const AvatarIconEditor = ({
let shouldCancel = false; let shouldCancel = false;
async function loadAvatar() { async function loadAvatar() {
const buffer = await avatarDataToArrayBuffer(avatarData); const buffer = await avatarDataToBytes(avatarData);
if (!shouldCancel) { if (!shouldCancel) {
setAvatarBuffer(buffer); setAvatarBuffer(buffer);
} }

View file

@ -19,7 +19,7 @@ const TEST_IMAGE = new Uint8Array(
'89504e470d0a1a0a0000000d4948445200000008000000080103000000fec12cc800000006504c5445ff00ff00ff000c82e9800000001849444154085b633061a8638863a867f8c720c760c12000001a4302f4d81dd9870000000049454e44ae426082', '89504e470d0a1a0a0000000d4948445200000008000000080103000000fec12cc800000006504c5445ff00ff00ff000c82e9800000001849444154085b633061a8638863a867f8c720c760c12000001a4302f4d81dd9870000000049454e44ae426082',
2 2
).map(bytePair => parseInt(bytePair.join(''), 16)) ).map(bytePair => parseInt(bytePair.join(''), 16))
).buffer; );
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
avatarColor: overrideProps.avatarColor, avatarColor: overrideProps.avatarColor,

View file

@ -9,17 +9,17 @@ import { LocalizerType } from '../types/Util';
import { Spinner } from './Spinner'; import { Spinner } from './Spinner';
import { AvatarColors, AvatarColorType } from '../types/Colors'; import { AvatarColors, AvatarColorType } from '../types/Colors';
import { getInitials } from '../util/getInitials'; import { getInitials } from '../util/getInitials';
import { imagePathToArrayBuffer } from '../util/imagePathToArrayBuffer'; import { imagePathToBytes } from '../util/imagePathToBytes';
export type PropsType = { export type PropsType = {
avatarColor?: AvatarColorType; avatarColor?: AvatarColorType;
avatarPath?: string; avatarPath?: string;
avatarValue?: ArrayBuffer; avatarValue?: Uint8Array;
conversationTitle?: string; conversationTitle?: string;
i18n: LocalizerType; i18n: LocalizerType;
isEditable?: boolean; isEditable?: boolean;
isGroup?: boolean; isGroup?: boolean;
onAvatarLoaded?: (avatarBuffer: ArrayBuffer) => unknown; onAvatarLoaded?: (avatarBuffer: Uint8Array) => unknown;
onClear?: () => unknown; onClear?: () => unknown;
onClick?: () => unknown; onClick?: () => unknown;
style?: CSSProperties; style?: CSSProperties;
@ -48,7 +48,7 @@ export const AvatarPreview = ({
avatarValue ? undefined : avatarPath avatarValue ? undefined : avatarPath
); );
const [avatarPreview, setAvatarPreview] = useState<ArrayBuffer | undefined>(); const [avatarPreview, setAvatarPreview] = useState<Uint8Array | undefined>();
// Loads the initial avatarPath if one is provided. // Loads the initial avatarPath if one is provided.
useEffect(() => { useEffect(() => {
@ -61,7 +61,7 @@ export const AvatarPreview = ({
(async () => { (async () => {
try { try {
const buffer = await imagePathToArrayBuffer(startingAvatarPath); const buffer = await imagePathToBytes(startingAvatarPath);
if (shouldCancel) { if (shouldCancel) {
return; return;
} }
@ -95,7 +95,7 @@ export const AvatarPreview = ({
} }
}, [avatarValue]); }, [avatarValue]);
// Creates the object URL to render the ArrayBuffer image // Creates the object URL to render the Uint8Array image
const [objectUrl, setObjectUrl] = useState<undefined | string>(); const [objectUrl, setObjectUrl] = useState<undefined | string>();
useEffect(() => { useEffect(() => {

View file

@ -19,7 +19,7 @@ import { AvatarDataType } from '../types/Avatar';
import { AvatarModalButtons } from './AvatarModalButtons'; import { AvatarModalButtons } from './AvatarModalButtons';
import { BetterAvatarBubble } from './BetterAvatarBubble'; import { BetterAvatarBubble } from './BetterAvatarBubble';
import { LocalizerType } from '../types/Util'; import { LocalizerType } from '../types/Util';
import { avatarDataToArrayBuffer } from '../util/avatarDataToArrayBuffer'; import { avatarDataToBytes } from '../util/avatarDataToBytes';
import { createAvatarData } from '../util/createAvatarData'; import { createAvatarData } from '../util/createAvatarData';
import { import {
getFittedFontSize, getFittedFontSize,
@ -27,7 +27,7 @@ import {
} from '../util/avatarTextSizeCalculator'; } from '../util/avatarTextSizeCalculator';
type DoneHandleType = ( type DoneHandleType = (
avatarBuffer: ArrayBuffer, avatarBuffer: Uint8Array,
avatarData: AvatarDataType avatarData: AvatarDataType
) => unknown; ) => unknown;
@ -111,7 +111,7 @@ export const AvatarTextEditor = ({
text: inputText, text: inputText,
}); });
const buffer = await avatarDataToArrayBuffer(newAvatarData); const buffer = await avatarDataToBytes(newAvatarData);
onDoneRef.current(buffer, newAvatarData); onDoneRef.current(buffer, newAvatarData);
}, [inputText, selectedColor]); }, [inputText, selectedColor]);

View file

@ -10,7 +10,7 @@ import { processImageFile } from '../util/processImageFile';
export type PropsType = { export type PropsType = {
className: string; className: string;
i18n: LocalizerType; i18n: LocalizerType;
onChange: (avatar: ArrayBuffer) => unknown; onChange: (avatar: Uint8Array) => unknown;
}; };
export const AvatarUploadButton = ({ export const AvatarUploadButton = ({
@ -30,7 +30,7 @@ export const AvatarUploadButton = ({
let shouldCancel = false; let shouldCancel = false;
(async () => { (async () => {
let newAvatar: ArrayBuffer; let newAvatar: Uint8Array;
try { try {
newAvatar = await processImageFile(processingFile); newAvatar = await processImageFile(processingFile);
} catch (err) { } catch (err) {

View file

@ -7,7 +7,7 @@ import { AvatarDataType } from '../types/Avatar';
import { BetterAvatarBubble } from './BetterAvatarBubble'; import { BetterAvatarBubble } from './BetterAvatarBubble';
import { LocalizerType } from '../types/Util'; import { LocalizerType } from '../types/Util';
import { Spinner } from './Spinner'; import { Spinner } from './Spinner';
import { avatarDataToArrayBuffer } from '../util/avatarDataToArrayBuffer'; import { avatarDataToBytes } from '../util/avatarDataToBytes';
type AvatarSize = 48 | 80; type AvatarSize = 48 | 80;
@ -15,7 +15,7 @@ export type PropsType = {
avatarData: AvatarDataType; avatarData: AvatarDataType;
i18n: LocalizerType; i18n: LocalizerType;
isSelected?: boolean; isSelected?: boolean;
onClick: (avatarBuffer: ArrayBuffer | undefined) => unknown; onClick: (avatarBuffer: Uint8Array | undefined) => unknown;
onDelete: () => unknown; onDelete: () => unknown;
size?: AvatarSize; size?: AvatarSize;
}; };
@ -28,7 +28,7 @@ export const BetterAvatar = ({
onDelete, onDelete,
size = 48, size = 48,
}: PropsType): JSX.Element => { }: PropsType): JSX.Element => {
const [avatarBuffer, setAvatarBuffer] = useState<ArrayBuffer | undefined>( const [avatarBuffer, setAvatarBuffer] = useState<Uint8Array | undefined>(
avatarData.buffer avatarData.buffer
); );
const [avatarURL, setAvatarURL] = useState<string | undefined>(undefined); const [avatarURL, setAvatarURL] = useState<string | undefined>(undefined);
@ -37,7 +37,7 @@ export const BetterAvatar = ({
let shouldCancel = false; let shouldCancel = false;
async function makeAvatar() { async function makeAvatar() {
const buffer = await avatarDataToArrayBuffer(avatarData); const buffer = await avatarDataToBytes(avatarData);
if (!shouldCancel) { if (!shouldCancel) {
setAvatarBuffer(buffer); setAvatarBuffer(buffer);
} }
@ -56,7 +56,7 @@ export const BetterAvatar = ({
}; };
}, [avatarBuffer, avatarData]); }, [avatarBuffer, avatarData]);
// Convert avatar's ArrayBuffer to a URL object // Convert avatar's Uint8Array to a URL object
useEffect(() => { useEffect(() => {
if (avatarBuffer) { if (avatarBuffer) {
const url = URL.createObjectURL(new Blob([avatarBuffer])); const url = URL.createObjectURL(new Blob([avatarBuffer]));

View file

@ -102,7 +102,7 @@ export type PropsType = {
switchToAssociatedView?: boolean; switchToAssociatedView?: boolean;
}) => void; }) => void;
setComposeSearchTerm: (composeSearchTerm: string) => void; setComposeSearchTerm: (composeSearchTerm: string) => void;
setComposeGroupAvatar: (_: undefined | ArrayBuffer) => void; setComposeGroupAvatar: (_: undefined | Uint8Array) => void;
setComposeGroupName: (_: string) => void; setComposeGroupName: (_: string) => void;
setComposeGroupExpireTimer: (_: number) => void; setComposeGroupExpireTimer: (_: number) => void;
showArchivedConversations: () => void; showArchivedConversations: () => void;

View file

@ -37,7 +37,7 @@ type PropsExternalType = {
onEditStateChanged: (editState: EditState) => unknown; onEditStateChanged: (editState: EditState) => unknown;
onProfileChanged: ( onProfileChanged: (
profileData: ProfileDataType, profileData: ProfileDataType,
avatarBuffer?: ArrayBuffer avatarBuffer?: Uint8Array
) => unknown; ) => unknown;
}; };
@ -126,7 +126,7 @@ export const ProfileEditor = ({
aboutText, aboutText,
}); });
const [avatarBuffer, setAvatarBuffer] = useState<ArrayBuffer | undefined>( const [avatarBuffer, setAvatarBuffer] = useState<Uint8Array | undefined>(
undefined undefined
); );
const [stagedProfile, setStagedProfile] = useState<ProfileDataType>({ const [stagedProfile, setStagedProfile] = useState<ProfileDataType>({
@ -153,7 +153,7 @@ export const ProfileEditor = ({
); );
const handleAvatarChanged = useCallback( const handleAvatarChanged = useCallback(
(avatar: ArrayBuffer | undefined) => { (avatar: Uint8Array | undefined) => {
setAvatarBuffer(avatar); setAvatarBuffer(avatar);
setEditState(EditState.None); setEditState(EditState.None);
onProfileChanged(stagedProfile, avatar); onProfileChanged(stagedProfile, avatar);

View file

@ -18,7 +18,7 @@ export type PropsDataType = {
type PropsType = { type PropsType = {
myProfileChanged: ( myProfileChanged: (
profileData: ProfileDataType, profileData: ProfileDataType,
avatarBuffer?: ArrayBuffer avatarBuffer?: Uint8Array
) => unknown; ) => unknown;
toggleProfileEditor: () => unknown; toggleProfileEditor: () => unknown;
toggleProfileEditorHasError: () => unknown; toggleProfileEditorHasError: () => unknown;

View file

@ -72,7 +72,7 @@ export type StateProps = {
showConversationNotificationsSettings: () => void; showConversationNotificationsSettings: () => void;
updateGroupAttributes: ( updateGroupAttributes: (
_: Readonly<{ _: Readonly<{
avatar?: undefined | ArrayBuffer; avatar?: undefined | Uint8Array;
description?: string; description?: string;
title?: string; title?: string;
}> }>
@ -169,7 +169,7 @@ export const ConversationDetails: React.ComponentType<Props> = ({
} }
makeRequest={async ( makeRequest={async (
options: Readonly<{ options: Readonly<{
avatar?: undefined | ArrayBuffer; avatar?: undefined | Uint8Array;
description?: string; description?: string;
title?: string; title?: string;
}> }>

View file

@ -34,7 +34,7 @@ type PropsType = {
initiallyFocusDescription: boolean; initiallyFocusDescription: boolean;
makeRequest: ( makeRequest: (
_: Readonly<{ _: Readonly<{
avatar?: undefined | ArrayBuffer; avatar?: undefined | Uint8Array;
description?: string; description?: string;
title?: undefined | string; title?: undefined | string;
}> }>
@ -73,7 +73,7 @@ export const EditConversationAttributesModal: FunctionComponent<PropsType> = ({
const startingAvatarPathRef = useRef<undefined | string>(externalAvatarPath); const startingAvatarPathRef = useRef<undefined | string>(externalAvatarPath);
const [editingAvatar, setEditingAvatar] = useState(false); const [editingAvatar, setEditingAvatar] = useState(false);
const [avatar, setAvatar] = useState<undefined | ArrayBuffer>(); const [avatar, setAvatar] = useState<undefined | Uint8Array>();
const [rawTitle, setRawTitle] = useState(externalTitle); const [rawTitle, setRawTitle] = useState(externalTitle);
const [rawGroupDescription, setRawGroupDescription] = useState( const [rawGroupDescription, setRawGroupDescription] = useState(
externalGroupDescription externalGroupDescription
@ -111,7 +111,7 @@ export const EditConversationAttributesModal: FunctionComponent<PropsType> = ({
event.preventDefault(); event.preventDefault();
const request: { const request: {
avatar?: undefined | ArrayBuffer; avatar?: undefined | Uint8Array;
description?: string; description?: string;
title?: string; title?: string;
} = {}; } = {};

View file

@ -56,7 +56,7 @@ export abstract class LeftPaneHelper<T> {
composeSaveAvatarToDisk: SaveAvatarToDiskActionType; composeSaveAvatarToDisk: SaveAvatarToDiskActionType;
createGroup: () => unknown; createGroup: () => unknown;
i18n: LocalizerType; i18n: LocalizerType;
setComposeGroupAvatar: (_: undefined | ArrayBuffer) => unknown; setComposeGroupAvatar: (_: undefined | Uint8Array) => unknown;
setComposeGroupName: (_: string) => unknown; setComposeGroupName: (_: string) => unknown;
setComposeGroupExpireTimer: (_: number) => void; setComposeGroupExpireTimer: (_: number) => void;
onChangeComposeSearchTerm: ( onChangeComposeSearchTerm: (

View file

@ -24,7 +24,7 @@ import {
import { AvatarColors } from '../../types/Colors'; import { AvatarColors } from '../../types/Colors';
export type LeftPaneSetGroupMetadataPropsType = { export type LeftPaneSetGroupMetadataPropsType = {
groupAvatar: undefined | ArrayBuffer; groupAvatar: undefined | Uint8Array;
groupName: string; groupName: string;
groupExpireTimer: number; groupExpireTimer: number;
hasError: boolean; hasError: boolean;
@ -37,7 +37,7 @@ export type LeftPaneSetGroupMetadataPropsType = {
/* eslint-disable class-methods-use-this */ /* eslint-disable class-methods-use-this */
export class LeftPaneSetGroupMetadataHelper extends LeftPaneHelper<LeftPaneSetGroupMetadataPropsType> { export class LeftPaneSetGroupMetadataHelper extends LeftPaneHelper<LeftPaneSetGroupMetadataPropsType> {
private readonly groupAvatar: undefined | ArrayBuffer; private readonly groupAvatar: undefined | Uint8Array;
private readonly groupName: string; private readonly groupName: string;
@ -127,7 +127,7 @@ export class LeftPaneSetGroupMetadataHelper extends LeftPaneHelper<LeftPaneSetGr
composeSaveAvatarToDisk: SaveAvatarToDiskActionType; composeSaveAvatarToDisk: SaveAvatarToDiskActionType;
createGroup: () => unknown; createGroup: () => unknown;
i18n: LocalizerType; i18n: LocalizerType;
setComposeGroupAvatar: (_: undefined | ArrayBuffer) => unknown; setComposeGroupAvatar: (_: undefined | Uint8Array) => unknown;
setComposeGroupExpireTimer: (_: number) => void; setComposeGroupExpireTimer: (_: number) => void;
setComposeGroupName: (_: string) => unknown; setComposeGroupName: (_: string) => unknown;
toggleComposeEditingAvatar: () => unknown; toggleComposeEditingAvatar: () => unknown;

129
ts/context/Crypto.ts Normal file
View file

@ -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));
}
}

View file

@ -2,6 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { Bytes } from './Bytes'; import { Bytes } from './Bytes';
import { Crypto } from './Crypto';
import { import {
createNativeThemeListener, createNativeThemeListener,
MinimalIPC, MinimalIPC,
@ -10,6 +11,8 @@ import {
export class Context { export class Context {
public readonly bytes = new Bytes(); public readonly bytes = new Bytes();
public readonly crypto = new Crypto();
public readonly nativeThemeListener; public readonly nativeThemeListener;
constructor(ipc: MinimalIPC) { constructor(ipc: MinimalIPC) {

View file

@ -53,9 +53,7 @@ import {
import { import {
computeHash, computeHash,
deriveMasterKeyFromGroupV1, deriveMasterKeyFromGroupV1,
fromEncodedBinaryToArrayBuffer,
getRandomBytes, getRandomBytes,
typedArrayToArrayBuffer,
} from './Crypto'; } from './Crypto';
import { import {
GroupCredentialsType, GroupCredentialsType,
@ -224,9 +222,6 @@ export type GroupFields = {
readonly publicParams: Uint8Array; readonly publicParams: Uint8Array;
}; };
// TODO: remove once we move away from ArrayBuffers
const FIXMEU8 = Uint8Array;
const MAX_CACHED_GROUP_FIELDS = 100; const MAX_CACHED_GROUP_FIELDS = 100;
const groupFieldsCache = new LRU<string, GroupFields>({ const groupFieldsCache = new LRU<string, GroupFields>({
@ -256,7 +251,7 @@ type UpdatesResultType = {
}; };
type UploadedAvatarType = { type UploadedAvatarType = {
data: ArrayBuffer; data: Uint8Array;
hash: string; hash: string;
key: string; key: string;
}; };
@ -277,7 +272,7 @@ const GROUP_INVITE_LINK_PASSWORD_LENGTH = 16;
// Group Links // Group Links
export function generateGroupInviteLinkPassword(): ArrayBuffer { export function generateGroupInviteLinkPassword(): Uint8Array {
return getRandomBytes(GROUP_INVITE_LINK_PASSWORD_LENGTH); return getRandomBytes(GROUP_INVITE_LINK_PASSWORD_LENGTH);
} }
@ -366,24 +361,24 @@ async function uploadAvatar(
logId: string; logId: string;
publicParams: string; publicParams: string;
secretParams: string; secretParams: string;
} & ({ path: string } | { data: ArrayBuffer }) } & ({ path: string } | { data: Uint8Array })
): Promise<UploadedAvatarType> { ): Promise<UploadedAvatarType> {
const { logId, publicParams, secretParams } = options; const { logId, publicParams, secretParams } = options;
try { try {
const clientZkGroupCipher = getClientZkGroupCipher(secretParams); const clientZkGroupCipher = getClientZkGroupCipher(secretParams);
let data: ArrayBuffer; let data: Uint8Array;
if ('data' in options) { if ('data' in options) {
({ data } = options); ({ data } = options);
} else { } else {
data = await window.Signal.Migrations.readAttachmentData(options.path); data = await window.Signal.Migrations.readAttachmentData(options.path);
} }
const hash = await computeHash(data); const hash = computeHash(data);
const blobPlaintext = Proto.GroupAttributeBlob.encode({ const blobPlaintext = Proto.GroupAttributeBlob.encode({
avatar: new FIXMEU8(data), avatar: data,
}).finish(); }).finish();
const ciphertext = encryptGroupBlob(clientZkGroupCipher, blobPlaintext); const ciphertext = encryptGroupBlob(clientZkGroupCipher, blobPlaintext);
@ -731,7 +726,7 @@ export async function buildUpdateAttributesChange(
'id' | 'revision' | 'publicParams' | 'secretParams' 'id' | 'revision' | 'publicParams' | 'secretParams'
>, >,
attributes: Readonly<{ attributes: Readonly<{
avatar?: undefined | ArrayBuffer; avatar?: undefined | Uint8Array;
description?: string; description?: string;
title?: string; title?: string;
}> }>
@ -1309,7 +1304,7 @@ export async function modifyGroupV2({
window.Signal.Util.sendToGroup({ window.Signal.Util.sendToGroup({
groupSendOptions: { groupSendOptions: {
groupV2: conversation.getGroupV2Info({ groupV2: conversation.getGroupV2Info({
groupChange: typedArrayToArrayBuffer(groupChangeBuffer), groupChange: groupChangeBuffer,
includePendingMembers: true, includePendingMembers: true,
extraConversationsForSend, extraConversationsForSend,
}), }),
@ -1497,7 +1492,7 @@ export async function createGroupV2({
avatars, avatars,
}: Readonly<{ }: Readonly<{
name: string; name: string;
avatar: undefined | ArrayBuffer; avatar: undefined | Uint8Array;
expireTimer: undefined | number; expireTimer: undefined | number;
conversationIds: Array<string>; conversationIds: Array<string>;
avatars?: Array<AvatarDataType>; avatars?: Array<AvatarDataType>;
@ -1514,7 +1509,7 @@ export async function createGroupV2({
const ACCESS_ENUM = Proto.AccessControl.AccessRequired; const ACCESS_ENUM = Proto.AccessControl.AccessRequired;
const MEMBER_ROLE_ENUM = Proto.Member.Role; const MEMBER_ROLE_ENUM = Proto.Member.Role;
const masterKeyBuffer = new FIXMEU8(getRandomBytes(32)); const masterKeyBuffer = getRandomBytes(32);
const fields = deriveGroupFields(masterKeyBuffer); const fields = deriveGroupFields(masterKeyBuffer);
const groupId = Bytes.toBase64(fields.id); const groupId = Bytes.toBase64(fields.id);
@ -1763,10 +1758,8 @@ export async function hasV1GroupBeenMigrated(
throw new Error(`checkForGV2Existence/${logId}: No groupId!`); throw new Error(`checkForGV2Existence/${logId}: No groupId!`);
} }
const idBuffer = fromEncodedBinaryToArrayBuffer(groupId); const idBuffer = Bytes.fromBinary(groupId);
const masterKeyBuffer = new FIXMEU8( const masterKeyBuffer = deriveMasterKeyFromGroupV1(idBuffer);
await deriveMasterKeyFromGroupV1(idBuffer)
);
const fields = deriveGroupFields(masterKeyBuffer); const fields = deriveGroupFields(masterKeyBuffer);
try { try {
@ -1783,9 +1776,7 @@ export async function hasV1GroupBeenMigrated(
} }
} }
export async function maybeDeriveGroupV2Id( export function maybeDeriveGroupV2Id(conversation: ConversationModel): boolean {
conversation: ConversationModel
): Promise<boolean> {
const isGroupV1 = getIsGroupV1(conversation.attributes); const isGroupV1 = getIsGroupV1(conversation.attributes);
const groupV1Id = conversation.get('groupId'); const groupV1Id = conversation.get('groupId');
const derived = conversation.get('derivedGroupV2Id'); const derived = conversation.get('derivedGroupV2Id');
@ -1794,10 +1785,8 @@ export async function maybeDeriveGroupV2Id(
return false; return false;
} }
const v1IdBuffer = fromEncodedBinaryToArrayBuffer(groupV1Id); const v1IdBuffer = Bytes.fromBinary(groupV1Id);
const masterKeyBuffer = new FIXMEU8( const masterKeyBuffer = deriveMasterKeyFromGroupV1(v1IdBuffer);
await deriveMasterKeyFromGroupV1(v1IdBuffer)
);
const fields = deriveGroupFields(masterKeyBuffer); const fields = deriveGroupFields(masterKeyBuffer);
const derivedGroupV2Id = Bytes.toBase64(fields.id); const derivedGroupV2Id = Bytes.toBase64(fields.id);
@ -2050,10 +2039,8 @@ export async function initiateMigrationToGroupV2(
); );
} }
const groupV1IdBuffer = fromEncodedBinaryToArrayBuffer(previousGroupV1Id); const groupV1IdBuffer = Bytes.fromBinary(previousGroupV1Id);
const masterKeyBuffer = new FIXMEU8( const masterKeyBuffer = deriveMasterKeyFromGroupV1(groupV1IdBuffer);
await deriveMasterKeyFromGroupV1(groupV1IdBuffer)
);
const fields = deriveGroupFields(masterKeyBuffer); const fields = deriveGroupFields(masterKeyBuffer);
const groupId = Bytes.toBase64(fields.id); const groupId = Bytes.toBase64(fields.id);
@ -2219,9 +2206,7 @@ export async function initiateMigrationToGroupV2(
const logId = conversation.idForLogging(); const logId = conversation.idForLogging();
const timestamp = Date.now(); const timestamp = Date.now();
const ourProfileKey: const ourProfileKey = await ourProfileKeyService.get();
| ArrayBuffer
| undefined = await ourProfileKeyService.get();
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
const sendOptions = await getSendOptions(conversation.attributes); const sendOptions = await getSendOptions(conversation.attributes);
@ -2408,10 +2393,8 @@ export async function joinGroupV2ViaLinkAndMigrate({
} }
// Derive GroupV2 fields // Derive GroupV2 fields
const groupV1IdBuffer = fromEncodedBinaryToArrayBuffer(previousGroupV1Id); const groupV1IdBuffer = Bytes.fromBinary(previousGroupV1Id);
const masterKeyBuffer = new FIXMEU8( const masterKeyBuffer = deriveMasterKeyFromGroupV1(groupV1IdBuffer);
await deriveMasterKeyFromGroupV1(groupV1IdBuffer)
);
const fields = deriveGroupFields(masterKeyBuffer); const fields = deriveGroupFields(masterKeyBuffer);
const groupId = Bytes.toBase64(fields.id); const groupId = Bytes.toBase64(fields.id);
@ -2506,10 +2489,8 @@ export async function respondToGroupV2Migration({
conversation.hasMember(ourConversationId); conversation.hasMember(ourConversationId);
// Derive GroupV2 fields // Derive GroupV2 fields
const groupV1IdBuffer = fromEncodedBinaryToArrayBuffer(previousGroupV1Id); const groupV1IdBuffer = Bytes.fromBinary(previousGroupV1Id);
const masterKeyBuffer = new FIXMEU8( const masterKeyBuffer = deriveMasterKeyFromGroupV1(groupV1IdBuffer);
await deriveMasterKeyFromGroupV1(groupV1IdBuffer)
);
const fields = deriveGroupFields(masterKeyBuffer); const fields = deriveGroupFields(masterKeyBuffer);
const groupId = Bytes.toBase64(fields.id); const groupId = Bytes.toBase64(fields.id);
@ -3421,7 +3402,7 @@ async function integrateGroupChange({
if (groupChange) { if (groupChange) {
groupChangeActions = Proto.GroupChange.Actions.decode( groupChangeActions = Proto.GroupChange.Actions.decode(
groupChange.actions || new FIXMEU8(0) groupChange.actions || new Uint8Array(0)
); );
if ( if (
@ -4541,7 +4522,7 @@ async function applyGroupChange({
export async function decryptGroupAvatar( export async function decryptGroupAvatar(
avatarKey: string, avatarKey: string,
secretParamsBase64: string secretParamsBase64: string
): Promise<ArrayBuffer> { ): Promise<Uint8Array> {
const sender = window.textsecure.messaging; const sender = window.textsecure.messaging;
if (!sender) { if (!sender) {
throw new Error( 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 clientZkGroupCipher = getClientZkGroupCipher(secretParamsBase64);
const plaintext = decryptGroupBlob(clientZkGroupCipher, ciphertext); const plaintext = decryptGroupBlob(clientZkGroupCipher, ciphertext);
const blob = Proto.GroupAttributeBlob.decode(plaintext); 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 // Ovewriting result.avatar as part of functionality
@ -4583,7 +4564,7 @@ export async function applyNewAvatar(
} }
const data = await decryptGroupAvatar(newAvatar, result.secretParams); 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) { if (result.avatar && result.avatar.path && result.avatar.hash !== hash) {
await window.Signal.Migrations.deleteAttachmentData(result.avatar.path); await window.Signal.Migrations.deleteAttachmentData(result.avatar.path);

View file

@ -465,7 +465,7 @@ async function getMessageSendData({
mentions: undefined | BodyRangesType; mentions: undefined | BodyRangesType;
messageTimestamp: number; messageTimestamp: number;
preview: Array<PreviewType>; preview: Array<PreviewType>;
profileKey: undefined | ArrayBuffer; profileKey: undefined | Uint8Array;
quote: WhatIsThis; quote: WhatIsThis;
sticker: WhatIsThis; sticker: WhatIsThis;
}> { }> {

View file

@ -64,7 +64,7 @@ export type LinkPreviewMetadata = {
}; };
export type LinkPreviewImage = { export type LinkPreviewImage = {
data: ArrayBuffer; data: Uint8Array;
contentType: MIMEType; contentType: MIMEType;
}; };
@ -290,7 +290,7 @@ const getHtmlDocument = async (
let result: HTMLDocument = emptyHtmlDocument(); let result: HTMLDocument = emptyHtmlDocument();
const maxHtmlBytesToLoad = Math.min(contentLength, MAX_HTML_BYTES_TO_LOAD); 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; let bytesLoadedSoFar = 0;
try { try {
@ -578,9 +578,9 @@ export async function fetchLinkPreviewImage(
return null; return null;
} }
let data: ArrayBuffer; let data: Uint8Array;
try { try {
data = await response.arrayBuffer(); data = await response.buffer();
} catch (err) { } catch (err) {
log.warn('fetchLinkPreviewImage: failed to read body; bailing'); log.warn('fetchLinkPreviewImage: failed to read body; bailing');
return null; return null;

View file

@ -7,7 +7,7 @@ import { v4 as getGuid } from 'uuid';
import dataInterface from '../sql/Client'; import dataInterface from '../sql/Client';
import * as durations from '../util/durations'; import * as durations from '../util/durations';
import { downloadAttachment } from '../util/downloadAttachment'; import { downloadAttachment } from '../util/downloadAttachment';
import { stringFromBytes } from '../Crypto'; import * as Bytes from '../Bytes';
import { import {
AttachmentDownloadJobType, AttachmentDownloadJobType,
AttachmentDownloadJobTypeType, AttachmentDownloadJobTypeType,
@ -319,7 +319,7 @@ async function _addAttachmentToMessage(
attachment attachment
); );
message.set({ message.set({
body: attachment.error ? message.get('body') : stringFromBytes(data), body: attachment.error ? message.get('body') : Bytes.toString(data),
bodyPending: false, bodyPending: false,
}); });
} finally { } finally {

4
ts/model-types.d.ts vendored
View file

@ -94,7 +94,7 @@ export type MessageAttributesType = {
bodyRanges?: BodyRangesType; bodyRanges?: BodyRangesType;
callHistoryDetails?: CallHistoryDetailsFromDiskType; callHistoryDetails?: CallHistoryDetailsFromDiskType;
changedId?: string; changedId?: string;
dataMessage?: ArrayBuffer | null; dataMessage?: Uint8Array | null;
decrypted_at?: number; decrypted_at?: number;
deletedForEveryone?: boolean; deletedForEveryone?: boolean;
deletedForEveryoneTimestamp?: number; deletedForEveryoneTimestamp?: number;
@ -355,7 +355,7 @@ export type GroupV2PendingAdminApprovalType = {
}; };
export type VerificationOptions = { export type VerificationOptions = {
key?: null | ArrayBuffer; key?: null | Uint8Array;
viaContactSync?: boolean; viaContactSync?: boolean;
viaStorageServiceSync?: boolean; viaStorageServiceSync?: boolean;
viaSyncMessage?: boolean; viaSyncMessage?: boolean;

View file

@ -16,6 +16,8 @@ import {
} from '../model-types.d'; } from '../model-types.d';
import { AttachmentType, isGIF } from '../types/Attachment'; import { AttachmentType, isGIF } from '../types/Attachment';
import { CallMode, CallHistoryDetailsType } from '../types/Calling'; 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 * as Stickers from '../types/Stickers';
import { CapabilityError } from '../types/errors'; import { CapabilityError } from '../types/errors';
import type { import type {
@ -40,16 +42,10 @@ import { sniffImageMimeType } from '../util/sniffImageMimeType';
import { isValidE164 } from '../util/isValidE164'; import { isValidE164 } from '../util/isValidE164';
import { MIMEType, IMAGE_JPEG, IMAGE_GIF, IMAGE_WEBP } from '../types/MIME'; import { MIMEType, IMAGE_JPEG, IMAGE_GIF, IMAGE_WEBP } from '../types/MIME';
import { UUID } from '../types/UUID'; import { UUID } from '../types/UUID';
import { import { deriveAccessKey, decryptProfileName, decryptProfile } from '../Crypto';
arrayBufferToBase64,
base64ToArrayBuffer,
deriveAccessKey,
fromEncodedBinaryToArrayBuffer,
stringFromBytes,
} from '../Crypto';
import * as Bytes from '../Bytes'; import * as Bytes from '../Bytes';
import { BodyRangesType } from '../types/Util'; import { BodyRangesType } from '../types/Util';
import { getTextWithMentions } from '../util'; import { getTextWithMentions } from '../util/getTextWithMentions';
import { migrateColor } from '../util/migrateColor'; import { migrateColor } from '../util/migrateColor';
import { isNotNil } from '../util/isNotNil'; import { isNotNil } from '../util/isNotNil';
import { dropNull } from '../util/dropNull'; import { dropNull } from '../util/dropNull';
@ -98,14 +94,11 @@ import { getAvatarData } from '../util/getAvatarData';
import { createIdenticon } from '../util/createIdenticon'; import { createIdenticon } from '../util/createIdenticon';
import * as log from '../logging/log'; import * as log from '../logging/log';
// TODO: remove once we move away from ArrayBuffers
const FIXMEU8 = Uint8Array;
/* eslint-disable more/no-then */ /* eslint-disable more/no-then */
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
const { Services, Util } = window.Signal; const { Services, Util } = window.Signal;
const { EmbeddedContact, Message } = window.Signal.Types; const { Message } = window.Signal.Types;
const { const {
deleteAttachmentData, deleteAttachmentData,
doesAttachmentExist, doesAttachmentExist,
@ -1125,7 +1118,7 @@ export class ConversationModel extends window.Backbone
includePendingMembers, includePendingMembers,
extraConversationsForSend, extraConversationsForSend,
}: { }: {
groupChange?: ArrayBuffer; groupChange?: Uint8Array;
includePendingMembers?: boolean; includePendingMembers?: boolean;
extraConversationsForSend?: Array<string>; extraConversationsForSend?: Array<string>;
} = {}): GroupV2InfoType | undefined { } = {}): GroupV2InfoType | undefined {
@ -1143,7 +1136,7 @@ export class ConversationModel extends window.Backbone
includePendingMembers, includePendingMembers,
extraConversationsForSend, 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'); const groupIdString = this.get('groupId');
if (!groupIdString) { if (!groupIdString) {
@ -1173,10 +1166,10 @@ export class ConversationModel extends window.Backbone
} }
if (isGroupV1(this.attributes)) { if (isGroupV1(this.attributes)) {
return fromEncodedBinaryToArrayBuffer(groupIdString); return Bytes.fromBinary(groupIdString);
} }
if (isGroupV2(this.attributes)) { if (isGroupV2(this.attributes)) {
return base64ToArrayBuffer(groupIdString); return Bytes.fromBase64(groupIdString);
} }
return undefined; return undefined;
@ -1253,12 +1246,9 @@ export class ConversationModel extends window.Backbone
} }
async cleanup(): Promise<void> { async cleanup(): Promise<void> {
await window.Signal.Types.Conversation.deleteExternalFiles( await Conversation.deleteExternalFiles(this.attributes, {
this.attributes, deleteAttachmentData,
{ });
deleteAttachmentData,
}
);
} }
async onNewMessage(message: MessageModel): Promise<void> { async onNewMessage(message: MessageModel): Promise<void> {
@ -1838,7 +1828,7 @@ export class ConversationModel extends window.Backbone
if (!error.response) { if (!error.response) {
throw error; throw error;
} else { } else {
const errorDetails = stringFromBytes(error.response); const errorDetails = Bytes.toString(error.response);
if (errorDetails !== ALREADY_REQUESTED_TO_JOIN) { if (errorDetails !== ALREADY_REQUESTED_TO_JOIN) {
throw error; throw error;
} else { } else {
@ -1914,7 +1904,7 @@ export class ConversationModel extends window.Backbone
async updateGroupAttributesV2( async updateGroupAttributesV2(
attributes: Readonly<{ attributes: Readonly<{
avatar?: undefined | ArrayBuffer; avatar?: undefined | Uint8Array;
description?: string; description?: string;
title?: string; title?: string;
}> }>
@ -3340,7 +3330,7 @@ export class ConversationModel extends window.Backbone
const sendOptions = await getSendOptions(this.attributes); const sendOptions = await getSendOptions(this.attributes);
const promise = (async () => { const promise = (async () => {
let profileKey: ArrayBuffer | undefined; let profileKey: Uint8Array | undefined;
if (this.get('profileSharing')) { if (this.get('profileSharing')) {
profileKey = await ourProfileKeyService.get(); profileKey = await ourProfileKeyService.get();
} }
@ -3474,7 +3464,7 @@ export class ConversationModel extends window.Backbone
throw new Error('Cannot send reaction while offline!'); throw new Error('Cannot send reaction while offline!');
} }
let profileKey: ArrayBuffer | undefined; let profileKey: Uint8Array | undefined;
if (this.get('profileSharing')) { if (this.get('profileSharing')) {
profileKey = await ourProfileKeyService.get(); profileKey = await ourProfileKeyService.get();
} }
@ -3905,7 +3895,7 @@ export class ConversationModel extends window.Backbone
return; return;
} }
const groupInviteLinkPassword = arrayBufferToBase64( const groupInviteLinkPassword = Bytes.toBase64(
window.Signal.Groups.generateGroupInviteLinkPassword() window.Signal.Groups.generateGroupInviteLinkPassword()
); );
@ -3932,9 +3922,7 @@ export class ConversationModel extends window.Backbone
value && !this.get('groupInviteLinkPassword'); value && !this.get('groupInviteLinkPassword');
const groupInviteLinkPassword = const groupInviteLinkPassword =
this.get('groupInviteLinkPassword') || this.get('groupInviteLinkPassword') ||
arrayBufferToBase64( Bytes.toBase64(window.Signal.Groups.generateGroupInviteLinkPassword());
window.Signal.Groups.generateGroupInviteLinkPassword()
);
log.info('toggleGroupLink for conversation', this.idForLogging(), value); log.info('toggleGroupLink for conversation', this.idForLogging(), value);
@ -4410,24 +4398,21 @@ export class ConversationModel extends window.Backbone
if (!encryptedName) { if (!encryptedName) {
return; return;
} }
// isn't this already an ArrayBuffer? // isn't this already an Uint8Array?
const key = (this.get('profileKey') as unknown) as string; const key = (this.get('profileKey') as unknown) as string;
if (!key) { if (!key) {
return; return;
} }
// decode // decode
const keyBuffer = base64ToArrayBuffer(key); const keyBuffer = Bytes.fromBase64(key);
// decrypt // decrypt
const { given, family } = await window.textsecure.crypto.decryptProfileName( const { given, family } = decryptProfileName(encryptedName, keyBuffer);
encryptedName,
keyBuffer
);
// encode // encode
const profileName = given ? stringFromBytes(given) : undefined; const profileName = given ? Bytes.toString(given) : undefined;
const profileFamilyName = family ? stringFromBytes(family) : undefined; const profileFamilyName = family ? Bytes.toString(family) : undefined;
// set then check for changes // set then check for changes
const oldName = this.getProfileName(); const oldName = this.getProfileName();
@ -4467,22 +4452,19 @@ export class ConversationModel extends window.Backbone
} }
const avatar = await window.textsecure.messaging.getAvatar(avatarPath); 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; const key = (this.get('profileKey') as unknown) as string;
if (!key) { if (!key) {
return; return;
} }
const keyBuffer = base64ToArrayBuffer(key); const keyBuffer = Bytes.fromBase64(key);
// decrypt // decrypt
const decrypted = await window.textsecure.crypto.decryptProfile( const decrypted = decryptProfile(avatar, keyBuffer);
avatar,
keyBuffer
);
// update the conversation avatar only if hash differs // update the conversation avatar only if hash differs
if (decrypted) { if (decrypted) {
const newAttributes = await window.Signal.Types.Conversation.maybeUpdateProfileAvatar( const newAttributes = await Conversation.maybeUpdateProfileAvatar(
this.attributes, this.attributes,
decrypted, decrypted,
{ {
@ -4540,9 +4522,9 @@ export class ConversationModel extends window.Backbone
return; return;
} }
const profileKeyBuffer = base64ToArrayBuffer(profileKey); const profileKeyBuffer = Bytes.fromBase64(profileKey);
const accessKeyBuffer = await deriveAccessKey(profileKeyBuffer); const accessKeyBuffer = deriveAccessKey(profileKeyBuffer);
const accessKey = arrayBufferToBase64(accessKeyBuffer); const accessKey = Bytes.toBase64(accessKeyBuffer);
this.set({ accessKey }); this.set({ accessKey });
} }

View file

@ -41,8 +41,12 @@ import {
getStickerPackStatus, getStickerPackStatus,
} from '../types/Stickers'; } from '../types/Stickers';
import * as Stickers 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 { AttachmentType, isImage, isVideo } from '../types/Attachment';
import * as Attachment from '../types/Attachment';
import { IMAGE_WEBP, stringToMIMEType } from '../types/MIME'; import { IMAGE_WEBP, stringToMIMEType } from '../types/MIME';
import * as MIME from '../types/MIME';
import { ReadStatus } from '../messages/MessageReadStatus'; import { ReadStatus } from '../messages/MessageReadStatus';
import { import {
SendActionType, SendActionType,
@ -115,6 +119,8 @@ import { normalMessageSendJobQueue } from '../jobs/normalMessageSendJobQueue';
import { notificationService } from '../services/notifications'; import { notificationService } from '../services/notifications';
import type { PreviewType as OutgoingPreviewType } from '../textsecure/SendMessage'; import type { PreviewType as OutgoingPreviewType } from '../textsecure/SendMessage';
import * as log from '../logging/log'; import * as log from '../logging/log';
import * as Bytes from '../Bytes';
import { computeHash } from '../Crypto';
/* eslint-disable camelcase */ /* eslint-disable camelcase */
/* eslint-disable more/no-then */ /* eslint-disable more/no-then */
@ -128,13 +134,7 @@ declare const _: typeof window._;
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
const { const { Message: TypedMessage } = window.Signal.Types;
Message: TypedMessage,
Attachment,
MIME,
EmbeddedContact,
Errors,
} = window.Signal.Types;
const { const {
deleteExternalMessageFiles, deleteExternalMessageFiles,
upgradeMessageSchema, upgradeMessageSchema,
@ -142,7 +142,6 @@ const {
const { getTextWithMentions, GoogleChrome } = window.Signal.Util; const { getTextWithMentions, GoogleChrome } = window.Signal.Util;
const { addStickerPackReference, getMessageBySender } = window.Signal.Data; const { addStickerPackReference, getMessageBySender } = window.Signal.Data;
const { bytesFromString } = window.Signal.Crypto;
export function isQuoteAMatch( export function isQuoteAMatch(
message: MessageModel | null | undefined, message: MessageModel | null | undefined,
@ -1560,7 +1559,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
} }
async sendSyncMessageOnly( async sendSyncMessageOnly(
dataMessage: ArrayBuffer, dataMessage: Uint8Array,
saveErrors?: (errors: Array<Error>) => void saveErrors?: (errors: Array<Error>) => void
): Promise<void> { ): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@ -2691,9 +2690,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
downloadedAvatar downloadedAvatar
); );
hash = await window.Signal.Types.Conversation.computeHash( hash = computeHash(loadedAttachment.data);
loadedAttachment.data
);
} }
} catch (err) { } catch (err) {
log.info('handleDataMessage: group avatar download failed'); log.info('handleDataMessage: group avatar download failed');
@ -2719,7 +2716,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
let avatar = null; let avatar = null;
if (downloadedAvatar && avatarAttachment !== null) { if (downloadedAvatar && avatarAttachment !== null) {
const onDiskAttachment = await window.Signal.Types.Attachment.migrateDataToFileSystem( const onDiskAttachment = await Attachment.migrateDataToFileSystem(
downloadedAvatar, downloadedAvatar,
{ {
writeNewAttachmentData: writeNewAttachmentData:
@ -3328,7 +3325,7 @@ window.Whisper.Message.getLongMessageAttachment = ({
}; };
} }
const data = bytesFromString(body); const data = Bytes.fromString(body);
const attachment = { const attachment = {
contentType: MIME.LONG_MESSAGE, contentType: MIME.LONG_MESSAGE,
fileName: `long-message-${now}.txt`, fileName: `long-message-${now}.txt`,

View file

@ -56,11 +56,7 @@ import { LocalizerType } from '../types/Util';
import { UUID } from '../types/UUID'; import { UUID } from '../types/UUID';
import { ConversationModel } from '../models/conversations'; import { ConversationModel } from '../models/conversations';
import * as Bytes from '../Bytes'; import * as Bytes from '../Bytes';
import { import { uuidToBytes, bytesToUuid } from '../Crypto';
uuidToArrayBuffer,
arrayBufferToUuid,
typedArrayToArrayBuffer,
} from '../Crypto';
import { dropNull, shallowDropNull } from '../util/dropNull'; import { dropNull, shallowDropNull } from '../util/dropNull';
import { getOwn } from '../util/getOwn'; import { getOwn } from '../util/getOwn';
import { isNormalNumber } from '../util/isNormalNumber'; import { isNormalNumber } from '../util/isNormalNumber';
@ -270,7 +266,7 @@ export class CallingClass {
return; return;
} }
RingRTC.setSelfUuid(Buffer.from(uuidToArrayBuffer(ourUuid))); RingRTC.setSelfUuid(Buffer.from(uuidToBytes(ourUuid)));
} }
async startCallingLobby( async startCallingLobby(
@ -480,7 +476,7 @@ export class CallingClass {
return getMembershipList(conversationId).map( return getMembershipList(conversationId).map(
member => member =>
new GroupMemberInfo( new GroupMemberInfo(
Buffer.from(uuidToArrayBuffer(member.uuid)), Buffer.from(uuidToBytes(member.uuid)),
Buffer.from(member.uuidCiphertext) Buffer.from(member.uuidCiphertext)
) )
); );
@ -772,18 +768,16 @@ export class CallingClass {
): GroupCallPeekInfoType { ): GroupCallPeekInfoType {
return { return {
uuids: peekInfo.joinedMembers.map(uuidBuffer => { uuids: peekInfo.joinedMembers.map(uuidBuffer => {
let uuid = arrayBufferToUuid(typedArrayToArrayBuffer(uuidBuffer)); let uuid = bytesToUuid(uuidBuffer);
if (!uuid) { if (!uuid) {
log.error( 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'; uuid = '00000000-0000-0000-0000-000000000000';
} }
return uuid; return uuid;
}), }),
creatorUuid: creatorUuid: peekInfo.creator && bytesToUuid(peekInfo.creator),
peekInfo.creator &&
arrayBufferToUuid(typedArrayToArrayBuffer(peekInfo.creator)),
eraId: peekInfo.eraId, eraId: peekInfo.eraId,
maxDevices: peekInfo.maxDevices ?? Infinity, maxDevices: peekInfo.maxDevices ?? Infinity,
deviceCount: peekInfo.deviceCount, deviceCount: peekInfo.deviceCount,
@ -820,12 +814,10 @@ export class CallingClass {
? this.formatGroupCallPeekInfoForRedux(peekInfo) ? this.formatGroupCallPeekInfoForRedux(peekInfo)
: undefined, : undefined,
remoteParticipants: remoteDeviceStates.map(remoteDeviceState => { remoteParticipants: remoteDeviceStates.map(remoteDeviceState => {
let uuid = arrayBufferToUuid( let uuid = bytesToUuid(remoteDeviceState.userId);
typedArrayToArrayBuffer(remoteDeviceState.userId)
);
if (!uuid) { if (!uuid) {
log.error( 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'; uuid = '00000000-0000-0000-0000-000000000000';
} }
@ -1441,7 +1433,7 @@ export class CallingClass {
} }
const sourceUuid = envelope.sourceUuid const sourceUuid = envelope.sourceUuid
? uuidToArrayBuffer(envelope.sourceUuid) ? uuidToBytes(envelope.sourceUuid)
: null; : null;
const messageAgeSec = envelope.messageAgeSec ? envelope.messageAgeSec : 0; const messageAgeSec = envelope.messageAgeSec ? envelope.messageAgeSec : 0;
@ -1531,7 +1523,7 @@ export class CallingClass {
data: Uint8Array, data: Uint8Array,
urgency: CallMessageUrgency urgency: CallMessageUrgency
): Promise<boolean> { ): Promise<boolean> {
const userId = arrayBufferToUuid(typedArrayToArrayBuffer(recipient)); const userId = bytesToUuid(recipient);
if (!userId) { if (!userId) {
log.error('handleSendCallMessage(): bad recipient UUID'); log.error('handleSendCallMessage(): bad recipient UUID');
return false; return false;
@ -1594,7 +1586,7 @@ export class CallingClass {
const groupId = groupIdBytes.toString('base64'); const groupId = groupIdBytes.toString('base64');
const ringerUuid = arrayBufferToUuid(typedArrayToArrayBuffer(ringerBytes)); const ringerUuid = bytesToUuid(ringerBytes);
if (!ringerUuid) { if (!ringerUuid) {
log.error('handleGroupCallRingUpdate(): ringerUuid was invalid'); log.error('handleGroupCallRingUpdate(): ringerUuid was invalid');
return; return;
@ -1862,7 +1854,7 @@ export class CallingClass {
url, url,
httpMethod, httpMethod,
headers, headers,
body ? typedArrayToArrayBuffer(body) : undefined body
); );
} catch (err) { } catch (err) {
if (err.code !== -1) { if (err.code !== -1) {
@ -1925,7 +1917,10 @@ export class CallingClass {
const isContactUnknown = !conversation.isFromOrAddedByTrustedContact(); const isContactUnknown = !conversation.isFromOrAddedByTrustedContact();
return { return {
iceServer, iceServer: {
...iceServer,
urls: iceServer.urls.slice(),
},
hideIp: shouldRelayCalls || isContactUnknown, hideIp: shouldRelayCalls || isContactUnknown,
bandwidthMode: BandwidthMode.Normal, bandwidthMode: BandwidthMode.Normal,
}; };
@ -2005,9 +2000,7 @@ export class CallingClass {
if (!peekInfo || !peekInfo.eraId || !peekInfo.creator) { if (!peekInfo || !peekInfo.eraId || !peekInfo.creator) {
return; return;
} }
const creatorUuid = arrayBufferToUuid( const creatorUuid = bytesToUuid(peekInfo.creator);
typedArrayToArrayBuffer(peekInfo.creator)
);
if (!creatorUuid) { if (!creatorUuid) {
log.error('updateCallHistoryForGroupCall(): bad creator UUID'); log.error('updateCallHistoryForGroupCall(): bad creator UUID');
return; return;

View file

@ -7,7 +7,7 @@ import * as log from '../logging/log';
import { StorageInterface } from '../types/Storage.d'; import { StorageInterface } from '../types/Storage.d';
export class OurProfileKeyService { export class OurProfileKeyService {
private getPromise: undefined | Promise<undefined | ArrayBuffer>; private getPromise: undefined | Promise<undefined | Uint8Array>;
private promisesBlockingGet: Array<Promise<unknown>> = []; private promisesBlockingGet: Array<Promise<unknown>> = [];
@ -26,7 +26,7 @@ export class OurProfileKeyService {
this.storage = storage; this.storage = storage;
} }
get(): Promise<undefined | ArrayBuffer> { get(): Promise<undefined | Uint8Array> {
if (this.getPromise) { if (this.getPromise) {
log.info( log.info(
'Our profile key service: was already fetching. Piggybacking off of that' 'Our profile key service: was already fetching. Piggybacking off of that'
@ -38,7 +38,7 @@ export class OurProfileKeyService {
return this.getPromise; return this.getPromise;
} }
async set(newValue: undefined | ArrayBuffer): Promise<void> { async set(newValue: undefined | Uint8Array): Promise<void> {
log.info('Our profile key service: updating profile key'); log.info('Our profile key service: updating profile key');
assert(this.storage, 'OurProfileKeyService was not initialized'); assert(this.storage, 'OurProfileKeyService was not initialized');
if (newValue) { if (newValue) {
@ -52,7 +52,7 @@ export class OurProfileKeyService {
this.promisesBlockingGet.push(promise); this.promisesBlockingGet.push(promise);
} }
private async doGet(): Promise<undefined | ArrayBuffer> { private async doGet(): Promise<undefined | Uint8Array> {
log.info( log.info(
`Our profile key service: waiting for ${this.promisesBlockingGet.length} promises before fetching` `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'); log.info('Our profile key service: fetching profile key from storage');
const result = this.storage.get('profileKey'); const result = this.storage.get('profileKey');
if (result === undefined || result instanceof ArrayBuffer) { if (result === undefined || result instanceof Uint8Array) {
return result; return result;
} }
assert( assert(
false, 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; return undefined;
} }

View file

@ -7,7 +7,6 @@ import {
SerializedCertificateType, SerializedCertificateType,
} from '../textsecure/OutgoingMessage'; } from '../textsecure/OutgoingMessage';
import * as Bytes from '../Bytes'; import * as Bytes from '../Bytes';
import { typedArrayToArrayBuffer } from '../Crypto';
import { assert } from '../util/assert'; import { assert } from '../util/assert';
import { missingCaseError } from '../util/missingCaseError'; import { missingCaseError } from '../util/missingCaseError';
import { normalizeNumber } from '../util/normalizeNumber'; import { normalizeNumber } from '../util/normalizeNumber';
@ -177,7 +176,7 @@ export class SenderCertificateService {
const serializedCertificate = { const serializedCertificate = {
expires: expires - CLOCK_SKEW_THRESHOLD, expires: expires - CLOCK_SKEW_THRESHOLD,
serialized: typedArrayToArrayBuffer(certificate), serialized: certificate,
}; };
await storage.put(modeToStorageKey(mode), serializedCertificate); await storage.put(modeToStorageKey(mode), serializedCertificate);

View file

@ -4,15 +4,14 @@
import { debounce, isNumber } from 'lodash'; import { debounce, isNumber } from 'lodash';
import pMap from 'p-map'; import pMap from 'p-map';
import Crypto from '../textsecure/Crypto';
import dataInterface from '../sql/Client'; import dataInterface from '../sql/Client';
import * as Bytes from '../Bytes'; import * as Bytes from '../Bytes';
import { import {
arrayBufferToBase64, getRandomBytes,
base64ToArrayBuffer,
deriveStorageItemKey, deriveStorageItemKey,
deriveStorageManifestKey, deriveStorageManifestKey,
typedArrayToArrayBuffer, encryptProfile,
decryptProfile,
} from '../Crypto'; } from '../Crypto';
import { import {
mergeAccountRecord, mergeAccountRecord,
@ -44,9 +43,6 @@ import * as log from '../logging/log';
type IManifestRecordIdentifier = Proto.ManifestRecord.IIdentifier; type IManifestRecordIdentifier = Proto.ManifestRecord.IIdentifier;
// TODO: remove once we move away from ArrayBuffers
const FIXMEU8 = Uint8Array;
const { const {
eraseStorageServiceStateFromConversations, eraseStorageServiceStateFromConversations,
updateConversation, updateConversation,
@ -94,32 +90,32 @@ async function encryptRecord(
const storageItem = new Proto.StorageItem(); const storageItem = new Proto.StorageItem();
const storageKeyBuffer = storageID const storageKeyBuffer = storageID
? base64ToArrayBuffer(String(storageID)) ? Bytes.fromBase64(String(storageID))
: generateStorageID(); : generateStorageID();
const storageKeyBase64 = window.storage.get('storageKey'); const storageKeyBase64 = window.storage.get('storageKey');
if (!storageKeyBase64) { if (!storageKeyBase64) {
throw new Error('No storage key'); throw new Error('No storage key');
} }
const storageKey = base64ToArrayBuffer(storageKeyBase64); const storageKey = Bytes.fromBase64(storageKeyBase64);
const storageItemKey = await deriveStorageItemKey( const storageItemKey = deriveStorageItemKey(
storageKey, storageKey,
arrayBufferToBase64(storageKeyBuffer) Bytes.toBase64(storageKeyBuffer)
); );
const encryptedRecord = await Crypto.encryptProfile( const encryptedRecord = encryptProfile(
typedArrayToArrayBuffer(Proto.StorageRecord.encode(storageRecord).finish()), Proto.StorageRecord.encode(storageRecord).finish(),
storageItemKey storageItemKey
); );
storageItem.key = new FIXMEU8(storageKeyBuffer); storageItem.key = storageKeyBuffer;
storageItem.value = new FIXMEU8(encryptedRecord); storageItem.value = encryptedRecord;
return storageItem; return storageItem;
} }
function generateStorageID(): ArrayBuffer { function generateStorageID(): Uint8Array {
return Crypto.getRandomBytes(16); return getRandomBytes(16);
} }
type GeneratedManifestType = { type GeneratedManifestType = {
@ -127,7 +123,7 @@ type GeneratedManifestType = {
conversation: ConversationModel; conversation: ConversationModel;
storageID: string | undefined; storageID: string | undefined;
}>; }>;
deleteKeys: Array<ArrayBuffer>; deleteKeys: Array<Uint8Array>;
newItems: Set<Proto.IStorageItem>; newItems: Set<Proto.IStorageItem>;
storageManifest: Proto.IStorageManifest; storageManifest: Proto.IStorageManifest;
}; };
@ -149,7 +145,7 @@ async function generateManifest(
const conversationsToUpdate = []; const conversationsToUpdate = [];
const insertKeys: Array<string> = []; const insertKeys: Array<string> = [];
const deleteKeys: Array<ArrayBuffer> = []; const deleteKeys: Array<Uint8Array> = [];
const manifestRecordKeys: Set<IManifestRecordIdentifier> = new Set(); const manifestRecordKeys: Set<IManifestRecordIdentifier> = new Set();
const newItems: Set<Proto.IStorageItem> = new Set(); const newItems: Set<Proto.IStorageItem> = new Set();
@ -202,7 +198,7 @@ async function generateManifest(
!currentStorageID; !currentStorageID;
const storageID = isNewItem const storageID = isNewItem
? arrayBufferToBase64(generateStorageID()) ? Bytes.toBase64(generateStorageID())
: currentStorageID; : currentStorageID;
let storageItem; let storageItem;
@ -243,7 +239,7 @@ async function generateManifest(
'storageService.generateManifest: deleting key', 'storageService.generateManifest: deleting key',
redactStorageID(oldStorageID) redactStorageID(oldStorageID)
); );
deleteKeys.push(base64ToArrayBuffer(oldStorageID)); deleteKeys.push(Bytes.fromBase64(oldStorageID));
} }
conversationsToUpdate.push({ conversationsToUpdate.push({
@ -323,7 +319,7 @@ async function generateManifest(
// Ensure all deletes are not present in the manifest // Ensure all deletes are not present in the manifest
const hasDeleteKey = deleteKeys.find( const hasDeleteKey = deleteKeys.find(
key => arrayBufferToBase64(key) === storageID key => Bytes.toBase64(key) === storageID
); );
if (hasDeleteKey) { if (hasDeleteKey) {
log.info( log.info(
@ -400,7 +396,7 @@ async function generateManifest(
if (deleteKeys.length !== pendingDeletes.size) { if (deleteKeys.length !== pendingDeletes.size) {
const localDeletes = deleteKeys.map(key => const localDeletes = deleteKeys.map(key =>
redactStorageID(arrayBufferToBase64(key)) redactStorageID(Bytes.toBase64(key))
); );
const remoteDeletes: Array<string> = []; const remoteDeletes: Array<string> = [];
pendingDeletes.forEach(id => remoteDeletes.push(redactStorageID(id))); 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'); throw new Error('invalid write insert items length do not match');
} }
deleteKeys.forEach(key => { deleteKeys.forEach(key => {
const storageID = arrayBufferToBase64(key); const storageID = Bytes.toBase64(key);
if (!pendingDeletes.has(storageID)) { if (!pendingDeletes.has(storageID)) {
throw new Error( throw new Error(
'invalid write delete key missing from pending deletes' 'invalid write delete key missing from pending deletes'
@ -441,21 +437,16 @@ async function generateManifest(
if (!storageKeyBase64) { if (!storageKeyBase64) {
throw new Error('No storage key'); throw new Error('No storage key');
} }
const storageKey = base64ToArrayBuffer(storageKeyBase64); const storageKey = Bytes.fromBase64(storageKeyBase64);
const storageManifestKey = await deriveStorageManifestKey( const storageManifestKey = deriveStorageManifestKey(storageKey, version);
storageKey, const encryptedManifest = encryptProfile(
version Proto.ManifestRecord.encode(manifestRecord).finish(),
);
const encryptedManifest = await Crypto.encryptProfile(
typedArrayToArrayBuffer(
Proto.ManifestRecord.encode(manifestRecord).finish()
),
storageManifestKey storageManifestKey
); );
const storageManifest = new Proto.StorageManifest(); const storageManifest = new Proto.StorageManifest();
storageManifest.version = version; storageManifest.version = version;
storageManifest.value = new FIXMEU8(encryptedManifest); storageManifest.value = encryptedManifest;
return { return {
conversationsToUpdate, conversationsToUpdate,
@ -494,13 +485,11 @@ async function uploadManifest(
const writeOperation = new Proto.WriteOperation(); const writeOperation = new Proto.WriteOperation();
writeOperation.manifest = storageManifest; writeOperation.manifest = storageManifest;
writeOperation.insertItem = Array.from(newItems); writeOperation.insertItem = Array.from(newItems);
writeOperation.deleteKey = deleteKeys.map(key => new FIXMEU8(key)); writeOperation.deleteKey = deleteKeys;
log.info('storageService.uploadManifest: uploading...', version); log.info('storageService.uploadManifest: uploading...', version);
await window.textsecure.messaging.modifyStorageRecords( await window.textsecure.messaging.modifyStorageRecords(
typedArrayToArrayBuffer( Proto.WriteOperation.encode(writeOperation).finish(),
Proto.WriteOperation.encode(writeOperation).finish()
),
{ {
credentials, credentials,
} }
@ -626,19 +615,16 @@ async function decryptManifest(
if (!storageKeyBase64) { if (!storageKeyBase64) {
throw new Error('No storage key'); throw new Error('No storage key');
} }
const storageKey = base64ToArrayBuffer(storageKeyBase64); const storageKey = Bytes.fromBase64(storageKeyBase64);
const storageManifestKey = await deriveStorageManifestKey( const storageManifestKey = deriveStorageManifestKey(
storageKey, storageKey,
normalizeNumber(version ?? 0) normalizeNumber(version ?? 0)
); );
strictAssert(value, 'StorageManifest has no value field'); strictAssert(value, 'StorageManifest has no value field');
const decryptedManifest = await Crypto.decryptProfile( const decryptedManifest = decryptProfile(value, storageManifestKey);
typedArrayToArrayBuffer(value),
storageManifestKey
);
return Proto.ManifestRecord.decode(new FIXMEU8(decryptedManifest)); return Proto.ManifestRecord.decode(decryptedManifest);
} }
async function fetchManifest( async function fetchManifest(
@ -660,9 +646,7 @@ async function fetchManifest(
greaterThanVersion: manifestVersion, greaterThanVersion: manifestVersion,
} }
); );
const encryptedManifest = Proto.StorageManifest.decode( const encryptedManifest = Proto.StorageManifest.decode(manifestBinary);
new FIXMEU8(manifestBinary)
);
// if we don't get a value we're assuming that there's no newer manifest // if we don't get a value we're assuming that there's no newer manifest
if (!encryptedManifest.value || !encryptedManifest.version) { if (!encryptedManifest.value || !encryptedManifest.version) {
@ -855,7 +839,7 @@ async function processRemoteRecords(
if (!storageKeyBase64) { if (!storageKeyBase64) {
throw new Error('No storage key'); throw new Error('No storage key');
} }
const storageKey = base64ToArrayBuffer(storageKeyBase64); const storageKey = Bytes.fromBase64(storageKeyBase64);
log.info( log.info(
'storageService.processRemoteRecords: remote only keys', 'storageService.processRemoteRecords: remote only keys',
@ -869,15 +853,13 @@ async function processRemoteRecords(
const credentials = window.storage.get('storageCredentials'); const credentials = window.storage.get('storageCredentials');
const storageItemsBuffer = await window.textsecure.messaging.getStorageRecords( const storageItemsBuffer = await window.textsecure.messaging.getStorageRecords(
typedArrayToArrayBuffer(Proto.ReadOperation.encode(readOperation).finish()), Proto.ReadOperation.encode(readOperation).finish(),
{ {
credentials, credentials,
} }
); );
const storageItems = Proto.StorageItems.decode( const storageItems = Proto.StorageItems.decode(storageItemsBuffer);
new FIXMEU8(storageItemsBuffer)
);
if (!storageItems.items) { if (!storageItems.items) {
log.info('storageService.processRemoteRecords: No storage items retrieved'); log.info('storageService.processRemoteRecords: No storage items retrieved');
@ -903,15 +885,12 @@ async function processRemoteRecords(
const base64ItemID = Bytes.toBase64(key); const base64ItemID = Bytes.toBase64(key);
const storageItemKey = await deriveStorageItemKey( const storageItemKey = deriveStorageItemKey(storageKey, base64ItemID);
storageKey,
base64ItemID
);
let storageItemPlaintext; let storageItemPlaintext;
try { try {
storageItemPlaintext = await Crypto.decryptProfile( storageItemPlaintext = decryptProfile(
typedArrayToArrayBuffer(storageItemCiphertext), storageItemCiphertext,
storageItemKey storageItemKey
); );
} catch (err) { } catch (err) {
@ -922,9 +901,7 @@ async function processRemoteRecords(
throw err; throw err;
} }
const storageRecord = Proto.StorageRecord.decode( const storageRecord = Proto.StorageRecord.decode(storageItemPlaintext);
new FIXMEU8(storageItemPlaintext)
);
const remoteRecord = remoteOnlyRecords.get(base64ItemID); const remoteRecord = remoteOnlyRecords.get(base64ItemID);
if (!remoteRecord) { if (!remoteRecord) {

View file

@ -4,7 +4,7 @@
import { isEqual, isNumber } from 'lodash'; import { isEqual, isNumber } from 'lodash';
import Long from 'long'; import Long from 'long';
import { deriveMasterKeyFromGroupV1, typedArrayToArrayBuffer } from '../Crypto'; import { deriveMasterKeyFromGroupV1 } from '../Crypto';
import * as Bytes from '../Bytes'; import * as Bytes from '../Bytes';
import dataInterface from '../sql/Client'; import dataInterface from '../sql/Client';
import { import {
@ -43,9 +43,6 @@ import * as log from '../logging/log';
const { updateConversation } = dataInterface; const { updateConversation } = dataInterface;
// TODO: remove once we move away from ArrayBuffers
const FIXMEU8 = Uint8Array;
type RecordClass = type RecordClass =
| Proto.IAccountRecord | Proto.IAccountRecord
| Proto.IContactRecord | Proto.IContactRecord
@ -134,7 +131,7 @@ export async function toContactRecord(
? await window.textsecure.storage.protocol.loadIdentityKey(maybeUuid) ? await window.textsecure.storage.protocol.loadIdentityKey(maybeUuid)
: undefined; : undefined;
if (identityKey) { if (identityKey) {
contactRecord.identityKey = new FIXMEU8(identityKey); contactRecord.identityKey = identityKey;
} }
const verified = conversation.get('verified'); const verified = conversation.get('verified');
if (verified) { if (verified) {
@ -397,13 +394,13 @@ function doRecordsConflict(
const localValue = localRecord[key]; const localValue = localRecord[key];
const remoteValue = remoteRecord[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. // are comparing them both equally by converting them into base64 string.
if (localValue instanceof Uint8Array) { if (localValue instanceof Uint8Array) {
const areEqual = Bytes.areEqual(localValue, remoteValue); const areEqual = Bytes.areEqual(localValue, remoteValue);
if (!areEqual) { if (!areEqual) {
log.info( log.info(
'storageService.doRecordsConflict: Conflict found for ArrayBuffer', 'storageService.doRecordsConflict: Conflict found for Uint8Array',
key, key,
idForLogging idForLogging
); );
@ -525,10 +522,8 @@ export async function mergeGroupV1Record(
// It's possible this group was migrated to a GV2 if so we attempt to // 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 // retrieve the master key and find the conversation locally. If we
// are successful then we continue setting and applying state. // are successful then we continue setting and applying state.
const masterKeyBuffer = await deriveMasterKeyFromGroupV1( const masterKeyBuffer = deriveMasterKeyFromGroupV1(groupV1Record.id);
typedArrayToArrayBuffer(groupV1Record.id) const fields = deriveGroupFields(masterKeyBuffer);
);
const fields = deriveGroupFields(new FIXMEU8(masterKeyBuffer));
const derivedGroupV2Id = Bytes.toBase64(fields.id); const derivedGroupV2Id = Bytes.toBase64(fields.id);
log.info( log.info(
@ -771,9 +766,7 @@ export async function mergeContactRecord(
const storageServiceVerified = contactRecord.identityState || 0; const storageServiceVerified = contactRecord.identityState || 0;
if (verified !== storageServiceVerified) { if (verified !== storageServiceVerified) {
const verifiedOptions = { const verifiedOptions = {
key: contactRecord.identityKey key: contactRecord.identityKey,
? typedArrayToArrayBuffer(contactRecord.identityKey)
: undefined,
viaStorageServiceSync: true, viaStorageServiceSync: true,
}; };
const STATE_ENUM = Proto.ContactRecord.IdentityState; const STATE_ENUM = Proto.ContactRecord.IdentityState;
@ -900,7 +893,7 @@ export async function mergeAccountRecord(
window.storage.put('phoneNumberDiscoverability', discoverability); window.storage.put('phoneNumberDiscoverability', discoverability);
if (profileKey) { if (profileKey) {
ourProfileKeyService.set(typedArrayToArrayBuffer(profileKey)); ourProfileKeyService.set(profileKey);
} }
if (pinnedConversations) { if (pinnedConversations) {

View file

@ -10,7 +10,7 @@ import { handleMessageSend } from '../util/handleMessageSend';
export async function writeProfile( export async function writeProfile(
conversation: ConversationType, conversation: ConversationType,
avatarBuffer?: ArrayBuffer avatarBuffer?: Uint8Array
): Promise<void> { ): Promise<void> {
// Before we write anything we request the user's profile so that we can // 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 // have an up-to-date paymentAddress to be able to include it when we write

View file

@ -24,7 +24,7 @@ import {
uniq, uniq,
} from 'lodash'; } from 'lodash';
import { arrayBufferToBase64, base64ToArrayBuffer } from '../Crypto'; import * as Bytes from '../Bytes';
import { CURRENT_SCHEMA_VERSION } from '../../js/modules/types/message'; import { CURRENT_SCHEMA_VERSION } from '../../js/modules/types/message';
import { createBatcher } from '../util/batcher'; import { createBatcher } from '../util/batcher';
import { assert } from '../util/assert'; import { assert } from '../util/assert';
@ -577,7 +577,7 @@ function makeChannel(fnName: string) {
}; };
} }
function keysToArrayBuffer(keys: Array<string>, data: any) { function keysToBytes(keys: Array<string>, data: any) {
const updated = cloneDeep(data); const updated = cloneDeep(data);
const max = keys.length; const max = keys.length;
@ -586,14 +586,14 @@ function keysToArrayBuffer(keys: Array<string>, data: any) {
const value = get(data, key); const value = get(data, key);
if (value) { if (value) {
set(updated, key, base64ToArrayBuffer(value)); set(updated, key, Bytes.fromBase64(value));
} }
} }
return updated; return updated;
} }
function keysFromArrayBuffer(keys: Array<string>, data: any) { function keysFromBytes(keys: Array<string>, data: any) {
const updated = cloneDeep(data); const updated = cloneDeep(data);
const max = keys.length; const max = keys.length;
@ -602,7 +602,7 @@ function keysFromArrayBuffer(keys: Array<string>, data: any) {
const value = get(data, key); const value = get(data, key);
if (value) { 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']; const IDENTITY_KEY_KEYS = ['publicKey'];
async function createOrUpdateIdentityKey(data: IdentityKeyType) { async function createOrUpdateIdentityKey(data: IdentityKeyType) {
const updated = keysFromArrayBuffer(IDENTITY_KEY_KEYS, data); const updated = keysFromBytes(IDENTITY_KEY_KEYS, data);
await channels.createOrUpdateIdentityKey(updated); await channels.createOrUpdateIdentityKey(updated);
} }
async function getIdentityKeyById(id: IdentityKeyIdType) { async function getIdentityKeyById(id: IdentityKeyIdType) {
const data = await channels.getIdentityKeyById(id); const data = await channels.getIdentityKeyById(id);
return keysToArrayBuffer(IDENTITY_KEY_KEYS, data); return keysToBytes(IDENTITY_KEY_KEYS, data);
} }
async function bulkAddIdentityKeys(array: Array<IdentityKeyType>) { async function bulkAddIdentityKeys(array: Array<IdentityKeyType>) {
const updated = map(array, data => const updated = map(array, data => keysFromBytes(IDENTITY_KEY_KEYS, data));
keysFromArrayBuffer(IDENTITY_KEY_KEYS, data)
);
await channels.bulkAddIdentityKeys(updated); await channels.bulkAddIdentityKeys(updated);
} }
async function removeIdentityKeyById(id: IdentityKeyIdType) { async function removeIdentityKeyById(id: IdentityKeyIdType) {
@ -660,22 +658,22 @@ async function removeAllIdentityKeys() {
async function getAllIdentityKeys() { async function getAllIdentityKeys() {
const keys = await channels.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 // Pre Keys
async function createOrUpdatePreKey(data: PreKeyType) { async function createOrUpdatePreKey(data: PreKeyType) {
const updated = keysFromArrayBuffer(PRE_KEY_KEYS, data); const updated = keysFromBytes(PRE_KEY_KEYS, data);
await channels.createOrUpdatePreKey(updated); await channels.createOrUpdatePreKey(updated);
} }
async function getPreKeyById(id: PreKeyIdType) { async function getPreKeyById(id: PreKeyIdType) {
const data = await channels.getPreKeyById(id); const data = await channels.getPreKeyById(id);
return keysToArrayBuffer(PRE_KEY_KEYS, data); return keysToBytes(PRE_KEY_KEYS, data);
} }
async function bulkAddPreKeys(array: Array<PreKeyType>) { async function bulkAddPreKeys(array: Array<PreKeyType>) {
const updated = map(array, data => keysFromArrayBuffer(PRE_KEY_KEYS, data)); const updated = map(array, data => keysFromBytes(PRE_KEY_KEYS, data));
await channels.bulkAddPreKeys(updated); await channels.bulkAddPreKeys(updated);
} }
async function removePreKeyById(id: PreKeyIdType) { async function removePreKeyById(id: PreKeyIdType) {
@ -687,30 +685,28 @@ async function removeAllPreKeys() {
async function getAllPreKeys() { async function getAllPreKeys() {
const keys = await channels.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 // Signed Pre Keys
const PRE_KEY_KEYS = ['privateKey', 'publicKey']; const PRE_KEY_KEYS = ['privateKey', 'publicKey'];
async function createOrUpdateSignedPreKey(data: SignedPreKeyType) { async function createOrUpdateSignedPreKey(data: SignedPreKeyType) {
const updated = keysFromArrayBuffer(PRE_KEY_KEYS, data); const updated = keysFromBytes(PRE_KEY_KEYS, data);
await channels.createOrUpdateSignedPreKey(updated); await channels.createOrUpdateSignedPreKey(updated);
} }
async function getSignedPreKeyById(id: SignedPreKeyIdType) { async function getSignedPreKeyById(id: SignedPreKeyIdType) {
const data = await channels.getSignedPreKeyById(id); const data = await channels.getSignedPreKeyById(id);
return keysToArrayBuffer(PRE_KEY_KEYS, data); return keysToBytes(PRE_KEY_KEYS, data);
} }
async function getAllSignedPreKeys() { async function getAllSignedPreKeys() {
const keys = await channels.getAllSignedPreKeys(); const keys = await channels.getAllSignedPreKeys();
return keys.map((key: SignedPreKeyType) => return keys.map((key: SignedPreKeyType) => keysToBytes(PRE_KEY_KEYS, key));
keysToArrayBuffer(PRE_KEY_KEYS, key)
);
} }
async function bulkAddSignedPreKeys(array: Array<SignedPreKeyType>) { async function bulkAddSignedPreKeys(array: Array<SignedPreKeyType>) {
const updated = map(array, data => keysFromArrayBuffer(PRE_KEY_KEYS, data)); const updated = map(array, data => keysFromBytes(PRE_KEY_KEYS, data));
await channels.bulkAddSignedPreKeys(updated); await channels.bulkAddSignedPreKeys(updated);
} }
async function removeSignedPreKeyById(id: SignedPreKeyIdType) { async function removeSignedPreKeyById(id: SignedPreKeyIdType) {
@ -736,7 +732,7 @@ async function createOrUpdateItem<K extends ItemKeyType>(data: ItemType<K>) {
} }
const keys = ITEM_KEYS[id]; 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); await channels.createOrUpdateItem(updated);
} }
@ -746,7 +742,7 @@ async function getItemById<K extends ItemKeyType>(
const keys = ITEM_KEYS[id]; const keys = ITEM_KEYS[id];
const data = await channels.getItemById(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() { async function getAllItems() {
const items = await channels.getAllItems(); const items = await channels.getAllItems();
@ -760,7 +756,7 @@ async function getAllItems() {
const keys = ITEM_KEYS[key]; const keys = ITEM_KEYS[key];
const deserializedValue = Array.isArray(keys) const deserializedValue = Array.isArray(keys)
? keysToArrayBuffer(keys, { value }).value ? keysToBytes(keys, { value }).value
: value; : value;
result[key] = deserializedValue; result[key] = deserializedValue;

View file

@ -65,7 +65,7 @@ export type IdentityKeyType = {
firstUse: boolean; firstUse: boolean;
id: UUIDStringType | `conversation:${UUIDStringType}`; id: UUIDStringType | `conversation:${UUIDStringType}`;
nonblockingApproval: boolean; nonblockingApproval: boolean;
publicKey: ArrayBuffer; publicKey: Uint8Array;
timestamp: number; timestamp: number;
verified: number; verified: number;
}; };
@ -85,8 +85,8 @@ export type PreKeyType = {
id: `${UUIDStringType}:${number}`; id: `${UUIDStringType}:${number}`;
keyId: number; keyId: number;
ourUuid: UUIDStringType; ourUuid: UUIDStringType;
privateKey: ArrayBuffer; privateKey: Uint8Array;
publicKey: ArrayBuffer; publicKey: Uint8Array;
}; };
export type PreKeyIdType = PreKeyType['id']; export type PreKeyIdType = PreKeyType['id'];
export type SearchResultMessageType = { export type SearchResultMessageType = {
@ -101,7 +101,7 @@ export type ClientSearchResultMessageType = MessageType & {
export type SentProtoType = { export type SentProtoType = {
contentHint: number; contentHint: number;
proto: Buffer; proto: Uint8Array;
timestamp: number; timestamp: number;
}; };
export type SentProtoWithMessageIdsType = SentProtoType & { export type SentProtoWithMessageIdsType = SentProtoType & {
@ -128,7 +128,7 @@ export type SenderKeyType = {
senderId: string; senderId: string;
distributionId: string; distributionId: string;
// Raw data to serialize/deserialize into signal-client SenderKeyRecord // Raw data to serialize/deserialize into signal-client SenderKeyRecord
data: Buffer; data: Uint8Array;
lastUpdatedDate: number; lastUpdatedDate: number;
}; };
export type SenderKeyIdType = SenderKeyType['id']; export type SenderKeyIdType = SenderKeyType['id'];
@ -149,8 +149,8 @@ export type SignedPreKeyType = {
ourUuid: UUIDStringType; ourUuid: UUIDStringType;
id: `${UUIDStringType}:${number}`; id: `${UUIDStringType}:${number}`;
keyId: number; keyId: number;
privateKey: ArrayBuffer; privateKey: Uint8Array;
publicKey: ArrayBuffer; publicKey: Uint8Array;
}; };
export type SignedPreKeyIdType = SignedPreKeyType['id']; export type SignedPreKeyIdType = SignedPreKeyType['id'];

View file

@ -37,7 +37,7 @@ type CleanedDataValue =
| boolean | boolean
| null | null
| undefined | undefined
| Buffer | Uint8Array
| CleanedObject | CleanedObject
| CleanedArray; | CleanedArray;
/* eslint-disable no-restricted-syntax */ /* eslint-disable no-restricted-syntax */
@ -111,7 +111,7 @@ function cleanDataInner(
return undefined; return undefined;
} }
if (data instanceof Buffer) { if (data instanceof Uint8Array) {
return data; return data;
} }

View file

@ -255,7 +255,7 @@ export enum OneTimeModalState {
} }
type ComposerGroupCreationState = { type ComposerGroupCreationState = {
groupAvatar: undefined | ArrayBuffer; groupAvatar: undefined | Uint8Array;
groupName: string; groupName: string;
groupExpireTimer: number; groupExpireTimer: number;
maximumGroupSizeModalState: OneTimeModalState; maximumGroupSizeModalState: OneTimeModalState;
@ -632,7 +632,7 @@ export type ShowArchivedConversationsActionType = {
}; };
type SetComposeGroupAvatarActionType = { type SetComposeGroupAvatarActionType = {
type: 'SET_COMPOSE_GROUP_AVATAR'; type: 'SET_COMPOSE_GROUP_AVATAR';
payload: { groupAvatar: undefined | ArrayBuffer }; payload: { groupAvatar: undefined | Uint8Array };
}; };
type SetComposeGroupNameActionType = { type SetComposeGroupNameActionType = {
type: 'SET_COMPOSE_GROUP_NAME'; type: 'SET_COMPOSE_GROUP_NAME';
@ -931,7 +931,7 @@ function saveAvatarToDisk(
): ThunkAction<void, RootStateType, unknown, ReplaceAvatarsActionType> { ): ThunkAction<void, RootStateType, unknown, ReplaceAvatarsActionType> {
return async (dispatch, getState) => { return async (dispatch, getState) => {
if (!avatarData.buffer) { if (!avatarData.buffer) {
throw new Error('No avatar ArrayBuffer provided'); throw new Error('No avatar Uint8Array provided');
} }
strictAssert(conversationId, 'conversationId not provided'); strictAssert(conversationId, 'conversationId not provided');
@ -966,7 +966,7 @@ function saveAvatarToDisk(
function myProfileChanged( function myProfileChanged(
profileData: ProfileDataType, profileData: ProfileDataType,
avatarBuffer?: ArrayBuffer avatarBuffer?: Uint8Array
): ThunkAction< ): ThunkAction<
void, void,
RootStateType, RootStateType,
@ -1138,7 +1138,7 @@ function composeSaveAvatarToDisk(
): ThunkAction<void, RootStateType, unknown, ComposeSaveAvatarActionType> { ): ThunkAction<void, RootStateType, unknown, ComposeSaveAvatarActionType> {
return async dispatch => { return async dispatch => {
if (!avatarData.buffer) { if (!avatarData.buffer) {
throw new Error('No avatar ArrayBuffer provided'); throw new Error('No avatar Uint8Array provided');
} }
const imagePath = await window.Signal.Migrations.writeNewAvatarData( const imagePath = await window.Signal.Migrations.writeNewAvatarData(
@ -1601,7 +1601,7 @@ function scrollToMessage(
} }
function setComposeGroupAvatar( function setComposeGroupAvatar(
groupAvatar: undefined | ArrayBuffer groupAvatar: undefined | Uint8Array
): SetComposeGroupAvatarActionType { ): SetComposeGroupAvatarActionType {
return { return {
type: 'SET_COMPOSE_GROUP_AVATAR', type: 'SET_COMPOSE_GROUP_AVATAR',
@ -2884,7 +2884,7 @@ export function reducer(
let recommendedGroupSizeModalState: OneTimeModalState; let recommendedGroupSizeModalState: OneTimeModalState;
let maximumGroupSizeModalState: OneTimeModalState; let maximumGroupSizeModalState: OneTimeModalState;
let groupName: string; let groupName: string;
let groupAvatar: undefined | ArrayBuffer; let groupAvatar: undefined | Uint8Array;
let groupExpireTimer: number; let groupExpireTimer: number;
let userAvatarData = getDefaultAvatars(true); let userAvatarData = getDefaultAvatars(true);

View file

@ -541,7 +541,7 @@ const getGroupCreationComposerState = createSelector(
composerState composerState
): { ): {
groupName: string; groupName: string;
groupAvatar: undefined | ArrayBuffer; groupAvatar: undefined | Uint8Array;
groupExpireTimer: number; groupExpireTimer: number;
selectedConversationIds: Array<string>; selectedConversationIds: Array<string>;
} => { } => {
@ -566,7 +566,7 @@ const getGroupCreationComposerState = createSelector(
export const getComposeGroupAvatar = createSelector( export const getComposeGroupAvatar = createSelector(
getGroupCreationComposerState, getGroupCreationComposerState,
(composerState): undefined | ArrayBuffer => composerState.groupAvatar (composerState): undefined | Uint8Array => composerState.groupAvatar
); );
export const getComposeGroupName = createSelector( export const getComposeGroupName = createSelector(

View file

@ -36,7 +36,7 @@ export type SmartConversationDetailsProps = {
) => void; ) => void;
updateGroupAttributes: ( updateGroupAttributes: (
_: Readonly<{ _: Readonly<{
avatar?: undefined | ArrayBuffer; avatar?: undefined | Uint8Array;
title?: string; title?: string;
}> }>
) => Promise<void>; ) => Promise<void>;

View file

@ -5,7 +5,6 @@ import { assert } from 'chai';
import { Writer } from 'protobufjs'; import { Writer } from 'protobufjs';
import * as Bytes from '../Bytes'; import * as Bytes from '../Bytes';
import { typedArrayToArrayBuffer } from '../Crypto';
import { SignalService as Proto } from '../protobuf'; import { SignalService as Proto } from '../protobuf';
import { ContactBuffer, GroupBuffer } from '../textsecure/ContactsParser'; import { ContactBuffer, GroupBuffer } from '../textsecure/ContactsParser';
@ -19,7 +18,7 @@ describe('ContactsParser', () => {
} }
describe('ContactBuffer', () => { describe('ContactBuffer', () => {
function getTestBuffer(): ArrayBuffer { function getTestBuffer(): Uint8Array {
const avatarBuffer = generateAvatar(); const avatarBuffer = generateAvatar();
const contactInfoBuffer = Proto.ContactDetails.encode({ const contactInfoBuffer = Proto.ContactDetails.encode({
@ -39,12 +38,12 @@ describe('ContactsParser', () => {
chunks.push(avatarBuffer); chunks.push(avatarBuffer);
} }
return typedArrayToArrayBuffer(Bytes.concatenate(chunks)); return Bytes.concatenate(chunks);
} }
it('parses an array buffer of contacts', () => { it('parses an array buffer of contacts', () => {
const arrayBuffer = getTestBuffer(); const bytes = getTestBuffer();
const contactBuffer = new ContactBuffer(arrayBuffer); const contactBuffer = new ContactBuffer(bytes);
let contact = contactBuffer.next(); let contact = contactBuffer.next();
let count = 0; let count = 0;
while (contact !== undefined) { while (contact !== undefined) {
@ -59,7 +58,7 @@ describe('ContactsParser', () => {
assert.strictEqual(contact.avatar?.length, 255); assert.strictEqual(contact.avatar?.length, 255);
assert.strictEqual(contact.avatar?.data.byteLength, 255); assert.strictEqual(contact.avatar?.data.byteLength, 255);
const avatarBytes = new Uint8Array( const avatarBytes = new Uint8Array(
contact.avatar?.data || new ArrayBuffer(0) contact.avatar?.data || new Uint8Array(0)
); );
for (let j = 0; j < 255; j += 1) { for (let j = 0; j < 255; j += 1) {
assert.strictEqual(avatarBytes[j], j); assert.strictEqual(avatarBytes[j], j);
@ -71,7 +70,7 @@ describe('ContactsParser', () => {
}); });
describe('GroupBuffer', () => { describe('GroupBuffer', () => {
function getTestBuffer(): ArrayBuffer { function getTestBuffer(): Uint8Array {
const avatarBuffer = generateAvatar(); const avatarBuffer = generateAvatar();
const groupInfoBuffer = Proto.GroupDetails.encode({ const groupInfoBuffer = Proto.GroupDetails.encode({
@ -91,12 +90,12 @@ describe('ContactsParser', () => {
chunks.push(avatarBuffer); chunks.push(avatarBuffer);
} }
return typedArrayToArrayBuffer(Bytes.concatenate(chunks)); return Bytes.concatenate(chunks);
} }
it('parses an array buffer of groups', () => { it('parses an array buffer of groups', () => {
const arrayBuffer = getTestBuffer(); const bytes = getTestBuffer();
const groupBuffer = new GroupBuffer(arrayBuffer); const groupBuffer = new GroupBuffer(bytes);
let group = groupBuffer.next(); let group = groupBuffer.next();
let count = 0; let count = 0;
while (group !== undefined) { while (group !== undefined) {
@ -113,7 +112,7 @@ describe('ContactsParser', () => {
assert.strictEqual(group.avatar?.length, 255); assert.strictEqual(group.avatar?.length, 255);
assert.strictEqual(group.avatar?.data.byteLength, 255); assert.strictEqual(group.avatar?.data.byteLength, 255);
const avatarBytes = new Uint8Array( const avatarBytes = new Uint8Array(
group.avatar?.data || new ArrayBuffer(0) group.avatar?.data || new Uint8Array(0)
); );
for (let j = 0; j < 255; j += 1) { for (let j = 0; j < 255; j += 1) {
assert.strictEqual(avatarBytes[j], j); assert.strictEqual(avatarBytes[j], j);

View file

@ -6,6 +6,7 @@ import * as sinon from 'sinon';
import { noop } from 'lodash'; import { noop } from 'lodash';
import { sleep } from '../../util/sleep'; import { sleep } from '../../util/sleep';
import { constantTimeEqual } from '../../Crypto';
import { OurProfileKeyService } from '../../services/ourProfileKey'; import { OurProfileKeyService } from '../../services/ourProfileKey';
describe('"our profile key" service', () => { describe('"our profile key" service', () => {
@ -18,14 +19,17 @@ describe('"our profile key" service', () => {
describe('get', () => { describe('get', () => {
it("fetches the key from storage if it's there", async () => { it("fetches the key from storage if it's there", async () => {
const fakeProfileKey = new ArrayBuffer(2); const fakeProfileKey = new Uint8Array(2);
const fakeStorage = createFakeStorage(); const fakeStorage = createFakeStorage();
fakeStorage.get.withArgs('profileKey').returns(fakeProfileKey); fakeStorage.get.withArgs('profileKey').returns(fakeProfileKey);
const service = new OurProfileKeyService(); const service = new OurProfileKeyService();
service.initialize(fakeStorage); 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 () => { it('resolves with undefined if the key is not in storage', async () => {
@ -39,7 +43,7 @@ describe('"our profile key" service', () => {
let onReadyCallback = noop; let onReadyCallback = noop;
const fakeStorage = { const fakeStorage = {
...createFakeStorage(), ...createFakeStorage(),
get: sinon.stub().returns(new ArrayBuffer(2)), get: sinon.stub().returns(new Uint8Array(2)),
onready: sinon.stub().callsFake(callback => { onready: sinon.stub().callsFake(callback => {
onReadyCallback = 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 () => { 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(); const fakeStorage = createFakeStorage();
fakeStorage.get.returns(new ArrayBuffer(2)); fakeStorage.get.returns(new Uint8Array(2));
const service = new OurProfileKeyService(); const service = new OurProfileKeyService();
service.initialize(fakeStorage); service.initialize(fakeStorage);
@ -153,7 +157,7 @@ describe('"our profile key" service', () => {
describe('set', () => { describe('set', () => {
it('updates the key in storage', async () => { it('updates the key in storage', async () => {
const fakeProfileKey = new ArrayBuffer(2); const fakeProfileKey = new Uint8Array(2);
const fakeStorage = createFakeStorage(); const fakeStorage = createFakeStorage();
const service = new OurProfileKeyService(); const service = new OurProfileKeyService();

View file

@ -62,7 +62,7 @@ describe('cleanDataForIpc', () => {
}); });
it('keeps Buffers in a field', () => { it('keeps Buffers in a field', () => {
const buffer = Buffer.from('AABBCC', 'hex'); const buffer = new Uint8Array([0xaa, 0xbb, 0xcc]);
assert.deepEqual(cleanDataForIpc(buffer), { assert.deepEqual(cleanDataForIpc(buffer), {
cleaned: buffer, cleaned: buffer,
@ -85,11 +85,6 @@ describe('cleanDataForIpc', () => {
}); });
it('converts other iterables to arrays', () => { 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])), { assert.deepEqual(cleanDataForIpc(new Float32Array([1, 2, 3])), {
cleaned: [1, 2, 3], cleaned: [1, 2, 3],
pathsChanged: ['root'], pathsChanged: ['root'],

View file

@ -1680,14 +1680,11 @@ describe('both/state/selectors/conversations', () => {
...getEmptyState(), ...getEmptyState(),
composer: { composer: {
...defaultSetGroupMetadataComposerState, ...defaultSetGroupMetadataComposerState,
groupAvatar: new Uint8Array([1, 2, 3]).buffer, groupAvatar: new Uint8Array([1, 2, 3]),
}, },
}, },
}; };
assert.deepEqual( assert.deepEqual(getComposeGroupAvatar(state), new Uint8Array([1, 2, 3]));
getComposeGroupAvatar(state),
new Uint8Array([1, 2, 3]).buffer
);
}); });
}); });

View file

@ -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(''));
});
});
});

View file

@ -5,11 +5,11 @@
import { assert } from 'chai'; import { assert } from 'chai';
import * as Bytes from '../../Bytes';
import { import {
LocalUserDataType, LocalUserDataType,
sessionRecordToProtobuf, sessionRecordToProtobuf,
} from '../../util/sessionTranslation'; } from '../../util/sessionTranslation';
import { base64ToArrayBuffer } from '../../Crypto';
const getRecordCopy = (record: any): any => JSON.parse(JSON.stringify(record)); const getRecordCopy = (record: any): any => JSON.parse(JSON.stringify(record));
@ -18,7 +18,7 @@ describe('sessionTranslation', () => {
beforeEach(() => { beforeEach(() => {
ourData = { ourData = {
identityKeyPublic: base64ToArrayBuffer( identityKeyPublic: Bytes.fromBase64(
'Baioqfzc/5JD6b+GNqapPouf6eHK7xr9ynLJHnvl+444' 'Baioqfzc/5JD6b+GNqapPouf6eHK7xr9ynLJHnvl+444'
), ),
registrationId: 3554, registrationId: 3554,

View file

@ -3,141 +3,160 @@
import { assert } from 'chai'; import { assert } from 'chai';
import * as Bytes from '../Bytes';
import * as Curve from '../Curve'; import * as Curve from '../Curve';
import * as Crypto from '../Crypto'; import {
import TSCrypto, { PaddedLengths } from '../textsecure/Crypto'; 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('Crypto', () => {
describe('encrypting and decrypting profile data', () => { describe('encrypting and decrypting profile data', () => {
const NAME_PADDED_LENGTH = 53; const NAME_PADDED_LENGTH = 53;
describe('encrypting and decrypting profile names', () => { 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 name = 'Alice';
const buffer = Crypto.bytesFromString(name); const buffer = Bytes.fromString(name);
const key = Crypto.getRandomBytes(32); const key = getRandomBytes(32);
const encrypted = await TSCrypto.encryptProfileItemWithPadding( const encrypted = encryptProfileItemWithPadding(
buffer, buffer,
key, key,
PaddedLengths.Name PaddedLengths.Name
); );
assert.equal(encrypted.byteLength, NAME_PADDED_LENGTH + 16 + 12); assert.equal(encrypted.byteLength, NAME_PADDED_LENGTH + 16 + 12);
const { given, family } = await TSCrypto.decryptProfileName( const { given, family } = decryptProfileName(
Crypto.arrayBufferToBase64(encrypted), Bytes.toBase64(encrypted),
key key
); );
assert.strictEqual(family, null); 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 name = '01234567890123456789012345678901234567890123456789123';
const buffer = Crypto.bytesFromString(name); const buffer = Bytes.fromString(name);
const key = Crypto.getRandomBytes(32); const key = getRandomBytes(32);
const encrypted = await TSCrypto.encryptProfileItemWithPadding( const encrypted = encryptProfileItemWithPadding(
buffer, buffer,
key, key,
PaddedLengths.Name PaddedLengths.Name
); );
assert.equal(encrypted.byteLength, NAME_PADDED_LENGTH + 16 + 12); assert.equal(encrypted.byteLength, NAME_PADDED_LENGTH + 16 + 12);
const { given, family } = await TSCrypto.decryptProfileName( const { given, family } = decryptProfileName(
Crypto.arrayBufferToBase64(encrypted), Bytes.toBase64(encrypted),
key key
); );
assert.strictEqual(Crypto.stringFromBytes(given), name); assert.strictEqual(Bytes.toString(given), name);
assert.strictEqual(family, null); 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 = const name =
'01234567890123456789\u000001234567890123456789012345678912'; '01234567890123456789\u000001234567890123456789012345678912';
const buffer = Crypto.bytesFromString(name); const buffer = Bytes.fromString(name);
const key = Crypto.getRandomBytes(32); const key = getRandomBytes(32);
const encrypted = await TSCrypto.encryptProfileItemWithPadding( const encrypted = encryptProfileItemWithPadding(
buffer, buffer,
key, key,
PaddedLengths.Name PaddedLengths.Name
); );
assert.equal(encrypted.byteLength, NAME_PADDED_LENGTH + 16 + 12); assert.equal(encrypted.byteLength, NAME_PADDED_LENGTH + 16 + 12);
const { given, family } = await TSCrypto.decryptProfileName( const { given, family } = decryptProfileName(
Crypto.arrayBufferToBase64(encrypted), Bytes.toBase64(encrypted),
key key
); );
assert.strictEqual(Bytes.toString(given), '01234567890123456789');
assert.strictEqual( assert.strictEqual(
Crypto.stringFromBytes(given), family && Bytes.toString(family),
'01234567890123456789'
);
assert.strictEqual(
family && Crypto.stringFromBytes(family),
'01234567890123456789012345678912' '01234567890123456789012345678912'
); );
}); });
it('handles a string with family/given name', async () => { it('handles a string with family/given name', () => {
const name = 'Alice\0Jones'; const name = 'Alice\0Jones';
const buffer = Crypto.bytesFromString(name); const buffer = Bytes.fromString(name);
const key = Crypto.getRandomBytes(32); const key = getRandomBytes(32);
const encrypted = await TSCrypto.encryptProfileItemWithPadding( const encrypted = encryptProfileItemWithPadding(
buffer, buffer,
key, key,
PaddedLengths.Name PaddedLengths.Name
); );
assert.equal(encrypted.byteLength, NAME_PADDED_LENGTH + 16 + 12); assert.equal(encrypted.byteLength, NAME_PADDED_LENGTH + 16 + 12);
const { given, family } = await TSCrypto.decryptProfileName( const { given, family } = decryptProfileName(
Crypto.arrayBufferToBase64(encrypted), Bytes.toBase64(encrypted),
key key
); );
assert.strictEqual(Crypto.stringFromBytes(given), 'Alice'); assert.strictEqual(Bytes.toString(given), 'Alice');
assert.strictEqual(family && Crypto.stringFromBytes(family), 'Jones'); assert.strictEqual(family && Bytes.toString(family), 'Jones');
}); });
it('works for empty string', async () => { it('works for empty string', () => {
const name = Crypto.bytesFromString(''); const name = Bytes.fromString('');
const key = Crypto.getRandomBytes(32); const key = getRandomBytes(32);
const encrypted = await TSCrypto.encryptProfileItemWithPadding( const encrypted = encryptProfileItemWithPadding(
name, name,
key, key,
PaddedLengths.Name PaddedLengths.Name
); );
assert.equal(encrypted.byteLength, NAME_PADDED_LENGTH + 16 + 12); assert.equal(encrypted.byteLength, NAME_PADDED_LENGTH + 16 + 12);
const { given, family } = await TSCrypto.decryptProfileName( const { given, family } = decryptProfileName(
Crypto.arrayBufferToBase64(encrypted), Bytes.toBase64(encrypted),
key key
); );
assert.strictEqual(family, null); assert.strictEqual(family, null);
assert.strictEqual(given.byteLength, 0); assert.strictEqual(given.byteLength, 0);
assert.strictEqual(Crypto.stringFromBytes(given), ''); assert.strictEqual(Bytes.toString(given), '');
}); });
}); });
describe('encrypting and decrypting profile avatars', () => { describe('encrypting and decrypting profile avatars', () => {
it('encrypts and decrypts', async () => { it('encrypts and decrypts', async () => {
const buffer = Crypto.bytesFromString('This is an avatar'); const buffer = Bytes.fromString('This is an avatar');
const key = Crypto.getRandomBytes(32); const key = getRandomBytes(32);
const encrypted = await TSCrypto.encryptProfile(buffer, key); const encrypted = encryptProfile(buffer, key);
assert(encrypted.byteLength === buffer.byteLength + 16 + 12); assert(encrypted.byteLength === buffer.byteLength + 16 + 12);
const decrypted = await TSCrypto.decryptProfile(encrypted, key); const decrypted = decryptProfile(encrypted, key);
assert(Crypto.constantTimeEqual(buffer, decrypted)); assert(constantTimeEqual(buffer, decrypted));
}); });
it('throws when decrypting with the wrong key', async () => { it('throws when decrypting with the wrong key', () => {
const buffer = Crypto.bytesFromString('This is an avatar'); const buffer = Bytes.fromString('This is an avatar');
const key = Crypto.getRandomBytes(32); const key = getRandomBytes(32);
const badKey = Crypto.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); assert(encrypted.byteLength === buffer.byteLength + 16 + 12);
await assert.isRejected( assert.throws(
TSCrypto.decryptProfile(encrypted, badKey), () => decryptProfile(encrypted, badKey),
'Failed to decrypt profile data. Most likely the profile key has changed.' 'Failed to decrypt profile data. Most likely the profile key has changed.'
); );
}); });
@ -147,7 +166,7 @@ describe('Crypto', () => {
describe('generateRegistrationId', () => { describe('generateRegistrationId', () => {
it('generates an integer between 0 and 16383 (inclusive)', () => { it('generates an integer between 0 and 16383 (inclusive)', () => {
for (let i = 0; i < 100; i += 1) { for (let i = 0; i < 100; i += 1) {
const id = Crypto.generateRegistrationId(); const id = generateRegistrationId();
assert.isAtLeast(id, 0); assert.isAtLeast(id, 0);
assert.isAtMost(id, 16383); assert.isAtMost(id, 16383);
assert(Number.isInteger(id)); assert(Number.isInteger(id));
@ -157,27 +176,27 @@ describe('Crypto', () => {
describe('deriveSecrets', () => { describe('deriveSecrets', () => {
it('derives key parts via HKDF', () => { it('derives key parts via HKDF', () => {
const input = Crypto.getRandomBytes(32); const input = getRandomBytes(32);
const salt = Crypto.getRandomBytes(32); const salt = getRandomBytes(32);
const info = Crypto.bytesFromString('Hello world'); const info = Bytes.fromString('Hello world');
const result = Crypto.deriveSecrets(input, salt, info); const result = deriveSecrets(input, salt, info);
assert.lengthOf(result, 3); assert.lengthOf(result, 3);
result.forEach(part => { result.forEach(part => {
// This is a smoke test; HKDF is tested as part of @signalapp/signal-client. // 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); assert.strictEqual(part.byteLength, 32);
}); });
}); });
}); });
describe('accessKey/profileKey', () => { describe('accessKey/profileKey', () => {
it('verification roundtrips', async () => { it('verification roundtrips', () => {
const profileKey = await Crypto.getRandomBytes(32); const profileKey = getRandomBytes(32);
const accessKey = await Crypto.deriveAccessKey(profileKey); 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); assert.strictEqual(correct, true);
}); });
@ -208,12 +227,12 @@ describe('Crypto', () => {
]; ];
vectors.forEach((vector, index) => { vectors.forEach((vector, index) => {
it(`vector ${index}`, async () => { it(`vector ${index}`, () => {
const gv1 = Crypto.hexToArrayBuffer(vector.gv1); const gv1 = Bytes.fromHex(vector.gv1);
const expectedHex = vector.masterKey; const expectedHex = vector.masterKey;
const actual = await Crypto.deriveMasterKeyFromGroupV1(gv1); const actual = deriveMasterKeyFromGroupV1(gv1);
const actualHex = Crypto.arrayBufferToHex(actual); const actualHex = Bytes.toHex(actual);
assert.strictEqual(actualHex, expectedHex); assert.strictEqual(actualHex, expectedHex);
}); });
@ -221,34 +240,30 @@ describe('Crypto', () => {
}); });
describe('symmetric encryption', () => { describe('symmetric encryption', () => {
it('roundtrips', async () => { it('roundtrips', () => {
const message = 'this is my message'; const message = 'this is my message';
const plaintext = Crypto.bytesFromString(message); const plaintext = Bytes.fromString(message);
const key = Crypto.getRandomBytes(32); const key = getRandomBytes(32);
const encrypted = await Crypto.encryptSymmetric(key, plaintext); const encrypted = encryptSymmetric(key, plaintext);
const decrypted = await Crypto.decryptSymmetric(key, encrypted); const decrypted = decryptSymmetric(key, encrypted);
const equal = Crypto.constantTimeEqual(plaintext, decrypted); const equal = constantTimeEqual(plaintext, decrypted);
if (!equal) { if (!equal) {
throw new Error('The output and input did not match!'); 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 message = 'this is my message';
const plaintext = Crypto.bytesFromString(message); const plaintext = Bytes.fromString(message);
const key = Crypto.getRandomBytes(32); const key = getRandomBytes(32);
const encrypted = await Crypto.encryptSymmetric(key, plaintext); const encrypted = encryptSymmetric(key, plaintext);
const uintArray = new Uint8Array(encrypted); encrypted[2] += 2;
uintArray[2] += 2;
try { try {
await Crypto.decryptSymmetric( decryptSymmetric(key, encrypted);
key,
Crypto.typedArrayToArrayBuffer(uintArray)
);
} catch (error) { } catch (error) {
assert.strictEqual( assert.strictEqual(
error.message, error.message,
@ -260,20 +275,16 @@ describe('Crypto', () => {
throw new Error('Expected error to be thrown'); 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 message = 'this is my message';
const plaintext = Crypto.bytesFromString(message); const plaintext = Bytes.fromString(message);
const key = Crypto.getRandomBytes(32); const key = getRandomBytes(32);
const encrypted = await Crypto.encryptSymmetric(key, plaintext); const encrypted = encryptSymmetric(key, plaintext);
const uintArray = new Uint8Array(encrypted); encrypted[encrypted.length - 3] += 2;
uintArray[uintArray.length - 3] += 2;
try { try {
await Crypto.decryptSymmetric( decryptSymmetric(key, encrypted);
key,
Crypto.typedArrayToArrayBuffer(uintArray)
);
} catch (error) { } catch (error) {
assert.strictEqual( assert.strictEqual(
error.message, error.message,
@ -285,20 +296,16 @@ describe('Crypto', () => {
throw new Error('Expected error to be thrown'); 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 message = 'this is my message';
const plaintext = Crypto.bytesFromString(message); const plaintext = Bytes.fromString(message);
const key = Crypto.getRandomBytes(32); const key = getRandomBytes(32);
const encrypted = await Crypto.encryptSymmetric(key, plaintext); const encrypted = encryptSymmetric(key, plaintext);
const uintArray = new Uint8Array(encrypted); encrypted[35] += 9;
uintArray[35] += 9;
try { try {
await Crypto.decryptSymmetric( decryptSymmetric(key, encrypted);
key,
Crypto.typedArrayToArrayBuffer(uintArray)
);
} catch (error) { } catch (error) {
assert.strictEqual( assert.strictEqual(
error.message, error.message,
@ -312,33 +319,24 @@ describe('Crypto', () => {
}); });
describe('encrypted device name', () => { describe('encrypted device name', () => {
it('roundtrips', async () => { it('roundtrips', () => {
const deviceName = 'v1.19.0 on Windows 10'; const deviceName = 'v1.19.0 on Windows 10';
const identityKey = Curve.generateKeyPair(); const identityKey = Curve.generateKeyPair();
const encrypted = await Crypto.encryptDeviceName( const encrypted = encryptDeviceName(deviceName, identityKey.pubKey);
deviceName, const decrypted = decryptDeviceName(encrypted, identityKey.privKey);
identityKey.pubKey
);
const decrypted = await Crypto.decryptDeviceName(
encrypted,
identityKey.privKey
);
assert.strictEqual(decrypted, deviceName); 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 deviceName = 'v1.19.0 on Windows 10';
const identityKey = Curve.generateKeyPair(); const identityKey = Curve.generateKeyPair();
const encrypted = await Crypto.encryptDeviceName( const encrypted = encryptDeviceName(deviceName, identityKey.pubKey);
deviceName, encrypted.syntheticIv = getRandomBytes(16);
identityKey.pubKey
);
encrypted.syntheticIv = Crypto.getRandomBytes(16);
try { try {
await Crypto.decryptDeviceName(encrypted, identityKey.privKey); decryptDeviceName(encrypted, identityKey.privKey);
} catch (error) { } catch (error) {
assert.strictEqual( assert.strictEqual(
error.message, 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', () => { describe('verifyHmacSha256', () => {
it('rejects if their MAC is too short', async () => { it('rejects if their MAC is too short', () => {
const key = Crypto.getRandomBytes(32); const key = getRandomBytes(32);
const plaintext = Crypto.bytesFromString('Hello world'); const plaintext = Bytes.fromString('Hello world');
const ourMac = await Crypto.hmacSha256(key, plaintext); const ourMac = hmacSha256(key, plaintext);
const theirMac = ourMac.slice(0, -1); const theirMac = ourMac.slice(0, -1);
let error; let error;
try { try {
await Crypto.verifyHmacSha256( verifyHmacSha256(plaintext, key, theirMac, ourMac.byteLength);
plaintext,
key,
theirMac,
ourMac.byteLength
);
} catch (err) { } catch (err) {
error = err; error = err;
} }
@ -395,19 +362,14 @@ describe('Crypto', () => {
assert.strictEqual(error.message, 'Bad MAC length'); assert.strictEqual(error.message, 'Bad MAC length');
}); });
it('rejects if their MAC is too long', async () => { it('rejects if their MAC is too long', () => {
const key = Crypto.getRandomBytes(32); const key = getRandomBytes(32);
const plaintext = Crypto.bytesFromString('Hello world'); const plaintext = Bytes.fromString('Hello world');
const ourMac = await Crypto.hmacSha256(key, plaintext); const ourMac = hmacSha256(key, plaintext);
const theirMac = Crypto.concatenateBytes(ourMac, new Uint8Array([0xff])); const theirMac = Bytes.concatenate([ourMac, new Uint8Array([0xff])]);
let error; let error;
try { try {
await Crypto.verifyHmacSha256( verifyHmacSha256(plaintext, key, theirMac, ourMac.byteLength);
plaintext,
key,
theirMac,
ourMac.byteLength
);
} catch (err) { } catch (err) {
error = err; error = err;
} }
@ -415,19 +377,14 @@ describe('Crypto', () => {
assert.strictEqual(error.message, 'Bad MAC length'); assert.strictEqual(error.message, 'Bad MAC length');
}); });
it('rejects if our MAC is shorter than the specified length', async () => { it('rejects if our MAC is shorter than the specified length', () => {
const key = Crypto.getRandomBytes(32); const key = getRandomBytes(32);
const plaintext = Crypto.bytesFromString('Hello world'); const plaintext = Bytes.fromString('Hello world');
const ourMac = await Crypto.hmacSha256(key, plaintext); const ourMac = hmacSha256(key, plaintext);
const theirMac = ourMac; const theirMac = ourMac;
let error; let error;
try { try {
await Crypto.verifyHmacSha256( verifyHmacSha256(plaintext, key, theirMac, ourMac.byteLength + 1);
plaintext,
key,
theirMac,
ourMac.byteLength + 1
);
} catch (err) { } catch (err) {
error = err; error = err;
} }
@ -435,20 +392,15 @@ describe('Crypto', () => {
assert.strictEqual(error.message, 'Bad MAC length'); assert.strictEqual(error.message, 'Bad MAC length');
}); });
it("rejects if the MACs don't match", async () => { it("rejects if the MACs don't match", () => {
const plaintext = Crypto.bytesFromString('Hello world'); const plaintext = Bytes.fromString('Hello world');
const ourKey = Crypto.getRandomBytes(32); const ourKey = getRandomBytes(32);
const ourMac = await Crypto.hmacSha256(ourKey, plaintext); const ourMac = hmacSha256(ourKey, plaintext);
const theirKey = Crypto.getRandomBytes(32); const theirKey = getRandomBytes(32);
const theirMac = await Crypto.hmacSha256(theirKey, plaintext); const theirMac = hmacSha256(theirKey, plaintext);
let error; let error;
try { try {
await Crypto.verifyHmacSha256( verifyHmacSha256(plaintext, ourKey, theirMac, ourMac.byteLength);
plaintext,
ourKey,
theirMac,
ourMac.byteLength
);
} catch (err) { } catch (err) {
error = err; error = err;
} }
@ -456,11 +408,11 @@ describe('Crypto', () => {
assert.strictEqual(error.message, 'Bad MAC'); assert.strictEqual(error.message, 'Bad MAC');
}); });
it('resolves with undefined if the MACs match exactly', async () => { it('resolves with undefined if the MACs match exactly', () => {
const key = Crypto.getRandomBytes(32); const key = getRandomBytes(32);
const plaintext = Crypto.bytesFromString('Hello world'); const plaintext = Bytes.fromString('Hello world');
const theirMac = await Crypto.hmacSha256(key, plaintext); const theirMac = hmacSha256(key, plaintext);
const result = await Crypto.verifyHmacSha256( const result = verifyHmacSha256(
plaintext, plaintext,
key, key,
theirMac, theirMac,
@ -469,11 +421,11 @@ describe('Crypto', () => {
assert.isUndefined(result); assert.isUndefined(result);
}); });
it('resolves with undefined if the first `length` bytes of the MACs match', async () => { it('resolves with undefined if the first `length` bytes of the MACs match', () => {
const key = Crypto.getRandomBytes(32); const key = getRandomBytes(32);
const plaintext = Crypto.bytesFromString('Hello world'); const plaintext = Bytes.fromString('Hello world');
const theirMac = (await Crypto.hmacSha256(key, plaintext)).slice(0, -5); const theirMac = hmacSha256(key, plaintext).slice(0, -5);
const result = await Crypto.verifyHmacSha256( const result = verifyHmacSha256(
plaintext, plaintext,
key, key,
theirMac, theirMac,
@ -483,102 +435,86 @@ describe('Crypto', () => {
}); });
}); });
describe('uuidToArrayBuffer', () => { describe('uuidToBytes', () => {
const { uuidToArrayBuffer } = Crypto; it('converts valid UUIDs to Uint8Arrays', () => {
const expectedResult = new Uint8Array([
it('converts valid UUIDs to ArrayBuffers', () => { 0x22,
const expectedResult = Crypto.typedArrayToArrayBuffer( 0x6e,
new Uint8Array([ 0x44,
0x22, 0x02,
0x6e, 0x7f,
0x44, 0xfc,
0x02, 0x45,
0x7f, 0x43,
0xfc, 0x85,
0x45, 0xc9,
0x43, 0x46,
0x85, 0x22,
0xc9, 0xc5,
0x46, 0x0a,
0x22, 0x5b,
0xc5, 0x14,
0x0a, ]);
0x5b,
0x14,
])
);
assert.deepEqual( assert.deepEqual(
uuidToArrayBuffer('226e4402-7ffc-4543-85c9-4622c50a5b14'), uuidToBytes('226e4402-7ffc-4543-85c9-4622c50a5b14'),
expectedResult expectedResult
); );
assert.deepEqual( assert.deepEqual(
uuidToArrayBuffer('226E4402-7FFC-4543-85C9-4622C50A5B14'), uuidToBytes('226E4402-7FFC-4543-85C9-4622C50A5B14'),
expectedResult expectedResult
); );
}); });
it('returns an empty ArrayBuffer for strings of the wrong length', () => { it('returns an empty Uint8Array for strings of the wrong length', () => {
assert.deepEqual(uuidToArrayBuffer(''), new ArrayBuffer(0)); assert.deepEqual(uuidToBytes(''), new Uint8Array(0));
assert.deepEqual(uuidToArrayBuffer('abc'), new ArrayBuffer(0)); assert.deepEqual(uuidToBytes('abc'), new Uint8Array(0));
assert.deepEqual( assert.deepEqual(
uuidToArrayBuffer('032deadf0d5e4ee78da28e75b1dfb284'), uuidToBytes('032deadf0d5e4ee78da28e75b1dfb284'),
new ArrayBuffer(0) new Uint8Array(0)
); );
assert.deepEqual( assert.deepEqual(
uuidToArrayBuffer('deaed5eb-d983-456a-a954-9ad7a006b271aaaaaaaaaa'), uuidToBytes('deaed5eb-d983-456a-a954-9ad7a006b271aaaaaaaaaa'),
new ArrayBuffer(0) new Uint8Array(0)
); );
}); });
}); });
describe('arrayBufferToUuid', () => { describe('bytesToUuid', () => {
const { arrayBufferToUuid } = Crypto; it('converts valid Uint8Arrays to UUID strings', () => {
const buf = new Uint8Array([
it('converts valid ArrayBuffers to UUID strings', () => { 0x22,
const buf = Crypto.typedArrayToArrayBuffer( 0x6e,
new Uint8Array([ 0x44,
0x22, 0x02,
0x6e, 0x7f,
0x44, 0xfc,
0x02, 0x45,
0x7f, 0x43,
0xfc, 0x85,
0x45, 0xc9,
0x43, 0x46,
0x85, 0x22,
0xc9, 0xc5,
0x46, 0x0a,
0x22, 0x5b,
0xc5, 0x14,
0x0a, ]);
0x5b,
0x14,
])
);
assert.deepEqual( assert.deepEqual(
arrayBufferToUuid(buf), bytesToUuid(buf),
'226e4402-7ffc-4543-85c9-4622c50a5b14' '226e4402-7ffc-4543-85c9-4622c50a5b14'
); );
}); });
it('returns undefined if passed an all-zero buffer', () => { 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', () => { it('returns undefined if passed the wrong number of bytes', () => {
assert.isUndefined(arrayBufferToUuid(new ArrayBuffer(0))); assert.isUndefined(bytesToUuid(new Uint8Array(0)));
assert.isUndefined( assert.isUndefined(bytesToUuid(new Uint8Array([0x22])));
arrayBufferToUuid( assert.isUndefined(bytesToUuid(new Uint8Array(Array(17).fill(0x22))));
Crypto.typedArrayToArrayBuffer(new Uint8Array([0x22]))
)
);
assert.isUndefined(
arrayBufferToUuid(
Crypto.typedArrayToArrayBuffer(new Uint8Array(Array(17).fill(0x22)))
)
);
}); });
}); });
}); });

View file

@ -3,18 +3,12 @@
import { assert } from 'chai'; import { assert } from 'chai';
import { import * as Bytes from '../Bytes';
arrayBufferToHex, import { constantTimeEqual } from '../Crypto';
constantTimeEqual,
getRandomBytes,
hexToArrayBuffer,
typedArrayToArrayBuffer,
} from '../Crypto';
import { import {
calculateSignature, calculateSignature,
clampPrivateKey, clampPrivateKey,
createKeyPair, createKeyPair,
copyArrayBuffer,
calculateAgreement, calculateAgreement,
generateKeyPair, generateKeyPair,
generatePreKey, generatePreKey,
@ -25,7 +19,7 @@ import {
describe('Curve', () => { describe('Curve', () => {
it('verifySignature roundtrip', () => { it('verifySignature roundtrip', () => {
const message = typedArrayToArrayBuffer(Buffer.from('message')); const message = Buffer.from('message');
const { pubKey, privKey } = generateKeyPair(); const { pubKey, privKey } = generateKeyPair();
const signature = calculateSignature(privKey, message); const signature = calculateSignature(privKey, message);
const verified = verifySignature(pubKey, message, signature); 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', () => { describe('#createKeyPair', () => {
it('does not modify unclamped private key', () => { it('does not modify unclamped private key', () => {
const initialHex = const initialHex =
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
const privateKey = hexToArrayBuffer(initialHex); const privateKey = Bytes.fromHex(initialHex);
const copyOfPrivateKey = copyArrayBuffer(privateKey); const copyOfPrivateKey = new Uint8Array(privateKey);
assert.isTrue( assert.isTrue(
constantTimeEqual(privateKey, copyOfPrivateKey), constantTimeEqual(privateKey, copyOfPrivateKey),
@ -143,7 +107,7 @@ describe('Curve', () => {
// But the keypair that comes out has indeed been updated // But the keypair that comes out has indeed been updated
assert.notEqual( assert.notEqual(
initialHex, initialHex,
arrayBufferToHex(keyPair.privKey), Bytes.toHex(keyPair.privKey),
'keypair check' 'keypair check'
); );
assert.isFalse( assert.isFalse(
@ -155,10 +119,10 @@ describe('Curve', () => {
it('does not modify clamped private key', () => { it('does not modify clamped private key', () => {
const initialHex = const initialHex =
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
const privateKey = hexToArrayBuffer(initialHex); const privateKey = Bytes.fromHex(initialHex);
clampPrivateKey(privateKey); clampPrivateKey(privateKey);
const postClampHex = arrayBufferToHex(privateKey); const postClampHex = Bytes.toHex(privateKey);
const copyOfPrivateKey = copyArrayBuffer(privateKey); const copyOfPrivateKey = new Uint8Array(privateKey);
assert.notEqual(postClampHex, initialHex, 'initial clamp check'); assert.notEqual(postClampHex, initialHex, 'initial clamp check');
assert.isTrue( assert.isTrue(
@ -178,11 +142,7 @@ describe('Curve', () => {
); );
// The keypair that comes out hasn't been updated either // The keypair that comes out hasn't been updated either
assert.equal( assert.equal(postClampHex, Bytes.toHex(keyPair.privKey), 'keypair check');
postClampHex,
arrayBufferToHex(keyPair.privKey),
'keypair check'
);
assert.isTrue( assert.isTrue(
constantTimeEqual(privateKey, keyPair.privKey), constantTimeEqual(privateKey, keyPair.privKey),
'keypair vs incoming value' 'keypair vs incoming value'

View file

@ -15,9 +15,6 @@ import { DecryptionErrorEvent } from '../textsecure/messageReceiverEvents';
import { SignalService as Proto } from '../protobuf'; import { SignalService as Proto } from '../protobuf';
import * as Crypto from '../Crypto'; import * as Crypto from '../Crypto';
// TODO: remove once we move away from ArrayBuffers
const FIXMEU8 = Uint8Array;
describe('MessageReceiver', () => { describe('MessageReceiver', () => {
const number = '+19999999999'; const number = '+19999999999';
const uuid = 'aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee'; const uuid = 'aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee';
@ -54,7 +51,7 @@ describe('MessageReceiver', () => {
sourceUuid: uuid, sourceUuid: uuid,
sourceDevice: deviceId, sourceDevice: deviceId,
timestamp: Date.now(), timestamp: Date.now(),
content: new FIXMEU8(Crypto.getRandomBytes(200)), content: Crypto.getRandomBytes(200),
}).finish(); }).finish();
messageReceiver.handleRequest( messageReceiver.handleRequest(

View file

@ -13,15 +13,11 @@ import {
import { v4 as getGuid } from 'uuid'; import { v4 as getGuid } from 'uuid';
import { signal } from '../protobuf/compiled'; import { signal } from '../protobuf/compiled';
import { sessionStructureToArrayBuffer } from '../util/sessionTranslation'; import { sessionStructureToBytes } from '../util/sessionTranslation';
import { Zone } from '../util/Zone'; import { Zone } from '../util/Zone';
import { import * as Bytes from '../Bytes';
getRandomBytes, import { getRandomBytes, constantTimeEqual } from '../Crypto';
constantTimeEqual,
typedArrayToArrayBuffer as toArrayBuffer,
arrayBufferToBase64 as toBase64,
} from '../Crypto';
import { clampPrivateKey, setPublicKeyTypeByte } from '../Curve'; import { clampPrivateKey, setPublicKeyTypeByte } from '../Curve';
import { GLOBAL_ZONE, SignalProtocolStore } from '../SignalProtocolStore'; import { GLOBAL_ZONE, SignalProtocolStore } from '../SignalProtocolStore';
import { Address } from '../types/Address'; import { Address } from '../types/Address';
@ -53,20 +49,20 @@ describe('SignalProtocolStore', () => {
if (isOpen) { if (isOpen) {
proto.currentSession = new SessionStructure(); proto.currentSession = new SessionStructure();
proto.currentSession.aliceBaseKey = toUint8Array(getPublicKey()); proto.currentSession.aliceBaseKey = getPublicKey();
proto.currentSession.localIdentityPublic = toUint8Array(getPublicKey()); proto.currentSession.localIdentityPublic = getPublicKey();
proto.currentSession.localRegistrationId = 435; proto.currentSession.localRegistrationId = 435;
proto.currentSession.previousCounter = 1; proto.currentSession.previousCounter = 1;
proto.currentSession.remoteIdentityPublic = toUint8Array(getPublicKey()); proto.currentSession.remoteIdentityPublic = getPublicKey();
proto.currentSession.remoteRegistrationId = 243; proto.currentSession.remoteRegistrationId = 243;
proto.currentSession.rootKey = toUint8Array(getPrivateKey()); proto.currentSession.rootKey = getPrivateKey();
proto.currentSession.sessionVersion = 3; proto.currentSession.sessionVersion = 3;
} }
return SessionRecord.deserialize( return SessionRecord.deserialize(
Buffer.from(sessionStructureToArrayBuffer(proto)) Buffer.from(sessionStructureToBytes(proto))
); );
} }
@ -80,19 +76,19 @@ describe('SignalProtocolStore', () => {
const senderChainKey = new SenderKeyStateStructure.SenderChainKey(); const senderChainKey = new SenderKeyStateStructure.SenderChainKey();
senderChainKey.iteration = 10; senderChainKey.iteration = 10;
senderChainKey.seed = toUint8Array(getPublicKey()); senderChainKey.seed = getPublicKey();
state.senderChainKey = senderChainKey; state.senderChainKey = senderChainKey;
const senderSigningKey = new SenderKeyStateStructure.SenderSigningKey(); const senderSigningKey = new SenderKeyStateStructure.SenderSigningKey();
senderSigningKey.public = toUint8Array(getPublicKey()); senderSigningKey.public = getPublicKey();
senderSigningKey.private = toUint8Array(getPrivateKey()); senderSigningKey.private = getPrivateKey();
state.senderSigningKey = senderSigningKey; state.senderSigningKey = senderSigningKey;
state.senderMessageKeys = []; state.senderMessageKeys = [];
const messageKey = new SenderKeyStateStructure.SenderMessageKey(); const messageKey = new SenderKeyStateStructure.SenderMessageKey();
messageKey.iteration = 234; messageKey.iteration = 234;
messageKey.seed = toUint8Array(getPublicKey()); messageKey.seed = getPublicKey();
state.senderMessageKeys.push(messageKey); state.senderMessageKeys.push(messageKey);
proto.senderKeyStates = []; proto.senderKeyStates = [];
@ -105,10 +101,6 @@ describe('SignalProtocolStore', () => {
); );
} }
function toUint8Array(buffer: ArrayBuffer): Uint8Array {
return new Uint8Array(buffer);
}
function getPrivateKey() { function getPrivateKey() {
const key = getRandomBytes(32); const key = getRandomBytes(32);
clampPrivateKey(key); clampPrivateKey(key);
@ -141,8 +133,8 @@ describe('SignalProtocolStore', () => {
window.storage.put('registrationIdMap', { [ourUuid.toString()]: 1337 }); window.storage.put('registrationIdMap', { [ourUuid.toString()]: 1337 });
window.storage.put('identityKeyMap', { window.storage.put('identityKeyMap', {
[ourUuid.toString()]: { [ourUuid.toString()]: {
privKey: toBase64(identityKey.privKey), privKey: Bytes.toBase64(identityKey.privKey),
pubKey: toBase64(identityKey.pubKey), pubKey: Bytes.toBase64(identityKey.pubKey),
}, },
}); });
await window.storage.fetch(); await window.storage.fetch();
@ -194,10 +186,7 @@ describe('SignalProtocolStore', () => {
} }
assert.isTrue( assert.isTrue(
constantTimeEqual( constantTimeEqual(expected.serialize(), actual.serialize())
toArrayBuffer(expected.serialize()),
toArrayBuffer(actual.serialize())
)
); );
await store.removeSenderKey(qualifiedAddress, distributionId); await store.removeSenderKey(qualifiedAddress, distributionId);
@ -230,10 +219,7 @@ describe('SignalProtocolStore', () => {
} }
assert.isTrue( assert.isTrue(
constantTimeEqual( constantTimeEqual(expected.serialize(), actual.serialize())
toArrayBuffer(expected.serialize()),
toArrayBuffer(actual.serialize())
)
); );
await store.removeSenderKey(qualifiedAddress, distributionId); await store.removeSenderKey(qualifiedAddress, distributionId);
@ -1254,12 +1240,8 @@ describe('SignalProtocolStore', () => {
} }
const keyPair = { const keyPair = {
pubKey: window.Signal.Crypto.typedArrayToArrayBuffer( pubKey: key.publicKey().serialize(),
key.publicKey().serialize() privKey: key.privateKey().serialize(),
),
privKey: window.Signal.Crypto.typedArrayToArrayBuffer(
key.privateKey().serialize()
),
}; };
assert.isTrue(constantTimeEqual(keyPair.pubKey, testKey.pubKey)); assert.isTrue(constantTimeEqual(keyPair.pubKey, testKey.pubKey));
@ -1286,12 +1268,8 @@ describe('SignalProtocolStore', () => {
} }
const keyPair = { const keyPair = {
pubKey: window.Signal.Crypto.typedArrayToArrayBuffer( pubKey: key.publicKey().serialize(),
key.publicKey().serialize() privKey: key.privateKey().serialize(),
),
privKey: window.Signal.Crypto.typedArrayToArrayBuffer(
key.privateKey().serialize()
),
}; };
assert.isTrue(constantTimeEqual(keyPair.pubKey, testKey.pubKey)); assert.isTrue(constantTimeEqual(keyPair.pubKey, testKey.pubKey));

View file

@ -4,22 +4,19 @@
import { assert } from 'chai'; import { assert } from 'chai';
import crypto from 'crypto'; import crypto from 'crypto';
import { typedArrayToArrayBuffer as toArrayBuffer } from '../../Crypto';
import { import {
CipherType,
HashType, HashType,
hash, hash,
sign, sign,
encrypt, encrypt,
decrypt, decrypt,
} from '../../util/synchronousCrypto'; } from '../../Crypto';
describe('synchronousCrypto', () => { describe('SignalContext.Crypto', () => {
describe('hash', () => { describe('hash', () => {
it('returns SHA512 hash of the input', () => { it('returns SHA512 hash of the input', () => {
const result = hash( const result = hash(HashType.size512, Buffer.from('signal'));
HashType.size512,
toArrayBuffer(Buffer.from('signal'))
);
assert.strictEqual( assert.strictEqual(
Buffer.from(result).toString('base64'), Buffer.from(result).toString('base64'),
'WxneQjrfSlY95Bi+SAzDAr2cf3mxUXePeNYn6DILN4a8NFr9VelTbP5tGHdthi+' + 'WxneQjrfSlY95Bi+SAzDAr2cf3mxUXePeNYn6DILN4a8NFr9VelTbP5tGHdthi+' +
@ -30,10 +27,7 @@ describe('synchronousCrypto', () => {
describe('sign', () => { describe('sign', () => {
it('returns hmac SHA256 hash of the input', () => { it('returns hmac SHA256 hash of the input', () => {
const result = sign( const result = sign(Buffer.from('secret'), Buffer.from('signal'));
toArrayBuffer(Buffer.from('secret')),
toArrayBuffer(Buffer.from('signal'))
);
assert.strictEqual( assert.strictEqual(
Buffer.from(result).toString('base64'), Buffer.from(result).toString('base64'),
@ -44,12 +38,20 @@ describe('synchronousCrypto', () => {
describe('encrypt+decrypt', () => { describe('encrypt+decrypt', () => {
it('returns original input', () => { it('returns original input', () => {
const iv = toArrayBuffer(crypto.randomBytes(16)); const iv = crypto.randomBytes(16);
const key = toArrayBuffer(crypto.randomBytes(32)); const key = crypto.randomBytes(32);
const input = toArrayBuffer(Buffer.from('plaintext')); const input = Buffer.from('plaintext');
const ciphertext = encrypt(key, input, iv); const ciphertext = encrypt(CipherType.AES256CBC, {
const plaintext = decrypt(key, ciphertext, iv); key,
iv,
plaintext: input,
});
const plaintext = decrypt(CipherType.AES256CBC, {
key,
iv,
ciphertext,
});
assert.strictEqual(Buffer.from(plaintext).toString(), 'plaintext'); assert.strictEqual(Buffer.from(plaintext).toString(), 'plaintext');
}); });

View file

@ -2,6 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai'; import { assert } from 'chai';
import { Response } from 'node-fetch';
import * as sinon from 'sinon'; import * as sinon from 'sinon';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
@ -9,8 +10,6 @@ import AbortController from 'abort-controller';
import { IMAGE_JPEG, stringToMIMEType } from '../../types/MIME'; import { IMAGE_JPEG, stringToMIMEType } from '../../types/MIME';
import * as log from '../../logging/log'; import * as log from '../../logging/log';
import { typedArrayToArrayBuffer } from '../../Crypto';
import { import {
fetchLinkPreviewImage, fetchLinkPreviewImage,
fetchLinkPreviewMetadata, fetchLinkPreviewMetadata,
@ -1155,7 +1154,7 @@ describe('link preview fetching', () => {
new AbortController().signal new AbortController().signal
), ),
{ {
data: typedArrayToArrayBuffer(fixture), data: fixture,
contentType: stringToMIMEType(contentType), contentType: stringToMIMEType(contentType),
} }
); );
@ -1217,7 +1216,7 @@ describe('link preview fetching', () => {
const fakeFetch = stub(); const fakeFetch = stub();
fakeFetch.onFirstCall().resolves( fakeFetch.onFirstCall().resolves(
new Response(null, { new Response(Buffer.from(''), {
status: 301, status: 301,
headers: { headers: {
Location: '/result.jpg', Location: '/result.jpg',
@ -1240,7 +1239,7 @@ describe('link preview fetching', () => {
new AbortController().signal new AbortController().signal
), ),
{ {
data: typedArrayToArrayBuffer(fixture), data: fixture,
contentType: IMAGE_JPEG, contentType: IMAGE_JPEG,
} }
); );
@ -1336,7 +1335,7 @@ describe('link preview fetching', () => {
}); });
it('sends WhatsApp as the User-Agent for compatibility', async () => { 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( await fetchLinkPreviewImage(
fakeFetch, fakeFetch,
@ -1368,7 +1367,7 @@ describe('link preview fetching', () => {
}, },
}); });
sinon sinon
.stub(response, 'arrayBuffer') .stub(response, 'buffer')
.rejects(new Error('Should not be called')); .rejects(new Error('Should not be called'));
sinon.stub(response, 'blob').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')); sinon.stub(response, 'text').rejects(new Error('Should not be called'));
@ -1402,9 +1401,9 @@ describe('link preview fetching', () => {
'Content-Length': fixture.length.toString(), 'Content-Length': fixture.length.toString(),
}, },
}); });
const oldArrayBufferMethod = response.arrayBuffer.bind(response); const oldBufferMethod = response.buffer.bind(response);
sinon.stub(response, 'arrayBuffer').callsFake(async () => { sinon.stub(response, 'buffer').callsFake(async () => {
const data = await oldArrayBufferMethod(); const data = await oldBufferMethod();
abortController.abort(); abortController.abort();
return data; return data;
}); });

View file

@ -136,7 +136,7 @@ describe('Message', () => {
}, },
}); });
const fakeDataMessage = new ArrayBuffer(0); const fakeDataMessage = new Uint8Array(0);
const conversation1Uuid = conversation1.get('uuid'); const conversation1Uuid = conversation1.get('uuid');
const ignoredUuid = window.getGuid(); const ignoredUuid = window.getGuid();

View file

@ -10,7 +10,6 @@ import { v4 as uuid } from 'uuid';
import Long from 'long'; import Long from 'long';
import * as durations from '../../util/durations'; import * as durations from '../../util/durations';
import * as Bytes from '../../Bytes'; import * as Bytes from '../../Bytes';
import { typedArrayToArrayBuffer } from '../../Crypto';
import { SenderCertificateMode } from '../../textsecure/OutgoingMessage'; import { SenderCertificateMode } from '../../textsecure/OutgoingMessage';
import { SignalService as Proto } from '../../protobuf'; import { SignalService as Proto } from '../../protobuf';
@ -22,6 +21,7 @@ describe('SenderCertificateService', () => {
const FIFTEEN_MINUTES = 15 * durations.MINUTE; const FIFTEEN_MINUTES = 15 * durations.MINUTE;
let fakeValidCertificate: SenderCertificate; let fakeValidCertificate: SenderCertificate;
let fakeValidEncodedCertificate: Uint8Array;
let fakeValidCertificateExpiry: number; let fakeValidCertificateExpiry: number;
let fakeServer: any; let fakeServer: any;
let fakeNavigator: { onLine: boolean }; let fakeNavigator: { onLine: boolean };
@ -47,12 +47,13 @@ describe('SenderCertificateService', () => {
fakeValidCertificate.certificate = SenderCertificate.Certificate.encode( fakeValidCertificate.certificate = SenderCertificate.Certificate.encode(
certificate certificate
).finish(); ).finish();
fakeValidEncodedCertificate = SenderCertificate.encode(
fakeValidCertificate
).finish();
fakeServer = { fakeServer = {
getSenderCertificate: sinon.stub().resolves({ getSenderCertificate: sinon.stub().resolves({
certificate: Bytes.toBase64( certificate: Bytes.toBase64(fakeValidEncodedCertificate),
SenderCertificate.encode(fakeValidCertificate).finish()
),
}), }),
}; };
@ -77,7 +78,7 @@ describe('SenderCertificateService', () => {
it('returns valid yes-E164 certificates from storage if they exist', async () => { it('returns valid yes-E164 certificates from storage if they exist', async () => {
const cert = { const cert = {
expires: Date.now() + 123456, expires: Date.now() + 123456,
serialized: new ArrayBuffer(2), serialized: new Uint8Array(2),
}; };
fakeStorage.get.withArgs('senderCertificate').returns(cert); fakeStorage.get.withArgs('senderCertificate').returns(cert);
@ -94,7 +95,7 @@ describe('SenderCertificateService', () => {
it('returns valid no-E164 certificates from storage if they exist', async () => { it('returns valid no-E164 certificates from storage if they exist', async () => {
const cert = { const cert = {
expires: Date.now() + 123456, expires: Date.now() + 123456,
serialized: new ArrayBuffer(2), serialized: new Uint8Array(2),
}; };
fakeStorage.get.withArgs('senderCertificateNoE164').returns(cert); fakeStorage.get.withArgs('senderCertificateNoE164').returns(cert);
@ -113,16 +114,12 @@ describe('SenderCertificateService', () => {
assert.deepEqual(await service.get(SenderCertificateMode.WithE164), { assert.deepEqual(await service.get(SenderCertificateMode.WithE164), {
expires: fakeValidCertificateExpiry - FIFTEEN_MINUTES, expires: fakeValidCertificateExpiry - FIFTEEN_MINUTES,
serialized: typedArrayToArrayBuffer( serialized: fakeValidEncodedCertificate,
SenderCertificate.encode(fakeValidCertificate).finish()
),
}); });
sinon.assert.calledWithMatch(fakeStorage.put, 'senderCertificate', { sinon.assert.calledWithMatch(fakeStorage.put, 'senderCertificate', {
expires: fakeValidCertificateExpiry - FIFTEEN_MINUTES, expires: fakeValidCertificateExpiry - FIFTEEN_MINUTES,
serialized: typedArrayToArrayBuffer( serialized: Buffer.from(fakeValidEncodedCertificate),
SenderCertificate.encode(fakeValidCertificate).finish()
),
}); });
sinon.assert.calledWith(fakeServer.getSenderCertificate, false); sinon.assert.calledWith(fakeServer.getSenderCertificate, false);
@ -133,16 +130,12 @@ describe('SenderCertificateService', () => {
assert.deepEqual(await service.get(SenderCertificateMode.WithoutE164), { assert.deepEqual(await service.get(SenderCertificateMode.WithoutE164), {
expires: fakeValidCertificateExpiry - FIFTEEN_MINUTES, expires: fakeValidCertificateExpiry - FIFTEEN_MINUTES,
serialized: typedArrayToArrayBuffer( serialized: fakeValidEncodedCertificate,
SenderCertificate.encode(fakeValidCertificate).finish()
),
}); });
sinon.assert.calledWithMatch(fakeStorage.put, 'senderCertificateNoE164', { sinon.assert.calledWithMatch(fakeStorage.put, 'senderCertificateNoE164', {
expires: fakeValidCertificateExpiry - FIFTEEN_MINUTES, expires: fakeValidCertificateExpiry - FIFTEEN_MINUTES,
serialized: typedArrayToArrayBuffer( serialized: Buffer.from(fakeValidEncodedCertificate),
SenderCertificate.encode(fakeValidCertificate).finish()
),
}); });
sinon.assert.calledWith(fakeServer.getSenderCertificate, true); sinon.assert.calledWith(fakeServer.getSenderCertificate, true);
@ -153,7 +146,7 @@ describe('SenderCertificateService', () => {
fakeStorage.get.withArgs('senderCertificate').returns({ fakeStorage.get.withArgs('senderCertificate').returns({
expires: Date.now() - 1000, expires: Date.now() - 1000,
serialized: new ArrayBuffer(2), serialized: new Uint8Array(2),
}); });
await service.get(SenderCertificateMode.WithE164); await service.get(SenderCertificateMode.WithE164);
@ -165,7 +158,7 @@ describe('SenderCertificateService', () => {
const service = initializeTestService(); const service = initializeTestService();
fakeStorage.get.withArgs('senderCertificate').returns({ fakeStorage.get.withArgs('senderCertificate').returns({
serialized: 'not an arraybuffer', serialized: 'not an uint8array',
}); });
await service.get(SenderCertificateMode.WithE164); await service.get(SenderCertificateMode.WithE164);

View file

@ -6,11 +6,7 @@ import { v4 as getGuid } from 'uuid';
import { assert } from 'chai'; import { assert } from 'chai';
import dataInterface from '../../sql/Client'; import dataInterface from '../../sql/Client';
import { import { constantTimeEqual, getRandomBytes } from '../../Crypto';
constantTimeEqual,
getRandomBytes,
typedArrayToArrayBuffer,
} from '../../Crypto';
const { const {
_getAllSentProtoMessageIds, _getAllSentProtoMessageIds,
@ -33,7 +29,7 @@ describe('sendLog', () => {
}); });
it('roundtrips with insertSentProto/getAllSentProtos', async () => { it('roundtrips with insertSentProto/getAllSentProtos', async () => {
const bytes = Buffer.from(getRandomBytes(128)); const bytes = getRandomBytes(128);
const timestamp = Date.now(); const timestamp = Date.now();
const proto = { const proto = {
contentHint: 1, contentHint: 1,
@ -52,12 +48,7 @@ describe('sendLog', () => {
const actual = allProtos[0]; const actual = allProtos[0];
assert.strictEqual(actual.contentHint, proto.contentHint); assert.strictEqual(actual.contentHint, proto.contentHint);
assert.isTrue( assert.isTrue(constantTimeEqual(actual.proto, proto.proto));
constantTimeEqual(
typedArrayToArrayBuffer(actual.proto),
typedArrayToArrayBuffer(proto.proto)
)
);
assert.strictEqual(actual.timestamp, proto.timestamp); assert.strictEqual(actual.timestamp, proto.timestamp);
await removeAllSentProtos(); await removeAllSentProtos();
@ -70,7 +61,7 @@ describe('sendLog', () => {
assert.lengthOf(await _getAllSentProtoMessageIds(), 0); assert.lengthOf(await _getAllSentProtoMessageIds(), 0);
assert.lengthOf(await _getAllSentProtoRecipients(), 0); assert.lengthOf(await _getAllSentProtoRecipients(), 0);
const bytes = Buffer.from(getRandomBytes(128)); const bytes = getRandomBytes(128);
const timestamp = Date.now(); const timestamp = Date.now();
const proto = { const proto = {
contentHint: 1, contentHint: 1,
@ -114,7 +105,7 @@ describe('sendLog', () => {
{ forceSave: true } { forceSave: true }
); );
const bytes = Buffer.from(getRandomBytes(128)); const bytes = getRandomBytes(128);
const proto = { const proto = {
contentHint: 1, contentHint: 1,
proto: bytes, proto: bytes,
@ -148,12 +139,12 @@ describe('sendLog', () => {
}; };
const proto1 = { const proto1 = {
contentHint: 7, contentHint: 7,
proto: Buffer.from(getRandomBytes(128)), proto: getRandomBytes(128),
timestamp, timestamp,
}; };
const proto2 = { const proto2 = {
contentHint: 9, contentHint: 9,
proto: Buffer.from(getRandomBytes(128)), proto: getRandomBytes(128),
timestamp, timestamp,
}; };
@ -182,7 +173,7 @@ describe('sendLog', () => {
const messageIds = [getGuid()]; const messageIds = [getGuid()];
const proto = { const proto = {
contentHint: 1, contentHint: 1,
proto: Buffer.from(getRandomBytes(128)), proto: getRandomBytes(128),
timestamp, timestamp,
}; };
@ -220,17 +211,17 @@ describe('sendLog', () => {
const proto1 = { const proto1 = {
contentHint: 1, contentHint: 1,
proto: Buffer.from(getRandomBytes(128)), proto: getRandomBytes(128),
timestamp: timestamp + 10, timestamp: timestamp + 10,
}; };
const proto2 = { const proto2 = {
contentHint: 2, contentHint: 2,
proto: Buffer.from(getRandomBytes(128)), proto: getRandomBytes(128),
timestamp, timestamp,
}; };
const proto3 = { const proto3 = {
contentHint: 0, contentHint: 0,
proto: Buffer.from(getRandomBytes(128)), proto: getRandomBytes(128),
timestamp: timestamp - 15, timestamp: timestamp - 15,
}; };
await insertSentProto(proto1, { await insertSentProto(proto1, {
@ -261,22 +252,12 @@ describe('sendLog', () => {
const actual1 = allProtos[0]; const actual1 = allProtos[0];
assert.strictEqual(actual1.contentHint, proto1.contentHint); assert.strictEqual(actual1.contentHint, proto1.contentHint);
assert.isTrue( assert.isTrue(constantTimeEqual(actual1.proto, proto1.proto));
constantTimeEqual(
typedArrayToArrayBuffer(actual1.proto),
typedArrayToArrayBuffer(proto1.proto)
)
);
assert.strictEqual(actual1.timestamp, proto1.timestamp); assert.strictEqual(actual1.timestamp, proto1.timestamp);
const actual2 = allProtos[1]; const actual2 = allProtos[1];
assert.strictEqual(actual2.contentHint, proto2.contentHint); assert.strictEqual(actual2.contentHint, proto2.contentHint);
assert.isTrue( assert.isTrue(constantTimeEqual(actual2.proto, proto2.proto));
constantTimeEqual(
typedArrayToArrayBuffer(actual2.proto),
typedArrayToArrayBuffer(proto2.proto)
)
);
assert.strictEqual(actual2.timestamp, proto2.timestamp); assert.strictEqual(actual2.timestamp, proto2.timestamp);
}); });
}); });
@ -291,17 +272,17 @@ describe('sendLog', () => {
const timestamp = Date.now(); const timestamp = Date.now();
const proto1 = { const proto1 = {
contentHint: 1, contentHint: 1,
proto: Buffer.from(getRandomBytes(128)), proto: getRandomBytes(128),
timestamp, timestamp,
}; };
const proto2 = { const proto2 = {
contentHint: 1, contentHint: 1,
proto: Buffer.from(getRandomBytes(128)), proto: getRandomBytes(128),
timestamp: timestamp - 10, timestamp: timestamp - 10,
}; };
const proto3 = { const proto3 = {
contentHint: 1, contentHint: 1,
proto: Buffer.from(getRandomBytes(128)), proto: getRandomBytes(128),
timestamp: timestamp - 20, timestamp: timestamp - 20,
}; };
await insertSentProto(proto1, { await insertSentProto(proto1, {
@ -344,7 +325,7 @@ describe('sendLog', () => {
const recipientUuid2 = getGuid(); const recipientUuid2 = getGuid();
const proto = { const proto = {
contentHint: 1, contentHint: 1,
proto: Buffer.from(getRandomBytes(128)), proto: getRandomBytes(128),
timestamp, timestamp,
}; };
await insertSentProto(proto, { await insertSentProto(proto, {
@ -375,7 +356,7 @@ describe('sendLog', () => {
const recipientUuid2 = getGuid(); const recipientUuid2 = getGuid();
const proto = { const proto = {
contentHint: 1, contentHint: 1,
proto: Buffer.from(getRandomBytes(128)), proto: getRandomBytes(128),
timestamp, timestamp,
}; };
await insertSentProto(proto, { await insertSentProto(proto, {
@ -424,7 +405,7 @@ describe('sendLog', () => {
const recipientUuid2 = getGuid(); const recipientUuid2 = getGuid();
const proto = { const proto = {
contentHint: 1, contentHint: 1,
proto: Buffer.from(getRandomBytes(128)), proto: getRandomBytes(128),
timestamp, timestamp,
}; };
await insertSentProto(proto, { await insertSentProto(proto, {
@ -469,7 +450,7 @@ describe('sendLog', () => {
const messageIds = [getGuid(), getGuid()]; const messageIds = [getGuid(), getGuid()];
const proto = { const proto = {
contentHint: 1, contentHint: 1,
proto: Buffer.from(getRandomBytes(128)), proto: getRandomBytes(128),
timestamp, timestamp,
}; };
await insertSentProto(proto, { await insertSentProto(proto, {
@ -493,12 +474,7 @@ describe('sendLog', () => {
throw new Error('Failed to fetch proto!'); throw new Error('Failed to fetch proto!');
} }
assert.strictEqual(actual.contentHint, proto.contentHint); assert.strictEqual(actual.contentHint, proto.contentHint);
assert.isTrue( assert.isTrue(constantTimeEqual(actual.proto, proto.proto));
constantTimeEqual(
typedArrayToArrayBuffer(actual.proto),
typedArrayToArrayBuffer(proto.proto)
)
);
assert.strictEqual(actual.timestamp, proto.timestamp); assert.strictEqual(actual.timestamp, proto.timestamp);
assert.sameMembers(actual.messageIds, messageIds); assert.sameMembers(actual.messageIds, messageIds);
}); });
@ -509,7 +485,7 @@ describe('sendLog', () => {
const recipientUuid = getGuid(); const recipientUuid = getGuid();
const proto = { const proto = {
contentHint: 1, contentHint: 1,
proto: Buffer.from(getRandomBytes(128)), proto: getRandomBytes(128),
timestamp, timestamp,
}; };
await insertSentProto(proto, { await insertSentProto(proto, {
@ -533,12 +509,7 @@ describe('sendLog', () => {
throw new Error('Failed to fetch proto!'); throw new Error('Failed to fetch proto!');
} }
assert.strictEqual(actual.contentHint, proto.contentHint); assert.strictEqual(actual.contentHint, proto.contentHint);
assert.isTrue( assert.isTrue(constantTimeEqual(actual.proto, proto.proto));
constantTimeEqual(
typedArrayToArrayBuffer(actual.proto),
typedArrayToArrayBuffer(proto.proto)
)
);
assert.strictEqual(actual.timestamp, proto.timestamp); assert.strictEqual(actual.timestamp, proto.timestamp);
assert.deepEqual(actual.messageIds, []); assert.deepEqual(actual.messageIds, []);
}); });
@ -549,7 +520,7 @@ describe('sendLog', () => {
const recipientUuid = getGuid(); const recipientUuid = getGuid();
const proto = { const proto = {
contentHint: 1, contentHint: 1,
proto: Buffer.from(getRandomBytes(128)), proto: getRandomBytes(128),
timestamp, timestamp,
}; };
await insertSentProto(proto, { await insertSentProto(proto, {
@ -577,7 +548,7 @@ describe('sendLog', () => {
const recipientUuid = getGuid(); const recipientUuid = getGuid();
const proto = { const proto = {
contentHint: 1, contentHint: 1,
proto: Buffer.from(getRandomBytes(128)), proto: getRandomBytes(128),
timestamp, timestamp,
}; };
await insertSentProto(proto, { await insertSentProto(proto, {
@ -606,7 +577,7 @@ describe('sendLog', () => {
const recipientUuid = getGuid(); const recipientUuid = getGuid();
const proto = { const proto = {
contentHint: 1, contentHint: 1,
proto: Buffer.from(getRandomBytes(128)), proto: getRandomBytes(128),
timestamp, timestamp,
}; };
await insertSentProto(proto, { await insertSentProto(proto, {

View file

@ -645,7 +645,7 @@ describe('both/state/ducks/conversations', () => {
...defaultSetGroupMetadataComposerState, ...defaultSetGroupMetadataComposerState,
selectedConversationIds: ['abc123'], selectedConversationIds: ['abc123'],
groupName: 'Foo Bar Group', 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.calledOnce(createGroupStub);
sinon.assert.calledWith(createGroupStub, { sinon.assert.calledWith(createGroupStub, {
name: 'Foo Bar Group', name: 'Foo Bar Group',
avatar: new Uint8Array([1, 2, 3]).buffer, avatar: new Uint8Array([1, 2, 3]),
avatars: [], avatars: [],
expireTimer: 0, expireTimer: 0,
conversationIds: ['abc123'], conversationIds: ['abc123'],
@ -1172,7 +1172,7 @@ describe('both/state/ducks/conversations', () => {
...getEmptyState(), ...getEmptyState(),
composer: { composer: {
...defaultSetGroupMetadataComposerState, ...defaultSetGroupMetadataComposerState,
groupAvatar: new ArrayBuffer(2), groupAvatar: new Uint8Array(2),
}, },
}; };
const action = setComposeGroupAvatar(undefined); const action = setComposeGroupAvatar(undefined);
@ -1185,7 +1185,7 @@ describe('both/state/ducks/conversations', () => {
}); });
it("can set the composer's group avatar", () => { 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 = { const state = {
...getEmptyState(), ...getEmptyState(),
@ -1450,7 +1450,7 @@ describe('both/state/ducks/conversations', () => {
composer: { composer: {
...defaultSetGroupMetadataComposerState, ...defaultSetGroupMetadataComposerState,
groupName: 'Foo Bar Group', groupName: 'Foo Bar Group',
groupAvatar: new Uint8Array([4, 2]).buffer, groupAvatar: new Uint8Array([4, 2]),
}, },
}; };
const action = showChooseGroupMembers(); const action = showChooseGroupMembers();
@ -1460,7 +1460,7 @@ describe('both/state/ducks/conversations', () => {
assert.deepEqual(result.composer, { assert.deepEqual(result.composer, {
...defaultChooseGroupMembersComposerState, ...defaultChooseGroupMembersComposerState,
groupName: 'Foo Bar Group', 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', searchTerm: 'foo bar',
selectedConversationIds: ['abc', 'def'], selectedConversationIds: ['abc', 'def'],
groupName: 'Foo Bar Group', groupName: 'Foo Bar Group',
groupAvatar: new Uint8Array([6, 9]).buffer, groupAvatar: new Uint8Array([6, 9]),
}, },
}; };
const action = startSettingGroupMetadata(); const action = startSettingGroupMetadata();
@ -1528,7 +1528,7 @@ describe('both/state/ducks/conversations', () => {
...defaultSetGroupMetadataComposerState, ...defaultSetGroupMetadataComposerState,
selectedConversationIds: ['abc', 'def'], selectedConversationIds: ['abc', 'def'],
groupName: 'Foo Bar Group', groupName: 'Foo Bar Group',
groupAvatar: new Uint8Array([6, 9]).buffer, groupAvatar: new Uint8Array([6, 9]),
}); });
}); });

View file

@ -59,7 +59,7 @@ describe('AccountManager', () => {
describe('encrypted device name', () => { describe('encrypted device name', () => {
it('roundtrips', async () => { it('roundtrips', async () => {
const deviceName = 'v2.5.0 on Ubunto 20.04'; const deviceName = 'v2.5.0 on Ubunto 20.04';
const encrypted = await accountManager.encryptDeviceName( const encrypted = accountManager.encryptDeviceName(
deviceName, deviceName,
identityKey identityKey
); );
@ -72,11 +72,8 @@ describe('AccountManager', () => {
assert.strictEqual(decrypted, deviceName); assert.strictEqual(decrypted, deviceName);
}); });
it('handles falsey deviceName', async () => { it('handles falsey deviceName', () => {
const encrypted = await accountManager.encryptDeviceName( const encrypted = accountManager.encryptDeviceName('', identityKey);
'',
identityKey
);
assert.strictEqual(encrypted, null); assert.strictEqual(encrypted, null);
}); });
}); });

View file

@ -3,11 +3,8 @@
import { assert } from 'chai'; import { assert } from 'chai';
import { import { toBase64 } from '../../Bytes';
typedArrayToArrayBuffer as toArrayBuffer, import { constantTimeEqual } from '../../Crypto';
arrayBufferToBase64 as toBase64,
constantTimeEqual,
} from '../../Crypto';
import { generateKeyPair } from '../../Curve'; import { generateKeyPair } from '../../Curve';
import AccountManager, { import AccountManager, {
GeneratedKeysType, GeneratedKeysType,
@ -17,7 +14,7 @@ import { UUID } from '../../types/UUID';
const { textsecure } = window; const { textsecure } = window;
const assertEqualArrayBuffers = (a: ArrayBuffer, b: ArrayBuffer) => { const assertEqualBuffers = (a: Uint8Array, b: Uint8Array) => {
assert.isTrue(constantTimeEqual(a, b)); assert.isTrue(constantTimeEqual(a, b));
}; };
@ -54,10 +51,7 @@ describe('Key generation', function thisNeeded() {
if (!keyPair) { if (!keyPair) {
throw new Error(`PreKey ${resultKey.keyId} not found`); throw new Error(`PreKey ${resultKey.keyId} not found`);
} }
assertEqualArrayBuffers( assertEqualBuffers(resultKey.publicKey, keyPair.publicKey().serialize());
resultKey.publicKey,
toArrayBuffer(keyPair.publicKey().serialize())
);
} }
async function validateResultSignedKey( async function validateResultSignedKey(
resultSignedKey: Pick<SignedPreKeyType, 'keyId' | 'publicKey'> resultSignedKey: Pick<SignedPreKeyType, 'keyId' | 'publicKey'>
@ -69,9 +63,9 @@ describe('Key generation', function thisNeeded() {
if (!keyPair) { if (!keyPair) {
throw new Error(`SignedPreKey ${resultSignedKey.keyId} not found`); throw new Error(`SignedPreKey ${resultSignedKey.keyId} not found`);
} }
assertEqualArrayBuffers( assertEqualBuffers(
resultSignedKey.publicKey, resultSignedKey.publicKey,
toArrayBuffer(keyPair.publicKey().serialize()) keyPair.publicKey().serialize()
); );
} }
@ -123,7 +117,7 @@ describe('Key generation', function thisNeeded() {
}); });
it('returns a signed prekey', () => { it('returns a signed prekey', () => {
assert.strictEqual(result.signedPreKey.keyId, 1); assert.strictEqual(result.signedPreKey.keyId, 1);
assert.instanceOf(result.signedPreKey.signature, ArrayBuffer); assert.instanceOf(result.signedPreKey.signature, Uint8Array);
return validateResultSignedKey(result.signedPreKey); return validateResultSignedKey(result.signedPreKey);
}); });
}); });
@ -157,7 +151,7 @@ describe('Key generation', function thisNeeded() {
}); });
it('returns a signed prekey', () => { it('returns a signed prekey', () => {
assert.strictEqual(result.signedPreKey.keyId, 2); assert.strictEqual(result.signedPreKey.keyId, 2);
assert.instanceOf(result.signedPreKey.signature, ArrayBuffer); assert.instanceOf(result.signedPreKey.signature, Uint8Array);
return validateResultSignedKey(result.signedPreKey); return validateResultSignedKey(result.signedPreKey);
}); });
}); });
@ -191,7 +185,7 @@ describe('Key generation', function thisNeeded() {
}); });
it('result contains a signed prekey', () => { it('result contains a signed prekey', () => {
assert.strictEqual(result.signedPreKey.keyId, 3); assert.strictEqual(result.signedPreKey.keyId, 3);
assert.instanceOf(result.signedPreKey.signature, ArrayBuffer); assert.instanceOf(result.signedPreKey.signature, Uint8Array);
return validateResultSignedKey(result.signedPreKey); return validateResultSignedKey(result.signedPreKey);
}); });
}); });

View file

@ -26,7 +26,7 @@ describe('canvasToBlob', () => {
const result = await canvasToBlob(canvas); const result = await canvasToBlob(canvas);
assert.strictEqual( assert.strictEqual(
sniffImageMimeType(await result.arrayBuffer()), sniffImageMimeType(new Uint8Array(await result.arrayBuffer())),
IMAGE_JPEG IMAGE_JPEG
); );
@ -39,7 +39,7 @@ describe('canvasToBlob', () => {
const result = await canvasToBlob(canvas, IMAGE_PNG); const result = await canvasToBlob(canvas, IMAGE_PNG);
assert.strictEqual( assert.strictEqual(
sniffImageMimeType(await result.arrayBuffer()), sniffImageMimeType(new Uint8Array(await result.arrayBuffer())),
IMAGE_PNG IMAGE_PNG
); );
}); });

View file

@ -5,9 +5,9 @@ import { assert } from 'chai';
import { IMAGE_JPEG, IMAGE_PNG } from '../../types/MIME'; import { IMAGE_JPEG, IMAGE_PNG } from '../../types/MIME';
import { sniffImageMimeType } from '../../util/sniffImageMimeType'; import { sniffImageMimeType } from '../../util/sniffImageMimeType';
import { canvasToArrayBuffer } from '../../util/canvasToArrayBuffer'; import { canvasToBytes } from '../../util/canvasToBytes';
describe('canvasToArrayBuffer', () => { describe('canvasToBytes', () => {
let canvas: HTMLCanvasElement; let canvas: HTMLCanvasElement;
beforeEach(() => { beforeEach(() => {
canvas = document.createElement('canvas'); canvas = document.createElement('canvas');
@ -22,18 +22,18 @@ describe('canvasToArrayBuffer', () => {
context.fillRect(10, 10, 20, 20); context.fillRect(10, 10, 20, 20);
}); });
it('converts a canvas to an ArrayBuffer, JPEG by default', async () => { it('converts a canvas to an Uint8Array, JPEG by default', async () => {
const result = await canvasToArrayBuffer(canvas); const result = await canvasToBytes(canvas);
assert.strictEqual(sniffImageMimeType(result), IMAGE_JPEG); assert.strictEqual(sniffImageMimeType(result), IMAGE_JPEG);
// These are just smoke tests. // These are just smoke tests.
assert.instanceOf(result, ArrayBuffer); assert.instanceOf(result, Uint8Array);
assert.isAtLeast(result.byteLength, 50); assert.isAtLeast(result.byteLength, 50);
}); });
it('can convert a canvas to a PNG ArrayBuffer', async () => { it('can convert a canvas to a PNG Uint8Array', async () => {
const result = await canvasToArrayBuffer(canvas, IMAGE_PNG); const result = await canvasToBytes(canvas, IMAGE_PNG);
assert.strictEqual(sniffImageMimeType(result), IMAGE_PNG); assert.strictEqual(sniffImageMimeType(result), IMAGE_PNG);
}); });

View file

@ -4,24 +4,24 @@
import { assert } from 'chai'; import { assert } from 'chai';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import Crypto from '../../textsecure/Crypto'; import * as Bytes from '../../Bytes';
import { import {
arrayBufferToBase64,
base64ToArrayBuffer,
stringFromBytes,
trimForDisplay, trimForDisplay,
getRandomBytes,
decryptProfileName,
decryptProfile,
} from '../../Crypto'; } from '../../Crypto';
import { encryptProfileData } from '../../util/encryptProfileData'; import { encryptProfileData } from '../../util/encryptProfileData';
describe('encryptProfileData', () => { describe('encryptProfileData', () => {
it('encrypts and decrypts properly', async () => { it('encrypts and decrypts properly', async () => {
const keyBuffer = Crypto.getRandomBytes(32); const keyBuffer = getRandomBytes(32);
const conversation = { const conversation = {
aboutEmoji: '🐢', aboutEmoji: '🐢',
aboutText: 'I like turtles', aboutText: 'I like turtles',
familyName: 'Kid', familyName: 'Kid',
firstName: 'Zombie', firstName: 'Zombie',
profileKey: arrayBufferToBase64(keyBuffer), profileKey: Bytes.toBase64(keyBuffer),
uuid: uuid(), uuid: uuid(),
// To satisfy TS // To satisfy TS
@ -39,17 +39,17 @@ describe('encryptProfileData', () => {
assert.isDefined(encrypted.name); assert.isDefined(encrypted.name);
assert.isDefined(encrypted.commitment); assert.isDefined(encrypted.commitment);
const decryptedProfileNameBytes = await Crypto.decryptProfileName( const decryptedProfileNameBytes = decryptProfileName(
encrypted.name, encrypted.name,
keyBuffer keyBuffer
); );
assert.equal( assert.equal(
stringFromBytes(decryptedProfileNameBytes.given), Bytes.toString(decryptedProfileNameBytes.given),
conversation.firstName conversation.firstName
); );
if (decryptedProfileNameBytes.family) { if (decryptedProfileNameBytes.family) {
assert.equal( assert.equal(
stringFromBytes(decryptedProfileNameBytes.family), Bytes.toString(decryptedProfileNameBytes.family),
conversation.familyName conversation.familyName
); );
} else { } else {
@ -57,12 +57,12 @@ describe('encryptProfileData', () => {
} }
if (encrypted.about) { if (encrypted.about) {
const decryptedAboutBytes = await Crypto.decryptProfile( const decryptedAboutBytes = decryptProfile(
base64ToArrayBuffer(encrypted.about), Bytes.fromBase64(encrypted.about),
keyBuffer keyBuffer
); );
assert.equal( assert.equal(
stringFromBytes(trimForDisplay(decryptedAboutBytes)), Bytes.toString(trimForDisplay(decryptedAboutBytes)),
conversation.aboutText conversation.aboutText
); );
} else { } else {
@ -70,12 +70,12 @@ describe('encryptProfileData', () => {
} }
if (encrypted.aboutEmoji) { if (encrypted.aboutEmoji) {
const decryptedAboutEmojiBytes = await Crypto.decryptProfile( const decryptedAboutEmojiBytes = await decryptProfile(
base64ToArrayBuffer(encrypted.aboutEmoji), Bytes.fromBase64(encrypted.aboutEmoji),
keyBuffer keyBuffer
); );
assert.equal( assert.equal(
stringFromBytes(trimForDisplay(decryptedAboutEmojiBytes)), Bytes.toString(trimForDisplay(decryptedAboutEmojiBytes)),
conversation.aboutEmoji conversation.aboutEmoji
); );
} else { } else {

View file

@ -4,16 +4,16 @@
import { assert } from 'chai'; import { assert } from 'chai';
import path from 'path'; import path from 'path';
import { imagePathToArrayBuffer } from '../../util/imagePathToArrayBuffer'; import { imagePathToBytes } from '../../util/imagePathToBytes';
describe('imagePathToArrayBuffer', () => { describe('imagePathToBytes', () => {
it('converts an image to an ArrayBuffer', async () => { it('converts an image to an Bytes', async () => {
const avatarPath = path.join( const avatarPath = path.join(
__dirname, __dirname,
'../../../fixtures/kitten-3-64-64.jpg' '../../../fixtures/kitten-3-64-64.jpg'
); );
const buffer = await imagePathToArrayBuffer(avatarPath); const buffer = await imagePathToBytes(avatarPath);
assert.isDefined(buffer); assert.isDefined(buffer);
assert(buffer instanceof ArrayBuffer); assert(buffer instanceof Uint8Array);
}); });
}); });

View file

@ -6,7 +6,7 @@ import { assert } from 'chai';
import * as Attachment from '../../types/Attachment'; import * as Attachment from '../../types/Attachment';
import * as MIME from '../../types/MIME'; import * as MIME from '../../types/MIME';
import { SignalService } from '../../protobuf'; import { SignalService } from '../../protobuf';
import { stringToArrayBuffer } from '../../util/stringToArrayBuffer'; import * as Bytes from '../../Bytes';
import * as logger from '../../logging/log'; import * as logger from '../../logging/log';
describe('Attachment', () => { describe('Attachment', () => {
@ -38,7 +38,7 @@ describe('Attachment', () => {
describe('getFileExtension', () => { describe('getFileExtension', () => {
it('should return file extension from content type', () => { it('should return file extension from content type', () => {
const input: Attachment.AttachmentType = { const input: Attachment.AttachmentType = {
data: stringToArrayBuffer('foo'), data: Bytes.fromString('foo'),
contentType: MIME.IMAGE_GIF, contentType: MIME.IMAGE_GIF,
}; };
assert.strictEqual(Attachment.getFileExtension(input), 'gif'); assert.strictEqual(Attachment.getFileExtension(input), 'gif');
@ -46,7 +46,7 @@ describe('Attachment', () => {
it('should return file extension for QuickTime videos', () => { it('should return file extension for QuickTime videos', () => {
const input: Attachment.AttachmentType = { const input: Attachment.AttachmentType = {
data: stringToArrayBuffer('foo'), data: Bytes.fromString('foo'),
contentType: MIME.VIDEO_QUICKTIME, contentType: MIME.VIDEO_QUICKTIME,
}; };
assert.strictEqual(Attachment.getFileExtension(input), 'mov'); assert.strictEqual(Attachment.getFileExtension(input), 'mov');
@ -58,7 +58,7 @@ describe('Attachment', () => {
it('should return existing filename if present', () => { it('should return existing filename if present', () => {
const attachment: Attachment.AttachmentType = { const attachment: Attachment.AttachmentType = {
fileName: 'funny-cat.mov', fileName: 'funny-cat.mov',
data: stringToArrayBuffer('foo'), data: Bytes.fromString('foo'),
contentType: MIME.VIDEO_QUICKTIME, contentType: MIME.VIDEO_QUICKTIME,
}; };
const actual = Attachment.getSuggestedFilename({ attachment }); const actual = Attachment.getSuggestedFilename({ attachment });
@ -69,7 +69,7 @@ describe('Attachment', () => {
context('for attachment without filename', () => { context('for attachment without filename', () => {
it('should generate a filename based on timestamp', () => { it('should generate a filename based on timestamp', () => {
const attachment: Attachment.AttachmentType = { const attachment: Attachment.AttachmentType = {
data: stringToArrayBuffer('foo'), data: Bytes.fromString('foo'),
contentType: MIME.VIDEO_QUICKTIME, contentType: MIME.VIDEO_QUICKTIME,
}; };
const timestamp = new Date(new Date(0).getTimezoneOffset() * 60 * 1000); const timestamp = new Date(new Date(0).getTimezoneOffset() * 60 * 1000);
@ -84,7 +84,7 @@ describe('Attachment', () => {
context('for attachment with index', () => { context('for attachment with index', () => {
it('should generate a filename based on timestamp', () => { it('should generate a filename based on timestamp', () => {
const attachment: Attachment.AttachmentType = { const attachment: Attachment.AttachmentType = {
data: stringToArrayBuffer('foo'), data: Bytes.fromString('foo'),
contentType: MIME.VIDEO_QUICKTIME, contentType: MIME.VIDEO_QUICKTIME,
}; };
const timestamp = new Date(new Date(0).getTimezoneOffset() * 60 * 1000); const timestamp = new Date(new Date(0).getTimezoneOffset() * 60 * 1000);
@ -103,7 +103,7 @@ describe('Attachment', () => {
it('should return true for images', () => { it('should return true for images', () => {
const attachment: Attachment.AttachmentType = { const attachment: Attachment.AttachmentType = {
fileName: 'meme.gif', fileName: 'meme.gif',
data: stringToArrayBuffer('gif'), data: Bytes.fromString('gif'),
contentType: MIME.IMAGE_GIF, contentType: MIME.IMAGE_GIF,
}; };
assert.isTrue(Attachment.isVisualMedia(attachment)); assert.isTrue(Attachment.isVisualMedia(attachment));
@ -112,7 +112,7 @@ describe('Attachment', () => {
it('should return true for videos', () => { it('should return true for videos', () => {
const attachment: Attachment.AttachmentType = { const attachment: Attachment.AttachmentType = {
fileName: 'meme.mp4', fileName: 'meme.mp4',
data: stringToArrayBuffer('mp4'), data: Bytes.fromString('mp4'),
contentType: MIME.VIDEO_MP4, contentType: MIME.VIDEO_MP4,
}; };
assert.isTrue(Attachment.isVisualMedia(attachment)); assert.isTrue(Attachment.isVisualMedia(attachment));
@ -122,7 +122,7 @@ describe('Attachment', () => {
const attachment: Attachment.AttachmentType = { const attachment: Attachment.AttachmentType = {
fileName: 'Voice Message.aac', fileName: 'Voice Message.aac',
flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE, flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE,
data: stringToArrayBuffer('voice message'), data: Bytes.fromString('voice message'),
contentType: MIME.AUDIO_AAC, contentType: MIME.AUDIO_AAC,
}; };
assert.isFalse(Attachment.isVisualMedia(attachment)); assert.isFalse(Attachment.isVisualMedia(attachment));
@ -131,7 +131,7 @@ describe('Attachment', () => {
it('should return false for other attachments', () => { it('should return false for other attachments', () => {
const attachment: Attachment.AttachmentType = { const attachment: Attachment.AttachmentType = {
fileName: 'foo.json', fileName: 'foo.json',
data: stringToArrayBuffer('{"foo": "bar"}'), data: Bytes.fromString('{"foo": "bar"}'),
contentType: MIME.APPLICATION_JSON, contentType: MIME.APPLICATION_JSON,
}; };
assert.isFalse(Attachment.isVisualMedia(attachment)); assert.isFalse(Attachment.isVisualMedia(attachment));
@ -142,7 +142,7 @@ describe('Attachment', () => {
it('should return true for JSON', () => { it('should return true for JSON', () => {
const attachment: Attachment.AttachmentType = { const attachment: Attachment.AttachmentType = {
fileName: 'foo.json', fileName: 'foo.json',
data: stringToArrayBuffer('{"foo": "bar"}'), data: Bytes.fromString('{"foo": "bar"}'),
contentType: MIME.APPLICATION_JSON, contentType: MIME.APPLICATION_JSON,
}; };
assert.isTrue(Attachment.isFile(attachment)); assert.isTrue(Attachment.isFile(attachment));
@ -151,7 +151,7 @@ describe('Attachment', () => {
it('should return false for images', () => { it('should return false for images', () => {
const attachment: Attachment.AttachmentType = { const attachment: Attachment.AttachmentType = {
fileName: 'meme.gif', fileName: 'meme.gif',
data: stringToArrayBuffer('gif'), data: Bytes.fromString('gif'),
contentType: MIME.IMAGE_GIF, contentType: MIME.IMAGE_GIF,
}; };
assert.isFalse(Attachment.isFile(attachment)); assert.isFalse(Attachment.isFile(attachment));
@ -160,7 +160,7 @@ describe('Attachment', () => {
it('should return false for videos', () => { it('should return false for videos', () => {
const attachment: Attachment.AttachmentType = { const attachment: Attachment.AttachmentType = {
fileName: 'meme.mp4', fileName: 'meme.mp4',
data: stringToArrayBuffer('mp4'), data: Bytes.fromString('mp4'),
contentType: MIME.VIDEO_MP4, contentType: MIME.VIDEO_MP4,
}; };
assert.isFalse(Attachment.isFile(attachment)); assert.isFalse(Attachment.isFile(attachment));
@ -170,7 +170,7 @@ describe('Attachment', () => {
const attachment: Attachment.AttachmentType = { const attachment: Attachment.AttachmentType = {
fileName: 'Voice Message.aac', fileName: 'Voice Message.aac',
flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE, flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE,
data: stringToArrayBuffer('voice message'), data: Bytes.fromString('voice message'),
contentType: MIME.AUDIO_AAC, contentType: MIME.AUDIO_AAC,
}; };
assert.isFalse(Attachment.isFile(attachment)); assert.isFalse(Attachment.isFile(attachment));
@ -182,7 +182,7 @@ describe('Attachment', () => {
const attachment: Attachment.AttachmentType = { const attachment: Attachment.AttachmentType = {
fileName: 'Voice Message.aac', fileName: 'Voice Message.aac',
flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE, flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE,
data: stringToArrayBuffer('voice message'), data: Bytes.fromString('voice message'),
contentType: MIME.AUDIO_AAC, contentType: MIME.AUDIO_AAC,
}; };
assert.isTrue(Attachment.isVoiceMessage(attachment)); assert.isTrue(Attachment.isVoiceMessage(attachment));
@ -190,7 +190,7 @@ describe('Attachment', () => {
it('should return true for legacy Android voice message attachment', () => { it('should return true for legacy Android voice message attachment', () => {
const attachment: Attachment.AttachmentType = { const attachment: Attachment.AttachmentType = {
data: stringToArrayBuffer('voice message'), data: Bytes.fromString('voice message'),
contentType: MIME.AUDIO_MP3, contentType: MIME.AUDIO_MP3,
}; };
assert.isTrue(Attachment.isVoiceMessage(attachment)); assert.isTrue(Attachment.isVoiceMessage(attachment));
@ -199,7 +199,7 @@ describe('Attachment', () => {
it('should return false for other attachments', () => { it('should return false for other attachments', () => {
const attachment: Attachment.AttachmentType = { const attachment: Attachment.AttachmentType = {
fileName: 'foo.gif', fileName: 'foo.gif',
data: stringToArrayBuffer('foo'), data: Bytes.fromString('foo'),
contentType: MIME.IMAGE_GIF, contentType: MIME.IMAGE_GIF,
}; };
assert.isFalse(Attachment.isVoiceMessage(attachment)); assert.isFalse(Attachment.isVoiceMessage(attachment));
@ -359,7 +359,7 @@ describe('Attachment', () => {
it('should write data to disk and store relative path to it', async () => { it('should write data to disk and store relative path to it', async () => {
const input = { const input = {
contentType: MIME.IMAGE_JPEG, contentType: MIME.IMAGE_JPEG,
data: stringToArrayBuffer('Above us only sky'), data: Bytes.fromString('Above us only sky'),
fileName: 'foo.jpg', fileName: 'foo.jpg',
size: 1111, size: 1111,
}; };
@ -371,8 +371,8 @@ describe('Attachment', () => {
size: 1111, size: 1111,
}; };
const expectedAttachmentData = stringToArrayBuffer('Above us only sky'); const expectedAttachmentData = Bytes.fromString('Above us only sky');
const writeNewAttachmentData = async (attachmentData: ArrayBuffer) => { const writeNewAttachmentData = async (attachmentData: Uint8Array) => {
assert.deepEqual(attachmentData, expectedAttachmentData); assert.deepEqual(attachmentData, expectedAttachmentData);
return 'abc/abcdefgh123456789'; return 'abc/abcdefgh123456789';
}; };
@ -419,7 +419,7 @@ describe('Attachment', () => {
Attachment.migrateDataToFileSystem(input, { Attachment.migrateDataToFileSystem(input, {
writeNewAttachmentData, writeNewAttachmentData,
}), }),
'Expected `attachment.data` to be an array buffer; got: number' 'Expected `attachment.data` to be a typed array; got: number'
); );
}); });
}); });

View file

@ -6,7 +6,6 @@ import * as sinon from 'sinon';
import { IMAGE_GIF, IMAGE_PNG } from '../../types/MIME'; import { IMAGE_GIF, IMAGE_PNG } from '../../types/MIME';
import { MessageAttributesType } from '../../model-types.d'; import { MessageAttributesType } from '../../model-types.d';
import { stringToArrayBuffer } from '../../util/stringToArrayBuffer';
import { import {
Avatar, Avatar,
Email, Email,
@ -400,7 +399,7 @@ describe('Contact', () => {
otherKey: 'otherValue', otherKey: 'otherValue',
avatar: { avatar: {
contentType: 'image/png', contentType: 'image/png',
data: stringToArrayBuffer('Its easy if you try'), data: Buffer.from('Its easy if you try'),
}, },
} as unknown) as Avatar, } as unknown) as Avatar,
}, },

View file

@ -7,7 +7,7 @@ import * as Message from '../../../types/message/initializeAttachmentMetadata';
import { IncomingMessage } from '../../../types/Message'; import { IncomingMessage } from '../../../types/Message';
import { SignalService } from '../../../protobuf'; import { SignalService } from '../../../protobuf';
import * as MIME from '../../../types/MIME'; import * as MIME from '../../../types/MIME';
import { stringToArrayBuffer } from '../../../util/stringToArrayBuffer'; import * as Bytes from '../../../Bytes';
describe('Message', () => { describe('Message', () => {
describe('initializeAttachmentMetadata', () => { describe('initializeAttachmentMetadata', () => {
@ -22,7 +22,7 @@ describe('Message', () => {
attachments: [ attachments: [
{ {
contentType: MIME.IMAGE_JPEG, contentType: MIME.IMAGE_JPEG,
data: stringToArrayBuffer('foo'), data: Bytes.fromString('foo'),
fileName: 'foo.jpg', fileName: 'foo.jpg',
size: 1111, size: 1111,
}, },
@ -38,7 +38,7 @@ describe('Message', () => {
attachments: [ attachments: [
{ {
contentType: MIME.IMAGE_JPEG, contentType: MIME.IMAGE_JPEG,
data: stringToArrayBuffer('foo'), data: Bytes.fromString('foo'),
fileName: 'foo.jpg', fileName: 'foo.jpg',
size: 1111, size: 1111,
}, },
@ -63,7 +63,7 @@ describe('Message', () => {
attachments: [ attachments: [
{ {
contentType: MIME.APPLICATION_OCTET_STREAM, contentType: MIME.APPLICATION_OCTET_STREAM,
data: stringToArrayBuffer('foo'), data: Bytes.fromString('foo'),
fileName: 'foo.bin', fileName: 'foo.bin',
size: 1111, size: 1111,
}, },
@ -79,7 +79,7 @@ describe('Message', () => {
attachments: [ attachments: [
{ {
contentType: MIME.APPLICATION_OCTET_STREAM, contentType: MIME.APPLICATION_OCTET_STREAM,
data: stringToArrayBuffer('foo'), data: Bytes.fromString('foo'),
fileName: 'foo.bin', fileName: 'foo.bin',
size: 1111, size: 1111,
}, },
@ -105,7 +105,7 @@ describe('Message', () => {
{ {
contentType: MIME.AUDIO_AAC, contentType: MIME.AUDIO_AAC,
flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE, flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE,
data: stringToArrayBuffer('foo'), data: Bytes.fromString('foo'),
fileName: 'Voice Message.aac', fileName: 'Voice Message.aac',
size: 1111, size: 1111,
}, },
@ -122,7 +122,7 @@ describe('Message', () => {
{ {
contentType: MIME.AUDIO_AAC, contentType: MIME.AUDIO_AAC,
flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE, flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE,
data: stringToArrayBuffer('foo'), data: Bytes.fromString('foo'),
fileName: 'Voice Message.aac', fileName: 'Voice Message.aac',
size: 1111, size: 1111,
}, },
@ -147,7 +147,7 @@ describe('Message', () => {
attachments: [ attachments: [
{ {
contentType: MIME.LONG_MESSAGE, contentType: MIME.LONG_MESSAGE,
data: stringToArrayBuffer('foo'), data: Bytes.fromString('foo'),
fileName: 'message.txt', fileName: 'message.txt',
size: 1111, size: 1111,
}, },
@ -163,7 +163,7 @@ describe('Message', () => {
attachments: [ attachments: [
{ {
contentType: MIME.LONG_MESSAGE, contentType: MIME.LONG_MESSAGE,
data: stringToArrayBuffer('foo'), data: Bytes.fromString('foo'),
fileName: 'message.txt', fileName: 'message.txt',
size: 1111, size: 1111,
}, },

View file

@ -4,8 +4,6 @@
import { assert } from 'chai'; import { assert } from 'chai';
import { size } from '../../util/iterables'; import { size } from '../../util/iterables';
import { typedArrayToArrayBuffer } from '../../Crypto';
import { getProvisioningUrl } from '../../util/getProvisioningUrl'; import { getProvisioningUrl } from '../../util/getProvisioningUrl';
// It'd be nice to run these tests in the renderer, too, but [Chromium's `URL` doesn't // 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 uuid = 'a08bf1fd-1799-427f-a551-70af747e3956';
const publicKey = new Uint8Array([9, 8, 7, 6, 5, 4, 3]); 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); const resultUrl = new URL(result);
assert.strictEqual(resultUrl.protocol, 'sgnl:'); assert.strictEqual(resultUrl.protocol, 'sgnl:');

View file

@ -13,8 +13,6 @@ import {
IMAGE_WEBP, IMAGE_WEBP,
} from '../../types/MIME'; } from '../../types/MIME';
import { typedArrayToArrayBuffer } from '../../Crypto';
import { sniffImageMimeType } from '../../util/sniffImageMimeType'; import { sniffImageMimeType } from '../../util/sniffImageMimeType';
describe('sniffImageMimeType', () => { describe('sniffImageMimeType', () => {
@ -89,11 +87,4 @@ describe('sniffImageMimeType', () => {
IMAGE_JPEG IMAGE_JPEG
); );
}); });
it('handles ArrayBuffers', async () => {
const arrayBuffer = typedArrayToArrayBuffer(
await fixture('kitten-1-64-64.jpg')
);
assert.strictEqual(sniffImageMimeType(arrayBuffer), IMAGE_JPEG);
});
}); });

2
ts/textsecure.d.ts vendored
View file

@ -3,7 +3,6 @@
import { UnidentifiedSenderMessageContent } from '@signalapp/signal-client'; import { UnidentifiedSenderMessageContent } from '@signalapp/signal-client';
import Crypto from './textsecure/Crypto';
import MessageSender from './textsecure/SendMessage'; import MessageSender from './textsecure/SendMessage';
import SyncRequest from './textsecure/SyncRequest'; import SyncRequest from './textsecure/SyncRequest';
import EventTarget from './textsecure/EventTarget'; import EventTarget from './textsecure/EventTarget';
@ -37,7 +36,6 @@ export type UnprocessedType = {
export { StorageServiceCallOptionsType, StorageServiceCredentials }; export { StorageServiceCallOptionsType, StorageServiceCredentials };
export type TextSecureType = { export type TextSecureType = {
crypto: typeof Crypto;
storage: Storage; storage: Storage;
server: WebAPIType; server: WebAPIType;
messageSender: MessageSender; messageSender: MessageSender;

View file

@ -13,7 +13,6 @@ import EventTarget from './EventTarget';
import type { WebAPIType } from './WebAPI'; import type { WebAPIType } from './WebAPI';
import { HTTPError } from './Errors'; import { HTTPError } from './Errors';
import { KeyPairType, CompatSignedPreKeyType } from './Types.d'; import { KeyPairType, CompatSignedPreKeyType } from './Types.d';
import utils from './Helpers';
import ProvisioningCipher from './ProvisioningCipher'; import ProvisioningCipher from './ProvisioningCipher';
import { IncomingWebSocketRequest } from './WebsocketResources'; import { IncomingWebSocketRequest } from './WebsocketResources';
import createTaskWithTimeout from './TaskWithTimeout'; import createTaskWithTimeout from './TaskWithTimeout';
@ -22,8 +21,8 @@ import {
deriveAccessKey, deriveAccessKey,
generateRegistrationId, generateRegistrationId,
getRandomBytes, getRandomBytes,
typedArrayToArrayBuffer, decryptDeviceName,
arrayBufferToBase64, encryptDeviceName,
} from '../Crypto'; } from '../Crypto';
import { import {
generateKeyPair, generateKeyPair,
@ -45,9 +44,6 @@ const PREKEY_ROTATION_AGE = DAY * 1.5;
const PROFILE_KEY_LENGTH = 32; const PROFILE_KEY_LENGTH = 32;
const SIGNED_KEY_GEN_BATCH_SIZE = 100; const SIGNED_KEY_GEN_BATCH_SIZE = 100;
// TODO: remove once we move away from ArrayBuffers
const FIXMEU8 = Uint8Array;
function getIdentifier(id: string | undefined) { function getIdentifier(id: string | undefined) {
if (!id || !id.length) { if (!id || !id.length) {
return id; return id;
@ -64,15 +60,15 @@ function getIdentifier(id: string | undefined) {
export type GeneratedKeysType = { export type GeneratedKeysType = {
preKeys: Array<{ preKeys: Array<{
keyId: number; keyId: number;
publicKey: ArrayBuffer; publicKey: Uint8Array;
}>; }>;
signedPreKey: { signedPreKey: {
keyId: number; keyId: number;
publicKey: ArrayBuffer; publicKey: Uint8Array;
signature: ArrayBuffer; signature: Uint8Array;
keyPair: KeyPairType; keyPair: KeyPairType;
}; };
identityKey: ArrayBuffer; identityKey: Uint8Array;
}; };
export default class AccountManager extends EventTarget { export default class AccountManager extends EventTarget {
@ -94,19 +90,16 @@ export default class AccountManager extends EventTarget {
return this.server.requestVerificationSMS(number); return this.server.requestVerificationSMS(number);
} }
async encryptDeviceName(name: string, identityKey: KeyPairType) { encryptDeviceName(name: string, identityKey: KeyPairType) {
if (!name) { if (!name) {
return null; return null;
} }
const encrypted = await window.Signal.Crypto.encryptDeviceName( const encrypted = encryptDeviceName(name, identityKey.pubKey);
name,
identityKey.pubKey
);
const proto = new Proto.DeviceName(); const proto = new Proto.DeviceName();
proto.ephemeralPublic = new FIXMEU8(encrypted.ephemeralPublic); proto.ephemeralPublic = encrypted.ephemeralPublic;
proto.syntheticIv = new FIXMEU8(encrypted.syntheticIv); proto.syntheticIv = encrypted.syntheticIv;
proto.ciphertext = new FIXMEU8(encrypted.ciphertext); proto.ciphertext = encrypted.ciphertext;
const bytes = Proto.DeviceName.encode(proto).finish(); const bytes = Proto.DeviceName.encode(proto).finish();
return Bytes.toBase64(bytes); return Bytes.toBase64(bytes);
@ -127,16 +120,8 @@ export default class AccountManager extends EventTarget {
proto.ephemeralPublic && proto.syntheticIv && proto.ciphertext, proto.ephemeralPublic && proto.syntheticIv && proto.ciphertext,
'Missing required fields in DeviceName' '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( const name = decryptDeviceName(proto, identityKey.privKey);
encrypted,
identityKey.privKey
);
return name; return name;
} }
@ -155,10 +140,7 @@ export default class AccountManager extends EventTarget {
identityKeyPair !== undefined, identityKeyPair !== undefined,
"Can't encrypt device name without identity key pair" "Can't encrypt device name without identity key pair"
); );
const base64 = await this.encryptDeviceName( const base64 = this.encryptDeviceName(deviceName || '', identityKeyPair);
deviceName || '',
identityKeyPair
);
if (base64) { if (base64) {
await this.server.updateDeviceName(base64); await this.server.updateDeviceName(base64);
@ -173,7 +155,7 @@ export default class AccountManager extends EventTarget {
return this.queueTask(async () => { return this.queueTask(async () => {
const identityKeyPair = generateKeyPair(); const identityKeyPair = generateKeyPair();
const profileKey = getRandomBytes(PROFILE_KEY_LENGTH); const profileKey = getRandomBytes(PROFILE_KEY_LENGTH);
const accessKey = await deriveAccessKey(profileKey); const accessKey = deriveAccessKey(profileKey);
await this.createAccount( await this.createAccount(
number, number,
@ -469,15 +451,15 @@ export default class AccountManager extends EventTarget {
number: string, number: string,
verificationCode: string, verificationCode: string,
identityKeyPair: KeyPairType, identityKeyPair: KeyPairType,
profileKey: ArrayBuffer | undefined, profileKey: Uint8Array | undefined,
deviceName: string | null, deviceName: string | null,
userAgent?: string | null, userAgent?: string | null,
readReceipts?: boolean | null, readReceipts?: boolean | null,
options: { accessKey?: ArrayBuffer; uuid?: string } = {} options: { accessKey?: Uint8Array; uuid?: string } = {}
): Promise<void> { ): Promise<void> {
const { storage } = window.textsecure; const { storage } = window.textsecure;
const { accessKey, uuid } = options; const { accessKey, uuid } = options;
let password = btoa(utils.getString(getRandomBytes(16))); let password = Bytes.toBase64(getRandomBytes(16));
password = password.substring(0, password.length - 2); password = password.substring(0, password.length - 2);
const registrationId = generateRegistrationId(); const registrationId = generateRegistrationId();
@ -486,10 +468,7 @@ export default class AccountManager extends EventTarget {
let encryptedDeviceName; let encryptedDeviceName;
if (deviceName) { if (deviceName) {
encryptedDeviceName = await this.encryptDeviceName( encryptedDeviceName = this.encryptDeviceName(deviceName, identityKeyPair);
deviceName,
identityKeyPair
);
await this.deviceNameIsEncrypted(); await this.deviceNameIsEncrypted();
} }
@ -601,8 +580,8 @@ export default class AccountManager extends EventTarget {
const identityKeyMap = { const identityKeyMap = {
...(storage.get('identityKeyMap') || {}), ...(storage.get('identityKeyMap') || {}),
[ourUuid]: { [ourUuid]: {
pubKey: arrayBufferToBase64(identityKeyPair.pubKey), pubKey: Bytes.toBase64(identityKeyPair.pubKey),
privKey: arrayBufferToBase64(identityKeyPair.privKey), privKey: Bytes.toBase64(identityKeyPair.privKey),
}, },
}; };
const registrationIdMap = { const registrationIdMap = {

View file

@ -7,7 +7,6 @@ import { Reader } from 'protobufjs';
import { SignalService as Proto } from '../protobuf'; import { SignalService as Proto } from '../protobuf';
import { normalizeUuid } from '../util/normalizeUuid'; import { normalizeUuid } from '../util/normalizeUuid';
import { typedArrayToArrayBuffer } from '../Crypto';
import * as log from '../logging/log'; import * as log from '../logging/log';
import Avatar = Proto.ContactDetails.IAvatar; import Avatar = Proto.ContactDetails.IAvatar;
@ -22,24 +21,21 @@ export type MessageWithAvatar<Message extends OptionalAvatar> = Omit<
Message, Message,
'avatar' 'avatar'
> & { > & {
avatar?: (Avatar & { data: ArrayBuffer }) | null; avatar?: (Avatar & { data: Uint8Array }) | null;
}; };
export type ModifiedGroupDetails = MessageWithAvatar<Proto.GroupDetails>; export type ModifiedGroupDetails = MessageWithAvatar<Proto.GroupDetails>;
export type ModifiedContactDetails = MessageWithAvatar<Proto.ContactDetails>; export type ModifiedContactDetails = MessageWithAvatar<Proto.ContactDetails>;
// TODO: remove once we move away from ArrayBuffers
const FIXMEU8 = Uint8Array;
class ParserBase< class ParserBase<
Message extends OptionalAvatar, Message extends OptionalAvatar,
Decoder extends DecoderBase<Message> Decoder extends DecoderBase<Message>
> { > {
protected readonly reader: Reader; protected readonly reader: Reader;
constructor(arrayBuffer: ArrayBuffer, private readonly decoder: Decoder) { constructor(bytes: Uint8Array, private readonly decoder: Decoder) {
this.reader = new Reader(new FIXMEU8(arrayBuffer)); this.reader = new Reader(bytes);
} }
protected decodeDelimited(): MessageWithAvatar<Message> | undefined { protected decodeDelimited(): MessageWithAvatar<Message> | undefined {
@ -74,7 +70,7 @@ class ParserBase<
avatar: { avatar: {
...proto.avatar, ...proto.avatar,
data: typedArrayToArrayBuffer(avatarData), data: avatarData,
}, },
}; };
} catch (error) { } catch (error) {
@ -91,7 +87,7 @@ export class GroupBuffer extends ParserBase<
Proto.GroupDetails, Proto.GroupDetails,
typeof Proto.GroupDetails typeof Proto.GroupDetails
> { > {
constructor(arrayBuffer: ArrayBuffer) { constructor(arrayBuffer: Uint8Array) {
super(arrayBuffer, Proto.GroupDetails); super(arrayBuffer, Proto.GroupDetails);
} }
@ -124,7 +120,7 @@ export class ContactBuffer extends ParserBase<
Proto.ContactDetails, Proto.ContactDetails,
typeof Proto.ContactDetails typeof Proto.ContactDetails
> { > {
constructor(arrayBuffer: ArrayBuffer) { constructor(arrayBuffer: Uint8Array) {
super(arrayBuffer, Proto.ContactDetails); super(arrayBuffer, Proto.ContactDetails);
} }

View file

@ -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<void> {
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<ArrayBuffer> {
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<EncryptedAttachment> {
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<ArrayBuffer> {
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<ArrayBuffer> {
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<ArrayBuffer> {
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;

View file

@ -77,14 +77,14 @@ export class ReplayableError extends Error {
export class OutgoingIdentityKeyError extends ReplayableError { export class OutgoingIdentityKeyError extends ReplayableError {
identifier: string; identifier: string;
identityKey: ArrayBuffer; identityKey: Uint8Array;
// Note: Data to resend message is no longer captured // Note: Data to resend message is no longer captured
constructor( constructor(
incomingIdentifier: string, incomingIdentifier: string,
_m: ArrayBuffer, _m: Uint8Array,
_t: number, _t: number,
identityKey: ArrayBuffer identityKey: Uint8Array
) { ) {
const identifier = incomingIdentifier.split('.')[0]; const identifier = incomingIdentifier.split('.')[0];
@ -188,7 +188,7 @@ export class SendMessageProtoError extends Error implements CallbackResultType {
public readonly unidentifiedDeliveries?: Array<string>; public readonly unidentifiedDeliveries?: Array<string>;
public readonly dataMessage?: ArrayBuffer; public readonly dataMessage?: Uint8Array;
// Fields necesary for send log save // Fields necesary for send log save
public readonly contentHint?: number; public readonly contentHint?: number;

View file

@ -1,8 +1,6 @@
// Copyright 2020 Signal Messenger, LLC // Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { stringToArrayBuffer } from '../util/stringToArrayBuffer';
/* eslint-disable guard-for-in */ /* eslint-disable guard-for-in */
/* eslint-disable no-restricted-syntax */ /* eslint-disable no-restricted-syntax */
/* eslint-disable no-proto */ /* eslint-disable no-proto */
@ -71,7 +69,6 @@ const utils = {
number[0] === '+' && /^[0-9]+$/.test(number.substring(1)), number[0] === '+' && /^[0-9]+$/.test(number.substring(1)),
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
jsonThing: (thing: unknown) => JSON.stringify(ensureStringed(thing)), jsonThing: (thing: unknown) => JSON.stringify(ensureStringed(thing)),
stringToArrayBuffer,
unencodeNumber: (number: string): Array<string> => number.split('.'), unencodeNumber: (number: string): Array<string> => number.split('.'),
}; };

View file

@ -43,7 +43,7 @@ import { normalizeUuid } from '../util/normalizeUuid';
import { normalizeNumber } from '../util/normalizeNumber'; import { normalizeNumber } from '../util/normalizeNumber';
import { parseIntOrThrow } from '../util/parseIntOrThrow'; import { parseIntOrThrow } from '../util/parseIntOrThrow';
import { Zone } from '../util/Zone'; import { Zone } from '../util/Zone';
import { deriveMasterKeyFromGroupV1, typedArrayToArrayBuffer } from '../Crypto'; import { deriveMasterKeyFromGroupV1 } from '../Crypto';
import { DownloadedAttachmentType } from '../types/Attachment'; import { DownloadedAttachmentType } from '../types/Attachment';
import { Address } from '../types/Address'; import { Address } from '../types/Address';
import { QualifiedAddress } from '../types/QualifiedAddress'; import { QualifiedAddress } from '../types/QualifiedAddress';
@ -103,9 +103,6 @@ import {
} from './messageReceiverEvents'; } from './messageReceiverEvents';
import * as log from '../logging/log'; import * as log from '../logging/log';
// TODO: remove once we move away from ArrayBuffers
const FIXMEU8 = Uint8Array;
const GROUPV1_ID_LENGTH = 16; const GROUPV1_ID_LENGTH = 16;
const GROUPV2_ID_LENGTH = 32; const GROUPV2_ID_LENGTH = 32;
const RETRY_TIMEOUT = 2 * 60 * 1000; const RETRY_TIMEOUT = 2 * 60 * 1000;
@ -1137,9 +1134,9 @@ export default class MessageReceiver
if ( if (
!verifySignature( !verifySignature(
typedArrayToArrayBuffer(this.serverTrustRoot), this.serverTrustRoot,
typedArrayToArrayBuffer(serverCertificate.certificateData()), serverCertificate.certificateData(),
typedArrayToArrayBuffer(serverCertificate.signature()) serverCertificate.signature()
) )
) { ) {
throw new Error( throw new Error(
@ -1150,9 +1147,9 @@ export default class MessageReceiver
if ( if (
!verifySignature( !verifySignature(
typedArrayToArrayBuffer(serverCertificate.key().serialize()), serverCertificate.key().serialize(),
typedArrayToArrayBuffer(certificate.certificate()), certificate.certificate(),
typedArrayToArrayBuffer(certificate.signature()) certificate.signature()
) )
) { ) {
throw new Error( throw new Error(
@ -1448,9 +1445,7 @@ export default class MessageReceiver
ciphertext: Uint8Array ciphertext: Uint8Array
): Promise<Uint8Array | undefined> { ): Promise<Uint8Array | undefined> {
try { try {
const plaintext = await this.innerDecrypt(stores, envelope, ciphertext); return await this.innerDecrypt(stores, envelope, ciphertext);
return new FIXMEU8(plaintext);
} catch (error) { } catch (error) {
this.removeFromCache(envelope); this.removeFromCache(envelope);
const uuid = envelope.sourceUuid; const uuid = envelope.sourceUuid;
@ -1486,9 +1481,7 @@ export default class MessageReceiver
if (uuid && deviceId) { if (uuid && deviceId) {
const { usmc } = envelope; const { usmc } = envelope;
const event = new DecryptionErrorEvent({ const event = new DecryptionErrorEvent({
cipherTextBytes: usmc cipherTextBytes: usmc ? usmc.contents() : undefined,
? typedArrayToArrayBuffer(usmc.contents())
: undefined,
cipherTextType: usmc ? usmc.msgType() : undefined, cipherTextType: usmc ? usmc.msgType() : undefined,
contentHint: envelope.contentHint, contentHint: envelope.contentHint,
groupId: envelope.groupId, groupId: envelope.groupId,
@ -1955,7 +1948,7 @@ export default class MessageReceiver
if (groupId && groupId.byteLength > 0) { if (groupId && groupId.byteLength > 0) {
if (groupId.byteLength === GROUPV1_ID_LENGTH) { if (groupId.byteLength === GROUPV1_ID_LENGTH) {
groupIdString = Bytes.toBinary(groupId); groupIdString = Bytes.toBinary(groupId);
groupV2IdString = await this.deriveGroupV2FromV1(groupId); groupV2IdString = this.deriveGroupV2FromV1(groupId);
} else if (groupId.byteLength === GROUPV2_ID_LENGTH) { } else if (groupId.byteLength === GROUPV2_ID_LENGTH) {
groupV2IdString = Bytes.toBase64(groupId); groupV2IdString = Bytes.toBase64(groupId);
} else { } else {
@ -2024,16 +2017,14 @@ export default class MessageReceiver
return false; return false;
} }
private async deriveGroupV2FromV1(groupId: Uint8Array): Promise<string> { private deriveGroupV2FromV1(groupId: Uint8Array): string {
if (groupId.byteLength !== GROUPV1_ID_LENGTH) { if (groupId.byteLength !== GROUPV1_ID_LENGTH) {
throw new Error( throw new Error(
`deriveGroupV2FromV1: had id with wrong byteLength: ${groupId.byteLength}` `deriveGroupV2FromV1: had id with wrong byteLength: ${groupId.byteLength}`
); );
} }
const masterKey = await deriveMasterKeyFromGroupV1( const masterKey = deriveMasterKeyFromGroupV1(groupId);
typedArrayToArrayBuffer(groupId) const data = deriveGroupFields(masterKey);
);
const data = deriveGroupFields(new FIXMEU8(masterKey));
return Bytes.toBase64(data.id); return Bytes.toBase64(data.id);
} }
@ -2246,7 +2237,7 @@ export default class MessageReceiver
if (groupId && groupId.byteLength > 0) { if (groupId && groupId.byteLength > 0) {
if (groupId.byteLength === GROUPV1_ID_LENGTH) { if (groupId.byteLength === GROUPV1_ID_LENGTH) {
groupIdString = Bytes.toBinary(groupId); groupIdString = Bytes.toBinary(groupId);
groupV2IdString = await this.deriveGroupV2FromV1(groupId); groupV2IdString = this.deriveGroupV2FromV1(groupId);
} else if (groupId.byteLength === GROUPV2_ID_LENGTH) { } else if (groupId.byteLength === GROUPV2_ID_LENGTH) {
groupV2IdString = Bytes.toBase64(groupId); groupV2IdString = Bytes.toBase64(groupId);
} else { } else {
@ -2300,7 +2291,7 @@ export default class MessageReceiver
} }
const ev = new KeysEvent( const ev = new KeysEvent(
typedArrayToArrayBuffer(sync.storageService), sync.storageService,
this.removeFromCache.bind(this, envelope) this.removeFromCache.bind(this, envelope)
); );
@ -2343,9 +2334,7 @@ export default class MessageReceiver
'handleVerified.destinationUuid' 'handleVerified.destinationUuid'
) )
: undefined, : undefined,
identityKey: verified.identityKey identityKey: verified.identityKey ? verified.identityKey : undefined,
? typedArrayToArrayBuffer(verified.identityKey)
: undefined,
}, },
this.removeFromCache.bind(this, envelope) this.removeFromCache.bind(this, envelope)
); );

View file

@ -37,7 +37,6 @@ import { Address } from '../types/Address';
import { QualifiedAddress } from '../types/QualifiedAddress'; import { QualifiedAddress } from '../types/QualifiedAddress';
import { UUID } from '../types/UUID'; import { UUID } from '../types/UUID';
import { Sessions, IdentityKeys } from '../LibSignalStores'; import { Sessions, IdentityKeys } from '../LibSignalStores';
import { typedArrayToArrayBuffer as toArrayBuffer } from '../Crypto';
import { updateConversationsWithUuidLookup } from '../updateConversationsWithUuidLookup'; import { updateConversationsWithUuidLookup } from '../updateConversationsWithUuidLookup';
import { getKeysForIdentifier } from './getKeysForIdentifier'; import { getKeysForIdentifier } from './getKeysForIdentifier';
import { SignalService as Proto } from '../protobuf'; import { SignalService as Proto } from '../protobuf';
@ -63,7 +62,7 @@ type SendMetadata = {
export const serializedCertificateSchema = z export const serializedCertificateSchema = z
.object({ .object({
expires: z.number().optional(), expires: z.number().optional(),
serialized: z.instanceof(ArrayBuffer), serialized: z.instanceof(Uint8Array),
}) })
.nonstrict(); .nonstrict();
@ -331,7 +330,7 @@ export default class OutgoingMessage {
}); });
} }
getPlaintext(): ArrayBuffer { getPlaintext(): Uint8Array {
if (!this.plaintext) { if (!this.plaintext) {
const { message } = this; const { message } = this;
@ -341,7 +340,7 @@ export default class OutgoingMessage {
this.plaintext = message.serialize(); this.plaintext = message.serialize();
} }
} }
return toArrayBuffer(this.plaintext); return this.plaintext;
} }
getContentProtoBytes(): Uint8Array | undefined { getContentProtoBytes(): Uint8Array | undefined {
@ -720,7 +719,7 @@ export default class OutgoingMessage {
identifier, identifier,
error.originalMessage, error.originalMessage,
error.timestamp, error.timestamp,
error.identityKey error.identityKey && new Uint8Array(error.identityKey)
); );
this.registerError(identifier, 'Untrusted identity', newError); this.registerError(identifier, 'Untrusted identity', newError);
} else { } else {

View file

@ -5,21 +5,17 @@
/* eslint-disable max-classes-per-file */ /* eslint-disable max-classes-per-file */
import { KeyPairType } from './Types.d'; import { KeyPairType } from './Types.d';
import * as Bytes from '../Bytes';
import { import {
decryptAes256CbcPkcsPadding, decryptAes256CbcPkcsPadding,
deriveSecrets, deriveSecrets,
bytesFromString,
verifyHmacSha256, verifyHmacSha256,
typedArrayToArrayBuffer,
} from '../Crypto'; } from '../Crypto';
import { calculateAgreement, createKeyPair, generateKeyPair } from '../Curve'; import { calculateAgreement, createKeyPair, generateKeyPair } from '../Curve';
import { SignalService as Proto } from '../protobuf'; import { SignalService as Proto } from '../protobuf';
import { strictAssert } from '../util/assert'; import { strictAssert } from '../util/assert';
import { normalizeUuid } from '../util/normalizeUuid'; import { normalizeUuid } from '../util/normalizeUuid';
// TODO: remove once we move away from ArrayBuffers
const FIXMEU8 = Uint8Array;
type ProvisionDecryptResult = { type ProvisionDecryptResult = {
identityKeyPair: KeyPairType; identityKeyPair: KeyPairType;
number?: string; number?: string;
@ -27,7 +23,7 @@ type ProvisionDecryptResult = {
provisioningCode?: string; provisioningCode?: string;
userAgent?: string; userAgent?: string;
readReceipts?: boolean; readReceipts?: boolean;
profileKey?: ArrayBuffer; profileKey?: Uint8Array;
}; };
class ProvisioningCipherInner { class ProvisioningCipherInner {
@ -55,34 +51,20 @@ class ProvisioningCipherInner {
throw new Error('ProvisioningCipher.decrypt: No keypair!'); throw new Error('ProvisioningCipher.decrypt: No keypair!');
} }
const ecRes = calculateAgreement( const ecRes = calculateAgreement(masterEphemeral, this.keyPair.privKey);
typedArrayToArrayBuffer(masterEphemeral),
this.keyPair.privKey
);
const keys = deriveSecrets( const keys = deriveSecrets(
ecRes, ecRes,
new ArrayBuffer(32), new Uint8Array(32),
bytesFromString('TextSecure Provisioning Message') Bytes.fromString('TextSecure Provisioning Message')
);
await verifyHmacSha256(
typedArrayToArrayBuffer(ivAndCiphertext),
keys[1],
typedArrayToArrayBuffer(mac),
32
); );
verifyHmacSha256(ivAndCiphertext, keys[1], mac, 32);
const plaintext = await decryptAes256CbcPkcsPadding( const plaintext = decryptAes256CbcPkcsPadding(keys[0], ciphertext, iv);
keys[0], const provisionMessage = Proto.ProvisionMessage.decode(plaintext);
typedArrayToArrayBuffer(ciphertext),
typedArrayToArrayBuffer(iv)
);
const provisionMessage = Proto.ProvisionMessage.decode(
new FIXMEU8(plaintext)
);
const privKey = provisionMessage.identityKeyPrivate; const privKey = provisionMessage.identityKeyPrivate;
strictAssert(privKey, 'Missing identityKeyPrivate in ProvisionMessage'); strictAssert(privKey, 'Missing identityKeyPrivate in ProvisionMessage');
const keyPair = createKeyPair(typedArrayToArrayBuffer(privKey)); const keyPair = createKeyPair(privKey);
const { uuid } = provisionMessage; const { uuid } = provisionMessage;
strictAssert(uuid, 'Missing uuid in provisioning message'); strictAssert(uuid, 'Missing uuid in provisioning message');
@ -96,12 +78,12 @@ class ProvisioningCipherInner {
readReceipts: provisionMessage.readReceipts, readReceipts: provisionMessage.readReceipts,
}; };
if (provisionMessage.profileKey) { if (provisionMessage.profileKey) {
ret.profileKey = typedArrayToArrayBuffer(provisionMessage.profileKey); ret.profileKey = provisionMessage.profileKey;
} }
return ret; return ret;
} }
async getPublicKey(): Promise<ArrayBuffer> { async getPublicKey(): Promise<Uint8Array> {
if (!this.keyPair) { if (!this.keyPair) {
this.keyPair = generateKeyPair(); this.keyPair = generateKeyPair();
} }
@ -126,5 +108,5 @@ export default class ProvisioningCipher {
provisionEnvelope: Proto.ProvisionEnvelope provisionEnvelope: Proto.ProvisionEnvelope
) => Promise<ProvisionDecryptResult>; ) => Promise<ProvisionDecryptResult>;
getPublicKey: () => Promise<ArrayBuffer>; getPublicKey: () => Promise<Uint8Array>;
} }

View file

@ -39,14 +39,8 @@ import OutgoingMessage, {
SerializedCertificateType, SerializedCertificateType,
SendLogCallbackType, SendLogCallbackType,
} from './OutgoingMessage'; } from './OutgoingMessage';
import Crypto from './Crypto';
import * as Bytes from '../Bytes'; import * as Bytes from '../Bytes';
import { import { getRandomBytes, getZeroes, encryptAttachment } from '../Crypto';
concatenateBytes,
getRandomBytes,
getZeroes,
typedArrayToArrayBuffer,
} from '../Crypto';
import { import {
StorageServiceCallOptionsType, StorageServiceCallOptionsType,
StorageServiceCredentials, StorageServiceCredentials,
@ -111,7 +105,7 @@ type GroupCallUpdateType = {
export type AttachmentType = { export type AttachmentType = {
size: number; size: number;
data: ArrayBuffer; data: Uint8Array;
contentType: string; contentType: string;
fileName: string; fileName: string;
@ -137,7 +131,7 @@ export type MessageOptionsType = {
groupV2?: GroupV2InfoType; groupV2?: GroupV2InfoType;
needsSync?: boolean; needsSync?: boolean;
preview?: ReadonlyArray<PreviewType> | null; preview?: ReadonlyArray<PreviewType> | null;
profileKey?: ArrayBuffer; profileKey?: Uint8Array;
quote?: any; quote?: any;
recipients: ReadonlyArray<string>; recipients: ReadonlyArray<string>;
sticker?: any; sticker?: any;
@ -154,7 +148,7 @@ export type GroupSendOptionsType = {
groupV1?: GroupV1InfoType; groupV1?: GroupV1InfoType;
messageText?: string; messageText?: string;
preview?: any; preview?: any;
profileKey?: ArrayBuffer; profileKey?: Uint8Array;
quote?: any; quote?: any;
reaction?: any; reaction?: any;
sticker?: any; sticker?: any;
@ -164,9 +158,6 @@ export type GroupSendOptionsType = {
groupCallUpdate?: GroupCallUpdateType; groupCallUpdate?: GroupCallUpdateType;
}; };
// TODO: remove once we move away from ArrayBuffers
const FIXMEU8 = Uint8Array;
class Message { class Message {
attachments: ReadonlyArray<any>; attachments: ReadonlyArray<any>;
@ -187,7 +178,7 @@ class Message {
preview: any; preview: any;
profileKey?: ArrayBuffer; profileKey?: Uint8Array;
quote?: { quote?: {
id?: number; id?: number;
@ -210,7 +201,7 @@ class Message {
timestamp: number; timestamp: number;
dataMessage: any; dataMessage?: Proto.DataMessage;
attachmentPointers: Array<Proto.IAttachmentPointer> = []; attachmentPointers: Array<Proto.IAttachmentPointer> = [];
@ -298,7 +289,7 @@ class Message {
} }
toProto(): Proto.DataMessage { toProto(): Proto.DataMessage {
if (this.dataMessage instanceof Proto.DataMessage) { if (this.dataMessage) {
return this.dataMessage; return this.dataMessage;
} }
const proto = new Proto.DataMessage(); const proto = new Proto.DataMessage();
@ -405,7 +396,7 @@ class Message {
proto.expireTimer = this.expireTimer; proto.expireTimer = this.expireTimer;
} }
if (this.profileKey) { if (this.profileKey) {
proto.profileKey = new FIXMEU8(this.profileKey); proto.profileKey = this.profileKey;
} }
if (this.deletedForEveryoneTimestamp) { if (this.deletedForEveryoneTimestamp) {
proto.delete = { proto.delete = {
@ -437,10 +428,8 @@ class Message {
return proto; return proto;
} }
toArrayBuffer() { encode() {
return typedArrayToArrayBuffer( return Proto.DataMessage.encode(this.toProto()).finish();
Proto.DataMessage.encode(this.toProto()).finish()
);
} }
} }
@ -489,15 +478,15 @@ export default class MessageSender {
const paddingLength = (new Uint16Array(buffer)[0] & 0x1ff) + 1; const paddingLength = (new Uint16Array(buffer)[0] & 0x1ff) + 1;
// Generate a random padding buffer of the chosen size // Generate a random padding buffer of the chosen size
return new FIXMEU8(getRandomBytes(paddingLength)); return getRandomBytes(paddingLength);
} }
getPaddedAttachment(data: Readonly<ArrayBuffer>): ArrayBuffer { getPaddedAttachment(data: Readonly<Uint8Array>): Uint8Array {
const size = data.byteLength; const size = data.byteLength;
const paddedSize = this._getAttachmentSizeBucket(size); const paddedSize = this._getAttachmentSizeBucket(size);
const padding = getZeroes(paddedSize - size); const padding = getZeroes(paddedSize - size);
return concatenateBytes(data, padding); return Bytes.concatenate([data, padding]);
} }
async makeAttachmentPointer( async makeAttachmentPointer(
@ -509,9 +498,9 @@ export default class MessageSender {
); );
const { data, size } = attachment; const { data, size } = attachment;
if (!(data instanceof ArrayBuffer) && !ArrayBuffer.isView(data)) { if (!(data instanceof Uint8Array)) {
throw new Error( 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) { if (data.byteLength !== size) {
@ -524,15 +513,15 @@ export default class MessageSender {
const key = getRandomBytes(64); const key = getRandomBytes(64);
const iv = getRandomBytes(16); 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 id = await this.server.putAttachment(result.ciphertext);
const proto = new Proto.AttachmentPointer(); const proto = new Proto.AttachmentPointer();
proto.cdnId = Long.fromString(id); proto.cdnId = Long.fromString(id);
proto.contentType = attachment.contentType; proto.contentType = attachment.contentType;
proto.key = new FIXMEU8(key); proto.key = key;
proto.size = attachment.size; proto.size = attachment.size;
proto.digest = new FIXMEU8(result.digest); proto.digest = result.digest;
if (attachment.fileName) { if (attachment.fileName) {
proto.fileName = attachment.fileName; proto.fileName = attachment.fileName;
@ -651,9 +640,9 @@ export default class MessageSender {
async getDataMessage( async getDataMessage(
options: Readonly<MessageOptionsType> options: Readonly<MessageOptionsType>
): Promise<ArrayBuffer> { ): Promise<Uint8Array> {
const message = await this.getHydratedMessage(options); const message = await this.getHydratedMessage(options);
return message.toArrayBuffer(); return message.encode();
} }
async getContentMessage( async getContentMessage(
@ -685,7 +674,7 @@ export default class MessageSender {
getTypingContentMessage( getTypingContentMessage(
options: Readonly<{ options: Readonly<{
recipientId?: string; recipientId?: string;
groupId?: ArrayBuffer; groupId?: Uint8Array;
groupMembers: ReadonlyArray<string>; groupMembers: ReadonlyArray<string>;
isTyping: boolean; isTyping: boolean;
timestamp?: number; timestamp?: number;
@ -705,7 +694,7 @@ export default class MessageSender {
const typingMessage = new Proto.TypingMessage(); const typingMessage = new Proto.TypingMessage();
if (groupId) { if (groupId) {
typingMessage.groupId = new FIXMEU8(groupId); typingMessage.groupId = groupId;
} }
typingMessage.action = action; typingMessage.action = action;
typingMessage.timestamp = finalTimestamp; typingMessage.timestamp = finalTimestamp;
@ -823,7 +812,7 @@ export default class MessageSender {
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
this.sendMessageProto({ this.sendMessageProto({
callback: (res: CallbackResultType) => { callback: (res: CallbackResultType) => {
res.dataMessage = message.toArrayBuffer(); res.dataMessage = message.encode();
if (res.errors && res.errors.length > 0) { if (res.errors && res.errors.length > 0) {
reject(new SendMessageProtoError(res)); reject(new SendMessageProtoError(res));
} else { } else {
@ -988,7 +977,7 @@ export default class MessageSender {
expireTimer: number | undefined; expireTimer: number | undefined;
contentHint: number; contentHint: number;
groupId: string | undefined; groupId: string | undefined;
profileKey?: ArrayBuffer; profileKey?: Uint8Array;
options?: SendOptionsType; options?: SendOptionsType;
}>): Promise<CallbackResultType> { }>): Promise<CallbackResultType> {
return this.sendMessage({ return this.sendMessage({
@ -1026,7 +1015,7 @@ export default class MessageSender {
isUpdate, isUpdate,
options, options,
}: Readonly<{ }: Readonly<{
encodedDataMessage: ArrayBuffer; encodedDataMessage: Uint8Array;
timestamp: number; timestamp: number;
destination: string | undefined; destination: string | undefined;
destinationUuid: string | null | undefined; destinationUuid: string | null | undefined;
@ -1038,9 +1027,7 @@ export default class MessageSender {
}>): Promise<CallbackResultType> { }>): Promise<CallbackResultType> {
const myUuid = window.textsecure.storage.user.getCheckedUuid(); const myUuid = window.textsecure.storage.user.getCheckedUuid();
const dataMessage = Proto.DataMessage.decode( const dataMessage = Proto.DataMessage.decode(encodedDataMessage);
new FIXMEU8(encodedDataMessage)
);
const sentMessage = new Proto.SyncMessage.Sent(); const sentMessage = new Proto.SyncMessage.Sent();
sentMessage.timestamp = timestamp; sentMessage.timestamp = timestamp;
sentMessage.message = dataMessage; sentMessage.message = dataMessage;
@ -1356,7 +1343,7 @@ export default class MessageSender {
responseArgs: Readonly<{ responseArgs: Readonly<{
threadE164?: string; threadE164?: string;
threadUuid?: string; threadUuid?: string;
groupId?: ArrayBuffer; groupId?: Uint8Array;
type: number; type: number;
}>, }>,
options?: Readonly<SendOptionsType> options?: Readonly<SendOptionsType>
@ -1373,7 +1360,7 @@ export default class MessageSender {
response.threadUuid = responseArgs.threadUuid; response.threadUuid = responseArgs.threadUuid;
} }
if (responseArgs.groupId) { if (responseArgs.groupId) {
response.groupId = new FIXMEU8(responseArgs.groupId); response.groupId = responseArgs.groupId;
} }
response.type = responseArgs.type; response.type = responseArgs.type;
syncMessage.messageRequestResponse = response; syncMessage.messageRequestResponse = response;
@ -1435,7 +1422,7 @@ export default class MessageSender {
destinationE164: string | undefined, destinationE164: string | undefined,
destinationUuid: string | undefined, destinationUuid: string | undefined,
state: number, state: number,
identityKey: Readonly<ArrayBuffer>, identityKey: Readonly<Uint8Array>,
options?: Readonly<SendOptionsType> options?: Readonly<SendOptionsType>
): Promise<CallbackResultType> { ): Promise<CallbackResultType> {
const myUuid = window.textsecure.storage.user.getCheckedUuid(); const myUuid = window.textsecure.storage.user.getCheckedUuid();
@ -1468,7 +1455,7 @@ export default class MessageSender {
if (destinationUuid) { if (destinationUuid) {
verified.destinationUuid = destinationUuid; verified.destinationUuid = destinationUuid;
} }
verified.identityKey = new FIXMEU8(identityKey); verified.identityKey = identityKey;
verified.nullMessage = padding; verified.nullMessage = padding;
const syncMessage = this.createSyncMessage(); const syncMessage = this.createSyncMessage();
@ -1491,7 +1478,7 @@ export default class MessageSender {
// Sending messages to contacts // Sending messages to contacts
async sendProfileKeyUpdate( async sendProfileKeyUpdate(
profileKey: Readonly<ArrayBuffer>, profileKey: Readonly<Uint8Array>,
recipients: ReadonlyArray<string>, recipients: ReadonlyArray<string>,
options: Readonly<SendOptionsType>, options: Readonly<SendOptionsType>,
groupId?: string groupId?: string
@ -1712,11 +1699,9 @@ export default class MessageSender {
return sendToContactPromise; return sendToContactPromise;
} }
const buffer = typedArrayToArrayBuffer( const encodedDataMessage = Proto.DataMessage.encode(proto).finish();
Proto.DataMessage.encode(proto).finish()
);
const sendSyncPromise = this.sendSyncMessage({ const sendSyncPromise = this.sendSyncMessage({
encodedDataMessage: buffer, encodedDataMessage,
timestamp, timestamp,
destination: e164, destination: e164,
destinationUuid: uuid, destinationUuid: uuid,
@ -1738,7 +1723,7 @@ export default class MessageSender {
identifier: string, identifier: string,
expireTimer: number | undefined, expireTimer: number | undefined,
timestamp: number, timestamp: number,
profileKey?: Readonly<ArrayBuffer>, profileKey?: Readonly<Uint8Array>,
options?: Readonly<SendOptionsType> options?: Readonly<SendOptionsType>
): Promise<CallbackResultType> { ): Promise<CallbackResultType> {
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
@ -1869,9 +1854,7 @@ export default class MessageSender {
timestamp: number; timestamp: number;
}>): Promise<CallbackResultType> { }>): Promise<CallbackResultType> {
const dataMessage = proto.dataMessage const dataMessage = proto.dataMessage
? typedArrayToArrayBuffer( ? Proto.DataMessage.encode(proto.dataMessage).finish()
Proto.DataMessage.encode(proto.dataMessage).finish()
)
: undefined; : undefined;
const myE164 = window.textsecure.storage.user.getNumber(); const myE164 = window.textsecure.storage.user.getNumber();
@ -2034,7 +2017,7 @@ export default class MessageSender {
groupIdentifiers: ReadonlyArray<string>, groupIdentifiers: ReadonlyArray<string>,
expireTimer: number | undefined, expireTimer: number | undefined,
timestamp: number, timestamp: number,
profileKey?: Readonly<ArrayBuffer>, profileKey?: Readonly<Uint8Array>,
options?: Readonly<SendOptionsType> options?: Readonly<SendOptionsType>
): Promise<CallbackResultType> { ): Promise<CallbackResultType> {
const myNumber = window.textsecure.storage.user.getNumber(); const myNumber = window.textsecure.storage.user.getNumber();
@ -2163,7 +2146,7 @@ export default class MessageSender {
return this.server.getGroupLog(startVersion, options); return this.server.getGroupLog(startVersion, options);
} }
async getGroupAvatar(key: string): Promise<ArrayBuffer> { async getGroupAvatar(key: string): Promise<Uint8Array> {
return this.server.getGroupAvatar(key); return this.server.getGroupAvatar(key);
} }
@ -2176,8 +2159,8 @@ export default class MessageSender {
} }
async sendWithSenderKey( async sendWithSenderKey(
data: Readonly<ArrayBuffer>, data: Readonly<Uint8Array>,
accessKeys: Readonly<ArrayBuffer>, accessKeys: Readonly<Uint8Array>,
timestamp: number, timestamp: number,
online?: boolean online?: boolean
): Promise<MultiRecipient200ResponseType> { ): Promise<MultiRecipient200ResponseType> {
@ -2211,21 +2194,21 @@ export default class MessageSender {
async getStorageManifest( async getStorageManifest(
options: Readonly<StorageServiceCallOptionsType> options: Readonly<StorageServiceCallOptionsType>
): Promise<ArrayBuffer> { ): Promise<Uint8Array> {
return this.server.getStorageManifest(options); return this.server.getStorageManifest(options);
} }
async getStorageRecords( async getStorageRecords(
data: Readonly<ArrayBuffer>, data: Readonly<Uint8Array>,
options: Readonly<StorageServiceCallOptionsType> options: Readonly<StorageServiceCallOptionsType>
): Promise<ArrayBuffer> { ): Promise<Uint8Array> {
return this.server.getStorageRecords(data, options); return this.server.getStorageRecords(data, options);
} }
async modifyStorageRecords( async modifyStorageRecords(
data: Readonly<ArrayBuffer>, data: Readonly<Uint8Array>,
options: Readonly<StorageServiceCallOptionsType> options: Readonly<StorageServiceCallOptionsType>
): Promise<ArrayBuffer> { ): Promise<Uint8Array> {
return this.server.modifyStorageRecords(data, options); return this.server.modifyStorageRecords(data, options);
} }
@ -2249,7 +2232,7 @@ export default class MessageSender {
async uploadAvatar( async uploadAvatar(
requestHeaders: Readonly<UploadAvatarHeadersType>, requestHeaders: Readonly<UploadAvatarHeadersType>,
avatarData: Readonly<ArrayBuffer> avatarData: Readonly<Uint8Array>
): Promise<string> { ): Promise<string> {
return this.server.uploadAvatar(requestHeaders, avatarData); return this.server.uploadAvatar(requestHeaders, avatarData);
} }

View file

@ -28,9 +28,6 @@ import { ConnectTimeoutError, HTTPError } from './Errors';
import { handleStatusCode, translateError } from './Utils'; import { handleStatusCode, translateError } from './Utils';
import { WebAPICredentials, IRequestHandler } from './Types.d'; import { WebAPICredentials, IRequestHandler } from './Types.d';
// TODO: remove once we move away from ArrayBuffers
const FIXMEU8 = Uint8Array;
const TEN_SECONDS = 10 * durations.SECOND; const TEN_SECONDS = 10 * durations.SECOND;
const FIVE_MINUTES = 5 * durations.MINUTE; const FIVE_MINUTES = 5 * durations.MINUTE;
@ -289,7 +286,7 @@ export class SocketManager extends EventListener {
} else if (body instanceof Uint8Array) { } else if (body instanceof Uint8Array) {
bodyBytes = body; bodyBytes = body;
} else if (body instanceof ArrayBuffer) { } else if (body instanceof ArrayBuffer) {
bodyBytes = new FIXMEU8(body); throw new Error('Unsupported body type: ArrayBuffer');
} else if (typeof body === 'string') { } else if (typeof body === 'string') {
bodyBytes = Bytes.fromString(body); bodyBytes = Bytes.fromString(body);
} else { } else {

View file

@ -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;

View file

@ -45,7 +45,7 @@ export type DeviceType = {
export type CompatSignedPreKeyType = { export type CompatSignedPreKeyType = {
keyId: number; keyId: number;
keyPair: KeyPairType; keyPair: KeyPairType;
signature: ArrayBuffer; signature: Uint8Array;
}; };
export type CompatPreKeyType = { export type CompatPreKeyType = {
@ -56,8 +56,8 @@ export type CompatPreKeyType = {
// How we work with these types thereafter // How we work with these types thereafter
export type KeyPairType = { export type KeyPairType = {
privKey: ArrayBuffer; privKey: Uint8Array;
pubKey: ArrayBuffer; pubKey: Uint8Array;
}; };
export type OuterSignedPrekeyType = { export type OuterSignedPrekeyType = {
@ -65,8 +65,8 @@ export type OuterSignedPrekeyType = {
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
created_at: number; created_at: number;
keyId: number; keyId: number;
privKey: ArrayBuffer; privKey: Uint8Array;
pubKey: ArrayBuffer; pubKey: Uint8Array;
}; };
export type SessionResetsType = Record<string, number>; export type SessionResetsType = Record<string, number>;
@ -231,7 +231,7 @@ export interface CallbackResultType {
failoverIdentifiers?: Array<string>; failoverIdentifiers?: Array<string>;
errors?: Array<CustomError>; errors?: Array<CustomError>;
unidentifiedDeliveries?: Array<string>; unidentifiedDeliveries?: Array<string>;
dataMessage?: ArrayBuffer; dataMessage?: Uint8Array;
// Fields necesary for send log save // Fields necesary for send log save
contentHint?: number; contentHint?: number;

File diff suppressed because it is too large Load diff

View file

@ -8,9 +8,8 @@ import { dropNull } from '../util/dropNull';
import { DownloadedAttachmentType } from '../types/Attachment'; import { DownloadedAttachmentType } from '../types/Attachment';
import * as MIME from '../types/MIME'; import * as MIME from '../types/MIME';
import * as Bytes from '../Bytes'; import * as Bytes from '../Bytes';
import { typedArrayToArrayBuffer } from '../Crypto'; import { getFirstBytes, decryptAttachment } from '../Crypto';
import Crypto from './Crypto';
import { ProcessedAttachment } from './Types.d'; import { ProcessedAttachment } from './Types.d';
import type { WebAPIType } from './WebAPI'; import type { WebAPIType } from './WebAPI';
@ -36,10 +35,10 @@ export async function downloadAttachment(
strictAssert(key, 'attachment has no key'); strictAssert(key, 'attachment has no key');
strictAssert(digest, 'attachment has no digest'); strictAssert(digest, 'attachment has no digest');
const paddedData = await Crypto.decryptAttachment( const paddedData = decryptAttachment(
encrypted, encrypted,
typedArrayToArrayBuffer(Bytes.fromBase64(key)), Bytes.fromBase64(key),
typedArrayToArrayBuffer(Bytes.fromBase64(digest)) Bytes.fromBase64(digest)
); );
if (!isNumber(size)) { 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 { return {
...omit(attachment, 'digest', 'key'), ...omit(attachment, 'digest', 'key'),

View file

@ -5,17 +5,14 @@ import EventTarget from './EventTarget';
import AccountManager from './AccountManager'; import AccountManager from './AccountManager';
import MessageReceiver from './MessageReceiver'; import MessageReceiver from './MessageReceiver';
import utils from './Helpers'; import utils from './Helpers';
import Crypto from './Crypto';
import { ContactBuffer, GroupBuffer } from './ContactsParser'; import { ContactBuffer, GroupBuffer } from './ContactsParser';
import SyncRequest from './SyncRequest'; import SyncRequest from './SyncRequest';
import MessageSender from './SendMessage'; import MessageSender from './SendMessage';
import StringView from './StringView';
import { Storage } from './Storage'; import { Storage } from './Storage';
import * as WebAPI from './WebAPI'; import * as WebAPI from './WebAPI';
import WebSocketResource from './WebsocketResources'; import WebSocketResource from './WebsocketResources';
export const textsecure = { export const textsecure = {
crypto: Crypto,
utils, utils,
storage: new Storage(), storage: new Storage(),
@ -26,7 +23,6 @@ export const textsecure = {
MessageReceiver, MessageReceiver,
MessageSender, MessageSender,
SyncRequest, SyncRequest,
StringView,
WebAPI, WebAPI,
WebSocketResource, WebSocketResource,
}; };

Some files were not shown because too many files have changed in this diff Show more