1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/services/fxaccounts/FxAccounts.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,882 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +this.EXPORTED_SYMBOLS = ["fxAccounts", "FxAccounts"]; 1.9 + 1.10 +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; 1.11 + 1.12 +Cu.import("resource://gre/modules/Log.jsm"); 1.13 +Cu.import("resource://gre/modules/Promise.jsm"); 1.14 +Cu.import("resource://gre/modules/osfile.jsm"); 1.15 +Cu.import("resource://services-common/utils.js"); 1.16 +Cu.import("resource://services-crypto/utils.js"); 1.17 +Cu.import("resource://gre/modules/Services.jsm"); 1.18 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.19 +Cu.import("resource://gre/modules/Timer.jsm"); 1.20 +Cu.import("resource://gre/modules/Task.jsm"); 1.21 +Cu.import("resource://gre/modules/FxAccountsCommon.js"); 1.22 + 1.23 +XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsClient", 1.24 + "resource://gre/modules/FxAccountsClient.jsm"); 1.25 + 1.26 +XPCOMUtils.defineLazyModuleGetter(this, "jwcrypto", 1.27 + "resource://gre/modules/identity/jwcrypto.jsm"); 1.28 + 1.29 +// All properties exposed by the public FxAccounts API. 1.30 +let publicProperties = [ 1.31 + "getAccountsClient", 1.32 + "getAccountsSignInURI", 1.33 + "getAccountsSignUpURI", 1.34 + "getAssertion", 1.35 + "getKeys", 1.36 + "getSignedInUser", 1.37 + "loadAndPoll", 1.38 + "localtimeOffsetMsec", 1.39 + "now", 1.40 + "promiseAccountsForceSigninURI", 1.41 + "resendVerificationEmail", 1.42 + "setSignedInUser", 1.43 + "signOut", 1.44 + "version", 1.45 + "whenVerified" 1.46 +]; 1.47 + 1.48 +// An AccountState object holds all state related to one specific account. 1.49 +// Only one AccountState is ever "current" in the FxAccountsInternal object - 1.50 +// whenever a user logs out or logs in, the current AccountState is discarded, 1.51 +// making it impossible for the wrong state or state data to be accidentally 1.52 +// used. 1.53 +// In addition, it has some promise-related helpers to ensure that if an 1.54 +// attempt is made to resolve a promise on a "stale" state (eg, if an 1.55 +// operation starts, but a different user logs in before the operation 1.56 +// completes), the promise will be rejected. 1.57 +// It is intended to be used thusly: 1.58 +// somePromiseBasedFunction: function() { 1.59 +// let currentState = this.currentAccountState; 1.60 +// return someOtherPromiseFunction().then( 1.61 +// data => currentState.resolve(data) 1.62 +// ); 1.63 +// } 1.64 +// If the state has changed between the function being called and the promise 1.65 +// being resolved, the .resolve() call will actually be rejected. 1.66 +AccountState = function(fxaInternal) { 1.67 + this.fxaInternal = fxaInternal; 1.68 +}; 1.69 + 1.70 +AccountState.prototype = { 1.71 + cert: null, 1.72 + keyPair: null, 1.73 + signedInUser: null, 1.74 + whenVerifiedDeferred: null, 1.75 + whenKeysReadyDeferred: null, 1.76 + 1.77 + get isCurrent() this.fxaInternal && this.fxaInternal.currentAccountState === this, 1.78 + 1.79 + abort: function() { 1.80 + if (this.whenVerifiedDeferred) { 1.81 + this.whenVerifiedDeferred.reject( 1.82 + new Error("Verification aborted; Another user signing in")); 1.83 + this.whenVerifiedDeferred = null; 1.84 + } 1.85 + 1.86 + if (this.whenKeysReadyDeferred) { 1.87 + this.whenKeysReadyDeferred.reject( 1.88 + new Error("Verification aborted; Another user signing in")); 1.89 + this.whenKeysReadyDeferred = null; 1.90 + } 1.91 + this.cert = null; 1.92 + this.keyPair = null; 1.93 + this.signedInUser = null; 1.94 + this.fxaInternal = null; 1.95 + }, 1.96 + 1.97 + getUserAccountData: function() { 1.98 + // Skip disk if user is cached. 1.99 + if (this.signedInUser) { 1.100 + return this.resolve(this.signedInUser.accountData); 1.101 + } 1.102 + 1.103 + return this.fxaInternal.signedInUserStorage.get().then( 1.104 + user => { 1.105 + if (logPII) { 1.106 + // don't stringify unless it will be written. We should replace this 1.107 + // check with param substitutions added in bug 966674 1.108 + log.debug("getUserAccountData -> " + JSON.stringify(user)); 1.109 + } 1.110 + if (user && user.version == this.version) { 1.111 + log.debug("setting signed in user"); 1.112 + this.signedInUser = user; 1.113 + } 1.114 + return this.resolve(user ? user.accountData : null); 1.115 + }, 1.116 + err => { 1.117 + if (err instanceof OS.File.Error && err.becauseNoSuchFile) { 1.118 + // File hasn't been created yet. That will be done 1.119 + // on the first call to getSignedInUser 1.120 + return this.resolve(null); 1.121 + } 1.122 + return this.reject(err); 1.123 + } 1.124 + ); 1.125 + }, 1.126 + 1.127 + setUserAccountData: function(accountData) { 1.128 + return this.fxaInternal.signedInUserStorage.get().then(record => { 1.129 + if (!this.isCurrent) { 1.130 + return this.reject(new Error("Another user has signed in")); 1.131 + } 1.132 + record.accountData = accountData; 1.133 + this.signedInUser = record; 1.134 + return this.fxaInternal.signedInUserStorage.set(record) 1.135 + .then(() => this.resolve(accountData)); 1.136 + }); 1.137 + }, 1.138 + 1.139 + 1.140 + getCertificate: function(data, keyPair, mustBeValidUntil) { 1.141 + if (logPII) { 1.142 + // don't stringify unless it will be written. We should replace this 1.143 + // check with param substitutions added in bug 966674 1.144 + log.debug("getCertificate" + JSON.stringify(this.signedInUser)); 1.145 + } 1.146 + // TODO: get the lifetime from the cert's .exp field 1.147 + if (this.cert && this.cert.validUntil > mustBeValidUntil) { 1.148 + log.debug(" getCertificate already had one"); 1.149 + return this.resolve(this.cert.cert); 1.150 + } 1.151 + // else get our cert signed 1.152 + let willBeValidUntil = this.fxaInternal.now() + CERT_LIFETIME; 1.153 + return this.fxaInternal.getCertificateSigned(data.sessionToken, 1.154 + keyPair.serializedPublicKey, 1.155 + CERT_LIFETIME).then( 1.156 + cert => { 1.157 + log.debug("getCertificate got a new one: " + !!cert); 1.158 + this.cert = { 1.159 + cert: cert, 1.160 + validUntil: willBeValidUntil 1.161 + }; 1.162 + return cert; 1.163 + } 1.164 + ).then(result => this.resolve(result)); 1.165 + }, 1.166 + 1.167 + getKeyPair: function(mustBeValidUntil) { 1.168 + if (this.keyPair && (this.keyPair.validUntil > mustBeValidUntil)) { 1.169 + log.debug("getKeyPair: already have a keyPair"); 1.170 + return this.resolve(this.keyPair.keyPair); 1.171 + } 1.172 + // Otherwse, create a keypair and set validity limit. 1.173 + let willBeValidUntil = this.fxaInternal.now() + KEY_LIFETIME; 1.174 + let d = Promise.defer(); 1.175 + jwcrypto.generateKeyPair("DS160", (err, kp) => { 1.176 + if (err) { 1.177 + return this.reject(err); 1.178 + } 1.179 + this.keyPair = { 1.180 + keyPair: kp, 1.181 + validUntil: willBeValidUntil 1.182 + }; 1.183 + log.debug("got keyPair"); 1.184 + delete this.cert; 1.185 + d.resolve(this.keyPair.keyPair); 1.186 + }); 1.187 + return d.promise.then(result => this.resolve(result)); 1.188 + }, 1.189 + 1.190 + resolve: function(result) { 1.191 + if (!this.isCurrent) { 1.192 + log.info("An accountState promise was resolved, but was actually rejected" + 1.193 + " due to a different user being signed in. Originally resolved" + 1.194 + " with: " + result); 1.195 + return Promise.reject(new Error("A different user signed in")); 1.196 + } 1.197 + return Promise.resolve(result); 1.198 + }, 1.199 + 1.200 + reject: function(error) { 1.201 + // It could be argued that we should just let it reject with the original 1.202 + // error - but this runs the risk of the error being (eg) a 401, which 1.203 + // might cause the consumer to attempt some remediation and cause other 1.204 + // problems. 1.205 + if (!this.isCurrent) { 1.206 + log.info("An accountState promise was rejected, but we are ignoring that" + 1.207 + "reason and rejecting it due to a different user being signed in." + 1.208 + "Originally rejected with: " + reason); 1.209 + return Promise.reject(new Error("A different user signed in")); 1.210 + } 1.211 + return Promise.reject(error); 1.212 + }, 1.213 + 1.214 +} 1.215 + 1.216 +/** 1.217 + * Copies properties from a given object to another object. 1.218 + * 1.219 + * @param from (object) 1.220 + * The object we read property descriptors from. 1.221 + * @param to (object) 1.222 + * The object that we set property descriptors on. 1.223 + * @param options (object) (optional) 1.224 + * {keys: [...]} 1.225 + * Lets the caller pass the names of all properties they want to be 1.226 + * copied. Will copy all properties of the given source object by 1.227 + * default. 1.228 + * {bind: object} 1.229 + * Lets the caller specify the object that will be used to .bind() 1.230 + * all function properties we find to. Will bind to the given target 1.231 + * object by default. 1.232 + */ 1.233 +function copyObjectProperties(from, to, opts = {}) { 1.234 + let keys = (opts && opts.keys) || Object.keys(from); 1.235 + let thisArg = (opts && opts.bind) || to; 1.236 + 1.237 + for (let prop of keys) { 1.238 + let desc = Object.getOwnPropertyDescriptor(from, prop); 1.239 + 1.240 + if (typeof(desc.value) == "function") { 1.241 + desc.value = desc.value.bind(thisArg); 1.242 + } 1.243 + 1.244 + if (desc.get) { 1.245 + desc.get = desc.get.bind(thisArg); 1.246 + } 1.247 + 1.248 + if (desc.set) { 1.249 + desc.set = desc.set.bind(thisArg); 1.250 + } 1.251 + 1.252 + Object.defineProperty(to, prop, desc); 1.253 + } 1.254 +} 1.255 + 1.256 +/** 1.257 + * The public API's constructor. 1.258 + */ 1.259 +this.FxAccounts = function (mockInternal) { 1.260 + let internal = new FxAccountsInternal(); 1.261 + let external = {}; 1.262 + 1.263 + // Copy all public properties to the 'external' object. 1.264 + let prototype = FxAccountsInternal.prototype; 1.265 + let options = {keys: publicProperties, bind: internal}; 1.266 + copyObjectProperties(prototype, external, options); 1.267 + 1.268 + // Copy all of the mock's properties to the internal object. 1.269 + if (mockInternal && !mockInternal.onlySetInternal) { 1.270 + copyObjectProperties(mockInternal, internal); 1.271 + } 1.272 + 1.273 + if (mockInternal) { 1.274 + // Exposes the internal object for testing only. 1.275 + external.internal = internal; 1.276 + } 1.277 + 1.278 + return Object.freeze(external); 1.279 +} 1.280 + 1.281 +/** 1.282 + * The internal API's constructor. 1.283 + */ 1.284 +function FxAccountsInternal() { 1.285 + this.version = DATA_FORMAT_VERSION; 1.286 + 1.287 + // Make a local copy of these constants so we can mock it in testing 1.288 + this.POLL_STEP = POLL_STEP; 1.289 + this.POLL_SESSION = POLL_SESSION; 1.290 + // We will create this.pollTimeRemaining below; it will initially be 1.291 + // set to the value of POLL_SESSION. 1.292 + 1.293 + // We interact with the Firefox Accounts auth server in order to confirm that 1.294 + // a user's email has been verified and also to fetch the user's keys from 1.295 + // the server. We manage these processes in possibly long-lived promises 1.296 + // that are internal to this object (never exposed to callers). Because 1.297 + // Firefox Accounts allows for only one logged-in user, and because it's 1.298 + // conceivable that while we are waiting to verify one identity, a caller 1.299 + // could start verification on a second, different identity, we need to be 1.300 + // able to abort all work on the first sign-in process. The currentTimer and 1.301 + // currentAccountState are used for this purpose. 1.302 + // (XXX - should the timer be directly on the currentAccountState?) 1.303 + this.currentTimer = null; 1.304 + this.currentAccountState = new AccountState(this); 1.305 + 1.306 + // We don't reference |profileDir| in the top-level module scope 1.307 + // as we may be imported before we know where it is. 1.308 + this.signedInUserStorage = new JSONStorage({ 1.309 + filename: DEFAULT_STORAGE_FILENAME, 1.310 + baseDir: OS.Constants.Path.profileDir, 1.311 + }); 1.312 +} 1.313 + 1.314 +/** 1.315 + * The internal API's prototype. 1.316 + */ 1.317 +FxAccountsInternal.prototype = { 1.318 + 1.319 + /** 1.320 + * The current data format's version number. 1.321 + */ 1.322 + version: DATA_FORMAT_VERSION, 1.323 + 1.324 + _fxAccountsClient: null, 1.325 + 1.326 + get fxAccountsClient() { 1.327 + if (!this._fxAccountsClient) { 1.328 + this._fxAccountsClient = new FxAccountsClient(); 1.329 + } 1.330 + return this._fxAccountsClient; 1.331 + }, 1.332 + 1.333 + /** 1.334 + * Return the current time in milliseconds as an integer. Allows tests to 1.335 + * manipulate the date to simulate certificate expiration. 1.336 + */ 1.337 + now: function() { 1.338 + return this.fxAccountsClient.now(); 1.339 + }, 1.340 + 1.341 + getAccountsClient: function() { 1.342 + return this.fxAccountsClient; 1.343 + }, 1.344 + 1.345 + /** 1.346 + * Return clock offset in milliseconds, as reported by the fxAccountsClient. 1.347 + * This can be overridden for testing. 1.348 + * 1.349 + * The offset is the number of milliseconds that must be added to the client 1.350 + * clock to make it equal to the server clock. For example, if the client is 1.351 + * five minutes ahead of the server, the localtimeOffsetMsec will be -300000. 1.352 + */ 1.353 + get localtimeOffsetMsec() { 1.354 + return this.fxAccountsClient.localtimeOffsetMsec; 1.355 + }, 1.356 + 1.357 + /** 1.358 + * Ask the server whether the user's email has been verified 1.359 + */ 1.360 + checkEmailStatus: function checkEmailStatus(sessionToken) { 1.361 + return this.fxAccountsClient.recoveryEmailStatus(sessionToken); 1.362 + }, 1.363 + 1.364 + /** 1.365 + * Once the user's email is verified, we can request the keys 1.366 + */ 1.367 + fetchKeys: function fetchKeys(keyFetchToken) { 1.368 + log.debug("fetchKeys: " + !!keyFetchToken); 1.369 + if (logPII) { 1.370 + log.debug("fetchKeys - the token is " + keyFetchToken); 1.371 + } 1.372 + return this.fxAccountsClient.accountKeys(keyFetchToken); 1.373 + }, 1.374 + 1.375 + // set() makes sure that polling is happening, if necessary. 1.376 + // get() does not wait for verification, and returns an object even if 1.377 + // unverified. The caller of get() must check .verified . 1.378 + // The "fxaccounts:onverified" event will fire only when the verified 1.379 + // state goes from false to true, so callers must register their observer 1.380 + // and then call get(). In particular, it will not fire when the account 1.381 + // was found to be verified in a previous boot: if our stored state says 1.382 + // the account is verified, the event will never fire. So callers must do: 1.383 + // register notification observer (go) 1.384 + // userdata = get() 1.385 + // if (userdata.verified()) {go()} 1.386 + 1.387 + /** 1.388 + * Get the user currently signed in to Firefox Accounts. 1.389 + * 1.390 + * @return Promise 1.391 + * The promise resolves to the credentials object of the signed-in user: 1.392 + * { 1.393 + * email: The user's email address 1.394 + * uid: The user's unique id 1.395 + * sessionToken: Session for the FxA server 1.396 + * kA: An encryption key from the FxA server 1.397 + * kB: An encryption key derived from the user's FxA password 1.398 + * verified: email verification status 1.399 + * authAt: The time (seconds since epoch) that this record was 1.400 + * authenticated 1.401 + * } 1.402 + * or null if no user is signed in. 1.403 + */ 1.404 + getSignedInUser: function getSignedInUser() { 1.405 + let currentState = this.currentAccountState; 1.406 + return currentState.getUserAccountData().then(data => { 1.407 + if (!data) { 1.408 + return null; 1.409 + } 1.410 + if (!this.isUserEmailVerified(data)) { 1.411 + // If the email is not verified, start polling for verification, 1.412 + // but return null right away. We don't want to return a promise 1.413 + // that might not be fulfilled for a long time. 1.414 + this.startVerifiedCheck(data); 1.415 + } 1.416 + return data; 1.417 + }).then(result => currentState.resolve(result)); 1.418 + }, 1.419 + 1.420 + /** 1.421 + * Set the current user signed in to Firefox Accounts. 1.422 + * 1.423 + * @param credentials 1.424 + * The credentials object obtained by logging in or creating 1.425 + * an account on the FxA server: 1.426 + * { 1.427 + * authAt: The time (seconds since epoch) that this record was 1.428 + * authenticated 1.429 + * email: The users email address 1.430 + * keyFetchToken: a keyFetchToken which has not yet been used 1.431 + * sessionToken: Session for the FxA server 1.432 + * uid: The user's unique id 1.433 + * unwrapBKey: used to unwrap kB, derived locally from the 1.434 + * password (not revealed to the FxA server) 1.435 + * verified: true/false 1.436 + * } 1.437 + * @return Promise 1.438 + * The promise resolves to null when the data is saved 1.439 + * successfully and is rejected on error. 1.440 + */ 1.441 + setSignedInUser: function setSignedInUser(credentials) { 1.442 + log.debug("setSignedInUser - aborting any existing flows"); 1.443 + this.abortExistingFlow(); 1.444 + 1.445 + let record = {version: this.version, accountData: credentials}; 1.446 + let currentState = this.currentAccountState; 1.447 + // Cache a clone of the credentials object. 1.448 + currentState.signedInUser = JSON.parse(JSON.stringify(record)); 1.449 + 1.450 + // This promise waits for storage, but not for verification. 1.451 + // We're telling the caller that this is durable now. 1.452 + return this.signedInUserStorage.set(record).then(() => { 1.453 + this.notifyObservers(ONLOGIN_NOTIFICATION); 1.454 + if (!this.isUserEmailVerified(credentials)) { 1.455 + this.startVerifiedCheck(credentials); 1.456 + } 1.457 + }).then(result => currentState.resolve(result)); 1.458 + }, 1.459 + 1.460 + /** 1.461 + * returns a promise that fires with the assertion. If there is no verified 1.462 + * signed-in user, fires with null. 1.463 + */ 1.464 + getAssertion: function getAssertion(audience) { 1.465 + log.debug("enter getAssertion()"); 1.466 + let currentState = this.currentAccountState; 1.467 + let mustBeValidUntil = this.now() + ASSERTION_USE_PERIOD; 1.468 + return currentState.getUserAccountData().then(data => { 1.469 + if (!data) { 1.470 + // No signed-in user 1.471 + return null; 1.472 + } 1.473 + if (!this.isUserEmailVerified(data)) { 1.474 + // Signed-in user has not verified email 1.475 + return null; 1.476 + } 1.477 + return currentState.getKeyPair(mustBeValidUntil).then(keyPair => { 1.478 + return currentState.getCertificate(data, keyPair, mustBeValidUntil) 1.479 + .then(cert => { 1.480 + return this.getAssertionFromCert(data, keyPair, cert, audience); 1.481 + }); 1.482 + }); 1.483 + }).then(result => currentState.resolve(result)); 1.484 + }, 1.485 + 1.486 + /** 1.487 + * Resend the verification email fot the currently signed-in user. 1.488 + * 1.489 + */ 1.490 + resendVerificationEmail: function resendVerificationEmail() { 1.491 + let currentState = this.currentAccountState; 1.492 + return this.getSignedInUser().then(data => { 1.493 + // If the caller is asking for verification to be re-sent, and there is 1.494 + // no signed-in user to begin with, this is probably best regarded as an 1.495 + // error. 1.496 + if (data) { 1.497 + this.pollEmailStatus(currentState, data.sessionToken, "start"); 1.498 + return this.fxAccountsClient.resendVerificationEmail(data.sessionToken); 1.499 + } 1.500 + throw new Error("Cannot resend verification email; no signed-in user"); 1.501 + }); 1.502 + }, 1.503 + 1.504 + /* 1.505 + * Reset state such that any previous flow is canceled. 1.506 + */ 1.507 + abortExistingFlow: function abortExistingFlow() { 1.508 + if (this.currentTimer) { 1.509 + log.debug("Polling aborted; Another user signing in"); 1.510 + clearTimeout(this.currentTimer); 1.511 + this.currentTimer = 0; 1.512 + } 1.513 + this.currentAccountState.abort(); 1.514 + this.currentAccountState = new AccountState(this); 1.515 + }, 1.516 + 1.517 + signOut: function signOut(localOnly) { 1.518 + let currentState = this.currentAccountState; 1.519 + let sessionToken; 1.520 + return currentState.getUserAccountData().then(data => { 1.521 + // Save the session token for use in the call to signOut below. 1.522 + sessionToken = data && data.sessionToken; 1.523 + return this._signOutLocal(); 1.524 + }).then(() => { 1.525 + // FxAccountsManager calls here, then does its own call 1.526 + // to FxAccountsClient.signOut(). 1.527 + if (!localOnly) { 1.528 + // Wrap this in a promise so *any* errors in signOut won't 1.529 + // block the local sign out. This is *not* returned. 1.530 + Promise.resolve().then(() => { 1.531 + // This can happen in the background and shouldn't block 1.532 + // the user from signing out. The server must tolerate 1.533 + // clients just disappearing, so this call should be best effort. 1.534 + return this._signOutServer(sessionToken); 1.535 + }).then(null, err => { 1.536 + log.error("Error during remote sign out of Firefox Accounts: " + err); 1.537 + }); 1.538 + } 1.539 + }).then(() => { 1.540 + this.notifyObservers(ONLOGOUT_NOTIFICATION); 1.541 + }); 1.542 + }, 1.543 + 1.544 + /** 1.545 + * This function should be called in conjunction with a server-side 1.546 + * signOut via FxAccountsClient. 1.547 + */ 1.548 + _signOutLocal: function signOutLocal() { 1.549 + this.abortExistingFlow(); 1.550 + this.currentAccountState.signedInUser = null; // clear in-memory cache 1.551 + return this.signedInUserStorage.set(null); 1.552 + }, 1.553 + 1.554 + _signOutServer: function signOutServer(sessionToken) { 1.555 + return this.fxAccountsClient.signOut(sessionToken); 1.556 + }, 1.557 + 1.558 + /** 1.559 + * Fetch encryption keys for the signed-in-user from the FxA API server. 1.560 + * 1.561 + * Not for user consumption. Exists to cause the keys to be fetch. 1.562 + * 1.563 + * Returns user data so that it can be chained with other methods. 1.564 + * 1.565 + * @return Promise 1.566 + * The promise resolves to the credentials object of the signed-in user: 1.567 + * { 1.568 + * email: The user's email address 1.569 + * uid: The user's unique id 1.570 + * sessionToken: Session for the FxA server 1.571 + * kA: An encryption key from the FxA server 1.572 + * kB: An encryption key derived from the user's FxA password 1.573 + * verified: email verification status 1.574 + * } 1.575 + * or null if no user is signed in 1.576 + */ 1.577 + getKeys: function() { 1.578 + let currentState = this.currentAccountState; 1.579 + return currentState.getUserAccountData().then((userData) => { 1.580 + if (!userData) { 1.581 + throw new Error("Can't get keys; User is not signed in"); 1.582 + } 1.583 + if (userData.kA && userData.kB) { 1.584 + return userData; 1.585 + } 1.586 + if (!currentState.whenKeysReadyDeferred) { 1.587 + currentState.whenKeysReadyDeferred = Promise.defer(); 1.588 + if (userData.keyFetchToken) { 1.589 + this.fetchAndUnwrapKeys(userData.keyFetchToken).then( 1.590 + (dataWithKeys) => { 1.591 + if (!dataWithKeys.kA || !dataWithKeys.kB) { 1.592 + currentState.whenKeysReadyDeferred.reject( 1.593 + new Error("user data missing kA or kB") 1.594 + ); 1.595 + return; 1.596 + } 1.597 + currentState.whenKeysReadyDeferred.resolve(dataWithKeys); 1.598 + }, 1.599 + (err) => { 1.600 + currentState.whenKeysReadyDeferred.reject(err); 1.601 + } 1.602 + ); 1.603 + } else { 1.604 + currentState.whenKeysReadyDeferred.reject('No keyFetchToken'); 1.605 + } 1.606 + } 1.607 + return currentState.whenKeysReadyDeferred.promise; 1.608 + }).then(result => currentState.resolve(result)); 1.609 + }, 1.610 + 1.611 + fetchAndUnwrapKeys: function(keyFetchToken) { 1.612 + if (logPII) { 1.613 + log.debug("fetchAndUnwrapKeys: token: " + keyFetchToken); 1.614 + } 1.615 + let currentState = this.currentAccountState; 1.616 + return Task.spawn(function* task() { 1.617 + // Sign out if we don't have a key fetch token. 1.618 + if (!keyFetchToken) { 1.619 + log.warn("improper fetchAndUnwrapKeys() call: token missing"); 1.620 + yield this.signOut(); 1.621 + return null; 1.622 + } 1.623 + 1.624 + let {kA, wrapKB} = yield this.fetchKeys(keyFetchToken); 1.625 + 1.626 + let data = yield currentState.getUserAccountData(); 1.627 + 1.628 + // Sanity check that the user hasn't changed out from under us 1.629 + if (data.keyFetchToken !== keyFetchToken) { 1.630 + throw new Error("Signed in user changed while fetching keys!"); 1.631 + } 1.632 + 1.633 + // Next statements must be synchronous until we setUserAccountData 1.634 + // so that we don't risk getting into a weird state. 1.635 + let kB_hex = CryptoUtils.xor(CommonUtils.hexToBytes(data.unwrapBKey), 1.636 + wrapKB); 1.637 + 1.638 + if (logPII) { 1.639 + log.debug("kB_hex: " + kB_hex); 1.640 + } 1.641 + data.kA = CommonUtils.bytesAsHex(kA); 1.642 + data.kB = CommonUtils.bytesAsHex(kB_hex); 1.643 + 1.644 + delete data.keyFetchToken; 1.645 + 1.646 + log.debug("Keys Obtained: kA=" + !!data.kA + ", kB=" + !!data.kB); 1.647 + if (logPII) { 1.648 + log.debug("Keys Obtained: kA=" + data.kA + ", kB=" + data.kB); 1.649 + } 1.650 + 1.651 + yield currentState.setUserAccountData(data); 1.652 + // We are now ready for business. This should only be invoked once 1.653 + // per setSignedInUser(), regardless of whether we've rebooted since 1.654 + // setSignedInUser() was called. 1.655 + this.notifyObservers(ONVERIFIED_NOTIFICATION); 1.656 + return data; 1.657 + }.bind(this)).then(result => currentState.resolve(result)); 1.658 + }, 1.659 + 1.660 + getAssertionFromCert: function(data, keyPair, cert, audience) { 1.661 + log.debug("getAssertionFromCert"); 1.662 + let payload = {}; 1.663 + let d = Promise.defer(); 1.664 + let options = { 1.665 + duration: ASSERTION_LIFETIME, 1.666 + localtimeOffsetMsec: this.localtimeOffsetMsec, 1.667 + now: this.now() 1.668 + }; 1.669 + let currentState = this.currentAccountState; 1.670 + // "audience" should look like "http://123done.org". 1.671 + // The generated assertion will expire in two minutes. 1.672 + jwcrypto.generateAssertion(cert, keyPair, audience, options, (err, signed) => { 1.673 + if (err) { 1.674 + log.error("getAssertionFromCert: " + err); 1.675 + d.reject(err); 1.676 + } else { 1.677 + log.debug("getAssertionFromCert returning signed: " + !!signed); 1.678 + if (logPII) { 1.679 + log.debug("getAssertionFromCert returning signed: " + signed); 1.680 + } 1.681 + d.resolve(signed); 1.682 + } 1.683 + }); 1.684 + return d.promise.then(result => currentState.resolve(result)); 1.685 + }, 1.686 + 1.687 + getCertificateSigned: function(sessionToken, serializedPublicKey, lifetime) { 1.688 + log.debug("getCertificateSigned: " + !!sessionToken + " " + !!serializedPublicKey); 1.689 + if (logPII) { 1.690 + log.debug("getCertificateSigned: " + sessionToken + " " + serializedPublicKey); 1.691 + } 1.692 + return this.fxAccountsClient.signCertificate( 1.693 + sessionToken, 1.694 + JSON.parse(serializedPublicKey), 1.695 + lifetime 1.696 + ); 1.697 + }, 1.698 + 1.699 + getUserAccountData: function() { 1.700 + return this.currentAccountState.getUserAccountData(); 1.701 + }, 1.702 + 1.703 + isUserEmailVerified: function isUserEmailVerified(data) { 1.704 + return !!(data && data.verified); 1.705 + }, 1.706 + 1.707 + /** 1.708 + * Setup for and if necessary do email verification polling. 1.709 + */ 1.710 + loadAndPoll: function() { 1.711 + let currentState = this.currentAccountState; 1.712 + return currentState.getUserAccountData() 1.713 + .then(data => { 1.714 + if (data && !this.isUserEmailVerified(data)) { 1.715 + this.pollEmailStatus(currentState, data.sessionToken, "start"); 1.716 + } 1.717 + return data; 1.718 + }); 1.719 + }, 1.720 + 1.721 + startVerifiedCheck: function(data) { 1.722 + log.debug("startVerifiedCheck " + JSON.stringify(data)); 1.723 + // Get us to the verified state, then get the keys. This returns a promise 1.724 + // that will fire when we are completely ready. 1.725 + // 1.726 + // Login is truly complete once keys have been fetched, so once getKeys() 1.727 + // obtains and stores kA and kB, it will fire the onverified observer 1.728 + // notification. 1.729 + return this.whenVerified(data) 1.730 + .then(() => this.getKeys()); 1.731 + }, 1.732 + 1.733 + whenVerified: function(data) { 1.734 + let currentState = this.currentAccountState; 1.735 + if (data.verified) { 1.736 + log.debug("already verified"); 1.737 + return currentState.resolve(data); 1.738 + } 1.739 + if (!currentState.whenVerifiedDeferred) { 1.740 + log.debug("whenVerified promise starts polling for verified email"); 1.741 + this.pollEmailStatus(currentState, data.sessionToken, "start"); 1.742 + } 1.743 + return currentState.whenVerifiedDeferred.promise.then( 1.744 + result => currentState.resolve(result) 1.745 + ); 1.746 + }, 1.747 + 1.748 + notifyObservers: function(topic) { 1.749 + log.debug("Notifying observers of " + topic); 1.750 + Services.obs.notifyObservers(null, topic, null); 1.751 + }, 1.752 + 1.753 + // XXX - pollEmailStatus should maybe be on the AccountState object? 1.754 + pollEmailStatus: function pollEmailStatus(currentState, sessionToken, why) { 1.755 + log.debug("entering pollEmailStatus: " + why); 1.756 + if (why == "start") { 1.757 + // If we were already polling, stop and start again. This could happen 1.758 + // if the user requested the verification email to be resent while we 1.759 + // were already polling for receipt of an earlier email. 1.760 + this.pollTimeRemaining = this.POLL_SESSION; 1.761 + if (!currentState.whenVerifiedDeferred) { 1.762 + currentState.whenVerifiedDeferred = Promise.defer(); 1.763 + // This deferred might not end up with any handlers (eg, if sync 1.764 + // is yet to start up.) This might cause "A promise chain failed to 1.765 + // handle a rejection" messages, so add an error handler directly 1.766 + // on the promise to log the error. 1.767 + currentState.whenVerifiedDeferred.promise.then(null, err => { 1.768 + log.info("the wait for user verification was stopped: " + err); 1.769 + }); 1.770 + } 1.771 + } 1.772 + 1.773 + this.checkEmailStatus(sessionToken) 1.774 + .then((response) => { 1.775 + log.debug("checkEmailStatus -> " + JSON.stringify(response)); 1.776 + if (response && response.verified) { 1.777 + // Bug 947056 - Server should be able to tell FxAccounts.jsm to back 1.778 + // off or stop polling altogether 1.779 + currentState.getUserAccountData() 1.780 + .then((data) => { 1.781 + data.verified = true; 1.782 + return currentState.setUserAccountData(data); 1.783 + }) 1.784 + .then((data) => { 1.785 + // Now that the user is verified, we can proceed to fetch keys 1.786 + if (currentState.whenVerifiedDeferred) { 1.787 + currentState.whenVerifiedDeferred.resolve(data); 1.788 + delete currentState.whenVerifiedDeferred; 1.789 + } 1.790 + }); 1.791 + } else { 1.792 + log.debug("polling with step = " + this.POLL_STEP); 1.793 + this.pollTimeRemaining -= this.POLL_STEP; 1.794 + log.debug("time remaining: " + this.pollTimeRemaining); 1.795 + if (this.pollTimeRemaining > 0) { 1.796 + this.currentTimer = setTimeout(() => { 1.797 + this.pollEmailStatus(currentState, sessionToken, "timer")}, this.POLL_STEP); 1.798 + log.debug("started timer " + this.currentTimer); 1.799 + } else { 1.800 + if (currentState.whenVerifiedDeferred) { 1.801 + currentState.whenVerifiedDeferred.reject( 1.802 + new Error("User email verification timed out.") 1.803 + ); 1.804 + delete currentState.whenVerifiedDeferred; 1.805 + } 1.806 + } 1.807 + } 1.808 + }); 1.809 + }, 1.810 + 1.811 + // Return the URI of the remote UI flows. 1.812 + getAccountsSignUpURI: function() { 1.813 + let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.signup.uri"); 1.814 + if (!/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting 1.815 + throw new Error("Firefox Accounts server must use HTTPS"); 1.816 + } 1.817 + return url; 1.818 + }, 1.819 + 1.820 + // Return the URI of the remote UI flows. 1.821 + getAccountsSignInURI: function() { 1.822 + let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.signin.uri"); 1.823 + if (!/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting 1.824 + throw new Error("Firefox Accounts server must use HTTPS"); 1.825 + } 1.826 + return url; 1.827 + }, 1.828 + 1.829 + // Returns a promise that resolves with the URL to use to force a re-signin 1.830 + // of the current account. 1.831 + promiseAccountsForceSigninURI: function() { 1.832 + let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.force_auth.uri"); 1.833 + if (!/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting 1.834 + throw new Error("Firefox Accounts server must use HTTPS"); 1.835 + } 1.836 + let currentState = this.currentAccountState; 1.837 + // but we need to append the email address onto a query string. 1.838 + return this.getSignedInUser().then(accountData => { 1.839 + if (!accountData) { 1.840 + return null; 1.841 + } 1.842 + let newQueryPortion = url.indexOf("?") == -1 ? "?" : "&"; 1.843 + newQueryPortion += "email=" + encodeURIComponent(accountData.email); 1.844 + return url + newQueryPortion; 1.845 + }).then(result => currentState.resolve(result)); 1.846 + } 1.847 +}; 1.848 + 1.849 +/** 1.850 + * JSONStorage constructor that creates instances that may set/get 1.851 + * to a specified file, in a directory that will be created if it 1.852 + * doesn't exist. 1.853 + * 1.854 + * @param options { 1.855 + * filename: of the file to write to 1.856 + * baseDir: directory where the file resides 1.857 + * } 1.858 + * @return instance 1.859 + */ 1.860 +function JSONStorage(options) { 1.861 + this.baseDir = options.baseDir; 1.862 + this.path = OS.Path.join(options.baseDir, options.filename); 1.863 +}; 1.864 + 1.865 +JSONStorage.prototype = { 1.866 + set: function(contents) { 1.867 + return OS.File.makeDir(this.baseDir, {ignoreExisting: true}) 1.868 + .then(CommonUtils.writeJSON.bind(null, contents, this.path)); 1.869 + }, 1.870 + 1.871 + get: function() { 1.872 + return CommonUtils.readJSON(this.path); 1.873 + } 1.874 +}; 1.875 + 1.876 +// A getter for the instance to export 1.877 +XPCOMUtils.defineLazyGetter(this, "fxAccounts", function() { 1.878 + let a = new FxAccounts(); 1.879 + 1.880 + // XXX Bug 947061 - We need a strategy for resuming email verification after 1.881 + // browser restart 1.882 + a.loadAndPoll(); 1.883 + 1.884 + return a; 1.885 +});