Fetch sender certificates on-demand

This commit is contained in:
Evan Hahn 2021-04-08 11:24:21 -05:00 committed by Josh Perez
parent 6ff55914f0
commit a82fa86176
19 changed files with 752 additions and 298 deletions

View file

@ -722,19 +722,19 @@ export class ConversationController {
return null; return null;
} }
prepareForSend( async prepareForSend(
id: string | undefined, id: string | undefined,
options?: WhatIsThis options?: WhatIsThis
): { ): Promise<{
wrap: ( wrap: (
promise: Promise<CallbackResultType | void | null> promise: Promise<CallbackResultType | void | null>
) => Promise<CallbackResultType | void | null>; ) => Promise<CallbackResultType | void | null>;
sendOptions: SendOptionsType | undefined; sendOptions: SendOptionsType | undefined;
} { }> {
// id is any valid conversation identifier // id is any valid conversation identifier
const conversation = this.get(id); const conversation = this.get(id);
const sendOptions = conversation const sendOptions = conversation
? conversation.getSendOptions(options) ? await conversation.getSendOptions(options)
: undefined; : undefined;
const wrap = conversation const wrap = conversation
? conversation.wrapSend.bind(conversation) ? conversation.wrapSend.bind(conversation)

View file

@ -7,8 +7,7 @@ import { WhatIsThis } from './window.d';
import { getTitleBarVisibility, TitleBarVisibility } from './types/Settings'; import { getTitleBarVisibility, TitleBarVisibility } from './types/Settings';
import { isWindowDragElement } from './util/isWindowDragElement'; import { isWindowDragElement } from './util/isWindowDragElement';
import { assert } from './util/assert'; import { assert } from './util/assert';
import * as refreshSenderCertificate from './refreshSenderCertificate'; import { senderCertificateService } from './services/senderCertificate';
import { SenderCertificateMode } from './metadata/SecretSessionCipher';
import { routineProfileRefresh } from './routineProfileRefresh'; import { routineProfileRefresh } from './routineProfileRefresh';
import { isMoreRecentThan, isOlderThan } from './util/timestamp'; import { isMoreRecentThan, isOlderThan } from './util/timestamp';
import { isValidReactionEmoji } from './reactions/isValidReactionEmoji'; import { isValidReactionEmoji } from './reactions/isValidReactionEmoji';
@ -30,6 +29,17 @@ export async function startApp(): Promise<void> {
err && err.stack ? err.stack : err err && err.stack ? err.stack : err
); );
} }
window.textsecure.protobuf.onLoad(() => {
senderCertificateService.initialize({
WebAPI: window.WebAPI,
navigator,
onlineEventTarget: window,
SenderCertificate: window.textsecure.protobuf.SenderCertificate,
storage: window.storage,
});
});
const eventHandlerQueue = new window.PQueue({ const eventHandlerQueue = new window.PQueue({
concurrency: 1, concurrency: 1,
timeout: 1000 * 60 * 2, timeout: 1000 * 60 * 2,
@ -70,7 +80,7 @@ export async function startApp(): Promise<void> {
const { const {
wrap, wrap,
sendOptions, sendOptions,
} = window.ConversationController.prepareForSend(c.get('id')); } = await window.ConversationController.prepareForSend(c.get('id'));
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
await wrap( await wrap(
window.textsecure.messaging.sendDeliveryReceipt( window.textsecure.messaging.sendDeliveryReceipt(
@ -1592,11 +1602,14 @@ export async function startApp(): Promise<void> {
); );
} }
window.getSyncRequest = () => window.getSyncRequest = () => {
new window.textsecure.SyncRequest( const syncRequest = new window.textsecure.SyncRequest(
window.textsecure.messaging, window.textsecure.messaging,
messageReceiver messageReceiver
); );
syncRequest.start();
return syncRequest;
};
let disconnectTimer: WhatIsThis | null = null; let disconnectTimer: WhatIsThis | null = null;
let reconnectTimer: WhatIsThis | null = null; let reconnectTimer: WhatIsThis | null = null;
@ -1948,10 +1961,7 @@ export async function startApp(): Promise<void> {
); );
onChangeTheme(); onChangeTheme();
} }
const syncRequest = new window.textsecure.SyncRequest( const syncRequest = window.getSyncRequest();
window.textsecure.messaging,
messageReceiver
);
window.Whisper.events.trigger('contactsync:begin'); window.Whisper.events.trigger('contactsync:begin');
syncRequest.addEventListener('success', () => { syncRequest.addEventListener('success', () => {
window.log.info('sync successful'); window.log.info('sync successful');
@ -1969,7 +1979,7 @@ export async function startApp(): Promise<void> {
const { const {
wrap, wrap,
sendOptions, sendOptions,
} = window.ConversationController.prepareForSend(ourId, { } = await window.ConversationController.prepareForSend(ourId, {
syncMessage: true, syncMessage: true,
}); });
@ -2090,17 +2100,6 @@ export async function startApp(): Promise<void> {
newVersion newVersion
); );
[SenderCertificateMode.WithE164, SenderCertificateMode.WithoutE164].forEach(
mode => {
refreshSenderCertificate.initialize({
events: window.Whisper.events,
storage: window.storage,
mode,
navigator,
});
}
);
window.Whisper.deliveryReceiptQueue.start(); window.Whisper.deliveryReceiptQueue.start();
window.Whisper.Notifications.enable(); window.Whisper.Notifications.enable();

View file

@ -1241,7 +1241,7 @@ export async function modifyGroupV2({
? window.storage.get('profileKey') ? window.storage.get('profileKey')
: undefined; : undefined;
const sendOptions = conversation.getSendOptions(); const sendOptions = await conversation.getSendOptions();
const timestamp = Date.now(); const timestamp = Date.now();
const promise = conversation.wrapSend( const promise = conversation.wrapSend(

View file

@ -3,6 +3,7 @@
/* eslint-disable class-methods-use-this */ /* eslint-disable class-methods-use-this */
import * as z from 'zod';
import * as CiphertextMessage from './CiphertextMessage'; import * as CiphertextMessage from './CiphertextMessage';
import { import {
bytesFromString, bytesFromString,
@ -44,9 +45,16 @@ export const enum SenderCertificateMode {
WithoutE164, WithoutE164,
} }
export type SerializedCertificateType = { export const serializedCertificateSchema = z
serialized: ArrayBuffer; .object({
}; expires: z.number().optional(),
serialized: z.instanceof(ArrayBuffer),
})
.nonstrict();
export type SerializedCertificateType = z.infer<
typeof serializedCertificateSchema
>;
type ServerCertificateType = { type ServerCertificateType = {
id: number; id: number;

View file

@ -54,7 +54,11 @@ import {
PhoneNumberSharingMode, PhoneNumberSharingMode,
parsePhoneNumberSharingMode, parsePhoneNumberSharingMode,
} from '../util/phoneNumberSharingMode'; } from '../util/phoneNumberSharingMode';
import { SerializedCertificateType } from '../metadata/SecretSessionCipher'; import {
SenderCertificateMode,
SerializedCertificateType,
} from '../metadata/SecretSessionCipher';
import { senderCertificateService } from '../services/senderCertificate';
/* eslint-disable more/no-then */ /* eslint-disable more/no-then */
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@ -1090,7 +1094,7 @@ export class ConversationModel extends window.Backbone.Model<
return undefined; return undefined;
} }
sendTypingMessage(isTyping: boolean): void { async sendTypingMessage(isTyping: boolean): Promise<void> {
if (!window.textsecure.messaging) { if (!window.textsecure.messaging) {
return; return;
} }
@ -1109,7 +1113,7 @@ export class ConversationModel extends window.Backbone.Model<
return; return;
} }
const sendOptions = this.getSendOptions(); const sendOptions = await this.getSendOptions();
this.wrapSend( this.wrapSend(
window.textsecure.messaging.sendTypingMessage( window.textsecure.messaging.sendTypingMessage(
{ {
@ -1912,7 +1916,10 @@ export class ConversationModel extends window.Backbone.Model<
await this.applyMessageRequestResponse(response); await this.applyMessageRequestResponse(response);
const { ourNumber, ourUuid } = this; const { ourNumber, ourUuid } = this;
const { wrap, sendOptions } = window.ConversationController.prepareForSend( const {
wrap,
sendOptions,
} = await window.ConversationController.prepareForSend(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
ourNumber || ourUuid!, ourNumber || ourUuid!,
{ {
@ -2105,12 +2112,12 @@ export class ConversationModel extends window.Backbone.Model<
// Because syncVerification sends a (null) message to the target of the verify and // Because syncVerification sends a (null) message to the target of the verify and
// a sync message to our own devices, we need to send the accessKeys down for both // a sync message to our own devices, we need to send the accessKeys down for both
// contacts. So we merge their sendOptions. // contacts. So we merge their sendOptions.
const { sendOptions } = window.ConversationController.prepareForSend( const { sendOptions } = await window.ConversationController.prepareForSend(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.ourNumber || this.ourUuid!, this.ourNumber || this.ourUuid!,
{ syncMessage: true } { syncMessage: true }
); );
const contactSendOptions = this.getSendOptions(); const contactSendOptions = await this.getSendOptions();
const options = { ...sendOptions, ...contactSendOptions }; const options = { ...sendOptions, ...contactSendOptions };
const promise = window.textsecure.storage.protocol.loadIdentityKey(e164); const promise = window.textsecure.storage.protocol.loadIdentityKey(e164);
@ -3107,7 +3114,7 @@ export class ConversationModel extends window.Backbone.Model<
throw new Error('Cannot send DOE while offline!'); throw new Error('Cannot send DOE while offline!');
} }
const options = this.getSendOptions(); const options = await this.getSendOptions();
const promise = (() => { const promise = (() => {
if (this.isPrivate()) { if (this.isPrivate()) {
@ -3239,7 +3246,7 @@ export class ConversationModel extends window.Backbone.Model<
return message.sendSyncMessageOnly(dataMessage); return message.sendSyncMessageOnly(dataMessage);
} }
const options = this.getSendOptions(); const options = await this.getSendOptions();
const promise = (() => { const promise = (() => {
if (this.isPrivate()) { if (this.isPrivate()) {
@ -3302,7 +3309,7 @@ export class ConversationModel extends window.Backbone.Model<
await window.textsecure.messaging.sendProfileKeyUpdate( await window.textsecure.messaging.sendProfileKeyUpdate(
profileKey, profileKey,
recipients, recipients,
this.getSendOptions(), await this.getSendOptions(),
this.get('groupId') this.get('groupId')
); );
} }
@ -3435,7 +3442,7 @@ export class ConversationModel extends window.Backbone.Model<
} }
const conversationType = this.get('type'); const conversationType = this.get('type');
const options = this.getSendOptions(); const options = await this.getSendOptions();
let promise; let promise;
if (conversationType === Message.GROUP) { if (conversationType === Message.GROUP) {
@ -3555,17 +3562,17 @@ export class ConversationModel extends window.Backbone.Model<
); );
} }
getSendOptions(options = {}): SendOptionsType { async getSendOptions(options = {}): Promise<SendOptionsType> {
const sendMetadata = this.getSendMetadata(options); const sendMetadata = await this.getSendMetadata(options);
return { return {
sendMetadata, sendMetadata,
}; };
} }
getSendMetadata( async getSendMetadata(
options: { syncMessage?: string; disableMeCheck?: boolean } = {} options: { syncMessage?: string; disableMeCheck?: boolean } = {}
): SendMetadataType | undefined { ): Promise<SendMetadataType | undefined> {
const { syncMessage, disableMeCheck } = options; const { syncMessage, disableMeCheck } = options;
// START: this code has an Expiration date of ~2018/11/21 // START: this code has an Expiration date of ~2018/11/21
@ -3580,11 +3587,19 @@ export class ConversationModel extends window.Backbone.Model<
// END // END
if (!this.isPrivate()) { if (!this.isPrivate()) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion assert(
const infoArray = this.contactCollection!.map(conversation => this.contactCollection,
conversation.getSendMetadata(options) 'getSendMetadata: expected contactCollection to be defined'
); );
return Object.assign({}, ...infoArray); const result: SendMetadataType = {};
await Promise.all(
this.contactCollection.map(async conversation => {
const sendMetadata =
(await conversation.getSendMetadata(options)) || {};
Object.assign(result, sendMetadata);
})
);
return result;
} }
const accessKey = this.get('accessKey'); const accessKey = this.get('accessKey');
@ -3598,7 +3613,7 @@ export class ConversationModel extends window.Backbone.Model<
const e164 = this.get('e164'); const e164 = this.get('e164');
const uuid = this.get('uuid'); const uuid = this.get('uuid');
const senderCertificate = this.getSenderCertificateForDirectConversation(); const senderCertificate = await this.getSenderCertificateForDirectConversation();
// If we've never fetched user's profile, we default to what we have // If we've never fetched user's profile, we default to what we have
if (sealedSender === SEALED_SENDER.UNKNOWN) { if (sealedSender === SEALED_SENDER.UNKNOWN) {
@ -3630,9 +3645,9 @@ export class ConversationModel extends window.Backbone.Model<
}; };
} }
private getSenderCertificateForDirectConversation(): private getSenderCertificateForDirectConversation(): Promise<
| undefined undefined | SerializedCertificateType
| SerializedCertificateType { > {
if (!this.isPrivate()) { if (!this.isPrivate()) {
throw new Error( throw new Error(
'getSenderCertificateForDirectConversation should only be called for direct conversations' 'getSenderCertificateForDirectConversation should only be called for direct conversations'
@ -3643,33 +3658,26 @@ export class ConversationModel extends window.Backbone.Model<
window.storage.get('phoneNumberSharingMode') window.storage.get('phoneNumberSharingMode')
); );
let storageKey: 'senderCertificate' | 'senderCertificateNoE164'; let certificateMode: SenderCertificateMode;
switch (phoneNumberSharingMode) { switch (phoneNumberSharingMode) {
case PhoneNumberSharingMode.Everybody: case PhoneNumberSharingMode.Everybody:
storageKey = 'senderCertificate'; certificateMode = SenderCertificateMode.WithE164;
break; break;
case PhoneNumberSharingMode.ContactsOnly: { case PhoneNumberSharingMode.ContactsOnly: {
const isInSystemContacts = Boolean(this.get('name')); const isInSystemContacts = Boolean(this.get('name'));
storageKey = isInSystemContacts certificateMode = isInSystemContacts
? 'senderCertificate' ? SenderCertificateMode.WithE164
: 'senderCertificateNoE164'; : SenderCertificateMode.WithoutE164;
break; break;
} }
case PhoneNumberSharingMode.Nobody: case PhoneNumberSharingMode.Nobody:
storageKey = 'senderCertificateNoE164'; certificateMode = SenderCertificateMode.WithoutE164;
break; break;
default: default:
throw missingCaseError(phoneNumberSharingMode); throw missingCaseError(phoneNumberSharingMode);
} }
const result = window.storage.get<SerializedCertificateType>(storageKey); return senderCertificateService.get(certificateMode);
assert(
result,
`getSenderCertificateForDirectConversation: couldn't find a certificate stored in ${JSON.stringify(
storageKey
)}. Returning undefined`
);
return result;
} }
// Is this someone who is a contact, or are we sharing our profile with them? // Is this someone who is a contact, or are we sharing our profile with them?
@ -4055,7 +4063,7 @@ export class ConversationModel extends window.Backbone.Model<
if (this.get('profileSharing')) { if (this.get('profileSharing')) {
profileKey = window.storage.get('profileKey'); profileKey = window.storage.get('profileKey');
} }
const sendOptions = this.getSendOptions(); const sendOptions = await this.getSendOptions();
let promise; let promise;
if (this.isMe()) { if (this.isMe()) {
@ -4174,7 +4182,7 @@ export class ConversationModel extends window.Backbone.Model<
const message = window.MessageController.register(model.id, model); const message = window.MessageController.register(model.id, model);
this.addSingleMessage(message); this.addSingleMessage(message);
const options = this.getSendOptions(); const options = await this.getSendOptions();
message.send( message.send(
this.wrapSend( this.wrapSend(
// TODO: DESKTOP-724 // TODO: DESKTOP-724
@ -4227,7 +4235,7 @@ export class ConversationModel extends window.Backbone.Model<
const message = window.MessageController.register(model.id, model); const message = window.MessageController.register(model.id, model);
this.addSingleMessage(message); this.addSingleMessage(message);
const options = this.getSendOptions(); const options = await this.getSendOptions();
message.send( message.send(
this.wrapSend( this.wrapSend(
window.textsecure.messaging.leaveGroup( window.textsecure.messaging.leaveGroup(
@ -4299,7 +4307,7 @@ export class ConversationModel extends window.Backbone.Model<
// to a contact, we need accessKeys for both. // to a contact, we need accessKeys for both.
const { const {
sendOptions, sendOptions,
} = window.ConversationController.prepareForSend( } = await window.ConversationController.prepareForSend(
window.ConversationController.getOurConversationId(), window.ConversationController.getOurConversationId(),
{ syncMessage: true } { syncMessage: true }
); );
@ -4314,7 +4322,7 @@ export class ConversationModel extends window.Backbone.Model<
// Only send read receipts for accepted conversations // Only send read receipts for accepted conversations
if (window.storage.get('read-receipt-setting') && this.getAccepted()) { if (window.storage.get('read-receipt-setting') && this.getAccepted()) {
window.log.info(`Sending ${items.length} read receipts`); window.log.info(`Sending ${items.length} read receipts`);
const convoSendOptions = this.getSendOptions(); const convoSendOptions = await this.getSendOptions();
const receiptsBySender = window._.groupBy(items, 'senderId'); const receiptsBySender = window._.groupBy(items, 'senderId');
await Promise.all( await Promise.all(
@ -4452,7 +4460,8 @@ export class ConversationModel extends window.Backbone.Model<
)); ));
} }
const sendMetadata = c.getSendMetadata({ disableMeCheck: true }) || {}; const sendMetadata =
(await c.getSendMetadata({ disableMeCheck: true })) || {};
const getInfo = const getInfo =
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
sendMetadata[c.get('uuid')!] || sendMetadata[c.get('e164')!] || {}; sendMetadata[c.get('uuid')!] || sendMetadata[c.get('e164')!] || {};

View file

@ -1703,9 +1703,12 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
const { const {
wrap, wrap,
sendOptions, sendOptions,
} = window.ConversationController.prepareForSend(ourNumber || ourUuid, { } = await window.ConversationController.prepareForSend(
syncMessage: true, ourNumber || ourUuid,
}); {
syncMessage: true,
}
);
await wrap( await wrap(
window.textsecure.messaging.syncViewOnceOpen( window.textsecure.messaging.syncViewOnceOpen(
@ -2119,7 +2122,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
} }
let promise; let promise;
const options = conversation.getSendOptions(); const options = await conversation.getSendOptions();
if (conversation.isPrivate()) { if (conversation.isPrivate()) {
const [identifier] = recipients; const [identifier] = recipients;
@ -2312,9 +2315,10 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
return this.sendSyncMessageOnly(dataMessage); return this.sendSyncMessageOnly(dataMessage);
} }
const { wrap, sendOptions } = window.ConversationController.prepareForSend( const {
identifier wrap,
); sendOptions,
} = await window.ConversationController.prepareForSend(identifier);
const promise = window.textsecure.messaging.sendMessageToIdentifier( const promise = window.textsecure.messaging.sendMessageToIdentifier(
identifier, identifier,
body, body,
@ -2533,7 +2537,10 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
async sendSyncMessage(): Promise<WhatIsThis> { async sendSyncMessage(): Promise<WhatIsThis> {
const ourNumber = window.textsecure.storage.user.getNumber(); const ourNumber = window.textsecure.storage.user.getNumber();
const ourUuid = window.textsecure.storage.user.getUuid(); const ourUuid = window.textsecure.storage.user.getUuid();
const { wrap, sendOptions } = window.ConversationController.prepareForSend( const {
wrap,
sendOptions,
} = await window.ConversationController.prepareForSend(
ourUuid || ourNumber, ourUuid || ourNumber,
{ {
syncMessage: true, syncMessage: true,

View file

@ -1,178 +0,0 @@
// Copyright 2018-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { once } from 'lodash';
import * as log from './logging/log';
import { missingCaseError } from './util/missingCaseError';
import { SenderCertificateMode } from './metadata/SecretSessionCipher';
const ONE_DAY = 24 * 60 * 60 * 1000; // one day
const MINIMUM_TIME_LEFT = 2 * 60 * 60 * 1000; // two hours
let timeout: null | ReturnType<typeof setTimeout> = null;
let scheduledTime: null | number = null;
const removeOldKey = once((storage: typeof window.storage) => {
const oldCertKey = 'senderCertificateWithUuid';
const oldUuidCert = storage.get(oldCertKey);
if (oldUuidCert) {
storage.remove(oldCertKey);
}
});
// We need to refresh our own profile regularly to account for newly-added devices which
// do not support unidentified delivery.
function refreshOurProfile() {
window.log.info('refreshOurProfile');
const ourId = window.ConversationController.getOurConversationIdOrThrow();
const conversation = window.ConversationController.get(ourId);
conversation?.getProfiles();
}
export function initialize({
events,
storage,
mode,
navigator,
}: Readonly<{
events: {
on: (name: string, callback: () => void) => void;
};
storage: typeof window.storage;
mode: SenderCertificateMode;
navigator: Navigator;
}>): void {
let storageKey: 'senderCertificate' | 'senderCertificateNoE164';
let logString: string;
switch (mode) {
case SenderCertificateMode.WithE164:
storageKey = 'senderCertificate';
logString = 'sender certificate WITH E164';
break;
case SenderCertificateMode.WithoutE164:
storageKey = 'senderCertificateNoE164';
logString = 'sender certificate WITHOUT E164';
break;
default:
throw missingCaseError(mode);
}
runWhenOnline();
removeOldKey(storage);
events.on('timetravel', scheduleNextRotation);
function scheduleNextRotation() {
const now = Date.now();
const certificate = storage.get(storageKey);
if (!certificate || !certificate.expires) {
setTimeoutForNextRun(scheduledTime || now);
return;
}
// If we have a time in place and it's already before the safety zone before expire,
// we keep it
if (
scheduledTime &&
scheduledTime <= certificate.expires - MINIMUM_TIME_LEFT
) {
setTimeoutForNextRun(scheduledTime);
return;
}
// Otherwise, we reset every day, or earlier if the safety zone requires it
const time = Math.min(
now + ONE_DAY,
certificate.expires - MINIMUM_TIME_LEFT
);
setTimeoutForNextRun(time);
}
async function saveCert(certificate: string): Promise<void> {
const arrayBuffer = window.Signal.Crypto.base64ToArrayBuffer(certificate);
const decodedContainer = window.textsecure.protobuf.SenderCertificate.decode(
arrayBuffer
);
const decodedCert = window.textsecure.protobuf.SenderCertificate.Certificate.decode(
decodedContainer.certificate
);
// We don't want to send a protobuf-generated object across IPC, so we make
// our own object.
const toSave = {
expires: decodedCert.expires.toNumber(),
serialized: arrayBuffer,
};
await storage.put(storageKey, toSave);
}
async function run(): Promise<void> {
log.info(`refreshSenderCertificate: Getting new ${logString}...`);
try {
const OLD_USERNAME = storage.get('number_id');
const USERNAME = storage.get('uuid_id');
const PASSWORD = storage.get('password');
const server = window.WebAPI.connect({
username: USERNAME || OLD_USERNAME,
password: PASSWORD,
});
const omitE164 = mode === SenderCertificateMode.WithoutE164;
const { certificate } = await server.getSenderCertificate(omitE164);
await saveCert(certificate);
scheduledTime = null;
scheduleNextRotation();
} catch (error) {
log.error(
`refreshSenderCertificate: Get failed for ${logString}. Trying again in five minutes...`,
error && error.stack ? error.stack : error
);
scheduledTime = Date.now() + 5 * 60 * 1000;
scheduleNextRotation();
}
refreshOurProfile();
}
function runWhenOnline() {
if (navigator.onLine) {
run();
} else {
log.info(
'refreshSenderCertificate: Offline. Will update certificate when online...'
);
const listener = () => {
log.info(
'refreshSenderCertificate: Online. Now updating certificate...'
);
window.removeEventListener('online', listener);
run();
};
window.addEventListener('online', listener);
}
}
function setTimeoutForNextRun(time = Date.now()) {
const now = Date.now();
if (scheduledTime !== time || !timeout) {
log.info(
`refreshSenderCertificate: Next ${logString} refresh scheduled for`,
new Date(time).toISOString()
);
}
scheduledTime = time;
const waitTime = Math.max(0, time - now);
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(runWhenOnline, waitTime);
}
}

View file

@ -730,10 +730,10 @@ export class CallingClass {
}); });
} }
private sendGroupCallUpdateMessage( private async sendGroupCallUpdateMessage(
conversationId: string, conversationId: string,
eraId: string eraId: string
): void { ): Promise<void> {
const conversation = window.ConversationController.get(conversationId); const conversation = window.ConversationController.get(conversationId);
if (!conversation) { if (!conversation) {
window.log.error( window.log.error(
@ -743,7 +743,7 @@ export class CallingClass {
} }
const groupV2 = conversation.getGroupV2Info(); const groupV2 = conversation.getGroupV2Info();
const sendOptions = conversation.getSendOptions(); const sendOptions = await conversation.getSendOptions();
if (!groupV2) { if (!groupV2) {
window.log.error( window.log.error(
'Unable to send group call update message for conversation that lacks groupV2 info' 'Unable to send group call update message for conversation that lacks groupV2 info'
@ -1258,7 +1258,7 @@ export class CallingClass {
): Promise<boolean> { ): Promise<boolean> {
const conversation = window.ConversationController.get(remoteUserId); const conversation = window.ConversationController.get(remoteUserId);
const sendOptions = conversation const sendOptions = conversation
? conversation.getSendOptions() ? await conversation.getSendOptions()
: undefined; : undefined;
if (!window.textsecure.messaging) { if (!window.textsecure.messaging) {

View file

@ -0,0 +1,253 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import {
SenderCertificateMode,
serializedCertificateSchema,
SerializedCertificateType,
} from '../metadata/SecretSessionCipher';
import { SenderCertificateClass } from '../textsecure';
import { base64ToArrayBuffer } from '../Crypto';
import { assert } from '../util/assert';
import { missingCaseError } from '../util/missingCaseError';
import { waitForOnline } from '../util/waitForOnline';
import * as log from '../logging/log';
// We define a stricter storage here that returns `unknown` instead of `any`.
type Storage = {
get(key: string): unknown;
put(key: string, value: unknown): Promise<void>;
remove(key: string): Promise<void>;
};
// In case your clock is different from the server's, we "fake" expire certificates early.
const CLOCK_SKEW_THRESHOLD = 15 * 60 * 1000;
// This is exported for testing.
export class SenderCertificateService {
private WebAPI?: typeof window.WebAPI;
private SenderCertificate?: typeof SenderCertificateClass;
private fetchPromises: Map<
SenderCertificateMode,
Promise<undefined | SerializedCertificateType>
> = new Map();
private navigator?: { onLine: boolean };
private onlineEventTarget?: EventTarget;
private storage?: Storage;
initialize({
SenderCertificate,
WebAPI,
navigator,
onlineEventTarget,
storage,
}: {
WebAPI: typeof window.WebAPI;
navigator: Readonly<{ onLine: boolean }>;
onlineEventTarget: EventTarget;
SenderCertificate: typeof SenderCertificateClass;
storage: Storage;
}): void {
log.info('Sender certificate service initialized');
this.SenderCertificate = SenderCertificate;
this.WebAPI = WebAPI;
this.navigator = navigator;
this.onlineEventTarget = onlineEventTarget;
this.storage = storage;
removeOldKey(storage);
}
async get(
mode: SenderCertificateMode
): Promise<undefined | SerializedCertificateType> {
const storedCertificate = this.getStoredCertificate(mode);
if (storedCertificate) {
log.info(
`Sender certificate service found a valid ${modeToLogString(
mode
)} certificate in storage; skipping fetch`
);
return storedCertificate;
}
return this.fetchCertificate(mode);
}
private getStoredCertificate(
mode: SenderCertificateMode
): undefined | SerializedCertificateType {
const { storage } = this;
assert(
storage,
'Sender certificate service method was called before it was initialized'
);
const valueInStorage = storage.get(modeToStorageKey(mode));
return serializedCertificateSchema.check(valueInStorage) &&
isExpirationValid(valueInStorage.expires)
? valueInStorage
: undefined;
}
private fetchCertificate(
mode: SenderCertificateMode
): Promise<undefined | SerializedCertificateType> {
// This prevents multiple concurrent fetches.
const existingPromise = this.fetchPromises.get(mode);
if (existingPromise) {
log.info(
`Sender certificate service was already fetching a ${modeToLogString(
mode
)} certificate; piggybacking off of that`
);
return existingPromise;
}
let promise: Promise<undefined | SerializedCertificateType>;
const doFetch = async () => {
const result = await this.fetchAndSaveCertificate(mode);
assert(
this.fetchPromises.get(mode) === promise,
'Sender certificate service was deleting a different promise than expected'
);
this.fetchPromises.delete(mode);
return result;
};
promise = doFetch();
assert(
!this.fetchPromises.has(mode),
'Sender certificate service somehow already had a promise for this mode'
);
this.fetchPromises.set(mode, promise);
return promise;
}
private async fetchAndSaveCertificate(
mode: SenderCertificateMode
): Promise<undefined | SerializedCertificateType> {
const { SenderCertificate, storage, navigator, onlineEventTarget } = this;
assert(
SenderCertificate && storage && navigator && onlineEventTarget,
'Sender certificate service method was called before it was initialized'
);
log.info(
`Sender certificate service: fetching and saving a ${modeToLogString(
mode
)} certificate`
);
await waitForOnline(navigator, onlineEventTarget);
let certificateString: string;
try {
certificateString = await this.requestSenderCertificate(mode);
} catch (err) {
log.warn(
`Sender certificate service could not fetch a ${modeToLogString(
mode
)} certificate. Returning undefined`,
err && err.stack ? err.stack : err
);
return undefined;
}
const certificate = base64ToArrayBuffer(certificateString);
const decodedContainer = SenderCertificate.decode(certificate);
const decodedCert = decodedContainer.certificate
? SenderCertificate.Certificate.decode(decodedContainer.certificate)
: undefined;
const expires = decodedCert?.expires?.toNumber();
if (!isExpirationValid(expires)) {
log.warn(
`Sender certificate service fetched a ${modeToLogString(
mode
)} certificate from the server that was already expired (or was invalid). Is your system clock off?`
);
return undefined;
}
const serializedCertificate = {
expires: expires - CLOCK_SKEW_THRESHOLD,
serialized: certificate,
};
await storage.put(modeToStorageKey(mode), serializedCertificate);
return serializedCertificate;
}
private async requestSenderCertificate(
mode: SenderCertificateMode
): Promise<string> {
const { storage, WebAPI } = this;
assert(
storage && WebAPI,
'Sender certificate service method was called before it was initialized'
);
const username = storage.get('uuid_id') || storage.get('number_id');
const password = storage.get('password');
if (typeof username !== 'string') {
throw new Error(
'Sender certificate service: username in storage was not a string. Cannot connect'
);
}
if (typeof password !== 'string') {
throw new Error(
'Sender certificate service: password in storage was not a string. Cannot connect'
);
}
const server = WebAPI.connect({ username, password });
const omitE164 = mode === SenderCertificateMode.WithoutE164;
const { certificate } = await server.getSenderCertificate(omitE164);
return certificate;
}
}
function modeToStorageKey(
mode: SenderCertificateMode
): 'senderCertificate' | 'senderCertificateNoE164' {
switch (mode) {
case SenderCertificateMode.WithE164:
return 'senderCertificate';
case SenderCertificateMode.WithoutE164:
return 'senderCertificateNoE164';
default:
throw missingCaseError(mode);
}
}
function modeToLogString(mode: SenderCertificateMode): string {
switch (mode) {
case SenderCertificateMode.WithE164:
return 'yes-E164';
case SenderCertificateMode.WithoutE164:
return 'no-E164';
default:
throw missingCaseError(mode);
}
}
function isExpirationValid(expiration: unknown): expiration is number {
return typeof expiration === 'number' && expiration > Date.now();
}
function removeOldKey(storage: Readonly<Storage>) {
const oldCertKey = 'senderCertificateWithUuid';
const oldUuidCert = storage.get(oldCertKey);
if (oldUuidCert) {
storage.remove(oldCertKey);
}
}
export const senderCertificateService = new SenderCertificateService();

View file

@ -1,16 +1,18 @@
// Copyright 2019-2020 Signal Messenger, LLC // Copyright 2019-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
export function sendStickerPackSync( export async function sendStickerPackSync(
packId: string, packId: string,
packKey: string, packKey: string,
installed: boolean installed: boolean
): void { ): Promise<void> {
const { ConversationController, textsecure, log } = window; const { ConversationController, textsecure, log } = window;
const ourNumber = textsecure.storage.user.getNumber(); const ourNumber = textsecure.storage.user.getNumber();
const { wrap, sendOptions } = ConversationController.prepareForSend( const { wrap, sendOptions } = await ConversationController.prepareForSend(
ourNumber, ourNumber,
{ syncMessage: true } {
syncMessage: true,
}
); );
if (!textsecure.messaging) { if (!textsecure.messaging) {

View file

@ -0,0 +1,257 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// 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 { SenderCertificateMode } from '../../metadata/SecretSessionCipher';
import { SenderCertificateService } from '../../services/senderCertificate';
describe('SenderCertificateService', () => {
const FIFTEEN_MINUTES = 15 * 60 * 1000;
let fakeValidCertificate: SenderCertificateClass;
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,
storage: fakeStorage,
});
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;
const certificate = new SenderCertificate.Certificate();
certificate.expires = global.window.dcodeIO.Long.fromNumber(
fakeValidCertificateExpiry
);
fakeValidCertificate.certificate = certificate.toArrayBuffer();
fakeServer = {
getSenderCertificate: sinon.stub().resolves({
certificate: arrayBufferToBase64(fakeValidCertificate.toArrayBuffer()),
}),
};
fakeWebApi = { connect: sinon.stub().returns(fakeServer) };
fakeNavigator = { onLine: true };
fakeWindow = {
addEventListener: sinon.stub(),
dispatchEvent: sinon.stub(),
removeEventListener: sinon.stub(),
};
fakeStorage = {
get: sinon.stub(),
put: sinon.stub().resolves(),
remove: sinon.stub().resolves(),
};
fakeStorage.get.withArgs('uuid_id').returns(`${uuid()}.2`);
fakeStorage.get.withArgs('password').returns('abc123');
});
describe('initialize', () => {
it('removes an old storage service key if it was present', () => {
fakeStorage.get
.withArgs('senderCertificateWithUuid')
.returns('some value');
initializeTestService();
sinon.assert.calledWith(fakeStorage.remove, 'senderCertificateWithUuid');
});
it("doesn't remove anything from storage if it wasn't there", () => {
initializeTestService();
sinon.assert.notCalled(fakeStorage.put);
});
});
describe('get', () => {
it('returns valid yes-E164 certificates from storage if they exist', async () => {
const cert = {
expires: Date.now() + 123456,
serialized: new ArrayBuffer(2),
};
fakeStorage.get.withArgs('senderCertificate').returns(cert);
const service = initializeTestService();
assert.strictEqual(
await service.get(SenderCertificateMode.WithE164),
cert
);
sinon.assert.notCalled(fakeStorage.put);
});
it('returns valid no-E164 certificates from storage if they exist', async () => {
const cert = {
expires: Date.now() + 123456,
serialized: new ArrayBuffer(2),
};
fakeStorage.get.withArgs('senderCertificateNoE164').returns(cert);
const service = initializeTestService();
assert.strictEqual(
await service.get(SenderCertificateMode.WithoutE164),
cert
);
sinon.assert.notCalled(fakeStorage.put);
});
it('returns and stores a newly-fetched yes-E164 certificate if none was in storage', async () => {
const service = initializeTestService();
assert.deepEqual(await service.get(SenderCertificateMode.WithE164), {
expires: fakeValidCertificateExpiry - FIFTEEN_MINUTES,
serialized: fakeValidCertificate.toArrayBuffer(),
});
sinon.assert.calledWithMatch(fakeStorage.put, 'senderCertificate', {
expires: fakeValidCertificateExpiry - FIFTEEN_MINUTES,
serialized: fakeValidCertificate.toArrayBuffer(),
});
sinon.assert.calledWith(fakeServer.getSenderCertificate, false);
});
it('returns and stores a newly-fetched no-E164 certificate if none was in storage', async () => {
const service = initializeTestService();
assert.deepEqual(await service.get(SenderCertificateMode.WithoutE164), {
expires: fakeValidCertificateExpiry - FIFTEEN_MINUTES,
serialized: fakeValidCertificate.toArrayBuffer(),
});
sinon.assert.calledWithMatch(fakeStorage.put, 'senderCertificateNoE164', {
expires: fakeValidCertificateExpiry - FIFTEEN_MINUTES,
serialized: fakeValidCertificate.toArrayBuffer(),
});
sinon.assert.calledWith(fakeServer.getSenderCertificate, true);
});
it('fetches new certificates if the value in storage has already expired', async () => {
const service = initializeTestService();
fakeStorage.get.withArgs('senderCertificate').returns({
expires: Date.now() - 1000,
serialized: new ArrayBuffer(2),
});
await service.get(SenderCertificateMode.WithE164);
sinon.assert.called(fakeServer.getSenderCertificate);
});
it('fetches new certificates if the value in storage is invalid', async () => {
const service = initializeTestService();
fakeStorage.get.withArgs('senderCertificate').returns({
serialized: 'not an arraybuffer',
});
await service.get(SenderCertificateMode.WithE164);
sinon.assert.called(fakeServer.getSenderCertificate);
});
it('only hits the server once per certificate type when requesting many times', async () => {
const service = initializeTestService();
await Promise.all([
service.get(SenderCertificateMode.WithE164),
service.get(SenderCertificateMode.WithoutE164),
service.get(SenderCertificateMode.WithE164),
service.get(SenderCertificateMode.WithoutE164),
service.get(SenderCertificateMode.WithE164),
service.get(SenderCertificateMode.WithoutE164),
service.get(SenderCertificateMode.WithE164),
service.get(SenderCertificateMode.WithoutE164),
]);
sinon.assert.calledTwice(fakeServer.getSenderCertificate);
});
it('hits the server again after a request has completed', async () => {
const service = initializeTestService();
await service.get(SenderCertificateMode.WithE164);
sinon.assert.calledOnce(fakeServer.getSenderCertificate);
await service.get(SenderCertificateMode.WithE164);
sinon.assert.calledTwice(fakeServer.getSenderCertificate);
});
it('returns undefined if the request to the server fails', async () => {
const service = initializeTestService();
fakeServer.getSenderCertificate.rejects(new Error('uh oh'));
assert.isUndefined(await service.get(SenderCertificateMode.WithE164));
});
it('returns undefined if the server returns an already-expired certificate', async () => {
const service = initializeTestService();
const expiredCertificate = new SenderCertificate();
const certificate = new SenderCertificate.Certificate();
certificate.expires = global.window.dcodeIO.Long.fromNumber(
Date.now() - 1000
);
expiredCertificate.certificate = certificate.toArrayBuffer();
fakeServer.getSenderCertificate.resolves({
certificate: arrayBufferToBase64(expiredCertificate.toArrayBuffer()),
});
assert.isUndefined(await service.get(SenderCertificateMode.WithE164));
});
});
});

View file

@ -0,0 +1,51 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import * as sinon from 'sinon';
import { waitForOnline } from '../../util/waitForOnline';
describe('waitForOnline', () => {
function getFakeWindow(): EventTarget {
const result = new EventTarget();
sinon.stub(result, 'addEventListener');
sinon.stub(result, 'removeEventListener');
return result;
}
it("resolves immediately if you're online", async () => {
const fakeNavigator = { onLine: true };
const fakeWindow = getFakeWindow();
await waitForOnline(fakeNavigator, fakeWindow);
sinon.assert.notCalled(fakeWindow.addEventListener as sinon.SinonStub);
sinon.assert.notCalled(fakeWindow.removeEventListener as sinon.SinonStub);
});
it("if you're offline, resolves as soon as you're online", async () => {
const fakeNavigator = { onLine: false };
const fakeWindow = getFakeWindow();
(fakeWindow.addEventListener as sinon.SinonStub)
.withArgs('online')
.callsFake((_eventName: string, callback: () => void) => {
setTimeout(callback, 0);
});
let done = false;
const promise = (async () => {
await waitForOnline(fakeNavigator, fakeWindow);
done = true;
})();
assert.isFalse(done);
await promise;
assert.isTrue(done);
sinon.assert.calledOnce(fakeWindow.addEventListener as sinon.SinonStub);
sinon.assert.calledOnce(fakeWindow.removeEventListener as sinon.SinonStub);
});
});

3
ts/textsecure.d.ts vendored
View file

@ -10,6 +10,7 @@ import {
import Crypto from './textsecure/Crypto'; import Crypto from './textsecure/Crypto';
import MessageReceiver from './textsecure/MessageReceiver'; import MessageReceiver from './textsecure/MessageReceiver';
import MessageSender from './textsecure/SendMessage'; import MessageSender from './textsecure/SendMessage';
import SyncRequest from './textsecure/SyncRequest';
import EventTarget from './textsecure/EventTarget'; import EventTarget from './textsecure/EventTarget';
import { ByteBufferClass } from './window.d'; import { ByteBufferClass } from './window.d';
import SendMessage, { SendOptionsType } from './textsecure/SendMessage'; import SendMessage, { SendOptionsType } from './textsecure/SendMessage';
@ -90,7 +91,7 @@ export type TextSecureType = {
MessageReceiver: typeof MessageReceiver; MessageReceiver: typeof MessageReceiver;
AccountManager: WhatIsThis; AccountManager: WhatIsThis;
MessageSender: WhatIsThis; MessageSender: WhatIsThis;
SyncRequest: WhatIsThis; SyncRequest: typeof SyncRequest;
}; };
type StoredSignedPreKeyType = SignedPreKeyType & { type StoredSignedPreKeyType = SignedPreKeyType & {

View file

@ -1255,7 +1255,7 @@ class MessageReceiverInner extends EventTarget {
await sessionCipher.closeOpenSessionForDevice(); await sessionCipher.closeOpenSessionForDevice();
// Send a null message with newly-created session // Send a null message with newly-created session
const sendOptions = conversation.getSendOptions(); const sendOptions = await conversation.getSendOptions();
await window.textsecure.messaging.sendNullMessage({ uuid }, sendOptions); await window.textsecure.messaging.sendNullMessage({ uuid }, sendOptions);
} }

View file

@ -1,4 +1,4 @@
// Copyright 2020 Signal Messenger, LLC // Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable more/no-then */ /* eslint-disable more/no-then */
@ -9,9 +9,10 @@
import EventTarget from './EventTarget'; import EventTarget from './EventTarget';
import MessageReceiver from './MessageReceiver'; import MessageReceiver from './MessageReceiver';
import MessageSender from './SendMessage'; import MessageSender from './SendMessage';
import { assert } from '../util/assert';
class SyncRequestInner extends EventTarget { class SyncRequestInner extends EventTarget {
receiver: MessageReceiver; private started = false;
contactSync?: boolean; contactSync?: boolean;
@ -23,7 +24,10 @@ class SyncRequestInner extends EventTarget {
ongroup: Function; ongroup: Function;
constructor(sender: MessageSender, receiver: MessageReceiver) { constructor(
private sender: MessageSender,
private receiver: MessageReceiver
) {
super(); super();
if ( if (
@ -34,21 +38,30 @@ class SyncRequestInner extends EventTarget {
'Tried to construct a SyncRequest without MessageSender and MessageReceiver' 'Tried to construct a SyncRequest without MessageSender and MessageReceiver'
); );
} }
this.receiver = receiver;
this.oncontact = this.onContactSyncComplete.bind(this); this.oncontact = this.onContactSyncComplete.bind(this);
receiver.addEventListener('contactsync', this.oncontact); receiver.addEventListener('contactsync', this.oncontact);
this.ongroup = this.onGroupSyncComplete.bind(this); this.ongroup = this.onGroupSyncComplete.bind(this);
receiver.addEventListener('groupsync', this.ongroup); receiver.addEventListener('groupsync', this.ongroup);
}
async start(): Promise<void> {
if (this.started) {
assert(false, 'SyncRequestInner: started more than once. Doing nothing');
return;
}
this.started = true;
const { sender } = this;
const ourNumber = window.textsecure.storage.user.getNumber(); const ourNumber = window.textsecure.storage.user.getNumber();
const { wrap, sendOptions } = window.ConversationController.prepareForSend( const {
ourNumber, wrap,
{ sendOptions,
syncMessage: true, } = await window.ConversationController.prepareForSend(ourNumber, {
} syncMessage: true,
); });
window.log.info('SyncRequest created. Sending config sync request...'); window.log.info('SyncRequest created. Sending config sync request...');
wrap(sender.sendRequestConfigurationSyncMessage(sendOptions)); wrap(sender.sendRequestConfigurationSyncMessage(sendOptions));
@ -106,13 +119,20 @@ class SyncRequestInner extends EventTarget {
} }
export default class SyncRequest { export default class SyncRequest {
constructor(sender: MessageSender, receiver: MessageReceiver) { private inner: SyncRequestInner;
const inner = new SyncRequestInner(sender, receiver);
this.addEventListener = inner.addEventListener.bind(inner);
this.removeEventListener = inner.removeEventListener.bind(inner);
}
addEventListener: (name: string, handler: Function) => void; addEventListener: (name: string, handler: Function) => void;
removeEventListener: (name: string, handler: Function) => void; removeEventListener: (name: string, handler: Function) => void;
constructor(sender: MessageSender, receiver: MessageReceiver) {
const inner = new SyncRequestInner(sender, receiver);
this.inner = inner;
this.addEventListener = inner.addEventListener.bind(inner);
this.removeEventListener = inner.removeEventListener.bind(inner);
}
start(): void {
this.inner.start();
}
} }

View file

@ -804,7 +804,9 @@ export type WebAPIType = {
} }
) => Promise<any>; ) => Promise<any>;
getProvisioningSocket: () => WebSocket; getProvisioningSocket: () => WebSocket;
getSenderCertificate: (withUuid?: boolean) => Promise<any>; getSenderCertificate: (
withUuid?: boolean
) => Promise<{ certificate: string }>;
getSticker: (packId: string, stickerId: number) => Promise<any>; getSticker: (packId: string, stickerId: number) => Promise<any>;
getStickerPackManifest: (packId: string) => Promise<StickerPackManifestType>; getStickerPackManifest: (packId: string) => Promise<StickerPackManifestType>;
getStorageCredentials: MessageSender['getStorageCredentials']; getStorageCredentials: MessageSender['getStorageCredentials'];

View file

@ -17020,7 +17020,7 @@
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
"path": "ts/shims/textsecure.js", "path": "ts/shims/textsecure.js",
"line": " wrap(textsecure.messaging.sendStickerPackSync([", "line": " wrap(textsecure.messaging.sendStickerPackSync([",
"lineNumber": 14, "lineNumber": 16,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2020-02-07T19:52:28.522Z" "updated": "2020-02-07T19:52:28.522Z"
}, },
@ -17028,7 +17028,7 @@
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
"path": "ts/shims/textsecure.ts", "path": "ts/shims/textsecure.ts",
"line": " wrap(", "line": " wrap(",
"lineNumber": 24, "lineNumber": 26,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2020-02-07T19:52:28.522Z" "updated": "2020-02-07T19:52:28.522Z"
}, },
@ -17100,7 +17100,7 @@
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
"path": "ts/textsecure/SyncRequest.js", "path": "ts/textsecure/SyncRequest.js",
"line": " wrap(sender.sendRequestConfigurationSyncMessage(sendOptions));", "line": " wrap(sender.sendRequestConfigurationSyncMessage(sendOptions));",
"lineNumber": 32, "lineNumber": 43,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2020-04-05T23:45:16.746Z" "updated": "2020-04-05T23:45:16.746Z"
}, },
@ -17108,7 +17108,7 @@
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
"path": "ts/textsecure/SyncRequest.js", "path": "ts/textsecure/SyncRequest.js",
"line": " wrap(sender.sendRequestBlockSyncMessage(sendOptions));", "line": " wrap(sender.sendRequestBlockSyncMessage(sendOptions));",
"lineNumber": 34, "lineNumber": 45,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2020-04-05T23:45:16.746Z" "updated": "2020-04-05T23:45:16.746Z"
}, },
@ -17116,7 +17116,7 @@
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
"path": "ts/textsecure/SyncRequest.js", "path": "ts/textsecure/SyncRequest.js",
"line": " wrap(sender.sendRequestContactSyncMessage(sendOptions))", "line": " wrap(sender.sendRequestContactSyncMessage(sendOptions))",
"lineNumber": 36, "lineNumber": 47,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2020-04-05T23:45:16.746Z" "updated": "2020-04-05T23:45:16.746Z"
}, },
@ -17124,7 +17124,7 @@
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
"path": "ts/textsecure/SyncRequest.js", "path": "ts/textsecure/SyncRequest.js",
"line": " return wrap(sender.sendRequestGroupSyncMessage(sendOptions));", "line": " return wrap(sender.sendRequestGroupSyncMessage(sendOptions));",
"lineNumber": 39, "lineNumber": 50,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2020-04-05T23:45:16.746Z" "updated": "2020-04-05T23:45:16.746Z"
}, },
@ -17132,7 +17132,7 @@
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
"path": "ts/textsecure/SyncRequest.ts", "path": "ts/textsecure/SyncRequest.ts",
"line": " wrap(sender.sendRequestConfigurationSyncMessage(sendOptions));", "line": " wrap(sender.sendRequestConfigurationSyncMessage(sendOptions));",
"lineNumber": 54, "lineNumber": 67,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2020-04-05T23:45:16.746Z" "updated": "2020-04-05T23:45:16.746Z"
}, },
@ -17140,7 +17140,7 @@
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
"path": "ts/textsecure/SyncRequest.ts", "path": "ts/textsecure/SyncRequest.ts",
"line": " wrap(sender.sendRequestBlockSyncMessage(sendOptions));", "line": " wrap(sender.sendRequestBlockSyncMessage(sendOptions));",
"lineNumber": 57, "lineNumber": 70,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2020-04-05T23:45:16.746Z" "updated": "2020-04-05T23:45:16.746Z"
}, },
@ -17148,7 +17148,7 @@
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
"path": "ts/textsecure/SyncRequest.ts", "path": "ts/textsecure/SyncRequest.ts",
"line": " wrap(sender.sendRequestContactSyncMessage(sendOptions))", "line": " wrap(sender.sendRequestContactSyncMessage(sendOptions))",
"lineNumber": 60, "lineNumber": 73,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2020-04-05T23:45:16.746Z" "updated": "2020-04-05T23:45:16.746Z"
}, },
@ -17156,7 +17156,7 @@
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
"path": "ts/textsecure/SyncRequest.ts", "path": "ts/textsecure/SyncRequest.ts",
"line": " return wrap(sender.sendRequestGroupSyncMessage(sendOptions));", "line": " return wrap(sender.sendRequestGroupSyncMessage(sendOptions));",
"lineNumber": 63, "lineNumber": 76,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2020-04-05T23:45:16.746Z" "updated": "2020-04-05T23:45:16.746Z"
}, },
@ -17172,7 +17172,7 @@
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
"path": "ts/textsecure/WebAPI.ts", "path": "ts/textsecure/WebAPI.ts",
"line": " const byteBuffer = window.dcodeIO.ByteBuffer.wrap(", "line": " const byteBuffer = window.dcodeIO.ByteBuffer.wrap(",
"lineNumber": 2232, "lineNumber": 2234,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2020-09-08T23:07:22.682Z" "updated": "2020-09-08T23:07:22.682Z"
}, },
@ -17284,4 +17284,4 @@
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2021-04-06T23:11:04.431Z" "updated": "2021-04-06T23:11:04.431Z"
} }
] ]

21
ts/util/waitForOnline.ts Normal file
View file

@ -0,0 +1,21 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
export function waitForOnline(
navigator: Readonly<{ onLine: boolean }>,
onlineEventTarget: EventTarget
): Promise<void> {
return new Promise(resolve => {
if (navigator.onLine) {
resolve();
return;
}
const listener = () => {
onlineEventTarget.removeEventListener('online', listener);
resolve();
};
onlineEventTarget.addEventListener('online', listener);
});
}

6
ts/window.d.ts vendored
View file

@ -70,8 +70,8 @@ import * as searchDuck from './state/ducks/search';
import * as stickersDuck from './state/ducks/stickers'; import * as stickersDuck from './state/ducks/stickers';
import * as conversationsSelectors from './state/selectors/conversations'; import * as conversationsSelectors from './state/selectors/conversations';
import * as searchSelectors from './state/selectors/search'; import * as searchSelectors from './state/selectors/search';
import { SendOptionsType } from './textsecure/SendMessage';
import AccountManager from './textsecure/AccountManager'; import AccountManager from './textsecure/AccountManager';
import { SendOptionsType } from './textsecure/SendMessage';
import Data from './sql/Client'; import Data from './sql/Client';
import { UserMessage } from './types/Message'; import { UserMessage } from './types/Message';
import { PhoneNumberFormat } from 'google-libphonenumber'; import { PhoneNumberFormat } from 'google-libphonenumber';
@ -97,6 +97,7 @@ import { ElectronLocaleType } from './util/mapToSupportLocale';
import { SignalProtocolStore } from './LibSignalStore'; import { SignalProtocolStore } from './LibSignalStore';
import { StartupQueue } from './util/StartupQueue'; import { StartupQueue } from './util/StartupQueue';
import * as synchronousCrypto from './util/synchronousCrypto'; import * as synchronousCrypto from './util/synchronousCrypto';
import SyncRequest from './textsecure/SyncRequest';
export { Long } from 'long'; export { Long } from 'long';
@ -169,7 +170,7 @@ declare global {
getServerPublicParams: () => string; getServerPublicParams: () => string;
getSfuUrl: () => string; getSfuUrl: () => string;
getSocketStatus: () => number; getSocketStatus: () => number;
getSyncRequest: () => WhatIsThis; getSyncRequest: () => SyncRequest;
getTitle: () => string; getTitle: () => string;
waitForEmptyEventQueue: () => Promise<void>; waitForEmptyEventQueue: () => Promise<void>;
getVersion: () => string; getVersion: () => string;
@ -578,6 +579,7 @@ export type DCodeIOType = {
fromString: (str: string | null) => Long; fromString: (str: string | null) => Long;
isLong: (obj: unknown) => obj is Long; isLong: (obj: unknown) => obj is Long;
}; };
ProtoBuf: WhatIsThis;
}; };
type MessageControllerType = { type MessageControllerType = {