summaryrefslogtreecommitdiffstats
path: root/comm/mail/extensions/openpgp/content/modules/keyLookupHelper.jsm
blob: 621b61b2aed83e4e67c8f779c2fd016d3eb5c333 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */

"use strict";

const EXPORTED_SYMBOLS = ["KeyLookupHelper"];

const { XPCOMUtils } = ChromeUtils.importESModule(
  "resource://gre/modules/XPCOMUtils.sys.mjs"
);

const lazy = {};

XPCOMUtils.defineLazyModuleGetters(lazy, {
  CollectedKeysDB: "chrome://openpgp/content/modules/CollectedKeysDB.jsm",
  EnigmailDialog: "chrome://openpgp/content/modules/dialog.jsm",
  EnigmailKey: "chrome://openpgp/content/modules/key.jsm",
  EnigmailKeyRing: "chrome://openpgp/content/modules/keyRing.jsm",
  EnigmailKeyServer: "chrome://openpgp/content/modules/keyserver.jsm",
  EnigmailKeyserverURIs: "chrome://openpgp/content/modules/keyserverUris.jsm",
  EnigmailWkdLookup: "chrome://openpgp/content/modules/wkdLookup.jsm",
});

XPCOMUtils.defineLazyGetter(lazy, "l10n", () => {
  return new Localization(["messenger/openpgp/openpgp.ftl"], true);
});

