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
import { randomBytes } from 'crypto';
import { basename, extname, join, normalize, relative } from 'path';
import { basename, join, normalize, relative } from 'path';
import { app, dialog, shell, remote } from 'electron';
import fastGlob from 'fast-glob';
import glob from 'glob';
import pify from 'pify';
import fse from 'fs-extra';
import { map, isArrayBuffer, isString } from 'lodash';
import { map, isTypedArray, isString } from 'lodash';
import normalizePath from 'normalize-path';
import sanitizeFilename from 'sanitize-filename';
import getGuid from 'uuid/v4';
import { typedArrayToArrayBuffer } from '../ts/Crypto';
import { isPathInside } from '../ts/util/isPathInside';
import { isWindows } from '../ts/OS';
import { writeWindowsZoneIdentifier } from '../ts/util/windowsZoneIdentifier';
@ -123,12 +121,12 @@ export const clearTempPath = (userDataPath: string): Promise<void> => {
export const createReader = (
root: string
): ((relativePath: string) => Promise<ArrayBuffer>) => {
): ((relativePath: string) => Promise<Uint8Array>) => {
if (!isString(root)) {
throw new TypeError("'root' must be a path");
}
return async (relativePath: string): Promise<ArrayBuffer> => {
return async (relativePath: string): Promise<Uint8Array> => {
if (!isString(relativePath)) {
throw new TypeError("'relativePath' must be a string");
}
@ -138,8 +136,7 @@ export const createReader = (
if (!isPathInside(normalized, root)) {
throw new Error('Invalid relative path');
}
const buffer = await fse.readFile(normalized);
return typedArrayToArrayBuffer(buffer);
return fse.readFile(normalized);
};
};
@ -203,48 +200,9 @@ export const copyIntoAttachmentsDirectory = (
};
};
export const writeToDownloads = async ({
data,
name,
}: {
data: ArrayBuffer;
name: string;
}): Promise<{ fullPath: string; name: string }> => {
const appToUse = getApp();
const downloadsPath =
appToUse.getPath('downloads') || appToUse.getPath('home');
const sanitized = sanitizeFilename(name);
const extension = extname(sanitized);
const fileBasename = basename(sanitized, extension);
const getCandidateName = (count: number) =>
`${fileBasename} (${count})${extension}`;
const existingFiles = await fse.readdir(downloadsPath);
let candidateName = sanitized;
let count = 0;
while (existingFiles.includes(candidateName)) {
count += 1;
candidateName = getCandidateName(count);
}
const target = join(downloadsPath, candidateName);
const normalized = normalize(target);
if (!isPathInside(normalized, downloadsPath)) {
throw new Error('Invalid filename!');
}
await writeWithAttributes(normalized, data);
return {
fullPath: normalized,
name: candidateName,
};
};
async function writeWithAttributes(
target: string,
data: ArrayBuffer
data: Uint8Array
): Promise<void> {
await fse.writeFile(target, Buffer.from(data));
@ -292,7 +250,7 @@ export const saveAttachmentToDisk = async ({
data,
name,
}: {
data: ArrayBuffer;
data: Uint8Array;
name: string;
}): Promise<null | { fullPath: string; name: string }> => {
const dialogToUse = dialog || remote.dialog;
@ -327,20 +285,20 @@ export const openFileInFolder = async (target: string): Promise<void> => {
export const createWriterForNew = (
root: string
): ((arrayBuffer: ArrayBuffer) => Promise<string>) => {
): ((bytes: Uint8Array) => Promise<string>) => {
if (!isString(root)) {
throw new TypeError("'root' must be a path");
}
return async (arrayBuffer: ArrayBuffer) => {
if (!isArrayBuffer(arrayBuffer)) {
throw new TypeError("'arrayBuffer' must be an array buffer");
return async (bytes: Uint8Array) => {
if (!isTypedArray(bytes)) {
throw new TypeError("'bytes' must be a typed array");
}
const name = createName();
const relativePath = getRelativePath(name);
return createWriterForExisting(root)({
data: arrayBuffer,
data: bytes,
path: relativePath,
});
};
@ -348,27 +306,27 @@ export const createWriterForNew = (
export const createWriterForExisting = (
root: string
): ((options: { data: ArrayBuffer; path: string }) => Promise<string>) => {
): ((options: { data: Uint8Array; path: string }) => Promise<string>) => {
if (!isString(root)) {
throw new TypeError("'root' must be a path");
}
return async ({
data: arrayBuffer,
data: bytes,
path: relativePath,
}: {
data: ArrayBuffer;
data: Uint8Array;
path: string;
}): Promise<string> => {
if (!isString(relativePath)) {
throw new TypeError("'relativePath' must be a path");
}
if (!isArrayBuffer(arrayBuffer)) {
if (!isTypedArray(bytes)) {
throw new TypeError("'arrayBuffer' must be an array buffer");
}
const buffer = Buffer.from(arrayBuffer);
const buffer = Buffer.from(bytes);
const absolutePath = join(root, relativePath);
const normalized = normalize(absolutePath);
if (!isPathInside(normalized, root)) {

View file

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

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 Attachment = require('../../../ts/types/Attachment');
const Errors = require('../../../ts/types/errors');
const SchemaVersion = require('./schema_version');
const SchemaVersion = require('../../../ts/types/SchemaVersion');
const {
initializeAttachmentMetadata,
} = require('../../../ts/types/message/initializeAttachmentMetadata');

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: {
assert: true,
assertEqualArrayBuffers: true,
getString: true,
hexToArrayBuffer: true,
stringToArrayBuffer: true,
},
};

View file

@ -46,18 +46,6 @@ mocha.reporter(SauceReporter);
/*
* global helpers for tests
*/
window.assertEqualArrayBuffers = (ab1, ab2) => {
assert.deepEqual(new Uint8Array(ab1), new Uint8Array(ab2));
};
window.hexToArrayBuffer = str => {
const ret = new ArrayBuffer(str.length / 2);
const array = new Uint8Array(ret);
for (let i = 0; i < str.length / 2; i += 1) {
array[i] = parseInt(str.substr(i * 2, 2), 16);
}
return ret;
};
window.Whisper = window.Whisper || {};
window.Whisper.events = {

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
></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="account_manager_test.js"></script>
<script type="text/javascript" src="sendmessage_test.js"></script>

View file

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

View file

@ -9,7 +9,6 @@ const { readFile } = require('fs');
const config = require('url').parse(window.location.toString(), true).query;
const { noop, uniqBy } = require('lodash');
const pMap = require('p-map');
const client = require('@signalapp/signal-client');
// It is important to call this as early as possible
require('../ts/windows/context');
@ -55,20 +54,6 @@ const Signal = require('../js/modules/signal');
window.Signal = Signal.setup({});
window.textsecure = require('../ts/textsecure').default;
window.libsignal = window.libsignal || {};
window.libsignal.HKDF = {};
window.libsignal.HKDF.deriveSecrets = (input, salt, info) => {
const hkdf = client.HKDF.new(3);
const output = hkdf.deriveSecrets(
3 * 32,
Buffer.from(input),
Buffer.from(info),
Buffer.from(salt)
);
return [output.slice(0, 32), output.slice(32, 64), output.slice(64, 96)];
};
window.synchronousCrypto = require('../ts/util/synchronousCrypto');
const { initialize: initializeWebAPI } = require('../ts/textsecure/WebAPI');
const {
getAnimatedPngDataIfExists,
@ -206,7 +191,7 @@ window.encryptAndUpload = async (
const { value: password } = passwordItem;
const packKey = window.Signal.Crypto.getRandomBytes(32);
const encryptionKey = await deriveStickerPackKey(packKey);
const encryptionKey = deriveStickerPackKey(packKey);
const iv = window.Signal.Crypto.getRandomBytes(16);
const server = WebAPI.connect({
@ -265,9 +250,7 @@ window.encryptAndUpload = async (
async function encrypt(data, key, iv) {
const { ciphertext } = await window.textsecure.crypto.encryptAttachment(
data instanceof ArrayBuffer
? data
: window.Signal.Crypto.typedArrayToArrayBuffer(data),
data,
key,
iv
);

View file

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

View file

@ -51,18 +51,6 @@ Whisper.Database.id = 'test';
/*
* global helpers for tests
*/
window.assertEqualArrayBuffers = (ab1, ab2) => {
assert.deepEqual(new Uint8Array(ab1), new Uint8Array(ab2));
};
window.hexToArrayBuffer = str => {
const ret = new ArrayBuffer(str.length / 2);
const array = new Uint8Array(ret);
for (let i = 0; i < str.length / 2; i += 1) {
array[i] = parseInt(str.substr(i * 2, 2), 16);
}
return ret;
};
function deleteIndexedDB() {
return new Promise((resolve, reject) => {

View file

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

View file

@ -6,7 +6,7 @@ const sinon = require('sinon');
const Message = require('../../../js/modules/types/message');
const { SignalService } = require('../../../ts/protobuf');
const { stringToArrayBuffer } = require('../../../ts/util/stringToArrayBuffer');
const Bytes = require('../../../ts/Bytes');
describe('Message', () => {
const logger = {
@ -60,7 +60,7 @@ describe('Message', () => {
attachments: [
{
path: 'ab/abcdefghi',
data: stringToArrayBuffer('Its easy if you try'),
data: Bytes.fromString('Its easy if you try'),
},
],
};
@ -78,9 +78,9 @@ describe('Message', () => {
const writeExistingAttachmentData = attachment => {
assert.equal(attachment.path, 'ab/abcdefghi');
assert.deepEqual(
attachment.data,
stringToArrayBuffer('Its easy if you try')
assert.strictEqual(
Bytes.toString(attachment.data),
'Its easy if you try'
);
};
@ -101,7 +101,7 @@ describe('Message', () => {
{
thumbnail: {
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 => {
assert.equal(attachment.path, 'ab/abcdefghi');
assert.deepEqual(
attachment.data,
stringToArrayBuffer('Its easy if you try')
assert.strictEqual(
Bytes.toString(attachment.data),
'Its easy if you try'
);
};
@ -151,7 +151,7 @@ describe('Message', () => {
isProfile: false,
avatar: {
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 => {
assert.equal(attachment.path, 'ab/abcdefghi');
assert.deepEqual(
attachment.data,
stringToArrayBuffer('Its easy if you try')
assert.strictEqual(
Bytes.toString(attachment.data),
'Its easy if you try'
);
};
@ -268,7 +268,7 @@ describe('Message', () => {
{
contentType: 'audio/aac',
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',
size: 1111,
},
@ -292,12 +292,13 @@ describe('Message', () => {
contact: [],
};
const expectedAttachmentData = stringToArrayBuffer(
'Its easy if you try'
);
const expectedAttachmentData = 'Its easy if you try';
const context = {
writeNewAttachmentData: async attachmentData => {
assert.deepEqual(attachmentData, expectedAttachmentData);
assert.strictEqual(
Bytes.toString(attachmentData),
expectedAttachmentData
);
return 'abc/abcdefg';
},
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 {
if (isGroupV1(conversation.attributes)) {
await maybeDeriveGroupV2Id(conversation);
maybeDeriveGroupV2Id(conversation);
}
await saveConversation(conversation.attributes);
} catch (error) {
@ -577,8 +577,7 @@ export class ConversationController {
let groupV2Id: undefined | string;
if (isGroupV1(conversation.attributes)) {
// eslint-disable-next-line no-await-in-loop
await maybeDeriveGroupV2Id(conversation);
maybeDeriveGroupV2Id(conversation);
groupV2Id = conversation.get('derivedGroupV2Id');
assert(
groupV2Id,
@ -836,7 +835,7 @@ export class ConversationController {
// Hydrate contactCollection, now that initial fetch is complete
conversation.fetchContacts();
const isChanged = await maybeDeriveGroupV2Id(conversation);
const isChanged = maybeDeriveGroupV2Id(conversation);
if (isChanged) {
updateConversation(conversation.attributes);
}

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -24,7 +24,7 @@ import {
import { AvatarColors } from '../../types/Colors';
export type LeftPaneSetGroupMetadataPropsType = {
groupAvatar: undefined | ArrayBuffer;
groupAvatar: undefined | Uint8Array;
groupName: string;
groupExpireTimer: number;
hasError: boolean;
@ -37,7 +37,7 @@ export type LeftPaneSetGroupMetadataPropsType = {
/* eslint-disable class-methods-use-this */
export class LeftPaneSetGroupMetadataHelper extends LeftPaneHelper<LeftPaneSetGroupMetadataPropsType> {
private readonly groupAvatar: undefined | ArrayBuffer;
private readonly groupAvatar: undefined | Uint8Array;
private readonly groupName: string;
@ -127,7 +127,7 @@ export class LeftPaneSetGroupMetadataHelper extends LeftPaneHelper<LeftPaneSetGr
composeSaveAvatarToDisk: SaveAvatarToDiskActionType;
createGroup: () => unknown;
i18n: LocalizerType;
setComposeGroupAvatar: (_: undefined | ArrayBuffer) => unknown;
setComposeGroupAvatar: (_: undefined | Uint8Array) => unknown;
setComposeGroupExpireTimer: (_: number) => void;
setComposeGroupName: (_: string) => 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
import { Bytes } from './Bytes';
import { Crypto } from './Crypto';
import {
createNativeThemeListener,
MinimalIPC,
@ -10,6 +11,8 @@ import {
export class Context {
public readonly bytes = new Bytes();
public readonly crypto = new Crypto();
public readonly nativeThemeListener;
constructor(ipc: MinimalIPC) {

View file

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -10,7 +10,7 @@ import { handleMessageSend } from '../util/handleMessageSend';
export async function writeProfile(
conversation: ConversationType,
avatarBuffer?: ArrayBuffer
avatarBuffer?: Uint8Array
): Promise<void> {
// 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -6,6 +6,7 @@ import * as sinon from 'sinon';
import { noop } from 'lodash';
import { sleep } from '../../util/sleep';
import { constantTimeEqual } from '../../Crypto';
import { OurProfileKeyService } from '../../services/ourProfileKey';
describe('"our profile key" service', () => {
@ -18,14 +19,17 @@ describe('"our profile key" service', () => {
describe('get', () => {
it("fetches the key from storage if it's there", async () => {
const fakeProfileKey = new ArrayBuffer(2);
const fakeProfileKey = new Uint8Array(2);
const fakeStorage = createFakeStorage();
fakeStorage.get.withArgs('profileKey').returns(fakeProfileKey);
const service = new OurProfileKeyService();
service.initialize(fakeStorage);
assert.strictEqual(await service.get(), fakeProfileKey);
const profileKey = await service.get();
assert.isTrue(
profileKey && constantTimeEqual(profileKey, fakeProfileKey)
);
});
it('resolves with undefined if the key is not in storage', async () => {
@ -39,7 +43,7 @@ describe('"our profile key" service', () => {
let onReadyCallback = noop;
const fakeStorage = {
...createFakeStorage(),
get: sinon.stub().returns(new ArrayBuffer(2)),
get: sinon.stub().returns(new Uint8Array(2)),
onready: sinon.stub().callsFake(callback => {
onReadyCallback = callback;
}),
@ -106,7 +110,7 @@ describe('"our profile key" service', () => {
it("if there are blocking promises, doesn't grab the profile key from storage more than once (in other words, subsequent calls piggyback)", async () => {
const fakeStorage = createFakeStorage();
fakeStorage.get.returns(new ArrayBuffer(2));
fakeStorage.get.returns(new Uint8Array(2));
const service = new OurProfileKeyService();
service.initialize(fakeStorage);
@ -153,7 +157,7 @@ describe('"our profile key" service', () => {
describe('set', () => {
it('updates the key in storage', async () => {
const fakeProfileKey = new ArrayBuffer(2);
const fakeProfileKey = new Uint8Array(2);
const fakeStorage = createFakeStorage();
const service = new OurProfileKeyService();

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

2
ts/textsecure.d.ts vendored
View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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 = {
keyId: number;
keyPair: KeyPairType;
signature: ArrayBuffer;
signature: Uint8Array;
};
export type CompatPreKeyType = {
@ -56,8 +56,8 @@ export type CompatPreKeyType = {
// How we work with these types thereafter
export type KeyPairType = {
privKey: ArrayBuffer;
pubKey: ArrayBuffer;
privKey: Uint8Array;
pubKey: Uint8Array;
};
export type OuterSignedPrekeyType = {
@ -65,8 +65,8 @@ export type OuterSignedPrekeyType = {
// eslint-disable-next-line camelcase
created_at: number;
keyId: number;
privKey: ArrayBuffer;
pubKey: ArrayBuffer;
privKey: Uint8Array;
pubKey: Uint8Array;
};
export type SessionResetsType = Record<string, number>;
@ -231,7 +231,7 @@ export interface CallbackResultType {
failoverIdentifiers?: Array<string>;
errors?: Array<CustomError>;
unidentifiedDeliveries?: Array<string>;
dataMessage?: ArrayBuffer;
dataMessage?: Uint8Array;
// Fields necesary for send log save
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 * as MIME from '../types/MIME';
import * as Bytes from '../Bytes';
import { typedArrayToArrayBuffer } from '../Crypto';
import { getFirstBytes, decryptAttachment } from '../Crypto';
import Crypto from './Crypto';
import { ProcessedAttachment } from './Types.d';
import type { WebAPIType } from './WebAPI';
@ -36,10 +35,10 @@ export async function downloadAttachment(
strictAssert(key, 'attachment has no key');
strictAssert(digest, 'attachment has no digest');
const paddedData = await Crypto.decryptAttachment(
const paddedData = decryptAttachment(
encrypted,
typedArrayToArrayBuffer(Bytes.fromBase64(key)),
typedArrayToArrayBuffer(Bytes.fromBase64(digest))
Bytes.fromBase64(key),
Bytes.fromBase64(digest)
);
if (!isNumber(size)) {
@ -48,7 +47,7 @@ export async function downloadAttachment(
);
}
const data = window.Signal.Crypto.getFirstBytes(paddedData, size);
const data = getFirstBytes(paddedData, size);
return {
...omit(attachment, 'digest', 'key'),

View file

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

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