Periodically refresh both sender certificates

This commit is contained in:
Evan Hahn 2021-04-02 11:23:47 -05:00 committed by Josh Perez
parent 99928ee831
commit b95dd207ca
3 changed files with 53 additions and 50 deletions

View file

@ -8,6 +8,7 @@ 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 * as refreshSenderCertificate from './refreshSenderCertificate';
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';
@ -2069,11 +2070,17 @@ export async function startApp(): Promise<void> {
window.Whisper.events, window.Whisper.events,
newVersion newVersion
); );
refreshSenderCertificate.initialize({
events: window.Whisper.events, [SenderCertificateMode.WithE164, SenderCertificateMode.WithoutE164].forEach(
storage: window.storage, mode => {
navigator, 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

@ -39,6 +39,11 @@ type ValidatorType = {
): Promise<void>; ): Promise<void>;
}; };
export const enum SenderCertificateMode {
WithE164,
WithoutE164,
}
export type SerializedCertificateType = { export type SerializedCertificateType = {
serialized: ArrayBuffer; serialized: ArrayBuffer;
}; };

View file

@ -1,14 +1,24 @@
// Copyright 2018-2021 Signal Messenger, LLC // Copyright 2018-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { once } from 'lodash';
import * as log from './logging/log'; 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 ONE_DAY = 24 * 60 * 60 * 1000; // one day
const MINIMUM_TIME_LEFT = 2 * 60 * 60 * 1000; // two hours const MINIMUM_TIME_LEFT = 2 * 60 * 60 * 1000; // two hours
let timeout: null | ReturnType<typeof setTimeout> = null; let timeout: null | ReturnType<typeof setTimeout> = null;
let scheduledTime: null | number = null; let scheduledTime: null | number = null;
let scheduleNext: null | (() => void) = 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 // We need to refresh our own profile regularly to account for newly-added devices which
// do not support unidentified delivery. // do not support unidentified delivery.
@ -22,28 +32,39 @@ function refreshOurProfile() {
export function initialize({ export function initialize({
events, events,
storage, storage,
mode,
navigator, navigator,
}: Readonly<{ }: Readonly<{
events: { events: {
on: (name: string, callback: () => void) => void; on: (name: string, callback: () => void) => void;
}; };
storage: typeof window.storage; storage: typeof window.storage;
mode: SenderCertificateMode;
navigator: Navigator; navigator: Navigator;
}>): void { }>): void {
// We don't want to set up all of the below functions, but we do want to ensure that our let storageKey: 'senderCertificate' | 'senderCertificateNoE164';
// refresh timer is up-to-date. let logString: string;
if (scheduleNext) { switch (mode) {
scheduleNext(); case SenderCertificateMode.WithE164:
return; storageKey = 'senderCertificate';
logString = 'sender certificate WITH E164';
break;
case SenderCertificateMode.WithoutE164:
storageKey = 'senderCertificateNoE164';
logString = 'sender certificate WITHOUT E164';
break;
default:
throw missingCaseError(mode);
} }
runWhenOnline(); runWhenOnline();
removeOldKey(storage);
events.on('timetravel', scheduleNextRotation); events.on('timetravel', scheduleNextRotation);
function scheduleNextRotation() { function scheduleNextRotation() {
const now = Date.now(); const now = Date.now();
const certificate = storage.get('senderCertificate'); const certificate = storage.get(storageKey);
if (!certificate || !certificate.expires) { if (!certificate || !certificate.expires) {
setTimeoutForNextRun(scheduledTime || now); setTimeoutForNextRun(scheduledTime || now);
@ -68,16 +89,7 @@ export function initialize({
setTimeoutForNextRun(time); setTimeoutForNextRun(time);
} }
// Keeping this entrypoint around so more inialize() calls just kick the timing async function saveCert(certificate: string): Promise<void> {
scheduleNext = scheduleNextRotation;
async function saveCert({
certificate,
key,
}: {
certificate: string;
key: string;
}): Promise<void> {
const arrayBuffer = window.Signal.Crypto.base64ToArrayBuffer(certificate); const arrayBuffer = window.Signal.Crypto.base64ToArrayBuffer(certificate);
const decodedContainer = window.textsecure.protobuf.SenderCertificate.decode( const decodedContainer = window.textsecure.protobuf.SenderCertificate.decode(
arrayBuffer arrayBuffer
@ -92,19 +104,11 @@ export function initialize({
expires: decodedCert.expires.toNumber(), expires: decodedCert.expires.toNumber(),
serialized: arrayBuffer, serialized: arrayBuffer,
}; };
await storage.put(key, toSave); await storage.put(storageKey, toSave);
}
async function removeOldKey(): Promise<void> {
const oldCertKey = 'senderCertificateWithUuid';
const oldUuidCert = storage.get(oldCertKey);
if (oldUuidCert) {
await storage.remove(oldCertKey);
}
} }
async function run(): Promise<void> { async function run(): Promise<void> {
log.info('refreshSenderCertificate: Getting new certificate...'); log.info(`refreshSenderCertificate: Getting new ${logString}...`);
try { try {
const OLD_USERNAME = storage.get('number_id'); const OLD_USERNAME = storage.get('number_id');
const USERNAME = storage.get('uuid_id'); const USERNAME = storage.get('uuid_id');
@ -114,29 +118,16 @@ export function initialize({
password: PASSWORD, password: PASSWORD,
}); });
const omitE164 = true; const omitE164 = mode === SenderCertificateMode.WithoutE164;
const [ const { certificate } = await server.getSenderCertificate(omitE164);
{ certificate },
{ certificate: certificateWithNoE164 },
] = await Promise.all([
server.getSenderCertificate(),
server.getSenderCertificate(omitE164),
]);
await Promise.all([ await saveCert(certificate);
saveCert({ certificate, key: 'senderCertificate' }),
saveCert({
certificate: certificateWithNoE164,
key: 'senderCertificateNoE164',
}),
removeOldKey(),
]);
scheduledTime = null; scheduledTime = null;
scheduleNextRotation(); scheduleNextRotation();
} catch (error) { } catch (error) {
log.error( log.error(
'refreshSenderCertificate: Get failed. Trying again in five minutes...', `refreshSenderCertificate: Get failed for ${logString}. Trying again in five minutes...`,
error && error.stack ? error.stack : error error && error.stack ? error.stack : error
); );
@ -171,7 +162,7 @@ export function initialize({
if (scheduledTime !== time || !timeout) { if (scheduledTime !== time || !timeout) {
log.info( log.info(
'Next sender certificate refresh scheduled for', `refreshSenderCertificate: Next ${logString} refresh scheduled for`,
new Date(time).toISOString() new Date(time).toISOString()
); );
} }