diff --git a/debug_log_preload.js b/debug_log_preload.js index c63f7a552..f4725a495 100644 --- a/debug_log_preload.js +++ b/debug_log_preload.js @@ -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'); diff --git a/permissions_popup_preload.js b/permissions_popup_preload.js index 317bc2fa1..5d5f56e4e 100644 --- a/permissions_popup_preload.js +++ b/permissions_popup_preload.js @@ -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; diff --git a/preload.js b/preload.js index 37d3ed5b5..3e639914a 100644 --- a/preload.js +++ b/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); diff --git a/screenShare_preload.js b/screenShare_preload.js index 001f86cc6..11510d9b6 100644 --- a/screenShare_preload.js +++ b/screenShare_preload.js @@ -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'); diff --git a/settings_preload.js b/settings_preload.js index 82bd87920..021584e5a 100644 --- a/settings_preload.js +++ b/settings_preload.js @@ -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); diff --git a/sticker-creator/preload.js b/sticker-creator/preload.js index e84c26a33..c5c75bb7d 100644 --- a/sticker-creator/preload.js +++ b/sticker-creator/preload.js @@ -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'); diff --git a/test/setup-test-node.js b/test/setup-test-node.js index 21384c4fe..2cb0d27ab 100644 --- a/test/setup-test-node.js +++ b/test/setup-test-node.js @@ -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() diff --git a/ts/Bytes.ts b/ts/Bytes.ts new file mode 100644 index 000000000..bee2032fd --- /dev/null +++ b/ts/Bytes.ts @@ -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 { + 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); +} diff --git a/ts/context/Bytes.ts b/ts/context/Bytes.ts new file mode 100644 index 000000000..bc0d2ce04 --- /dev/null +++ b/ts/context/Bytes.ts @@ -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 { + 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); + } +} diff --git a/ts/context/index.ts b/ts/context/index.ts new file mode 100644 index 000000000..9409e09cf --- /dev/null +++ b/ts/context/index.ts @@ -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(); +} diff --git a/ts/groups.ts b/ts/groups.ts index 3f15a98ed..c67eabba4 100644 --- a/ts/groups.ts +++ b/ts/groups.ts @@ -21,9 +21,10 @@ import { import { isStorageWriteFeatureEnabled } from './storage/isFeatureEnabled'; import dataInterface from './sql/Client'; import { toWebSafeBase64, fromWebSafeBase64 } from './util/webSafeBase64'; -import { assert } from './util/assert'; +import { assert, strictAssert } from './util/assert'; import { isMoreRecentThan } from './util/timestamp'; -import { isByteBufferEmpty } from './util/isByteBufferEmpty'; +import { normalizeUuid } from './util/normalizeUuid'; +import { dropNull } from './util/dropNull'; import { ConversationAttributesType, GroupV2MemberType, @@ -49,27 +50,12 @@ import { } from './util/zkgroup'; import * as universalExpireTimer from './util/universalExpireTimer'; import { - arrayBufferToBase64, - arrayBufferToHex, - base64ToArrayBuffer, computeHash, deriveMasterKeyFromGroupV1, fromEncodedBinaryToArrayBuffer, getRandomBytes, + typedArrayToArrayBuffer, } from './Crypto'; -import { - AccessRequiredEnum, - GroupAttributeBlobClass, - GroupChangeClass, - GroupChangesClass, - GroupClass, - GroupJoinInfoClass, - MemberClass, - MemberPendingAdminApprovalClass, - MemberPendingProfileKeyClass, - ProtoBigNumberType, - ProtoBinaryType, -} from './textsecure.d'; import { GroupCredentialsType, GroupLogResponseType, @@ -86,6 +72,9 @@ import { } from './util/whatTypeOfConversation'; import { handleMessageSend } from './util/handleMessageSend'; import { getSendOptions } from './util/getSendOptions'; +import * as Bytes from './Bytes'; +import { SignalService as Proto } from './protobuf'; +import AccessRequiredEnum = Proto.AccessControl.AccessRequired; export { joinViaLink } from './groups/joinViaLink'; @@ -222,11 +211,14 @@ export type GroupV2ChangeType = { }; export type GroupFields = { - readonly id: ArrayBuffer; - readonly secretParams: ArrayBuffer; - readonly publicParams: ArrayBuffer; + readonly id: Uint8Array; + readonly secretParams: Uint8Array; + readonly publicParams: Uint8Array; }; +// TODO: remove once we move away from ArrayBuffers +const FIXMEU8 = Uint8Array; + const MAX_CACHED_GROUP_FIELDS = 100; const groupFieldsCache = new LRU({ @@ -286,15 +278,15 @@ export function generateGroupInviteLinkPassword(): ArrayBuffer { export async function getPreJoinGroupInfo( inviteLinkPasswordBase64: string, masterKeyBase64: string -): Promise { +): Promise { const data = window.Signal.Groups.deriveGroupFields( - base64ToArrayBuffer(masterKeyBase64) + Bytes.fromBase64(masterKeyBase64) ); return makeRequestWithTemporalRetry({ logId: `groupv2(${data.id})`, - publicParams: arrayBufferToBase64(data.publicParams), - secretParams: arrayBufferToBase64(data.secretParams), + publicParams: Bytes.toBase64(data.publicParams), + secretParams: Bytes.toBase64(data.secretParams), request: (sender, options) => sender.getGroupFromLink(inviteLinkPasswordBase64, options), }); @@ -303,17 +295,14 @@ export async function getPreJoinGroupInfo( export function buildGroupLink(conversation: ConversationModel): string { const { masterKey, groupInviteLinkPassword } = conversation.attributes; - const subProto = new window.textsecure.protobuf.GroupInviteLink.GroupInviteLinkContentsV1(); - subProto.groupMasterKey = window.Signal.Crypto.base64ToArrayBuffer(masterKey); - subProto.inviteLinkPassword = window.Signal.Crypto.base64ToArrayBuffer( - groupInviteLinkPassword - ); + const bytes = Proto.GroupInviteLink.encode({ + v1Contents: { + groupMasterKey: Bytes.fromBase64(masterKey), + inviteLinkPassword: Bytes.fromBase64(groupInviteLinkPassword), + }, + }).finish(); - const proto = new window.textsecure.protobuf.GroupInviteLink(); - proto.v1Contents = subProto; - - const bytes = proto.toArrayBuffer(); - const hash = toWebSafeBase64(window.Signal.Crypto.arrayBufferToBase64(bytes)); + const hash = toWebSafeBase64(Bytes.toBase64(bytes)); return `https://signal.group/#${hash}`; } @@ -322,11 +311,9 @@ export function parseGroupLink( hash: string ): { masterKey: string; inviteLinkPassword: string } { const base64 = fromWebSafeBase64(hash); - const buffer = base64ToArrayBuffer(base64); + const buffer = Bytes.fromBase64(base64); - const inviteLinkProto = window.textsecure.protobuf.GroupInviteLink.decode( - buffer - ); + const inviteLinkProto = Proto.GroupInviteLink.decode(buffer); if ( inviteLinkProto.contents !== 'v1Contents' || !inviteLinkProto.v1Contents @@ -338,22 +325,23 @@ export function parseGroupLink( throw error; } - if (isByteBufferEmpty(inviteLinkProto.v1Contents.groupMasterKey)) { + const { + groupMasterKey: groupMasterKeyRaw, + inviteLinkPassword: inviteLinkPasswordRaw, + } = inviteLinkProto.v1Contents; + + if (!groupMasterKeyRaw || !groupMasterKeyRaw.length) { throw new Error('v1Contents.groupMasterKey had no data!'); } - if (isByteBufferEmpty(inviteLinkProto.v1Contents.inviteLinkPassword)) { + if (!inviteLinkPasswordRaw || !inviteLinkPasswordRaw.length) { throw new Error('v1Contents.inviteLinkPassword had no data!'); } - const masterKey: string = inviteLinkProto.v1Contents.groupMasterKey.toString( - 'base64' - ); + const masterKey = Bytes.toBase64(groupMasterKeyRaw); if (masterKey.length !== 44) { throw new Error(`masterKey had unexpected length ${masterKey.length}`); } - const inviteLinkPassword: string = inviteLinkProto.v1Contents.inviteLinkPassword.toString( - 'base64' - ); + const inviteLinkPassword = Bytes.toBase64(inviteLinkPasswordRaw); if (inviteLinkPassword.length === 0) { throw new Error( `inviteLinkPassword had unexpected length ${inviteLinkPassword.length}` @@ -386,9 +374,9 @@ async function uploadAvatar( const hash = await computeHash(data); - const blob = new window.textsecure.protobuf.GroupAttributeBlob(); - blob.avatar = data; - const blobPlaintext = blob.toArrayBuffer(); + const blobPlaintext = Proto.GroupAttributeBlob.encode({ + avatar: new FIXMEU8(data), + }).finish(); const ciphertext = encryptGroupBlob(clientZkGroupCipher, blobPlaintext); const key = await makeRequestWithTemporalRetry({ @@ -416,10 +404,10 @@ async function uploadAvatar( function buildGroupTitleBuffer( clientZkGroupCipher: ClientZkGroupCipher, title: string -): ArrayBuffer { - const titleBlob = new window.textsecure.protobuf.GroupAttributeBlob(); - titleBlob.title = title; - const titleBlobPlaintext = titleBlob.toArrayBuffer(); +): Uint8Array { + const titleBlobPlaintext = Proto.GroupAttributeBlob.encode({ + title, + }).finish(); const result = encryptGroupBlob(clientZkGroupCipher, titleBlobPlaintext); @@ -433,10 +421,10 @@ function buildGroupTitleBuffer( function buildGroupDescriptionBuffer( clientZkGroupCipher: ClientZkGroupCipher, description: string -): ArrayBuffer { - const attrsBlob = new window.textsecure.protobuf.GroupAttributeBlob(); - attrsBlob.descriptionText = description; - const attrsBlobPlaintext = attrsBlob.toArrayBuffer(); +): Uint8Array { + const attrsBlobPlaintext = Proto.GroupAttributeBlob.encode({ + descriptionText: description, + }).finish(); const result = encryptGroupBlob(clientZkGroupCipher, attrsBlobPlaintext); @@ -464,9 +452,9 @@ function buildGroupProto( > & { avatarUrl?: string; } -): GroupClass { - const MEMBER_ROLE_ENUM = window.textsecure.protobuf.Member.Role; - const ACCESS_ENUM = window.textsecure.protobuf.AccessControl.AccessRequired; +): Proto.Group { + const MEMBER_ROLE_ENUM = Proto.Member.Role; + const ACCESS_ENUM = Proto.AccessControl.AccessRequired; const logId = `groupv2(${attributes.id})`; const { publicParams, secretParams } = attributes; @@ -487,9 +475,9 @@ function buildGroupProto( const clientZkProfileCipher = getClientZkProfileOperations( serverPublicParamsBase64 ); - const proto = new window.textsecure.protobuf.Group(); + const proto = new Proto.Group(); - proto.publicKey = base64ToArrayBuffer(publicParams); + proto.publicKey = Bytes.fromBase64(publicParams); proto.version = attributes.revision || 0; if (attributes.name) { @@ -501,16 +489,16 @@ function buildGroupProto( } if (attributes.expireTimer) { - const timerBlob = new window.textsecure.protobuf.GroupAttributeBlob(); - timerBlob.disappearingMessagesDuration = attributes.expireTimer; - const timerBlobPlaintext = timerBlob.toArrayBuffer(); + const timerBlobPlaintext = Proto.GroupAttributeBlob.encode({ + disappearingMessagesDuration: attributes.expireTimer, + }).finish(); proto.disappearingMessagesTimer = encryptGroupBlob( clientZkGroupCipher, timerBlobPlaintext ); } - const accessControl = new window.textsecure.protobuf.AccessControl(); + const accessControl = new Proto.AccessControl(); if (attributes.accessControl) { accessControl.attributes = attributes.accessControl.attributes || ACCESS_ENUM.MEMBER; @@ -523,7 +511,7 @@ function buildGroupProto( proto.accessControl = accessControl; proto.members = (attributes.membersV2 || []).map(item => { - const member = new window.textsecure.protobuf.Member(); + const member = new Proto.Member(); const conversation = window.ConversationController.get(item.conversationId); if (!conversation) { @@ -571,8 +559,8 @@ function buildGroupProto( proto.membersPendingProfileKey = (attributes.pendingMembersV2 || []).map( item => { - const pendingMember = new window.textsecure.protobuf.MemberPendingProfileKey(); - const member = new window.textsecure.protobuf.Member(); + const pendingMember = new Proto.MemberPendingProfileKey(); + const member = new Proto.Member(); const conversation = window.ConversationController.get( item.conversationId @@ -607,8 +595,8 @@ export async function buildAddMembersChange( 'id' | 'publicParams' | 'revision' | 'secretParams' >, conversationIds: ReadonlyArray -): Promise { - const MEMBER_ROLE_ENUM = window.textsecure.protobuf.Member.Role; +): Promise { + const MEMBER_ROLE_ENUM = Proto.Member.Role; const { id, publicParams, revision, secretParams } = conversation; @@ -644,8 +632,8 @@ export async function buildAddMembersChange( const now = Date.now(); - const addMembers: Array = []; - const addPendingMembers: Array = []; + const addMembers: Array = []; + const addPendingMembers: Array = []; await Promise.all( conversationIds.map(async conversationId => { @@ -692,7 +680,7 @@ export async function buildAddMembersChange( return; } - const member = new window.textsecure.protobuf.Member(); + const member = new Proto.Member(); member.userId = encryptUuid(clientZkGroupCipher, uuid); member.role = MEMBER_ROLE_ENUM.DEFAULT; member.joinedAtVersion = newGroupVersion; @@ -707,18 +695,18 @@ export async function buildAddMembersChange( secretParams ); - const addMemberAction = new window.textsecure.protobuf.GroupChange.Actions.AddMemberAction(); + const addMemberAction = new Proto.GroupChange.Actions.AddMemberAction(); addMemberAction.added = member; addMemberAction.joinFromInviteLink = false; addMembers.push(addMemberAction); } else { - const memberPendingProfileKey = new window.textsecure.protobuf.MemberPendingProfileKey(); + const memberPendingProfileKey = new Proto.MemberPendingProfileKey(); memberPendingProfileKey.member = member; memberPendingProfileKey.addedByUserId = ourUuidCipherTextBuffer; memberPendingProfileKey.timestamp = now; - const addPendingMemberAction = new window.textsecure.protobuf.GroupChange.Actions.AddMemberPendingProfileKeyAction(); + const addPendingMemberAction = new Proto.GroupChange.Actions.AddMemberPendingProfileKeyAction(); addPendingMemberAction.added = memberPendingProfileKey; addPendingMembers.push(addPendingMemberAction); @@ -726,7 +714,7 @@ export async function buildAddMembersChange( }) ); - const actions = new window.textsecure.protobuf.GroupChange.Actions(); + const actions = new Proto.GroupChange.Actions(); if (!addMembers.length && !addPendingMembers.length) { // This shouldn't happen. When these actions are passed to `modifyGroupV2`, a warning // will be logged. @@ -753,7 +741,7 @@ export async function buildUpdateAttributesChange( description?: string; title?: string; }> -): Promise { +): Promise { const { publicParams, secretParams, revision, id } = conversation; const logId = `groupv2(${id})`; @@ -769,7 +757,7 @@ export async function buildUpdateAttributesChange( ); } - const actions = new window.textsecure.protobuf.GroupChange.Actions(); + const actions = new Proto.GroupChange.Actions(); let hasChangedSomething = false; @@ -783,7 +771,7 @@ export async function buildUpdateAttributesChange( if ('avatar' in attributes) { hasChangedSomething = true; - actions.modifyAvatar = new window.textsecure.protobuf.GroupChange.Actions.ModifyAvatarAction(); + actions.modifyAvatar = new Proto.GroupChange.Actions.ModifyAvatarAction(); const { avatar } = attributes; if (avatar) { const uploadedAvatar = await uploadAvatar({ @@ -802,7 +790,7 @@ export async function buildUpdateAttributesChange( if (title) { hasChangedSomething = true; - actions.modifyTitle = new window.textsecure.protobuf.GroupChange.Actions.ModifyTitleAction(); + actions.modifyTitle = new Proto.GroupChange.Actions.ModifyTitleAction(); actions.modifyTitle.title = buildGroupTitleBuffer( clientZkGroupCipher, title @@ -813,7 +801,7 @@ export async function buildUpdateAttributesChange( if (typeof description === 'string') { hasChangedSomething = true; - actions.modifyDescription = new window.textsecure.protobuf.GroupChange.Actions.ModifyDescriptionAction(); + actions.modifyDescription = new Proto.GroupChange.Actions.ModifyDescriptionAction(); actions.modifyDescription.descriptionBytes = buildGroupDescriptionBuffer( clientZkGroupCipher, description @@ -835,12 +823,12 @@ export function buildDisappearingMessagesTimerChange({ expireTimer, group, }: { - expireTimer?: number; + expireTimer: number; group: ConversationAttributesType; -}): GroupChangeClass.Actions { - const actions = new window.textsecure.protobuf.GroupChange.Actions(); +}): Proto.GroupChange.Actions { + const actions = new Proto.GroupChange.Actions(); - const blob = new window.textsecure.protobuf.GroupAttributeBlob(); + const blob = new Proto.GroupAttributeBlob(); blob.disappearingMessagesDuration = expireTimer; if (!group.secretParams) { @@ -850,10 +838,10 @@ export function buildDisappearingMessagesTimerChange({ } const clientZkGroupCipher = getClientZkGroupCipher(group.secretParams); - const blobPlaintext = blob.toArrayBuffer(); + const blobPlaintext = Proto.GroupAttributeBlob.encode(blob).finish(); const blobCipherText = encryptGroupBlob(clientZkGroupCipher, blobPlaintext); - const timerAction = new window.textsecure.protobuf.GroupChange.Actions.ModifyDisappearingMessagesTimerAction(); + const timerAction = new Proto.GroupChange.Actions.ModifyDisappearingMessagesTimerAction(); timerAction.timer = blobCipherText; actions.version = (group.revision || 0) + 1; @@ -865,13 +853,13 @@ export function buildDisappearingMessagesTimerChange({ export function buildInviteLinkPasswordChange( group: ConversationAttributesType, inviteLinkPassword: string -): GroupChangeClass.Actions { - const inviteLinkPasswordAction = new window.textsecure.protobuf.GroupChange.Actions.ModifyInviteLinkPasswordAction(); - inviteLinkPasswordAction.inviteLinkPassword = base64ToArrayBuffer( +): Proto.GroupChange.Actions { + const inviteLinkPasswordAction = new Proto.GroupChange.Actions.ModifyInviteLinkPasswordAction(); + inviteLinkPasswordAction.inviteLinkPassword = Bytes.fromBase64( inviteLinkPassword ); - const actions = new window.textsecure.protobuf.GroupChange.Actions(); + const actions = new Proto.GroupChange.Actions(); actions.version = (group.revision || 0) + 1; actions.modifyInviteLinkPassword = inviteLinkPasswordAction; @@ -882,16 +870,16 @@ export function buildNewGroupLinkChange( group: ConversationAttributesType, inviteLinkPassword: string, addFromInviteLinkAccess: AccessRequiredEnum -): GroupChangeClass.Actions { - const accessControlAction = new window.textsecure.protobuf.GroupChange.Actions.ModifyAddFromInviteLinkAccessControlAction(); +): Proto.GroupChange.Actions { + const accessControlAction = new Proto.GroupChange.Actions.ModifyAddFromInviteLinkAccessControlAction(); accessControlAction.addFromInviteLinkAccess = addFromInviteLinkAccess; - const inviteLinkPasswordAction = new window.textsecure.protobuf.GroupChange.Actions.ModifyInviteLinkPasswordAction(); - inviteLinkPasswordAction.inviteLinkPassword = base64ToArrayBuffer( + const inviteLinkPasswordAction = new Proto.GroupChange.Actions.ModifyInviteLinkPasswordAction(); + inviteLinkPasswordAction.inviteLinkPassword = Bytes.fromBase64( inviteLinkPassword ); - const actions = new window.textsecure.protobuf.GroupChange.Actions(); + const actions = new Proto.GroupChange.Actions(); actions.version = (group.revision || 0) + 1; actions.modifyAddFromInviteLinkAccess = accessControlAction; actions.modifyInviteLinkPassword = inviteLinkPasswordAction; @@ -902,11 +890,11 @@ export function buildNewGroupLinkChange( export function buildAccessControlAddFromInviteLinkChange( group: ConversationAttributesType, value: AccessRequiredEnum -): GroupChangeClass.Actions { - const accessControlAction = new window.textsecure.protobuf.GroupChange.Actions.ModifyAddFromInviteLinkAccessControlAction(); +): Proto.GroupChange.Actions { + const accessControlAction = new Proto.GroupChange.Actions.ModifyAddFromInviteLinkAccessControlAction(); accessControlAction.addFromInviteLinkAccess = value; - const actions = new window.textsecure.protobuf.GroupChange.Actions(); + const actions = new Proto.GroupChange.Actions(); actions.version = (group.revision || 0) + 1; actions.modifyAddFromInviteLinkAccess = accessControlAction; @@ -916,11 +904,11 @@ export function buildAccessControlAddFromInviteLinkChange( export function buildAccessControlAttributesChange( group: ConversationAttributesType, value: AccessRequiredEnum -): GroupChangeClass.Actions { - const accessControlAction = new window.textsecure.protobuf.GroupChange.Actions.ModifyAttributesAccessControlAction(); +): Proto.GroupChange.Actions { + const accessControlAction = new Proto.GroupChange.Actions.ModifyAttributesAccessControlAction(); accessControlAction.attributesAccess = value; - const actions = new window.textsecure.protobuf.GroupChange.Actions(); + const actions = new Proto.GroupChange.Actions(); actions.version = (group.revision || 0) + 1; actions.modifyAttributesAccess = accessControlAction; @@ -930,11 +918,11 @@ export function buildAccessControlAttributesChange( export function buildAccessControlMembersChange( group: ConversationAttributesType, value: AccessRequiredEnum -): GroupChangeClass.Actions { - const accessControlAction = new window.textsecure.protobuf.GroupChange.Actions.ModifyMembersAccessControlAction(); +): Proto.GroupChange.Actions { + const accessControlAction = new Proto.GroupChange.Actions.ModifyMembersAccessControlAction(); accessControlAction.membersAccess = value; - const actions = new window.textsecure.protobuf.GroupChange.Actions(); + const actions = new Proto.GroupChange.Actions(); actions.version = (group.revision || 0) + 1; actions.modifyMemberAccess = accessControlAction; @@ -948,8 +936,8 @@ export function buildDeletePendingAdminApprovalMemberChange({ }: { group: ConversationAttributesType; uuid: string; -}): GroupChangeClass.Actions { - const actions = new window.textsecure.protobuf.GroupChange.Actions(); +}): Proto.GroupChange.Actions { + const actions = new Proto.GroupChange.Actions(); if (!group.secretParams) { throw new Error( @@ -959,7 +947,7 @@ export function buildDeletePendingAdminApprovalMemberChange({ const clientZkGroupCipher = getClientZkGroupCipher(group.secretParams); const uuidCipherTextBuffer = encryptUuid(clientZkGroupCipher, uuid); - const deleteMemberPendingAdminApproval = new window.textsecure.protobuf.GroupChange.Actions.DeleteMemberPendingAdminApprovalAction(); + const deleteMemberPendingAdminApproval = new Proto.GroupChange.Actions.DeleteMemberPendingAdminApprovalAction(); deleteMemberPendingAdminApproval.deletedUserId = uuidCipherTextBuffer; actions.version = (group.revision || 0) + 1; @@ -978,8 +966,8 @@ export function buildAddPendingAdminApprovalMemberChange({ group: ConversationAttributesType; profileKeyCredentialBase64: string; serverPublicParamsBase64: string; -}): GroupChangeClass.Actions { - const actions = new window.textsecure.protobuf.GroupChange.Actions(); +}): Proto.GroupChange.Actions { + const actions = new Proto.GroupChange.Actions(); if (!group.secretParams) { throw new Error( @@ -990,14 +978,14 @@ export function buildAddPendingAdminApprovalMemberChange({ serverPublicParamsBase64 ); - const addMemberPendingAdminApproval = new window.textsecure.protobuf.GroupChange.Actions.AddMemberPendingAdminApprovalAction(); + const addMemberPendingAdminApproval = new Proto.GroupChange.Actions.AddMemberPendingAdminApprovalAction(); const presentation = createProfileKeyCredentialPresentation( clientZkProfileCipher, profileKeyCredentialBase64, group.secretParams ); - const added = new window.textsecure.protobuf.MemberPendingAdminApproval(); + const added = new Proto.MemberPendingAdminApproval(); added.presentation = presentation; addMemberPendingAdminApproval.added = added; @@ -1017,10 +1005,10 @@ export function buildAddMember({ profileKeyCredentialBase64: string; serverPublicParamsBase64: string; joinFromInviteLink?: boolean; -}): GroupChangeClass.Actions { - const MEMBER_ROLE_ENUM = window.textsecure.protobuf.Member.Role; +}): Proto.GroupChange.Actions { + const MEMBER_ROLE_ENUM = Proto.Member.Role; - const actions = new window.textsecure.protobuf.GroupChange.Actions(); + const actions = new Proto.GroupChange.Actions(); if (!group.secretParams) { throw new Error('buildAddMember: group was missing secretParams!'); @@ -1029,14 +1017,14 @@ export function buildAddMember({ serverPublicParamsBase64 ); - const addMember = new window.textsecure.protobuf.GroupChange.Actions.AddMemberAction(); + const addMember = new Proto.GroupChange.Actions.AddMemberAction(); const presentation = createProfileKeyCredentialPresentation( clientZkProfileCipher, profileKeyCredentialBase64, group.secretParams ); - const added = new window.textsecure.protobuf.Member(); + const added = new Proto.Member(); added.presentation = presentation; added.role = MEMBER_ROLE_ENUM.DEFAULT; @@ -1054,8 +1042,8 @@ export function buildDeletePendingMemberChange({ }: { uuids: Array; group: ConversationAttributesType; -}): GroupChangeClass.Actions { - const actions = new window.textsecure.protobuf.GroupChange.Actions(); +}): Proto.GroupChange.Actions { + const actions = new Proto.GroupChange.Actions(); if (!group.secretParams) { throw new Error( @@ -1066,7 +1054,7 @@ export function buildDeletePendingMemberChange({ const deletePendingMembers = uuids.map(uuid => { const uuidCipherTextBuffer = encryptUuid(clientZkGroupCipher, uuid); - const deletePendingMember = new window.textsecure.protobuf.GroupChange.Actions.DeleteMemberPendingProfileKeyAction(); + const deletePendingMember = new Proto.GroupChange.Actions.DeleteMemberPendingProfileKeyAction(); deletePendingMember.deletedUserId = uuidCipherTextBuffer; return deletePendingMember; }); @@ -1083,8 +1071,8 @@ export function buildDeleteMemberChange({ }: { uuid: string; group: ConversationAttributesType; -}): GroupChangeClass.Actions { - const actions = new window.textsecure.protobuf.GroupChange.Actions(); +}): Proto.GroupChange.Actions { + const actions = new Proto.GroupChange.Actions(); if (!group.secretParams) { throw new Error('buildDeleteMemberChange: group was missing secretParams!'); @@ -1092,7 +1080,7 @@ export function buildDeleteMemberChange({ const clientZkGroupCipher = getClientZkGroupCipher(group.secretParams); const uuidCipherTextBuffer = encryptUuid(clientZkGroupCipher, uuid); - const deleteMember = new window.textsecure.protobuf.GroupChange.Actions.DeleteMemberAction(); + const deleteMember = new Proto.GroupChange.Actions.DeleteMemberAction(); deleteMember.deletedUserId = uuidCipherTextBuffer; actions.version = (group.revision || 0) + 1; @@ -1109,8 +1097,8 @@ export function buildModifyMemberRoleChange({ uuid: string; group: ConversationAttributesType; role: number; -}): GroupChangeClass.Actions { - const actions = new window.textsecure.protobuf.GroupChange.Actions(); +}): Proto.GroupChange.Actions { + const actions = new Proto.GroupChange.Actions(); if (!group.secretParams) { throw new Error('buildMakeAdminChange: group was missing secretParams!'); @@ -1119,7 +1107,7 @@ export function buildModifyMemberRoleChange({ const clientZkGroupCipher = getClientZkGroupCipher(group.secretParams); const uuidCipherTextBuffer = encryptUuid(clientZkGroupCipher, uuid); - const toggleAdmin = new window.textsecure.protobuf.GroupChange.Actions.ModifyMemberRoleAction(); + const toggleAdmin = new Proto.GroupChange.Actions.ModifyMemberRoleAction(); toggleAdmin.userId = uuidCipherTextBuffer; toggleAdmin.role = role; @@ -1135,9 +1123,9 @@ export function buildPromotePendingAdminApprovalMemberChange({ }: { group: ConversationAttributesType; uuid: string; -}): GroupChangeClass.Actions { - const MEMBER_ROLE_ENUM = window.textsecure.protobuf.Member.Role; - const actions = new window.textsecure.protobuf.GroupChange.Actions(); +}): Proto.GroupChange.Actions { + const MEMBER_ROLE_ENUM = Proto.Member.Role; + const actions = new Proto.GroupChange.Actions(); if (!group.secretParams) { throw new Error( @@ -1148,7 +1136,7 @@ export function buildPromotePendingAdminApprovalMemberChange({ const clientZkGroupCipher = getClientZkGroupCipher(group.secretParams); const uuidCipherTextBuffer = encryptUuid(clientZkGroupCipher, uuid); - const promotePendingMember = new window.textsecure.protobuf.GroupChange.Actions.PromoteMemberPendingAdminApprovalAction(); + const promotePendingMember = new Proto.GroupChange.Actions.PromoteMemberPendingAdminApprovalAction(); promotePendingMember.userId = uuidCipherTextBuffer; promotePendingMember.role = MEMBER_ROLE_ENUM.DEFAULT; @@ -1166,8 +1154,8 @@ export function buildPromoteMemberChange({ group: ConversationAttributesType; profileKeyCredentialBase64: string; serverPublicParamsBase64: string; -}): GroupChangeClass.Actions { - const actions = new window.textsecure.protobuf.GroupChange.Actions(); +}): Proto.GroupChange.Actions { + const actions = new Proto.GroupChange.Actions(); if (!group.secretParams) { throw new Error( @@ -1184,7 +1172,7 @@ export function buildPromoteMemberChange({ group.secretParams ); - const promotePendingMember = new window.textsecure.protobuf.GroupChange.Actions.PromoteMemberPendingProfileKeyAction(); + const promotePendingMember = new Proto.GroupChange.Actions.PromoteMemberPendingProfileKeyAction(); promotePendingMember.presentation = presentation; actions.version = (group.revision || 0) + 1; @@ -1198,10 +1186,10 @@ export async function uploadGroupChange({ group, inviteLinkPassword, }: { - actions: GroupChangeClass.Actions; + actions: Proto.GroupChange.IActions; group: ConversationAttributesType; inviteLinkPassword?: string; -}): Promise { +}): Promise { const logId = idForLogging(group.groupId); // Ensure we have the credentials we need before attempting GroupsV2 operations @@ -1231,7 +1219,7 @@ export async function modifyGroupV2({ name, }: { conversation: ConversationModel; - createGroupChange: () => Promise; + createGroupChange: () => Promise; extraConversationsForSend?: Array; inviteLinkPassword?: string; name: string; @@ -1288,8 +1276,10 @@ export async function modifyGroupV2({ group: conversation.attributes, }); - const groupChangeBuffer = groupChange.toArrayBuffer(); - const groupChangeBase64 = arrayBufferToBase64(groupChangeBuffer); + const groupChangeBuffer = Proto.GroupChange.encode( + groupChange + ).finish(); + const groupChangeBase64 = Bytes.toBase64(groupChangeBuffer); // Apply change locally, just like we would with an incoming change. This will // change conversation state and add change notifications to the timeline. @@ -1306,15 +1296,13 @@ export async function modifyGroupV2({ const sendOptions = await getSendOptions(conversation.attributes); const timestamp = Date.now(); - const { - ContentHint, - } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; + const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; const promise = handleMessageSend( window.Signal.Util.sendToGroup( { groupV2: conversation.getGroupV2Info({ - groupChange: groupChangeBuffer, + groupChange: typedArrayToArrayBuffer(groupChangeBuffer), includePendingMembers: true, extraConversationsForSend, }), @@ -1382,8 +1370,8 @@ export function idForLogging(groupId: string | undefined): string { return `groupv2(${groupId})`; } -export function deriveGroupFields(masterKey: ArrayBuffer): GroupFields { - const cacheKey = arrayBufferToBase64(masterKey); +export function deriveGroupFields(masterKey: Uint8Array): GroupFields { + const cacheKey = Bytes.toBase64(masterKey); const cached = groupFieldsCache.get(cacheKey); if (cached) { return cached; @@ -1504,18 +1492,18 @@ export async function createGroupV2({ ); } - const ACCESS_ENUM = window.textsecure.protobuf.AccessControl.AccessRequired; - const MEMBER_ROLE_ENUM = window.textsecure.protobuf.Member.Role; + const ACCESS_ENUM = Proto.AccessControl.AccessRequired; + const MEMBER_ROLE_ENUM = Proto.Member.Role; - const masterKeyBuffer = getRandomBytes(32); + const masterKeyBuffer = new FIXMEU8(getRandomBytes(32)); const fields = deriveGroupFields(masterKeyBuffer); - const groupId = arrayBufferToBase64(fields.id); + const groupId = Bytes.toBase64(fields.id); const logId = `groupv2(${groupId})`; - const masterKey = arrayBufferToBase64(masterKeyBuffer); - const secretParams = arrayBufferToBase64(fields.secretParams); - const publicParams = arrayBufferToBase64(fields.publicParams); + const masterKey = Bytes.toBase64(masterKeyBuffer); + const secretParams = Bytes.toBase64(fields.secretParams); + const publicParams = Bytes.toBase64(fields.publicParams); const ourConversationId = window.ConversationController.getOurConversationIdOrThrow(); const ourConversation = window.ConversationController.get(ourConversationId); @@ -1680,9 +1668,7 @@ export async function createGroupV2({ const groupV2Info = conversation.getGroupV2Info({ includePendingMembers: true, }); - const { - ContentHint, - } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; + const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; const sendOptions = await getSendOptions(conversation.attributes); await wrapWithSyncMessageSend({ @@ -1754,14 +1740,16 @@ export async function hasV1GroupBeenMigrated( } const idBuffer = fromEncodedBinaryToArrayBuffer(groupId); - const masterKeyBuffer = await deriveMasterKeyFromGroupV1(idBuffer); + const masterKeyBuffer = new FIXMEU8( + await deriveMasterKeyFromGroupV1(idBuffer) + ); const fields = deriveGroupFields(masterKeyBuffer); try { await makeRequestWithTemporalRetry({ logId: `getGroup/${logId}`, - publicParams: arrayBufferToBase64(fields.publicParams), - secretParams: arrayBufferToBase64(fields.secretParams), + publicParams: Bytes.toBase64(fields.publicParams), + secretParams: Bytes.toBase64(fields.secretParams), request: (sender, options) => sender.getGroup(options), }); return true; @@ -1783,9 +1771,11 @@ export async function maybeDeriveGroupV2Id( } const v1IdBuffer = fromEncodedBinaryToArrayBuffer(groupV1Id); - const masterKeyBuffer = await deriveMasterKeyFromGroupV1(v1IdBuffer); + const masterKeyBuffer = new FIXMEU8( + await deriveMasterKeyFromGroupV1(v1IdBuffer) + ); const fields = deriveGroupFields(masterKeyBuffer); - const derivedGroupV2Id = arrayBufferToBase64(fields.id); + const derivedGroupV2Id = Bytes.toBase64(fields.id); conversation.set({ derivedGroupV2Id, @@ -1843,7 +1833,7 @@ export async function getGroupMigrationMembers( previousGroupV1Members: Array; }> { const logId = conversation.idForLogging(); - const MEMBER_ROLE_ENUM = window.textsecure.protobuf.Member.Role; + const MEMBER_ROLE_ENUM = Proto.Member.Role; const ourConversationId = window.ConversationController.getOurConversationId(); if (!ourConversationId) { @@ -2025,8 +2015,7 @@ export async function initiateMigrationToGroupV2( try { await conversation.queueJob('initiateMigrationToGroupV2', async () => { - const ACCESS_ENUM = - window.textsecure.protobuf.AccessControl.AccessRequired; + const ACCESS_ENUM = Proto.AccessControl.AccessRequired; const isEligible = isGroupEligibleToMigrate(conversation); const previousGroupV1Id = conversation.get('groupId'); @@ -2038,18 +2027,20 @@ export async function initiateMigrationToGroupV2( } const groupV1IdBuffer = fromEncodedBinaryToArrayBuffer(previousGroupV1Id); - const masterKeyBuffer = await deriveMasterKeyFromGroupV1(groupV1IdBuffer); + const masterKeyBuffer = new FIXMEU8( + await deriveMasterKeyFromGroupV1(groupV1IdBuffer) + ); const fields = deriveGroupFields(masterKeyBuffer); - const groupId = arrayBufferToBase64(fields.id); + const groupId = Bytes.toBase64(fields.id); const logId = `groupv2(${groupId})`; window.log.info( `initiateMigrationToGroupV2/${logId}: Migrating from ${conversation.idForLogging()}` ); - const masterKey = arrayBufferToBase64(masterKeyBuffer); - const secretParams = arrayBufferToBase64(fields.secretParams); - const publicParams = arrayBufferToBase64(fields.publicParams); + const masterKey = Bytes.toBase64(masterKeyBuffer); + const secretParams = Bytes.toBase64(fields.secretParams); + const publicParams = Bytes.toBase64(fields.publicParams); const ourConversationId = window.ConversationController.getOurConversationId(); if (!ourConversationId) { @@ -2208,9 +2199,7 @@ export async function initiateMigrationToGroupV2( | ArrayBuffer | undefined = await ourProfileKeyService.get(); - const { - ContentHint, - } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; + const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; const sendOptions = await getSendOptions(conversation.attributes); await wrapWithSyncMessageSend({ @@ -2380,18 +2369,20 @@ export async function joinGroupV2ViaLinkAndMigrate({ // Derive GroupV2 fields const groupV1IdBuffer = fromEncodedBinaryToArrayBuffer(previousGroupV1Id); - const masterKeyBuffer = await deriveMasterKeyFromGroupV1(groupV1IdBuffer); + const masterKeyBuffer = new FIXMEU8( + await deriveMasterKeyFromGroupV1(groupV1IdBuffer) + ); const fields = deriveGroupFields(masterKeyBuffer); - const groupId = arrayBufferToBase64(fields.id); + const groupId = Bytes.toBase64(fields.id); const logId = idForLogging(groupId); window.log.info( `joinGroupV2ViaLinkAndMigrate/${logId}: Migrating from ${conversation.idForLogging()}` ); - const masterKey = arrayBufferToBase64(masterKeyBuffer); - const secretParams = arrayBufferToBase64(fields.secretParams); - const publicParams = arrayBufferToBase64(fields.publicParams); + const masterKey = Bytes.toBase64(masterKeyBuffer); + const secretParams = Bytes.toBase64(fields.secretParams); + const publicParams = Bytes.toBase64(fields.publicParams); // A mini-migration, which will not show dropped/invited members const newAttributes = { @@ -2476,18 +2467,20 @@ export async function respondToGroupV2Migration({ // Derive GroupV2 fields const groupV1IdBuffer = fromEncodedBinaryToArrayBuffer(previousGroupV1Id); - const masterKeyBuffer = await deriveMasterKeyFromGroupV1(groupV1IdBuffer); + const masterKeyBuffer = new FIXMEU8( + await deriveMasterKeyFromGroupV1(groupV1IdBuffer) + ); const fields = deriveGroupFields(masterKeyBuffer); - const groupId = arrayBufferToBase64(fields.id); + const groupId = Bytes.toBase64(fields.id); const logId = idForLogging(groupId); window.log.info( `respondToGroupV2Migration/${logId}: Migrating from ${conversation.idForLogging()}` ); - const masterKey = arrayBufferToBase64(masterKeyBuffer); - const secretParams = arrayBufferToBase64(fields.secretParams); - const publicParams = arrayBufferToBase64(fields.publicParams); + const masterKey = Bytes.toBase64(masterKeyBuffer); + const secretParams = Bytes.toBase64(fields.secretParams); + const publicParams = Bytes.toBase64(fields.publicParams); const previousGroupV1Members = conversation.get('members'); const previousGroupV1MembersIds = conversation.getMemberIds(); @@ -2516,7 +2509,7 @@ export async function respondToGroupV2Migration({ members: undefined, }; - let firstGroupState: GroupClass | undefined | null; + let firstGroupState: Proto.IGroup | null | undefined; try { const response: GroupLogResponseType = await makeRequestWithTemporalRetry({ @@ -2890,10 +2883,8 @@ async function getGroupUpdates({ (isInitialCreationMessage || weAreAwaitingApproval || isOneVersionUp) ) { window.log.info(`getGroupUpdates/${logId}: Processing just one change`); - const groupChangeBuffer = base64ToArrayBuffer(groupChangeBase64); - const groupChange = window.textsecure.protobuf.GroupChange.decode( - groupChangeBuffer - ); + const groupChangeBuffer = Bytes.fromBase64(groupChangeBase64); + const groupChange = Proto.GroupChange.decode(groupChangeBuffer); const isChangeSupported = !isNumber(groupChange.changeEpoch) || groupChange.changeEpoch <= SUPPORTED_CHANGE_EPOCH; @@ -3019,7 +3010,7 @@ async function updateGroupViaSingleChange({ serverPublicParamsBase64, }: { group: ConversationAttributesType; - groupChange: GroupChangeClass; + groupChange: Proto.IGroupChange; newRevision: number; serverPublicParamsBase64: string; }): Promise { @@ -3190,10 +3181,10 @@ function getGroupCredentials({ ); return { - groupPublicParamsHex: arrayBufferToHex( - base64ToArrayBuffer(groupPublicParamsBase64) + groupPublicParamsHex: Bytes.toHex( + Bytes.fromBase64(groupPublicParamsBase64) ), - authCredentialPresentationHex: arrayBufferToHex(presentation), + authCredentialPresentationHex: Bytes.toHex(presentation), }; } @@ -3230,7 +3221,7 @@ async function getGroupDelta({ let revisionToFetch = isNumber(currentRevision) ? currentRevision + 1 : 0; let response; - const changes: Array = []; + const changes: Array = []; do { // eslint-disable-next-line no-await-in-loop response = await sender.getGroupLog(revisionToFetch, options); @@ -3256,7 +3247,7 @@ async function integrateGroupChanges({ }: { group: ConversationAttributesType; newRevision: number; - changes: Array; + changes: Array; }): Promise { const logId = idForLogging(group.groupId); let attributes = group; @@ -3293,8 +3284,8 @@ async function integrateGroupChanges({ } = await integrateGroupChange({ group: attributes, newRevision, - groupChange, - groupState, + groupChange: dropNull(groupChange), + groupState: dropNull(groupState), }); attributes = newAttributes; @@ -3350,8 +3341,8 @@ async function integrateGroupChange({ newRevision, }: { group: ConversationAttributesType; - groupChange?: GroupChangeClass; - groupState?: GroupClass; + groupChange?: Proto.IGroupChange; + groupState?: Proto.IGroup; newRevision: number; }): Promise { const logId = idForLogging(group.groupId); @@ -3376,13 +3367,13 @@ async function integrateGroupChange({ // These need to be populated from the groupChange. But we might not get one! let isChangeSupported = false; let isMoreThanOneVersionUp = false; - let groupChangeActions: undefined | GroupChangeClass.Actions; - let decryptedChangeActions: undefined | GroupChangeClass.Actions; + let groupChangeActions: undefined | Proto.GroupChange.IActions; + let decryptedChangeActions: undefined | DecryptedGroupChangeActions; let sourceConversationId: undefined | string; if (groupChange) { - groupChangeActions = window.textsecure.protobuf.GroupChange.Actions.decode( - groupChange.actions.toArrayBuffer() + groupChangeActions = Proto.GroupChange.Actions.decode( + groupChange.actions || new FIXMEU8(0) ); if ( @@ -3402,7 +3393,12 @@ async function integrateGroupChange({ logId ); + strictAssert( + decryptedChangeActions !== undefined, + 'Should have decrypted group actions' + ); const { sourceUuid } = decryptedChangeActions; + strictAssert(sourceUuid, 'Should have source UUID'); const sourceConversation = window.ConversationController.getOrCreate( sourceUuid, 'private' @@ -3566,7 +3562,7 @@ function extractDiffs({ const logId = idForLogging(old.groupId); const details: Array = []; const ourConversationId = window.ConversationController.getOurConversationId(); - const ACCESS_ENUM = window.textsecure.protobuf.AccessControl.AccessRequired; + const ACCESS_ENUM = Proto.AccessControl.AccessRequired; let areWeInGroup = false; let areWeInvitedToGroup = false; @@ -3947,8 +3943,7 @@ function extractDiffs({ ...generateBasicMessage(), type: 'timer-notification', sourceUuid, - flags: - window.textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE, + flags: Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE, expirationTimerUpdate: { expireTimer: current.expireTimer || 0, sourceUuid, @@ -3967,13 +3962,13 @@ function extractDiffs({ function profileKeysToMembers(items: Array) { return items.map(item => ({ - profileKey: arrayBufferToBase64(item.profileKey), + profileKey: Bytes.toBase64(item.profileKey), uuid: item.uuid, })); } type GroupChangeMemberType = { - profileKey: ArrayBuffer; + profileKey: Uint8Array; uuid: string; }; type GroupApplyResultType = { @@ -3986,15 +3981,15 @@ async function applyGroupChange({ group, sourceConversationId, }: { - actions: GroupChangeClass.Actions; + actions: DecryptedGroupChangeActions; group: ConversationAttributesType; sourceConversationId: string; }): Promise { const logId = idForLogging(group.groupId); const ourConversationId = window.ConversationController.getOurConversationId(); - const ACCESS_ENUM = window.textsecure.protobuf.AccessControl.AccessRequired; - const MEMBER_ROLE_ENUM = window.textsecure.protobuf.Member.Role; + const ACCESS_ENUM = Proto.AccessControl.AccessRequired; + const MEMBER_ROLE_ENUM = Proto.Member.Role; const version = actions.version || 0; const result = { ...group }; @@ -4019,7 +4014,7 @@ async function applyGroupChange({ // version?: number; result.revision = version; - // addMembers?: Array; + // addMembers?: Array; (actions.addMembers || []).forEach(addMember => { const { added } = addMember; if (!added) { @@ -4069,7 +4064,7 @@ async function applyGroupChange({ } }); - // deleteMembers?: Array; + // deleteMembers?: Array; (actions.deleteMembers || []).forEach(deleteMember => { const { deletedUserId } = deleteMember; if (!deletedUserId) { @@ -4092,7 +4087,7 @@ async function applyGroupChange({ } }); - // modifyMemberRoles?: Array; + // modifyMemberRoles?: Array; (actions.modifyMemberRoles || []).forEach(modifyMemberRole => { const { role, userId } = modifyMemberRole; if (!role || !userId) { @@ -4117,7 +4112,7 @@ async function applyGroupChange({ }); // modifyMemberProfileKeys?: - // Array; + // Array; (actions.modifyMemberProfileKeys || []).forEach(modifyMemberProfileKey => { const { profileKey, uuid } = modifyMemberProfileKey; if (!profileKey || !uuid) { @@ -4133,7 +4128,7 @@ async function applyGroupChange({ }); // addPendingMembers?: Array< - // GroupChangeClass.Actions.AddMemberPendingProfileKeyAction + // GroupChange.Actions.AddMemberPendingProfileKeyAction // >; (actions.addPendingMembers || []).forEach(addPendingMember => { const { added } = addPendingMember; @@ -4177,7 +4172,7 @@ async function applyGroupChange({ }); // deletePendingMembers?: Array< - // GroupChangeClass.Actions.DeleteMemberPendingProfileKeyAction + // GroupChange.Actions.DeleteMemberPendingProfileKeyAction // >; (actions.deletePendingMembers || []).forEach(deletePendingMember => { const { deletedUserId } = deletePendingMember; @@ -4202,7 +4197,7 @@ async function applyGroupChange({ }); // promotePendingMembers?: Array< - // GroupChangeClass.Actions.PromoteMemberPendingProfileKeyAction + // GroupChange.Actions.PromoteMemberPendingProfileKeyAction // >; (actions.promotePendingMembers || []).forEach(promotePendingMember => { const { profileKey, uuid } = promotePendingMember; @@ -4246,7 +4241,7 @@ async function applyGroupChange({ }); }); - // modifyTitle?: GroupChangeClass.Actions.ModifyTitleAction; + // modifyTitle?: GroupChange.Actions.ModifyTitleAction; if (actions.modifyTitle) { const { title } = actions.modifyTitle; if (title && title.content === 'title') { @@ -4259,16 +4254,16 @@ async function applyGroupChange({ } } - // modifyAvatar?: GroupChangeClass.Actions.ModifyAvatarAction; + // modifyAvatar?: GroupChange.Actions.ModifyAvatarAction; if (actions.modifyAvatar) { const { avatar } = actions.modifyAvatar; - await applyNewAvatar(avatar, result, logId); + await applyNewAvatar(dropNull(avatar), result, logId); } // modifyDisappearingMessagesTimer?: - // GroupChangeClass.Actions.ModifyDisappearingMessagesTimerAction; + // GroupChange.Actions.ModifyDisappearingMessagesTimerAction; if (actions.modifyDisappearingMessagesTimer) { - const disappearingMessagesTimer: GroupAttributeBlobClass | undefined = + const disappearingMessagesTimer: Proto.GroupAttributeBlob | undefined = actions.modifyDisappearingMessagesTimer.timer; if ( disappearingMessagesTimer && @@ -4291,7 +4286,7 @@ async function applyGroupChange({ }; // modifyAttributesAccess?: - // GroupChangeClass.Actions.ModifyAttributesAccessControlAction; + // GroupChange.Actions.ModifyAttributesAccessControlAction; if (actions.modifyAttributesAccess) { result.accessControl = { ...result.accessControl, @@ -4300,7 +4295,7 @@ async function applyGroupChange({ }; } - // modifyMemberAccess?: GroupChangeClass.Actions.ModifyMembersAccessControlAction; + // modifyMemberAccess?: GroupChange.Actions.ModifyMembersAccessControlAction; if (actions.modifyMemberAccess) { result.accessControl = { ...result.accessControl, @@ -4309,7 +4304,7 @@ async function applyGroupChange({ } // modifyAddFromInviteLinkAccess?: - // GroupChangeClass.Actions.ModifyAddFromInviteLinkAccessControlAction; + // GroupChange.Actions.ModifyAddFromInviteLinkAccessControlAction; if (actions.modifyAddFromInviteLinkAccess) { result.accessControl = { ...result.accessControl, @@ -4320,7 +4315,7 @@ async function applyGroupChange({ } // addMemberPendingAdminApprovals?: Array< - // GroupChangeClass.Actions.AddMemberPendingAdminApprovalAction + // GroupChange.Actions.AddMemberPendingAdminApprovalAction // >; (actions.addMemberPendingAdminApprovals || []).forEach( pendingAdminApproval => { @@ -4370,7 +4365,7 @@ async function applyGroupChange({ ); // deleteMemberPendingAdminApprovals?: Array< - // GroupChangeClass.Actions.DeleteMemberPendingAdminApprovalAction + // GroupChange.Actions.DeleteMemberPendingAdminApprovalAction // >; (actions.deleteMemberPendingAdminApprovals || []).forEach( deleteAdminApproval => { @@ -4397,7 +4392,7 @@ async function applyGroupChange({ ); // promoteMemberPendingAdminApprovals?: Array< - // GroupChangeClass.Actions.PromoteMemberPendingAdminApprovalAction + // GroupChange.Actions.PromoteMemberPendingAdminApprovalAction // >; (actions.promoteMemberPendingAdminApprovals || []).forEach( promoteAdminApproval => { @@ -4443,7 +4438,7 @@ async function applyGroupChange({ } ); - // modifyInviteLinkPassword?: GroupChangeClass.Actions.ModifyInviteLinkPasswordAction; + // modifyInviteLinkPassword?: GroupChange.Actions.ModifyInviteLinkPasswordAction; if (actions.modifyInviteLinkPassword) { const { inviteLinkPassword } = actions.modifyInviteLinkPassword; if (inviteLinkPassword) { @@ -4453,7 +4448,7 @@ async function applyGroupChange({ } } - // modifyDescription?: GroupChangeClass.Actions.ModifyDescriptionAction; + // modifyDescription?: GroupChange.Actions.ModifyDescriptionAction; if (actions.modifyDescription) { const { descriptionBytes } = actions.modifyDescription; if (descriptionBytes && descriptionBytes.content === 'descriptionText') { @@ -4492,17 +4487,17 @@ export async function decryptGroupAvatar( ); } - const ciphertext = await sender.getGroupAvatar(avatarKey); + const ciphertext = new FIXMEU8(await sender.getGroupAvatar(avatarKey)); const clientZkGroupCipher = getClientZkGroupCipher(secretParamsBase64); const plaintext = decryptGroupBlob(clientZkGroupCipher, ciphertext); - const blob = window.textsecure.protobuf.GroupAttributeBlob.decode(plaintext); + const blob = Proto.GroupAttributeBlob.decode(plaintext); if (blob.content !== 'avatar') { throw new Error( `decryptGroupAvatar: Returned blob had incorrect content: ${blob.content}` ); } - return blob.avatar.toArrayBuffer(); + return typedArrayToArrayBuffer(blob.avatar); } // Ovewriting result.avatar as part of functionality @@ -4563,12 +4558,12 @@ async function applyGroupState({ sourceConversationId, }: { group: ConversationAttributesType; - groupState: GroupClass; + groupState: DecryptedGroupState; sourceConversationId?: string; }): Promise { const logId = idForLogging(group.groupId); - const ACCESS_ENUM = window.textsecure.protobuf.AccessControl.AccessRequired; - const MEMBER_ROLE_ENUM = window.textsecure.protobuf.Member.Role; + const ACCESS_ENUM = Proto.AccessControl.AccessRequired; + const MEMBER_ROLE_ENUM = Proto.Member.Role; const version = groupState.version || 0; const result = { ...group }; const newProfileKeys: Array = []; @@ -4586,7 +4581,7 @@ async function applyGroupState({ } // avatar - await applyNewAvatar(groupState.avatar, result, logId); + await applyNewAvatar(dropNull(groupState.avatar), result, logId); // disappearingMessagesTimer // Note: during decryption, disappearingMessageTimer becomes a GroupAttributeBlob @@ -4617,7 +4612,7 @@ async function applyGroupState({ // members if (groupState.members) { - result.membersV2 = groupState.members.map((member: MemberClass) => { + result.membersV2 = groupState.members.map(member => { const conversation = window.ConversationController.getOrCreate( member.userId, 'private' @@ -4643,10 +4638,12 @@ async function applyGroupState({ ); } - newProfileKeys.push({ - profileKey: member.profileKey, - uuid: member.userId, - }); + if (member.profileKey) { + newProfileKeys.push({ + profileKey: member.profileKey, + uuid: member.userId, + }); + } return { role: member.role || MEMBER_ROLE_ENUM.DEFAULT, @@ -4659,7 +4656,7 @@ async function applyGroupState({ // membersPendingProfileKey if (groupState.membersPendingProfileKey) { result.pendingMembersV2 = groupState.membersPendingProfileKey.map( - (member: MemberPendingProfileKeyClass) => { + member => { let pending; let invitedBy; @@ -4691,10 +4688,12 @@ async function applyGroupState({ ); } - newProfileKeys.push({ - profileKey: member.member.profileKey, - uuid: member.member.userId, - }); + if (member.member.profileKey) { + newProfileKeys.push({ + profileKey: member.member.profileKey, + uuid: member.member.userId, + }); + } return { addedByUserId: invitedBy.id, @@ -4709,7 +4708,7 @@ async function applyGroupState({ // membersPendingAdminApproval if (groupState.membersPendingAdminApproval) { result.pendingAdminApprovalV2 = groupState.membersPendingAdminApproval.map( - (member: MemberPendingAdminApprovalClass) => { + member => { let pending; if (member.userId) { @@ -4754,7 +4753,7 @@ async function applyGroupState({ } function isValidRole(role?: number): role is number { - const MEMBER_ROLE_ENUM = window.textsecure.protobuf.Member.Role; + const MEMBER_ROLE_ENUM = Proto.Member.Role; return ( role === MEMBER_ROLE_ENUM.ADMINISTRATOR || role === MEMBER_ROLE_ENUM.DEFAULT @@ -4762,13 +4761,13 @@ function isValidRole(role?: number): role is number { } function isValidAccess(access?: number): access is number { - const ACCESS_ENUM = window.textsecure.protobuf.AccessControl.AccessRequired; + const ACCESS_ENUM = Proto.AccessControl.AccessRequired; return access === ACCESS_ENUM.ADMINISTRATOR || access === ACCESS_ENUM.MEMBER; } function isValidLinkAccess(access?: number): access is number { - const ACCESS_ENUM = window.textsecure.protobuf.AccessControl.AccessRequired; + const ACCESS_ENUM = Proto.AccessControl.AccessRequired; return ( access === ACCESS_ENUM.UNKNOWN || @@ -4778,14 +4777,18 @@ function isValidLinkAccess(access?: number): access is number { ); } -function isValidProfileKey(buffer?: ArrayBuffer): boolean { - return Boolean(buffer && buffer.byteLength === 32); +function isValidProfileKey(buffer?: Uint8Array): boolean { + return Boolean(buffer && buffer.length === 32); } function normalizeTimestamp( - timestamp: ProtoBigNumberType -): number | ProtoBigNumberType { + timestamp: number | Long | null | undefined +): number { if (!timestamp) { + return 0; + } + + if (typeof timestamp === 'number') { return timestamp; } @@ -4799,90 +4802,145 @@ function normalizeTimestamp( return asNumber; } -/* eslint-disable no-param-reassign */ +type DecryptedGroupChangeActions = { + version?: number; + sourceUuid?: string; + addMembers?: ReadonlyArray<{ + added: DecryptedMember; + joinFromInviteLink: boolean; + }>; + deleteMembers?: ReadonlyArray<{ + deletedUserId: string; + }>; + modifyMemberRoles?: ReadonlyArray<{ + userId: string; + role: Proto.Member.Role; + }>; + modifyMemberProfileKeys?: ReadonlyArray<{ + profileKey: Uint8Array; + uuid: string; + }>; + addPendingMembers?: ReadonlyArray<{ + added: DecryptedMemberPendingProfileKey; + }>; + deletePendingMembers?: ReadonlyArray<{ + deletedUserId: string; + }>; + promotePendingMembers?: ReadonlyArray<{ + profileKey: Uint8Array; + uuid: string; + }>; + modifyTitle?: { + title?: Proto.GroupAttributeBlob; + }; + modifyDisappearingMessagesTimer?: { + timer?: Proto.GroupAttributeBlob; + }; + addMemberPendingAdminApprovals?: ReadonlyArray<{ + added: DecryptedMemberPendingAdminApproval; + }>; + deleteMemberPendingAdminApprovals?: ReadonlyArray<{ + deletedUserId: string; + }>; + promoteMemberPendingAdminApprovals?: ReadonlyArray<{ + userId: string; + role: Proto.Member.Role; + }>; + modifyInviteLinkPassword?: { + inviteLinkPassword?: string; + }; + modifyDescription?: { + descriptionBytes?: Proto.GroupAttributeBlob; + }; +} & Pick< + Proto.GroupChange.IActions, + | 'modifyAttributesAccess' + | 'modifyMemberAccess' + | 'modifyAddFromInviteLinkAccess' + | 'modifyAvatar' +>; function decryptGroupChange( - actions: GroupChangeClass.Actions, + actions: Readonly, groupSecretParams: string, logId: string -): GroupChangeClass.Actions { +): DecryptedGroupChangeActions { + const result: DecryptedGroupChangeActions = { + version: dropNull(actions.version), + }; + const clientZkGroupCipher = getClientZkGroupCipher(groupSecretParams); - if (!isByteBufferEmpty(actions.sourceUuid)) { + if (actions.sourceUuid && actions.sourceUuid.length !== 0) { try { - actions.sourceUuid = decryptUuid( - clientZkGroupCipher, - actions.sourceUuid.toArrayBuffer() + result.sourceUuid = normalizeUuid( + decryptUuid(clientZkGroupCipher, actions.sourceUuid), + 'actions.sourceUuid' ); } catch (error) { window.log.warn( - `decryptGroupChange/${logId}: Unable to decrypt sourceUuid. Clearing sourceUuid.`, + `decryptGroupChange/${logId}: Unable to decrypt sourceUuid.`, error && error.stack ? error.stack : error ); - actions.sourceUuid = undefined; } - window.normalizeUuids(actions, ['sourceUuid'], 'groups.decryptGroupChange'); - - if (!window.isValidGuid(actions.sourceUuid)) { + if (!window.isValidGuid(result.sourceUuid)) { window.log.warn( `decryptGroupChange/${logId}: Invalid sourceUuid. Clearing sourceUuid.` ); - actions.sourceUuid = undefined; + result.sourceUuid = undefined; } } else { throw new Error('decryptGroupChange: Missing sourceUuid'); } - // addMembers?: Array; - actions.addMembers = compact( + // addMembers?: Array; + result.addMembers = compact( (actions.addMembers || []).map(addMember => { - if (addMember.added) { - const decrypted = decryptMember( - clientZkGroupCipher, - addMember.added, - logId - ); - if (!decrypted) { - return null; - } - - addMember.added = decrypted; - return addMember; + strictAssert( + addMember.added, + 'decryptGroupChange: AddMember was missing added field!' + ); + const decrypted = decryptMember( + clientZkGroupCipher, + addMember.added, + logId + ); + if (!decrypted) { + return null; } - throw new Error('decryptGroupChange: AddMember was missing added field!'); + + return { + added: decrypted, + joinFromInviteLink: Boolean(addMember.joinFromInviteLink), + }; }) ); - // deleteMembers?: Array; - actions.deleteMembers = compact( + // deleteMembers?: Array; + result.deleteMembers = compact( (actions.deleteMembers || []).map(deleteMember => { - if (!isByteBufferEmpty(deleteMember.deletedUserId)) { - try { - deleteMember.deletedUserId = decryptUuid( - clientZkGroupCipher, - deleteMember.deletedUserId.toArrayBuffer() - ); - } catch (error) { - window.log.warn( - `decryptGroupChange/${logId}: Unable to decrypt deleteMembers.deletedUserId. Dropping member.`, - error && error.stack ? error.stack : error - ); - return null; - } - } else { - throw new Error( - 'decryptGroupChange: deleteMember.deletedUserId was missing' - ); - } - - window.normalizeUuids( - deleteMember, - ['deletedUserId'], - 'groups.decryptGroupChange' + const { deletedUserId } = deleteMember; + strictAssert( + Bytes.isNotEmpty(deletedUserId), + 'decryptGroupChange: deleteMember.deletedUserId was missing' ); - if (!window.isValidGuid(deleteMember.deletedUserId)) { + let userId: string; + try { + userId = normalizeUuid( + decryptUuid(clientZkGroupCipher, deletedUserId), + 'actions.deleteMembers.deletedUserId' + ); + } catch (error) { + window.log.warn( + `decryptGroupChange/${logId}: Unable to decrypt deleteMembers.deletedUserId. Dropping member.`, + error && error.stack ? error.stack : error + ); + return null; + } + + if (!window.isValidGuid(userId)) { window.log.warn( `decryptGroupChange/${logId}: Dropping deleteMember due to invalid userId` ); @@ -4890,39 +4948,33 @@ function decryptGroupChange( return null; } - return deleteMember; + return { deletedUserId: userId }; }) ); - // modifyMemberRoles?: Array; - actions.modifyMemberRoles = compact( + // modifyMemberRoles?: Array; + result.modifyMemberRoles = compact( (actions.modifyMemberRoles || []).map(modifyMember => { - if (!isByteBufferEmpty(modifyMember.userId)) { - try { - modifyMember.userId = decryptUuid( - clientZkGroupCipher, - modifyMember.userId.toArrayBuffer() - ); - } catch (error) { - window.log.warn( - `decryptGroupChange/${logId}: Unable to decrypt modifyMemberRole.userId. Dropping member.`, - error && error.stack ? error.stack : error - ); - return null; - } - } else { - throw new Error( - 'decryptGroupChange: modifyMemberRole.userId was missing' - ); - } - - window.normalizeUuids( - modifyMember, - ['userId'], - 'groups.decryptGroupChange' + strictAssert( + Bytes.isNotEmpty(modifyMember.userId), + 'decryptGroupChange: modifyMemberRole.userId was missing' ); - if (!window.isValidGuid(modifyMember.userId)) { + let userId: string; + try { + userId = normalizeUuid( + decryptUuid(clientZkGroupCipher, modifyMember.userId), + 'actions.modifyMemberRoles.userId' + ); + } catch (error) { + window.log.warn( + `decryptGroupChange/${logId}: Unable to decrypt modifyMemberRole.userId. Dropping member.`, + error && error.stack ? error.stack : error + ); + return null; + } + + if (!window.isValidGuid(userId)) { window.log.warn( `decryptGroupChange/${logId}: Dropping modifyMemberRole due to invalid userId` ); @@ -4930,117 +4982,109 @@ function decryptGroupChange( return null; } - if (!isValidRole(modifyMember.role)) { + const role = dropNull(modifyMember.role); + if (!isValidRole(role)) { throw new Error( `decryptGroupChange: modifyMemberRole had invalid role ${modifyMember.role}` ); } - return modifyMember; + return { + role, + userId, + }; }) ); // modifyMemberProfileKeys?: Array< - // GroupChangeClass.Actions.ModifyMemberProfileKeyAction + // GroupChange.Actions.ModifyMemberProfileKeyAction // >; - actions.modifyMemberProfileKeys = compact( + result.modifyMemberProfileKeys = compact( (actions.modifyMemberProfileKeys || []).map(modifyMemberProfileKey => { - if (!isByteBufferEmpty(modifyMemberProfileKey.presentation)) { - const { profileKey, uuid } = decryptProfileKeyCredentialPresentation( - clientZkGroupCipher, - modifyMemberProfileKey.presentation.toArrayBuffer() - ); + const { presentation } = modifyMemberProfileKey; + strictAssert( + Bytes.isNotEmpty(presentation), + 'decryptGroupChange: modifyMemberProfileKey.presentation was missing' + ); - modifyMemberProfileKey.profileKey = profileKey; - modifyMemberProfileKey.uuid = uuid; + const decryptedPresentation = decryptProfileKeyCredentialPresentation( + clientZkGroupCipher, + presentation + ); - if ( - !modifyMemberProfileKey.uuid || - !modifyMemberProfileKey.profileKey - ) { - throw new Error( - 'decryptGroupChange: uuid or profileKey missing after modifyMemberProfileKey decryption!' - ); - } - - if (!window.isValidGuid(modifyMemberProfileKey.uuid)) { - window.log.warn( - `decryptGroupChange/${logId}: Dropping modifyMemberProfileKey due to invalid userId` - ); - - return null; - } - - if (!isValidProfileKey(modifyMemberProfileKey.profileKey)) { - throw new Error( - 'decryptGroupChange: modifyMemberProfileKey had invalid profileKey' - ); - } - } else { + if (!decryptedPresentation.uuid || !decryptedPresentation.profileKey) { throw new Error( - 'decryptGroupChange: modifyMemberProfileKey.presentation was missing' + 'decryptGroupChange: uuid or profileKey missing after modifyMemberProfileKey decryption!' ); } - return modifyMemberProfileKey; + if (!window.isValidGuid(decryptedPresentation.uuid)) { + window.log.warn( + `decryptGroupChange/${logId}: Dropping modifyMemberProfileKey due to invalid userId` + ); + + return null; + } + + if (!isValidProfileKey(decryptedPresentation.profileKey)) { + throw new Error( + 'decryptGroupChange: modifyMemberProfileKey had invalid profileKey' + ); + } + + return decryptedPresentation; }) ); // addPendingMembers?: Array< - // GroupChangeClass.Actions.AddMemberPendingProfileKeyAction + // GroupChange.Actions.AddMemberPendingProfileKeyAction // >; - actions.addPendingMembers = compact( + result.addPendingMembers = compact( (actions.addPendingMembers || []).map(addPendingMember => { - if (addPendingMember.added) { - const decrypted = decryptMemberPendingProfileKey( - clientZkGroupCipher, - addPendingMember.added, - logId - ); - if (!decrypted) { - return null; - } - - addPendingMember.added = decrypted; - return addPendingMember; - } - throw new Error( + strictAssert( + addPendingMember.added, 'decryptGroupChange: addPendingMember was missing added field!' ); + const decrypted = decryptMemberPendingProfileKey( + clientZkGroupCipher, + addPendingMember.added, + logId + ); + if (!decrypted) { + return null; + } + + return { + added: decrypted, + }; }) ); // deletePendingMembers?: Array< - // GroupChangeClass.Actions.DeleteMemberPendingProfileKeyAction + // GroupChange.Actions.DeleteMemberPendingProfileKeyAction // >; - actions.deletePendingMembers = compact( + result.deletePendingMembers = compact( (actions.deletePendingMembers || []).map(deletePendingMember => { - if (!isByteBufferEmpty(deletePendingMember.deletedUserId)) { - try { - deletePendingMember.deletedUserId = decryptUuid( - clientZkGroupCipher, - deletePendingMember.deletedUserId.toArrayBuffer() - ); - } catch (error) { - window.log.warn( - `decryptGroupChange/${logId}: Unable to decrypt deletePendingMembers.deletedUserId. Dropping member.`, - error && error.stack ? error.stack : error - ); - return null; - } - } else { - throw new Error( - 'decryptGroupChange: deletePendingMembers.deletedUserId was missing' + const { deletedUserId } = deletePendingMember; + strictAssert( + Bytes.isNotEmpty(deletedUserId), + 'decryptGroupChange: deletePendingMembers.deletedUserId was missing' + ); + let userId: string; + try { + userId = normalizeUuid( + decryptUuid(clientZkGroupCipher, deletedUserId), + 'actions.deletePendingMembers.deletedUserId' ); + } catch (error) { + window.log.warn( + `decryptGroupChange/${logId}: Unable to decrypt deletePendingMembers.deletedUserId. Dropping member.`, + error && error.stack ? error.stack : error + ); + return null; } - window.normalizeUuids( - deletePendingMember, - ['deletedUserId'], - 'groups.decryptGroupChange' - ); - - if (!window.isValidGuid(deletePendingMember.deletedUserId)) { + if (!window.isValidGuid(userId)) { window.log.warn( `decryptGroupChange/${logId}: Dropping deletePendingMember due to invalid deletedUserId` ); @@ -5048,195 +5092,200 @@ function decryptGroupChange( return null; } - return deletePendingMember; + return { + deletedUserId: userId, + }; }) ); // promotePendingMembers?: Array< - // GroupChangeClass.Actions.PromoteMemberPendingProfileKeyAction + // GroupChange.Actions.PromoteMemberPendingProfileKeyAction // >; - actions.promotePendingMembers = compact( + result.promotePendingMembers = compact( (actions.promotePendingMembers || []).map(promotePendingMember => { - if (!isByteBufferEmpty(promotePendingMember.presentation)) { - const { profileKey, uuid } = decryptProfileKeyCredentialPresentation( - clientZkGroupCipher, - promotePendingMember.presentation.toArrayBuffer() - ); + const { presentation } = promotePendingMember; + strictAssert( + Bytes.isNotEmpty(presentation), + 'decryptGroupChange: promotePendingMember.presentation was missing' + ); + const decryptedPresentation = decryptProfileKeyCredentialPresentation( + clientZkGroupCipher, + presentation + ); - promotePendingMember.profileKey = profileKey; - promotePendingMember.uuid = uuid; - - if (!promotePendingMember.uuid || !promotePendingMember.profileKey) { - throw new Error( - 'decryptGroupChange: uuid or profileKey missing after promotePendingMember decryption!' - ); - } - - if (!window.isValidGuid(promotePendingMember.uuid)) { - window.log.warn( - `decryptGroupChange/${logId}: Dropping modifyMemberProfileKey due to invalid userId` - ); - - return null; - } - - if (!isValidProfileKey(promotePendingMember.profileKey)) { - throw new Error( - 'decryptGroupChange: modifyMemberProfileKey had invalid profileKey' - ); - } - } else { + if (!decryptedPresentation.uuid || !decryptedPresentation.profileKey) { throw new Error( - 'decryptGroupChange: promotePendingMember.presentation was missing' + 'decryptGroupChange: uuid or profileKey missing after promotePendingMember decryption!' ); } - return promotePendingMember; + if (!window.isValidGuid(decryptedPresentation.uuid)) { + window.log.warn( + `decryptGroupChange/${logId}: Dropping modifyMemberProfileKey due to invalid userId` + ); + + return null; + } + + if (!isValidProfileKey(decryptedPresentation.profileKey)) { + throw new Error( + 'decryptGroupChange: modifyMemberProfileKey had invalid profileKey' + ); + } + + return decryptedPresentation; }) ); - // modifyTitle?: GroupChangeClass.Actions.ModifyTitleAction; - if (actions.modifyTitle && !isByteBufferEmpty(actions.modifyTitle.title)) { - try { - actions.modifyTitle.title = window.textsecure.protobuf.GroupAttributeBlob.decode( - decryptGroupBlob( - clientZkGroupCipher, - actions.modifyTitle.title.toArrayBuffer() - ) - ); - } catch (error) { - window.log.warn( - `decryptGroupChange/${logId}: Unable to decrypt modifyTitle.title`, - error && error.stack ? error.stack : error - ); - actions.modifyTitle.title = undefined; + // modifyTitle?: GroupChange.Actions.ModifyTitleAction; + if (actions.modifyTitle) { + const { title } = actions.modifyTitle; + + if (Bytes.isNotEmpty(title)) { + try { + result.modifyTitle = { + title: Proto.GroupAttributeBlob.decode( + decryptGroupBlob(clientZkGroupCipher, title) + ), + }; + } catch (error) { + window.log.warn( + `decryptGroupChange/${logId}: Unable to decrypt modifyTitle.title`, + error && error.stack ? error.stack : error + ); + } + } else { + result.modifyTitle = {}; } - } else if (actions.modifyTitle) { - actions.modifyTitle.title = undefined; } - // modifyAvatar?: GroupChangeClass.Actions.ModifyAvatarAction; + // modifyAvatar?: GroupChange.Actions.ModifyAvatarAction; // Note: decryption happens during application of the change, on download of the avatar + result.modifyAvatar = actions.modifyAvatar; // modifyDisappearingMessagesTimer?: - // GroupChangeClass.Actions.ModifyDisappearingMessagesTimerAction; - if ( - actions.modifyDisappearingMessagesTimer && - !isByteBufferEmpty(actions.modifyDisappearingMessagesTimer.timer) - ) { - try { - actions.modifyDisappearingMessagesTimer.timer = window.textsecure.protobuf.GroupAttributeBlob.decode( - decryptGroupBlob( - clientZkGroupCipher, - actions.modifyDisappearingMessagesTimer.timer.toArrayBuffer() - ) - ); - } catch (error) { - window.log.warn( - `decryptGroupChange/${logId}: Unable to decrypt modifyDisappearingMessagesTimer.timer`, - error && error.stack ? error.stack : error - ); - actions.modifyDisappearingMessagesTimer.timer = undefined; + // GroupChange.Actions.ModifyDisappearingMessagesTimerAction; + if (actions.modifyDisappearingMessagesTimer) { + const { timer } = actions.modifyDisappearingMessagesTimer; + + if (Bytes.isNotEmpty(timer)) { + try { + result.modifyDisappearingMessagesTimer = { + timer: Proto.GroupAttributeBlob.decode( + decryptGroupBlob(clientZkGroupCipher, timer) + ), + }; + } catch (error) { + window.log.warn( + `decryptGroupChange/${logId}: Unable to decrypt modifyDisappearingMessagesTimer.timer`, + error && error.stack ? error.stack : error + ); + } + } else { + result.modifyDisappearingMessagesTimer = {}; } - } else if (actions.modifyDisappearingMessagesTimer) { - actions.modifyDisappearingMessagesTimer.timer = undefined; } // modifyAttributesAccess?: - // GroupChangeClass.Actions.ModifyAttributesAccessControlAction; - if ( - actions.modifyAttributesAccess && - !isValidAccess(actions.modifyAttributesAccess.attributesAccess) - ) { - throw new Error( + // GroupChange.Actions.ModifyAttributesAccessControlAction; + if (actions.modifyAttributesAccess) { + const attributesAccess = dropNull( + actions.modifyAttributesAccess.attributesAccess + ); + strictAssert( + isValidAccess(attributesAccess), `decryptGroupChange: modifyAttributesAccess.attributesAccess was not valid: ${actions.modifyAttributesAccess.attributesAccess}` ); + + result.modifyAttributesAccess = { + attributesAccess, + }; } - // modifyMemberAccess?: GroupChangeClass.Actions.ModifyMembersAccessControlAction; - if ( - actions.modifyMemberAccess && - !isValidAccess(actions.modifyMemberAccess.membersAccess) - ) { - throw new Error( + // modifyMemberAccess?: GroupChange.Actions.ModifyMembersAccessControlAction; + if (actions.modifyMemberAccess) { + const membersAccess = dropNull(actions.modifyMemberAccess.membersAccess); + strictAssert( + isValidAccess(membersAccess), `decryptGroupChange: modifyMemberAccess.membersAccess was not valid: ${actions.modifyMemberAccess.membersAccess}` ); + + result.modifyMemberAccess = { + membersAccess, + }; } // modifyAddFromInviteLinkAccess?: - // GroupChangeClass.Actions.ModifyAddFromInviteLinkAccessControlAction; - if ( - actions.modifyAddFromInviteLinkAccess && - !isValidLinkAccess( + // GroupChange.Actions.ModifyAddFromInviteLinkAccessControlAction; + if (actions.modifyAddFromInviteLinkAccess) { + const addFromInviteLinkAccess = dropNull( actions.modifyAddFromInviteLinkAccess.addFromInviteLinkAccess - ) - ) { - throw new Error( + ); + strictAssert( + isValidLinkAccess(addFromInviteLinkAccess), `decryptGroupChange: modifyAddFromInviteLinkAccess.addFromInviteLinkAccess was not valid: ${actions.modifyAddFromInviteLinkAccess.addFromInviteLinkAccess}` ); + + result.modifyAddFromInviteLinkAccess = { + addFromInviteLinkAccess, + }; } // addMemberPendingAdminApprovals?: Array< - // GroupChangeClass.Actions.AddMemberPendingAdminApprovalAction + // GroupChange.Actions.AddMemberPendingAdminApprovalAction // >; - actions.addMemberPendingAdminApprovals = compact( + result.addMemberPendingAdminApprovals = compact( (actions.addMemberPendingAdminApprovals || []).map( addPendingAdminApproval => { - if (addPendingAdminApproval.added) { - const decrypted = decryptMemberPendingAdminApproval( - clientZkGroupCipher, - addPendingAdminApproval.added, - logId - ); - if (!decrypted) { - window.log.warn( - `decryptGroupChange/${logId}: Unable to decrypt addPendingAdminApproval.added. Dropping member.` - ); - return null; - } - - addPendingAdminApproval.added = decrypted; - return addPendingAdminApproval; - } - throw new Error( + const { added } = addPendingAdminApproval; + strictAssert( + added, 'decryptGroupChange: addPendingAdminApproval was missing added field!' ); + + const decrypted = decryptMemberPendingAdminApproval( + clientZkGroupCipher, + added, + logId + ); + if (!decrypted) { + window.log.warn( + `decryptGroupChange/${logId}: Unable to decrypt addPendingAdminApproval.added. Dropping member.` + ); + return null; + } + + return { added: decrypted }; } ) ); // deleteMemberPendingAdminApprovals?: Array< - // GroupChangeClass.Actions.DeleteMemberPendingAdminApprovalAction + // GroupChange.Actions.DeleteMemberPendingAdminApprovalAction // >; - actions.deleteMemberPendingAdminApprovals = compact( + result.deleteMemberPendingAdminApprovals = compact( (actions.deleteMemberPendingAdminApprovals || []).map( deletePendingApproval => { - if (!isByteBufferEmpty(deletePendingApproval.deletedUserId)) { - try { - deletePendingApproval.deletedUserId = decryptUuid( - clientZkGroupCipher, - deletePendingApproval.deletedUserId.toArrayBuffer() - ); - } catch (error) { - window.log.warn( - `decryptGroupChange/${logId}: Unable to decrypt deletePendingApproval.deletedUserId. Dropping member.`, - error && error.stack ? error.stack : error - ); - return null; - } - } else { - throw new Error( - 'decryptGroupChange: deletePendingApproval.deletedUserId was missing' - ); - } - - window.normalizeUuids( - deletePendingApproval, - ['deletedUserId'], - 'groups.decryptGroupChange' + const { deletedUserId } = deletePendingApproval; + strictAssert( + Bytes.isNotEmpty(deletedUserId), + 'decryptGroupChange: deletePendingApproval.deletedUserId was missing' ); - if (!window.isValidGuid(deletePendingApproval.deletedUserId)) { + let userId: string; + try { + userId = normalizeUuid( + decryptUuid(clientZkGroupCipher, deletedUserId), + 'actions.deleteMemberPendingAdminApprovals' + ); + } catch (error) { + window.log.warn( + `decryptGroupChange/${logId}: Unable to decrypt deletePendingApproval.deletedUserId. Dropping member.`, + error && error.stack ? error.stack : error + ); + return null; + } + if (!window.isValidGuid(userId)) { window.log.warn( `decryptGroupChange/${logId}: Dropping deletePendingApproval due to invalid deletedUserId` ); @@ -5244,114 +5293,115 @@ function decryptGroupChange( return null; } - return deletePendingApproval; + return { deletedUserId: userId }; } ) ); // promoteMemberPendingAdminApprovals?: Array< - // GroupChangeClass.Actions.PromoteMemberPendingAdminApprovalAction + // GroupChange.Actions.PromoteMemberPendingAdminApprovalAction // >; - actions.promoteMemberPendingAdminApprovals = compact( + result.promoteMemberPendingAdminApprovals = compact( (actions.promoteMemberPendingAdminApprovals || []).map( promoteAdminApproval => { - if (!isByteBufferEmpty(promoteAdminApproval.userId)) { - try { - promoteAdminApproval.userId = decryptUuid( - clientZkGroupCipher, - promoteAdminApproval.userId.toArrayBuffer() - ); - } catch (error) { - window.log.warn( - `decryptGroupChange/${logId}: Unable to decrypt promoteAdminApproval.userId. Dropping member.`, - error && error.stack ? error.stack : error - ); - return null; - } - } else { - throw new Error( - 'decryptGroupChange: promoteAdminApproval.userId was missing' + const { userId } = promoteAdminApproval; + strictAssert( + Bytes.isNotEmpty(userId), + 'decryptGroupChange: promoteAdminApproval.userId was missing' + ); + + let decryptedUserId: string; + try { + decryptedUserId = normalizeUuid( + decryptUuid(clientZkGroupCipher, userId), + 'actions.promoteMemberPendingAdminApprovals.userId' ); + } catch (error) { + window.log.warn( + `decryptGroupChange/${logId}: Unable to decrypt promoteAdminApproval.userId. Dropping member.`, + error && error.stack ? error.stack : error + ); + return null; } - if (!isValidRole(promoteAdminApproval.role)) { + const role = dropNull(promoteAdminApproval.role); + if (!isValidRole(role)) { throw new Error( `decryptGroupChange: promoteAdminApproval had invalid role ${promoteAdminApproval.role}` ); } - return promoteAdminApproval; + return { role, userId: decryptedUserId }; } ) ); - // modifyInviteLinkPassword?: GroupChangeClass.Actions.ModifyInviteLinkPasswordAction; - if ( - actions.modifyInviteLinkPassword && - !isByteBufferEmpty(actions.modifyInviteLinkPassword.inviteLinkPassword) - ) { - actions.modifyInviteLinkPassword.inviteLinkPassword = actions.modifyInviteLinkPassword.inviteLinkPassword.toString( - 'base64' - ); - } else { - actions.modifyInviteLinkPassword = undefined; - } - - // modifyDescription?: GroupChangeClass.Actions.ModifyDescriptionAction; - if ( - actions.modifyDescription && - !isByteBufferEmpty(actions.modifyDescription.descriptionBytes) - ) { - try { - actions.modifyDescription.descriptionBytes = window.textsecure.protobuf.GroupAttributeBlob.decode( - decryptGroupBlob( - clientZkGroupCipher, - actions.modifyDescription.descriptionBytes.toArrayBuffer() - ) - ); - } catch (error) { - window.log.warn( - `decryptGroupChange/${logId}: Unable to decrypt modifyDescription.descriptionBytes`, - error && error.stack ? error.stack : error - ); - actions.modifyDescription.descriptionBytes = undefined; + // modifyInviteLinkPassword?: GroupChange.Actions.ModifyInviteLinkPasswordAction; + if (actions.modifyInviteLinkPassword) { + const { inviteLinkPassword: password } = actions.modifyInviteLinkPassword; + if (Bytes.isNotEmpty(password)) { + result.modifyInviteLinkPassword = { + inviteLinkPassword: Bytes.toBase64(password), + }; + } else { + result.modifyInviteLinkPassword = {}; } - } else if (actions.modifyDescription) { - actions.modifyDescription.descriptionBytes = undefined; } - return actions; + // modifyDescription?: GroupChange.Actions.ModifyDescriptionAction; + if (actions.modifyDescription) { + const { descriptionBytes } = actions.modifyDescription; + if (Bytes.isNotEmpty(descriptionBytes)) { + try { + result.modifyDescription = { + descriptionBytes: Proto.GroupAttributeBlob.decode( + decryptGroupBlob(clientZkGroupCipher, descriptionBytes) + ), + }; + } catch (error) { + window.log.warn( + `decryptGroupChange/${logId}: Unable to decrypt modifyDescription.descriptionBytes`, + error && error.stack ? error.stack : error + ); + } + } else { + result.modifyDescription = {}; + } + } + + return result; } export function decryptGroupTitle( - title: ProtoBinaryType, + title: Uint8Array | undefined, secretParams: string ): string | undefined { const clientZkGroupCipher = getClientZkGroupCipher(secretParams); - if (!isByteBufferEmpty(title)) { - const blob = window.textsecure.protobuf.GroupAttributeBlob.decode( - decryptGroupBlob(clientZkGroupCipher, title.toArrayBuffer()) - ); + if (!title || !title.length) { + return undefined; + } + const blob = Proto.GroupAttributeBlob.decode( + decryptGroupBlob(clientZkGroupCipher, title) + ); - if (blob && blob.content === 'title') { - return blob.title; - } + if (blob && blob.content === 'title') { + return blob.title; } return undefined; } export function decryptGroupDescription( - description: ProtoBinaryType, + description: Uint8Array | undefined, secretParams: string ): string | undefined { const clientZkGroupCipher = getClientZkGroupCipher(secretParams); - if (isByteBufferEmpty(description)) { + if (!description || !description.length) { return undefined; } - const blob = window.textsecure.protobuf.GroupAttributeBlob.decode( - decryptGroupBlob(clientZkGroupCipher, description.toArrayBuffer()) + const blob = Proto.GroupAttributeBlob.decode( + decryptGroupBlob(clientZkGroupCipher, description) ); if (blob && blob.content === 'descriptionText') { @@ -5361,40 +5411,58 @@ export function decryptGroupDescription( return undefined; } +type DecryptedGroupState = { + title?: Proto.GroupAttributeBlob; + disappearingMessagesTimer?: Proto.GroupAttributeBlob; + accessControl?: { + attributes: number; + members: number; + addFromInviteLink: number; + }; + version?: number; + members?: ReadonlyArray; + membersPendingProfileKey?: ReadonlyArray; + membersPendingAdminApproval?: ReadonlyArray; + inviteLinkPassword?: string; + descriptionBytes?: Proto.GroupAttributeBlob; + avatar?: string; +}; + function decryptGroupState( - groupState: GroupClass, + groupState: Readonly, groupSecretParams: string, logId: string -): GroupClass { +): DecryptedGroupState { const clientZkGroupCipher = getClientZkGroupCipher(groupSecretParams); + const result: DecryptedGroupState = {}; // title - if (!isByteBufferEmpty(groupState.title)) { + if (Bytes.isNotEmpty(groupState.title)) { try { - groupState.title = window.textsecure.protobuf.GroupAttributeBlob.decode( - decryptGroupBlob(clientZkGroupCipher, groupState.title.toArrayBuffer()) + result.title = Proto.GroupAttributeBlob.decode( + decryptGroupBlob(clientZkGroupCipher, groupState.title) ); } catch (error) { window.log.warn( `decryptGroupState/${logId}: Unable to decrypt title. Clearing it.`, error && error.stack ? error.stack : error ); - groupState.title = undefined; } - } else { - groupState.title = undefined; } // avatar // Note: decryption happens during application of the change, on download of the avatar // disappearing message timer - if (!isByteBufferEmpty(groupState.disappearingMessagesTimer)) { + if ( + groupState.disappearingMessagesTimer && + groupState.disappearingMessagesTimer.length + ) { try { - groupState.disappearingMessagesTimer = window.textsecure.protobuf.GroupAttributeBlob.decode( + result.disappearingMessagesTimer = Proto.GroupAttributeBlob.decode( decryptGroupBlob( clientZkGroupCipher, - groupState.disappearingMessagesTimer.toArrayBuffer() + groupState.disappearingMessagesTimer ) ); } catch (error) { @@ -5402,40 +5470,49 @@ function decryptGroupState( `decryptGroupState/${logId}: Unable to decrypt disappearing message timer. Clearing it.`, error && error.stack ? error.stack : error ); - groupState.disappearingMessagesTimer = undefined; } - } else { - groupState.disappearingMessagesTimer = undefined; } // accessControl - if (!isValidAccess(groupState.accessControl?.attributes)) { - throw new Error( - `decryptGroupState: Access control for attributes is invalid: ${groupState.accessControl?.attributes}` + { + const { accessControl } = groupState; + strictAssert(accessControl, 'No accessControl field found'); + + const attributes = dropNull(accessControl.attributes); + const members = dropNull(accessControl.members); + const addFromInviteLink = dropNull(accessControl.addFromInviteLink); + + strictAssert( + isValidAccess(attributes), + `decryptGroupState: Access control for attributes is invalid: ${attributes}` ); - } - if (!isValidAccess(groupState.accessControl?.members)) { - throw new Error( - `decryptGroupState: Access control for members is invalid: ${groupState.accessControl?.members}` + strictAssert( + isValidAccess(members), + `decryptGroupState: Access control for members is invalid: ${members}` ); - } - if (!isValidLinkAccess(groupState.accessControl?.addFromInviteLink)) { - throw new Error( - `decryptGroupState: Access control for invite link is invalid: ${groupState.accessControl?.addFromInviteLink}` + strictAssert( + isValidLinkAccess(addFromInviteLink), + `decryptGroupState: Access control for invite link is invalid: ${addFromInviteLink}` ); + + result.accessControl = { + attributes, + members, + addFromInviteLink, + }; } // version - if (!isNumber(groupState.version)) { - throw new Error( - `decryptGroupState: Expected version to be a number; it was ${groupState.version}` - ); - } + strictAssert( + isNumber(groupState.version), + `decryptGroupState: Expected version to be a number; it was ${groupState.version}` + ); + result.version = groupState.version; // members if (groupState.members) { - groupState.members = compact( - groupState.members.map((member: MemberClass) => + result.members = compact( + groupState.members.map((member: Proto.IMember) => decryptMember(clientZkGroupCipher, member, logId) ) ); @@ -5443,9 +5520,9 @@ function decryptGroupState( // membersPendingProfileKey if (groupState.membersPendingProfileKey) { - groupState.membersPendingProfileKey = compact( + result.membersPendingProfileKey = compact( groupState.membersPendingProfileKey.map( - (member: MemberPendingProfileKeyClass) => + (member: Proto.IMemberPendingProfileKey) => decryptMemberPendingProfileKey(clientZkGroupCipher, member, logId) ) ); @@ -5453,292 +5530,313 @@ function decryptGroupState( // membersPendingAdminApproval if (groupState.membersPendingAdminApproval) { - groupState.membersPendingAdminApproval = compact( + result.membersPendingAdminApproval = compact( groupState.membersPendingAdminApproval.map( - (member: MemberPendingAdminApprovalClass) => + (member: Proto.IMemberPendingAdminApproval) => decryptMemberPendingAdminApproval(clientZkGroupCipher, member, logId) ) ); } // inviteLinkPassword - if (!isByteBufferEmpty(groupState.inviteLinkPassword)) { - groupState.inviteLinkPassword = groupState.inviteLinkPassword.toString( - 'base64' - ); - } else { - groupState.inviteLinkPassword = undefined; + if (Bytes.isNotEmpty(groupState.inviteLinkPassword)) { + result.inviteLinkPassword = Bytes.toBase64(groupState.inviteLinkPassword); } // descriptionBytes - if (!isByteBufferEmpty(groupState.descriptionBytes)) { + if (Bytes.isNotEmpty(groupState.descriptionBytes)) { try { - groupState.descriptionBytes = window.textsecure.protobuf.GroupAttributeBlob.decode( - decryptGroupBlob( - clientZkGroupCipher, - groupState.descriptionBytes.toArrayBuffer() - ) + result.descriptionBytes = Proto.GroupAttributeBlob.decode( + decryptGroupBlob(clientZkGroupCipher, groupState.descriptionBytes) ); } catch (error) { window.log.warn( `decryptGroupState/${logId}: Unable to decrypt descriptionBytes. Clearing it.`, error && error.stack ? error.stack : error ); - groupState.descriptionBytes = undefined; } - } else { - groupState.descriptionBytes = undefined; } - return groupState; + result.avatar = dropNull(groupState.avatar); + + return result; } +type DecryptedMember = Readonly<{ + userId: string; + profileKey: Uint8Array; + role: Proto.Member.Role; + joinedAtVersion?: number; +}>; + function decryptMember( clientZkGroupCipher: ClientZkGroupCipher, - member: MemberClass, + member: Readonly, logId: string -) { +): DecryptedMember | undefined { // userId - if (!isByteBufferEmpty(member.userId)) { - try { - member.userId = decryptUuid( - clientZkGroupCipher, - member.userId.toArrayBuffer() - ); - } catch (error) { - window.log.warn( - `decryptMember/${logId}: Unable to decrypt member userid. Dropping member.`, - error && error.stack ? error.stack : error - ); - return null; - } + strictAssert( + Bytes.isNotEmpty(member.userId), + 'decryptMember: Member had missing userId' + ); - window.normalizeUuids(member, ['userId'], 'groups.decryptMember'); + let userId: string; + try { + userId = normalizeUuid( + decryptUuid(clientZkGroupCipher, member.userId), + 'decryptMember.userId' + ); + } catch (error) { + window.log.warn( + `decryptMember/${logId}: Unable to decrypt member userid. Dropping member.`, + error && error.stack ? error.stack : error + ); + return undefined; + } - if (!window.isValidGuid(member.userId)) { - window.log.warn( - `decryptMember/${logId}: Dropping member due to invalid userId` - ); + if (!window.isValidGuid(userId)) { + window.log.warn( + `decryptMember/${logId}: Dropping member due to invalid userId` + ); - return null; - } - } else { - throw new Error('decryptMember: Member had missing userId'); + return undefined; } // profileKey - if (!isByteBufferEmpty(member.profileKey)) { - member.profileKey = decryptProfileKey( - clientZkGroupCipher, - member.profileKey.toArrayBuffer(), - member.userId - ); + strictAssert( + Bytes.isNotEmpty(member.profileKey), + 'decryptMember: Member had missing profileKey' + ); + const profileKey = decryptProfileKey( + clientZkGroupCipher, + member.profileKey, + userId + ); - if (!isValidProfileKey(member.profileKey)) { - throw new Error('decryptMember: Member had invalid profileKey'); - } - } else { - throw new Error('decryptMember: Member had missing profileKey'); + if (!isValidProfileKey(profileKey)) { + throw new Error('decryptMember: Member had invalid profileKey'); } // role - if (!isValidRole(member.role)) { + const role = dropNull(member.role); + + if (!isValidRole(role)) { throw new Error(`decryptMember: Member had invalid role ${member.role}`); } - return member; + return { + userId, + profileKey, + role, + joinedAtVersion: dropNull(member.joinedAtVersion), + }; } +type DecryptedMemberPendingProfileKey = { + addedByUserId: string; + timestamp: number; + member: { + userId: string; + profileKey?: Uint8Array; + role?: Proto.Member.Role; + }; +}; + function decryptMemberPendingProfileKey( clientZkGroupCipher: ClientZkGroupCipher, - member: MemberPendingProfileKeyClass, + member: Readonly, logId: string -) { +): DecryptedMemberPendingProfileKey | undefined { // addedByUserId - if (!isByteBufferEmpty(member.addedByUserId)) { - try { - member.addedByUserId = decryptUuid( - clientZkGroupCipher, - member.addedByUserId.toArrayBuffer() - ); - } catch (error) { - window.log.warn( - `decryptMemberPendingProfileKey/${logId}: Unable to decrypt pending member addedByUserId. Dropping member.`, - error && error.stack ? error.stack : error - ); - return null; - } + strictAssert( + Bytes.isNotEmpty(member.addedByUserId), + 'decryptMemberPendingProfileKey: Member had missing addedByUserId' + ); - window.normalizeUuids( - member, - ['addedByUserId'], - 'groups.decryptMemberPendingProfileKey' + let addedByUserId: string; + try { + addedByUserId = normalizeUuid( + decryptUuid(clientZkGroupCipher, member.addedByUserId), + 'decryptMemberPendingProfileKey.addedByUserId' ); + } catch (error) { + window.log.warn( + `decryptMemberPendingProfileKey/${logId}: Unable to decrypt pending member addedByUserId. Dropping member.`, + error && error.stack ? error.stack : error + ); + return undefined; + } - if (!window.isValidGuid(member.addedByUserId)) { - window.log.warn( - `decryptMemberPendingProfileKey/${logId}: Dropping pending member due to invalid addedByUserId` - ); - return null; - } - } else { - throw new Error( - 'decryptMemberPendingProfileKey: Member had missing addedByUserId' + if (!window.isValidGuid(addedByUserId)) { + window.log.warn( + `decryptMemberPendingProfileKey/${logId}: Dropping pending member due to invalid addedByUserId` ); + return undefined; } // timestamp - member.timestamp = normalizeTimestamp(member.timestamp); + const timestamp = normalizeTimestamp(member.timestamp); if (!member.member) { window.log.warn( `decryptMemberPendingProfileKey/${logId}: Dropping pending member due to missing member details` ); - return null; + return undefined; } - const { userId, profileKey, role } = member.member; + const { userId, profileKey } = member.member; // userId - if (!isByteBufferEmpty(userId)) { - try { - member.member.userId = decryptUuid( - clientZkGroupCipher, - userId.toArrayBuffer() - ); - } catch (error) { - window.log.warn( - `decryptMemberPendingProfileKey/${logId}: Unable to decrypt pending member userId. Dropping member.`, - error && error.stack ? error.stack : error - ); - return null; - } + strictAssert( + Bytes.isNotEmpty(userId), + 'decryptMemberPendingProfileKey: Member had missing member.userId' + ); - window.normalizeUuids( - member.member, - ['userId'], - 'groups.decryptMemberPendingProfileKey' + let decryptedUserId: string; + try { + decryptedUserId = normalizeUuid( + decryptUuid(clientZkGroupCipher, userId), + 'decryptMemberPendingProfileKey.member.userId' + ); + } catch (error) { + window.log.warn( + `decryptMemberPendingProfileKey/${logId}: Unable to decrypt pending member userId. Dropping member.`, + error && error.stack ? error.stack : error + ); + return undefined; + } + + if (!window.isValidGuid(decryptedUserId)) { + window.log.warn( + `decryptMemberPendingProfileKey/${logId}: Dropping pending member due to invalid member.userId` ); - if (!window.isValidGuid(member.member.userId)) { - window.log.warn( - `decryptMemberPendingProfileKey/${logId}: Dropping pending member due to invalid member.userId` - ); - - return null; - } - } else { - throw new Error( - 'decryptMemberPendingProfileKey: Member had missing member.userId' - ); + return undefined; } // profileKey - if (!isByteBufferEmpty(profileKey)) { + let decryptedProfileKey: Uint8Array | undefined; + if (Bytes.isNotEmpty(profileKey)) { try { - member.member.profileKey = decryptProfileKey( + decryptedProfileKey = decryptProfileKey( clientZkGroupCipher, - profileKey.toArrayBuffer(), - member.member.userId + profileKey, + decryptedUserId ); } catch (error) { window.log.warn( `decryptMemberPendingProfileKey/${logId}: Unable to decrypt pending member profileKey. Dropping profileKey.`, error && error.stack ? error.stack : error ); - member.member.profileKey = null; } - if (!isValidProfileKey(member.member.profileKey)) { + if (!isValidProfileKey(decryptedProfileKey)) { window.log.warn( `decryptMemberPendingProfileKey/${logId}: Dropping profileKey, since it was invalid` ); - - member.member.profileKey = null; + decryptedProfileKey = undefined; } } // role - if (!isValidRole(role)) { - throw new Error( - `decryptMemberPendingProfileKey: Member had invalid role ${role}` - ); - } + const role = dropNull(member.member.role); - return member; + strictAssert( + isValidRole(role), + `decryptMemberPendingProfileKey: Member had invalid role ${role}` + ); + + return { + addedByUserId, + timestamp, + member: { + userId: decryptedUserId, + profileKey: decryptedProfileKey, + role, + }, + }; } +type DecryptedMemberPendingAdminApproval = { + userId: string; + profileKey?: Uint8Array; + timestamp: number; +}; + function decryptMemberPendingAdminApproval( clientZkGroupCipher: ClientZkGroupCipher, - member: MemberPendingAdminApprovalClass, + member: Readonly, logId: string -) { +): DecryptedMemberPendingAdminApproval | undefined { // timestamp - member.timestamp = normalizeTimestamp(member.timestamp); + const timestamp = normalizeTimestamp(member.timestamp); const { userId, profileKey } = member; // userId - if (!isByteBufferEmpty(userId)) { - try { - member.userId = decryptUuid(clientZkGroupCipher, userId.toArrayBuffer()); - } catch (error) { - window.log.warn( - `decryptMemberPendingAdminApproval/${logId}: Unable to decrypt pending member userId. Dropping member.`, - error && error.stack ? error.stack : error - ); - return null; - } + strictAssert( + Bytes.isNotEmpty(userId), + 'decryptMemberPendingAdminApproval: Missing userId' + ); - window.normalizeUuids( - member, - ['userId'], - 'groups.decryptMemberPendingAdminApproval' + let decryptedUserId: string; + try { + decryptedUserId = normalizeUuid( + decryptUuid(clientZkGroupCipher, userId), + 'decryptMemberPendingAdminApproval.userId' + ); + } catch (error) { + window.log.warn( + `decryptMemberPendingAdminApproval/${logId}: Unable to decrypt pending member userId. Dropping member.`, + error && error.stack ? error.stack : error + ); + return undefined; + } + + if (!window.isValidGuid(decryptedUserId)) { + window.log.warn( + `decryptMemberPendingAdminApproval/${logId}: Invalid userId. Dropping member.` ); - if (!window.isValidGuid(member.userId)) { - window.log.warn( - `decryptMemberPendingAdminApproval/${logId}: Invalid userId. Dropping member.` - ); - - return null; - } - } else { - throw new Error('decryptMemberPendingAdminApproval: Missing userId'); + return undefined; } // profileKey - if (!isByteBufferEmpty(profileKey)) { + let decryptedProfileKey: Uint8Array | undefined; + if (Bytes.isNotEmpty(profileKey)) { try { - member.profileKey = decryptProfileKey( + decryptedProfileKey = decryptProfileKey( clientZkGroupCipher, - profileKey.toArrayBuffer(), - member.userId + profileKey, + decryptedUserId ); } catch (error) { window.log.warn( `decryptMemberPendingAdminApproval/${logId}: Unable to decrypt profileKey. Dropping profileKey.`, error && error.stack ? error.stack : error ); - member.profileKey = null; } - if (!isValidProfileKey(member.profileKey)) { + if (!isValidProfileKey(decryptedProfileKey)) { window.log.warn( `decryptMemberPendingAdminApproval/${logId}: Dropping profileKey, since it was invalid` ); - member.profileKey = null; + decryptedProfileKey = undefined; } } - return member; + return { + timestamp, + userId: decryptedUserId, + profileKey: decryptedProfileKey, + }; } export function getMembershipList( conversationId: string -): Array<{ uuid: string; uuidCiphertext: ArrayBuffer }> { +): Array<{ uuid: string; uuidCiphertext: Uint8Array }> { const conversation = window.ConversationController.get(conversationId); if (!conversation) { throw new Error('getMembershipList: cannot find conversation'); diff --git a/ts/groups/joinViaLink.ts b/ts/groups/joinViaLink.ts index eecf3edd4..f39e091ce 100644 --- a/ts/groups/joinViaLink.ts +++ b/ts/groups/joinViaLink.ts @@ -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 { let inviteLinkPassword: string; @@ -42,11 +42,11 @@ export async function joinViaLink(hash: string): Promise { 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 { return; } - let result: GroupJoinInfoClass; + let result: Proto.GroupJoinInfo; try { result = await longRunningTaskWrapper({ diff --git a/ts/models/conversations.ts b/ts/models/conversations.ts index e71ec491b..ce1b72741 100644 --- a/ts/models/conversations.ts +++ b/ts/models/conversations.ts @@ -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 { + ): Promise { 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 { + ): Promise { 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 { + ): Promise { 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 { + ): Promise { 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 { + ): Promise { const idLog = this.idForLogging(); const toRequest = window.ConversationController.get(conversationId); @@ -610,7 +615,7 @@ export class ConversationModel extends window.Backbone async removePendingMember( conversationIds: Array - ): Promise { + ): Promise { const idLog = this.idForLogging(); const uuids = conversationIds @@ -656,7 +661,7 @@ export class ConversationModel extends window.Backbone async removeMember( conversationId: string - ): Promise { + ): Promise { 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 { + ): Promise { if (!isGroupV2(this.attributes)) { return undefined; } @@ -738,7 +743,7 @@ export class ConversationModel extends window.Backbone inviteLinkPassword, name, }: { - createGroupChange: () => Promise; + createGroupChange: () => Promise; extraConversationsForSend?: Array; 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; } diff --git a/ts/services/calling.ts b/ts/services/calling.ts index 582e1819d..cf89c7ed7 100644 --- a/ts/services/calling.ts +++ b/ts/services/calling.ts @@ -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) ) ); } diff --git a/ts/services/storageRecordOps.ts b/ts/services/storageRecordOps.ts index 8fc5732ef..b0578b9ab 100644 --- a/ts/services/storageRecordOps.ts +++ b/ts/services/storageRecordOps.ts @@ -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 { - 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; diff --git a/ts/test-both/util/Bytes_test.ts b/ts/test-both/util/Bytes_test.ts new file mode 100644 index 000000000..e344aaeb0 --- /dev/null +++ b/ts/test-both/util/Bytes_test.ts @@ -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); + }); + }); +}); diff --git a/ts/test-both/util/assert_test.ts b/ts/test-both/util/assert_test.ts index d69d0530a..005332210 100644 --- a/ts/test-both/util/assert_test.ts +++ b/ts/test-both/util/assert_test.ts @@ -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'); + }); }); }); diff --git a/ts/test-both/util/dropNull_test.ts b/ts/test-both/util/dropNull_test.ts new file mode 100644 index 000000000..d13d897d1 --- /dev/null +++ b/ts/test-both/util/dropNull_test.ts @@ -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'); + }); +}); diff --git a/ts/test-both/util/isValidGuid_test.ts b/ts/test-both/util/isValidGuid_test.ts new file mode 100644 index 000000000..db2e56517 --- /dev/null +++ b/ts/test-both/util/isValidGuid_test.ts @@ -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())); + }); +}); diff --git a/ts/test-both/util/normalizeUuid_test.ts b/ts/test-both/util/normalizeUuid_test.ts new file mode 100644 index 000000000..0adc75370 --- /dev/null +++ b/ts/test-both/util/normalizeUuid_test.ts @@ -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); + }); +}); diff --git a/ts/textsecure/MessageReceiver.ts b/ts/textsecure/MessageReceiver.ts index 7aa44666d..a464b539b 100644 --- a/ts/textsecure/MessageReceiver.ts +++ b/ts/textsecure/MessageReceiver.ts @@ -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'); diff --git a/ts/textsecure/SendMessage.ts b/ts/textsecure/SendMessage.ts index db3671c50..618a43471 100644 --- a/ts/textsecure/SendMessage.ts +++ b/ts/textsecure/SendMessage.ts @@ -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; }; @@ -1961,27 +1958,27 @@ export default class MessageSender { } async createGroup( - group: GroupClass, + group: Proto.IGroup, options: GroupCredentialsType ): Promise { return this.server.createGroup(group, options); } async uploadGroupAvatar( - avatar: ArrayBuffer, + avatar: Uint8Array, options: GroupCredentialsType ): Promise { return this.server.uploadGroupAvatar(avatar, options); } - async getGroup(options: GroupCredentialsType): Promise { + async getGroup(options: GroupCredentialsType): Promise { return this.server.getGroup(options); } async getGroupFromLink( groupInviteLink: string, auth: GroupCredentialsType - ): Promise { + ): Promise { 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 { + ): Promise { return this.server.modifyGroup(changes, options, inviteLinkBase64); } @@ -2060,7 +2057,7 @@ export default class MessageSender { async getGroupMembershipToken( options: GroupCredentialsType - ): Promise { + ): Promise { return this.server.getGroupExternalCredential(options); } diff --git a/ts/textsecure/WebAPI.ts b/ts/textsecure/WebAPI.ts index 94c389079..44c8141ae 100644 --- a/ts/textsecure/WebAPI.ts +++ b/ts/textsecure/WebAPI.ts @@ -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; createGroup: ( - group: GroupClass, + group: Proto.IGroup, options: GroupCredentialsType ) => Promise; getAttachment: (cdnKey: string, cdnNumber?: number) => Promise; getAvatar: (path: string) => Promise; getDevices: () => Promise; - getGroup: (options: GroupCredentialsType) => Promise; + getGroup: (options: GroupCredentialsType) => Promise; getGroupFromLink: ( inviteLinkPassword: string, auth: GroupCredentialsType - ) => Promise; + ) => Promise; getGroupAvatar: (key: string) => Promise; getGroupCredentials: ( startDay: number, @@ -957,7 +957,7 @@ export type WebAPIType = { ) => Promise>; getGroupExternalCredential: ( options: GroupCredentialsType - ) => Promise; + ) => Promise; getGroupLog: ( startVersion: number, options: GroupCredentialsType @@ -1020,10 +1020,10 @@ export type WebAPIType = { body: ArrayBuffer | undefined ) => Promise; modifyGroup: ( - changes: GroupChangeClass.Actions, + changes: Proto.GroupChange.IActions, options: GroupCredentialsType, inviteLinkBase64?: string - ) => Promise; + ) => Promise; modifyStorageRecords: MessageSender['modifyStorageRecords']; putAttachment: (encryptedBin: ArrayBuffer) => Promise; registerCapabilities: (capabilities: CapabilitiesUploadType) => Promise; @@ -1060,7 +1060,7 @@ export type WebAPIType = { setSignedPreKey: (signedPreKey: SignedPreKeyType) => Promise; updateDeviceName: (deviceName: string) => Promise; uploadGroupAvatar: ( - avatarData: ArrayBuffer, + avatarData: Uint8Array, options: GroupCredentialsType ) => Promise; whoami: () => Promise; @@ -2150,7 +2150,7 @@ export function initialize({ async function getGroupExternalCredential( options: GroupCredentialsType - ): Promise { + ): Promise { 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 { 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 { 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 { + ): Promise { 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 { + ): Promise { 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 { + ): Promise { 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'); diff --git a/ts/util/assert.ts b/ts/util/assert.ts index 378bc0014..a6fb957d2 100644 --- a/ts/util/assert.ts +++ b/ts/util/assert.ts @@ -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); + } +} diff --git a/ts/util/dropNull.ts b/ts/util/dropNull.ts new file mode 100644 index 000000000..f4651b1ea --- /dev/null +++ b/ts/util/dropNull.ts @@ -0,0 +1,11 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +export function dropNull( + value: NonNullable | null | undefined +): T | undefined { + if (value === null) { + return undefined; + } + return value; +} diff --git a/ts/util/isValidGuid.ts b/ts/util/isValidGuid.ts new file mode 100644 index 000000000..6009e2969 --- /dev/null +++ b/ts/util/isValidGuid.ts @@ -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 + ); diff --git a/ts/util/normalizeUuid.ts b/ts/util/normalizeUuid.ts new file mode 100644 index 000000000..23b99ccca --- /dev/null +++ b/ts/util/normalizeUuid.ts @@ -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(); +} diff --git a/ts/util/zkgroup.ts b/ts/util/zkgroup.ts index 6b4c013a9..5a3855960 100644 --- a/ts/util/zkgroup.ts +++ b/ts/util/zkgroup.ts @@ -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( diff --git a/ts/views/conversation_view.ts b/ts/views/conversation_view.ts index 68ff2158f..d7b90cc07 100644 --- a/ts/views/conversation_view.ts +++ b/ts/views/conversation_view.ts @@ -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( diff --git a/ts/window.d.ts b/ts/window.d.ts index a74896f2f..08c7994d1 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -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;