Fetch sender certificates on-demand
This commit is contained in:
parent
6ff55914f0
commit
a82fa86176
19 changed files with 752 additions and 298 deletions
|
@ -722,19 +722,19 @@ export class ConversationController {
|
|||
return null;
|
||||
}
|
||||
|
||||
prepareForSend(
|
||||
async prepareForSend(
|
||||
id: string | undefined,
|
||||
options?: WhatIsThis
|
||||
): {
|
||||
): Promise<{
|
||||
wrap: (
|
||||
promise: Promise<CallbackResultType | void | null>
|
||||
) => Promise<CallbackResultType | void | null>;
|
||||
sendOptions: SendOptionsType | undefined;
|
||||
} {
|
||||
}> {
|
||||
// id is any valid conversation identifier
|
||||
const conversation = this.get(id);
|
||||
const sendOptions = conversation
|
||||
? conversation.getSendOptions(options)
|
||||
? await conversation.getSendOptions(options)
|
||||
: undefined;
|
||||
const wrap = conversation
|
||||
? conversation.wrapSend.bind(conversation)
|
||||
|
|
|
@ -7,8 +7,7 @@ import { WhatIsThis } from './window.d';
|
|||
import { getTitleBarVisibility, TitleBarVisibility } from './types/Settings';
|
||||
import { isWindowDragElement } from './util/isWindowDragElement';
|
||||
import { assert } from './util/assert';
|
||||
import * as refreshSenderCertificate from './refreshSenderCertificate';
|
||||
import { SenderCertificateMode } from './metadata/SecretSessionCipher';
|
||||
import { senderCertificateService } from './services/senderCertificate';
|
||||
import { routineProfileRefresh } from './routineProfileRefresh';
|
||||
import { isMoreRecentThan, isOlderThan } from './util/timestamp';
|
||||
import { isValidReactionEmoji } from './reactions/isValidReactionEmoji';
|
||||
|
@ -30,6 +29,17 @@ export async function startApp(): Promise<void> {
|
|||
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({
|
||||
concurrency: 1,
|
||||
timeout: 1000 * 60 * 2,
|
||||
|
@ -70,7 +80,7 @@ export async function startApp(): Promise<void> {
|
|||
const {
|
||||
wrap,
|
||||
sendOptions,
|
||||
} = window.ConversationController.prepareForSend(c.get('id'));
|
||||
} = await window.ConversationController.prepareForSend(c.get('id'));
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await wrap(
|
||||
window.textsecure.messaging.sendDeliveryReceipt(
|
||||
|
@ -1592,11 +1602,14 @@ export async function startApp(): Promise<void> {
|
|||
);
|
||||
}
|
||||
|
||||
window.getSyncRequest = () =>
|
||||
new window.textsecure.SyncRequest(
|
||||
window.getSyncRequest = () => {
|
||||
const syncRequest = new window.textsecure.SyncRequest(
|
||||
window.textsecure.messaging,
|
||||
messageReceiver
|
||||
);
|
||||
syncRequest.start();
|
||||
return syncRequest;
|
||||
};
|
||||
|
||||
let disconnectTimer: WhatIsThis | null = null;
|
||||
let reconnectTimer: WhatIsThis | null = null;
|
||||
|
@ -1948,10 +1961,7 @@ export async function startApp(): Promise<void> {
|
|||
);
|
||||
onChangeTheme();
|
||||
}
|
||||
const syncRequest = new window.textsecure.SyncRequest(
|
||||
window.textsecure.messaging,
|
||||
messageReceiver
|
||||
);
|
||||
const syncRequest = window.getSyncRequest();
|
||||
window.Whisper.events.trigger('contactsync:begin');
|
||||
syncRequest.addEventListener('success', () => {
|
||||
window.log.info('sync successful');
|
||||
|
@ -1969,7 +1979,7 @@ export async function startApp(): Promise<void> {
|
|||
const {
|
||||
wrap,
|
||||
sendOptions,
|
||||
} = window.ConversationController.prepareForSend(ourId, {
|
||||
} = await window.ConversationController.prepareForSend(ourId, {
|
||||
syncMessage: true,
|
||||
});
|
||||
|
||||
|
@ -2090,17 +2100,6 @@ export async function startApp(): Promise<void> {
|
|||
newVersion
|
||||
);
|
||||
|
||||
[SenderCertificateMode.WithE164, SenderCertificateMode.WithoutE164].forEach(
|
||||
mode => {
|
||||
refreshSenderCertificate.initialize({
|
||||
events: window.Whisper.events,
|
||||
storage: window.storage,
|
||||
mode,
|
||||
navigator,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
window.Whisper.deliveryReceiptQueue.start();
|
||||
window.Whisper.Notifications.enable();
|
||||
|
||||
|
|
|
@ -1241,7 +1241,7 @@ export async function modifyGroupV2({
|
|||
? window.storage.get('profileKey')
|
||||
: undefined;
|
||||
|
||||
const sendOptions = conversation.getSendOptions();
|
||||
const sendOptions = await conversation.getSendOptions();
|
||||
const timestamp = Date.now();
|
||||
|
||||
const promise = conversation.wrapSend(
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import * as z from 'zod';
|
||||
import * as CiphertextMessage from './CiphertextMessage';
|
||||
import {
|
||||
bytesFromString,
|
||||
|
@ -44,9 +45,16 @@ export const enum SenderCertificateMode {
|
|||
WithoutE164,
|
||||
}
|
||||
|
||||
export type SerializedCertificateType = {
|
||||
serialized: ArrayBuffer;
|
||||
};
|
||||
export const serializedCertificateSchema = z
|
||||
.object({
|
||||
expires: z.number().optional(),
|
||||
serialized: z.instanceof(ArrayBuffer),
|
||||
})
|
||||
.nonstrict();
|
||||
|
||||
export type SerializedCertificateType = z.infer<
|
||||
typeof serializedCertificateSchema
|
||||
>;
|
||||
|
||||
type ServerCertificateType = {
|
||||
id: number;
|
||||
|
|
|
@ -54,7 +54,11 @@ import {
|
|||
PhoneNumberSharingMode,
|
||||
parsePhoneNumberSharingMode,
|
||||
} from '../util/phoneNumberSharingMode';
|
||||
import { SerializedCertificateType } from '../metadata/SecretSessionCipher';
|
||||
import {
|
||||
SenderCertificateMode,
|
||||
SerializedCertificateType,
|
||||
} from '../metadata/SecretSessionCipher';
|
||||
import { senderCertificateService } from '../services/senderCertificate';
|
||||
|
||||
/* eslint-disable more/no-then */
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
@ -1090,7 +1094,7 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
return undefined;
|
||||
}
|
||||
|
||||
sendTypingMessage(isTyping: boolean): void {
|
||||
async sendTypingMessage(isTyping: boolean): Promise<void> {
|
||||
if (!window.textsecure.messaging) {
|
||||
return;
|
||||
}
|
||||
|
@ -1109,7 +1113,7 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
return;
|
||||
}
|
||||
|
||||
const sendOptions = this.getSendOptions();
|
||||
const sendOptions = await this.getSendOptions();
|
||||
this.wrapSend(
|
||||
window.textsecure.messaging.sendTypingMessage(
|
||||
{
|
||||
|
@ -1912,7 +1916,10 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
await this.applyMessageRequestResponse(response);
|
||||
|
||||
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
|
||||
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
|
||||
// a sync message to our own devices, we need to send the accessKeys down for both
|
||||
// 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
|
||||
this.ourNumber || this.ourUuid!,
|
||||
{ syncMessage: true }
|
||||
);
|
||||
const contactSendOptions = this.getSendOptions();
|
||||
const contactSendOptions = await this.getSendOptions();
|
||||
const options = { ...sendOptions, ...contactSendOptions };
|
||||
|
||||
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!');
|
||||
}
|
||||
|
||||
const options = this.getSendOptions();
|
||||
const options = await this.getSendOptions();
|
||||
|
||||
const promise = (() => {
|
||||
if (this.isPrivate()) {
|
||||
|
@ -3239,7 +3246,7 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
return message.sendSyncMessageOnly(dataMessage);
|
||||
}
|
||||
|
||||
const options = this.getSendOptions();
|
||||
const options = await this.getSendOptions();
|
||||
|
||||
const promise = (() => {
|
||||
if (this.isPrivate()) {
|
||||
|
@ -3302,7 +3309,7 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
await window.textsecure.messaging.sendProfileKeyUpdate(
|
||||
profileKey,
|
||||
recipients,
|
||||
this.getSendOptions(),
|
||||
await this.getSendOptions(),
|
||||
this.get('groupId')
|
||||
);
|
||||
}
|
||||
|
@ -3435,7 +3442,7 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
}
|
||||
|
||||
const conversationType = this.get('type');
|
||||
const options = this.getSendOptions();
|
||||
const options = await this.getSendOptions();
|
||||
|
||||
let promise;
|
||||
if (conversationType === Message.GROUP) {
|
||||
|
@ -3555,17 +3562,17 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
);
|
||||
}
|
||||
|
||||
getSendOptions(options = {}): SendOptionsType {
|
||||
const sendMetadata = this.getSendMetadata(options);
|
||||
async getSendOptions(options = {}): Promise<SendOptionsType> {
|
||||
const sendMetadata = await this.getSendMetadata(options);
|
||||
|
||||
return {
|
||||
sendMetadata,
|
||||
};
|
||||
}
|
||||
|
||||
getSendMetadata(
|
||||
async getSendMetadata(
|
||||
options: { syncMessage?: string; disableMeCheck?: boolean } = {}
|
||||
): SendMetadataType | undefined {
|
||||
): Promise<SendMetadataType | undefined> {
|
||||
const { syncMessage, disableMeCheck } = options;
|
||||
|
||||
// START: this code has an Expiration date of ~2018/11/21
|
||||
|
@ -3580,11 +3587,19 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
// END
|
||||
|
||||
if (!this.isPrivate()) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const infoArray = this.contactCollection!.map(conversation =>
|
||||
conversation.getSendMetadata(options)
|
||||
assert(
|
||||
this.contactCollection,
|
||||
'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');
|
||||
|
@ -3598,7 +3613,7 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
const e164 = this.get('e164');
|
||||
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 (sealedSender === SEALED_SENDER.UNKNOWN) {
|
||||
|
@ -3630,9 +3645,9 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
};
|
||||
}
|
||||
|
||||
private getSenderCertificateForDirectConversation():
|
||||
| undefined
|
||||
| SerializedCertificateType {
|
||||
private getSenderCertificateForDirectConversation(): Promise<
|
||||
undefined | SerializedCertificateType
|
||||
> {
|
||||
if (!this.isPrivate()) {
|
||||
throw new Error(
|
||||
'getSenderCertificateForDirectConversation should only be called for direct conversations'
|
||||
|
@ -3643,33 +3658,26 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
window.storage.get('phoneNumberSharingMode')
|
||||
);
|
||||
|
||||
let storageKey: 'senderCertificate' | 'senderCertificateNoE164';
|
||||
let certificateMode: SenderCertificateMode;
|
||||
switch (phoneNumberSharingMode) {
|
||||
case PhoneNumberSharingMode.Everybody:
|
||||
storageKey = 'senderCertificate';
|
||||
certificateMode = SenderCertificateMode.WithE164;
|
||||
break;
|
||||
case PhoneNumberSharingMode.ContactsOnly: {
|
||||
const isInSystemContacts = Boolean(this.get('name'));
|
||||
storageKey = isInSystemContacts
|
||||
? 'senderCertificate'
|
||||
: 'senderCertificateNoE164';
|
||||
certificateMode = isInSystemContacts
|
||||
? SenderCertificateMode.WithE164
|
||||
: SenderCertificateMode.WithoutE164;
|
||||
break;
|
||||
}
|
||||
case PhoneNumberSharingMode.Nobody:
|
||||
storageKey = 'senderCertificateNoE164';
|
||||
certificateMode = SenderCertificateMode.WithoutE164;
|
||||
break;
|
||||
default:
|
||||
throw missingCaseError(phoneNumberSharingMode);
|
||||
}
|
||||
|
||||
const result = window.storage.get<SerializedCertificateType>(storageKey);
|
||||
assert(
|
||||
result,
|
||||
`getSenderCertificateForDirectConversation: couldn't find a certificate stored in ${JSON.stringify(
|
||||
storageKey
|
||||
)}. Returning undefined`
|
||||
);
|
||||
return result;
|
||||
return senderCertificateService.get(certificateMode);
|
||||
}
|
||||
|
||||
// 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')) {
|
||||
profileKey = window.storage.get('profileKey');
|
||||
}
|
||||
const sendOptions = this.getSendOptions();
|
||||
const sendOptions = await this.getSendOptions();
|
||||
let promise;
|
||||
|
||||
if (this.isMe()) {
|
||||
|
@ -4174,7 +4182,7 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
const message = window.MessageController.register(model.id, model);
|
||||
this.addSingleMessage(message);
|
||||
|
||||
const options = this.getSendOptions();
|
||||
const options = await this.getSendOptions();
|
||||
message.send(
|
||||
this.wrapSend(
|
||||
// TODO: DESKTOP-724
|
||||
|
@ -4227,7 +4235,7 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
const message = window.MessageController.register(model.id, model);
|
||||
this.addSingleMessage(message);
|
||||
|
||||
const options = this.getSendOptions();
|
||||
const options = await this.getSendOptions();
|
||||
message.send(
|
||||
this.wrapSend(
|
||||
window.textsecure.messaging.leaveGroup(
|
||||
|
@ -4299,7 +4307,7 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
// to a contact, we need accessKeys for both.
|
||||
const {
|
||||
sendOptions,
|
||||
} = window.ConversationController.prepareForSend(
|
||||
} = await window.ConversationController.prepareForSend(
|
||||
window.ConversationController.getOurConversationId(),
|
||||
{ syncMessage: true }
|
||||
);
|
||||
|
@ -4314,7 +4322,7 @@ export class ConversationModel extends window.Backbone.Model<
|
|||
// Only send read receipts for accepted conversations
|
||||
if (window.storage.get('read-receipt-setting') && this.getAccepted()) {
|
||||
window.log.info(`Sending ${items.length} read receipts`);
|
||||
const convoSendOptions = this.getSendOptions();
|
||||
const convoSendOptions = await this.getSendOptions();
|
||||
const receiptsBySender = window._.groupBy(items, 'senderId');
|
||||
|
||||
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 =
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
sendMetadata[c.get('uuid')!] || sendMetadata[c.get('e164')!] || {};
|
||||
|
|
|
@ -1703,9 +1703,12 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
const {
|
||||
wrap,
|
||||
sendOptions,
|
||||
} = window.ConversationController.prepareForSend(ourNumber || ourUuid, {
|
||||
syncMessage: true,
|
||||
});
|
||||
} = await window.ConversationController.prepareForSend(
|
||||
ourNumber || ourUuid,
|
||||
{
|
||||
syncMessage: true,
|
||||
}
|
||||
);
|
||||
|
||||
await wrap(
|
||||
window.textsecure.messaging.syncViewOnceOpen(
|
||||
|
@ -2119,7 +2122,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
}
|
||||
|
||||
let promise;
|
||||
const options = conversation.getSendOptions();
|
||||
const options = await conversation.getSendOptions();
|
||||
|
||||
if (conversation.isPrivate()) {
|
||||
const [identifier] = recipients;
|
||||
|
@ -2312,9 +2315,10 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
return this.sendSyncMessageOnly(dataMessage);
|
||||
}
|
||||
|
||||
const { wrap, sendOptions } = window.ConversationController.prepareForSend(
|
||||
identifier
|
||||
);
|
||||
const {
|
||||
wrap,
|
||||
sendOptions,
|
||||
} = await window.ConversationController.prepareForSend(identifier);
|
||||
const promise = window.textsecure.messaging.sendMessageToIdentifier(
|
||||
identifier,
|
||||
body,
|
||||
|
@ -2533,7 +2537,10 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
async sendSyncMessage(): Promise<WhatIsThis> {
|
||||
const ourNumber = window.textsecure.storage.user.getNumber();
|
||||
const ourUuid = window.textsecure.storage.user.getUuid();
|
||||
const { wrap, sendOptions } = window.ConversationController.prepareForSend(
|
||||
const {
|
||||
wrap,
|
||||
sendOptions,
|
||||
} = await window.ConversationController.prepareForSend(
|
||||
ourUuid || ourNumber,
|
||||
{
|
||||
syncMessage: true,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -730,10 +730,10 @@ export class CallingClass {
|
|||
});
|
||||
}
|
||||
|
||||
private sendGroupCallUpdateMessage(
|
||||
private async sendGroupCallUpdateMessage(
|
||||
conversationId: string,
|
||||
eraId: string
|
||||
): void {
|
||||
): Promise<void> {
|
||||
const conversation = window.ConversationController.get(conversationId);
|
||||
if (!conversation) {
|
||||
window.log.error(
|
||||
|
@ -743,7 +743,7 @@ export class CallingClass {
|
|||
}
|
||||
|
||||
const groupV2 = conversation.getGroupV2Info();
|
||||
const sendOptions = conversation.getSendOptions();
|
||||
const sendOptions = await conversation.getSendOptions();
|
||||
if (!groupV2) {
|
||||
window.log.error(
|
||||
'Unable to send group call update message for conversation that lacks groupV2 info'
|
||||
|
@ -1258,7 +1258,7 @@ export class CallingClass {
|
|||
): Promise<boolean> {
|
||||
const conversation = window.ConversationController.get(remoteUserId);
|
||||
const sendOptions = conversation
|
||||
? conversation.getSendOptions()
|
||||
? await conversation.getSendOptions()
|
||||
: undefined;
|
||||
|
||||
if (!window.textsecure.messaging) {
|
||||
|
|
253
ts/services/senderCertificate.ts
Normal file
253
ts/services/senderCertificate.ts
Normal 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();
|
|
@ -1,16 +1,18 @@
|
|||
// Copyright 2019-2020 Signal Messenger, LLC
|
||||
// Copyright 2019-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
export function sendStickerPackSync(
|
||||
export async function sendStickerPackSync(
|
||||
packId: string,
|
||||
packKey: string,
|
||||
installed: boolean
|
||||
): void {
|
||||
): Promise<void> {
|
||||
const { ConversationController, textsecure, log } = window;
|
||||
const ourNumber = textsecure.storage.user.getNumber();
|
||||
const { wrap, sendOptions } = ConversationController.prepareForSend(
|
||||
const { wrap, sendOptions } = await ConversationController.prepareForSend(
|
||||
ourNumber,
|
||||
{ syncMessage: true }
|
||||
{
|
||||
syncMessage: true,
|
||||
}
|
||||
);
|
||||
|
||||
if (!textsecure.messaging) {
|
||||
|
|
257
ts/test-electron/services/senderCertificate_test.ts
Normal file
257
ts/test-electron/services/senderCertificate_test.ts
Normal 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));
|
||||
});
|
||||
});
|
||||
});
|
51
ts/test-electron/util/waitForOnline_test.ts
Normal file
51
ts/test-electron/util/waitForOnline_test.ts
Normal 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
3
ts/textsecure.d.ts
vendored
|
@ -10,6 +10,7 @@ import {
|
|||
import Crypto from './textsecure/Crypto';
|
||||
import MessageReceiver from './textsecure/MessageReceiver';
|
||||
import MessageSender from './textsecure/SendMessage';
|
||||
import SyncRequest from './textsecure/SyncRequest';
|
||||
import EventTarget from './textsecure/EventTarget';
|
||||
import { ByteBufferClass } from './window.d';
|
||||
import SendMessage, { SendOptionsType } from './textsecure/SendMessage';
|
||||
|
@ -90,7 +91,7 @@ export type TextSecureType = {
|
|||
MessageReceiver: typeof MessageReceiver;
|
||||
AccountManager: WhatIsThis;
|
||||
MessageSender: WhatIsThis;
|
||||
SyncRequest: WhatIsThis;
|
||||
SyncRequest: typeof SyncRequest;
|
||||
};
|
||||
|
||||
type StoredSignedPreKeyType = SignedPreKeyType & {
|
||||
|
|
|
@ -1255,7 +1255,7 @@ class MessageReceiverInner extends EventTarget {
|
|||
await sessionCipher.closeOpenSessionForDevice();
|
||||
|
||||
// Send a null message with newly-created session
|
||||
const sendOptions = conversation.getSendOptions();
|
||||
const sendOptions = await conversation.getSendOptions();
|
||||
await window.textsecure.messaging.sendNullMessage({ uuid }, sendOptions);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// Copyright 2020-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/* eslint-disable more/no-then */
|
||||
|
@ -9,9 +9,10 @@
|
|||
import EventTarget from './EventTarget';
|
||||
import MessageReceiver from './MessageReceiver';
|
||||
import MessageSender from './SendMessage';
|
||||
import { assert } from '../util/assert';
|
||||
|
||||
class SyncRequestInner extends EventTarget {
|
||||
receiver: MessageReceiver;
|
||||
private started = false;
|
||||
|
||||
contactSync?: boolean;
|
||||
|
||||
|
@ -23,7 +24,10 @@ class SyncRequestInner extends EventTarget {
|
|||
|
||||
ongroup: Function;
|
||||
|
||||
constructor(sender: MessageSender, receiver: MessageReceiver) {
|
||||
constructor(
|
||||
private sender: MessageSender,
|
||||
private receiver: MessageReceiver
|
||||
) {
|
||||
super();
|
||||
|
||||
if (
|
||||
|
@ -34,21 +38,30 @@ class SyncRequestInner extends EventTarget {
|
|||
'Tried to construct a SyncRequest without MessageSender and MessageReceiver'
|
||||
);
|
||||
}
|
||||
this.receiver = receiver;
|
||||
|
||||
this.oncontact = this.onContactSyncComplete.bind(this);
|
||||
receiver.addEventListener('contactsync', this.oncontact);
|
||||
|
||||
this.ongroup = this.onGroupSyncComplete.bind(this);
|
||||
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 { wrap, sendOptions } = window.ConversationController.prepareForSend(
|
||||
ourNumber,
|
||||
{
|
||||
syncMessage: true,
|
||||
}
|
||||
);
|
||||
const {
|
||||
wrap,
|
||||
sendOptions,
|
||||
} = await window.ConversationController.prepareForSend(ourNumber, {
|
||||
syncMessage: true,
|
||||
});
|
||||
|
||||
window.log.info('SyncRequest created. Sending config sync request...');
|
||||
wrap(sender.sendRequestConfigurationSyncMessage(sendOptions));
|
||||
|
@ -106,13 +119,20 @@ class SyncRequestInner extends EventTarget {
|
|||
}
|
||||
|
||||
export default class SyncRequest {
|
||||
constructor(sender: MessageSender, receiver: MessageReceiver) {
|
||||
const inner = new SyncRequestInner(sender, receiver);
|
||||
this.addEventListener = inner.addEventListener.bind(inner);
|
||||
this.removeEventListener = inner.removeEventListener.bind(inner);
|
||||
}
|
||||
private inner: SyncRequestInner;
|
||||
|
||||
addEventListener: (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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -804,7 +804,9 @@ export type WebAPIType = {
|
|||
}
|
||||
) => Promise<any>;
|
||||
getProvisioningSocket: () => WebSocket;
|
||||
getSenderCertificate: (withUuid?: boolean) => Promise<any>;
|
||||
getSenderCertificate: (
|
||||
withUuid?: boolean
|
||||
) => Promise<{ certificate: string }>;
|
||||
getSticker: (packId: string, stickerId: number) => Promise<any>;
|
||||
getStickerPackManifest: (packId: string) => Promise<StickerPackManifestType>;
|
||||
getStorageCredentials: MessageSender['getStorageCredentials'];
|
||||
|
|
|
@ -17020,7 +17020,7 @@
|
|||
"rule": "jQuery-wrap(",
|
||||
"path": "ts/shims/textsecure.js",
|
||||
"line": " wrap(textsecure.messaging.sendStickerPackSync([",
|
||||
"lineNumber": 14,
|
||||
"lineNumber": 16,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2020-02-07T19:52:28.522Z"
|
||||
},
|
||||
|
@ -17028,7 +17028,7 @@
|
|||
"rule": "jQuery-wrap(",
|
||||
"path": "ts/shims/textsecure.ts",
|
||||
"line": " wrap(",
|
||||
"lineNumber": 24,
|
||||
"lineNumber": 26,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2020-02-07T19:52:28.522Z"
|
||||
},
|
||||
|
@ -17100,7 +17100,7 @@
|
|||
"rule": "jQuery-wrap(",
|
||||
"path": "ts/textsecure/SyncRequest.js",
|
||||
"line": " wrap(sender.sendRequestConfigurationSyncMessage(sendOptions));",
|
||||
"lineNumber": 32,
|
||||
"lineNumber": 43,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2020-04-05T23:45:16.746Z"
|
||||
},
|
||||
|
@ -17108,7 +17108,7 @@
|
|||
"rule": "jQuery-wrap(",
|
||||
"path": "ts/textsecure/SyncRequest.js",
|
||||
"line": " wrap(sender.sendRequestBlockSyncMessage(sendOptions));",
|
||||
"lineNumber": 34,
|
||||
"lineNumber": 45,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2020-04-05T23:45:16.746Z"
|
||||
},
|
||||
|
@ -17116,7 +17116,7 @@
|
|||
"rule": "jQuery-wrap(",
|
||||
"path": "ts/textsecure/SyncRequest.js",
|
||||
"line": " wrap(sender.sendRequestContactSyncMessage(sendOptions))",
|
||||
"lineNumber": 36,
|
||||
"lineNumber": 47,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2020-04-05T23:45:16.746Z"
|
||||
},
|
||||
|
@ -17124,7 +17124,7 @@
|
|||
"rule": "jQuery-wrap(",
|
||||
"path": "ts/textsecure/SyncRequest.js",
|
||||
"line": " return wrap(sender.sendRequestGroupSyncMessage(sendOptions));",
|
||||
"lineNumber": 39,
|
||||
"lineNumber": 50,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2020-04-05T23:45:16.746Z"
|
||||
},
|
||||
|
@ -17132,7 +17132,7 @@
|
|||
"rule": "jQuery-wrap(",
|
||||
"path": "ts/textsecure/SyncRequest.ts",
|
||||
"line": " wrap(sender.sendRequestConfigurationSyncMessage(sendOptions));",
|
||||
"lineNumber": 54,
|
||||
"lineNumber": 67,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2020-04-05T23:45:16.746Z"
|
||||
},
|
||||
|
@ -17140,7 +17140,7 @@
|
|||
"rule": "jQuery-wrap(",
|
||||
"path": "ts/textsecure/SyncRequest.ts",
|
||||
"line": " wrap(sender.sendRequestBlockSyncMessage(sendOptions));",
|
||||
"lineNumber": 57,
|
||||
"lineNumber": 70,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2020-04-05T23:45:16.746Z"
|
||||
},
|
||||
|
@ -17148,7 +17148,7 @@
|
|||
"rule": "jQuery-wrap(",
|
||||
"path": "ts/textsecure/SyncRequest.ts",
|
||||
"line": " wrap(sender.sendRequestContactSyncMessage(sendOptions))",
|
||||
"lineNumber": 60,
|
||||
"lineNumber": 73,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2020-04-05T23:45:16.746Z"
|
||||
},
|
||||
|
@ -17156,7 +17156,7 @@
|
|||
"rule": "jQuery-wrap(",
|
||||
"path": "ts/textsecure/SyncRequest.ts",
|
||||
"line": " return wrap(sender.sendRequestGroupSyncMessage(sendOptions));",
|
||||
"lineNumber": 63,
|
||||
"lineNumber": 76,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2020-04-05T23:45:16.746Z"
|
||||
},
|
||||
|
@ -17172,7 +17172,7 @@
|
|||
"rule": "jQuery-wrap(",
|
||||
"path": "ts/textsecure/WebAPI.ts",
|
||||
"line": " const byteBuffer = window.dcodeIO.ByteBuffer.wrap(",
|
||||
"lineNumber": 2232,
|
||||
"lineNumber": 2234,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2020-09-08T23:07:22.682Z"
|
||||
},
|
||||
|
@ -17284,4 +17284,4 @@
|
|||
"reasonCategory": "falseMatch",
|
||||
"updated": "2021-04-06T23:11:04.431Z"
|
||||
}
|
||||
]
|
||||
]
|
21
ts/util/waitForOnline.ts
Normal file
21
ts/util/waitForOnline.ts
Normal 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
6
ts/window.d.ts
vendored
|
@ -70,8 +70,8 @@ import * as searchDuck from './state/ducks/search';
|
|||
import * as stickersDuck from './state/ducks/stickers';
|
||||
import * as conversationsSelectors from './state/selectors/conversations';
|
||||
import * as searchSelectors from './state/selectors/search';
|
||||
import { SendOptionsType } from './textsecure/SendMessage';
|
||||
import AccountManager from './textsecure/AccountManager';
|
||||
import { SendOptionsType } from './textsecure/SendMessage';
|
||||
import Data from './sql/Client';
|
||||
import { UserMessage } from './types/Message';
|
||||
import { PhoneNumberFormat } from 'google-libphonenumber';
|
||||
|
@ -97,6 +97,7 @@ import { ElectronLocaleType } from './util/mapToSupportLocale';
|
|||
import { SignalProtocolStore } from './LibSignalStore';
|
||||
import { StartupQueue } from './util/StartupQueue';
|
||||
import * as synchronousCrypto from './util/synchronousCrypto';
|
||||
import SyncRequest from './textsecure/SyncRequest';
|
||||
|
||||
export { Long } from 'long';
|
||||
|
||||
|
@ -169,7 +170,7 @@ declare global {
|
|||
getServerPublicParams: () => string;
|
||||
getSfuUrl: () => string;
|
||||
getSocketStatus: () => number;
|
||||
getSyncRequest: () => WhatIsThis;
|
||||
getSyncRequest: () => SyncRequest;
|
||||
getTitle: () => string;
|
||||
waitForEmptyEventQueue: () => Promise<void>;
|
||||
getVersion: () => string;
|
||||
|
@ -578,6 +579,7 @@ export type DCodeIOType = {
|
|||
fromString: (str: string | null) => Long;
|
||||
isLong: (obj: unknown) => obj is Long;
|
||||
};
|
||||
ProtoBuf: WhatIsThis;
|
||||
};
|
||||
|
||||
type MessageControllerType = {
|
||||
|
|
Loading…
Reference in a new issue