summaryrefslogtreecommitdiffstats
path: root/comm/mail/extensions/openpgp/content/modules/CollectedKeysDB.jsm
blob: 0c4581e9f1cccb24fbd2dbce3559937dae7f0bf1 (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
/* 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/. */

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

const lazy = {};

XPCOMUtils.defineLazyModuleGetters(lazy, {
  RNP: "chrome://openpgp/content/modules/RNP.jsm",
});

/**
 * Database of collected OpenPGP keys.
 */
const EXPORTED_SYMBOLS = ["CollectedKeysDB"];

var log = console.createInstance({
  prefix: "openpgp",
  maxLogLevel: "Warn",
  maxLogLevelPref: "openpgp.loglevel",
});

/**
 * Class that handles storage of OpenPGP keys that were found through various
 * sources.
 */
class CollectedKeysDB {
  /**
   * @param {IDBDatabase} database
   */
  constructor(db) {
    this.db = db;
    this.db.onclose = () => {
      log.debug("DB closed!");
    };
    this.db.onabort = () => {
      log.debug("DB operation aborted!");
    };
  }

  /**
   * Get a database instance.
   *
   * @returns {CollectedKeysDB} a instance.
   */
  static async getInstance() {
    return new Promise((resolve, reject) => {
      const VERSION = 1;
      let DBOpenRequest = indexedDB.open("openpgp_cache", VERSION);
      DBOpenRequest.onupgradeneeded = event => {
        let db = event.target.result;
        if (event.oldVersion < 1) {
          // Create an objectStore for this database
          let objectStore = db.createObjectStore("seen_keys", {
            keyPath: "fingerprint",
          });
          objectStore.createIndex("emails", "emails", {
            unique: false,
            multiEntry: true,
          });
          objectStore.createIndex("created", "created");
          objectStore.createIndex("expires", "expires");
          objectStore.createIndex("timestamp", "timestamp");
        }
        log.debug(`Database ready at version ${VERSION}`);
      };
      DBOpenRequest.onerror = event => {
        log.debug(`Error loading database: ${DBOpenRequest.error.message}`);
        reject(DBOpenRequest.error);
      };
      DBOpenRequest.onsuccess = event => {
        let keyDb = new CollectedKeysDB(DBOpenRequest.result);
        resolve(keyDb);
      };
    });
  }

  /**
   * @typedef {object} CollectedKey - Key details.
   * @property {string[]} emails - Lowercase email addresses associated with this key
   * @property {string} fingerprint - Key fingerprint.
   * @property {string[]} userIds - UserIds for this key.
   * @property {string} id - Key ID with a 0x prefix.
   * @property {string} pubKey - The public key data.
   * @property {Date} created - Key creation date.
   * @property {Date} expires - Key expiry date.
   * @property {Date} timestamp - Timestamp of last time this key was saved/updated.
   * @property {object[]} sources - List of sources we saw this key.
   * @property {string} sources.uri - URI of the source.
   * @property {string} sources.type - Type of source (e.g. attachment, wkd, keyserver)
   * @property {string} sources.description - Description of the source, if any. E.g. the attachment name.
   */

  /**
   * Store a key.
   *
   * @param {CollectedKey} key - the key to store.
   */
  async storeKey(key) {
    if (key.fingerprint?.length != 40) {
      throw new Error(`Invalid fingerprint: ${key.fingerprint}`);
    }
    return new Promise((resolve, reject) => {
      let transaction = this.db.transaction(["seen_keys"], "readwrite");
      transaction.oncomplete = () => {
        log.debug(`Stored key 0x${key.id} for ${key.emails}`);
        let window = Services.wm.getMostRecentWindow("mail:3pane");
        window.dispatchEvent(
          new CustomEvent("keycollected", { detail: { key } })
        );
        resolve();
      };
      transaction.onerror = () => {
        reject(transaction.error);
      };
      // log.debug(`Storing key: ${JSON.stringify(key, null, 2)}`);
      key.timestamp = new Date();
      transaction.objectStore("seen_keys").put(key);
      transaction.commit();
    });
  }

  /**
   * Find key for fingerprint.
   *
   * @param {string} fingerprint - Fingerprint to find key for.
   * @returns {CollectedKey} the key found, or null.
   */
  async findKeyForFingerprint(fingerprint) {
    if (fingerprint?.length != 40) {
      throw new Error(`Invalid fingerprint: ${fingerprint}`);
    }
    return new Promise((resolve, reject) => {
      let request = this.db
        .transaction("seen_keys")
        .objectStore("seen_keys")
        .get(fingerprint);

      request.onsuccess = event => {
        // If we didn't find anything, result is undefined. If so return null
        // so that we make it clear we found "something", but it was nothing.
        resolve(request.result || null);
      };
      request.onerror = event => {
        log.debug(`Find key failed: ${request.error.message}`);
        reject(request.error);
      };
    });
  }

  /**
   * Find keys for email.
   *
   * @param {string} email - Email to find keys for.
   * @returns {CollectedKey[]} the keys found.
   */
  async findKeysForEmail(email) {
    email = email.toLowerCase();
    return new Promise((resolve, reject) => {
      let keys = [];
      let index = this.db
        .transaction("seen_keys")
        .objectStore("seen_keys")
        .index("emails");
      index.openCursor(IDBKeyRange.only(email)).onsuccess = function (event) {
        let cursor = event.target.result;
        if (!cursor) {
          // All results done.
          resolve(keys);
          return;
        }
        keys.push(cursor.value);
        cursor.continue();
      };
    });
  }

  /**
   * Find existing key in the database, and use RNP to merge such a key
   * with the passed in keyBlock.
   * Merging will always use the email addresses and user IDs of the merged key,
   * which causes old revoked entries to be removed.
   * We keep the list of previously seen source locations.
   *
   * @param {EnigmailKeyOb} - key object
   * @param {string} keyBlock - public key to merge
   * @param {object} source - source of the information
   * @param {string} source.type - source type
   * @param {string} source.uri - source uri
   * @param {string?} source.description - source description
   * @returns {CollectedKey} merged key - not yet stored in the database
   */
  async mergeExisting(keyobj, keyBlock, source) {
    let fpr = keyobj.fpr;
    let existing = await this.findKeyForFingerprint(fpr);
    let newKey;
    let pubKey;
    if (existing) {
      pubKey = await lazy.RNP.mergePublicKeyBlocks(
        fpr,
        existing.pubKey,
        keyBlock
      );
      // Don't use EnigmailKey.getKeyListFromKeyBlock interactive.
      // Use low level API for obtaining key list, we don't want to
      // poison the app key cache.
      // We also don't want to obtain any additional revocation certs.
      let keys = await lazy.RNP.getKeyListFromKeyBlockImpl(
        pubKey,
        true,
        false,
        false,
        false
      );
      if (!keys || !keys.length) {
        throw new Error("Error getting keys from block");
      }
      if (keys.length != 1) {
        throw new Error(`Got ${keys.length} keys for fpr=${fpr}`);
      }
      newKey = keys[0];
    } else {
      pubKey = keyBlock;
      newKey = keyobj;
    }

    let key = {
      emails: newKey.userIds.map(uid =>
        MailServices.headerParser
          .makeFromDisplayAddress(uid.userId)[0]
          ?.email.toLowerCase()
          .trim()
      ),
      fingerprint: newKey.fpr,
      userIds: newKey.userIds.map(uid => uid.userId),
      id: newKey.keyId,
      pubKey,
      created: new Date(newKey.keyCreated * 1000),
      expires: newKey.expiryTime ? new Date(newKey.expiryTime * 1000) : null,
      sources: [source],
    };
    if (existing) {
      // Keep existing sources meta information.
      let sourceType = source.type;
      let sourceURI = source.uri;
      for (let oldSource of existing.sources.filter(
        s => !(s.type == sourceType && s.uri == sourceURI)
      )) {
        key.sources.push(oldSource);
      }
    }
    return key;
  }

  /**
   * Delete keys for email.
   *
   * @param {string} email - Email to delete keys for.
   */
  async deleteKeysForEmail(email) {
    email = email.toLowerCase();
    return new Promise((resolve, reject) => {
      let transaction = this.db.transaction(["seen_keys"], "readwrite");
      let objectStore = transaction.objectStore("seen_keys");
      let request = objectStore.index("emails").openKeyCursor();
      request.onsuccess = event => {
        let cursor = request.result;
        if (cursor) {
          objectStore.delete(cursor.primaryKey);
          cursor.continue();
        } else {
          log.debug(`Deleted all keys for ${email}.`);
        }
      };
      transaction.oncomplete = () => {
        log.debug(`Keys gone for email ${email}.`);
        resolve(email);
      };
      transaction.onerror = event => {
        log.debug(
          `Could not delete keys for email ${email}: ${transaction.error.message}`
        );
        reject(transaction.error);
      };
    });
  }

  /**
   * Delete key by fingerprint.
   *
   * @param {string} fingerprint - fingerprint of key to delete.
   */
  async deleteKey(fingerprint) {
    if (fingerprint.length != 40) {
      throw new Error(`Invalid fingerprint: ${fingerprint}`);
    }
    return new Promise((resolve, reject) => {
      let transaction = this.db.transaction(["seen_keys"], "readwrite");
      let request = transaction.objectStore("seen_keys").delete(fingerprint);
      request.onsuccess = () => {
        log.debug(`Keys gone for fingerprint ${fingerprint}.`);
        resolve(fingerprint);
      };
      request.onerror = event => {
        log.debug(
          `Could not delete keys for fingerprint ${fingerprint}: ${transaction.error.message}`
        );
        reject(transaction.error);
      };
    });
  }

  /**
   * Clear out data from the database.
   */
  async reset() {
    return new Promise((resolve, reject) => {
      let transaction = this.db.transaction(["seen_keys"], "readwrite");
      let objectStore = transaction.objectStore("seen_keys");
      transaction.oncomplete = () => {
        log.debug(`Objectstore cleared.`);
        resolve();
      };
      transaction.onerror = () => {
        log.debug(`Could not clear objectstore: ${transaction.error.message}`);
        reject(transaction.error);
      };
      objectStore.clear();
      transaction.commit();
    });
  }

  /**
   * Delete database.
   */
  static async deleteDb() {
    return new Promise((resolve, reject) => {
      let DBOpenRequest = indexedDB.deleteDatabase("seen_keys");
      DBOpenRequest.onsuccess = () => {
        log.debug(`Success deleting database.`);
        resolve();
      };
      DBOpenRequest.onerror = () => {
        log.debug(`Error deleting database: ${DBOpenRequest.error.message}`);
        reject(DBOpenRequest.error);
      };
    });
  }
}