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, parseEnvironment,
} = require('./ts/environment'); } = require('./ts/environment');
const { Context: SignalContext } = require('./ts/context');
window.SignalContext = new SignalContext();
const config = url.parse(window.location.toString(), true).query; const config = url.parse(window.location.toString(), true).query;
const { locale } = config; const { locale } = config;
const localeMessages = ipcRenderer.sendSync('locale-data'); const localeMessages = ipcRenderer.sendSync('locale-data');

View File

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

View File

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

View File

@ -18,6 +18,10 @@ const {
CallingScreenSharingController, CallingScreenSharingController,
} = require('./ts/components/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 config = url.parse(window.location.toString(), true).query;
const { locale } = config; const { locale } = config;
const localeMessages = ipcRenderer.sendSync('locale-data'); const localeMessages = ipcRenderer.sendSync('locale-data');

View File

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

View File

@ -21,12 +21,16 @@ const { makeGetter } = require('../preload_utils');
const { dialog } = remote; const { dialog } = remote;
const { nativeTheme } = remote.require('electron'); const { nativeTheme } = remote.require('electron');
const { Context: SignalContext } = require('../ts/context');
const STICKER_SIZE = 512; const STICKER_SIZE = 512;
const MIN_STICKER_DIMENSION = 10; const MIN_STICKER_DIMENSION = 10;
const MAX_STICKER_DIMENSION = STICKER_SIZE; const MAX_STICKER_DIMENSION = STICKER_SIZE;
const MAX_WEBP_STICKER_BYTE_LENGTH = 100 * 1024; const MAX_WEBP_STICKER_BYTE_LENGTH = 100 * 1024;
const MAX_ANIMATED_STICKER_BYTE_LENGTH = 300 * 1024; const MAX_ANIMATED_STICKER_BYTE_LENGTH = 300 * 1024;
window.SignalContext = new SignalContext();
setEnvironment(parseEnvironment(config.environment)); setEnvironment(parseEnvironment(config.environment));
window.sqlInitializer = require('../ts/sql/initialize'); 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 ByteBuffer = require('../components/bytebuffer/dist/ByteBufferAB.js');
const Long = require('../components/long/dist/Long.js'); const Long = require('../components/long/dist/Long.js');
const { setEnvironment, Environment } = require('../ts/environment'); const { setEnvironment, Environment } = require('../ts/environment');
const { Context: SignalContext } = require('../ts/context');
const { isValidGuid } = require('../ts/util/isValidGuid');
chai.use(chaiAsPromised); chai.use(chaiAsPromised);
@ -18,6 +20,7 @@ const storageMap = new Map();
// To replicate logic we have on the client side // To replicate logic we have on the client side
global.window = { global.window = {
SignalContext: new SignalContext(),
log: { log: {
info: (...args) => console.log(...args), info: (...args) => console.log(...args),
warn: (...args) => console.warn(...args), warn: (...args) => console.warn(...args),
@ -32,6 +35,7 @@ global.window = {
get: key => storageMap.get(key), get: key => storageMap.get(key),
put: async (key, value) => storageMap.set(key, value), put: async (key, value) => storageMap.set(key, value),
}, },
isValidGuid,
}; };
// For ducks/network.getEmptyState() // 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, LINK_VERSION_ERROR,
parseGroupLink, parseGroupLink,
} from '../groups'; } from '../groups';
import { arrayBufferToBase64, base64ToArrayBuffer } from '../Crypto'; import * as Bytes from '../Bytes';
import { longRunningTaskWrapper } from '../util/longRunningTaskWrapper'; import { longRunningTaskWrapper } from '../util/longRunningTaskWrapper';
import { isGroupV1 } from '../util/whatTypeOfConversation'; import { isGroupV1 } from '../util/whatTypeOfConversation';
import type { GroupJoinInfoClass } from '../textsecure.d';
import type { ConversationAttributesType } from '../model-types.d'; import type { ConversationAttributesType } from '../model-types.d';
import type { ConversationModel } from '../models/conversations'; import type { ConversationModel } from '../models/conversations';
import type { PreJoinConversationType } from '../state/ducks/conversations'; import type { PreJoinConversationType } from '../state/ducks/conversations';
import { SignalService as Proto } from '../protobuf';
export async function joinViaLink(hash: string): Promise<void> { export async function joinViaLink(hash: string): Promise<void> {
let inviteLinkPassword: string; let inviteLinkPassword: string;
@ -42,11 +42,11 @@ export async function joinViaLink(hash: string): Promise<void> {
return; return;
} }
const data = deriveGroupFields(base64ToArrayBuffer(masterKey)); const data = deriveGroupFields(Bytes.fromBase64(masterKey));
const id = arrayBufferToBase64(data.id); const id = Bytes.toBase64(data.id);
const logId = `groupv2(${id})`; const logId = `groupv2(${id})`;
const secretParams = arrayBufferToBase64(data.secretParams); const secretParams = Bytes.toBase64(data.secretParams);
const publicParams = arrayBufferToBase64(data.publicParams); const publicParams = Bytes.toBase64(data.publicParams);
const existingConversation = const existingConversation =
window.ConversationController.get(id) || window.ConversationController.get(id) ||
@ -70,7 +70,7 @@ export async function joinViaLink(hash: string): Promise<void> {
return; return;
} }
let result: GroupJoinInfoClass; let result: Proto.GroupJoinInfo;
try { try {
result = await longRunningTaskWrapper({ result = await longRunningTaskWrapper({

View File

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

View File

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

View File

@ -9,6 +9,7 @@ import {
deriveMasterKeyFromGroupV1, deriveMasterKeyFromGroupV1,
fromEncodedBinaryToArrayBuffer, fromEncodedBinaryToArrayBuffer,
} from '../Crypto'; } from '../Crypto';
import * as Bytes from '../Bytes';
import dataInterface from '../sql/Client'; import dataInterface from '../sql/Client';
import { import {
AccountRecordClass, AccountRecordClass,
@ -47,6 +48,9 @@ import { isGroupV1, isGroupV2 } from '../util/whatTypeOfConversation';
const { updateConversation } = dataInterface; const { updateConversation } = dataInterface;
// TODO: remove once we move away from ArrayBuffers
const FIXMEU8 = Uint8Array;
type RecordClass = type RecordClass =
| AccountRecordClass | AccountRecordClass
| ContactRecordClass | ContactRecordClass
@ -520,8 +524,8 @@ export async function mergeGroupV1Record(
// retrieve the master key and find the conversation locally. If we // retrieve the master key and find the conversation locally. If we
// are successful then we continue setting and applying state. // are successful then we continue setting and applying state.
const masterKeyBuffer = await deriveMasterKeyFromGroupV1(groupId); const masterKeyBuffer = await deriveMasterKeyFromGroupV1(groupId);
const fields = deriveGroupFields(masterKeyBuffer); const fields = deriveGroupFields(new FIXMEU8(masterKeyBuffer));
const derivedGroupV2Id = arrayBufferToBase64(fields.id); const derivedGroupV2Id = Bytes.toBase64(fields.id);
window.log.info( window.log.info(
'storageService.mergeGroupV1Record: failed to find group by v1 id ' + 'storageService.mergeGroupV1Record: failed to find group by v1 id ' +
@ -596,12 +600,12 @@ export async function mergeGroupV1Record(
async function getGroupV2Conversation( async function getGroupV2Conversation(
masterKeyBuffer: ArrayBuffer masterKeyBuffer: ArrayBuffer
): Promise<ConversationModel> { ): 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 masterKey = arrayBufferToBase64(masterKeyBuffer);
const secretParams = arrayBufferToBase64(groupFields.secretParams); const secretParams = Bytes.toBase64(groupFields.secretParams);
const publicParams = arrayBufferToBase64(groupFields.publicParams); const publicParams = Bytes.toBase64(groupFields.publicParams);
// First we check for an existing GroupV2 group // First we check for an existing GroupV2 group
const groupV2 = window.ConversationController.get(groupId); const groupV2 = window.ConversationController.get(groupId);
@ -944,7 +948,7 @@ export async function mergeAccountRecord(
} }
const masterKeyBuffer = pinnedConversation.groupMasterKey.toArrayBuffer(); const masterKeyBuffer = pinnedConversation.groupMasterKey.toArrayBuffer();
const groupFields = deriveGroupFields(masterKeyBuffer); const groupFields = deriveGroupFields(masterKeyBuffer);
const groupId = arrayBufferToBase64(groupFields.id); const groupId = Bytes.toBase64(groupFields.id);
conversationId = groupId; conversationId = groupId;
break; 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 * as chai from 'chai';
import { assert } from '../../util/assert'; import { assert, strictAssert } from '../../util/assert';
describe('assert', () => { describe('assert utilities', () => {
it('does nothing if the assertion passes', () => { describe('assert', () => {
assert(true, 'foo bar'); 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", () => { describe('strictAssert', () => {
chai.assert.throws(() => { it('does nothing if the assertion passes', () => {
assert(false, 'foo bar'); strictAssert(true, 'foo bar');
}, '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, { import WebSocketResource, {
IncomingWebSocketRequest, IncomingWebSocketRequest,
} from './WebsocketResources'; } from './WebsocketResources';
import * as Bytes from '../Bytes';
import Crypto from './Crypto'; import Crypto from './Crypto';
import { deriveMasterKeyFromGroupV1, typedArrayToArrayBuffer } from '../Crypto'; import { deriveMasterKeyFromGroupV1, typedArrayToArrayBuffer } from '../Crypto';
import { ContactBuffer, GroupBuffer } from './ContactsParser'; import { ContactBuffer, GroupBuffer } from './ContactsParser';
@ -73,6 +74,9 @@ import { ByteBufferClass } from '../window.d';
import { deriveGroupFields, MASTER_KEY_LENGTH } from '../groups'; import { deriveGroupFields, MASTER_KEY_LENGTH } from '../groups';
// TODO: remove once we move away from ArrayBuffers
const FIXMEU8 = Uint8Array;
const GROUPV1_ID_LENGTH = 16; const GROUPV1_ID_LENGTH = 16;
const GROUPV2_ID_LENGTH = 32; const GROUPV2_ID_LENGTH = 32;
const RETRY_TIMEOUT = 2 * 60 * 1000; const RETRY_TIMEOUT = 2 * 60 * 1000;
@ -1991,10 +1995,9 @@ class MessageReceiverInner extends EventTarget {
); );
} }
const masterKey = await deriveMasterKeyFromGroupV1(groupId); const masterKey = await deriveMasterKeyFromGroupV1(groupId);
const data = deriveGroupFields(masterKey); const data = deriveGroupFields(new FIXMEU8(masterKey));
const toBase64 = MessageReceiverInner.arrayBufferToStringBase64; return Bytes.toBase64(data.id);
return toBase64(data.id);
} }
async deriveGroupV1Data(message: DataMessageClass) { 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.masterKey = toBase64(masterKey);
groupV2.secretParams = toBase64(fields.secretParams); groupV2.secretParams = Bytes.toBase64(fields.secretParams);
groupV2.publicParams = toBase64(fields.publicParams); groupV2.publicParams = Bytes.toBase64(fields.publicParams);
groupV2.id = toBase64(fields.id); groupV2.id = Bytes.toBase64(fields.id);
if (groupV2.groupChange) { if (groupV2.groupChange) {
groupV2.groupChange = groupV2.groupChange.toString('base64'); groupV2.groupChange = groupV2.groupChange.toString('base64');

View File

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

View File

@ -47,23 +47,23 @@ import {
getBytes, getBytes,
getRandomValue, getRandomValue,
splitUuids, splitUuids,
typedArrayToArrayBuffer,
} from '../Crypto'; } from '../Crypto';
import { calculateAgreement, generateKeyPair } from '../Curve'; import { calculateAgreement, generateKeyPair } from '../Curve';
import * as linkPreviewFetch from '../linkPreviews/linkPreviewFetch'; import * as linkPreviewFetch from '../linkPreviews/linkPreviewFetch';
import { import {
AvatarUploadAttributesClass, AvatarUploadAttributesClass,
GroupChangeClass,
GroupChangesClass,
GroupClass,
GroupJoinInfoClass,
GroupExternalCredentialClass,
StorageServiceCallOptionsType, StorageServiceCallOptionsType,
StorageServiceCredentials, StorageServiceCredentials,
} from '../textsecure.d'; } from '../textsecure.d';
import { SignalService as Proto } from '../protobuf';
import MessageSender from './SendMessage'; 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 // 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 // web request fails, because it will force it to text. But it is very useful for
// debugging failed requests. // debugging failed requests.
@ -881,7 +881,7 @@ type AjaxOptionsType = {
basicAuth?: string; basicAuth?: string;
call: keyof typeof URL_CALLS; call: keyof typeof URL_CALLS;
contentType?: string; contentType?: string;
data?: ArrayBuffer | Buffer | string; data?: ArrayBuffer | Buffer | Uint8Array | string;
headers?: HeaderListType; headers?: HeaderListType;
host?: string; host?: string;
httpType: HTTPCodeType; httpType: HTTPCodeType;
@ -926,7 +926,7 @@ export type GroupLogResponseType = {
currentRevision?: number; currentRevision?: number;
start?: number; start?: number;
end?: number; end?: number;
changes: GroupChangesClass; changes: Proto.GroupChanges;
}; };
export type WebAPIType = { export type WebAPIType = {
@ -939,17 +939,17 @@ export type WebAPIType = {
options?: { accessKey?: ArrayBuffer } options?: { accessKey?: ArrayBuffer }
) => Promise<any>; ) => Promise<any>;
createGroup: ( createGroup: (
group: GroupClass, group: Proto.IGroup,
options: GroupCredentialsType options: GroupCredentialsType
) => Promise<void>; ) => Promise<void>;
getAttachment: (cdnKey: string, cdnNumber?: number) => Promise<any>; getAttachment: (cdnKey: string, cdnNumber?: number) => Promise<any>;
getAvatar: (path: string) => Promise<any>; getAvatar: (path: string) => Promise<any>;
getDevices: () => Promise<any>; getDevices: () => Promise<any>;
getGroup: (options: GroupCredentialsType) => Promise<GroupClass>; getGroup: (options: GroupCredentialsType) => Promise<Proto.Group>;
getGroupFromLink: ( getGroupFromLink: (
inviteLinkPassword: string, inviteLinkPassword: string,
auth: GroupCredentialsType auth: GroupCredentialsType
) => Promise<GroupJoinInfoClass>; ) => Promise<Proto.GroupJoinInfo>;
getGroupAvatar: (key: string) => Promise<ArrayBuffer>; getGroupAvatar: (key: string) => Promise<ArrayBuffer>;
getGroupCredentials: ( getGroupCredentials: (
startDay: number, startDay: number,
@ -957,7 +957,7 @@ export type WebAPIType = {
) => Promise<Array<GroupCredentialType>>; ) => Promise<Array<GroupCredentialType>>;
getGroupExternalCredential: ( getGroupExternalCredential: (
options: GroupCredentialsType options: GroupCredentialsType
) => Promise<GroupExternalCredentialClass>; ) => Promise<Proto.GroupExternalCredential>;
getGroupLog: ( getGroupLog: (
startVersion: number, startVersion: number,
options: GroupCredentialsType options: GroupCredentialsType
@ -1020,10 +1020,10 @@ export type WebAPIType = {
body: ArrayBuffer | undefined body: ArrayBuffer | undefined
) => Promise<ArrayBufferWithDetailsType>; ) => Promise<ArrayBufferWithDetailsType>;
modifyGroup: ( modifyGroup: (
changes: GroupChangeClass.Actions, changes: Proto.GroupChange.IActions,
options: GroupCredentialsType, options: GroupCredentialsType,
inviteLinkBase64?: string inviteLinkBase64?: string
) => Promise<GroupChangeClass>; ) => Promise<Proto.IGroupChange>;
modifyStorageRecords: MessageSender['modifyStorageRecords']; modifyStorageRecords: MessageSender['modifyStorageRecords'];
putAttachment: (encryptedBin: ArrayBuffer) => Promise<any>; putAttachment: (encryptedBin: ArrayBuffer) => Promise<any>;
registerCapabilities: (capabilities: CapabilitiesUploadType) => Promise<void>; registerCapabilities: (capabilities: CapabilitiesUploadType) => Promise<void>;
@ -1060,7 +1060,7 @@ export type WebAPIType = {
setSignedPreKey: (signedPreKey: SignedPreKeyType) => Promise<void>; setSignedPreKey: (signedPreKey: SignedPreKeyType) => Promise<void>;
updateDeviceName: (deviceName: string) => Promise<void>; updateDeviceName: (deviceName: string) => Promise<void>;
uploadGroupAvatar: ( uploadGroupAvatar: (
avatarData: ArrayBuffer, avatarData: Uint8Array,
options: GroupCredentialsType options: GroupCredentialsType
) => Promise<string>; ) => Promise<string>;
whoami: () => Promise<any>; whoami: () => Promise<any>;
@ -2150,7 +2150,7 @@ export function initialize({
async function getGroupExternalCredential( async function getGroupExternalCredential(
options: GroupCredentialsType options: GroupCredentialsType
): Promise<GroupExternalCredentialClass> { ): Promise<Proto.GroupExternalCredential> {
const basicAuth = generateGroupAuth( const basicAuth = generateGroupAuth(
options.groupPublicParamsHex, options.groupPublicParamsHex,
options.authCredentialPresentationHex options.authCredentialPresentationHex
@ -2165,9 +2165,7 @@ export function initialize({
host: storageUrl, host: storageUrl,
}); });
return window.textsecure.protobuf.GroupExternalCredential.decode( return Proto.GroupExternalCredential.decode(new FIXMEU8(response));
response
);
} }
function verifyAttributes(attributes: AvatarUploadAttributesClass) { function verifyAttributes(attributes: AvatarUploadAttributesClass) {
@ -2207,7 +2205,7 @@ export function initialize({
} }
async function uploadGroupAvatar( async function uploadGroupAvatar(
avatarData: ArrayBuffer, avatarData: Uint8Array,
options: GroupCredentialsType options: GroupCredentialsType
): Promise<string> { ): Promise<string> {
const basicAuth = generateGroupAuth( const basicAuth = generateGroupAuth(
@ -2229,7 +2227,10 @@ export function initialize({
const verified = verifyAttributes(attributes); const verified = verifyAttributes(attributes);
const { key } = verified; const { key } = verified;
const manifestParams = makePutParams(verified, avatarData); const manifestParams = makePutParams(
verified,
typedArrayToArrayBuffer(avatarData)
);
await _outerAjax(`${cdnUrlObject['0']}/`, { await _outerAjax(`${cdnUrlObject['0']}/`, {
...manifestParams, ...manifestParams,
@ -2255,14 +2256,14 @@ export function initialize({
} }
async function createGroup( async function createGroup(
group: GroupClass, group: Proto.IGroup,
options: GroupCredentialsType options: GroupCredentialsType
): Promise<void> { ): Promise<void> {
const basicAuth = generateGroupAuth( const basicAuth = generateGroupAuth(
options.groupPublicParamsHex, options.groupPublicParamsHex,
options.authCredentialPresentationHex options.authCredentialPresentationHex
); );
const data = group.toArrayBuffer(); const data = Proto.Group.encode(group).finish();
await _ajax({ await _ajax({
basicAuth, basicAuth,
@ -2276,7 +2277,7 @@ export function initialize({
async function getGroup( async function getGroup(
options: GroupCredentialsType options: GroupCredentialsType
): Promise<GroupClass> { ): Promise<Proto.Group> {
const basicAuth = generateGroupAuth( const basicAuth = generateGroupAuth(
options.groupPublicParamsHex, options.groupPublicParamsHex,
options.authCredentialPresentationHex options.authCredentialPresentationHex
@ -2291,13 +2292,13 @@ export function initialize({
responseType: 'arraybuffer', responseType: 'arraybuffer',
}); });
return window.textsecure.protobuf.Group.decode(response); return Proto.Group.decode(new FIXMEU8(response));
} }
async function getGroupFromLink( async function getGroupFromLink(
inviteLinkPassword: string, inviteLinkPassword: string,
auth: GroupCredentialsType auth: GroupCredentialsType
): Promise<GroupJoinInfoClass> { ): Promise<Proto.GroupJoinInfo> {
const basicAuth = generateGroupAuth( const basicAuth = generateGroupAuth(
auth.groupPublicParamsHex, auth.groupPublicParamsHex,
auth.authCredentialPresentationHex auth.authCredentialPresentationHex
@ -2315,19 +2316,19 @@ export function initialize({
redactUrl: _createRedactor(safeInviteLinkPassword), redactUrl: _createRedactor(safeInviteLinkPassword),
}); });
return window.textsecure.protobuf.GroupJoinInfo.decode(response); return Proto.GroupJoinInfo.decode(new FIXMEU8(response));
} }
async function modifyGroup( async function modifyGroup(
changes: GroupChangeClass.Actions, changes: Proto.GroupChange.IActions,
options: GroupCredentialsType, options: GroupCredentialsType,
inviteLinkBase64?: string inviteLinkBase64?: string
): Promise<GroupChangeClass> { ): Promise<Proto.IGroupChange> {
const basicAuth = generateGroupAuth( const basicAuth = generateGroupAuth(
options.groupPublicParamsHex, options.groupPublicParamsHex,
options.authCredentialPresentationHex options.authCredentialPresentationHex
); );
const data = changes.toArrayBuffer(); const data = Proto.GroupChange.Actions.encode(changes).finish();
const safeInviteLinkPassword = inviteLinkBase64 const safeInviteLinkPassword = inviteLinkBase64
? toWebSafeBase64(inviteLinkBase64) ? toWebSafeBase64(inviteLinkBase64)
: undefined; : undefined;
@ -2348,7 +2349,7 @@ export function initialize({
: undefined, : undefined,
}); });
return window.textsecure.protobuf.GroupChange.decode(response); return Proto.GroupChange.decode(new FIXMEU8(response));
} }
async function getGroupLog( async function getGroupLog(
@ -2370,7 +2371,7 @@ export function initialize({
urlParameters: `/${startVersion}`, urlParameters: `/${startVersion}`,
}); });
const { data, response } = withDetails; 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) { if (response && response.status === 206) {
const range = response.headers.get('Content-Range'); 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); 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, ServerPublicParams,
UuidCiphertext, UuidCiphertext,
} from 'zkgroup'; } from 'zkgroup';
import { import * as Bytes from '../Bytes';
arrayBufferToBase64,
arrayBufferToHex,
base64ToArrayBuffer,
typedArrayToArrayBuffer,
} from '../Crypto';
export * from 'zkgroup'; export * from 'zkgroup';
export function arrayBufferToCompatArray( export function uint8ArrayToCompatArray(
arrayBuffer: ArrayBuffer buffer: Uint8Array
): FFICompatArrayType { ): FFICompatArrayType {
const buffer = Buffer.from(arrayBuffer); return new FFICompatArray(Buffer.from(buffer));
return new FFICompatArray(buffer);
} }
export function compatArrayToArrayBuffer( export function compatArrayToUint8Array(
compatArray: FFICompatArrayType compatArray: FFICompatArrayType
): ArrayBuffer { ): Uint8Array {
return typedArrayToArrayBuffer(compatArray.buffer); return compatArray.buffer;
} }
export function base64ToCompatArray(base64: string): FFICompatArrayType { export function base64ToCompatArray(base64: string): FFICompatArrayType {
return arrayBufferToCompatArray(base64ToArrayBuffer(base64)); return uint8ArrayToCompatArray(Bytes.fromBase64(base64));
} }
export function compatArrayToBase64(compatArray: FFICompatArrayType): string { export function compatArrayToBase64(compatArray: FFICompatArrayType): string {
return arrayBufferToBase64(compatArrayToArrayBuffer(compatArray)); return Bytes.toBase64(compatArrayToUint8Array(compatArray));
} }
export function compatArrayToHex(compatArray: FFICompatArrayType): string { export function compatArrayToHex(compatArray: FFICompatArrayType): string {
return arrayBufferToHex(compatArrayToArrayBuffer(compatArray)); return Bytes.toHex(compatArrayToUint8Array(compatArray));
} }
// Scenarios // Scenarios
export function decryptGroupBlob( export function decryptGroupBlob(
clientZkGroupCipher: ClientZkGroupCipher, clientZkGroupCipher: ClientZkGroupCipher,
ciphertext: ArrayBuffer ciphertext: Uint8Array
): ArrayBuffer { ): Uint8Array {
return compatArrayToArrayBuffer( return compatArrayToUint8Array(
clientZkGroupCipher.decryptBlob(arrayBufferToCompatArray(ciphertext)) clientZkGroupCipher.decryptBlob(uint8ArrayToCompatArray(ciphertext))
); );
} }
export function decryptProfileKeyCredentialPresentation( export function decryptProfileKeyCredentialPresentation(
clientZkGroupCipher: ClientZkGroupCipher, clientZkGroupCipher: ClientZkGroupCipher,
presentationBuffer: ArrayBuffer presentationBuffer: Uint8Array
): { profileKey: ArrayBuffer; uuid: string } { ): { profileKey: Uint8Array; uuid: string } {
const presentation = new ProfileKeyCredentialPresentation( const presentation = new ProfileKeyCredentialPresentation(
arrayBufferToCompatArray(presentationBuffer) uint8ArrayToCompatArray(presentationBuffer)
); );
const uuidCiphertext = presentation.getUuidCiphertext(); const uuidCiphertext = presentation.getUuidCiphertext();
@ -83,18 +76,18 @@ export function decryptProfileKeyCredentialPresentation(
); );
return { return {
profileKey: compatArrayToArrayBuffer(profileKey.serialize()), profileKey: compatArrayToUint8Array(profileKey.serialize()),
uuid, uuid,
}; };
} }
export function decryptProfileKey( export function decryptProfileKey(
clientZkGroupCipher: ClientZkGroupCipher, clientZkGroupCipher: ClientZkGroupCipher,
profileKeyCiphertextBuffer: ArrayBuffer, profileKeyCiphertextBuffer: Uint8Array,
uuid: string uuid: string
): ArrayBuffer { ): Uint8Array {
const profileKeyCiphertext = new ProfileKeyCiphertext( const profileKeyCiphertext = new ProfileKeyCiphertext(
arrayBufferToCompatArray(profileKeyCiphertextBuffer) uint8ArrayToCompatArray(profileKeyCiphertextBuffer)
); );
const profileKey = clientZkGroupCipher.decryptProfileKey( const profileKey = clientZkGroupCipher.decryptProfileKey(
@ -102,15 +95,15 @@ export function decryptProfileKey(
uuid uuid
); );
return compatArrayToArrayBuffer(profileKey.serialize()); return compatArrayToUint8Array(profileKey.serialize());
} }
export function decryptUuid( export function decryptUuid(
clientZkGroupCipher: ClientZkGroupCipher, clientZkGroupCipher: ClientZkGroupCipher,
uuidCiphertextBuffer: ArrayBuffer uuidCiphertextBuffer: Uint8Array
): string { ): string {
const uuidCiphertext = new UuidCiphertext( const uuidCiphertext = new UuidCiphertext(
arrayBufferToCompatArray(uuidCiphertextBuffer) uint8ArrayToCompatArray(uuidCiphertextBuffer)
); );
return clientZkGroupCipher.decryptUuid(uuidCiphertext); return clientZkGroupCipher.decryptUuid(uuidCiphertext);
@ -129,56 +122,54 @@ export function deriveProfileKeyVersion(
} }
export function deriveGroupPublicParams( export function deriveGroupPublicParams(
groupSecretParamsBuffer: ArrayBuffer groupSecretParamsBuffer: Uint8Array
): ArrayBuffer { ): Uint8Array {
const groupSecretParams = new GroupSecretParams( const groupSecretParams = new GroupSecretParams(
arrayBufferToCompatArray(groupSecretParamsBuffer) uint8ArrayToCompatArray(groupSecretParamsBuffer)
); );
return compatArrayToArrayBuffer( return compatArrayToUint8Array(
groupSecretParams.getPublicParams().serialize() groupSecretParams.getPublicParams().serialize()
); );
} }
export function deriveGroupID( export function deriveGroupID(groupSecretParamsBuffer: Uint8Array): Uint8Array {
groupSecretParamsBuffer: ArrayBuffer
): ArrayBuffer {
const groupSecretParams = new GroupSecretParams( const groupSecretParams = new GroupSecretParams(
arrayBufferToCompatArray(groupSecretParamsBuffer) uint8ArrayToCompatArray(groupSecretParamsBuffer)
); );
return compatArrayToArrayBuffer( return compatArrayToUint8Array(
groupSecretParams.getPublicParams().getGroupIdentifier().serialize() groupSecretParams.getPublicParams().getGroupIdentifier().serialize()
); );
} }
export function deriveGroupSecretParams( export function deriveGroupSecretParams(
masterKeyBuffer: ArrayBuffer masterKeyBuffer: Uint8Array
): ArrayBuffer { ): Uint8Array {
const masterKey = new GroupMasterKey( const masterKey = new GroupMasterKey(
arrayBufferToCompatArray(masterKeyBuffer) uint8ArrayToCompatArray(masterKeyBuffer)
); );
const groupSecretParams = GroupSecretParams.deriveFromMasterKey(masterKey); const groupSecretParams = GroupSecretParams.deriveFromMasterKey(masterKey);
return compatArrayToArrayBuffer(groupSecretParams.serialize()); return compatArrayToUint8Array(groupSecretParams.serialize());
} }
export function encryptGroupBlob( export function encryptGroupBlob(
clientZkGroupCipher: ClientZkGroupCipher, clientZkGroupCipher: ClientZkGroupCipher,
plaintext: ArrayBuffer plaintext: Uint8Array
): ArrayBuffer { ): Uint8Array {
return compatArrayToArrayBuffer( return compatArrayToUint8Array(
clientZkGroupCipher.encryptBlob(arrayBufferToCompatArray(plaintext)) clientZkGroupCipher.encryptBlob(uint8ArrayToCompatArray(plaintext))
); );
} }
export function encryptUuid( export function encryptUuid(
clientZkGroupCipher: ClientZkGroupCipher, clientZkGroupCipher: ClientZkGroupCipher,
uuidPlaintext: string uuidPlaintext: string
): ArrayBuffer { ): Uint8Array {
const uuidCiphertext = clientZkGroupCipher.encryptUuid(uuidPlaintext); const uuidCiphertext = clientZkGroupCipher.encryptUuid(uuidPlaintext);
return compatArrayToArrayBuffer(uuidCiphertext.serialize()); return compatArrayToUint8Array(uuidCiphertext.serialize());
} }
export function generateProfileKeyCredentialRequest( export function generateProfileKeyCredentialRequest(
@ -206,7 +197,7 @@ export function getAuthCredentialPresentation(
clientZkAuthOperations: ClientZkAuthOperations, clientZkAuthOperations: ClientZkAuthOperations,
authCredentialBase64: string, authCredentialBase64: string,
groupSecretParamsBase64: string groupSecretParamsBase64: string
): ArrayBuffer { ): Uint8Array {
const authCredential = new AuthCredential( const authCredential = new AuthCredential(
base64ToCompatArray(authCredentialBase64) base64ToCompatArray(authCredentialBase64)
); );
@ -218,14 +209,14 @@ export function getAuthCredentialPresentation(
secretParams, secretParams,
authCredential authCredential
); );
return compatArrayToArrayBuffer(presentation.serialize()); return compatArrayToUint8Array(presentation.serialize());
} }
export function createProfileKeyCredentialPresentation( export function createProfileKeyCredentialPresentation(
clientZkProfileCipher: ClientZkProfileOperations, clientZkProfileCipher: ClientZkProfileOperations,
profileKeyCredentialBase64: string, profileKeyCredentialBase64: string,
groupSecretParamsBase64: string groupSecretParamsBase64: string
): ArrayBuffer { ): Uint8Array {
const profileKeyCredentialArray = base64ToCompatArray( const profileKeyCredentialArray = base64ToCompatArray(
profileKeyCredentialBase64 profileKeyCredentialBase64
); );
@ -241,7 +232,7 @@ export function createProfileKeyCredentialPresentation(
profileKeyCredential profileKeyCredential
); );
return compatArrayToArrayBuffer(presentation.serialize()); return compatArrayToUint8Array(presentation.serialize());
} }
export function getClientZkAuthOperations( export function getClientZkAuthOperations(

View File

@ -23,6 +23,7 @@ import {
isGroupV1, isGroupV1,
isMe, isMe,
} from '../util/whatTypeOfConversation'; } from '../util/whatTypeOfConversation';
import * as Bytes from '../Bytes';
import { import {
canReply, canReply,
getAttachmentsForMessage, getAttachmentsForMessage,
@ -4157,13 +4158,11 @@ Whisper.ConversationView = Whisper.View.extend({
} = window.Signal.Groups.parseGroupLink(groupData); } = window.Signal.Groups.parseGroupLink(groupData);
const fields = window.Signal.Groups.deriveGroupFields( 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 logId = `groupv2(${id})`;
const secretParams = window.Signal.Crypto.arrayBufferToBase64( const secretParams = Bytes.toBase64(fields.secretParams);
fields.secretParams
);
window.log.info(`getGroupPreview/${logId}: Fetching pre-join state`); window.log.info(`getGroupPreview/${logId}: Fetching pre-join state`);
const result = await window.Signal.Groups.getPreJoinGroupInfo( 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 { AttachmentType } from './types/Attachment';
import { ElectronLocaleType } from './util/mapToSupportLocale'; import { ElectronLocaleType } from './util/mapToSupportLocale';
import { SignalProtocolStore } from './SignalProtocolStore'; import { SignalProtocolStore } from './SignalProtocolStore';
import { Context as SignalContext } from './context';
import { StartupQueue } from './util/StartupQueue'; import { StartupQueue } from './util/StartupQueue';
import * as synchronousCrypto from './util/synchronousCrypto'; import * as synchronousCrypto from './util/synchronousCrypto';
import { SocketStatus } from './types/SocketStatus'; import { SocketStatus } from './types/SocketStatus';
import SyncRequest from './textsecure/SyncRequest'; import SyncRequest from './textsecure/SyncRequest';
import { ConversationColorType, CustomColorType } from './types/Colors'; import { ConversationColorType, CustomColorType } from './types/Colors';
import { MessageController } from './util/MessageController'; import { MessageController } from './util/MessageController';
import { isValidGuid } from './util/isValidGuid';
import { StateType } from './state/reducer'; import { StateType } from './state/reducer';
export { Long } from 'long'; export { Long } from 'long';
@ -211,7 +213,7 @@ declare global {
isAfterVersion: (version: string, anotherVersion: string) => boolean; isAfterVersion: (version: string, anotherVersion: string) => boolean;
isBeforeVersion: (version: string, anotherVersion: string) => boolean; isBeforeVersion: (version: string, anotherVersion: string) => boolean;
isFullScreen: () => boolean; isFullScreen: () => boolean;
isValidGuid: (maybeGuid: string | null) => boolean; isValidGuid: typeof isValidGuid;
isValidE164: (maybeE164: unknown) => boolean; isValidE164: (maybeE164: unknown) => boolean;
libphonenumber: { libphonenumber: {
util: { util: {
@ -524,6 +526,7 @@ declare global {
}; };
challengeHandler: ChallengeHandler; challengeHandler: ChallengeHandler;
}; };
SignalContext: SignalContext;
ConversationController: ConversationController; ConversationController: ConversationController;
Events: WhatIsThis; Events: WhatIsThis;