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