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 = ["EnigmailKeyServer"];
10
11const { XPCOMUtils } = ChromeUtils.import(
12  "resource://gre/modules/XPCOMUtils.jsm"
13);
14
15XPCOMUtils.defineLazyModuleGetters(this, {
16  EnigmailConstants: "chrome://openpgp/content/modules/constants.jsm",
17  EnigmailCryptoAPI: "chrome://openpgp/content/modules/cryptoAPI.jsm",
18  EnigmailData: "chrome://openpgp/content/modules/data.jsm",
19  EnigmailFuncs: "chrome://openpgp/content/modules/funcs.jsm",
20  EnigmailGpg: "chrome://openpgp/content/modules/constants.jsm",
21  EnigmailKeyRing: "chrome://openpgp/content/modules/keyRing.jsm",
22  EnigmailLog: "chrome://openpgp/content/modules/log.jsm",
23  FeedUtils: "resource:///modules/FeedUtils.jsm",
24  Services: "resource://gre/modules/Services.jsm",
25});
26
27XPCOMUtils.defineLazyGetter(this, "l10n", () => {
28  return new Localization(["messenger/openpgp/openpgp.ftl"], true);
29});
30
31const ENIG_DEFAULT_HKP_PORT = "11371";
32const ENIG_DEFAULT_HKPS_PORT = "443";
33const ENIG_DEFAULT_LDAP_PORT = "389";
34
35/**
36 KeySrvListener API
37 Object implementing:
38  - onProgress: function(percentComplete) [only implemented for download()]
39  - onCancel: function() - the body will be set by the callee
40*/
41
42function createError(errId) {
43  let msg = "";
44
45  switch (errId) {
46    case EnigmailConstants.KEYSERVER_ERR_ABORTED:
47      msg = l10n.formatValueSync("keyserver-error-aborted");
48      break;
49    case EnigmailConstants.KEYSERVER_ERR_SERVER_ERROR:
50      msg = l10n.formatValueSync("keyserver-error-server-error");
51      break;
52    case EnigmailConstants.KEYSERVER_ERR_SERVER_UNAVAILABLE:
53      msg = l10n.formatValueSync("keyserver-error-unavailable");
54      break;
55    case EnigmailConstants.KEYSERVER_ERR_SECURITY_ERROR:
56      msg = l10n.formatValueSync("keyserver-error-security-error");
57      break;
58    case EnigmailConstants.KEYSERVER_ERR_CERTIFICATE_ERROR:
59      msg = l10n.formatValueSync("keyserver-error-certificate-error");
60      break;
61    case EnigmailConstants.KEYSERVER_ERR_IMPORT_ERROR:
62      msg = l10n.formatValueSync("keyserver-error-import-error");
63      break;
64    case EnigmailConstants.KEYSERVER_ERR_UNKNOWN:
65      msg = l10n.formatValueSync("keyserver-error-unknown");
66      break;
67  }
68
69  return {
70    result: errId,
71    errorDetails: msg,
72  };
73}
74
75/**
76 * parse a keyserver specification and return host, protocol and port
77 *
78 * @param keyserver: String - name of keyserver with optional protocol and port.
79 *                       E.g. keys.gnupg.net, hkps://keys.gnupg.net:443
80 *
81 * @return Object: {port, host, protocol} (all Strings)
82 */
83function parseKeyserverUrl(keyserver) {
84  if (keyserver.length > 1024) {
85    // insane length of keyserver is forbidden
86    throw Components.Exception("", Cr.NS_ERROR_FAILURE);
87  }
88
89  keyserver = keyserver.toLowerCase().trim();
90  let protocol = "";
91  if (keyserver.search(/^[a-zA-Z0-9_.-]+:\/\//) === 0) {
92    protocol = keyserver.replace(/^([a-zA-Z0-9_.-]+)(:\/\/.*)/, "$1");
93    keyserver = keyserver.replace(/^[a-zA-Z0-9_.-]+:\/\//, "");
94  } else {
95    protocol = "hkp";
96  }
97
98  let port = "";
99  switch (protocol) {
100    case "hkp":
101      port = ENIG_DEFAULT_HKP_PORT;
102      break;
103    case "https":
104    case "hkps":
105      port = ENIG_DEFAULT_HKPS_PORT;
106      break;
107    case "ldap":
108      port = ENIG_DEFAULT_LDAP_PORT;
109      break;
110  }
111
112  let m = keyserver.match(/^(.+)(:)(\d+)$/);
113  if (m && m.length == 4) {
114    keyserver = m[1];
115    port = m[3];
116  }
117
118  if (keyserver.search(/^(keys\.mailvelope\.com|api\.protonmail\.ch)$/) === 0) {
119    protocol = "hkps";
120    port = ENIG_DEFAULT_HKPS_PORT;
121  }
122  if (keyserver.search(/^(keybase\.io)$/) === 0) {
123    protocol = "keybase";
124    port = ENIG_DEFAULT_HKPS_PORT;
125  }
126
127  return {
128    protocol,
129    host: keyserver,
130    port,
131  };
132}
133
134/**
135 Object to handle HKP/HKPS requests via builtin XMLHttpRequest()
136 */
137const accessHkpInternal = {
138  /**
139   * Create the payload of hkp requests (upload only)
140   *
141   */
142  buildHkpPayload(actionFlag, searchTerms) {
143    let payLoad = null,
144      keyData = "";
145
146    switch (actionFlag) {
147      case EnigmailConstants.UPLOAD_KEY:
148        keyData = EnigmailKeyRing.extractKey(false, searchTerms, null, {}, {});
149        if (keyData.length === 0) {
150          return null;
151        }
152
153        payLoad = "keytext=" + encodeURIComponent(keyData);
154        return payLoad;
155
156      case EnigmailConstants.DOWNLOAD_KEY:
157      case EnigmailConstants.DOWNLOAD_KEY_NO_IMPORT:
158      case EnigmailConstants.SEARCH_KEY:
159        return "";
160    }
161
162    // other actions are not yet implemented
163    return null;
164  },
165
166  /**
167   * return the URL and the HTTP access method for a given action
168   */
169  createRequestUrl(keyserver, actionFlag, searchTerm) {
170    let keySrv = parseKeyserverUrl(keyserver);
171
172    let method = "GET";
173    let protocol;
174
175    switch (keySrv.protocol) {
176      case "hkp":
177        protocol = "http";
178        break;
179      case "ldap":
180        throw Components.Exception("", Cr.NS_ERROR_FAILURE);
181      default:
182        // equals to hkps
183        protocol = "https";
184    }
185
186    let url = protocol + "://" + keySrv.host + ":" + keySrv.port;
187
188    if (actionFlag === EnigmailConstants.UPLOAD_KEY) {
189      url += "/pks/add";
190      method = "POST";
191    } else if (
192      actionFlag === EnigmailConstants.DOWNLOAD_KEY ||
193      actionFlag === EnigmailConstants.DOWNLOAD_KEY_NO_IMPORT
194    ) {
195      if (searchTerm.indexOf("0x") !== 0) {
196        searchTerm = "0x" + searchTerm;
197      }
198      url += "/pks/lookup?search=" + searchTerm + "&op=get&options=mr";
199    } else if (actionFlag === EnigmailConstants.SEARCH_KEY) {
200      url +=
201        "/pks/lookup?search=" +
202        escape(searchTerm) +
203        "&fingerprint=on&op=index&options=mr";
204    }
205
206    return {
207      url,
208      host: keySrv.host,
209      method,
210    };
211  },
212
213  /**
214   * Upload, search or download keys from a keyserver
215   * @param actionFlag:  Number  - Keyserver Action Flags: from EnigmailConstants
216   * @param keyId:      String  - space-separated list of search terms or key IDs
217   * @param keyserver:   String  - keyserver URL (optionally incl. protocol)
218   * @param listener:    optional Object implementing the KeySrvListener API (above)
219   *
220   * @return:   Promise<Number (Status-ID)>
221   */
222  accessKeyServer(actionFlag, keyserver, keyId, listener) {
223    EnigmailLog.DEBUG(
224      `keyserver.jsm: accessHkpInternal.accessKeyServer(${keyserver})\n`
225    );
226    if (!keyserver) {
227      throw new Error("accessKeyServer requires explicit keyserver parameter");
228    }
229
230    return new Promise((resolve, reject) => {
231      let xmlReq = null;
232      if (listener && typeof listener === "object") {
233        listener.onCancel = function() {
234          EnigmailLog.DEBUG(
235            `keyserver.jsm: accessHkpInternal.accessKeyServer - onCancel() called\n`
236          );
237          if (xmlReq) {
238            xmlReq.abort();
239          }
240          reject(createError(EnigmailConstants.KEYSERVER_ERR_ABORTED));
241        };
242      }
243      if (actionFlag === EnigmailConstants.REFRESH_KEY) {
244        // we don't (need to) distinguish between refresh and download for our internal protocol
245        actionFlag = EnigmailConstants.DOWNLOAD_KEY;
246      }
247
248      let payLoad = this.buildHkpPayload(actionFlag, keyId);
249      if (payLoad === null) {
250        reject(createError(EnigmailConstants.KEYSERVER_ERR_UNKNOWN));
251        return;
252      }
253
254      xmlReq = new XMLHttpRequest();
255
256      xmlReq.onload = function() {
257        EnigmailLog.DEBUG(
258          "keyserver.jsm: accessHkpInternal: onload(): status=" +
259            xmlReq.status +
260            "\n"
261        );
262        switch (actionFlag) {
263          case EnigmailConstants.UPLOAD_KEY:
264            EnigmailLog.DEBUG(
265              "keyserver.jsm: accessHkpInternal: onload: " +
266                xmlReq.responseText +
267                "\n"
268            );
269            if (xmlReq.status >= 400) {
270              reject(createError(EnigmailConstants.KEYSERVER_ERR_SERVER_ERROR));
271            } else {
272              resolve(0);
273            }
274            return;
275
276          case EnigmailConstants.SEARCH_KEY:
277            if (xmlReq.status === 404) {
278              // key not found
279              resolve("");
280            } else if (xmlReq.status >= 400) {
281              reject(createError(EnigmailConstants.KEYSERVER_ERR_SERVER_ERROR));
282            } else {
283              resolve(xmlReq.responseText);
284            }
285            return;
286
287          case EnigmailConstants.DOWNLOAD_KEY:
288          case EnigmailConstants.DOWNLOAD_KEY_NO_IMPORT:
289            if (xmlReq.status >= 400 && xmlReq.status < 500) {
290              // key not found
291              resolve(1);
292            } else if (xmlReq.status >= 500) {
293              EnigmailLog.DEBUG(
294                "keyserver.jsm: accessHkpInternal: onload: " +
295                  xmlReq.responseText +
296                  "\n"
297              );
298              reject(createError(EnigmailConstants.KEYSERVER_ERR_SERVER_ERROR));
299            } else {
300              let errorMsgObj = {},
301                importedKeysObj = {};
302              let importMinimal =
303                xmlReq.responseText.length > 1024000 &&
304                !EnigmailGpg.getGpgFeature("handles-huge-keys");
305              let r = EnigmailKeyRing.importKey(
306                null,
307                false,
308                xmlReq.responseText,
309                false,
310                "",
311                errorMsgObj,
312                importedKeysObj,
313                importMinimal
314              );
315              if (r === 0) {
316                resolve(importedKeysObj.value);
317              } else {
318                reject(
319                  createError(EnigmailConstants.KEYSERVER_ERR_IMPORT_ERROR)
320                );
321              }
322            }
323            return;
324        }
325        resolve(-1);
326      };
327
328      xmlReq.onerror = function(e) {
329        EnigmailLog.DEBUG(
330          "keyserver.jsm: accessHkpInternal.accessKeyServer: onerror: " +
331            e +
332            "\n"
333        );
334        let err = FeedUtils.createTCPErrorFromFailedXHR(e.target);
335        switch (err.type) {
336          case "SecurityCertificate":
337            reject(
338              createError(EnigmailConstants.KEYSERVER_ERR_CERTIFICATE_ERROR)
339            );
340            break;
341          case "SecurityProtocol":
342            reject(createError(EnigmailConstants.KEYSERVER_ERR_SECURITY_ERROR));
343            break;
344          case "Network":
345            reject(
346              createError(EnigmailConstants.KEYSERVER_ERR_SERVER_UNAVAILABLE)
347            );
348            break;
349        }
350        reject(createError(EnigmailConstants.KEYSERVER_ERR_SERVER_UNAVAILABLE));
351      };
352
353      xmlReq.onloadend = function() {
354        EnigmailLog.DEBUG(
355          "keyserver.jsm: accessHkpInternal.accessKeyServer: loadEnd\n"
356        );
357      };
358
359      let { url, method } = this.createRequestUrl(keyserver, actionFlag, keyId);
360
361      EnigmailLog.DEBUG(
362        `keyserver.jsm: accessHkpInternal.accessKeyServer: requesting ${url}\n`
363      );
364      xmlReq.open(method, url);
365      xmlReq.send(payLoad);
366    });
367  },
368
369  /**
370   * Download keys from a keyserver
371   * @param keyIDs:      String  - space-separated list of search terms or key IDs
372   * @param keyserver:   String  - keyserver URL (optionally incl. protocol)
373   * @param listener:    optional Object implementing the KeySrvListener API (above)
374   *
375   * @return:   Promise<...>
376   */
377  async download(autoImport, keyIDs, keyserver, listener = null) {
378    EnigmailLog.DEBUG(`keyserver.jsm: accessHkpInternal.download(${keyIDs})\n`);
379    let keyIdArr = keyIDs.split(/ +/);
380    let retObj = {
381      result: 0,
382      errorDetails: "",
383      keyList: [],
384    };
385
386    for (let i = 0; i < keyIdArr.length; i++) {
387      try {
388        let r = await this.accessKeyServer(
389          autoImport
390            ? EnigmailConstants.DOWNLOAD_KEY
391            : EnigmailConstants.DOWNLOAD_KEY_NO_IMPORT,
392          keyserver,
393          keyIdArr[i],
394          listener
395        );
396        if (Array.isArray(r)) {
397          retObj.keyList = retObj.keyList.concat(r);
398        }
399      } catch (ex) {
400        retObj.result = ex.result;
401        retObj.errorDetails = ex.errorDetails;
402        throw retObj;
403      }
404
405      if (listener && "onProgress" in listener) {
406        listener.onProgress(((i + 1) / keyIdArr.length) * 100);
407      }
408    }
409
410    return retObj;
411  },
412
413  refresh(keyServer, listener = null) {
414    let keyList = EnigmailKeyRing.getAllKeys()
415      .keyList.map(keyObj => {
416        return "0x" + keyObj.fpr;
417      })
418      .join(" ");
419
420    return this.download(true, keyList, keyServer, listener);
421  },
422
423  /**
424   * Upload keys to a keyserver
425   * @param keyIDs: String  - space-separated list of search terms or key IDs
426   * @param keyserver:   String  - keyserver URL (optionally incl. protocol)
427   * @param listener:    optional Object implementing the KeySrvListener API (above)
428   *
429   * @return:   Promise<...>
430   */
431  async upload(keyIDs, keyserver, listener = null) {
432    EnigmailLog.DEBUG(`keyserver.jsm: accessHkpInternal.upload(${keyIDs})\n`);
433    let keyIdArr = keyIDs.split(/ +/);
434    let retObj = {
435      result: 0,
436      errorDetails: "",
437      keyList: [],
438    };
439
440    for (let i = 0; i < keyIdArr.length; i++) {
441      try {
442        let r = await this.accessKeyServer(
443          EnigmailConstants.UPLOAD_KEY,
444          keyserver,
445          keyIdArr[i],
446          listener
447        );
448        if (r === 0) {
449          retObj.keyList.push(keyIdArr[i]);
450        } else {
451          retObj.result = r;
452        }
453      } catch (ex) {
454        retObj.result = ex.result;
455        retObj.errorDetails = ex.errorDetails;
456        throw retObj;
457      }
458
459      if (listener && "onProgress" in listener) {
460        listener.onProgress(((i + 1) / keyIdArr.length) * 100);
461      }
462    }
463
464    return retObj;
465  },
466
467  /**
468   * Search for keys on a keyserver
469   * @param searchTerm:  String  - search term
470   * @param keyserver:   String  - keyserver URL (optionally incl. protocol)
471   * @param listener:    optional Object implementing the KeySrvListener API (above)
472   *
473   * @return:   Promise<Object>
474   *    - result: Number
475   *    - pubKeys: Array of Object:
476   *         PubKeys: Object with:
477   *           - keyId: String
478   *           - keyLen: String
479   *           - keyType: String
480   *           - created: String (YYYY-MM-DD)
481   *           - status: String: one of ''=valid, r=revoked, e=expired
482   *           - uid: Array of Strings with UIDs
483   */
484  async searchKeyserver(searchTerm, keyserver, listener = null) {
485    EnigmailLog.DEBUG(
486      `keyserver.jsm: accessHkpInternal.search(${searchTerm})\n`
487    );
488    let retObj = {
489      result: 0,
490      errorDetails: "",
491      pubKeys: [],
492    };
493    let key = null;
494
495    let searchArr = searchTerm.split(/ +/);
496
497    for (let k in searchArr) {
498      let r = await this.accessKeyServer(
499        EnigmailConstants.SEARCH_KEY,
500        keyserver,
501        searchArr[k],
502        listener
503      );
504
505      let lines = r.split(/\r?\n/);
506
507      for (var i = 0; i < lines.length; i++) {
508        let line = lines[i].split(/:/).map(unescape);
509        if (line.length <= 1) {
510          continue;
511        }
512
513        switch (line[0]) {
514          case "info":
515            if (line[1] !== "1") {
516              // protocol version not supported
517              retObj.result = 7;
518              retObj.errorDetails = await l10n.formatValue(
519                "keyserver-error-unsupported"
520              );
521              retObj.pubKeys = [];
522              return retObj;
523            }
524            break;
525          case "pub":
526            if (line.length >= 6) {
527              if (key) {
528                retObj.pubKeys.push(key);
529                key = null;
530              }
531              let dat = new Date(line[4] * 1000);
532              let month = String(dat.getMonth() + 101).substr(1);
533              let day = String(dat.getDate() + 100).substr(1);
534              key = {
535                keyId: line[1],
536                keyLen: line[3],
537                keyType: line[2],
538                created: dat.getFullYear() + "-" + month + "-" + day,
539                uid: [],
540                status: line[6],
541              };
542            }
543            break;
544          case "uid":
545            key.uid.push(
546              EnigmailData.convertToUnicode(line[1].trim(), "utf-8")
547            );
548        }
549      }
550
551      if (key) {
552        retObj.pubKeys.push(key);
553      }
554    }
555
556    return retObj;
557  },
558};
559
560/**
561 Object to handle KeyBase requests (search & download only)
562 */
563const accessKeyBase = {
564  /**
565   * return the URL and the HTTP access method for a given action
566   */
567  createRequestUrl(actionFlag, searchTerm) {
568    let url = "https://keybase.io/_/api/1.0/user/";
569
570    if (actionFlag === EnigmailConstants.UPLOAD_KEY) {
571      // not supported
572      throw Components.Exception("", Cr.NS_ERROR_FAILURE);
573    } else if (
574      actionFlag === EnigmailConstants.DOWNLOAD_KEY ||
575      actionFlag === EnigmailConstants.DOWNLOAD_KEY_NO_IMPORT
576    ) {
577      if (searchTerm.indexOf("0x") === 0) {
578        searchTerm = searchTerm.substr(0, 40);
579      }
580      url +=
581        "lookup.json?key_fingerprint=" +
582        escape(searchTerm) +
583        "&fields=public_keys";
584    } else if (actionFlag === EnigmailConstants.SEARCH_KEY) {
585      url += "autocomplete.json?q=" + escape(searchTerm);
586    }
587
588    return {
589      url,
590      method: "GET",
591    };
592  },
593
594  /**
595   * Upload, search or download keys from a keyserver
596   * @param actionFlag:  Number  - Keyserver Action Flags: from EnigmailConstants
597   * @param keyId:      String  - space-separated list of search terms or key IDs
598   * @param listener:    optional Object implementing the KeySrvListener API (above)
599   *
600   * @return:   Promise<Number (Status-ID)>
601   */
602  accessKeyServer(actionFlag, keyId, listener) {
603    EnigmailLog.DEBUG(`keyserver.jsm: accessKeyBase: accessKeyServer()\n`);
604
605    return new Promise((resolve, reject) => {
606      let xmlReq = null;
607      if (listener && typeof listener === "object") {
608        listener.onCancel = function() {
609          EnigmailLog.DEBUG(
610            `keyserver.jsm: accessKeyBase: accessKeyServer - onCancel() called\n`
611          );
612          if (xmlReq) {
613            xmlReq.abort();
614          }
615          reject(createError(EnigmailConstants.KEYSERVER_ERR_ABORTED));
616        };
617      }
618      if (actionFlag === EnigmailConstants.REFRESH_KEY) {
619        // we don't (need to) distinguish between refresh and download for our internal protocol
620        actionFlag = EnigmailConstants.DOWNLOAD_KEY;
621      }
622
623      xmlReq = new XMLHttpRequest();
624
625      xmlReq.onload = function() {
626        EnigmailLog.DEBUG(
627          "keyserver.jsm: onload(): status=" + xmlReq.status + "\n"
628        );
629        switch (actionFlag) {
630          case EnigmailConstants.SEARCH_KEY:
631            if (xmlReq.status >= 400) {
632              reject(createError(EnigmailConstants.KEYSERVER_ERR_SERVER_ERROR));
633            } else {
634              resolve(xmlReq.responseText);
635            }
636            return;
637
638          case EnigmailConstants.DOWNLOAD_KEY:
639          case EnigmailConstants.DOWNLOAD_KEY_NO_IMPORT:
640            if (xmlReq.status >= 400 && xmlReq.status < 500) {
641              // key not found
642              resolve([]);
643            } else if (xmlReq.status >= 500) {
644              EnigmailLog.DEBUG(
645                "keyserver.jsm: onload: " + xmlReq.responseText + "\n"
646              );
647              reject(createError(EnigmailConstants.KEYSERVER_ERR_SERVER_ERROR));
648            } else {
649              try {
650                let resp = JSON.parse(xmlReq.responseText);
651                let imported = [];
652
653                if (resp.status.code === 0) {
654                  for (let hit in resp.them) {
655                    EnigmailLog.DEBUG(
656                      JSON.stringify(resp.them[hit].public_keys.primary) + "\n"
657                    );
658
659                    if (resp.them[hit] !== null) {
660                      let errorMsgObj = {},
661                        importedKeysObj = {};
662                      let r = EnigmailKeyRing.importKey(
663                        null,
664                        false,
665                        resp.them[hit].public_keys.primary.bundle,
666                        false,
667                        "",
668                        errorMsgObj,
669                        importedKeysObj
670                      );
671                      if (r === 0) {
672                        imported.push(importedKeysObj.value);
673                      }
674                    }
675                  }
676                }
677                resolve(imported);
678              } catch (ex) {
679                reject(createError(EnigmailConstants.KEYSERVER_ERR_UNKNOWN));
680              }
681            }
682            return;
683        }
684        resolve(-1);
685      };
686
687      xmlReq.onerror = function(e) {
688        EnigmailLog.DEBUG("keyserver.jsm: accessKeyBase: onerror: " + e + "\n");
689        let err = FeedUtils.createTCPErrorFromFailedXHR(e.target);
690        switch (err.type) {
691          case "SecurityCertificate":
692            reject(
693              createError(EnigmailConstants.KEYSERVER_ERR_CERTIFICATE_ERROR)
694            );
695            break;
696          case "SecurityProtocol":
697            reject(createError(EnigmailConstants.KEYSERVER_ERR_SECURITY_ERROR));
698            break;
699          case "Network":
700            reject(
701              createError(EnigmailConstants.KEYSERVER_ERR_SERVER_UNAVAILABLE)
702            );
703            break;
704        }
705        reject(createError(EnigmailConstants.KEYSERVER_ERR_SERVER_UNAVAILABLE));
706      };
707
708      xmlReq.onloadend = function() {
709        EnigmailLog.DEBUG("keyserver.jsm: accessKeyBase: loadEnd\n");
710      };
711
712      let { url, method } = this.createRequestUrl(actionFlag, keyId);
713
714      EnigmailLog.DEBUG(`keyserver.jsm: accessKeyBase: requesting ${url}\n`);
715      xmlReq.open(method, url);
716      xmlReq.send("");
717    });
718  },
719
720  /**
721   * Download keys from a KeyBase
722   * @param keyIDs:      String  - space-separated list of search terms or key IDs
723   * @param keyserver:   (not used for keybase)
724   * @param listener:    optional Object implementing the KeySrvListener API (above)
725   *
726   * @return:   Promise<...>
727   */
728  async download(autoImport, keyIDs, keyserver, listener = null) {
729    EnigmailLog.DEBUG(`keyserver.jsm: accessKeyBase: download()\n`);
730    let keyIdArr = keyIDs.split(/ +/);
731    let retObj = {
732      result: 0,
733      errorDetails: "",
734      keyList: [],
735    };
736
737    for (let i = 0; i < keyIdArr.length; i++) {
738      try {
739        let r = await this.accessKeyServer(
740          autoImport
741            ? EnigmailConstants.DOWNLOAD_KEY
742            : EnigmailConstants.DOWNLOAD_KEY_NO_IMPORT,
743          keyIdArr[i],
744          listener
745        );
746        if (r.length > 0) {
747          retObj.keyList = retObj.keyList.concat(r);
748        }
749      } catch (ex) {
750        retObj.result = ex.result;
751        retObj.errorDetails = ex.result;
752        throw retObj;
753      }
754
755      if (listener && "onProgress" in listener) {
756        listener.onProgress(i / keyIdArr.length);
757      }
758    }
759
760    return retObj;
761  },
762
763  /**
764   * Search for keys on a keyserver
765   * @param searchTerm:  String  - search term
766   * @param keyserver:   String  - keyserver URL (optionally incl. protocol)
767   * @param listener:    optional Object implementing the KeySrvListener API (above)
768   *
769   * @return:   Promise<Object>
770   *    - result: Number
771   *    - pubKeys: Array of Object:
772   *         PubKeys: Object with:
773   *           - keyId: String
774   *           - keyLen: String
775   *           - keyType: String
776   *           - created: String (YYYY-MM-DD)
777   *           - status: String: one of ''=valid, r=revoked, e=expired
778   *           - uid: Array of Strings with UIDs
779
780   */
781  async searchKeyserver(searchTerm, keyserver, listener = null) {
782    EnigmailLog.DEBUG(`keyserver.jsm: accessKeyBase: search()\n`);
783    let retObj = {
784      result: 0,
785      errorDetails: "",
786      pubKeys: [],
787    };
788
789    try {
790      let r = await this.accessKeyServer(
791        EnigmailConstants.SEARCH_KEY,
792        searchTerm,
793        listener
794      );
795
796      let res = JSON.parse(r);
797      let completions = res.completions;
798
799      for (let hit in completions) {
800        if (
801          completions[hit] &&
802          completions[hit].components.key_fingerprint !== undefined
803        ) {
804          let uid = completions[hit].components.username.val;
805          if ("full_name" in completions[hit].components) {
806            uid += " (" + completions[hit].components.full_name.val + ")";
807          }
808          let key = {
809            keyId: completions[
810              hit
811            ].components.key_fingerprint.val.toUpperCase(),
812            keyLen: completions[
813              hit
814            ].components.key_fingerprint.nbits.toString(),
815            keyType: completions[
816              hit
817            ].components.key_fingerprint.algo.toString(),
818            created: 0, //date.toDateString(),
819            uid: [uid],
820            status: "",
821          };
822          retObj.pubKeys.push(key);
823        }
824      }
825    } catch (ex) {
826      retObj.result = ex.result;
827      retObj.errorDetails = ex.errorDetails;
828      throw retObj;
829    }
830
831    return retObj;
832  },
833
834  upload() {
835    throw Components.Exception("", Cr.NS_ERROR_FAILURE);
836  },
837
838  refresh(keyServer, listener = null) {
839    EnigmailLog.DEBUG(`keyserver.jsm: accessKeyBase: refresh()\n`);
840    let keyList = EnigmailKeyRing.getAllKeys()
841      .keyList.map(keyObj => {
842        return "0x" + keyObj.fpr;
843      })
844      .join(" ");
845
846    return this.download(true, keyList, keyServer, listener);
847  },
848};
849
850function getAccessType(keyserver) {
851  if (!keyserver) {
852    throw new Error("getAccessType requires explicit keyserver parameter");
853  }
854
855  let srv = parseKeyserverUrl(keyserver);
856  switch (srv.protocol) {
857    case "keybase":
858      return accessKeyBase;
859    case "vks":
860      return accessVksServer;
861  }
862
863  if (srv.host.search(/keys.openpgp.org$/i) >= 0) {
864    return accessVksServer;
865  }
866
867  return accessHkpInternal;
868}
869
870/**
871 Object to handle VKS requests (for example keys.openpgp.org)
872 */
873const accessVksServer = {
874  /**
875   * Create the payload of VKS requests (currently upload only)
876   *
877   */
878  buildJsonPayload(actionFlag, searchTerms, locale) {
879    let payLoad = null,
880      keyData = "";
881
882    switch (actionFlag) {
883      case EnigmailConstants.UPLOAD_KEY:
884        keyData = EnigmailKeyRing.extractKey(false, searchTerms, null, {}, {});
885        if (keyData.length === 0) {
886          return null;
887        }
888
889        payLoad = JSON.stringify({
890          keytext: keyData,
891        });
892        return payLoad;
893
894      case EnigmailConstants.GET_CONFIRMATION_LINK:
895        payLoad = JSON.stringify({
896          token: searchTerms.token,
897          addresses: searchTerms.addresses,
898          locale: [locale],
899        });
900        return payLoad;
901
902      case EnigmailConstants.DOWNLOAD_KEY:
903      case EnigmailConstants.DOWNLOAD_KEY_NO_IMPORT:
904      case EnigmailConstants.SEARCH_KEY:
905        return "";
906    }
907
908    // other actions are not yet implemented
909    return null;
910  },
911
912  /**
913   * return the URL and the HTTP access method for a given action
914   */
915  createRequestUrl(keyserver, actionFlag, searchTerm) {
916    let keySrv = parseKeyserverUrl(keyserver);
917    let contentType = "text/plain;charset=UTF-8";
918
919    let method = "GET";
920
921    let url = "https://" + keySrv.host + ":443";
922
923    if (actionFlag === EnigmailConstants.UPLOAD_KEY) {
924      url += "/vks/v1/upload";
925      method = "POST";
926      contentType = "application/json";
927    } else if (actionFlag === EnigmailConstants.GET_CONFIRMATION_LINK) {
928      url += "/vks/v1/request-verify";
929      method = "POST";
930      contentType = "application/json";
931    } else if (
932      actionFlag === EnigmailConstants.DOWNLOAD_KEY ||
933      actionFlag === EnigmailConstants.DOWNLOAD_KEY_NO_IMPORT ||
934      actionFlag === EnigmailConstants.SEARCH_KEY
935    ) {
936      if (searchTerm) {
937        let lookup = "/vks/";
938        if (searchTerm.indexOf("0x") === 0) {
939          searchTerm = searchTerm.substr(2);
940          if (
941            searchTerm.length == 16 &&
942            searchTerm.search(/^[A-F0-9]+$/) === 0
943          ) {
944            lookup = "/vks/v1/by-keyid/" + searchTerm;
945          } else if (
946            searchTerm.length == 40 &&
947            searchTerm.search(/^[A-F0-9]+$/) === 0
948          ) {
949            lookup = "/vks/v1/by-fingerprint/" + searchTerm;
950          }
951        } else {
952          try {
953            searchTerm = EnigmailFuncs.stripEmail(searchTerm);
954          } catch (x) {}
955          lookup = "/vks/v1/by-email/" + searchTerm;
956        }
957        url += lookup;
958      }
959    }
960
961    return {
962      url,
963      method,
964      contentType,
965    };
966  },
967
968  /**
969   * Upload, search or download keys from a keyserver
970   * @param actionFlag:  Number  - Keyserver Action Flags: from EnigmailConstants
971   * @param keyId:       String  - space-separated list of search terms or key IDs
972   * @param keyserver:   String  - keyserver URL (optionally incl. protocol)
973   * @param listener:    optional Object implementing the KeySrvListener API (above)
974   *
975   * @return:   Promise<Number (Status-ID)>
976   */
977  accessKeyServer(actionFlag, keyserver, keyId, listener) {
978    EnigmailLog.DEBUG(
979      `keyserver.jsm: accessVksServer.accessKeyServer(${keyserver})\n`
980    );
981    if (keyserver === null) {
982      keyserver = "keys.openpgp.org";
983    }
984
985    return new Promise((resolve, reject) => {
986      let xmlReq = null;
987      if (listener && typeof listener === "object") {
988        listener.onCancel = function() {
989          EnigmailLog.DEBUG(
990            `keyserver.jsm: accessVksServer.accessKeyServer - onCancel() called\n`
991          );
992          if (xmlReq) {
993            xmlReq.abort();
994          }
995          reject(createError(EnigmailConstants.KEYSERVER_ERR_ABORTED));
996        };
997      }
998      if (actionFlag === EnigmailConstants.REFRESH_KEY) {
999        // we don't (need to) distinguish between refresh and download for our internal protocol
1000        actionFlag = EnigmailConstants.DOWNLOAD_KEY;
1001      }
1002
1003      let uiLocale = Services.locale.appLocalesAsBCP47[0];
1004      let payLoad = this.buildJsonPayload(actionFlag, keyId, uiLocale);
1005      if (payLoad === null) {
1006        reject(createError(EnigmailConstants.KEYSERVER_ERR_UNKNOWN));
1007        return;
1008      }
1009
1010      xmlReq = new XMLHttpRequest();
1011
1012      xmlReq.onload = function() {
1013        EnigmailLog.DEBUG(
1014          "keyserver.jsm: accessVksServer.onload(): status=" +
1015            xmlReq.status +
1016            "\n"
1017        );
1018        switch (actionFlag) {
1019          case EnigmailConstants.UPLOAD_KEY:
1020          case EnigmailConstants.GET_CONFIRMATION_LINK:
1021            EnigmailLog.DEBUG(
1022              "keyserver.jsm: accessVksServer.onload: " +
1023                xmlReq.responseText +
1024                "\n"
1025            );
1026            if (xmlReq.status >= 400) {
1027              reject(createError(EnigmailConstants.KEYSERVER_ERR_SERVER_ERROR));
1028            } else {
1029              resolve(xmlReq.responseText);
1030            }
1031            return;
1032
1033          case EnigmailConstants.SEARCH_KEY:
1034            if (xmlReq.status === 404) {
1035              // key not found
1036              resolve("");
1037            } else if (xmlReq.status >= 400) {
1038              reject(createError(EnigmailConstants.KEYSERVER_ERR_SERVER_ERROR));
1039            } else {
1040              resolve(xmlReq.responseText);
1041            }
1042            return;
1043
1044          case EnigmailConstants.DOWNLOAD_KEY:
1045          case EnigmailConstants.DOWNLOAD_KEY_NO_IMPORT:
1046            if (xmlReq.status >= 400 && xmlReq.status < 500) {
1047              // key not found
1048              resolve(1);
1049            } else if (xmlReq.status >= 500) {
1050              EnigmailLog.DEBUG(
1051                "keyserver.jsm: accessVksServer.onload: " +
1052                  xmlReq.responseText +
1053                  "\n"
1054              );
1055              reject(createError(EnigmailConstants.KEYSERVER_ERR_SERVER_ERROR));
1056            } else {
1057              let errorMsgObj = {},
1058                importedKeysObj = {};
1059              if (actionFlag === EnigmailConstants.DOWNLOAD_KEY) {
1060                let r = EnigmailKeyRing.importKey(
1061                  null,
1062                  false,
1063                  xmlReq.responseText,
1064                  false,
1065                  "",
1066                  errorMsgObj,
1067                  importedKeysObj
1068                );
1069                if (r === 0) {
1070                  resolve(importedKeysObj.value);
1071                } else {
1072                  reject(
1073                    createError(EnigmailConstants.KEYSERVER_ERR_IMPORT_ERROR)
1074                  );
1075                }
1076              } else {
1077                // DOWNLOAD_KEY_NO_IMPORT
1078                resolve(xmlReq.responseText);
1079              }
1080            }
1081            return;
1082        }
1083        resolve(-1);
1084      };
1085
1086      xmlReq.onerror = function(e) {
1087        EnigmailLog.DEBUG(
1088          "keyserver.jsm: accessVksServer.accessKeyServer: onerror: " + e + "\n"
1089        );
1090        let err = FeedUtils.createTCPErrorFromFailedXHR(e.target);
1091        switch (err.type) {
1092          case "SecurityCertificate":
1093            reject(
1094              createError(EnigmailConstants.KEYSERVER_ERR_CERTIFICATE_ERROR)
1095            );
1096            break;
1097          case "SecurityProtocol":
1098            reject(createError(EnigmailConstants.KEYSERVER_ERR_SECURITY_ERROR));
1099            break;
1100          case "Network":
1101            reject(
1102              createError(EnigmailConstants.KEYSERVER_ERR_SERVER_UNAVAILABLE)
1103            );
1104            break;
1105        }
1106        reject(createError(EnigmailConstants.KEYSERVER_ERR_SERVER_UNAVAILABLE));
1107      };
1108
1109      xmlReq.onloadend = function() {
1110        EnigmailLog.DEBUG(
1111          "keyserver.jsm: accessVksServer.accessKeyServer: loadEnd\n"
1112        );
1113      };
1114
1115      let { url, method, contentType } = this.createRequestUrl(
1116        keyserver,
1117        actionFlag,
1118        keyId
1119      );
1120
1121      EnigmailLog.DEBUG(
1122        `keyserver.jsm: accessVksServer.accessKeyServer: requesting ${method} for ${url}\n`
1123      );
1124      xmlReq.open(method, url);
1125      xmlReq.setRequestHeader("Content-Type", contentType);
1126      xmlReq.send(payLoad);
1127    });
1128  },
1129
1130  /**
1131   * Download keys from a keyserver
1132   * @param keyIDs:      String  - space-separated list of search terms or key IDs
1133   * @param keyserver:   String  - keyserver URL (optionally incl. protocol)
1134   * @param listener:    optional Object implementing the KeySrvListener API (above)
1135   *
1136   * @return:   Promise<...>
1137   */
1138  async download(autoImport, keyIDs, keyserver, listener = null) {
1139    EnigmailLog.DEBUG(`keyserver.jsm: accessVksServer.download(${keyIDs})\n`);
1140    let keyIdArr = keyIDs.split(/ +/);
1141    let retObj = {
1142      result: 0,
1143      errorDetails: "",
1144      keyList: [],
1145    };
1146
1147    for (let i = 0; i < keyIdArr.length; i++) {
1148      try {
1149        let r = await this.accessKeyServer(
1150          autoImport
1151            ? EnigmailConstants.DOWNLOAD_KEY
1152            : EnigmailConstants.DOWNLOAD_KEY_NO_IMPORT,
1153          keyserver,
1154          keyIdArr[i],
1155          listener
1156        );
1157        if (autoImport) {
1158          if (Array.isArray(r)) {
1159            retObj.keyList = retObj.keyList.concat(r);
1160          }
1161        } else if (typeof r == "string") {
1162          retObj.keyData = r;
1163        } else {
1164          retObj.result = r;
1165        }
1166      } catch (ex) {
1167        retObj.result = ex.result;
1168        retObj.errorDetails = ex.errorDetails;
1169        throw retObj;
1170      }
1171
1172      if (listener && "onProgress" in listener) {
1173        listener.onProgress(((i + 1) / keyIdArr.length) * 100);
1174      }
1175    }
1176
1177    return retObj;
1178  },
1179
1180  refresh(keyServer, listener = null) {
1181    let keyList = EnigmailKeyRing.getAllKeys()
1182      .keyList.map(keyObj => {
1183        return "0x" + keyObj.fpr;
1184      })
1185      .join(" ");
1186
1187    return this.download(true, keyList, keyServer, listener);
1188  },
1189
1190  async requestConfirmationLink(keyserver, jsonFragment) {
1191    EnigmailLog.DEBUG(
1192      `keyserver.jsm: accessVksServer.requestConfirmationLink()\n`
1193    );
1194
1195    let response = JSON.parse(jsonFragment);
1196
1197    let addr = [];
1198
1199    for (let email in response.status) {
1200      if (response.status[email] !== "published") {
1201        addr.push(email);
1202      }
1203    }
1204
1205    if (addr.length > 0) {
1206      let r = await this.accessKeyServer(
1207        EnigmailConstants.GET_CONFIRMATION_LINK,
1208        keyserver,
1209        {
1210          token: response.token,
1211          addresses: addr,
1212        },
1213        null
1214      );
1215
1216      if (typeof r === "string") {
1217        return addr.length;
1218      }
1219    }
1220
1221    return 0;
1222  },
1223
1224  /**
1225   * Upload keys to a keyserver
1226   * @param keyIDs: String  - space-separated list of search terms or key IDs
1227   * @param keyserver:   String  - keyserver URL (optionally incl. protocol)
1228   * @param listener:    optional Object implementing the KeySrvListener API (above)
1229   *
1230   * @return:   Promise<...>
1231   */
1232  async upload(keyIDs, keyserver, listener = null) {
1233    EnigmailLog.DEBUG(`keyserver.jsm: accessVksServer.upload(${keyIDs})\n`);
1234    let keyIdArr = keyIDs.split(/ +/);
1235    let retObj = {
1236      result: 0,
1237      errorDetails: "",
1238      keyList: [],
1239    };
1240
1241    for (let i = 0; i < keyIdArr.length; i++) {
1242      let keyObj = EnigmailKeyRing.getKeyById(keyIdArr[i]);
1243
1244      if (!keyObj.secretAvailable) {
1245        // VKS keyservers only accept uploading own keys
1246        retObj.result = 1;
1247        retObj.errorDetails = "NO_SECRET_KEY_AVAILABLE";
1248        throw retObj;
1249      }
1250
1251      try {
1252        let r = await this.accessKeyServer(
1253          EnigmailConstants.UPLOAD_KEY,
1254          keyserver,
1255          keyIdArr[i],
1256          listener
1257        );
1258        if (typeof r === "string") {
1259          retObj.keyList.push(keyIdArr[i]);
1260          let req = await this.requestConfirmationLink(keyserver, r);
1261
1262          if (req >= 0) {
1263            retObj.result = 0;
1264            retObj.numEmails = req;
1265          }
1266        } else {
1267          retObj.result = r;
1268        }
1269      } catch (ex) {
1270        retObj.result = ex.result;
1271        retObj.errorDetails = ex.errorDetails;
1272        throw retObj;
1273      }
1274
1275      if (listener && "onProgress" in listener) {
1276        listener.onProgress(((i + 1) / keyIdArr.length) * 100);
1277      }
1278    }
1279
1280    return retObj;
1281  },
1282
1283  /**
1284   * Search for keys on a keyserver
1285   * @param searchTerm:  String  - search term
1286   * @param keyserver:   String  - keyserver URL (optionally incl. protocol)
1287   * @param listener:    optional Object implementing the KeySrvListener API (above)
1288   *
1289   * @return:   Promise<Object>
1290   *    - result: Number
1291   *    - pubKeys: Array of Object:
1292   *         PubKeys: Object with:
1293   *           - keyId: String
1294   *           - keyLen: String
1295   *           - keyType: String
1296   *           - created: String (YYYY-MM-DD)
1297   *           - status: String: one of ''=valid, r=revoked, e=expired
1298   *           - uid: Array of Strings with UIDs
1299   */
1300  async searchKeyserver(searchTerm, keyserver, listener = null) {
1301    EnigmailLog.DEBUG(`keyserver.jsm: accessVksServer.search(${searchTerm})\n`);
1302    let retObj = {
1303      result: 0,
1304      errorDetails: "",
1305      pubKeys: [],
1306    };
1307    let key = null;
1308
1309    let searchArr = searchTerm.split(/ +/);
1310
1311    try {
1312      for (let i in searchArr) {
1313        let r = await this.accessKeyServer(
1314          EnigmailConstants.SEARCH_KEY,
1315          keyserver,
1316          searchArr[i],
1317          listener
1318        );
1319
1320        const cApi = EnigmailCryptoAPI();
1321        let keyList = await cApi.getKeyListFromKeyBlockAPI(
1322          r,
1323          true,
1324          false,
1325          true
1326        );
1327        if (!keyList) {
1328          retObj.result = -1;
1329          // TODO: should we set retObj.errorDetails to a string?
1330          return retObj;
1331        }
1332
1333        for (let k in keyList) {
1334          key = {
1335            keyId: keyList[k].fpr,
1336            keyLen: "0",
1337            keyType: "",
1338            created: keyList[k].created,
1339            uid: [keyList[k].name],
1340            status: keyList[k].revoke ? "r" : "",
1341          };
1342
1343          for (let uid of keyList[k].uids) {
1344            key.uid.push(uid);
1345          }
1346
1347          retObj.pubKeys.push(key);
1348        }
1349      }
1350    } catch (ex) {
1351      retObj.result = ex.result;
1352      retObj.errorDetails = ex.errorDetails;
1353      throw retObj;
1354    }
1355
1356    return retObj;
1357  },
1358};
1359
1360var EnigmailKeyServer = {
1361  /**
1362   * Download keys from a keyserver
1363   * @param keyIDs:      String  - space-separated list of FPRs or key IDs
1364   * @param keyserver:   String  - keyserver URL (optionally incl. protocol)
1365   * @param listener:    optional Object implementing the KeySrvListener API (above)
1366   *
1367   * @return:   Promise<Object>
1368   *     Object: - result: Number           - result Code (0 = OK),
1369   *             - keyList: Array of String - imported key FPR
1370   */
1371  download(keyIDs, keyserver = null, listener) {
1372    let acc = getAccessType(keyserver);
1373    return acc.download(true, keyIDs, keyserver, listener);
1374  },
1375
1376  downloadNoImport(keyIDs, keyserver = null, listener) {
1377    let acc = getAccessType(keyserver);
1378    return acc.download(false, keyIDs, keyserver, listener);
1379  },
1380
1381  /**
1382   * Upload keys to a keyserver
1383   * @param keyIDs:      String  - space-separated list of key IDs or FPR
1384   * @param keyserver:   String  - keyserver URL (optionally incl. protocol)
1385   * @param listener:    optional Object implementing the KeySrvListener API (above)
1386   *
1387   * @return:   Promise<Object>
1388   *     Object: - result: Number           - result Code (0 = OK),
1389   *             - keyList: Array of String - imported key FPR
1390   */
1391
1392  upload(keyIDs, keyserver = null, listener) {
1393    let acc = getAccessType(keyserver);
1394    return acc.upload(keyIDs, keyserver, listener);
1395  },
1396
1397  /**
1398   * Search keys on a keyserver
1399   * @param searchString: String - search term. Multiple email addresses can be search by spaces
1400   * @param keyserver:    String - keyserver URL (optionally incl. protocol)
1401   * @param listener:     optional Object implementing the KeySrvListener API (above)
1402   *
1403   * @return:   Promise<Object>
1404   *    - result: Number
1405   *    - pubKeys: Array of Object:
1406   *         PubKeys: Object with:
1407   *           - keyId: String
1408   *           - keyLen: String
1409   *           - keyType: String
1410   *           - created: String (YYYY-MM-DD)
1411   *           - status: String: one of ''=valid, r=revoked, e=expired
1412   *           - uid: Array of Strings with UIDs
1413   */
1414  searchKeyserver(searchString, keyserver = null, listener) {
1415    let acc = getAccessType(keyserver);
1416    return acc.search(searchString, keyserver, listener);
1417  },
1418
1419  /**
1420   * Refresh all keys
1421   *
1422   * @param keyserver:   String  - keyserver URL (optionally incl. protocol)
1423   * @param listener:    optional Object implementing the KeySrvListener API (above)
1424   *
1425   * @return:   Promise<resultStatus> (identical to download)
1426   */
1427  refresh(keyserver = null, listener) {
1428    let acc = getAccessType(keyserver);
1429    return acc.refresh(keyserver, listener);
1430  },
1431};
1432