/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; this.EXPORTED_SYMBOLS = ["BrowserID"]; const {classes: Cc, interfaces: Ci, utils: Cu} = Components; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://services-common/utils.js"); Cu.import("resource://services-common/log4moz.js"); Cu.import("resource://services-common/preferences.js"); const PREFS = new Preferences("services.aitc.browserid."); /** * This implementation will be replaced with native crypto and assertion * generation goodness. See bug 753238. */ function BrowserIDService() { this._log = Log4Moz.repository.getLogger("Service.AITC.BrowserID"); this._log.level = Log4Moz.Level[PREFS.get("log")]; } BrowserIDService.prototype = { /** * Getter that returns the freshest value for ID_URI. */ get ID_URI() { return PREFS.get("url"); }, /** * Obtain a BrowserID assertion with the specified characteristics. * * @param cb * (Function) Callback to be called with (err, assertion) where 'err' * can be an Error or NULL, and 'assertion' can be NULL or a valid * BrowserID assertion. If no callback is provided, an exception is * thrown. * * @param options * (Object) An object that may contain the following properties: * * "requiredEmail" : An email for which the assertion is to be * issued. If one could not be obtained, the call * will fail. If this property is not specified, * the default email as set by the user will be * chosen. If both this property and "sameEmailAs" * are set, an exception will be thrown. * * "sameEmailAs" : If set, instructs the function to issue an * assertion for the same email that was provided * to the domain specified by this value. If this * information could not be obtained, the call * will fail. If both this property and * "requiredEmail" are set, an exception will be * thrown. * * "audience" : The audience for which the assertion is to be * issued. If this property is not set an exception * will be thrown. * * Any properties not listed above will be ignored. * * (This function could use some love in terms of what arguments it accepts. * See bug 746401.) */ getAssertion: function getAssertion(cb, options) { if (!cb) { throw new Error("getAssertion called without a callback"); } if (!options) { throw new Error("getAssertion called without any options"); } if (!options.audience) { throw new Error("getAssertion called without an audience"); } if (options.sameEmailAs && options.requiredEmail) { throw new Error( "getAssertion sameEmailAs and requiredEmail are mutually exclusive" ); } new Sandbox(this._getEmails.bind(this, cb, options), this.ID_URI); }, /** * Obtain a BrowserID assertion by asking the user to login and select an * email address. * * @param cb * (Function) Callback to be called with (err, assertion) where 'err' * can be an Error or NULL, and 'assertion' can be NULL or a valid * BrowserID assertion. If no callback is provided, an exception is * thrown. * * @param win * (Window) A contentWindow that has a valid document loaded. If this * argument is provided the user will be asked to login in the context * of the document currently loaded in this window. * * The audience of the assertion will be set to the domain of the * loaded document, and the "audience" property in the "options" * argument (if provided), will be ignored. The email to which this * assertion issued will be selected by the user when they login (and * "requiredEmail" or "sameEmailAs", if provided, will be ignored). If * the user chooses to not login, this call will fail. * * Be aware! The provided contentWindow must also have loaded the * BrowserID include.js shim for this to work! This behavior is * temporary until we implement native support for navigator.id. * * @param options * (Object) Currently an empty object. Present for future compatiblity * when options for a login case may be added. Any properties, if * present, are ignored. */ getAssertionWithLogin: function getAssertionWithLogin(cb, win, options) { if (!cb) { throw new Error("getAssertionWithLogin called without a callback"); } if (!win) { throw new Error("getAssertionWithLogin called without a window"); } this._getAssertionWithLogin(cb, win); }, /** * Internal implementation methods begin here */ // Try to get the user's email(s). If user isn't logged in, this will be empty _getEmails: function _getEmails(cb, options, sandbox) { let self = this; if (!sandbox) { cb(new Error("Sandbox not created"), null); return; } function callback(res) { let emails = {}; try { emails = JSON.parse(res); } catch (e) { self._log.error("Exception in JSON.parse for _getAssertion: " + e); } self._gotEmails(emails, sandbox, cb, options); } sandbox.box.importFunction(callback); let scriptText = "var list = window.BrowserID.User.getStoredEmailKeypairs();" + "callback(JSON.stringify(list));"; Cu.evalInSandbox(scriptText, sandbox.box, "1.8", this.ID_URI, 1); }, // Received a list of emails from BrowserID for current user _gotEmails: function _gotEmails(emails, sandbox, cb, options) { let keys = Object.keys(emails); // If list is empty, user is not logged in, or doesn't have a default email. if (!keys.length) { sandbox.free(); let err = "User is not logged in, or no emails were found"; this._log.error(err); try { cb(new Error(err), null); } catch(e) { this._log.warn("Callback threw in _gotEmails " + CommonUtils.exceptionStr(e)); } return; } // User is logged in. For which email shall we get an assertion? // Case 1: Explicitely provided if (options.requiredEmail) { this._getAssertionWithEmail( sandbox, cb, options.requiredEmail, options.audience ); return; } // Case 2: Derive from a given domain if (options.sameEmailAs) { this._getAssertionWithDomain( sandbox, cb, options.sameEmailAs, options.audience ); return; } // Case 3: Default email this._getAssertionWithEmail( sandbox, cb, keys[0], options.audience ); return; }, /** * Open a login window and ask the user to login, returning the assertion * generated as a result to the caller. */ _getAssertionWithLogin: function _getAssertionWithLogin(cb, win) { // We're executing navigator.id.get as a content script in win. // This results in a popup that we will temporarily unblock. let pm = Services.perms; let principal = win.document.nodePrincipal; let oldPerm = pm.testExactPermissionFromPrincipal(principal, "popup"); try { pm.addFromPrincipal(principal, "popup", pm.ALLOW_ACTION); } catch(e) { this._log.warn("Setting popup blocking to false failed " + e); } // Open sandbox and execute script. This sandbox will be GC'ed. let sandbox = new Cu.Sandbox(win, { wantXrays: false, sandboxPrototype: win }); let self = this; function callback(val) { // Set popup blocker permission to original value. try { pm.addFromPrincipal(principal, "popup", oldPerm); } catch(e) { this._log.warn("Setting popup blocking to original value failed " + e); } if (val) { self._log.info("_getAssertionWithLogin succeeded"); try { cb(null, val); } catch(e) { self._log.warn("Callback threw in _getAssertionWithLogin " + CommonUtils.exceptionStr(e)); } } else { let msg = "Could not obtain assertion in _getAssertionWithLogin"; self._log.error(msg); try { cb(new Error(msg), null); } catch(e) { self._log.warn("Callback threw in _getAssertionWithLogin " + CommonUtils.exceptionStr(e)); } } } sandbox.importFunction(callback); function doGetAssertion() { self._log.info("_getAssertionWithLogin Started"); let scriptText = "window.navigator.id.get(" + " callback, {allowPersistent: true}" + ");"; Cu.evalInSandbox(scriptText, sandbox, "1.8", self.ID_URI, 1); } // Sometimes the provided win hasn't fully loaded yet if (!win.document || (win.document.readyState != "complete")) { win.addEventListener("DOMContentLoaded", function _contentLoaded() { win.removeEventListener("DOMContentLoaded", _contentLoaded, false); doGetAssertion(); }, false); } else { doGetAssertion(); } }, /** * Gets an assertion for the specified 'email' and 'audience' */ _getAssertionWithEmail: function _getAssertionWithEmail(sandbox, cb, email, audience) { let self = this; function onSuccess(res) { // Cleanup first. sandbox.free(); // The internal API sometimes calls onSuccess even though no assertion // could be obtained! Double check: if (!res) { let msg = "BrowserID.User.getAssertion empty assertion for " + email; self._log.error(msg); try { cb(new Error(msg), null); } catch(e) { self._log.warn("Callback threw in _getAssertionWithEmail " + CommonUtils.exceptionStr(e)); } return; } // Success self._log.info("BrowserID.User.getAssertion succeeded"); try { cb(null, res); } catch(e) { self._log.warn("Callback threw in _getAssertionWithEmail " + CommonUtils.exceptionStr(e)); } } function onError(err) { sandbox.free(); self._log.info("BrowserID.User.getAssertion failed"); try { cb(err, null); } catch(e) { self._log.warn("Callback threw in _getAssertionWithEmail " + CommonUtils.exceptionStr(e)); } } sandbox.box.importFunction(onSuccess); sandbox.box.importFunction(onError); self._log.info("_getAssertionWithEmail Started"); let scriptText = "window.BrowserID.User.getAssertion(" + "'" + email + "', " + "'" + audience + "', " + "onSuccess, " + "onError" + ");"; Cu.evalInSandbox(scriptText, sandbox.box, "1.8", this.ID_URI, 1); }, /** * Gets the email which was used to login to 'domain'. If one was found, * _getAssertionWithEmail is called to obtain the assertion. */ _getAssertionWithDomain: function _getAssertionWithDomain(sandbox, cb, domain, audience) { let self = this; function onDomainSuccess(email) { if (email) { self._getAssertionWithEmail(sandbox, cb, email, audience); } else { sandbox.free(); try { cb(new Error("No email found for _getAssertionWithDomain"), null); } catch(e) { self._log.warn("Callback threw in _getAssertionWithDomain " + CommonUtils.exceptionStr(e)); } } } sandbox.box.importFunction(onDomainSuccess); // This wil tell us which email was used to login to "domain", if any. self._log.info("_getAssertionWithDomain Started"); let scriptText = "onDomainSuccess(window.BrowserID.Storage.site.get(" + "'" + domain + "', " + "'email'" + "));"; Cu.evalInSandbox(scriptText, sandbox.box, "1.8", this.ID_URI, 1); }, }; /** * An object that represents a sandbox in an iframe loaded with uri. The * callback provided to the constructor will be invoked when the sandbox is * ready to be used. The callback will receive this object as its only argument * and the prepared sandbox may be accessed via the "box" property. * * Please call free() when you are finished with the sandbox to explicitely free * up all associated resources. * * @param cb * (function) Callback to be invoked with a Sandbox, when ready. * @param uri * (String) URI to be loaded in the Sandbox. */ function Sandbox(cb, uri) { this._uri = uri; // Put in a try/catch block because Services.wm.getMostRecentWindow, called in // _createFrame will be null in XPCShell. try { this._createFrame(); this._createSandbox(cb, uri); } catch(e) { this._log = Log4Moz.repository.getLogger("Service.AITC.BrowserID.Sandbox"); this._log.level = Log4Moz.Level[PREFS.get("log")]; this._log.error("Could not create Sandbox " + e); cb(null); } } Sandbox.prototype = { /** * Frees the sandbox and releases the iframe created to host it. */ free: function free() { delete this.box; this._container.removeChild(this._frame); this._frame = null; this._container = null; }, /** * Creates an empty, hidden iframe and sets it to the _iframe * property of this object. * * @return frame * (iframe) An empty, hidden iframe */ _createFrame: function _createFrame() { let doc = Services.wm.getMostRecentWindow("navigator:browser").document; // Insert iframe in to create docshell. let frame = doc.createElement("iframe"); frame.setAttribute("type", "content"); frame.setAttribute("collapsed", "true"); doc.documentElement.appendChild(frame); // Stop about:blank from being loaded. let webNav = frame.docShell.QueryInterface(Ci.nsIWebNavigation); webNav.stop(Ci.nsIWebNavigation.STOP_NETWORK); // Set instance properties. this._frame = frame; this._container = doc.documentElement; }, _createSandbox: function _createSandbox(cb, uri) { let self = this; this._frame.addEventListener( "DOMContentLoaded", function _makeSandboxContentLoaded(event) { if (event.target.location.toString() != uri) { return; } event.target.removeEventListener( "DOMContentLoaded", _makeSandboxContentLoaded, false ); let workerWindow = self._frame.contentWindow; self.box = new Cu.Sandbox(workerWindow, { wantXrays: false, sandboxPrototype: workerWindow }); cb(self); }, true ); // Load the iframe. this._frame.docShell.loadURI( uri, this._frame.docShell.LOAD_FLAGS_NONE, null, // referrer null, // postData null // headers ); }, }; XPCOMUtils.defineLazyGetter(this, "BrowserID", function() { return new BrowserIDService(); });