var KeyLookupHelper = {
  /**
   * Internal helper function, search for keys by either keyID
   * or email address on a keyserver.
   * Returns additional flags regarding lookup and import.
   * Will never show feedback prompts.
   *
   * @param {string} mode - "interactive-import" or "silent-collection"
   *    In interactive-import mode, the user will be asked to confirm
   *    import of keys into the permanent keyring.
   *    In silent-collection mode, only updates to existing keys will
   *    be imported. New keys will only be added to CollectedKeysDB.
   * @param {nsIWindow} window - parent window
   * @param {string} identifier - search value, either key ID or fingerprint or email address.
   * @returns {object} flags
   * @returns {boolean} flags.keyImported - At least one key was imported.
   * @returns {boolean} flags.foundUpdated - At least one update for a local existing key was found and imported.
   * @returns {boolean} flags.foundUnchanged - All found keys are identical to already existing local keys.
   * @returns {boolean} flags.collectedForLater - At least one key was added to CollectedKeysDB.
   */

  isExpiredOrRevoked(keyTrust) {
    return keyTrust.match(/e/i) || keyTrust.match(/r/i);
  },

  async _lookupAndImportOnKeyserver(mode, window, identifier) {
    let keyImported = false;
    let foundUpdated = false;
    let foundUnchanged = false;
    let collectedForLater = false;

    let ksArray = lazy.EnigmailKeyserverURIs.getKeyServers();
    if (!ksArray.length) {
      return false;
    }

    let continueSearching = true;
    for (let ks of ksArray) {
      let foundKey;
      if (ks.startsWith("vks://")) {
        foundKey = await lazy.EnigmailKeyServer.downloadNoImport(
          identifier,
          ks
        );
      } else if (ks.startsWith("hkp://") || ks.startsWith("hkps://")) {
        foundKey =
          await lazy.EnigmailKeyServer.searchAndDownloadSingleResultNoImport(
            identifier,
            ks
          );
      }
      if (foundKey && "keyData" in foundKey) {
        let errorInfo = {};
        let keyList = await lazy.EnigmailKey.getKeyListFromKeyBlock(
          foundKey.keyData,
          errorInfo,
          false,
          true,
          false
        );
        // We might get a zero length keyList, if we refuse to use the key
        // that we received because of its properties.
        if (keyList && keyList.length == 1) {
          let oldKey = lazy.EnigmailKeyRing.getKeyById(keyList[0].fpr);
          if (oldKey) {
            await lazy.EnigmailKeyRing.importKeyDataSilent(
              window,
              foundKey.keyData,
              true,
              "0x" + keyList[0].fpr
            );

            let updatedKey = lazy.EnigmailKeyRing.getKeyById(keyList[0].fpr);
            // If new imported/merged key is equal to old key,
            // don't notify about new keys details.
            if (JSON.stringify(oldKey) !== JSON.stringify(updatedKey)) {
              foundUpdated = true;
              keyImported = true;
              if (mode == "interactive-import") {
                lazy.EnigmailDialog.keyImportDlg(
                  window,
                  keyList.map(a => a.id)
                );
              }
            } else {
              foundUnchanged = true;
            }
          } else {
            keyList = keyList.filter(k => k.userIds.length);
            keyList = keyList.filter(k => !this.isExpiredOrRevoked(k.keyTrust));
            if (keyList.length && mode == "interactive-import") {
              keyImported =
                await lazy.EnigmailKeyRing.importKeyDataWithConfirmation(
                  window,
                  keyList,
                  foundKey.keyData,
                  true
                );
              if (keyImported) {
                // In interactive mode, don't offer the user to import keys multiple times.
                // When silently collecting keys, it's fine to discover everything we can.
                continueSearching = false;
              }
            }
            if (!keyImported) {
              collectedForLater = true;
              let db = await lazy.CollectedKeysDB.getInstance();
              for (let newKey of keyList) {
                // If key is known in the db: merge + update.
                let key = await db.mergeExisting(newKey, foundKey.keyData, {
                  uri: lazy.EnigmailKeyServer.serverReqURL(
                    `0x${newKey.fpr}`,
                    ks
                  ),
                  type: "keyserver",
                });
                await db.storeKey(key);
              }
            }
          }
        } else {
          if (keyList && keyList.length > 1) {
            throw new Error("Unexpected multiple results from keyserver " + ks);
          }
          console.log(
            "failed to process data retrieved from keyserver " +
              ks +
              ": " +
              errorInfo.value
          );
        }
      }
      if (!continueSearching) {
        break;
      }
    }

    return { keyImported, foundUpdated, foundUnchanged, collectedForLater };
  },

  /**
   * Search online for keys by key ID on keyserver.
   *
   * @param {string} mode - "interactive-import" or "silent-collection"
   *    In interactive-import mode, the user will be asked to confirm
   *    import of keys into the permanent keyring.
   *    In silent-collection mode, only updates to existing keys will
   *    be imported. New keys will only be added to CollectedKeysDB.
   * @param {nsIWindow} window - parent window
   * @param {string} keyId - the key ID to search for.
   * @param {boolean} giveFeedbackToUser - false to be silent,
   *    true to show feedback to user after search and import is complete.
   * @returns {boolean} - true if at least one key was imported.
   */
  async lookupAndImportByKeyID(mode, window, keyId, giveFeedbackToUser) {
    if (!/^0x/i.test(keyId)) {
      keyId = "0x" + keyId;
    }
    let importResult = await this._lookupAndImportOnKeyserver(
      mode,
      window,
      keyId
    );
    if (
      mode == "interactive-import" &&
      giveFeedbackToUser &&
      !importResult.keyImported
    ) {
      let msgId;
      if (importResult.foundUnchanged) {
        msgId = "no-update-found";
      } else {
        msgId = "no-key-found2";
      }
      let value = await lazy.l10n.formatValue(msgId);
      lazy.EnigmailDialog.alert(window, value);
    }
    return importResult.keyImported;
  },

  /**
   * Search online for keys by email address.
   * Will search both WKD and keyserver.
   *
   * @param {string} mode - "interactive-import" or "silent-collection"
   *    In interactive-import mode, the user will be asked to confirm
   *    import of keys into the permanent keyring.
   *    In silent-collection mode, only updates to existing keys will
   *    be imported. New keys will only be added to CollectedKeysDB.
   * @param {nsIWindow} window - parent window
   * @param {string} email - the email address to search for.
   * @param {boolean} giveFeedbackToUser - false to be silent,
   *    true to show feedback to user after search and import is complete.
   * @returns {boolean} - true if at least one key was imported.
   */
  async lookupAndImportByEmail(mode, window, email, giveFeedbackToUser) {
    let resultKeyImported = false;

    let wkdKeyImported = false;
    let wkdFoundUnchanged = false;

    let wkdResult;
    let wkdUrl;
    if (lazy.EnigmailWkdLookup.isWkdAvailable(email)) {
      wkdUrl = await lazy.EnigmailWkdLookup.getDownloadUrlFromEmail(
        email,
        true
      );
      wkdResult = await lazy.EnigmailWkdLookup.downloadKey(wkdUrl);
      if (!wkdResult) {
        wkdUrl = await lazy.EnigmailWkdLookup.getDownloadUrlFromEmail(
          email,
          false
        );
        wkdResult = await lazy.EnigmailWkdLookup.downloadKey(wkdUrl);
      }
    }

    if (!wkdResult) {
      console.debug("searchKeysOnInternet no wkd data for " + email);
    } else {
      let errorInfo = {};
      let keyList = await lazy.EnigmailKey.getKeyListFromKeyBlock(
        wkdResult,
        errorInfo,
        false,
        true,
        false,
        true
      );
      if (!keyList) {
        console.debug(
          "failed to process data retrieved from WKD server: " + errorInfo.value
        );
      } else {
        let existingKeys = [];
        let newKeys = [];

        for (let wkdKey of keyList) {
          let oldKey = lazy.EnigmailKeyRing.getKeyById(wkdKey.fpr);
          if (oldKey) {
            await lazy.EnigmailKeyRing.importKeyDataSilent(
              window,
              wkdKey.pubKey,
              true,
              "0x" + wkdKey.fpr
            );

            let updatedKey = lazy.EnigmailKeyRing.getKeyById(wkdKey.fpr);
            // If new imported/merged key is equal to old key,
            // don't notify about new keys details.
            if (JSON.stringify(oldKey) !== JSON.stringify(updatedKey)) {
              // If a caller ever needs information what we found,
              // this is the place to set: wkdFoundUpdated = true
              existingKeys.push(wkdKey.id);
            } else {
              wkdFoundUnchanged = true;
            }
          } else if (wkdKey.userIds.length) {
            newKeys.push(wkdKey);
          }
        }

        if (existingKeys.length) {
          if (mode == "interactive-import") {
            lazy.EnigmailDialog.keyImportDlg(window, existingKeys);
          }
          wkdKeyImported = true;
        }

        newKeys = newKeys.filter(k => !this.isExpiredOrRevoked(k.keyTrust));
        if (newKeys.length && mode == "interactive-import") {
          wkdKeyImported =
            wkdKeyImported ||
            (await lazy.EnigmailKeyRing.importKeyArrayWithConfirmation(
              window,
              newKeys,
              true
            ));
        }
        if (!wkdKeyImported) {
          // If a caller ever needs information what we found,
          // this is the place to set: wkdCollectedForLater = true
          let db = await lazy.CollectedKeysDB.getInstance();
          for (let newKey of newKeys) {
            // If key is known in the db: merge + update.
            let key = await db.mergeExisting(newKey, newKey.pubKey, {
              uri: wkdUrl,
              type: "wkd",
            });
            await db.storeKey(key);
          }
        }
      }
    }

    let { keyImported, foundUnchanged } =
      await this._lookupAndImportOnKeyserver(mode, window, email);
    resultKeyImported = wkdKeyImported || keyImported;

    if (
      mode == "interactive-import" &&
      giveFeedbackToUser &&
      !resultKeyImported &&
      !keyImported
    ) {
      let msgId;
      if (wkdFoundUnchanged || foundUnchanged) {
        msgId = "no-update-found";
      } else {
        msgId = "no-key-found2";
      }
      let value = await lazy.l10n.formatValue(msgId);
      lazy.EnigmailDialog.alert(window, value);
    }

    return resultKeyImported;
  },

  /**
   * This function will perform discovery of new or updated OpenPGP
   * keys using various mechanisms.
   *
   * @param {string} mode - "interactive-import" or "silent-collection"
   * @param {string} email - search for keys for this email address,
   *                         (parameter allowed to be null or empty)
   * @param {string[]} keyIds - KeyIDs that should be updated.
   *                            (parameter allowed to be null or empty)
   *
   * @returns {boolean} - Returns true if at least one key was imported.
   */
  async fullOnlineDiscovery(mode, window, email, keyIds) {
    // Try to get updates for all existing keys from keyserver,
    // by key ID, to get updated validy/revocation info.
    // (A revoked key on the keyserver might have no user ID.)
    let atLeastoneImport = false;
    if (keyIds) {
      for (let keyId of keyIds) {
        // Ensure the function call goes first in the logic or expression,
        // to ensure it's always called, even if atLeastoneImport is already true.
        let rv = await this.lookupAndImportByKeyID(mode, window, keyId, false);
        atLeastoneImport = rv || atLeastoneImport;
      }
    }
    // Now check for updated or new keys by email address
    let rv2 = await this.lookupAndImportByEmail(mode, window, email, false);
    atLeastoneImport = rv2 || atLeastoneImport;
    return atLeastoneImport;
  },
};