1/*
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5 */
6
7"use strict";
8
9const EXPORTED_SYMBOLS = ["EnigmailKeyRefreshService"];
10
11const { XPCOMUtils } = ChromeUtils.import(
12  "resource://gre/modules/XPCOMUtils.jsm"
13);
14
15Cu.importGlobalProperties(["crypto"]);
16
17XPCOMUtils.defineLazyModuleGetters(this, {
18  EnigmailKeyRing: "chrome://openpgp/content/modules/keyRing.jsm",
19  EnigmailKeyServer: "chrome://openpgp/content/modules/keyserver.jsm",
20  EnigmailKeyserverURIs: "chrome://openpgp/content/modules/keyserverUris.jsm",
21  EnigmailLog: "chrome://openpgp/content/modules/log.jsm",
22  Services: "resource://gre/modules/Services.jsm",
23});
24
25const ONE_HOUR_IN_MILLISEC = 60 * 60 * 1000;
26
27let gTimer = null;
28
29function getTimer() {
30  if (gTimer === null) {
31    gTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
32  }
33  return gTimer;
34}
35
36function calculateMaxTimeForRefreshInMilliseconds(totalPublicKeys) {
37  const millisecondsAvailableForRefresh =
38    Services.prefs.getIntPref("temp.openpgp.hoursPerWeekEnigmailIsOn") *
39    ONE_HOUR_IN_MILLISEC;
40  return Math.floor(millisecondsAvailableForRefresh / totalPublicKeys);
41}
42
43function calculateWaitTimeInMilliseconds(totalPublicKeys) {
44  const randomNumber = crypto.getRandomValues(new Uint32Array(1));
45  const maxTimeForRefresh = calculateMaxTimeForRefreshInMilliseconds(
46    totalPublicKeys
47  );
48  const minDelay =
49    Services.prefs.getIntPref("temp.openpgp.refreshMinDelaySeconds") * 1000;
50
51  EnigmailLog.DEBUG(
52    "keyRefreshService.jsm: Wait time = random number: " +
53      randomNumber +
54      " % max time for refresh: " +
55      maxTimeForRefresh +
56      "\n"
57  );
58
59  let millisec = randomNumber % maxTimeForRefresh;
60  if (millisec < minDelay) {
61    millisec += minDelay;
62  }
63
64  EnigmailLog.DEBUG(
65    "keyRefreshService.jsm: Time until next refresh in milliseconds: " +
66      millisec +
67      "\n"
68  );
69
70  return millisec;
71}
72
73function refreshKey() {
74  const timer = getTimer();
75  refreshWith(EnigmailKeyServer, timer, true);
76}
77
78function restartTimerInOneHour(timer) {
79  timer.initWithCallback(
80    refreshKey,
81    ONE_HOUR_IN_MILLISEC,
82    Ci.nsITimer.TYPE_ONE_SHOT
83  );
84}
85
86function setupNextRefresh(timer, waitTime) {
87  timer.initWithCallback(refreshKey, waitTime, Ci.nsITimer.TYPE_ONE_SHOT);
88}
89
90function logMissingInformation(keyIdsExist, validKeyserversExist) {
91  if (!keyIdsExist) {
92    EnigmailLog.DEBUG(
93      "keyRefreshService.jsm: No keys available to refresh yet. Will recheck in an hour.\n"
94    );
95  }
96  if (!validKeyserversExist) {
97    EnigmailLog.DEBUG(
98      "keyRefreshService.jsm: Either no keyservers exist or the protocols specified are invalid. Will recheck in an hour.\n"
99    );
100  }
101}
102
103function getRandomKeyId(randomNumber) {
104  const keyRingLength = EnigmailKeyRing.getAllKeys().keyList.length;
105
106  if (keyRingLength === 0) {
107    return null;
108  }
109
110  return EnigmailKeyRing.getAllKeys().keyList[randomNumber % keyRingLength]
111    .keyId;
112}
113
114function refreshKeyIfReady(keyserver, readyToRefresh, keyId) {
115  if (readyToRefresh) {
116    EnigmailLog.DEBUG(
117      "keyRefreshService.jsm: refreshing key ID " + keyId + "\n"
118    );
119    return keyserver.download(keyId);
120  }
121
122  return Promise.resolve(0);
123}
124
125async function refreshWith(keyserver, timer, readyToRefresh) {
126  const keyId = getRandomKeyId(crypto.getRandomValues(new Uint32Array(1)));
127  const keyIdsExist = keyId !== null;
128  const validKeyserversExist = EnigmailKeyserverURIs.validKeyserversExist();
129  const ioService = Services.io;
130
131  if (keyIdsExist && validKeyserversExist) {
132    if (ioService && !ioService.offline) {
133      // don't try to refresh if we are offline
134      await refreshKeyIfReady(keyserver, readyToRefresh, keyId);
135    } else {
136      EnigmailLog.DEBUG(
137        "keyRefreshService.jsm: offline - not refreshing any key\n"
138      );
139    }
140    const waitTime = calculateWaitTimeInMilliseconds(
141      EnigmailKeyRing.getAllKeys().keyList.length
142    );
143    setupNextRefresh(timer, waitTime);
144  } else {
145    logMissingInformation(keyIdsExist, validKeyserversExist);
146    restartTimerInOneHour(timer);
147  }
148}
149
150/**
151 * Starts a process to continuously refresh keys on a random time interval and in random order.
152 *
153 * The default time period for all keys to be refreshed is one week, although the user can specifically set this in their preferences
154 * The wait time to refresh the next key is selected at random, from a range of zero milliseconds to the maximum time to refresh a key
155 *
156 * The maximum time to refresh a single key is calculated by averaging the total refresh time by the total number of public keys to refresh
157 * For example, if a user has 12 public keys to refresh, the maximum time to refresh a single key (by default) will be: milliseconds per week divided by 12
158 *
159 * This service does not keep state, it will restart each time Enigmail is initialized.
160 *
161 * @param keyserver   | dependency injected for testability
162 */
163function start(keyserver) {
164  if (Services.prefs.getBoolPref("temp.openpgp.keyRefreshOn")) {
165    EnigmailLog.DEBUG("keyRefreshService.jsm: Started\n");
166    const timer = getTimer();
167    refreshWith(keyserver, timer, false);
168  }
169}
170
171/*
172  This module intializes the continuous key refresh functionality. This includes randomly selecting th key to refresh and the timing to wait between each refresh
173*/
174
175var EnigmailKeyRefreshService = {
176  start,
177};
178