/* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is Bookmarks Sync. * * The Initial Developer of the Original Code is Mozilla. * Portions created by the Initial Developer are Copyright (C) 2007 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Dan Mills * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ['Crypto']; const Cc = Components.classes; const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://weave/log4moz.js"); Cu.import("resource://weave/constants.js"); Cu.import("resource://weave/util.js"); Cu.import("resource://weave/async.js"); Function.prototype.async = Async.sugar; Utils.lazy(this, 'Crypto', CryptoSvc); function CryptoSvc() { this._init(); } CryptoSvc.prototype = { _logName: "Crypto", __os: null, get _os() { if (!this.__os) this.__os = Cc["@mozilla.org/observer-service;1"] .getService(Ci.nsIObserverService); return this.__os; }, __xxtea: {}, __xxteaLoaded: false, get _xxtea() { if (!this.__xxteaLoaded) { Cu.import("resource://weave/xxtea.js", this.__xxtea); this.__xxteaLoaded = true; } return this.__xxtea; }, get defaultAlgorithm() { let branch = Cc["@mozilla.org/preferences-service;1"] .getService(Ci.nsIPrefBranch); return branch.getCharPref("extensions.weave.encryption"); }, set defaultAlgorithm(value) { let branch = Cc["@mozilla.org/preferences-service;1"] .getService(Ci.nsIPrefBranch); let cur = branch.getCharPref("extensions.weave.encryption"); if (value != cur) branch.setCharPref("extensions.weave.encryption", value); }, _init: function Crypto__init() { this._log = Log4Moz.Service.getLogger("Service." + this._logName); this._log.level = Log4Moz.Level[Utils.prefs.getCharPref("log.logger.service.crypto")]; let branch = Cc["@mozilla.org/preferences-service;1"] .getService(Ci.nsIPrefBranch2); branch.addObserver("extensions.weave.encryption", this, false); }, _openssl: function Crypto__openssl() { let extMgr = Components.classes["@mozilla.org/extensions/manager;1"] .getService(Components.interfaces.nsIExtensionManager); let loc = extMgr.getInstallLocation("{340c2bbc-ce74-4362-90b5-7c26312808ef}"); let wrap = loc.getItemLocation("{340c2bbc-ce74-4362-90b5-7c26312808ef}"); wrap.append("openssl"); let bin; let os = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS; switch(os) { case "WINNT": wrap.append("win32"); wrap.append("exec.bat"); bin = wrap.parent.path + "\\openssl.exe"; break; case "Linux": case "Darwin": wrap.append("unix"); wrap.append("exec.sh"); bin = "openssl"; break; default: throw "encryption not supported on this platform: " + os; } let args = Array.prototype.slice.call(arguments); args.unshift(wrap, Utils.getTmp().path, bin); let rv = Utils.runCmd.apply(null, args); if (rv != 0) throw "openssl did not run successfully, error code " + rv; }, _opensslPBE: function Crypto__openssl(op, algorithm, input, password) { let inputFile = Utils.getTmp("input"); let [inputFOS] = Utils.open(inputFile, ">"); inputFOS.writeString(input); inputFOS.close(); // nsIProcess doesn't support stdin, so we write a file instead let passFile = Utils.getTmp("pass"); let [passFOS] = Utils.open(passFile, ">", PERMS_PASSFILE); passFOS.writeString(password); passFOS.close(); try { this._openssl(algorithm, op, "-a", "-salt", "-in", "input", "-out", "output", "-pass", "file:pass"); } catch (e) { throw e; } finally { passFile.remove(false); inputFile.remove(false); } let outputFile = Utils.getTmp("output"); let [outputFIS] = Utils.open(outputFile, "<"); let ret = Utils.readStream(outputFIS); outputFIS.close(); outputFile.remove(false); return ret; }, // generates a random string that can be used as a passphrase _opensslRand: function Crypto__opensslRand(length) { if (!length) length = 128; let outputFile = Utils.getTmp("output"); if (outputFile.exists()) outputFile.remove(false); this._openssl("rand", "-base64", "-out", "output", length); let [outputFIS] = Utils.open(outputFile, "<"); let ret = Utils.readStream(outputFIS); outputFIS.close(); outputFile.remove(false); return ret; }, // generates an rsa public/private key pair, with the private key encrypted _opensslRSAKeyGen: function Crypto__opensslRSAKeyGen(identity, algorithm, bits) { if (!algorithm) algorithm = "aes-256-cbc"; if (!bits) bits = "2048"; let privKeyF = Utils.getTmp("privkey.pem"); if (privKeyF.exists()) privKeyF.remove(false); this._openssl("genrsa", "-out", "privkey.pem", bits); let pubKeyF = Utils.getTmp("pubkey.pem"); if (pubKeyF.exists()) pubKeyF.remove(false); this._openssl("rsa", "-in", "privkey.pem", "-out", "pubkey.pem", "-outform", "PEM", "-pubout"); let cryptedKeyF = Utils.getTmp("enckey.pem"); if (cryptedKeyF.exists()) cryptedKeyF.remove(false); // nsIProcess doesn't support stdin, so we write a file instead let passFile = Utils.getTmp("pass"); let [passFOS] = Utils.open(passFile, ">", PERMS_PASSFILE); passFOS.writeString(identity.password); passFOS.close(); try { this._openssl("pkcs8", "-in", "privkey.pem", "-out", "enckey.pem", "-topk8", "-v2", algorithm, "-passout", "file:pass"); } catch (e) { throw e; } finally { passFile.remove(false); privKeyF.remove(false); } let [cryptedKeyFIS] = Utils.open(cryptedKeyF, "<"); let cryptedKey = Utils.readStream(cryptedKeyFIS); cryptedKeyFIS.close(); cryptedKeyF.remove(false); let [pubKeyFIS] = Utils.open(pubKeyF, "<"); let pubKey = Utils.readStream(pubKeyFIS); pubKeyFIS.close(); pubKeyF.remove(false); return [cryptedKey, pubKey]; }, // returns 'input' encrypted with the 'pubkey' public RSA key _opensslRSAencrypt: function Crypto__opensslRSAencrypt(input, identity) { let inputFile = Utils.getTmp("input"); let [inputFOS] = Utils.open(inputFile, ">"); inputFOS.writeString(input); inputFOS.close(); let keyFile = Utils.getTmp("key"); let [keyFOS] = Utils.open(keyFile, ">"); keyFOS.writeString(identity.pubkey); keyFOS.close(); let tmpFile = Utils.getTmp("tmp-output"); if (tmpFile.exists()) tmpFile.remove(false); let outputFile = Utils.getTmp("output"); if (outputFile.exists()) outputFile.remove(false); this._openssl("rsautl", "-encrypt", "-pubin", "-inkey", "key", "-in", "input", "-out", "tmp-output"); this._openssl("base64", "-in", "tmp-output", "-out", "output"); let [outputFIS] = Utils.open(outputFile, "<"); let output = Utils.readStream(outputFIS); outputFIS.close(); outputFile.remove(false); return output; }, // returns 'input' decrypted with the 'privkey' private RSA key and password _opensslRSAdecrypt: function Crypto__opensslRSAdecrypt(input, identity) { let inputFile = Utils.getTmp("input"); let [inputFOS] = Utils.open(inputFile, ">"); inputFOS.writeString(input); inputFOS.close(); let keyFile = Utils.getTmp("key"); let [keyFOS] = Utils.open(keyFile, ">"); keyFOS.writeString(identity.privkey); keyFOS.close(); let tmpKeyFile = Utils.getTmp("tmp-key"); if (tmpKeyFile.exists()) tmpKeyFile.remove(false); let tmpFile = Utils.getTmp("tmp-output"); if (tmpFile.exists()) tmpFile.remove(false); let outputFile = Utils.getTmp("output"); if (outputFile.exists()) outputFile.remove(false); // nsIProcess doesn't support stdin, so we write a file instead let passFile = Utils.getTmp("pass"); let [passFOS] = Utils.open(passFile, ">", PERMS_PASSFILE); passFOS.writeString(identity.password); passFOS.close(); try { this._openssl("base64", "-d", "-in", "input", "-out", "tmp-output"); // FIXME: this is because openssl.exe (in windows only) doesn't // seem to support -passin for rsautl, but it works for rsa. this._openssl("rsa", "-in", "key", "-out", "tmp-key", "-passin", "file:pass"); this._openssl("rsautl", "-decrypt", "-inkey", "tmp-key", "-in", "tmp-output", "-out", "output"); } catch(e) { throw e; } finally { passFile.remove(false); tmpKeyFile.remove(false); tmpFile.remove(false); keyFile.remove(false); } let [outputFIS] = Utils.open(outputFile, "<"); let output = Utils.readStream(outputFIS); outputFIS.close(); outputFile.remove(false); return output; }, // returns the public key from the private key _opensslRSAkeydecrypt: function Crypto__opensslRSAkeydecrypt(identity) { let keyFile = Utils.getTmp("key"); let [keyFOS] = Utils.open(keyFile, ">"); keyFOS.writeString(identity.privkey); keyFOS.close(); let outputFile = Utils.getTmp("output"); if (outputFile.exists()) outputFile.remove(false); // nsIProcess doesn't support stdin, so we write a file instead let passFile = Utils.getTmp("pass"); let [passFOS] = Utils.open(passFile, ">", PERMS_PASSFILE); passFOS.writeString(identity.password); passFOS.close(); try { this._openssl("rsa", "-in", "key", "-pubout", "-out", "output", "-passin", "file:pass"); } catch(e) { throw e; } finally { passFile.remove(false); } let [outputFIS] = Utils.open(outputFile, "<"); let output = Utils.readStream(outputFIS); outputFIS.close(); outputFile.remove(false); return output; }, QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupports]), // nsIObserver observe: function Sync_observe(subject, topic, data) { switch (topic) { case "extensions.weave.encryption": { let branch = Cc["@mozilla.org/preferences-service;1"] .getService(Ci.nsIPrefBranch); let cur = branch.getCharPref("extensions.weave.encryption"); if (cur == data) return; switch (data) { case "none": this._log.info("Encryption disabled"); break; case "XXTEA": case "XXXTEA": // Weave 0.1 had this typo this._log.info("Using encryption algorithm: " + data); break; default: this._log.warn("Unknown encryption algorithm, resetting"); branch.setCharPref("extensions.weave.encryption", "XXTEA"); return; // otherwise we'll send the alg changed event twice } // FIXME: listen to this bad boy somewhere this._os.notifyObservers(null, "weave:encryption:algorithm-changed", ""); } break; default: this._log.warn("Unknown encryption preference changed - ignoring"); } }, // Crypto PBEencrypt: function Crypto_PBEencrypt(data, identity, algorithm) { let self = yield; let ret; if (!algorithm) algorithm = this.defaultAlgorithm; if (algorithm != "none") this._log.debug("Encrypting data"); switch (algorithm) { case "none": ret = data; break; case "XXXTEA": // Weave 0.1.12.10 and below had this typo case "XXTEA": { let gen = this._xxtea.encrypt(data, identity.password); ret = gen.next(); let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); try { while (typeof(ret) == "object") { timer.initWithCallback(self.listener, 0, timer.TYPE_ONE_SHOT); yield; // Yield to main loop ret = gen.next(); } gen.close(); } finally { timer = null; } } break; case "aes-128-cbc": case "aes-192-cbc": case "aes-256-cbc": case "bf-cbc": case "des-ede3-cbc": ret = this._opensslPBE("-e", algorithm, data, identity.password); break; default: throw "Unknown encryption algorithm: " + algorithm; } if (algorithm != "none") this._log.debug("Done encrypting data"); self.done(ret); }, PBEdecrypt: function Crypto_PBEdecrypt(data, identity, algorithm) { let self = yield; let ret; if (!algorithm) algorithm = this.defaultAlgorithm; if (algorithm != "none") this._log.debug("Decrypting data"); switch (algorithm) { case "none": ret = data; break; case "XXXTEA": // Weave 0.1.12.10 and below had this typo case "XXTEA": { let gen = this._xxtea.decrypt(data, identity.password); ret = gen.next(); let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); try { while (typeof(ret) == "object") { timer.initWithCallback(self.listener, 0, timer.TYPE_ONE_SHOT); yield; // Yield to main loop ret = gen.next(); } gen.close(); } finally { timer = null; } } break; case "aes-128-cbc": case "aes-192-cbc": case "aes-256-cbc": case "bf-cbc": case "des-ede3-cbc": ret = this._opensslPBE("-d", algorithm, data, identity.password); break; default: throw "Unknown encryption algorithm: " + algorithm; } if (algorithm != "none") this._log.debug("Done decrypting data"); self.done(ret); }, PBEkeygen: function Crypto_PBEkeygen() { let self = yield; let ret = this._opensslRand(); self.done(ret); }, RSAkeygen: function Crypto_RSAkeygen(identity) { let self = yield; let ret = this._opensslRSAKeyGen(identity); self.done(ret); }, RSAencrypt: function Crypto_RSAencrypt(data, identity) { let self = yield; let ret = this._opensslRSAencrypt(data, identity); self.done(ret); }, RSAdecrypt: function Crypto_RSAdecrypt(data, identity) { let self = yield; let ret = this._opensslRSAdecrypt(data, identity); self.done(ret); }, RSAkeydecrypt: function Crypto_RSAkeydecrypt(identity) { let self = yield; let ret = this._opensslRSAkeydecrypt(identity); self.done(ret); } };