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
9var EXPORTED_SYMBOLS = ["EnigmailKey"];
10
11const { XPCOMUtils } = ChromeUtils.import(
12  "resource://gre/modules/XPCOMUtils.jsm"
13);
14
15XPCOMUtils.defineLazyModuleGetters(this, {
16  EnigmailCryptoAPI: "chrome://openpgp/content/modules/cryptoAPI.jsm",
17  EnigmailFiles: "chrome://openpgp/content/modules/files.jsm",
18  EnigmailLog: "chrome://openpgp/content/modules/log.jsm",
19  EnigmailKeyRing: "chrome://openpgp/content/modules/keyRing.jsm",
20  EnigmailDialog: "chrome://openpgp/content/modules/dialog.jsm",
21});
22
23XPCOMUtils.defineLazyGetter(this, "l10n", () => {
24  return new Localization(["messenger/openpgp/openpgp.ftl"], true);
25});
26
27var EnigmailKey = {
28  /**
29   * Format a key fingerprint
30   * @fingerprint |string|  -  unformated OpenPGP fingerprint
31   *
32   * @return |string| - formatted string
33   */
34  formatFpr(fingerprint) {
35    //EnigmailLog.DEBUG("key.jsm: EnigmailKey.formatFpr(" + fingerprint + ")\n");
36    // format key fingerprint
37    let r = "";
38    const fpr = fingerprint.match(
39      /(....)(....)(....)(....)(....)(....)(....)(....)(....)?(....)?/
40    );
41    if (fpr && fpr.length > 2) {
42      fpr.shift();
43      r = fpr.join(" ");
44    }
45
46    return r;
47  },
48
49  // Extract public key from Status Message
50  extractPubkey(statusMsg) {
51    const matchb = statusMsg.match(/(^|\n)NO_PUBKEY (\w{8})(\w{8})/);
52    if (matchb && matchb.length > 3) {
53      EnigmailLog.DEBUG(
54        "Enigmail.extractPubkey: NO_PUBKEY 0x" + matchb[3] + "\n"
55      );
56      return matchb[2] + matchb[3];
57    }
58    return null;
59  },
60
61  /**
62   * import a revocation certificate form a given keyblock string.
63   * Ask the user before importing the cert, and display an error
64   * message in case of failures.
65   */
66  importRevocationCert(keyId, keyBlockStr) {
67    let key = EnigmailKeyRing.getKeyById(keyId);
68
69    if (key) {
70      if (key.keyTrust === "r") {
71        // Key has already been revoked
72        l10n
73          .formatValue("revoke-key-already-revoked", {
74            keyId,
75          })
76          .then(value => {
77            EnigmailDialog.info(null, value);
78          });
79      } else {
80        let userId = key.userId + " - 0x" + key.keyId;
81        if (
82          !EnigmailDialog.confirmDlg(
83            null,
84            l10n.formatValueSync("revoke-key-question", { userId }),
85            l10n.formatValueSync("key-man-button-revoke-key")
86          )
87        ) {
88          return;
89        }
90
91        let errorMsgObj = {};
92        // TODO this will certainly not work yet, because RNP requires
93        // calling a different function for importing revocation
94        // signatures, see RNP.importRevImpl
95        if (
96          EnigmailKeyRing.importKey(
97            null,
98            false,
99            keyBlockStr,
100            false,
101            keyId,
102            errorMsgObj
103          ) > 0
104        ) {
105          EnigmailDialog.alert(null, errorMsgObj.value);
106        }
107      }
108    } else {
109      // Suitable key for revocation certificate is not present in keyring
110      l10n
111        .formatValue("revoke-key-not-present", {
112          keyId,
113        })
114        .then(value => {
115          EnigmailDialog.alert(null, value);
116        });
117    }
118  },
119
120  _keyListCache: new Map(),
121  _keyListCacheMaxEntries: 50,
122  _keyListCacheMaxKeySize: 30720,
123
124  /**
125   * Get details (key ID, UID) of the data contained in a OpenPGP key block
126   *
127   * @param keyBlockStr  String: the contents of one or more public keys
128   * @param errorMsgObj  Object: obj.value will contain an error message in case of failures
129   * @param interactive  Boolean: if in interactive mode, may display dialogs (default: true)
130   *
131   * @return Array of objects with the following structure:
132   *          - id (key ID)
133   *          - fpr
134   *          - name (the UID of the key)
135   *          - state (one of "old" [existing key], "new" [new key], "invalid" [key cannot not be imported])
136   */
137  getKeyListFromKeyBlock(
138    keyBlockStr,
139    errorMsgObj,
140    interactive = true,
141    pubkey,
142    seckey
143  ) {
144    EnigmailLog.DEBUG("key.jsm: getKeyListFromKeyBlock\n");
145    errorMsgObj.value = "";
146
147    let cacheEntry = this._keyListCache.get(keyBlockStr);
148    if (cacheEntry) {
149      // Remove and re-insert to move entry to the end of insertion order,
150      // so we know which entry was used least recently.
151      this._keyListCache.delete(keyBlockStr);
152      this._keyListCache.set(keyBlockStr, cacheEntry);
153
154      if (cacheEntry.error) {
155        errorMsgObj.value = cacheEntry.error;
156        return null;
157      }
158      return cacheEntry.data;
159    }
160
161    // We primarily want to cache single keys that are found in email
162    // attachments. We shouldn't attempt to cache larger key blocks
163    // that are likely arriving from explicit import attempts.
164    let updateCache = keyBlockStr.length < this._keyListCacheMaxKeySize;
165
166    if (
167      updateCache &&
168      this._keyListCache.size >= this._keyListCacheMaxEntries
169    ) {
170      // Remove oldest entry, make room for new entry.
171      this._keyListCache.delete(this._keyListCache.keys().next().value);
172    }
173
174    const cApi = EnigmailCryptoAPI();
175    let keyList;
176    let key = {};
177    let blocks;
178    errorMsgObj.value = "";
179
180    try {
181      keyList = cApi.sync(
182        cApi.getKeyListFromKeyBlockAPI(keyBlockStr, pubkey, seckey, true)
183      );
184    } catch (ex) {
185      errorMsgObj.value = ex.toString();
186      if (updateCache) {
187        this._keyListCache.set(keyBlockStr, {
188          error: errorMsgObj.value,
189          data: null,
190        });
191      }
192      return null;
193    }
194
195    if (!keyList) {
196      if (updateCache) {
197        this._keyListCache.set(keyBlockStr, { error: undefined, data: null });
198      }
199      return null;
200    }
201
202    if (interactive && keyList.length === 1) {
203      // TODO: not yet tested
204      key = keyList[0];
205      if ("revoke" in key && !("name" in key)) {
206        if (updateCache) {
207          this._keyListCache.set(keyBlockStr, { error: undefined, data: [] });
208        }
209        this.importRevocationCert(key.id, blocks.join("\n"));
210        return [];
211      }
212    }
213
214    if (updateCache) {
215      this._keyListCache.set(keyBlockStr, { error: undefined, data: keyList });
216    }
217    return keyList;
218  },
219
220  /**
221   * Get details of a key block to import. Works identically as getKeyListFromKeyBlock();
222   * except that the input is a file instead of a string
223   *
224   * @param file         nsIFile object - file to read
225   * @param errorMsgObj  Object - obj.value will contain error message
226   *
227   * @return Array (same as for getKeyListFromKeyBlock())
228   */
229  getKeyListFromKeyFile(path, errorMsgObj, pubkey, seckey) {
230    var contents = EnigmailFiles.readFile(path);
231    return this.getKeyListFromKeyBlock(
232      contents,
233      errorMsgObj,
234      true,
235      pubkey,
236      seckey
237    );
238  },
239
240  /**
241   * Compare 2 KeyIds of possible different length (short, long, FPR-length, with or without prefixed
242   * 0x are accepted)
243   *
244   * @param keyId1       string
245   * @param keyId2       string
246   *
247   * @return true or false, given the comparison of the last minimum-length characters.
248   */
249  compareKeyIds(keyId1, keyId2) {
250    var keyId1Raw = keyId1.replace(/^0x/, "").toUpperCase();
251    var keyId2Raw = keyId2.replace(/^0x/, "").toUpperCase();
252
253    var minlength = Math.min(keyId1Raw.length, keyId2Raw.length);
254
255    if (minlength < keyId1Raw.length) {
256      // Limit keyId1 to minlength
257      keyId1Raw = keyId1Raw.substr(-minlength, minlength);
258    }
259
260    if (minlength < keyId2Raw.length) {
261      // Limit keyId2 to minlength
262      keyId2Raw = keyId2Raw.substr(-minlength, minlength);
263    }
264
265    return keyId1Raw === keyId2Raw;
266  },
267};
268