More protobufjs use

This commit is contained in:
Fedor Indutny 2021-07-02 12:21:24 -07:00 committed by GitHub
parent 1fa0e6c8c1
commit 299fe2af36
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 646 additions and 447 deletions

View file

@ -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

View file

@ -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",

View file

@ -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
);

View file

@ -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<void> {
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<void> {
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<void> {
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<void> {
);
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<void> {
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<void> {
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<void> {
}
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:

View file

@ -98,7 +98,7 @@ export async function joinViaLink(hash: string): Promise<void> {
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

View file

@ -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<void> {
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);

View file

@ -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<MessageAttributesType> {
);
}
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<MessageAttributesType> {
}
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<MessageAttributesType> {
_i18n: unknown,
placeholders: Array<string>
) => 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<MessageAttributesType> {
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<MessageAttributesType> {
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<MessageAttributesType> {
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<MessageAttributesType> {
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<MessageAttributesType> {
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.

View file

@ -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 };

View file

@ -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}`,

View file

@ -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<undefined | SerializedCertificateType>
@ -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<undefined | SerializedCertificateType> {
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);

View file

@ -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);
});
});

View file

@ -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(

View file

@ -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));

View file

@ -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();

View file

@ -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<ArrayBuffer> {
const decodedMessage = message.toArrayBuffer();
if (signalingKey.byteLength !== 52) {
throw new Error('Got invalid length signalingKey');
}

View file

@ -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<string>,
headers: ReadonlyArray<string>,
serverTimestamp?: number
): number {
let messageAgeSec = 0; // Default to 0 in case of unreliable parameters.

View file

@ -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<ProvisionDecryptResult> {
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<ProvisionDecryptResult>;
getPublicKey: () => Promise<ArrayBuffer>;

View file

@ -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<string>;
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<string>;
public readonly body: Uint8Array | undefined;
respond: (status: number, message: string) => void;
public readonly headers: ReadonlyArray<string>;
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<string>;
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<number, OutgoingWebSocketRequest>();
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;
};

View file

@ -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();
}

View file

@ -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"