autocrypt.jsm 34.3 KB
Newer Older
1
2
/* 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
3
 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5
6
7
8
9
10
11
12
13
14

"use strict";

/**
 *  Module for dealing with received Autocrypt headers, level 0
 *  See details at https://github.com/mailencrypt/autocrypt
 */

var EXPORTED_SYMBOLS = ["EnigmailAutocrypt"];

const Cr = Components.results;
15

16
Components.utils.importGlobalProperties(["crypto"]);
17

18
19
20
21
22
23
24
25
26
27
28
29
const jsmime = ChromeUtils.import("resource:///modules/jsmime.jsm").jsmime;
const EnigmailCore = ChromeUtils.import("chrome://enigmail/content/modules/core.jsm").EnigmailCore;
const EnigmailLog = ChromeUtils.import("chrome://enigmail/content/modules/log.jsm").EnigmailLog;
const EnigmailLocale = ChromeUtils.import("chrome://enigmail/content/modules/locale.jsm").EnigmailLocale;
const EnigmailFuncs = ChromeUtils.import("chrome://enigmail/content/modules/funcs.jsm").EnigmailFuncs;
const EnigmailMime = ChromeUtils.import("chrome://enigmail/content/modules/mime.jsm").EnigmailMime;
const EnigmailSqliteDb = ChromeUtils.import("chrome://enigmail/content/modules/sqliteDb.jsm").EnigmailSqliteDb;
const PromiseUtils = ChromeUtils.import("resource://gre/modules/PromiseUtils.jsm").PromiseUtils;
const EnigmailTimer = ChromeUtils.import("chrome://enigmail/content/modules/timer.jsm").EnigmailTimer;
const EnigmailKey = ChromeUtils.import("chrome://enigmail/content/modules/key.jsm").EnigmailKey;
const EnigmailKeyRing = ChromeUtils.import("chrome://enigmail/content/modules/keyRing.jsm").EnigmailKeyRing;
const EnigmailOpenPGP = ChromeUtils.import("chrome://enigmail/content/modules/openpgp.jsm").EnigmailOpenPGP;
30
const getOpenPGPLibrary = ChromeUtils.import("chrome://enigmail/content/modules/stdlib/openpgp-loader.jsm").getOpenPGPLibrary;
31
32
33
34
35
36
37
38
39
const EnigmailRNG = ChromeUtils.import("chrome://enigmail/content/modules/rng.jsm").EnigmailRNG;
const EnigmailSend = ChromeUtils.import("chrome://enigmail/content/modules/send.jsm").EnigmailSend;
const EnigmailStreams = ChromeUtils.import("chrome://enigmail/content/modules/streams.jsm").EnigmailStreams;
const EnigmailArmor = ChromeUtils.import("chrome://enigmail/content/modules/armor.jsm").EnigmailArmor;
const EnigmailData = ChromeUtils.import("chrome://enigmail/content/modules/data.jsm").EnigmailData;
const EnigmailRules = ChromeUtils.import("chrome://enigmail/content/modules/rules.jsm").EnigmailRules;
const EnigmailStdlib = ChromeUtils.import("chrome://enigmail/content/modules/stdlib.jsm").EnigmailStdlib;
const EnigmailPrefs = ChromeUtils.import("chrome://enigmail/content/modules/prefs.jsm").EnigmailPrefs;
const EnigmailConstants = ChromeUtils.import("chrome://enigmail/content/modules/constants.jsm").EnigmailConstants;
40
const EnigmailCryptoAPI = ChromeUtils.import("chrome://enigmail/content/modules/cryptoAPI.jsm").EnigmailCryptoAPI;
41
42

var gCreatedSetupIds = [];
43
44
45
46
47

