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