Move to protobufjs in ts/groups.ts

This commit is contained in:
Fedor Indutny 2021-06-22 07:46:42 -07:00 committed by GitHub
parent 972a4cba0c
commit 9f0c630574
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 1424 additions and 964 deletions

View file

@ -13,6 +13,10 @@ const {
parseEnvironment,
} = require('./ts/environment');
const { Context: SignalContext } = require('./ts/context');
window.SignalContext = new SignalContext();
const config = url.parse(window.location.toString(), true).query;
const { locale } = config;
const localeMessages = ipcRenderer.sendSync('locale-data');

View file

@ -19,11 +19,15 @@ const {
const { nativeTheme } = remote.require('electron');
const { Context: SignalContext } = require('./ts/context');
const config = url.parse(window.location.toString(), true).query;
const { locale } = config;
const localeMessages = ipcRenderer.sendSync('locale-data');
setEnvironment(parseEnvironment(config.environment));
window.SignalContext = new SignalContext();
window.getEnvironment = getEnvironment;
window.getVersion = () => config.version;
window.theme = config.theme;

View file

@ -24,6 +24,10 @@ try {
const { app } = remote;
const { nativeTheme } = remote.require('electron');
const { Context: SignalContext } = require('./ts/context');
window.SignalContext = new SignalContext();
window.sqlInitializer = require('./ts/sql/initialize');
window.PROTO_ROOT = 'protos';
@ -483,6 +487,7 @@ try {
const { autoOrientImage } = require('./js/modules/auto_orient_image');
const { imageToBlurHash } = require('./ts/util/imageToBlurHash');
const { isGroupCallingEnabled } = require('./ts/util/isGroupCallingEnabled');
const { isValidGuid } = require('./ts/util/isValidGuid');
const { ActiveWindowService } = require('./ts/services/ActiveWindowService');
window.autoOrientImage = autoOrientImage;
@ -509,10 +514,7 @@ try {
reducedMotionSetting: Boolean(config.reducedMotionSetting),
};
window.isValidGuid = maybeGuid =>
/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i.test(
maybeGuid
);
window.isValidGuid = isValidGuid;
// https://stackoverflow.com/a/23299989
window.isValidE164 = maybeE164 => /^\+?[1-9]\d{1,14}$/.test(maybeE164);

View file

@ -18,6 +18,10 @@ const {
CallingScreenSharingController,
} = require('./ts/components/CallingScreenSharingController');
const { Context: SignalContext } = require('./ts/context');
window.SignalContext = new SignalContext();
const config = url.parse(window.location.toString(), true).query;
const { locale } = config;
const localeMessages = ipcRenderer.sendSync('locale-data');

View file

@ -20,6 +20,10 @@ setEnvironment(parseEnvironment(config.environment));
const { nativeTheme } = remote.require('electron');
const { Context: SignalContext } = require('./ts/context');
window.SignalContext = new SignalContext();
window.platform = process.platform;
window.theme = config.theme;
window.i18n = i18n.setup(locale, localeMessages);

View file

@ -21,12 +21,16 @@ const { makeGetter } = require('../preload_utils');
const { dialog } = remote;
const { nativeTheme } = remote.require('electron');
const { Context: SignalContext } = require('../ts/context');
const STICKER_SIZE = 512;
const MIN_STICKER_DIMENSION = 10;
const MAX_STICKER_DIMENSION = STICKER_SIZE;
const MAX_WEBP_STICKER_BYTE_LENGTH = 100 * 1024;
const MAX_ANIMATED_STICKER_BYTE_LENGTH = 300 * 1024;
window.SignalContext = new SignalContext();
setEnvironment(parseEnvironment(config.environment));
window.sqlInitializer = require('../ts/sql/initialize');

View file

@ -9,6 +9,8 @@ const chaiAsPromised = require('chai-as-promised');
const ByteBuffer = require('../components/bytebuffer/dist/ByteBufferAB.js');
const Long = require('../components/long/dist/Long.js');
const { setEnvironment, Environment } = require('../ts/environment');
const { Context: SignalContext } = require('../ts/context');
const { isValidGuid } = require('../ts/util/isValidGuid');
chai.use(chaiAsPromised);
@ -18,6 +20,7 @@ const storageMap = new Map();
// To replicate logic we have on the client side
global.window = {
SignalContext: new SignalContext(),
log: {
info: (...args) => console.log(...args),
warn: (...args) => console.warn(...args),
@ -32,6 +35,7 @@ global.window = {
get: key => storageMap.get(key),
put: async (key, value) => storageMap.set(key, value),
},
isValidGuid,
};
// For ducks/network.getEmptyState()

52
ts/Bytes.ts Normal file
View file

@ -0,0 +1,52 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
const { bytes } = window.SignalContext;
export function fromBase64(value: string): Uint8Array {
return bytes.fromBase64(value);
}
export function fromHex(value: string): Uint8Array {
return bytes.fromHex(value);
}
// TODO(indutny): deprecate it
export function fromBinary(value: string): Uint8Array {
return bytes.fromBinary(value);
}
export function fromString(value: string): Uint8Array {
return bytes.fromString(value);
}
export function toBase64(data: Uint8Array): string {
return bytes.toBase64(data);
}
export function toHex(data: Uint8Array): string {
return bytes.toHex(data);
}
// TODO(indutny): deprecate it
export function toBinary(data: Uint8Array): string {
return bytes.toBinary(data);
}
export function toString(data: Uint8Array): string {
return bytes.toString(data);
}
export function concatenate(list: Array<Uint8Array>): Uint8Array {
return bytes.concatenate(list);
}
export function isEmpty(data: Uint8Array | null | undefined): boolean {
return bytes.isEmpty(data);
}
export function isNotEmpty(
data: Uint8Array | null | undefined
): data is Uint8Array {
return !bytes.isEmpty(data);
}

57
ts/context/Bytes.ts Normal file
View file

@ -0,0 +1,57 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable class-methods-use-this */
import { Buffer } from 'buffer';
export class Bytes {
public fromBase64(value: string): Uint8Array {
return Buffer.from(value, 'base64');
}
public fromHex(value: string): Uint8Array {
return Buffer.from(value, 'hex');
}
// TODO(indutny): deprecate it
public fromBinary(value: string): Uint8Array {
return Buffer.from(value, 'binary');
}
public fromString(value: string): Uint8Array {
return Buffer.from(value);
}
public toBase64(data: Uint8Array): string {
return Buffer.from(data).toString('base64');
}
public toHex(data: Uint8Array): string {
return Buffer.from(data).toString('hex');
}
// TODO(indutny): deprecate it
public toBinary(data: Uint8Array): string {
return Buffer.from(data).toString('binary');
}
public toString(data: Uint8Array): string {
return Buffer.from(data).toString();
}
public concatenate(list: ReadonlyArray<Uint8Array>): Uint8Array {
return Buffer.concat(list);
}
public isEmpty(data: Uint8Array | null | undefined): boolean {
if (!data) {
return true;
}
return data.length === 0;
}
public isNotEmpty(data: Uint8Array | null | undefined): data is Uint8Array {
return !this.isEmpty(data);
}
}

8
ts/context/index.ts Normal file
View file

@ -0,0 +1,8 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { Bytes } from './Bytes';
export class Context {
public readonly bytes = new Bytes();
}

File diff suppressed because it is too large Load diff

View file

@ -11,14 +11,14 @@ import {
LINK_VERSION_ERROR,
parseGroupLink,
} from '../groups';
import { arrayBufferToBase64, base64ToArrayBuffer } from '../Crypto';
import * as Bytes from '../Bytes';
import { longRunningTaskWrapper } from '../util/longRunningTaskWrapper';
import { isGroupV1 } from '../util/whatTypeOfConversation';
import type { GroupJoinInfoClass } from '../textsecure.d';
import type { ConversationAttributesType } from '../model-types.d';
import type { ConversationModel } from '../models/conversations';
import type { PreJoinConversationType } from '../state/ducks/conversations';
import { SignalService as Proto } from '../protobuf';
export async function joinViaLink(hash: string): Promise<void> {
let inviteLinkPassword: string;
@ -42,11 +42,11 @@ export async function joinViaLink(hash: string): Promise<void> {
return;
}
const data = deriveGroupFields(base64ToArrayBuffer(masterKey));
const id = arrayBufferToBase64(data.id);
const data = deriveGroupFields(Bytes.fromBase64(masterKey));
const id = Bytes.toBase64(data.id);
const logId = `groupv2(${id})`;
const secretParams = arrayBufferToBase64(data.secretParams);
const publicParams = arrayBufferToBase64(data.publicParams);
const secretParams = Bytes.toBase64(data.secretParams);
const publicParams = Bytes.toBase64(data.publicParams);
const existingConversation =
window.ConversationController.get(id) ||
@ -70,7 +70,7 @@ export async function joinViaLink(hash: string): Promise<void> {
return;
}
let result: GroupJoinInfoClass;
let result: Proto.GroupJoinInfo;
try {
result = await longRunningTaskWrapper({

View file

@ -39,7 +39,8 @@ import {
trimForDisplay,
verifyAccessKey,
} from '../Crypto';
import { GroupChangeClass, DataMessageClass } from '../textsecure.d';
import * as Bytes from '../Bytes';
import { DataMessageClass } from '../textsecure.d';
import { BodyRangesType } from '../types/Util';
import { getTextWithMentions } from '../util';
import { migrateColor } from '../util/migrateColor';
@ -62,6 +63,7 @@ import {
isMe,
} from '../util/whatTypeOfConversation';
import { deprecated } from '../util/deprecated';
import { SignalService as Proto } from '../protobuf';
import {
hasErrors,
isIncoming,
@ -71,6 +73,9 @@ import {
import { Deletes } from '../messageModifiers/Deletes';
import { Reactions } from '../messageModifiers/Reactions';
// TODO: remove once we move away from ArrayBuffers
const FIXMEU8 = Uint8Array;
/* eslint-disable more/no-then */
window.Whisper = window.Whisper || {};
@ -385,7 +390,7 @@ export class ConversationModel extends window.Backbone
async updateExpirationTimerInGroupV2(
seconds?: number
): Promise<GroupChangeClass.Actions | undefined> {
): Promise<Proto.GroupChange.Actions | undefined> {
const idLog = this.idForLogging();
const current = this.get('expireTimer');
const bothFalsey = Boolean(current) === false && Boolean(seconds) === false;
@ -405,7 +410,7 @@ export class ConversationModel extends window.Backbone
async promotePendingMember(
conversationId: string
): Promise<GroupChangeClass.Actions | undefined> {
): Promise<Proto.GroupChange.Actions | undefined> {
const idLog = this.idForLogging();
// This user's pending state may have changed in the time between the user's
@ -449,7 +454,7 @@ export class ConversationModel extends window.Backbone
async approvePendingApprovalRequest(
conversationId: string
): Promise<GroupChangeClass.Actions | undefined> {
): Promise<Proto.GroupChange.Actions | undefined> {
const idLog = this.idForLogging();
// This user's pending state may have changed in the time between the user's
@ -484,7 +489,7 @@ export class ConversationModel extends window.Backbone
async denyPendingApprovalRequest(
conversationId: string
): Promise<GroupChangeClass.Actions | undefined> {
): Promise<Proto.GroupChange.Actions | undefined> {
const idLog = this.idForLogging();
// This user's pending state may have changed in the time between the user's
@ -518,7 +523,7 @@ export class ConversationModel extends window.Backbone
}
async addPendingApprovalRequest(): Promise<
GroupChangeClass.Actions | undefined
Proto.GroupChange.Actions | undefined
> {
const idLog = this.idForLogging();
@ -566,7 +571,7 @@ export class ConversationModel extends window.Backbone
async addMember(
conversationId: string
): Promise<GroupChangeClass.Actions | undefined> {
): Promise<Proto.GroupChange.Actions | undefined> {
const idLog = this.idForLogging();
const toRequest = window.ConversationController.get(conversationId);
@ -610,7 +615,7 @@ export class ConversationModel extends window.Backbone
async removePendingMember(
conversationIds: Array<string>
): Promise<GroupChangeClass.Actions | undefined> {
): Promise<Proto.GroupChange.Actions | undefined> {
const idLog = this.idForLogging();
const uuids = conversationIds
@ -656,7 +661,7 @@ export class ConversationModel extends window.Backbone
async removeMember(
conversationId: string
): Promise<GroupChangeClass.Actions | undefined> {
): Promise<Proto.GroupChange.Actions | undefined> {
const idLog = this.idForLogging();
// This user's pending state may have changed in the time between the user's
@ -691,7 +696,7 @@ export class ConversationModel extends window.Backbone
async toggleAdminChange(
conversationId: string
): Promise<GroupChangeClass.Actions | undefined> {
): Promise<Proto.GroupChange.Actions | undefined> {
if (!isGroupV2(this.attributes)) {
return undefined;
}
@ -738,7 +743,7 @@ export class ConversationModel extends window.Backbone
inviteLinkPassword,
name,
}: {
createGroupChange: () => Promise<GroupChangeClass.Actions | undefined>;
createGroupChange: () => Promise<Proto.GroupChange.Actions | undefined>;
extraConversationsForSend?: Array<string>;
inviteLinkPassword?: string;
name: string;
@ -1099,7 +1104,7 @@ export class ConversationModel extends window.Backbone
return undefined;
}
return {
masterKey: window.Signal.Crypto.base64ToArrayBuffer(
masterKey: Bytes.fromBase64(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.get('masterKey')!
),
@ -1109,7 +1114,7 @@ export class ConversationModel extends window.Backbone
includePendingMembers,
extraConversationsForSend,
}),
groupChange,
groupChange: groupChange ? new FIXMEU8(groupChange) : undefined,
};
}
@ -2832,8 +2837,7 @@ export class ConversationModel extends window.Backbone
validateUuid(): string | null {
if (isDirectConversation(this.attributes) && this.get('uuid')) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (window.isValidGuid(this.get('uuid')!)) {
if (window.isValidGuid(this.get('uuid'))) {
return null;
}

View file

@ -54,6 +54,7 @@ import {
base64ToArrayBuffer,
uuidToArrayBuffer,
arrayBufferToUuid,
typedArrayToArrayBuffer,
} from '../Crypto';
import { assert } from '../util/assert';
import { getOwn } from '../util/getOwn';
@ -384,7 +385,7 @@ export class CallingClass {
member =>
new GroupMemberInfo(
uuidToArrayBuffer(member.uuid),
member.uuidCiphertext
typedArrayToArrayBuffer(member.uuidCiphertext)
)
);
}

View file

@ -9,6 +9,7 @@ import {
deriveMasterKeyFromGroupV1,
fromEncodedBinaryToArrayBuffer,
} from '../Crypto';
import * as Bytes from '../Bytes';
import dataInterface from '../sql/Client';
import {
AccountRecordClass,
@ -47,6 +48,9 @@ import { isGroupV1, isGroupV2 } from '../util/whatTypeOfConversation';
const { updateConversation } = dataInterface;
// TODO: remove once we move away from ArrayBuffers
const FIXMEU8 = Uint8Array;
type RecordClass =
| AccountRecordClass
| ContactRecordClass
@ -520,8 +524,8 @@ export async function mergeGroupV1Record(
// retrieve the master key and find the conversation locally. If we
// are successful then we continue setting and applying state.
const masterKeyBuffer = await deriveMasterKeyFromGroupV1(groupId);
const fields = deriveGroupFields(masterKeyBuffer);
const derivedGroupV2Id = arrayBufferToBase64(fields.id);
const fields = deriveGroupFields(new FIXMEU8(masterKeyBuffer));
const derivedGroupV2Id = Bytes.toBase64(fields.id);
window.log.info(
'storageService.mergeGroupV1Record: failed to find group by v1 id ' +
@ -596,12 +600,12 @@ export async function mergeGroupV1Record(
async function getGroupV2Conversation(
masterKeyBuffer: ArrayBuffer
): Promise<ConversationModel> {
const groupFields = deriveGroupFields(masterKeyBuffer);
const groupFields = deriveGroupFields(new FIXMEU8(masterKeyBuffer));
const groupId = arrayBufferToBase64(groupFields.id);
const groupId = Bytes.toBase64(groupFields.id);
const masterKey = arrayBufferToBase64(masterKeyBuffer);
const secretParams = arrayBufferToBase64(groupFields.secretParams);
const publicParams = arrayBufferToBase64(groupFields.publicParams);
const secretParams = Bytes.toBase64(groupFields.secretParams);
const publicParams = Bytes.toBase64(groupFields.publicParams);
// First we check for an existing GroupV2 group
const groupV2 = window.ConversationController.get(groupId);
@ -944,7 +948,7 @@ export async function mergeAccountRecord(
}
const masterKeyBuffer = pinnedConversation.groupMasterKey.toArrayBuffer();
const groupFields = deriveGroupFields(masterKeyBuffer);
const groupId = arrayBufferToBase64(groupFields.id);
const groupId = Bytes.toBase64(groupFields.id);
conversationId = groupId;
break;

View file

@ -0,0 +1,90 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import * as Bytes from '../../Bytes';
describe('Bytes', () => {
it('converts to base64 and back', () => {
const bytes = new Uint8Array([1, 2, 3]);
const base64 = Bytes.toBase64(bytes);
assert.strictEqual(base64, 'AQID');
assert.deepEqual(Bytes.fromBase64(base64), bytes);
});
it('converts to hex and back', () => {
const bytes = new Uint8Array([1, 2, 3]);
const hex = Bytes.toHex(bytes);
assert.strictEqual(hex, '010203');
assert.deepEqual(Bytes.fromHex(hex), bytes);
});
it('converts to string and back', () => {
const bytes = new Uint8Array([0x61, 0x62, 0x63]);
const binary = Bytes.toString(bytes);
assert.strictEqual(binary, 'abc');
assert.deepEqual(Bytes.fromString(binary), bytes);
});
it('converts to binary and back', () => {
const bytes = new Uint8Array([0xff, 0x01]);
const binary = Bytes.toBinary(bytes);
assert.strictEqual(binary, '\xff\x01');
assert.deepEqual(Bytes.fromBinary(binary), bytes);
});
it('concatenates bytes', () => {
const result = Bytes.concatenate([
Bytes.fromString('hello'),
Bytes.fromString(' '),
Bytes.fromString('world'),
]);
assert.strictEqual(Bytes.toString(result), 'hello world');
});
describe('isEmpty', () => {
it('returns true for `undefined`', () => {
assert.strictEqual(Bytes.isEmpty(undefined), true);
});
it('returns true for `null`', () => {
assert.strictEqual(Bytes.isEmpty(null), true);
});
it('returns true for an empty Uint8Array', () => {
assert.strictEqual(Bytes.isEmpty(new Uint8Array(0)), true);
});
it('returns false for not empty Uint8Array', () => {
assert.strictEqual(Bytes.isEmpty(new Uint8Array(123)), false);
});
});
describe('isNotEmpty', () => {
it('returns false for `undefined`', () => {
assert.strictEqual(Bytes.isNotEmpty(undefined), false);
});
it('returns false for `null`', () => {
assert.strictEqual(Bytes.isNotEmpty(null), false);
});
it('returns false for an empty Uint8Array', () => {
assert.strictEqual(Bytes.isNotEmpty(new Uint8Array(0)), false);
});
it('returns true for not empty Uint8Array', () => {
assert.strictEqual(Bytes.isNotEmpty(new Uint8Array(123)), true);
});
});
});

View file

@ -3,16 +3,30 @@
import * as chai from 'chai';
import { assert } from '../../util/assert';
import { assert, strictAssert } from '../../util/assert';
describe('assert', () => {
it('does nothing if the assertion passes', () => {
assert(true, 'foo bar');
describe('assert utilities', () => {
describe('assert', () => {
it('does nothing if the assertion passes', () => {
assert(true, 'foo bar');
});
it("throws if the assertion fails, because we're in a test environment", () => {
chai.assert.throws(() => {
assert(false, 'foo bar');
}, 'foo bar');
});
});
it("throws because we're in a test environment", () => {
chai.assert.throws(() => {
assert(false, 'foo bar');
}, 'foo bar');
describe('strictAssert', () => {
it('does nothing if the assertion passes', () => {
strictAssert(true, 'foo bar');
});
it('throws if the assertion fails', () => {
chai.assert.throws(() => {
strictAssert(false, 'foo bar');
}, 'foo bar');
});
});
});

View file

@ -0,0 +1,19 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { dropNull } from '../../util/dropNull';
describe('dropNull', () => {
it('swaps null with undefined', () => {
assert.strictEqual(dropNull(null), undefined);
});
it('leaves undefined be', () => {
assert.strictEqual(dropNull(undefined), undefined);
});
it('non-null values undefined be', () => {
assert.strictEqual(dropNull('test'), 'test');
});
});

View file

@ -0,0 +1,33 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { isValidGuid } from '../../util/isValidGuid';
describe('isValidGuid', () => {
const LOWERCASE_V4_UUID = '9cb737ce-2bb3-4c21-9fe0-d286caa0ca68';
it('returns false for non-strings', () => {
assert.isFalse(isValidGuid(undefined));
assert.isFalse(isValidGuid(null));
assert.isFalse(isValidGuid(1234));
});
it('returns false for non-UUID strings', () => {
assert.isFalse(isValidGuid(''));
assert.isFalse(isValidGuid('hello world'));
assert.isFalse(isValidGuid(` ${LOWERCASE_V4_UUID}`));
assert.isFalse(isValidGuid(`${LOWERCASE_V4_UUID} `));
});
it("returns false for UUIDs that aren't version 4", () => {
assert.isFalse(isValidGuid('a200a6e0-d2d9-11eb-bda7-dd5936a30ddf'));
assert.isFalse(isValidGuid('2adb8b83-4f2c-55ca-a481-7f98b716e615'));
});
it('returns true for v4 UUIDs', () => {
assert.isTrue(isValidGuid(LOWERCASE_V4_UUID));
assert.isTrue(isValidGuid(LOWERCASE_V4_UUID.toUpperCase()));
});
});

View file

@ -0,0 +1,15 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { v4 as generateUuid } from 'uuid';
import { normalizeUuid } from '../../util/normalizeUuid';
describe('normalizeUuid', () => {
it('converts uuid to lower case', () => {
const uuid = generateUuid();
assert.strictEqual(normalizeUuid(uuid, 'context 1'), uuid);
assert.strictEqual(normalizeUuid(uuid.toUpperCase(), 'context 2'), uuid);
});
});

View file

@ -51,6 +51,7 @@ import utils from './Helpers';
import WebSocketResource, {
IncomingWebSocketRequest,
} from './WebsocketResources';
import * as Bytes from '../Bytes';
import Crypto from './Crypto';
import { deriveMasterKeyFromGroupV1, typedArrayToArrayBuffer } from '../Crypto';
import { ContactBuffer, GroupBuffer } from './ContactsParser';
@ -73,6 +74,9 @@ import { ByteBufferClass } from '../window.d';
import { deriveGroupFields, MASTER_KEY_LENGTH } from '../groups';
// 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;
@ -1991,10 +1995,9 @@ class MessageReceiverInner extends EventTarget {
);
}
const masterKey = await deriveMasterKeyFromGroupV1(groupId);
const data = deriveGroupFields(masterKey);
const data = deriveGroupFields(new FIXMEU8(masterKey));
const toBase64 = MessageReceiverInner.arrayBufferToStringBase64;
return toBase64(data.id);
return Bytes.toBase64(data.id);
}
async deriveGroupV1Data(message: DataMessageClass) {
@ -2040,11 +2043,11 @@ class MessageReceiverInner extends EventTarget {
);
}
const fields = deriveGroupFields(masterKey);
const fields = deriveGroupFields(new FIXMEU8(masterKey));
groupV2.masterKey = toBase64(masterKey);
groupV2.secretParams = toBase64(fields.secretParams);
groupV2.publicParams = toBase64(fields.publicParams);
groupV2.id = toBase64(fields.id);
groupV2.secretParams = Bytes.toBase64(fields.secretParams);
groupV2.publicParams = Bytes.toBase64(fields.publicParams);
groupV2.id = Bytes.toBase64(fields.id);
if (groupV2.groupChange) {
groupV2.groupChange = groupV2.groupChange.toString('base64');

View file

@ -43,10 +43,6 @@ import {
CallingMessageClass,
ContentClass,
DataMessageClass,
GroupChangeClass,
GroupClass,
GroupExternalCredentialClass,
GroupJoinInfoClass,
StorageServiceCallOptionsType,
StorageServiceCredentials,
SyncMessageClass,
@ -58,6 +54,7 @@ import {
LinkPreviewMetadata,
} from '../linkPreviews/linkPreviewFetch';
import { concat } from '../util/iterables';
import { SignalService as Proto } from '../protobuf';
function stringToArrayBuffer(str: string): ArrayBuffer {
if (typeof str !== 'string') {
@ -108,8 +105,8 @@ type QuoteAttachmentType = {
};
export type GroupV2InfoType = {
groupChange?: ArrayBuffer;
masterKey: ArrayBuffer;
groupChange?: Uint8Array;
masterKey: Uint8Array;
revision: number;
members: Array<string>;
};
@ -1961,27 +1958,27 @@ export default class MessageSender {
}
async createGroup(
group: GroupClass,
group: Proto.IGroup,
options: GroupCredentialsType
): Promise<void> {
return this.server.createGroup(group, options);
}
async uploadGroupAvatar(
avatar: ArrayBuffer,
avatar: Uint8Array,
options: GroupCredentialsType
): Promise<string> {
return this.server.uploadGroupAvatar(avatar, options);
}
async getGroup(options: GroupCredentialsType): Promise<GroupClass> {
async getGroup(options: GroupCredentialsType): Promise<Proto.Group> {
return this.server.getGroup(options);
}
async getGroupFromLink(
groupInviteLink: string,
auth: GroupCredentialsType
): Promise<GroupJoinInfoClass> {
): Promise<Proto.GroupJoinInfo> {
return this.server.getGroupFromLink(groupInviteLink, auth);
}
@ -1997,10 +1994,10 @@ export default class MessageSender {
}
async modifyGroup(
changes: GroupChangeClass.Actions,
changes: Proto.GroupChange.IActions,
options: GroupCredentialsType,
inviteLinkBase64?: string
): Promise<GroupChangeClass> {
): Promise<Proto.IGroupChange> {
return this.server.modifyGroup(changes, options, inviteLinkBase64);
}
@ -2060,7 +2057,7 @@ export default class MessageSender {
async getGroupMembershipToken(
options: GroupCredentialsType
): Promise<GroupExternalCredentialClass> {
): Promise<Proto.GroupExternalCredential> {
return this.server.getGroupExternalCredential(options);
}

View file

@ -47,23 +47,23 @@ import {
getBytes,
getRandomValue,
splitUuids,
typedArrayToArrayBuffer,
} from '../Crypto';
import { calculateAgreement, generateKeyPair } from '../Curve';
import * as linkPreviewFetch from '../linkPreviews/linkPreviewFetch';
import {
AvatarUploadAttributesClass,
GroupChangeClass,
GroupChangesClass,
GroupClass,
GroupJoinInfoClass,
GroupExternalCredentialClass,
StorageServiceCallOptionsType,
StorageServiceCredentials,
} from '../textsecure.d';
import { SignalService as Proto } from '../protobuf';
import MessageSender from './SendMessage';
// TODO: remove once we move away from ArrayBuffers
const FIXMEU8 = Uint8Array;
// Note: this will break some code that expects to be able to use err.response when a
// web request fails, because it will force it to text. But it is very useful for
// debugging failed requests.
@ -881,7 +881,7 @@ type AjaxOptionsType = {
basicAuth?: string;
call: keyof typeof URL_CALLS;
contentType?: string;
data?: ArrayBuffer | Buffer | string;
data?: ArrayBuffer | Buffer | Uint8Array | string;
headers?: HeaderListType;
host?: string;
httpType: HTTPCodeType;
@ -926,7 +926,7 @@ export type GroupLogResponseType = {
currentRevision?: number;
start?: number;
end?: number;
changes: GroupChangesClass;
changes: Proto.GroupChanges;
};
export type WebAPIType = {
@ -939,17 +939,17 @@ export type WebAPIType = {
options?: { accessKey?: ArrayBuffer }
) => Promise<any>;
createGroup: (
group: GroupClass,
group: Proto.IGroup,
options: GroupCredentialsType
) => Promise<void>;
getAttachment: (cdnKey: string, cdnNumber?: number) => Promise<any>;
getAvatar: (path: string) => Promise<any>;
getDevices: () => Promise<any>;
getGroup: (options: GroupCredentialsType) => Promise<GroupClass>;
getGroup: (options: GroupCredentialsType) => Promise<Proto.Group>;
getGroupFromLink: (
inviteLinkPassword: string,
auth: GroupCredentialsType
) => Promise<GroupJoinInfoClass>;
) => Promise<Proto.GroupJoinInfo>;
getGroupAvatar: (key: string) => Promise<ArrayBuffer>;
getGroupCredentials: (
startDay: number,
@ -957,7 +957,7 @@ export type WebAPIType = {
) => Promise<Array<GroupCredentialType>>;
getGroupExternalCredential: (
options: GroupCredentialsType
) => Promise<GroupExternalCredentialClass>;
) => Promise<Proto.GroupExternalCredential>;
getGroupLog: (
startVersion: number,
options: GroupCredentialsType
@ -1020,10 +1020,10 @@ export type WebAPIType = {
body: ArrayBuffer | undefined
) => Promise<ArrayBufferWithDetailsType>;
modifyGroup: (
changes: GroupChangeClass.Actions,
changes: Proto.GroupChange.IActions,
options: GroupCredentialsType,
inviteLinkBase64?: string
) => Promise<GroupChangeClass>;
) => Promise<Proto.IGroupChange>;
modifyStorageRecords: MessageSender['modifyStorageRecords'];
putAttachment: (encryptedBin: ArrayBuffer) => Promise<any>;
registerCapabilities: (capabilities: CapabilitiesUploadType) => Promise<void>;
@ -1060,7 +1060,7 @@ export type WebAPIType = {
setSignedPreKey: (signedPreKey: SignedPreKeyType) => Promise<void>;
updateDeviceName: (deviceName: string) => Promise<void>;
uploadGroupAvatar: (
avatarData: ArrayBuffer,
avatarData: Uint8Array,
options: GroupCredentialsType
) => Promise<string>;
whoami: () => Promise<any>;
@ -2150,7 +2150,7 @@ export function initialize({
async function getGroupExternalCredential(
options: GroupCredentialsType
): Promise<GroupExternalCredentialClass> {
): Promise<Proto.GroupExternalCredential> {
const basicAuth = generateGroupAuth(
options.groupPublicParamsHex,
options.authCredentialPresentationHex
@ -2165,9 +2165,7 @@ export function initialize({
host: storageUrl,
});
return window.textsecure.protobuf.GroupExternalCredential.decode(
response
);
return Proto.GroupExternalCredential.decode(new FIXMEU8(response));
}
function verifyAttributes(attributes: AvatarUploadAttributesClass) {
@ -2207,7 +2205,7 @@ export function initialize({
}
async function uploadGroupAvatar(
avatarData: ArrayBuffer,
avatarData: Uint8Array,
options: GroupCredentialsType
): Promise<string> {
const basicAuth = generateGroupAuth(
@ -2229,7 +2227,10 @@ export function initialize({
const verified = verifyAttributes(attributes);
const { key } = verified;
const manifestParams = makePutParams(verified, avatarData);
const manifestParams = makePutParams(
verified,
typedArrayToArrayBuffer(avatarData)
);
await _outerAjax(`${cdnUrlObject['0']}/`, {
...manifestParams,
@ -2255,14 +2256,14 @@ export function initialize({
}
async function createGroup(
group: GroupClass,
group: Proto.IGroup,
options: GroupCredentialsType
): Promise<void> {
const basicAuth = generateGroupAuth(
options.groupPublicParamsHex,
options.authCredentialPresentationHex
);
const data = group.toArrayBuffer();
const data = Proto.Group.encode(group).finish();
await _ajax({
basicAuth,
@ -2276,7 +2277,7 @@ export function initialize({
async function getGroup(
options: GroupCredentialsType
): Promise<GroupClass> {
): Promise<Proto.Group> {
const basicAuth = generateGroupAuth(
options.groupPublicParamsHex,
options.authCredentialPresentationHex
@ -2291,13 +2292,13 @@ export function initialize({
responseType: 'arraybuffer',
});
return window.textsecure.protobuf.Group.decode(response);
return Proto.Group.decode(new FIXMEU8(response));
}
async function getGroupFromLink(
inviteLinkPassword: string,
auth: GroupCredentialsType
): Promise<GroupJoinInfoClass> {
): Promise<Proto.GroupJoinInfo> {
const basicAuth = generateGroupAuth(
auth.groupPublicParamsHex,
auth.authCredentialPresentationHex
@ -2315,19 +2316,19 @@ export function initialize({
redactUrl: _createRedactor(safeInviteLinkPassword),
});
return window.textsecure.protobuf.GroupJoinInfo.decode(response);
return Proto.GroupJoinInfo.decode(new FIXMEU8(response));
}
async function modifyGroup(
changes: GroupChangeClass.Actions,
changes: Proto.GroupChange.IActions,
options: GroupCredentialsType,
inviteLinkBase64?: string
): Promise<GroupChangeClass> {
): Promise<Proto.IGroupChange> {
const basicAuth = generateGroupAuth(
options.groupPublicParamsHex,
options.authCredentialPresentationHex
);
const data = changes.toArrayBuffer();
const data = Proto.GroupChange.Actions.encode(changes).finish();
const safeInviteLinkPassword = inviteLinkBase64
? toWebSafeBase64(inviteLinkBase64)
: undefined;
@ -2348,7 +2349,7 @@ export function initialize({
: undefined,
});
return window.textsecure.protobuf.GroupChange.decode(response);
return Proto.GroupChange.decode(new FIXMEU8(response));
}
async function getGroupLog(
@ -2370,7 +2371,7 @@ export function initialize({
urlParameters: `/${startVersion}`,
});
const { data, response } = withDetails;
const changes = window.textsecure.protobuf.GroupChanges.decode(data);
const changes = Proto.GroupChanges.decode(new FIXMEU8(data));
if (response && response.status === 206) {
const range = response.headers.get('Content-Range');

View file

@ -19,3 +19,15 @@ export function assert(condition: unknown, message: string): asserts condition {
log.error('assert failure:', err && err.stack ? err.stack : err);
}
}
/**
* Throws an error if the condition is falsy, regardless of environment.
*/
export function strictAssert(
condition: unknown,
message: string
): asserts condition {
if (!condition) {
throw new Error(message);
}
}

11
ts/util/dropNull.ts Normal file
View file

@ -0,0 +1,11 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
export function dropNull<T>(
value: NonNullable<T> | null | undefined
): T | undefined {
if (value === null) {
return undefined;
}
return value;
}

8
ts/util/isValidGuid.ts Normal file
View file

@ -0,0 +1,8 @@
// Copyright 2017-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
export const isValidGuid = (value: unknown): value is string =>
typeof value === 'string' &&
/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i.test(
value
);

14
ts/util/normalizeUuid.ts Normal file
View file

@ -0,0 +1,14 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { isValidGuid } from './isValidGuid';
export function normalizeUuid(uuid: string, context: string): string {
if (!isValidGuid(uuid)) {
window.log.warn(
`Normalizing invalid uuid: ${uuid} in context "${context}"`
);
}
return uuid.toLowerCase();
}

View file

@ -19,58 +19,51 @@ import {
ServerPublicParams,
UuidCiphertext,
} from 'zkgroup';
import {
arrayBufferToBase64,
arrayBufferToHex,
base64ToArrayBuffer,
typedArrayToArrayBuffer,
} from '../Crypto';
import * as Bytes from '../Bytes';
export * from 'zkgroup';
export function arrayBufferToCompatArray(
arrayBuffer: ArrayBuffer
export function uint8ArrayToCompatArray(
buffer: Uint8Array
): FFICompatArrayType {
const buffer = Buffer.from(arrayBuffer);
return new FFICompatArray(buffer);
return new FFICompatArray(Buffer.from(buffer));
}
export function compatArrayToArrayBuffer(
export function compatArrayToUint8Array(
compatArray: FFICompatArrayType
): ArrayBuffer {
return typedArrayToArrayBuffer(compatArray.buffer);
): Uint8Array {
return compatArray.buffer;
}
export function base64ToCompatArray(base64: string): FFICompatArrayType {
return arrayBufferToCompatArray(base64ToArrayBuffer(base64));
return uint8ArrayToCompatArray(Bytes.fromBase64(base64));
}
export function compatArrayToBase64(compatArray: FFICompatArrayType): string {
return arrayBufferToBase64(compatArrayToArrayBuffer(compatArray));
return Bytes.toBase64(compatArrayToUint8Array(compatArray));
}
export function compatArrayToHex(compatArray: FFICompatArrayType): string {
return arrayBufferToHex(compatArrayToArrayBuffer(compatArray));
return Bytes.toHex(compatArrayToUint8Array(compatArray));
}
// Scenarios
export function decryptGroupBlob(
clientZkGroupCipher: ClientZkGroupCipher,
ciphertext: ArrayBuffer
): ArrayBuffer {
return compatArrayToArrayBuffer(
clientZkGroupCipher.decryptBlob(arrayBufferToCompatArray(ciphertext))
ciphertext: Uint8Array
): Uint8Array {
return compatArrayToUint8Array(
clientZkGroupCipher.decryptBlob(uint8ArrayToCompatArray(ciphertext))
);
}
export function decryptProfileKeyCredentialPresentation(
clientZkGroupCipher: ClientZkGroupCipher,
presentationBuffer: ArrayBuffer
): { profileKey: ArrayBuffer; uuid: string } {
presentationBuffer: Uint8Array
): { profileKey: Uint8Array; uuid: string } {
const presentation = new ProfileKeyCredentialPresentation(
arrayBufferToCompatArray(presentationBuffer)
uint8ArrayToCompatArray(presentationBuffer)
);
const uuidCiphertext = presentation.getUuidCiphertext();
@ -83,18 +76,18 @@ export function decryptProfileKeyCredentialPresentation(
);
return {
profileKey: compatArrayToArrayBuffer(profileKey.serialize()),
profileKey: compatArrayToUint8Array(profileKey.serialize()),
uuid,
};
}
export function decryptProfileKey(
clientZkGroupCipher: ClientZkGroupCipher,
profileKeyCiphertextBuffer: ArrayBuffer,
profileKeyCiphertextBuffer: Uint8Array,
uuid: string
): ArrayBuffer {
): Uint8Array {
const profileKeyCiphertext = new ProfileKeyCiphertext(
arrayBufferToCompatArray(profileKeyCiphertextBuffer)
uint8ArrayToCompatArray(profileKeyCiphertextBuffer)
);
const profileKey = clientZkGroupCipher.decryptProfileKey(
@ -102,15 +95,15 @@ export function decryptProfileKey(
uuid
);
return compatArrayToArrayBuffer(profileKey.serialize());
return compatArrayToUint8Array(profileKey.serialize());
}
export function decryptUuid(
clientZkGroupCipher: ClientZkGroupCipher,
uuidCiphertextBuffer: ArrayBuffer
uuidCiphertextBuffer: Uint8Array
): string {
const uuidCiphertext = new UuidCiphertext(
arrayBufferToCompatArray(uuidCiphertextBuffer)
uint8ArrayToCompatArray(uuidCiphertextBuffer)
);
return clientZkGroupCipher.decryptUuid(uuidCiphertext);
@ -129,56 +122,54 @@ export function deriveProfileKeyVersion(
}
export function deriveGroupPublicParams(
groupSecretParamsBuffer: ArrayBuffer
): ArrayBuffer {
groupSecretParamsBuffer: Uint8Array
): Uint8Array {
const groupSecretParams = new GroupSecretParams(
arrayBufferToCompatArray(groupSecretParamsBuffer)
uint8ArrayToCompatArray(groupSecretParamsBuffer)
);
return compatArrayToArrayBuffer(
return compatArrayToUint8Array(
groupSecretParams.getPublicParams().serialize()
);
}
export function deriveGroupID(
groupSecretParamsBuffer: ArrayBuffer
): ArrayBuffer {
export function deriveGroupID(groupSecretParamsBuffer: Uint8Array): Uint8Array {
const groupSecretParams = new GroupSecretParams(
arrayBufferToCompatArray(groupSecretParamsBuffer)
uint8ArrayToCompatArray(groupSecretParamsBuffer)
);
return compatArrayToArrayBuffer(
return compatArrayToUint8Array(
groupSecretParams.getPublicParams().getGroupIdentifier().serialize()
);
}
export function deriveGroupSecretParams(
masterKeyBuffer: ArrayBuffer
): ArrayBuffer {
masterKeyBuffer: Uint8Array
): Uint8Array {
const masterKey = new GroupMasterKey(
arrayBufferToCompatArray(masterKeyBuffer)
uint8ArrayToCompatArray(masterKeyBuffer)
);
const groupSecretParams = GroupSecretParams.deriveFromMasterKey(masterKey);
return compatArrayToArrayBuffer(groupSecretParams.serialize());
return compatArrayToUint8Array(groupSecretParams.serialize());
}
export function encryptGroupBlob(
clientZkGroupCipher: ClientZkGroupCipher,
plaintext: ArrayBuffer
): ArrayBuffer {
return compatArrayToArrayBuffer(
clientZkGroupCipher.encryptBlob(arrayBufferToCompatArray(plaintext))
plaintext: Uint8Array
): Uint8Array {
return compatArrayToUint8Array(
clientZkGroupCipher.encryptBlob(uint8ArrayToCompatArray(plaintext))
);
}
export function encryptUuid(
clientZkGroupCipher: ClientZkGroupCipher,
uuidPlaintext: string
): ArrayBuffer {
): Uint8Array {
const uuidCiphertext = clientZkGroupCipher.encryptUuid(uuidPlaintext);
return compatArrayToArrayBuffer(uuidCiphertext.serialize());
return compatArrayToUint8Array(uuidCiphertext.serialize());
}
export function generateProfileKeyCredentialRequest(
@ -206,7 +197,7 @@ export function getAuthCredentialPresentation(
clientZkAuthOperations: ClientZkAuthOperations,
authCredentialBase64: string,
groupSecretParamsBase64: string
): ArrayBuffer {
): Uint8Array {
const authCredential = new AuthCredential(
base64ToCompatArray(authCredentialBase64)
);
@ -218,14 +209,14 @@ export function getAuthCredentialPresentation(
secretParams,
authCredential
);
return compatArrayToArrayBuffer(presentation.serialize());
return compatArrayToUint8Array(presentation.serialize());
}
export function createProfileKeyCredentialPresentation(
clientZkProfileCipher: ClientZkProfileOperations,
profileKeyCredentialBase64: string,
groupSecretParamsBase64: string
): ArrayBuffer {
): Uint8Array {
const profileKeyCredentialArray = base64ToCompatArray(
profileKeyCredentialBase64
);
@ -241,7 +232,7 @@ export function createProfileKeyCredentialPresentation(
profileKeyCredential
);
return compatArrayToArrayBuffer(presentation.serialize());
return compatArrayToUint8Array(presentation.serialize());
}
export function getClientZkAuthOperations(

View file

@ -23,6 +23,7 @@ import {
isGroupV1,
isMe,
} from '../util/whatTypeOfConversation';
import * as Bytes from '../Bytes';
import {
canReply,
getAttachmentsForMessage,
@ -4157,13 +4158,11 @@ Whisper.ConversationView = Whisper.View.extend({
} = window.Signal.Groups.parseGroupLink(groupData);
const fields = window.Signal.Groups.deriveGroupFields(
window.Signal.Crypto.base64ToArrayBuffer(masterKey)
Bytes.fromBase64(masterKey)
);
const id = window.Signal.Crypto.arrayBufferToBase64(fields.id);
const id = Bytes.toBase64(fields.id);
const logId = `groupv2(${id})`;
const secretParams = window.Signal.Crypto.arrayBufferToBase64(
fields.secretParams
);
const secretParams = Bytes.toBase64(fields.secretParams);
window.log.info(`getGroupPreview/${logId}: Fetching pre-join state`);
const result = await window.Signal.Groups.getPreJoinGroupInfo(

5
ts/window.d.ts vendored
View file

@ -112,12 +112,14 @@ import { MIMEType } from './types/MIME';
import { AttachmentType } from './types/Attachment';
import { ElectronLocaleType } from './util/mapToSupportLocale';
import { SignalProtocolStore } from './SignalProtocolStore';
import { Context as SignalContext } from './context';
import { StartupQueue } from './util/StartupQueue';
import * as synchronousCrypto from './util/synchronousCrypto';
import { SocketStatus } from './types/SocketStatus';
import SyncRequest from './textsecure/SyncRequest';
import { ConversationColorType, CustomColorType } from './types/Colors';
import { MessageController } from './util/MessageController';
import { isValidGuid } from './util/isValidGuid';
import { StateType } from './state/reducer';
export { Long } from 'long';
@ -211,7 +213,7 @@ declare global {
isAfterVersion: (version: string, anotherVersion: string) => boolean;
isBeforeVersion: (version: string, anotherVersion: string) => boolean;
isFullScreen: () => boolean;
isValidGuid: (maybeGuid: string | null) => boolean;
isValidGuid: typeof isValidGuid;
isValidE164: (maybeE164: unknown) => boolean;
libphonenumber: {
util: {
@ -524,6 +526,7 @@ declare global {
};
challengeHandler: ChallengeHandler;
};
SignalContext: SignalContext;
ConversationController: ConversationController;
Events: WhatIsThis;