diff --git a/ACKNOWLEDGMENTS.md b/ACKNOWLEDGMENTS.md index 619b6b8083..9bc9603756 100644 --- a/ACKNOWLEDGMENTS.md +++ b/ACKNOWLEDGMENTS.md @@ -1687,6 +1687,210 @@ Signal Desktop makes use of the following open source projects. licenses; we recommend you read them, as their terms may differ from the terms above. +## long + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ## lru-cache The ISC License diff --git a/package.json b/package.json index c1ba16e5e0..e5aca6ca47 100644 --- a/package.json +++ b/package.json @@ -104,6 +104,7 @@ "js-yaml": "3.13.1", "linkify-it": "2.2.0", "lodash": "4.17.21", + "long": "4.0.0", "lru-cache": "6.0.0", "mac-screen-capture-permissions": "2.0.0", "memoizee": "0.4.14", diff --git a/sticker-creator/preload.js b/sticker-creator/preload.js index 642bb4e907..782b4c11ad 100644 --- a/sticker-creator/preload.js +++ b/sticker-creator/preload.js @@ -11,6 +11,7 @@ const { noop, uniqBy } = require('lodash'); const pMap = require('p-map'); const client = require('@signalapp/signal-client'); const { deriveStickerPackKey } = require('../ts/Crypto'); +const { SignalService: Proto } = require('../ts/protobuf'); const { getEnvironment, setEnvironment, @@ -219,24 +220,24 @@ window.encryptAndUpload = async ( 'imageData' ); - const manifestProto = new window.textsecure.protobuf.StickerPack(); + const manifestProto = new Proto.StickerPack(); manifestProto.title = manifest.title; manifestProto.author = manifest.author; manifestProto.stickers = stickers.map(({ emoji }, id) => { - const s = new window.textsecure.protobuf.StickerPack.Sticker(); + const s = new Proto.StickerPack.Sticker(); s.id = id; s.emoji = emoji; return s; }); - const coverSticker = new window.textsecure.protobuf.StickerPack.Sticker(); + const coverSticker = new Proto.StickerPack.Sticker(); coverSticker.id = uniqueStickers.length === stickers.length ? 0 : uniqueStickers.length - 1; coverSticker.emoji = ''; manifestProto.cover = coverSticker; const encryptedManifest = await encrypt( - manifestProto.toArrayBuffer(), + Proto.StickerPack.encode(manifestProto).finish(), encryptionKey, iv ); diff --git a/ts/background.ts b/ts/background.ts index 6c86bfb0aa..6212219379 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -59,6 +59,7 @@ import { SystemTraySetting, parseSystemTraySetting, } from './types/SystemTraySetting'; +import { SignalService as Proto } from './protobuf'; const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000; @@ -119,15 +120,12 @@ export async function startApp(): Promise { const reconnectBackOff = new BackOff(FIBONACCI_TIMEOUTS); - window.textsecure.protobuf.onLoad(() => { - window.storage.onready(() => { - senderCertificateService.initialize({ - WebAPI: window.WebAPI, - navigator, - onlineEventTarget: window, - SenderCertificate: window.textsecure.protobuf.SenderCertificate, - storage: window.storage, - }); + window.storage.onready(() => { + senderCertificateService.initialize({ + WebAPI: window.WebAPI, + navigator, + onlineEventTarget: window, + storage: window.storage, }); }); @@ -2876,7 +2874,7 @@ export async function startApp(): Promise { destinationUuid: data.sourceUuid, }); - const { PROFILE_KEY_UPDATE } = window.textsecure.protobuf.DataMessage.Flags; + const { PROFILE_KEY_UPDATE } = Proto.DataMessage.Flags; // eslint-disable-next-line no-bitwise const isProfileUpdate = Boolean(data.message.flags & PROFILE_KEY_UPDATE); if (isProfileUpdate) { @@ -3198,7 +3196,7 @@ export async function startApp(): Promise { sourceUuid: window.textsecure.storage.user.getUuid(), }); - const { PROFILE_KEY_UPDATE } = window.textsecure.protobuf.DataMessage.Flags; + const { PROFILE_KEY_UPDATE } = Proto.DataMessage.Flags; // eslint-disable-next-line no-bitwise const isProfileUpdate = Boolean(data.message.flags & PROFILE_KEY_UPDATE); if (isProfileUpdate) { @@ -3514,9 +3512,7 @@ export async function startApp(): Promise { ); try { - const { - ContentHint, - } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; + const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; const result = await window.textsecure.messaging.sendSenderKeyDistributionMessage( { @@ -3739,9 +3735,7 @@ export async function startApp(): Promise { return; } - const { - ContentHint, - } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; + const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; // 3. Determine how to represent this to the user. Three different options. @@ -3844,8 +3838,7 @@ export async function startApp(): Promise { const { eventType } = ev; - const FETCH_LATEST_ENUM = - window.textsecure.protobuf.SyncMessage.FetchLatest.Type; + const FETCH_LATEST_ENUM = Proto.SyncMessage.FetchLatest.Type; switch (eventType) { case FETCH_LATEST_ENUM.LOCAL_PROFILE: @@ -4008,13 +4001,13 @@ export async function startApp(): Promise { } switch (ev.verified.state) { - case window.textsecure.protobuf.Verified.State.DEFAULT: + case Proto.Verified.State.DEFAULT: state = 'DEFAULT'; break; - case window.textsecure.protobuf.Verified.State.VERIFIED: + case Proto.Verified.State.VERIFIED: state = 'VERIFIED'; break; - case window.textsecure.protobuf.Verified.State.UNVERIFIED: + case Proto.Verified.State.UNVERIFIED: state = 'UNVERIFIED'; break; default: diff --git a/ts/groups/joinViaLink.ts b/ts/groups/joinViaLink.ts index f39e091cea..ff72c83a51 100644 --- a/ts/groups/joinViaLink.ts +++ b/ts/groups/joinViaLink.ts @@ -98,7 +98,7 @@ export async function joinViaLink(hash: string): Promise { return; } - const ACCESS_ENUM = window.textsecure.protobuf.AccessControl.AccessRequired; + const ACCESS_ENUM = Proto.AccessControl.AccessRequired; if ( result.addFromInviteLink !== ACCESS_ENUM.ADMINISTRATOR && result.addFromInviteLink !== ACCESS_ENUM.ANY diff --git a/ts/models/conversations.ts b/ts/models/conversations.ts index 972ee0f789..65b7f8bb1d 100644 --- a/ts/models/conversations.ts +++ b/ts/models/conversations.ts @@ -724,7 +724,7 @@ export class ConversationModel extends window.Backbone ); } - const MEMBER_ROLES = window.textsecure.protobuf.Member.Role; + const MEMBER_ROLES = Proto.Member.Role; const role = this.isAdmin(conversationId) ? MEMBER_ROLES.DEFAULT @@ -1183,9 +1183,7 @@ export class ConversationModel extends window.Backbone } ); - const { - ContentHint, - } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; + const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; const sendOptions = await getSendOptions(this.attributes); if (isDirectConversation(this.attributes)) { @@ -1612,8 +1610,7 @@ export class ConversationModel extends window.Backbone { fromSync = false, viaStorageServiceSync = false } = {} ): Promise { try { - const messageRequestEnum = - window.textsecure.protobuf.SyncMessage.MessageRequestResponse.Type; + const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type; const isLocalAction = !fromSync && !viaStorageServiceSync; const ourConversationId = window.ConversationController.getOurConversationId(); @@ -1793,8 +1790,7 @@ export class ConversationModel extends window.Backbone } } - const messageRequestEnum = - window.textsecure.protobuf.SyncMessage.MessageRequestResponse.Type; + const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type; // Ensure active_at is set, because this is an event that justifies putting the group // in the left pane. @@ -2901,7 +2897,7 @@ export class ConversationModel extends window.Backbone return false; } - const MEMBER_ROLES = window.textsecure.protobuf.Member.Role; + const MEMBER_ROLES = Proto.Member.Role; return member.role === MEMBER_ROLES.ADMINISTRATOR; } @@ -2916,8 +2912,7 @@ export class ConversationModel extends window.Backbone const members = this.get('membersV2') || []; return members.map(member => ({ - isAdmin: - member.role === window.textsecure.protobuf.Member.Role.ADMINISTRATOR, + isAdmin: member.role === Proto.Member.Role.ADMINISTRATOR, conversationId: member.conversationId, })); } @@ -3213,9 +3208,7 @@ export class ConversationModel extends window.Backbone profileKey = await ourProfileKeyService.get(); } - const { - ContentHint, - } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; + const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; if (isDirectConversation(this.attributes)) { return window.textsecure.messaging.sendMessageToIdentifier({ @@ -3363,9 +3356,7 @@ export class ConversationModel extends window.Backbone } const options = await getSendOptions(this.attributes); - const { - ContentHint, - } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; + const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; const promise = (() => { if (isDirectConversation(this.attributes)) { @@ -3621,9 +3612,7 @@ export class ConversationModel extends window.Backbone const conversationType = this.get('type'); const options = await getSendOptions(this.attributes); - const { - ContentHint, - } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; + const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; let promise; if (conversationType === Message.GROUP) { @@ -3845,7 +3834,7 @@ export class ConversationModel extends window.Backbone value ); - const ACCESS_ENUM = window.textsecure.protobuf.AccessControl.AccessRequired; + const ACCESS_ENUM = Proto.AccessControl.AccessRequired; const addFromInviteLink = value ? ACCESS_ENUM.ANY : ACCESS_ENUM.UNSATISFIABLE; @@ -3889,7 +3878,7 @@ export class ConversationModel extends window.Backbone return; } - const ACCESS_ENUM = window.textsecure.protobuf.AccessControl.AccessRequired; + const ACCESS_ENUM = Proto.AccessControl.AccessRequired; const addFromInviteLink = value ? ACCESS_ENUM.ADMINISTRATOR @@ -3927,7 +3916,7 @@ export class ConversationModel extends window.Backbone ), }); - const ACCESS_ENUM = window.textsecure.protobuf.AccessControl.AccessRequired; + const ACCESS_ENUM = Proto.AccessControl.AccessRequired; this.set({ accessControl: { addFromInviteLink: @@ -3952,7 +3941,7 @@ export class ConversationModel extends window.Backbone ), }); - const ACCESS_ENUM = window.textsecure.protobuf.AccessControl.AccessRequired; + const ACCESS_ENUM = Proto.AccessControl.AccessRequired; this.set({ accessControl: { addFromInviteLink: @@ -4030,8 +4019,7 @@ export class ConversationModel extends window.Backbone sent_at: timestamp, received_at: window.Signal.Util.incrementMessageCounter(), received_at_ms: timestamp, - flags: - window.textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE, + flags: Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE, expirationTimerUpdate: { expireTimer, source, @@ -4068,8 +4056,7 @@ export class ConversationModel extends window.Backbone let promise; if (isMe(this.attributes)) { - const flags = - window.textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE; + const flags = Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE; const dataMessage = await window.textsecure.messaging.getDataMessage({ attachments: [], // body @@ -4166,7 +4153,7 @@ export class ConversationModel extends window.Backbone destination: this.get('e164'), destinationUuid: this.get('uuid'), recipients: this.getRecipients(), - flags: window.textsecure.protobuf.DataMessage.Flags.END_SESSION, + flags: Proto.DataMessage.Flags.END_SESSION, // TODO: DESKTOP-722 } as unknown) as MessageAttributesType); @@ -4887,8 +4874,7 @@ export class ConversationModel extends window.Backbone return true; } - const accessControlEnum = - window.textsecure.protobuf.AccessControl.AccessRequired; + const accessControlEnum = Proto.AccessControl.AccessRequired; const accessControl = this.get('accessControl'); const canAnyoneChangeTimer = accessControl && @@ -4913,7 +4899,7 @@ export class ConversationModel extends window.Backbone return ( this.areWeAdmin() || this.get('accessControl')?.attributes === - window.textsecure.protobuf.AccessControl.AccessRequired.MEMBER + Proto.AccessControl.AccessRequired.MEMBER ); } @@ -4922,7 +4908,7 @@ export class ConversationModel extends window.Backbone return false; } - const memberEnum = window.textsecure.protobuf.Member.Role; + const memberEnum = Proto.Member.Role; const members = this.get('membersV2') || []; const myId = window.ConversationController.getOurConversationId(); const me = members.find(item => item.conversationId === myId); diff --git a/ts/models/messages.ts b/ts/models/messages.ts index e853a4d297..26e1b8e145 100644 --- a/ts/models/messages.ts +++ b/ts/models/messages.ts @@ -78,6 +78,7 @@ import { ReadSyncs } from '../messageModifiers/ReadSyncs'; import { ViewSyncs } from '../messageModifiers/ViewSyncs'; import * as AttachmentDownloads from '../messageModifiers/AttachmentDownloads'; import * as LinkPreview from '../types/LinkPreview'; +import { SignalService as Proto } from '../protobuf'; /* eslint-disable camelcase */ /* eslint-disable more/no-then */ @@ -175,10 +176,8 @@ export class MessageModel extends window.Backbone.Model { ); } - this.CURRENT_PROTOCOL_VERSION = - window.textsecure.protobuf.DataMessage.ProtocolVersion.CURRENT; - this.INITIAL_PROTOCOL_VERSION = - window.textsecure.protobuf.DataMessage.ProtocolVersion.INITIAL; + this.CURRENT_PROTOCOL_VERSION = Proto.DataMessage.ProtocolVersion.CURRENT; + this.INITIAL_PROTOCOL_VERSION = Proto.DataMessage.ProtocolVersion.INITIAL; this.OUR_NUMBER = window.textsecure.storage.user.getNumber(); this.OUR_UUID = window.textsecure.storage.user.getUuid(); @@ -439,11 +438,10 @@ export class MessageModel extends window.Backbone.Model { } if (isGroupV2Change(attributes)) { - const { protobuf } = window.textsecure; const change = this.get('groupV2Change'); const lines = window.Signal.GroupChange.renderChange(change, { - AccessControlEnum: protobuf.AccessControl.AccessRequired, + AccessControlEnum: Proto.AccessControl.AccessRequired, i18n: window.i18n, ourConversationId: window.ConversationController.getOurConversationId(), renderContact: (conversationId: string) => { @@ -459,7 +457,7 @@ export class MessageModel extends window.Backbone.Model { _i18n: unknown, placeholders: Array ) => window.i18n(key, placeholders), - RoleEnum: protobuf.Member.Role, + RoleEnum: Proto.Member.Role, }); return { text: lines.join(' ') }; @@ -1285,9 +1283,7 @@ export class MessageModel extends window.Backbone.Model { let promise; const options = await getSendOptions(conversation.attributes); - const { - ContentHint, - } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; + const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; if (isDirectConversation(conversation.attributes)) { const [identifier] = recipients; @@ -1427,9 +1423,7 @@ export class MessageModel extends window.Backbone.Model { return this.sendSyncMessageOnly(dataMessage); } - const { - ContentHint, - } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; + const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; const parentConversation = this.getConversation(); const groupId = parentConversation?.get('groupId'); const { @@ -1440,7 +1434,7 @@ export class MessageModel extends window.Backbone.Model { groupId && isGroupV1(parentConversation?.attributes) ? { id: groupId, - type: window.textsecure.protobuf.GroupContext.Type.DELIVER, + type: Proto.GroupContext.Type.DELIVER, } : undefined; @@ -2385,7 +2379,7 @@ export class MessageModel extends window.Backbone.Model { const sourceUuid = message.get('sourceUuid'); const type = message.get('type'); const conversationId = message.get('conversationId'); - const GROUP_TYPES = window.textsecure.protobuf.GroupContext.Type; + const GROUP_TYPES = Proto.GroupContext.Type; const fromContact = this.getContact(); if (fromContact) { @@ -2568,8 +2562,7 @@ export class MessageModel extends window.Backbone.Model { const hasGroupV2Prop = Boolean(initialMessage.groupV2); const isV1GroupUpdate = initialMessage.group && - initialMessage.group.type !== - window.textsecure.protobuf.GroupContext.Type.DELIVER; + initialMessage.group.type !== Proto.GroupContext.Type.DELIVER; // Drop an incoming GroupV2 message if we or the sender are not part of the group // after applying the message's associated group changes. diff --git a/ts/protobuf/index.ts b/ts/protobuf/index.ts index c6e7e37c62..87b2f1a628 100644 --- a/ts/protobuf/index.ts +++ b/ts/protobuf/index.ts @@ -1,6 +1,12 @@ // Copyright 2018-2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +import * as protobuf from 'protobufjs/minimal'; +import Long from 'long'; + import { signalservice as SignalService } from './compiled'; +protobuf.util.Long = Long; +protobuf.configure(); + export { SignalService }; diff --git a/ts/services/calling.ts b/ts/services/calling.ts index aa3191d48b..91f842d29f 100644 --- a/ts/services/calling.ts +++ b/ts/services/calling.ts @@ -72,6 +72,7 @@ import { } from '../calling/constants'; import { notify } from './notify'; import { getSendOptions } from '../util/getSendOptions'; +import { SignalService as Proto } from '../protobuf'; const RINGRTC_HTTP_METHOD_TO_OUR_HTTP_METHOD: Map< HttpMethod, @@ -800,9 +801,7 @@ export class CallingClass { const timestamp = Date.now(); // We "fire and forget" because sending this message is non-essential. - const { - ContentHint, - } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; + const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; wrapWithSyncMessageSend({ conversation, logId: `sendToGroup/groupCallUpdate/${conversationId}-${eraId}`, diff --git a/ts/services/senderCertificate.ts b/ts/services/senderCertificate.ts index d6b9686810..70032c2c7d 100644 --- a/ts/services/senderCertificate.ts +++ b/ts/services/senderCertificate.ts @@ -6,14 +6,18 @@ import { serializedCertificateSchema, SerializedCertificateType, } from '../textsecure/OutgoingMessage'; -import { SenderCertificateClass } from '../textsecure'; -import { base64ToArrayBuffer } from '../Crypto'; +import * as Bytes from '../Bytes'; +import { typedArrayToArrayBuffer } from '../Crypto'; import { assert } from '../util/assert'; import { missingCaseError } from '../util/missingCaseError'; +import { normalizeNumber } from '../util/normalizeNumber'; import { waitForOnline } from '../util/waitForOnline'; import * as log from '../logging/log'; import { connectToServerWithStoredCredentials } from '../util/connectToServerWithStoredCredentials'; import { StorageInterface } from '../types/Storage.d'; +import { SignalService as Proto } from '../protobuf'; + +import SenderCertificate = Proto.SenderCertificate; function isWellFormed(data: unknown): data is SerializedCertificateType { return serializedCertificateSchema.safeParse(data).success; @@ -26,8 +30,6 @@ const CLOCK_SKEW_THRESHOLD = 15 * 60 * 1000; export class SenderCertificateService { private WebAPI?: typeof window.WebAPI; - private SenderCertificate?: typeof SenderCertificateClass; - private fetchPromises: Map< SenderCertificateMode, Promise @@ -40,7 +42,6 @@ export class SenderCertificateService { private storage?: StorageInterface; initialize({ - SenderCertificate, WebAPI, navigator, onlineEventTarget, @@ -49,12 +50,10 @@ export class SenderCertificateService { WebAPI: typeof window.WebAPI; navigator: Readonly<{ onLine: boolean }>; onlineEventTarget: EventTarget; - SenderCertificate: typeof SenderCertificateClass; storage: StorageInterface; }): void { log.info('Sender certificate service initialized'); - this.SenderCertificate = SenderCertificate; this.WebAPI = WebAPI; this.navigator = navigator; this.onlineEventTarget = onlineEventTarget; @@ -134,9 +133,9 @@ export class SenderCertificateService { private async fetchAndSaveCertificate( mode: SenderCertificateMode ): Promise { - const { SenderCertificate, storage, navigator, onlineEventTarget } = this; + const { storage, navigator, onlineEventTarget } = this; assert( - SenderCertificate && storage && navigator && onlineEventTarget, + storage && navigator && onlineEventTarget, 'Sender certificate service method was called before it was initialized' ); @@ -160,12 +159,12 @@ export class SenderCertificateService { ); return undefined; } - const certificate = base64ToArrayBuffer(certificateString); + const certificate = Bytes.fromBase64(certificateString); const decodedContainer = SenderCertificate.decode(certificate); const decodedCert = decodedContainer.certificate ? SenderCertificate.Certificate.decode(decodedContainer.certificate) : undefined; - const expires = decodedCert?.expires?.toNumber(); + const expires = normalizeNumber(decodedCert?.expires); if (!isExpirationValid(expires)) { log.warn( @@ -178,7 +177,7 @@ export class SenderCertificateService { const serializedCertificate = { expires: expires - CLOCK_SKEW_THRESHOLD, - serialized: certificate, + serialized: typedArrayToArrayBuffer(certificate), }; await storage.put(modeToStorageKey(mode), serializedCertificate); diff --git a/ts/test-both/util/normalizeNumber_test.ts b/ts/test-both/util/normalizeNumber_test.ts new file mode 100644 index 0000000000..b532e96ad1 --- /dev/null +++ b/ts/test-both/util/normalizeNumber_test.ts @@ -0,0 +1,21 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { assert } from 'chai'; +import Long from 'long'; + +import { normalizeNumber } from '../../util/normalizeNumber'; + +describe('normalizeNumber', () => { + it('returns undefined when input is undefined', () => { + assert.isUndefined(normalizeNumber(undefined)); + }); + + it('returns number when input is number', () => { + assert.strictEqual(normalizeNumber(123), 123); + }); + + it('returns number when input is Long', () => { + assert.strictEqual(normalizeNumber(new Long(123)), 123); + }); +}); diff --git a/ts/test-electron/WebsocketResources_test.ts b/ts/test-electron/WebsocketResources_test.ts index 06f469ee18..fb7d0d27b3 100644 --- a/ts/test-electron/WebsocketResources_test.ts +++ b/ts/test-electron/WebsocketResources_test.ts @@ -11,8 +11,10 @@ import { assert } from 'chai'; import * as sinon from 'sinon'; import EventEmitter from 'events'; import { connection as WebSocket } from 'websocket'; +import Long from 'long'; -import { typedArrayToArrayBuffer as toArrayBuffer } from '../Crypto'; +import { dropNull } from '../util/dropNull'; +import { SignalService as Proto } from '../protobuf'; import WebSocketResource from '../textsecure/WebsocketResources'; @@ -23,23 +25,38 @@ describe('WebSocket-Resource', () => { public close() {} } + const NOW = Date.now(); + + beforeEach(function beforeEach() { + this.sandbox = sinon.createSandbox(); + this.clock = this.sandbox.useFakeTimers({ + now: NOW, + }); + }); + + afterEach(function afterEach() { + this.sandbox.restore(); + }); + describe('requests and responses', () => { it('receives requests and sends responses', done => { // mock socket - const requestId = '1'; + const requestId = new Long(0xdeadbeef, 0x7fffffff); const socket = new FakeSocket(); sinon.stub(socket, 'sendBytes').callsFake((data: Uint8Array) => { - const message = window.textsecure.protobuf.WebSocketMessage.decode( - toArrayBuffer(data) - ); - assert.strictEqual( - message.type, - window.textsecure.protobuf.WebSocketMessage.Type.RESPONSE - ); + const message = Proto.WebSocketMessage.decode(data); + assert.strictEqual(message.type, Proto.WebSocketMessage.Type.RESPONSE); assert.strictEqual(message.response?.message, 'OK'); assert.strictEqual(message.response?.status, 200); - assert.strictEqual(message.response?.id.toString(), requestId); + const id = message.response?.id; + + if (id instanceof Long) { + assert(id.equals(requestId)); + } else { + assert(false, `id should be Long, got ${id}`); + } + done(); }); @@ -48,14 +65,7 @@ describe('WebSocket-Resource', () => { handleRequest(request: any) { assert.strictEqual(request.verb, 'PUT'); assert.strictEqual(request.path, '/some/path'); - assert.ok( - window.Signal.Crypto.constantTimeEqual( - request.body.toArrayBuffer(), - window.Signal.Crypto.typedArrayToArrayBuffer( - new Uint8Array([1, 2, 3]) - ) - ) - ); + assert.deepEqual(request.body, new Uint8Array([1, 2, 3])); request.respond(200, 'OK'); }, }); @@ -63,48 +73,30 @@ describe('WebSocket-Resource', () => { // mock socket request socket.emit('message', { type: 'binary', - binaryData: new Uint8Array( - new window.textsecure.protobuf.WebSocketMessage({ - type: window.textsecure.protobuf.WebSocketMessage.Type.REQUEST, - request: { - id: requestId, - verb: 'PUT', - path: '/some/path', - body: window.Signal.Crypto.typedArrayToArrayBuffer( - new Uint8Array([1, 2, 3]) - ), - }, - }) - .encode() - .toArrayBuffer() - ), + binaryData: Proto.WebSocketMessage.encode({ + type: Proto.WebSocketMessage.Type.REQUEST, + request: { + id: requestId, + verb: 'PUT', + path: '/some/path', + body: new Uint8Array([1, 2, 3]), + }, + }).finish(), }); }); it('sends requests and receives responses', done => { // mock socket and request handler - let requestId: Long | undefined; + let requestId: number | Long | undefined; const socket = new FakeSocket(); sinon.stub(socket, 'sendBytes').callsFake((data: Uint8Array) => { - const message = window.textsecure.protobuf.WebSocketMessage.decode( - toArrayBuffer(data) - ); - assert.strictEqual( - message.type, - window.textsecure.protobuf.WebSocketMessage.Type.REQUEST - ); + const message = Proto.WebSocketMessage.decode(data); + assert.strictEqual(message.type, Proto.WebSocketMessage.Type.REQUEST); assert.strictEqual(message.request?.verb, 'PUT'); assert.strictEqual(message.request?.path, '/some/path'); - assert.ok( - window.Signal.Crypto.constantTimeEqual( - message.request?.body.toArrayBuffer(), - window.Signal.Crypto.typedArrayToArrayBuffer( - new Uint8Array([1, 2, 3]) - ) - ) - ); - requestId = message.request?.id; + assert.deepEqual(message.request?.body, new Uint8Array([1, 2, 3])); + requestId = dropNull(message.request?.id); }); // actual test @@ -112,9 +104,7 @@ describe('WebSocket-Resource', () => { resource.sendRequest({ verb: 'PUT', path: '/some/path', - body: window.Signal.Crypto.typedArrayToArrayBuffer( - new Uint8Array([1, 2, 3]) - ), + body: new Uint8Array([1, 2, 3]), error: done, success(message: string, status: number) { assert.strictEqual(message, 'OK'); @@ -126,14 +116,10 @@ describe('WebSocket-Resource', () => { // mock socket response socket.emit('message', { type: 'binary', - binaryData: new Uint8Array( - new window.textsecure.protobuf.WebSocketMessage({ - type: window.textsecure.protobuf.WebSocketMessage.Type.RESPONSE, - response: { id: requestId, message: 'OK', status: 200 }, - }) - .encode() - .toArrayBuffer() - ), + binaryData: Proto.WebSocketMessage.encode({ + type: Proto.WebSocketMessage.Type.RESPONSE, + response: { id: requestId, message: 'OK', status: 200 }, + }).finish(), }); }); }); @@ -147,33 +133,27 @@ describe('WebSocket-Resource', () => { const resource = new WebSocketResource(socket as WebSocket); resource.close(); }); + + it('force closes the connection', function test(done) { + const socket = new FakeSocket(); + + const resource = new WebSocketResource(socket as WebSocket); + resource.close(); + + resource.addEventListener('close', () => done()); + + // Wait 5 seconds to forcefully close the connection + this.clock.next(); + }); }); describe('with a keepalive config', () => { - const NOW = Date.now(); - - beforeEach(function beforeEach() { - this.sandbox = sinon.createSandbox(); - this.clock = this.sandbox.useFakeTimers({ - now: NOW, - }); - }); - - afterEach(function afterEach() { - this.sandbox.restore(); - }); - it('sends keepalives once a minute', function test(done) { const socket = new FakeSocket(); sinon.stub(socket, 'sendBytes').callsFake(data => { - const message = window.textsecure.protobuf.WebSocketMessage.decode( - toArrayBuffer(data) - ); - assert.strictEqual( - message.type, - window.textsecure.protobuf.WebSocketMessage.Type.REQUEST - ); + const message = Proto.WebSocketMessage.decode(data); + assert.strictEqual(message.type, Proto.WebSocketMessage.Type.REQUEST); assert.strictEqual(message.request?.verb, 'GET'); assert.strictEqual(message.request?.path, '/v1/keepalive'); done(); @@ -190,13 +170,8 @@ describe('WebSocket-Resource', () => { const socket = new FakeSocket(); sinon.stub(socket, 'sendBytes').callsFake(data => { - const message = window.textsecure.protobuf.WebSocketMessage.decode( - toArrayBuffer(data) - ); - assert.strictEqual( - message.type, - window.textsecure.protobuf.WebSocketMessage.Type.REQUEST - ); + const message = Proto.WebSocketMessage.decode(data); + assert.strictEqual(message.type, Proto.WebSocketMessage.Type.REQUEST); assert.strictEqual(message.request?.verb, 'GET'); assert.strictEqual(message.request?.path, '/'); done(); @@ -245,13 +220,8 @@ describe('WebSocket-Resource', () => { const socket = new FakeSocket(); sinon.stub(socket, 'sendBytes').callsFake(data => { - const message = window.textsecure.protobuf.WebSocketMessage.decode( - toArrayBuffer(data) - ); - assert.strictEqual( - message.type, - window.textsecure.protobuf.WebSocketMessage.Type.REQUEST - ); + const message = Proto.WebSocketMessage.decode(data); + assert.strictEqual(message.type, Proto.WebSocketMessage.Type.REQUEST); assert.strictEqual(message.request?.verb, 'GET'); assert.strictEqual(message.request?.path, '/'); assert.strictEqual( diff --git a/ts/test-electron/services/senderCertificate_test.ts b/ts/test-electron/services/senderCertificate_test.ts index 8cdd6498f7..43c4aae646 100644 --- a/ts/test-electron/services/senderCertificate_test.ts +++ b/ts/test-electron/services/senderCertificate_test.ts @@ -4,33 +4,32 @@ // We allow `any`s because it's arduous to set up "real" WebAPIs and storages. /* eslint-disable @typescript-eslint/no-explicit-any */ -import * as fs from 'fs'; -import * as path from 'path'; import { assert } from 'chai'; import * as sinon from 'sinon'; import { v4 as uuid } from 'uuid'; -import { arrayBufferToBase64 } from '../../Crypto'; -import { SenderCertificateClass } from '../../textsecure'; +import * as Bytes from '../../Bytes'; +import { typedArrayToArrayBuffer } from '../../Crypto'; import { SenderCertificateMode } from '../../textsecure/OutgoingMessage'; +import { SignalService as Proto } from '../../protobuf'; import { SenderCertificateService } from '../../services/senderCertificate'; +import SenderCertificate = Proto.SenderCertificate; + describe('SenderCertificateService', () => { const FIFTEEN_MINUTES = 15 * 60 * 1000; - let fakeValidCertificate: SenderCertificateClass; + let fakeValidCertificate: SenderCertificate; let fakeValidCertificateExpiry: number; let fakeServer: any; let fakeWebApi: typeof window.WebAPI; let fakeNavigator: { onLine: boolean }; let fakeWindow: EventTarget; let fakeStorage: any; - let SenderCertificate: typeof SenderCertificateClass; function initializeTestService(): SenderCertificateService { const result = new SenderCertificateService(); result.initialize({ - SenderCertificate, WebAPI: fakeWebApi, navigator: fakeNavigator, onlineEventTarget: fakeWindow, @@ -39,27 +38,6 @@ describe('SenderCertificateService', () => { return result; } - before(done => { - const protoPath = path.join( - __dirname, - '..', - '..', - '..', - 'protos', - 'UnidentifiedDelivery.proto' - ); - fs.readFile(protoPath, 'utf8', (err, proto) => { - if (err) { - done(err); - return; - } - ({ SenderCertificate } = global.window.dcodeIO.ProtoBuf.loadProto( - proto - ).build('signalservice')); - done(); - }); - }); - beforeEach(() => { fakeValidCertificate = new SenderCertificate(); fakeValidCertificateExpiry = Date.now() + 604800000; @@ -67,11 +45,15 @@ describe('SenderCertificateService', () => { certificate.expires = global.window.dcodeIO.Long.fromNumber( fakeValidCertificateExpiry ); - fakeValidCertificate.certificate = certificate.toArrayBuffer(); + fakeValidCertificate.certificate = SenderCertificate.Certificate.encode( + certificate + ).finish(); fakeServer = { getSenderCertificate: sinon.stub().resolves({ - certificate: arrayBufferToBase64(fakeValidCertificate.toArrayBuffer()), + certificate: Bytes.toBase64( + SenderCertificate.encode(fakeValidCertificate).finish() + ), }), }; fakeWebApi = { connect: sinon.stub().returns(fakeServer) }; @@ -133,12 +115,16 @@ describe('SenderCertificateService', () => { assert.deepEqual(await service.get(SenderCertificateMode.WithE164), { expires: fakeValidCertificateExpiry - FIFTEEN_MINUTES, - serialized: fakeValidCertificate.toArrayBuffer(), + serialized: typedArrayToArrayBuffer( + SenderCertificate.encode(fakeValidCertificate).finish() + ), }); sinon.assert.calledWithMatch(fakeStorage.put, 'senderCertificate', { expires: fakeValidCertificateExpiry - FIFTEEN_MINUTES, - serialized: fakeValidCertificate.toArrayBuffer(), + serialized: typedArrayToArrayBuffer( + SenderCertificate.encode(fakeValidCertificate).finish() + ), }); sinon.assert.calledWith(fakeServer.getSenderCertificate, false); @@ -149,12 +135,16 @@ describe('SenderCertificateService', () => { assert.deepEqual(await service.get(SenderCertificateMode.WithoutE164), { expires: fakeValidCertificateExpiry - FIFTEEN_MINUTES, - serialized: fakeValidCertificate.toArrayBuffer(), + serialized: typedArrayToArrayBuffer( + SenderCertificate.encode(fakeValidCertificate).finish() + ), }); sinon.assert.calledWithMatch(fakeStorage.put, 'senderCertificateNoE164', { expires: fakeValidCertificateExpiry - FIFTEEN_MINUTES, - serialized: fakeValidCertificate.toArrayBuffer(), + serialized: typedArrayToArrayBuffer( + SenderCertificate.encode(fakeValidCertificate).finish() + ), }); sinon.assert.calledWith(fakeServer.getSenderCertificate, true); @@ -228,9 +218,13 @@ describe('SenderCertificateService', () => { certificate.expires = global.window.dcodeIO.Long.fromNumber( Date.now() - 1000 ); - expiredCertificate.certificate = certificate.toArrayBuffer(); + expiredCertificate.certificate = SenderCertificate.Certificate.encode( + certificate + ).finish(); fakeServer.getSenderCertificate.resolves({ - certificate: arrayBufferToBase64(expiredCertificate.toArrayBuffer()), + certificate: Bytes.toBase64( + SenderCertificate.encode(expiredCertificate).finish() + ), }); assert.isUndefined(await service.get(SenderCertificateMode.WithE164)); diff --git a/ts/textsecure/AccountManager.ts b/ts/textsecure/AccountManager.ts index f4a8b9405b..21583d640f 100644 --- a/ts/textsecure/AccountManager.ts +++ b/ts/textsecure/AccountManager.ts @@ -10,17 +10,18 @@ import PQueue from 'p-queue'; import EventTarget from './EventTarget'; import { WebAPIType } from './WebAPI'; -import MessageReceiver from './MessageReceiver'; import { KeyPairType, CompatSignedPreKeyType } from './Types.d'; import utils from './Helpers'; import ProvisioningCipher from './ProvisioningCipher'; import WebSocketResource, { IncomingWebSocketRequest, } from './WebsocketResources'; +import * as Bytes from '../Bytes'; import { deriveAccessKey, generateRegistrationId, getRandomBytes, + typedArrayToArrayBuffer, } from '../Crypto'; import { generateKeyPair, @@ -29,13 +30,18 @@ import { } from '../Curve'; import { isMoreRecentThan, isOlderThan } from '../util/timestamp'; import { ourProfileKeyService } from '../services/ourProfileKey'; +import { assert } from '../util/assert'; import { getProvisioningUrl } from '../util/getProvisioningUrl'; +import { SignalService as Proto } from '../protobuf'; const ARCHIVE_AGE = 30 * 24 * 60 * 60 * 1000; const PREKEY_ROTATION_AGE = 24 * 60 * 60 * 1000; const PROFILE_KEY_LENGTH = 32; const SIGNED_KEY_GEN_BATCH_SIZE = 100; +// TODO: remove once we move away from ArrayBuffers +const FIXMEU8 = Uint8Array; + function getIdentifier(id: string | undefined) { if (!id || !id.length) { return id; @@ -100,13 +106,13 @@ export default class AccountManager extends EventTarget { identityKey.pubKey ); - const proto = new window.textsecure.protobuf.DeviceName(); - proto.ephemeralPublic = encrypted.ephemeralPublic; - proto.syntheticIv = encrypted.syntheticIv; - proto.ciphertext = encrypted.ciphertext; + const proto = new Proto.DeviceName(); + proto.ephemeralPublic = new FIXMEU8(encrypted.ephemeralPublic); + proto.syntheticIv = new FIXMEU8(encrypted.syntheticIv); + proto.ciphertext = new FIXMEU8(encrypted.ciphertext); - const arrayBuffer = proto.encode().toArrayBuffer(); - return MessageReceiver.arrayBufferToStringBase64(arrayBuffer); + const bytes = Proto.DeviceName.encode(proto).finish(); + return Bytes.toBase64(bytes); } async decryptDeviceName(base64: string) { @@ -115,12 +121,16 @@ export default class AccountManager extends EventTarget { throw new Error('decryptDeviceName: No identity key pair!'); } - const arrayBuffer = MessageReceiver.stringToArrayBufferBase64(base64); - const proto = window.textsecure.protobuf.DeviceName.decode(arrayBuffer); + const bytes = Bytes.fromBase64(base64); + const proto = Proto.DeviceName.decode(bytes); + assert( + proto.ephemeralPublic && proto.syntheticIv && proto.ciphertext, + 'Missing required fields in DeviceName' + ); const encrypted = { - ephemeralPublic: proto.ephemeralPublic.toArrayBuffer(), - syntheticIv: proto.syntheticIv.toArrayBuffer(), - ciphertext: proto.ciphertext.toArrayBuffer(), + ephemeralPublic: typedArrayToArrayBuffer(proto.ephemeralPublic), + syntheticIv: typedArrayToArrayBuffer(proto.syntheticIv), + ciphertext: typedArrayToArrayBuffer(proto.ciphertext), }; const name = await window.Signal.Crypto.decryptDeviceName( @@ -223,9 +233,7 @@ export default class AccountManager extends EventTarget { request.verb === 'PUT' && request.body ) { - const proto = window.textsecure.protobuf.ProvisioningUuid.decode( - request.body - ); + const proto = Proto.ProvisioningUuid.decode(request.body); const { uuid } = proto; if (!uuid) { throw new Error('registerSecondDevice: expected a UUID'); @@ -243,10 +251,7 @@ export default class AccountManager extends EventTarget { request.verb === 'PUT' && request.body ) { - const envelope = window.textsecure.protobuf.ProvisionEnvelope.decode( - request.body, - 'binary' - ); + const envelope = Proto.ProvisionEnvelope.decode(request.body); request.respond(200, 'OK'); gotProvisionEnvelope = true; wsr.close(); diff --git a/ts/textsecure/Crypto.ts b/ts/textsecure/Crypto.ts index 1b43693ebc..80cdc5b9d1 100644 --- a/ts/textsecure/Crypto.ts +++ b/ts/textsecure/Crypto.ts @@ -4,7 +4,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable no-bitwise */ /* eslint-disable more/no-then */ -import { ByteBufferClass } from '../window.d'; import { decryptAes256CbcPkcsPadding, encryptAes256CbcPkcsPadding, @@ -145,11 +144,9 @@ async function verifyDigest( const Crypto = { // Decrypts message into a raw string async decryptWebsocketMessage( - message: ByteBufferClass, + decodedMessage: ArrayBuffer, signalingKey: ArrayBuffer ): Promise { - const decodedMessage = message.toArrayBuffer(); - if (signalingKey.byteLength !== 52) { throw new Error('Got invalid length signalingKey'); } diff --git a/ts/textsecure/MessageReceiver.ts b/ts/textsecure/MessageReceiver.ts index d79a3044d4..711d72dc88 100644 --- a/ts/textsecure/MessageReceiver.ts +++ b/ts/textsecure/MessageReceiver.ts @@ -521,11 +521,11 @@ class MessageReceiverInner extends EventTarget { if (headers.includes('X-Signal-Key: true')) { plaintext = await Crypto.decryptWebsocketMessage( - request.body, + typedArrayToArrayBuffer(request.body), this.signalingKey ); } else { - plaintext = request.body.toArrayBuffer(); + plaintext = typedArrayToArrayBuffer(request.body); } try { @@ -583,7 +583,7 @@ class MessageReceiverInner extends EventTarget { } calculateMessageAge( - headers: Array, + headers: ReadonlyArray, serverTimestamp?: number ): number { let messageAgeSec = 0; // Default to 0 in case of unreliable parameters. diff --git a/ts/textsecure/ProvisioningCipher.ts b/ts/textsecure/ProvisioningCipher.ts index 6677298b9e..8cac39bb25 100644 --- a/ts/textsecure/ProvisioningCipher.ts +++ b/ts/textsecure/ProvisioningCipher.ts @@ -5,14 +5,19 @@ /* eslint-disable max-classes-per-file */ import { KeyPairType } from './Types.d'; -import { ProvisionEnvelopeClass } from '../textsecure.d'; import { decryptAes256CbcPkcsPadding, deriveSecrets, bytesFromString, verifyHmacSha256, + typedArrayToArrayBuffer, } from '../Crypto'; import { calculateAgreement, createKeyPair, generateKeyPair } from '../Curve'; +import { SignalService as Proto } from '../protobuf'; +import { assert } from '../util/assert'; + +// TODO: remove once we move away from ArrayBuffers +const FIXMEU8 = Uint8Array; type ProvisionDecryptResult = { identityKeyPair: KeyPairType; @@ -28,10 +33,14 @@ class ProvisioningCipherInner { keyPair?: KeyPairType; async decrypt( - provisionEnvelope: ProvisionEnvelopeClass + provisionEnvelope: Proto.ProvisionEnvelope ): Promise { - const masterEphemeral = provisionEnvelope.publicKey.toArrayBuffer(); - const message = provisionEnvelope.body.toArrayBuffer(); + assert( + provisionEnvelope.publicKey && provisionEnvelope.body, + 'Missing required fields in ProvisionEnvelope' + ); + const masterEphemeral = provisionEnvelope.publicKey; + const message = provisionEnvelope.body; if (new Uint8Array(message)[0] !== 1) { throw new Error('Bad version number on ProvisioningMessage'); } @@ -45,25 +54,34 @@ class ProvisioningCipherInner { throw new Error('ProvisioningCipher.decrypt: No keypair!'); } - const ecRes = calculateAgreement(masterEphemeral, this.keyPair.privKey); + const ecRes = calculateAgreement( + typedArrayToArrayBuffer(masterEphemeral), + this.keyPair.privKey + ); const keys = deriveSecrets( ecRes, new ArrayBuffer(32), bytesFromString('TextSecure Provisioning Message') ); - await verifyHmacSha256(ivAndCiphertext, keys[1], mac, 32); + await verifyHmacSha256( + typedArrayToArrayBuffer(ivAndCiphertext), + keys[1], + typedArrayToArrayBuffer(mac), + 32 + ); const plaintext = await decryptAes256CbcPkcsPadding( keys[0], - ciphertext, - iv + typedArrayToArrayBuffer(ciphertext), + typedArrayToArrayBuffer(iv) ); - const provisionMessage = window.textsecure.protobuf.ProvisionMessage.decode( - plaintext + const provisionMessage = Proto.ProvisionMessage.decode( + new FIXMEU8(plaintext) ); - const privKey = provisionMessage.identityKeyPrivate.toArrayBuffer(); + const privKey = provisionMessage.identityKeyPrivate; + assert(privKey, 'Missing identityKeyPrivate in ProvisionMessage'); - const keyPair = createKeyPair(privKey); + const keyPair = createKeyPair(typedArrayToArrayBuffer(privKey)); window.normalizeUuids( provisionMessage, ['uuid'], @@ -79,7 +97,7 @@ class ProvisioningCipherInner { readReceipts: provisionMessage.readReceipts, }; if (provisionMessage.profileKey) { - ret.profileKey = provisionMessage.profileKey.toArrayBuffer(); + ret.profileKey = typedArrayToArrayBuffer(provisionMessage.profileKey); } return ret; } @@ -106,7 +124,7 @@ export default class ProvisioningCipher { } decrypt: ( - provisionEnvelope: ProvisionEnvelopeClass + provisionEnvelope: Proto.ProvisionEnvelope ) => Promise; getPublicKey: () => Promise; diff --git a/ts/textsecure/WebsocketResources.ts b/ts/textsecure/WebsocketResources.ts index 3961d6f130..010cfe4f02 100644 --- a/ts/textsecure/WebsocketResources.ts +++ b/ts/textsecure/WebsocketResources.ts @@ -1,9 +1,6 @@ // Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -/* eslint-disable no-param-reassign */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/ban-types */ /* eslint-disable max-classes-per-file */ /* * WebSocket-Resources @@ -29,228 +26,152 @@ import { connection as WebSocket, IMessage } from 'websocket'; -import { ByteBufferClass } from '../window.d'; -import { typedArrayToArrayBuffer as toArrayBuffer } from '../Crypto'; - import EventTarget from './EventTarget'; +import { dropNull } from '../util/dropNull'; import { isOlderThan } from '../util/timestamp'; +import { strictAssert } from '../util/assert'; +import { normalizeNumber } from '../util/normalizeNumber'; +import { SignalService as Proto } from '../protobuf'; -class Request { - verb: string; - - path: string; - - headers: Array; - - body: ByteBufferClass | null; - - success: Function; - - error: Function; - - id: number; - - response?: any; - - constructor(options: any) { - this.verb = options.verb || options.type; - this.path = options.path || options.url; - this.headers = options.headers; - this.body = options.body || options.data; - this.success = options.success; - this.error = options.error; - this.id = options.id; - - if (this.id === undefined) { - const bits = new Uint32Array(2); - window.crypto.getRandomValues(bits); - this.id = window.dcodeIO.Long.fromBits(bits[0], bits[1], true); - } - - if (this.body === undefined) { - this.body = null; - } - } -} +type Callback = ( + message: string, + status: number, + request: OutgoingWebSocketRequest +) => void; export class IncomingWebSocketRequest { - verb: string; + private readonly id: Long | number; - path: string; + public readonly verb: string; - body: ByteBufferClass | null; + public readonly path: string; - headers: Array; + public readonly body: Uint8Array | undefined; - respond: (status: number, message: string) => void; + public readonly headers: ReadonlyArray; - constructor(options: unknown) { - const request = new Request(options); - const { socket } = options as { socket: WebSocket }; + constructor( + request: Proto.IWebSocketRequestMessage, + private readonly socket: WebSocket + ) { + strictAssert(request.id, 'request without id'); + strictAssert(request.verb, 'request without verb'); + strictAssert(request.path, 'request without path'); + this.id = request.id; this.verb = request.verb; this.path = request.path; - this.body = request.body; - this.headers = request.headers; + this.body = dropNull(request.body); + this.headers = request.headers || []; + this.socket = socket; + } - this.respond = (status, message) => { - const ab = new window.textsecure.protobuf.WebSocketMessage({ - type: window.textsecure.protobuf.WebSocketMessage.Type.RESPONSE, - response: { id: request.id, message, status }, - }) - .encode() - .toArrayBuffer(); - socket.sendBytes(Buffer.from(ab)); - }; + public respond(status: number, message: string): void { + const bytes = Proto.WebSocketMessage.encode({ + type: Proto.WebSocketMessage.Type.RESPONSE, + response: { id: this.id, message, status }, + }).finish(); + + this.socket.sendBytes(Buffer.from(bytes)); } } -const outgoing: { - [id: number]: Request; -} = {}; -class OutgoingWebSocketRequest { - constructor(options: any, socket: WebSocket) { - const request = new Request(options); - outgoing[request.id] = request; - const ab = new window.textsecure.protobuf.WebSocketMessage({ - type: window.textsecure.protobuf.WebSocketMessage.Type.REQUEST, +export type OutgoingWebSocketRequestOptions = Readonly<{ + verb: string; + path: string; + body?: Uint8Array; + headers?: ReadonlyArray; + error?: Callback; + success?: Callback; +}>; + +export class OutgoingWebSocketRequest { + public readonly error: Callback | undefined; + + public readonly success: Callback | undefined; + + public response: Proto.IWebSocketResponseMessage | undefined; + + constructor( + id: number, + options: OutgoingWebSocketRequestOptions, + socket: WebSocket + ) { + this.error = options.error; + this.success = options.success; + + const bytes = Proto.WebSocketMessage.encode({ + type: Proto.WebSocketMessage.Type.REQUEST, request: { - verb: request.verb, - path: request.path, - body: request.body, - headers: request.headers, - id: request.id, + verb: options.verb, + path: options.path, + body: options.body, + headers: options.headers ? options.headers.slice() : undefined, + id, }, - }) - .encode() - .toArrayBuffer(); - socket.sendBytes(Buffer.from(ab)); + }).finish(); + socket.sendBytes(Buffer.from(bytes)); } } +export type WebSocketResourceOptions = { + handleRequest?: (request: IncomingWebSocketRequest) => void; + keepalive?: KeepAliveOptionsType | true; +}; + export default class WebSocketResource extends EventTarget { - closed?: boolean; + private outgoingId = 1; - close: (code?: number, reason?: string) => void; + private closed?: boolean; - sendRequest: (options: any) => OutgoingWebSocketRequest; + private readonly outgoingMap = new Map(); - keepalive?: KeepAlive; + private readonly boundOnMessage: (message: IMessage) => void; - constructor(socket: WebSocket, opts: any = {}) { + // Public for tests + public readonly keepalive?: KeepAlive; + + constructor( + private readonly socket: WebSocket, + private readonly options: WebSocketResourceOptions = {} + ) { super(); - let { handleRequest } = opts; - if (typeof handleRequest !== 'function') { - handleRequest = (request: IncomingWebSocketRequest) => { - request.respond(404, 'Not found'); - }; - } - this.sendRequest = options => new OutgoingWebSocketRequest(options, socket); + this.boundOnMessage = this.onMessage.bind(this); - // eslint-disable-next-line no-param-reassign - const onMessage = ({ type, binaryData }: IMessage): void => { - if (type !== 'binary' || !binaryData) { - throw new Error(`Unsupported websocket message type: ${type}`); - } + socket.on('message', this.boundOnMessage); - const message = window.textsecure.protobuf.WebSocketMessage.decode( - toArrayBuffer(binaryData) + if (options.keepalive) { + const keepalive = new KeepAlive( + this, + options.keepalive === true ? {} : options.keepalive ); - if ( - message.type === - window.textsecure.protobuf.WebSocketMessage.Type.REQUEST && - message.request - ) { - handleRequest( - new IncomingWebSocketRequest({ - verb: message.request.verb, - path: message.request.path, - body: message.request.body, - headers: message.request.headers, - id: message.request.id, - socket, - }) - ); - } else if ( - message.type === - window.textsecure.protobuf.WebSocketMessage.Type.RESPONSE && - message.response - ) { - const { response } = message; - const request = outgoing[response.id]; - if (request) { - request.response = response; - let callback = request.error; - if ( - response.status && - response.status >= 200 && - response.status < 300 - ) { - callback = request.success; - } + this.keepalive = keepalive; - if (typeof callback === 'function') { - callback(response.message, response.status, request); - } - } else { - throw new Error( - `Received response for unknown request ${message.response.id}` - ); - } - } - }; - socket.on('message', onMessage); - - if (opts.keepalive) { - this.keepalive = new KeepAlive(this, { - path: opts.keepalive.path, - disconnect: opts.keepalive.disconnect, - }); - const resetKeepAliveTimer = this.keepalive.reset.bind(this.keepalive); - - this.keepalive.reset(); - - socket.on('message', resetKeepAliveTimer); - socket.on('close', this.keepalive.stop.bind(this.keepalive)); + keepalive.reset(); + socket.on('message', () => keepalive.reset()); + socket.on('close', () => keepalive.stop()); } socket.on('close', () => { this.closed = true; }); + } - this.close = (code = 3000, reason) => { - if (this.closed) { - return; - } + public sendRequest( + options: OutgoingWebSocketRequestOptions + ): OutgoingWebSocketRequest { + const id = this.outgoingId; + strictAssert(!this.outgoingMap.has(id), 'Duplicate outgoing request'); - window.log.info('WebSocketResource.close()'); - if (this.keepalive) { - this.keepalive.stop(); - } + // eslint-disable-next-line no-bitwise + this.outgoingId = Math.max(1, (this.outgoingId + 1) & 0x7fffffff); - socket.close(code, reason); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - socket.removeListener('message', onMessage); + const outgoing = new OutgoingWebSocketRequest(id, options, this.socket); + this.outgoingMap.set(id, outgoing); - // On linux the socket can wait a long time to emit its close event if we've - // lost the internet connection. On the order of minutes. This speeds that - // process up. - setTimeout(() => { - if (this.closed) { - return; - } - this.closed = true; - - window.log.warn('Dispatching our own socket close event'); - const ev = new Event('close'); - ev.code = code; - ev.reason = reason; - this.dispatchEvent(ev); - }, 5000); - }; + return outgoing; } public forceKeepAlive(): void { @@ -259,9 +180,83 @@ export default class WebSocketResource extends EventTarget { } this.keepalive.send(); } + + public close(code = 3000, reason?: string): void { + if (this.closed) { + return; + } + + window.log.info('WebSocketResource.close()'); + if (this.keepalive) { + this.keepalive.stop(); + } + + this.socket.close(code, reason); + + this.socket.removeListener('message', this.boundOnMessage); + + // On linux the socket can wait a long time to emit its close event if we've + // lost the internet connection. On the order of minutes. This speeds that + // process up. + setTimeout(() => { + if (this.closed) { + return; + } + + window.log.warn('Dispatching our own socket close event'); + const ev = new Event('close'); + ev.code = code; + ev.reason = reason; + this.dispatchEvent(ev); + }, 5000); + } + + private onMessage({ type, binaryData }: IMessage): void { + if (type !== 'binary' || !binaryData) { + throw new Error(`Unsupported websocket message type: ${type}`); + } + + const message = Proto.WebSocketMessage.decode(binaryData); + if ( + message.type === Proto.WebSocketMessage.Type.REQUEST && + message.request + ) { + const handleRequest = + this.options.handleRequest || + (request => request.respond(404, 'Not found')); + handleRequest(new IncomingWebSocketRequest(message.request, this.socket)); + } else if ( + message.type === Proto.WebSocketMessage.Type.RESPONSE && + message.response + ) { + const { response } = message; + strictAssert(response.id, 'response without id'); + + const responseId = normalizeNumber(response.id); + const request = this.outgoingMap.get(responseId); + this.outgoingMap.delete(responseId); + + if (!request) { + throw new Error(`Received response for unknown request ${responseId}`); + } + + request.response = dropNull(response); + + let callback = request.error; + + const status = response.status ?? -1; + if (status >= 200 && status < 300) { + callback = request.success; + } + + if (typeof callback === 'function') { + callback(response.message ?? '', status, request); + } + } + } } -type KeepAliveOptionsType = { +export type KeepAliveOptionsType = { path?: string; disconnect?: boolean; }; diff --git a/ts/util/normalizeNumber.ts b/ts/util/normalizeNumber.ts new file mode 100644 index 0000000000..643dbdd867 --- /dev/null +++ b/ts/util/normalizeNumber.ts @@ -0,0 +1,17 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +export function normalizeNumber(value: number | Long): number; +export function normalizeNumber(value?: number | Long): number | undefined; + +export function normalizeNumber(value?: number | Long): number | undefined { + if (value === undefined) { + return undefined; + } + + if (typeof value === 'number') { + return value; + } + + return value.toNumber(); +} diff --git a/yarn.lock b/yarn.lock index 24dea81176..86cd625a4e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11596,7 +11596,7 @@ loglevel@^1.6.8: resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197" integrity sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw== -long@^4.0.0: +long@4.0.0, long@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28"