var EnigmailAutocrypt = {
  /**
   * Process the "Autocrypt:" header and if successful store the update in the database
   *
48
49
50
51
   * @param {String} fromAddr:               Address of sender (From: header)
   * @param {Array of String} headerDataArr: all instances of the Autocrypt: header found in the message
   * @param {String or Number} dateSent:     "Date:" field of the message as readable string or in seconds after 1970-01-01
   * @param {Boolean} autoCryptEnabled:      if true, autocrypt is enabled for the context of the message
52
   *
53
   * @return {Promise<Number>}: success: 0 = success, 1+ = failure
54
   */
55
  processAutocryptHeader: async function(fromAddr, headerDataArr, dateSent, autoCryptEnabled = false, isGossip = false) {
56
    EnigmailLog.DEBUG("autocrypt.jsm: processAutocryptHeader(): from=" + fromAddr + "\n");
57
    let conn;
58

59
    try {
60
61
62
63
      // critical parameters: {param: mandatory}
      const CRITICAL = {
        addr: true,
        keydata: true,
64
        type: false, // That's actually oboslete according to the Level 1 spec.
65
66
67
        "prefer-encrypt": false
      };

68
69
      try {
        fromAddr = EnigmailFuncs.stripEmail(fromAddr).toLowerCase();
70
71
      }
      catch (ex) {
72
        throw "processAutocryptHeader error " + ex;
73
      }
74
75
76
77
78
79
80
81
82
83
84
85
      let foundTypes = {};
      let paramArr = [];

      for (let hdrNum = 0; hdrNum < headerDataArr.length; hdrNum++) {

        let hdr = headerDataArr[hdrNum].replace(/[\r\n \t]/g, "");
        let k = hdr.search(/keydata=/);
        if (k > 0) {
          let d = hdr.substr(k);
          if (d.search(/"/) < 0) {
            hdr = hdr.replace(/keydata=/, 'keydata="') + '"';
          }
86
87
        }

88
        paramArr = EnigmailMime.getAllParameters(hdr);
89

90
91
92
93
94
        for (let i in CRITICAL) {
          if (CRITICAL[i]) {
            // found mandatory parameter
            if (!(i in paramArr)) {
              EnigmailLog.DEBUG("autocrypt.jsm: processAutocryptHeader: cannot find param '" + i + "'\n");
95
              return 1; // do nothing if not all mandatory parts are present
96
            }
97
98
99
          }
        }

100
101
102
103
        for (let i in paramArr) {
          if (i.substr(0, 1) !== "_") {
            if (!(i in CRITICAL)) {
              EnigmailLog.DEBUG("autocrypt.jsm: processAutocryptHeader: unknown critical param " + i + "\n");
104
              return 2; // do nothing if an unknown critical parameter is found
105
            }
106
107
108
          }
        }

109
110
111
        paramArr.addr = paramArr.addr.toLowerCase();

        if (fromAddr !== paramArr.addr) {
112
          EnigmailLog.DEBUG("autocrypt.jsm: processAutocryptHeader: from Addr " + fromAddr + " != " + paramArr.addr.toLowerCase() + "\n");
113

114
          return 3;
115
        }
116

117
        if (!("type" in paramArr)) {
118
          paramArr.type = (isGossip ? "1g" : "1");
119
120
        }
        else {
121
122
123
          paramArr.type = paramArr.type.toLowerCase();
          if (paramArr.type !== "1") {
            EnigmailLog.DEBUG("autocrypt.jsm: processAutocryptHeader: unknown type " + paramArr.type + "\n");
124
            return 4; // we currently only support 1 (=OpenPGP)
125
          }
126
127
        }

128
129
        try {
          let keyData = atob(paramArr.keydata);
130
131
        }
        catch (ex) {
132
          EnigmailLog.DEBUG("autocrypt.jsm: processAutocryptHeader: key is not base64-encoded\n");
133
          return 5;
134
135
136
137
        }

        if (paramArr.type in foundTypes) {
          EnigmailLog.DEBUG("autocrypt.jsm: processAutocryptHeader: duplicate header for type=" + paramArr.type + "\n");
138
          return 6; // do not process anything if more than one Autocrypt header for the same type is found
139
        }
140

141
        foundTypes[paramArr.type] = 1;
142
143
      }

144
145
146
147
      if (isGossip) {
        paramArr["prefer-encrypt"] = "nopreference";
      }

148
      if (!("prefer-encrypt" in paramArr)) {
149
        paramArr["prefer-encrypt"] = "nopreference";
150
      }
151

152
153
154
      let lastDate;
      if (typeof dateSent === "string") {
        lastDate = jsmime.headerparser.parseDateHeader(dateSent);
155
156
      }
      else {
157
158
        lastDate = new Date(dateSent * 1000);
      }
159
160
161
162
163
      let now = new Date();
      if (lastDate > now) {
        lastDate = now;
      }
      paramArr.dateSent = lastDate;
Patrick Brunschwig's avatar
Patrick Brunschwig committed
164

165
166
167
168
      if (("_enigmail_artificial" in paramArr) && (paramArr._enigmail_artificial === "yes")) {
        if ("_enigmail_fpr" in paramArr) {
          paramArr.fpr = paramArr._enigmail_fpr;
        }
169

170
171
        paramArr.keydata = "";
        paramArr.autocryptDate = 0;
172
173
      }
      else {
174
        paramArr.autocryptDate = lastDate;
175
176
      }

177
178
      try {
        conn = await EnigmailSqliteDb.openDatabase();
179
180
      }
      catch (ex) {
181
182
183
        EnigmailLog.DEBUG("autocrypt.jsm: processAutocryptHeader: could not open database\n");
        return 7;
      }
184

185
186
187
188
      let resultObj = await findUserRecord(conn, [fromAddr], paramArr.type);
      EnigmailLog.DEBUG("autocrypt.jsm: got " + resultObj.numRows + " rows\n");
      if (resultObj.data.length === 0) {
        await appendUser(conn, paramArr);
189
190
      }
      else {
191
192
        await updateUser(conn, paramArr, resultObj.data, autoCryptEnabled);
      }
193

194
195
196
      EnigmailLog.DEBUG("autocrypt.jsm: OK - closing connection\n");
      conn.close();
      return 0;
197
198
    }
    catch (err) {
199
200
201
202
      EnigmailLog.DEBUG("autocrypt.jsm: error - closing connection: " + err + "\n");
      conn.close();
      return 8;
    }
203
204
  },

205
  /**
206
207
208
   * Import autocrypt OpenPGP keys into regular keyring for a given list of email addresses
   * @param {Array of String} emailAddr: email addresses
   * @param {Boolean} acceptGossipKeys: import keys received via gossip
209
   *
210
   * @return {Promise<Array of keyObj>}
211
   */
212
213
214
215
  importAutocryptKeys: async function(emailAddr, acceptGossipKeys = false) {
    EnigmailLog.DEBUG("autocrypt.jsm: importAutocryptKeys()\n");

    let keyArr = await this.getOpenPGPKeyForEmail(emailAddr);
216
217
    if (!keyArr) return [];

218
219
220
221
222
223
224
225
226
227
228
229
230
    let importedKeys = [];
    let now = new Date();
    let prev = null;

    for (let i = 0; i < keyArr.length; i++) {
      if (prev && prev.email === keyArr[i].email && prev.type === "1" && keyArr[i].type === "1g") {
        // skip if we have "gossip" key preceeded by a "regular" key
        continue;
      }
      if (!acceptGossipKeys && keyArr[i].type === "1g") {
        EnigmailLog.DEBUG(`autocrypt.jsm: importAutocryptKeys: skipping gossip key for ${keyArr[i].email}\n`);
        continue;
      }
231

232
233
234
235
236
237
      prev = keyArr[i];
      if ((now - keyArr[i].lastAutocrypt) / (1000 * 60 * 60 * 24) < 366) {
        // only import keys received less than 12 months ago
        try {
          let keyData = atob(keyArr[i].keyData);
          if (keyData.length > 1) {
238
            importedKeys = await this.applyKeyFromKeydata(keyData, keyArr[i].email, keyArr[i].state, keyArr[i].type);
239
          }
240
241
        }
        catch (ex) {
242
243
244
245
          EnigmailLog.DEBUG("autocrypt.jsm importAutocryptKeys: exception " + ex.toString() + "\n");
        }
      }
    }
246

247
    return importedKeys;
248
249
  },

250
251
252
  /**
   * Import given key data and set the per-recipient rule accordingly
   *
Patrick Brunschwig's avatar
Patrick Brunschwig committed
253
   * @param {String} keyData - String key data (BLOB, binary form)
254
255
256
257
258
259
260
261
262
263
   * @param {String} email - email address associated with key
   * @param {String} autocryptState - mutual or nopreference
   * @param {String} type - autocrypt header type (1 / 1g)
   *
   * @return {Promise<Array of keys>} list of imported keys
   */
  applyKeyFromKeydata: async function(keyData, email, autocryptState, type) {
    let keysObj = {};
    let importedKeys = [];

264
    let pubkey = EnigmailOpenPGP.bytesToArmor(getOpenPGPLibrary().enums.armor.public_key, keyData);
265
    await EnigmailKeyRing.importKeyAsync(null, false, pubkey, "", {}, keysObj);
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287

    if (keysObj.value) {
      importedKeys = importedKeys.concat(keysObj.value);

      if (keysObj.value.length > 0) {
        let key = EnigmailKeyRing.getKeyById(keysObj.value[0]);

        // enable encryption if state (prefer-encrypt) is "mutual";
        // otherwise, disable it explicitely
        let signEncrypt = (autocryptState === "mutual" ? 1 : 0);

        if (key && key.fpr) {
          let ruleObj = {
            email: `{${EnigmailConstants.AC_RULE_PREFIX}${email}}`,
            keyList: `0x${key.fpr}`,
            sign: signEncrypt,
            encrypt: signEncrypt,
            pgpMime: 2,
            flags: 0
          };

          EnigmailRules.insertOrUpdateRule(ruleObj);
288
          await this.setKeyImported(null, email);
289
290
291
292
293
294
295
        }
      }
    }

    return importedKeys;
  },

296
297
298
  /**
   * Update key in the Autocrypt database to mark it "imported in keyring"
   */
299
300
  setKeyImported: async function(connection, email) {
    EnigmailLog.DEBUG(`autocrypt.jsm: setKeyImported(${email})\n`);
301
302
303
304
305
    try {
      let conn = connection;
      if (!conn) {
        conn = await EnigmailSqliteDb.openDatabase();
      }
306
      let updateStr = "update autocrypt_keydata set keyring_inserted = '1' where email = :email;";
307
308

      let updateObj = {
309
        email: email.toLowerCase()
310
311
312
313
314
315
316
317
318
319
320
321
322
      };

      await new Promise((resolve, reject) =>
        conn.executeTransaction(function _trx() {
          conn.execute(updateStr, updateObj).then(r => {
            resolve(r);
          }).catch(err => {
            EnigmailLog.DEBUG(`autocrypt.jsm: setKeyImported: error ${err}\n`);
            reject(err);
          });
        }));

      if (!connection) conn.close();
323
324
    }
    catch (err) {
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
      EnigmailLog.DEBUG(`autocrypt.jsm: setKeyImported: error ${err}\n`);
      throw err;
    }
  },

  /**
   * Go through all emails in the autocrypt store and determine which keys already
   * have a per-recipient rule
   */
  updateAllImportedKeys: async function() {
    EnigmailLog.DEBUG(`autocrypt.jsm: updateAllImportedKeys()\n`);
    try {
      let conn = await EnigmailSqliteDb.openDatabase();

      let rows = [];
      await conn.execute("select email, type from autocrypt_keydata where type = '1';", {},
        function _onRow(record) {
          rows.push(record.getResultByName("email"));
        });

      for (let i in rows) {
346
        let r = EnigmailRules.getRuleByEmail(`${EnigmailConstants.AC_RULE_PREFIX}${rows[i]}`);
347
348
349
350
351
352
353
354
        if (r) {
          await this.setKeyImported(conn, rows[i], "1");
        }

      }
      EnigmailLog.DEBUG(`autocrypt.jsm: updateAllImportedKeys done\n`);

      conn.close();
355
356
    }
    catch (err) {
357
358
359
360
361
      EnigmailLog.DEBUG(`autocrypt.jsm: updateAllImportedKeys: error ${err}\n`);
      throw err;
    }
  },

362
363
  /**
   * Find an autocrypt OpenPGP key for a given list of email addresses
364
   * @param emailAddr: Array of String - email addresses
365
   *
366
367
   * @return Promise(<Array of Object>)
   *      Object: {fpr, keyData, lastAutocrypt}
368
369
   */
  getOpenPGPKeyForEmail: function(emailAddr) {
370
    EnigmailLog.DEBUG("autocrypt.jsm: getOpenPGPKeyForEmail(" + emailAddr.join(",") + ")\n");
371
372
373
374

    let conn;

    return new Promise((resolve, reject) => {
375
      EnigmailSqliteDb.openDatabase().then(
376
377
        function onConnection(connection) {
          conn = connection;
378
          return findUserRecord(conn, emailAddr, "1,1g");
379
380
        },
        function onError(error) {
381
          EnigmailLog.DEBUG("autocrypt.jsm: getOpenPGPKeyForEmail: could not open database\n");
382
          reject("getOpenPGPKeyForEmail1 error " + error);
383
384
385
        }
      ).then(
        function gotData(resultObj) {
386
          EnigmailLog.DEBUG("autocrypt.jsm: getOpenPGPKeyForEmail got " + resultObj.numRows + " rows\n");
Patrick Brunschwig's avatar
Patrick Brunschwig committed
387
388
          conn.close();

389
390
          if (resultObj.data.length === 0) {
            resolve(null);
391
392
          }
          else {
393
394
395
396
            let retArr = [];
            for (let i in resultObj.data) {
              let record = resultObj.data[i];
              retArr.push({
397
                email: record.getResultByName("email"),
398
399
                fpr: record.getResultByName("fpr"),
                keyData: record.getResultByName("keydata"),
400
                state: record.getResultByName("state"),
401
                type: record.getResultByName("type"),
402
403
404
405
406
407
408
409
410
411
                lastAutocrypt: new Date(record.getResultByName("last_seen_autocrypt"))
              });
            }

            resolve(retArr);
          }
        }
      ).
      catch((err) => {
        conn.close();
412
        reject("getOpenPGPKeyForEmail: error " + err);
413
414
      });
    });
415
416
417
418
419
420
421
  },

  /**
   * Create Autocrypt Setup Message
   *
   * @param identity: Object - nsIMsgIdentity
   *
422
423
424
   * @return Promise({str, passwd}):
   *             msg:    String - complete setup message
   *             passwd: String - backup password
425
   */
426
  createSetupMessage: async function(identity) {
427
428
    EnigmailLog.DEBUG("autocrypt.jsm: createSetupMessage()\n");

429
430
    const openPGPjs = getOpenPGPLibrary();

431
432
433
434
435
436
    let keyId = "";
    let key;
    try {
      if (!EnigmailCore.getService(null, false)) {
        throw 0;
      }
437

438
439
440
      if (identity.getIntAttribute("pgpKeyMode") === 1) {
        keyId = identity.getCharAttribute("pgpkeyId");
      }
441

442
443
444
445
446
447
      if (keyId.length > 0) {
        key = EnigmailKeyRing.getKeyById(keyId);
      }
      else {
        key = EnigmailKeyRing.getSecretKeyByUserId(identity.email);
      }
448

449
450
451
452
      if (!key) {
        EnigmailLog.DEBUG("autocrypt.jsm: createSetupMessage: no key found for " + identity.email + "\n");
        throw 1;
      }
453

454
      let keyData = key.getSecretKey(true).keyData;
455

456
457
458
459
      if (!keyData || keyData.length === 0) {
        EnigmailLog.DEBUG("autocrypt.jsm: createSetupMessage: no key found for " + identity.email + "\n");
        throw 1;
      }
460

461
462
      let ac = EnigmailFuncs.getAccountForIdentity(identity);
      let preferEncrypt = ac.incomingServer.getIntValue("acPreferEncrypt") > 0 ? "mutual" : "nopreference";
463

464
465
466
      let innerMsg = EnigmailArmor.replaceArmorHeaders(keyData, {
        'Autocrypt-Prefer-Encrypt': preferEncrypt
      }) + '\r\n';
467

468
469
470
471
472
473
474
475
      let bkpCode = createBackupCode();
      let enc = {
        message: await openPGPjs.createMessage({
          text: innerMsg
        }),
        passwords: bkpCode,
        format: "armored"
      };
476

477
478
479
480
481
482
483
484
485
486
487
488
      // create symmetrically encrypted message
      try {
        let msg = await openPGPjs.encrypt(enc);
        let msgData = EnigmailArmor.replaceArmorHeaders(msg, {
          'Passphrase-Format': 'numeric9x4',
          'Passphrase-Begin': bkpCode.substr(0, 2)
        }).replace(/\n/g, "\r\n");

        let m = createBackupOuterMsg(identity.email, msgData);
        return {
          msg: m,
          passwd: bkpCode
489
        };
490
      }
491
492
493
      catch (e) {
        EnigmailLog.DEBUG("autocrypt.jsm: createSetupMessage: error " + e + "\n");
        throw 2;
494
      }
495
496
497
498
499
500
    }
    catch (ex) {
      EnigmailLog.DEBUG("autocrypt.jsm: createSetupMessage: error " + ex.toString() + "\n");
      EnigmailLog.DEBUG("  stack:\n" + ex.stack + "\n");
      throw 4;
    }
501
502
503
504
505
506
507
508
509
510
511
512
513
  },

  /**
   * Create and send the Autocrypt Setup Message to yourself
   * The message is sent asynchronously.
   *
   * @param identity: Object - nsIMsgIdentity
   *
   * @return Promise(passwd):
   *   passwd: String - backup password
   *
   */
  sendSetupMessage: function(identity) {
514
    EnigmailLog.DEBUG("autocrypt.jsm: sendSetupMessage()\n");
515
516

    let self = this;
517
    return new Promise((resolve, reject) => {
518
      self.createSetupMessage(identity).then(res => {
519
520
521
522
523
        let composeFields = Cc["@mozilla.org/messengercompose/composefields;1"].createInstance(Ci.nsIMsgCompFields);
        composeFields.characterSet = "UTF-8";
        composeFields.messageId = EnigmailRNG.generateRandomString(27) + "-enigmail";
        composeFields.from = identity.email;
        composeFields.to = identity.email;
524
        gCreatedSetupIds.push(composeFields.messageId);
525
526
527
528
529
530
531

        let now = new Date();
        let mimeStr = "Message-Id: " + composeFields.messageId + "\r\n" +
          "Date: " + now.toUTCString() + "\r\n" + res.msg;

        if (EnigmailSend.sendMessage(mimeStr, composeFields, null)) {
          resolve(res.passwd);
532
533
        }
        else {
534
535
          reject(99);
        }
536
537
      });
    });
538
539
  },

540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577

  /**
   * get the data of the attachment of a setup message
   *
   * @param attachmentUrl: String - URL of the attachment
   *
   * @return Promise(Object):
   *            attachmentData:   String - complete attachment data
   *            passphraseFormat: String - extracted format from the header (e.g. numeric9x4) [optional]
   *            passphraseHint:   String - 1st two digits of the password [optional]
   */
  getSetupMessageData: function(attachmentUrl) {
    EnigmailLog.DEBUG("autocrypt.jsm: getSetupMessageData()\n");

    return new Promise((resolve, reject) => {
      let s = EnigmailStreams.newStringStreamListener(data => {
        let start = {},
          end = {};
        let msgType = EnigmailArmor.locateArmoredBlock(data, 0, "", start, end, {});

        if (msgType === "MESSAGE") {
          EnigmailLog.DEBUG("autocrypt.jsm: getSetupMessageData: got backup key\n");
          let armorHdr = EnigmailArmor.getArmorHeaders(data);

          let passphraseFormat = "generic";
          if ("passphrase-format" in armorHdr) {
            passphraseFormat = armorHdr["passphrase-format"];
          }
          let passphraseHint = "";
          if ("passphrase-begin" in armorHdr) {
            passphraseHint = armorHdr["passphrase-begin"];
          }

          resolve({
            attachmentData: data,
            passphraseFormat: passphraseFormat,
            passphraseHint: passphraseHint
          });
578
579
        }
        else {
580
581
582
583
584
585
586
587
588
589
590
591
592
593
          reject("getSetupMessageData");
        }
      });

      let channel = EnigmailStreams.createChannel(attachmentUrl);
      channel.asyncOpen(s, null);
    });
  },

  /**
   * @return Promise(Object):
   *          fpr:           String - FPR of the imported key
   *          preferEncrypt: String - Autocrypt preferEncrypt value (e.g. mutual)
   */
Patrick Brunschwig's avatar
Patrick Brunschwig committed
594
  handleBackupMessage: async function(passwd, attachmentData, fromAddr) {
595
596
    EnigmailLog.DEBUG("autocrypt.jsm: handleBackupMessage()\n");

597
598
    const cApi = EnigmailCryptoAPI();
    const keyManagement = cApi.getKeyManagement();
599
    const openPGPjs = getOpenPGPLibrary();
600

Patrick Brunschwig's avatar
Patrick Brunschwig committed
601
602
603
    let start = {},
      end = {};
    EnigmailArmor.locateArmoredBlock(attachmentData, 0, "", start, end, {});
604

Patrick Brunschwig's avatar
Patrick Brunschwig committed
605
606
    let encMessage = await openPGPjs.readMessage({
      armoredMessage: attachmentData.substring(start.value, end.value)
607
    });
Patrick Brunschwig's avatar
Patrick Brunschwig committed
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652

    let enc = {
      message: encMessage,
      passwords: [passwd],
      format: 'utf8'
    };

    try {
      let msg = await openPGPjs.decrypt(enc);

      EnigmailLog.DEBUG("autocrypt.jsm: handleBackupMessage: data: " + msg.data.length + "\n");

      let setupData = importSetupKey(msg.data);
      if (setupData) {
        let resultObj = {
          returnCode: 0
        };

        if (cApi.supportsFeature("ownertrust")) {
          resultObj = await keyManagement.setKeyTrust(null, "0x" + setupData.fpr, "5");
        }
        if (resultObj.returnCode === 0) {
          let id = EnigmailStdlib.getIdentityForEmail(EnigmailFuncs.stripEmail(fromAddr).toLowerCase());
          let ac = EnigmailFuncs.getAccountForIdentity(id.identity);
          ac.incomingServer.setBoolValue("enableAutocrypt", true);
          ac.incomingServer.setIntValue("acPreferEncrypt", (setupData.preferEncrypt === "mutual" ? 1 : 0));
          id.identity.setCharAttribute("pgpkeyId", "0x" + setupData.fpr);
          id.identity.setBoolAttribute("enablePgp", true);
          id.identity.setBoolAttribute("pgpSignEncrypted", true);
          id.identity.setIntAttribute("pgpKeyMode", 1);
          return setupData;
        }
        else {
          throw "keyImportFailed";
        }

      }
      else {
        throw "keyImportFailed";
      }
    }
    catch (err) {
      EnigmailLog.DEBUG(`autocrypt.jsm: handleBackupMessage: caught exception: ${err.toString()}\n`);
      throw "wrongPasswd";
    }
653
654
655
656
657
658
659
  },

  /**
   * Determine if a message id was self-created (only during same TB session)
   */
  isSelfCreatedSetupMessage: function(messageId) {
    return (gCreatedSetupIds.indexOf(messageId) >= 0);
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
  },

  /**
   * Check if an account is set up with OpenPGP and if the configured key is valid
   *
   * @param emailAddr: String - email address identifying the account
   *
   * @return Boolean: true: account is valid / false: OpenPGP not configured or key not valid
   */
  isAccountSetupForPgp: function(emailAddr) {
    let id = EnigmailStdlib.getIdentityForEmail(EnigmailFuncs.stripEmail(emailAddr).toLowerCase());
    let keyObj = null;

    if (!(id && id.identity)) return false;
    if (!id.identity.getBoolAttribute("enablePgp")) return false;

    if (id.identity.getIntAttribute("pgpKeyMode") === 1) {
      keyObj = EnigmailKeyRing.getKeyById(id.identity.getCharAttribute("pgpkeyId"));
678
679
    }
    else {
680
681
682
683
684
685
686
687
688
689
690
691
      keyObj = EnigmailKeyRing.getSecretKeyByUserId(emailAddr);
    }

    if (!keyObj) return false;
    if (!keyObj.secretAvailable) return false;

    let o = keyObj.getEncryptionValidity();
    if (!o.keyValid) return false;
    o = keyObj.getSigningValidity();
    if (!o.keyValid) return false;

    return true;
692
693
694
  },

  /**
695
696
   * Delete the record for a user from the autocrypt keystore
   * The record with the highest precedence is deleted (i.e. type=1 before type=1g)
697
   */
698
699
  deleteUser: async function(email, type) {
    EnigmailLog.DEBUG(`autocrypt.jsm: deleteUser(${email})\n`);
700
    let conn = await EnigmailSqliteDb.openDatabase();
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721

    let updateStr = "delete from autocrypt_keydata where email = :email and type = :type";
    let updateObj = {
      email: email,
      type: type
    };

    await new Promise((resolve, reject) => {
      conn.executeTransaction(function _trx() {
        conn.execute(updateStr, updateObj).then(
          function _ok() {
            resolve();
          }
        ).catch(function _err() {
          reject("update failed");
        });
      });
    });
    EnigmailLog.DEBUG(" deletion complete\n");

    conn.close();
722
  }
723

724
725
726
727
728
729
};

/**
 * Find the database record for a given email address and type
 *
 * @param connection: Object - SQLite connection
730
 * @param emails      Array of String - Email addresses to search
731
 * @param type:       String - types to search (in lowercase), separated by comma
732
 *
733
734
735
 * @return {Promise<Object>}:
 *   numRows: number of results
 *   data:    array of RowObject. Query columns using data[i].getResultByName(columnName);
736
 */
737
async function findUserRecord(connection, emails, type) {
738
739
  EnigmailLog.DEBUG("autocrypt.jsm: findUserRecord\n");

740
  let data = [];
741
742
  let t = type.split(/[ ,]+/);

743
  let queryParam = {
744
745
    e0: emails[0],
    t0: t[0]
746
747
  };

748
749
  let numRows = 0;

750
751
752
753
754
755
  let search = ":e0";
  for (let i = 1; i < emails.length; i++) {
    search += ", :e" + i;
    queryParam["e" + i] = emails[i].toLowerCase();
  }

756
757
758
759
760
  let typeParam = ":t0";
  for (let i = 1; i < t.length; i++) {
    typeParam += ", :t" + i;
    queryParam["t" + i] = t[i];
  }
761

762
763
764
765
766
767
768
769
  try {
    await connection.execute(
      "select * from autocrypt_keydata where email in (" + search + ") and type in (" + typeParam + ") order by email, type", queryParam,
      function _onRow(row) {
        EnigmailLog.DEBUG("autocrypt.jsm: findUserRecord - got row\n");
        data.push(row);
        ++numRows;
      });
770
771
  }
  catch (x) {
772
773
774
775
776
777
778
779
    EnigmailLog.DEBUG(`autocrypt.jsm: findUserRecord: error ${x}\n`);
    throw x;
  }

  return {
    data: data,
    numRows: numRows
  };
780
781
782
783
784
785
786
787
788
789
}

/**
 * Create new database record for an Autorypt header
 *
 * @param connection: Object - SQLite connection
 * @param paramsArr:  Object - the Autocrypt header parameters
 *
 * @return Promise
 */
790
async function appendUser(connection, paramsArr) {
791
  EnigmailLog.DEBUG("autocrypt.jsm: appendUser(" + paramsArr.addr + ")\n");
792

793
  if (!("fpr" in paramsArr)) {
794
    await getFprForKey(paramsArr);
795
796
  }

797
  return new Promise((resolve, reject) => {
798

799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
    if (paramsArr.autocryptDate == 0) {
      // do not insert record for non-autocrypt mail
      resolve();
      return;
    }

    connection.executeTransaction(function _trx() {
      connection.execute("insert into autocrypt_keydata (email, keydata, fpr, type, last_seen_autocrypt, last_seen, state) values " +
        "(:email, :keyData, :fpr, :type, :lastAutocrypt, :lastSeen, :state)", {
          email: paramsArr.addr.toLowerCase(),
          keyData: paramsArr.keydata,
          fpr: ("fpr" in paramsArr ? paramsArr.fpr : ""),
          type: paramsArr.type,
          lastAutocrypt: paramsArr.dateSent.toJSON(),
          lastSeen: paramsArr.dateSent.toJSON(),
          state: paramsArr["prefer-encrypt"]
        }).then(
        function _ok() {
          EnigmailLog.DEBUG("autocrypt.jsm: appendUser - OK\n");
          resolve();
        }
      ).catch(function _err() {
        reject("appendUser");
      });
823
824
825
826
827
828
829
830
831
832
    });
  });
}

/**
 * Update the record for an email address and type, if the email we got is newer
 * than the latest record we already stored
 *
 * @param connection: Object - SQLite connection
 * @param paramsArr:  Object - the Autocrypt header parameters
833
 * @param resultRows: Array of mozIStorageRow - records stored in the database
834
 * @param autoCryptEnabled: Boolean: is autocrypt enabled for this transaction
835
836
837
 *
 * @return Promise
 */
838
async function updateUser(connection, paramsArr, resultRows, autoCryptEnabled) {
839
840
  EnigmailLog.DEBUG("autocrypt.jsm: updateUser\n");

841
  let currData = resultRows[0];
842
  let deferred = PromiseUtils.defer();
843
844

  let lastSeen = new Date(currData.getResultByName("last_seen"));
845
  let lastAutocrypt = new Date(currData.getResultByName("last_seen_autocrypt"));
846
  let notGossip = (currData.getResultByName("state") !== "gossip");
847
848
  let currentKeyData = currData.getResultByName("keydata");
  let isKeyInKeyring = (currData.getResultByName("keyring_inserted") === "1");
849

850
851
852
  if (lastSeen >= paramsArr.dateSent ||
    (paramsArr["prefer-encrypt"] === "gossip" && notGossip)) {
    EnigmailLog.DEBUG("autocrypt.jsm: updateUser: not a relevant new latest message\n");
853

854
    return;
855
856
857
858
  }

  EnigmailLog.DEBUG("autocrypt.jsm: updateUser: updating latest message\n");

859
860
861
862
863
  let updateStr;
  let updateObj;

  if (paramsArr.autocryptDate > 0) {
    lastAutocrypt = paramsArr.autocryptDate;
864
    if (!("fpr" in paramsArr)) {
865
      await getFprForKey(paramsArr);
866
867
    }

868
869
870
    updateStr = "update autocrypt_keydata set state = :state, keydata = :keyData, last_seen_autocrypt = :lastAutocrypt, " +
      "fpr = :fpr, last_seen = :lastSeen where email = :email and type = :type";
    updateObj = {
871
      email: paramsArr.addr.toLowerCase(),
872
873
874
875
876
877
878
      state: paramsArr["prefer-encrypt"],
      keyData: paramsArr.keydata,
      fpr: ("fpr" in paramsArr ? paramsArr.fpr : ""),
      type: paramsArr.type,
      lastAutocrypt: lastAutocrypt.toJSON(),
      lastSeen: paramsArr.dateSent.toJSON()
    };
879
880
  }
  else {
881
882
    updateStr = "update autocrypt_keydata set state = :state, last_seen = :lastSeen where email = :email and type = :type";
    updateObj = {
883
      email: paramsArr.addr.toLowerCase(),
884
885
886
887
      state: paramsArr["prefer-encrypt"],
      type: paramsArr.type,
      lastSeen: paramsArr.dateSent.toJSON()
    };
888
889
  }

890
  if (!("fpr" in paramsArr)) {
891
    await getFprForKey(paramsArr);
892
893
  }

894
895
896
897
898
899
900
901
902
  await new Promise((resolve, reject) => {
    connection.executeTransaction(function _trx() {
      connection.execute(updateStr, updateObj).then(
        function _ok() {
          resolve();
        }
      ).catch(function _err() {
        reject("update failed");
      });
903
904
905
    });
  });

906
907
908
909
910
911
912
  if (autoCryptEnabled && isKeyInKeyring && (currentKeyData !== paramsArr.keydata)) {
    await updateKeyIfNeeded(paramsArr.addr.toLowerCase(), paramsArr.keydata, paramsArr.fpr, paramsArr.type, paramsArr["prefer-encrypt"]);
  }

  return;
}

913

914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
/**
 * Determine if a key in the keyring should be replaced by a new (or updated) key
 * @param {String} email - Email address
 * @param {String} keydata - new keydata to import
 * @param {String} fpr - fingerprint of new key
 * @param {String} keyType - key type (1 / 1g)
 * @param {String} autocryptState - mutual or nopreference
 *
 * @return {Promise<Boolean>} - key updated
 */
async function updateKeyIfNeeded(email, keydata, fpr, keyType, autocryptState) {
  let ruleNode = EnigmailRules.getRuleByEmail(EnigmailConstants.AC_RULE_PREFIX + email);
  if (!ruleNode) return false;

  let doImport = false;

  let currentKeyId = ruleNode.getAttribute("keyList");
  if (`0x${fpr}` === currentKeyId || keyType === "1") {
    doImport = true;
933
934
  }
  else {
935
936
937
938
    // Gossip keys
    let keyObj = EnigmailKeyRing.getKeyById(currentKeyId);
    let encOk = keyObj.getEncryptionValidity().keyValid;

939
    if (!encOk) {
940
941
942
943
944
945
      // current key is not valid anymore
      doImport = true;
    }
  }

  if (doImport) {
Patrick Brunschwig's avatar
Patrick Brunschwig committed
946
    await EnigmailAutocrypt.applyKeyFromKeydata(atob(keydata), email, autocryptState, keyType);
947
948
949
  }

  return doImport;
950
}
951
952
953
954

/**
 * Set the fpr attribute for a given key parameter object
 */
955
956
957
958
959
async function getFprForKey(paramsArr) {
  let keyData = atob(paramsArr.keydata);

  const cApi = EnigmailCryptoAPI();

960
  try {
961
962
963
964
965
966
967
968
969
970
971
    let keyInfo = await cApi.getKeyListFromKeyBlock(keyData);

    // keyInfo is an object, not an array => convert to array 1st
    let keyArr = [];

    for (let k in keyInfo) {
      keyArr.push(keyInfo[k]);
    }

    if (keyArr.length === 1) {
      paramsArr.fpr = keyArr[0].fpr;
972
    }
973
974
  }
  catch (x) {}
975
}
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000


/**
 * Create the 9x4 digits backup code as defined in the Autocrypt spec
 *
 * @return String: xxxx-xxxx-...
 */

function createBackupCode() {
  let bkpCode = "";

  for (let i = 0; i < 9; i++) {
    if (i > 0) bkpCode += "-";

    let a = new Uint8Array(4);
    crypto.getRandomValues(a);
    for (let j = 0; j < 4; j++) {
      bkpCode += String(a[j] % 10);
    }
  }
  return bkpCode;
}


function createBackupOuterMsg(toEmail, encryptedMsg) {
For faster browsing, not all history is shown. View entire blame