Move to protobufjs in ts/groups.ts
This commit is contained in:
parent
972a4cba0c
commit
9f0c630574
30 changed files with 1424 additions and 964 deletions
|
@ -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');
|
||||
|
|
|
@ -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;
|
||||
|
|
10
preload.js
10
preload.js
|
@ -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);
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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
52
ts/Bytes.ts
Normal 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
57
ts/context/Bytes.ts
Normal 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
8
ts/context/index.ts
Normal 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();
|
||||
}
|
1720
ts/groups.ts
1720
ts/groups.ts
File diff suppressed because it is too large
Load diff
|
@ -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({
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
90
ts/test-both/util/Bytes_test.ts
Normal file
90
ts/test-both/util/Bytes_test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
19
ts/test-both/util/dropNull_test.ts
Normal file
19
ts/test-both/util/dropNull_test.ts
Normal 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');
|
||||
});
|
||||
});
|
33
ts/test-both/util/isValidGuid_test.ts
Normal file
33
ts/test-both/util/isValidGuid_test.ts
Normal 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()));
|
||||
});
|
||||
});
|
15
ts/test-both/util/normalizeUuid_test.ts
Normal file
15
ts/test-both/util/normalizeUuid_test.ts
Normal 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);
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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
11
ts/util/dropNull.ts
Normal 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
8
ts/util/isValidGuid.ts
Normal 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
14
ts/util/normalizeUuid.ts
Normal 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();
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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
5
ts/window.d.ts
vendored
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue