Signal-Desktop/js/modules/refresh_sender_certificate.js

154 lines
4.3 KiB
JavaScript

// Copyright 2018-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* global window, setTimeout, clearTimeout, textsecure, WebAPI, ConversationController */
module.exports = {
initialize,
};
const ONE_DAY = 24 * 60 * 60 * 1000; // one day
const MINIMUM_TIME_LEFT = 2 * 60 * 60 * 1000; // two hours
let timeout = null;
let scheduledTime = null;
let scheduleNext = null;
// 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 = ConversationController.getOurConversationId();
const conversation = ConversationController.get(ourId);
conversation.getProfiles();
}
function initialize({ events, storage, navigator, logger }) {
// We don't want to set up all of the below functions, but we do want to ensure that our
// refresh timer is up-to-date.
if (scheduleNext) {
scheduleNext();
return;
}
runWhenOnline();
events.on('timetravel', scheduleNextRotation);
function scheduleNextRotation() {
const now = Date.now();
const certificate = storage.get('senderCertificate');
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);
}
// Keeping this entrypoint around so more inialize() calls just kick the timing
scheduleNext = scheduleNextRotation;
async function run() {
logger.info('refreshSenderCertificate: Getting new certificate...');
try {
const OLD_USERNAME = storage.get('number_id');
const USERNAME = storage.get('uuid_id');
const PASSWORD = storage.get('password');
const server = WebAPI.connect({
username: USERNAME || OLD_USERNAME,
password: PASSWORD,
});
const { certificate } = await server.getSenderCertificate();
const arrayBuffer = window.Signal.Crypto.base64ToArrayBuffer(certificate);
const decodedContainer = textsecure.protobuf.SenderCertificate.decode(
arrayBuffer
);
const decodedCert = 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,
};
storage.put('senderCertificate', toSave);
const oldCertKey = 'senderCertificateWithUuid';
const oldUuidCert = storage.get(oldCertKey);
if (oldUuidCert) {
await storage.remove(oldCertKey);
}
scheduledTime = null;
scheduleNextRotation();
} catch (error) {
logger.error(
'refreshSenderCertificate: Get failed. 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 {
logger.info(
'refreshSenderCertificate: Offline. Will update certificate when online...'
);
const listener = () => {
logger.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) {
logger.info(
'Next sender certificate refresh scheduled for',
new Date(time).toISOString()
);
}
scheduledTime = time;
const waitTime = Math.max(0, time - now);
clearTimeout(timeout);
timeout = setTimeout(runWhenOnline, waitTime);
}
}