signal-desktop/ts/refreshSenderCertificate.ts
2021-04-05 18:08:50 -04:00

178 lines
5 KiB
TypeScript

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