Remove Firefox Accounts service and tie-ins.

See previous commit for removal of browser identity module.
This commit is contained in:
wolfbeast 2019-04-19 02:02:56 +02:00 committed by Roy Tam
commit c64dc935f9
420 changed files with 9 additions and 51958 deletions

View file

@ -901,18 +901,6 @@ pref("dom.push.maxRecentMessageIDsPerSubscription", 0);
pref("dom.push.enabled", false);
#endif
// The remote content URL where FxAccountsWebChannel messages originate. Must use HTTPS.
pref("identity.fxaccounts.remote.webchannel.uri", "https://accounts.firefox.com");
// The remote URL of the Firefox Account profile server.
pref("identity.fxaccounts.remote.profile.uri", "https://profile.accounts.firefox.com/v1");
// The remote URL of the Firefox Account oauth server.
pref("identity.fxaccounts.remote.oauth.uri", "https://oauth.accounts.firefox.com/v1");
// Token server used by Firefox Account-authenticated Sync.
pref("identity.sync.tokenserver.uri", "https://token.services.mozilla.com/1.0/sync/1.5");
// Enable Presentation API
pref("dom.presentation.enabled", false);
pref("dom.presentation.discovery.enabled", true);

View file

@ -780,21 +780,6 @@ sync_java_files = [TOPSRCDIR + '/mobile/android/services/src/main/java/org/mozil
'background/common/telemetry/TelemetryWrapper.java',
'background/db/CursorDumper.java',
'background/db/Tab.java',
'background/fxa/FxAccount20CreateDelegate.java',
'background/fxa/FxAccount20LoginDelegate.java',
'background/fxa/FxAccountClient.java',
'background/fxa/FxAccountClient20.java',
'background/fxa/FxAccountClientException.java',
'background/fxa/FxAccountRemoteError.java',
'background/fxa/FxAccountUtils.java',
'background/fxa/oauth/FxAccountAbstractClient.java',
'background/fxa/oauth/FxAccountAbstractClientException.java',
'background/fxa/oauth/FxAccountOAuthClient10.java',
'background/fxa/oauth/FxAccountOAuthRemoteError.java',
'background/fxa/PasswordStretcher.java',
'background/fxa/profile/FxAccountProfileClient10.java',
'background/fxa/QuickPasswordStretcher.java',
'background/fxa/SkewHandler.java',
'background/nativecode/NativeCrypto.java',
'background/preferences/PreferenceFragment.java',
'background/preferences/PreferenceManagerCompat.java',
@ -813,52 +798,6 @@ sync_java_files = [TOPSRCDIR + '/mobile/android/services/src/main/java/org/mozil
'browserid/verifier/BrowserIDVerifierDelegate.java',
'browserid/verifier/BrowserIDVerifierException.java',
'browserid/VerifyingPublicKey.java',
'fxa/AccountLoader.java',
'fxa/activities/CustomColorPreference.java',
'fxa/activities/FxAccountAbstractActivity.java',
'fxa/activities/FxAccountConfirmAccountActivityWeb.java',
'fxa/activities/FxAccountFinishMigratingActivityWeb.java',
'fxa/activities/FxAccountGetStartedActivityWeb.java',
'fxa/activities/FxAccountStatusActivity.java',
'fxa/activities/FxAccountStatusFragment.java',
'fxa/activities/FxAccountUpdateCredentialsActivityWeb.java',
'fxa/activities/FxAccountWebFlowActivity.java',
'fxa/activities/PicassoPreferenceIconTarget.java',
'fxa/authenticator/AccountPickler.java',
'fxa/authenticator/AndroidFxAccount.java',
'fxa/authenticator/FxAccountAuthenticator.java',
'fxa/authenticator/FxAccountAuthenticatorService.java',
'fxa/authenticator/FxAccountLoginDelegate.java',
'fxa/authenticator/FxAccountLoginException.java',
'fxa/authenticator/FxADefaultLoginStateMachineDelegate.java',
'fxa/FirefoxAccounts.java',
'fxa/FxAccountConstants.java',
'fxa/FxAccountDevice.java',
'fxa/FxAccountDeviceRegistrator.java',
'fxa/FxAccountPushHandler.java',
'fxa/login/BaseRequestDelegate.java',
'fxa/login/Cohabiting.java',
'fxa/login/Doghouse.java',
'fxa/login/Engaged.java',
'fxa/login/FxAccountLoginStateMachine.java',
'fxa/login/FxAccountLoginTransition.java',
'fxa/login/Married.java',
'fxa/login/MigratedFromSync11.java',
'fxa/login/Separated.java',
'fxa/login/State.java',
'fxa/login/StateFactory.java',
'fxa/login/TokensAndKeysState.java',
'fxa/receivers/FxAccountDeletedService.java',
'fxa/receivers/FxAccountUpgradeReceiver.java',
'fxa/sync/FxAccountNotificationManager.java',
'fxa/sync/FxAccountProfileService.java',
'fxa/sync/FxAccountSchedulePolicy.java',
'fxa/sync/FxAccountSyncAdapter.java',
'fxa/sync/FxAccountSyncDelegate.java',
'fxa/sync/FxAccountSyncService.java',
'fxa/sync/FxAccountSyncStatusHelper.java',
'fxa/sync/SchedulePolicy.java',
'fxa/SyncStatusListener.java',
'push/autopush/AutopushClient.java',
'push/autopush/AutopushClientException.java',
'push/RegisterUserAgentResponse.java',

View file

@ -22,7 +22,6 @@ import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.annotation.ReflectionTarget;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.fxa.FxAccountPushHandler;
import org.mozilla.gecko.gcm.GcmTokenClient;
import org.mozilla.gecko.push.autopush.AutopushClientException;
import org.mozilla.gecko.util.BundleEventListener;
@ -66,13 +65,10 @@ public class PushService implements BundleEventListener {
"PushServiceAndroidGCM:UnregisterUserAgent",
"PushServiceAndroidGCM:SubscribeChannel",
"PushServiceAndroidGCM:UnsubscribeChannel",
"FxAccountsPush:Initialized",
"FxAccountsPush:ReceivedPushMessageToDecode:Response",
"History:GetPrePathLastVisitedTimeMilliseconds",
};
private enum GeckoComponent {
FxAccountsPush,
PushServiceAndroidGCM
}
@ -98,7 +94,6 @@ public class PushService implements BundleEventListener {
// NB: These are not thread-safe, we're depending on these being access from the same background thread.
private boolean isReadyPushServiceAndroidGCM = false;
private boolean isReadyFxAccountsPush = false;
private final List<JSONObject> pendingPushMessages;
public PushService(Context context) {
@ -238,9 +233,6 @@ public class PushService implements BundleEventListener {
protected static void sendMessageToDecodeToGeckoService(final @NonNull JSONObject message) {
Log.i(LOG_TAG, "Delivering dom/push message to decode to Gecko!");
GeckoAppShell.notifyObservers("FxAccountsPush:ReceivedPushMessageToDecode",
message.toString(),
GeckoThread.State.PROFILE_READY);
}
protected void registerGeckoEventListener() {
@ -279,11 +271,6 @@ public class PushService implements BundleEventListener {
callback.sendSuccess(null);
return;
}
if ("FxAccountsPush:Initialized".equals(event)) {
processComponentState(GeckoComponent.FxAccountsPush, true);
callback.sendSuccess(null);
return;
}
if ("PushServiceAndroidGCM:Configure".equals(event)) {
final String endpoint = message.getString("endpoint");
if (endpoint == null) {
@ -390,10 +377,6 @@ public class PushService implements BundleEventListener {
callback.sendError("Could not unsubscribe from channel: " + channelID);
return;
}
if ("FxAccountsPush:ReceivedPushMessageToDecode:Response".equals(event)) {
FxAccountPushHandler.handleFxAPushMessage(context, message);
return;
}
if ("History:GetPrePathLastVisitedTimeMilliseconds".equals(event)) {
if (callback == null) {
Log.e(LOG_TAG, "callback must not be null in " + event);
@ -420,10 +403,7 @@ public class PushService implements BundleEventListener {
}
private void processComponentState(@NonNull GeckoComponent component, boolean isReady) {
if (component == GeckoComponent.FxAccountsPush) {
isReadyFxAccountsPush = isReady;
} else if (component == GeckoComponent.PushServiceAndroidGCM) {
if (component == GeckoComponent.PushServiceAndroidGCM) {
isReadyPushServiceAndroidGCM = isReady;
}
@ -435,7 +415,7 @@ public class PushService implements BundleEventListener {
}
private boolean canSendPushMessagesToGecko() {
return isReadyFxAccountsPush && isReadyPushServiceAndroidGCM;
return isReadyPushServiceAndroidGCM;
}
private static void sendPushMessagesToGecko(@NonNull List<JSONObject> messages) {

View file

@ -21,11 +21,6 @@
<item name="android:colorAccent">@color/fennec_ui_orange</item>
</style>
<style name="ActionBar.FxAccountStatusActivity" parent="@android:style/Widget.Material.ActionBar.Solid">
<item name="android:displayOptions">homeAsUp|showTitle</item>
<item name="android:titleTextStyle">@style/ActionBarTitleTextStyle</item>
</style>
<style name="GeckoAppBase" parent="Gecko">
<item name="android:actionButtonStyle">@style/GeckoActionBar.Button</item>
<item name="android:listViewStyle">@style/Widget.ListView</item>

View file

@ -1,351 +0,0 @@
// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
/* 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/. */
/**
* Wrap a remote fxa-content-server.
*
* An about:accounts tab loads and displays an fxa-content-server page,
* depending on the current Android Account status and an optional 'action'
* parameter.
*
* We show a spinner while the remote iframe is loading. We expect the
* WebChannel message listening to the fxa-content-server to send this tab's
* <browser>'s messageManager a LOADED message when the remote iframe provides
* the WebChannel LOADED message. See the messageManager registration and the
* |loadedDeferred| promise. This loosely couples the WebChannel implementation
* and about:accounts! (We need this coupling in order to distinguish
* WebChannel LOADED messages produced by multiple about:accounts tabs.)
*
* We capture error conditions by accessing the inner nsIWebNavigation of the
* iframe directly.
*/
"use strict";
var {classes: Cc, interfaces: Ci, utils: Cu} = Components; /*global Components */
Cu.import("resource://gre/modules/Accounts.jsm"); /*global Accounts */
Cu.import("resource://gre/modules/PromiseUtils.jsm"); /*global PromiseUtils */
Cu.import("resource://gre/modules/Services.jsm"); /*global Services */
Cu.import("resource://gre/modules/XPCOMUtils.jsm"); /*global XPCOMUtils */
const ACTION_URL_PARAM = "action";
const COMMAND_LOADED = "fxaccounts:loaded";
const log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.bind("FxAccounts");
XPCOMUtils.defineLazyServiceGetter(this, "ParentalControls",
"@mozilla.org/parental-controls-service;1", "nsIParentalControlsService");
// Shows the toplevel element with |id| to be shown - all other top-level
// elements are hidden.
// If |id| is 'spinner', then 'remote' is also shown, with opacity 0.
function show(id) {
let allTop = document.querySelectorAll(".toplevel");
for (let elt of allTop) {
if (elt.getAttribute("id") == id) {
elt.style.display = 'block';
} else {
elt.style.display = 'none';
}
}
if (id == 'spinner') {
document.getElementById('remote').style.display = 'block';
document.getElementById('remote').style.opacity = 0;
}
}
// Each time we try to load the remote <iframe>, loadedDeferred is replaced. It
// is resolved by a LOADED message, and rejected by a failure to load.
var loadedDeferred = null;
// We have a new load starting. Replace the existing promise with a new one,
// and queue up the transition to remote content.
function deferTransitionToRemoteAfterLoaded() {
log.d('Waiting for LOADED message.');
loadedDeferred = PromiseUtils.defer();
loadedDeferred.promise.then(() => {
log.d('Got LOADED message!');
document.getElementById("remote").style.opacity = 0;
show("remote");
document.getElementById("remote").style.opacity = 1;
})
.catch((e) => {
log.w('Did not get LOADED message: ' + e.toString());
});
}
function handleLoadedMessage(message) {
loadedDeferred.resolve();
};
var wrapper = {
iframe: null,
url: null,
init: function (url) {
this.url = url;
deferTransitionToRemoteAfterLoaded();
let iframe = document.getElementById("remote");
this.iframe = iframe;
this.iframe.QueryInterface(Ci.nsIFrameLoaderOwner);
let docShell = this.iframe.frameLoader.docShell;
docShell.QueryInterface(Ci.nsIWebProgress);
docShell.addProgressListener(this.iframeListener, Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
// Set the iframe's location with loadURI/LOAD_FLAGS_BYPASS_HISTORY to
// avoid having a new history entry being added.
let webNav = iframe.frameLoader.docShell.QueryInterface(Ci.nsIWebNavigation);
webNav.loadURI(url, Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY, null, null, null);
},
retry: function () {
deferTransitionToRemoteAfterLoaded();
let webNav = this.iframe.frameLoader.docShell.QueryInterface(Ci.nsIWebNavigation);
webNav.loadURI(this.url, Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY, null, null, null);
},
iframeListener: {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
Ci.nsISupportsWeakReference,
Ci.nsISupports]),
onStateChange: function(aWebProgress, aRequest, aState, aStatus) {
let failure = false;
// Captive portals sometimes redirect users
if ((aState & Ci.nsIWebProgressListener.STATE_REDIRECTING)) {
failure = true;
} else if ((aState & Ci.nsIWebProgressListener.STATE_STOP)) {
if (aRequest instanceof Ci.nsIHttpChannel) {
try {
failure = aRequest.responseStatus != 200;
} catch (e) {
failure = aStatus != Components.results.NS_OK;
}
}
}
// Calling cancel() will raise some OnStateChange notifications by itself,
// so avoid doing that more than once
if (failure && aStatus != Components.results.NS_BINDING_ABORTED) {
aRequest.cancel(Components.results.NS_BINDING_ABORTED);
// Since after a promise is fulfilled, subsequent fulfillments are
// treated as no-ops, we don't care that we might see multiple failures
// due to multiple listener callbacks. (It's not easy to extract this
// from the Promises spec, but it is widely quoted. Start with
// http://stackoverflow.com/a/18218542.)
loadedDeferred.reject(new Error("Failed in onStateChange!"));
show("networkError");
}
},
onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags) {
if (aRequest && aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
aRequest.cancel(Components.results.NS_BINDING_ABORTED);
// As above, we're not concerned by multiple listener callbacks.
loadedDeferred.reject(new Error("Failed in onLocationChange!"));
show("networkError");
}
},
onProgressChange: function() {},
onStatusChange: function() {},
onSecurityChange: function() {},
},
};
function retry() {
log.i("Retrying.");
show("spinner");
wrapper.retry();
}
function openPrefs() {
log.i("Opening Sync preferences.");
// If an Android Account exists, this will open the Status Activity.
// Otherwise, it will begin the Get Started flow. This should only be shown
// when an Account actually exists.
Accounts.launchSetup();
}
function getURLForAction(action, urlParams) {
let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.webchannel.uri");
url = url + (url.endsWith("/") ? "" : "/") + action;
const CONTEXT = "fx_fennec_v1";
// The only service managed by Fennec, to date, is Firefox Sync.
const SERVICE = "sync";
urlParams = urlParams || new URLSearchParams("");
urlParams.set('service', SERVICE);
urlParams.set('context', CONTEXT);
// Ideally we'd just merge urlParams with new URL(url).searchParams, but our
// URLSearchParams implementation doesn't support iteration (bug 1085284).
let urlParamStr = urlParams.toString();
if (urlParamStr) {
url += (url.includes("?") ? "&" : "?") + urlParamStr;
}
return url;
}
function updateDisplayedEmail(user) {
let emailDiv = document.getElementById("email");
if (emailDiv && user) {
emailDiv.textContent = user.email;
}
}
function init() {
// Test for restrictions before getFirefoxAccount(), since that will fail if
// we are restricted.
if (!ParentalControls.isAllowed(ParentalControls.MODIFY_ACCOUNTS)) {
// It's better to log and show an error message than to invite user
// confusion by removing about:accounts entirely. That is, if the user is
// restricted, this way they'll discover as much and may be able to get
// out of their restricted profile. If we remove about:accounts entirely,
// it will look like Fennec is buggy, and the user will be very confused.
log.e("This profile cannot connect to Firefox Accounts: showing restricted error.");
show("restrictedError");
return;
}
Accounts.getFirefoxAccount().then(user => {
// It's possible for the window to start closing before getting the user
// completes. Tests in particular can cause this.
if (window.closed) {
return;
}
updateDisplayedEmail(user);
// Ideally we'd use new URL(document.URL).searchParams, but for about: URIs,
// searchParams is empty.
let urlParams = new URLSearchParams(document.URL.split("?")[1] || "");
let action = urlParams.get(ACTION_URL_PARAM);
urlParams.delete(ACTION_URL_PARAM);
switch (action) {
case "signup":
if (user) {
// Asking to sign-up when already signed in just shows prefs.
show("prefs");
} else {
show("spinner");
wrapper.init(getURLForAction("signup", urlParams));
}
break;
case "signin":
if (user) {
// Asking to sign-in when already signed in just shows prefs.
show("prefs");
} else {
show("spinner");
wrapper.init(getURLForAction("signin", urlParams));
}
break;
case "force_auth":
if (user) {
show("spinner");
urlParams.set("email", user.email); // In future, pin using the UID.
wrapper.init(getURLForAction("force_auth", urlParams));
} else {
show("spinner");
wrapper.init(getURLForAction("signup", urlParams));
}
break;
case "manage":
if (user) {
show("spinner");
urlParams.set("email", user.email); // In future, pin using the UID.
wrapper.init(getURLForAction("settings", urlParams));
} else {
show("spinner");
wrapper.init(getURLForAction("signup", urlParams));
}
break;
case "avatar":
if (user) {
show("spinner");
urlParams.set("email", user.email); // In future, pin using the UID.
wrapper.init(getURLForAction("settings/avatar/change", urlParams));
} else {
show("spinner");
wrapper.init(getURLForAction("signup", urlParams));
}
break;
default:
// Unrecognized or no action specified.
if (action) {
log.w("Ignoring unrecognized action: " + action);
}
if (user) {
show("prefs");
} else {
show("spinner");
wrapper.init(getURLForAction("signup", urlParams));
}
break;
}
}).catch(e => {
log.e("Failed to get the signed in user: " + e.toString());
});
}
document.addEventListener("DOMContentLoaded", function onload() {
document.removeEventListener("DOMContentLoaded", onload, true);
init();
var buttonRetry = document.getElementById('buttonRetry');
buttonRetry.addEventListener('click', retry);
var buttonOpenPrefs = document.getElementById('buttonOpenPrefs');
buttonOpenPrefs.addEventListener('click', openPrefs);
}, true);
// This window is contained in a XUL <browser> element. Return the
// messageManager of that <browser> element, or null.
function getBrowserMessageManager() {
let browser = window
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShellTreeItem)
.rootTreeItem
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow)
.QueryInterface(Ci.nsIDOMChromeWindow)
.BrowserApp
.getBrowserForDocument(document);
if (browser) {
return browser.messageManager;
}
return null;
}
// Add a single listener for 'loaded' messages from the iframe in this
// <browser>. These 'loaded' messages are ferried from the WebChannel to just
// this <browser>.
var mm = getBrowserMessageManager();
if (mm) {
mm.addMessageListener(COMMAND_LOADED, handleLoadedMessage);
} else {
log.e('No messageManager, not listening for LOADED message!');
}
window.addEventListener("unload", function(event) {
try {
let mm = getBrowserMessageManager();
if (mm) {
mm.removeMessageListener(COMMAND_LOADED, handleLoadedMessage);
}
} catch (e) {
// This could fail if the page is being torn down, the tab is being
// destroyed, etc.
log.w('Not removing listener for LOADED message: ' + e.toString());
}
});

View file

@ -1,83 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- 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/. -->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd" [
<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
%brandDTD;
<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd" >
%globalDTD;
<!ENTITY % aboutDTD SYSTEM "chrome://browser/locale/aboutAccounts.dtd">
%aboutDTD;
]>
<html xmlns="http://www.w3.org/1999/xhtml" dir="&locale.dir;">
<head>
<title>Firefox Sync</title>
<meta name="viewport" content="width=device-width; user-scalable=0" />
<link rel="icon" type="image/png" sizes="64x64" href="chrome://branding/content/favicon64.png" />
<link rel="stylesheet" href="chrome://browser/skin/spinner.css" type="text/css"/>
<link rel="stylesheet" href="chrome://browser/skin/aboutBase.css" type="text/css"/>
<link rel="stylesheet" href="chrome://browser/skin/aboutAccounts.css" type="text/css"/>
</head>
<body>
<div id="spinner" class="toplevel">
<div class="container flex-column">
<!-- Empty text-container for spacing. -->
<div class="text-container flex-column" />
<div class="mui-refresh-main">
<div class="mui-refresh-wrapper">
<div class="mui-spinner-wrapper">
<div class="mui-spinner-main">
<div class="mui-spinner-left">
<div class="mui-half-circle-left" />
</div>
<div class="mui-spinner-right">
<div class="mui-half-circle-right" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<iframe mozframetype="content" id="remote" class="toplevel" />
<div id="prefs" class="toplevel">
<div class="container flex-column">
<div class="text-container flex-column">
<div class="text">&aboutAccounts.connected.title;</div>
<div class="hint">&aboutAccounts.connected.description;</div>
<div id="email" class="hint"></div>
</div>
<a id="buttonOpenPrefs" tabindex="0" href="#">&aboutAccounts.syncPreferences.label;</a>
</div>
</div>
<div id="networkError" class="toplevel">
<div class="container flex-column">
<div class="text-container flex-column">
<div class="text">&aboutAccounts.noConnection.title;</div>
</div>
<div class="button-row">
<button id="buttonRetry" class="button" tabindex="1">&aboutAccounts.retry.label;</button>
</div>
</div>
</div>
<div id="restrictedError" class="toplevel">
<div class="container flex-column">
<div class="text-container flex-column">
<div class="text">&aboutAccounts.restrictedError.title;</div>
<div class="hint">&aboutAccounts.restrictedError.description;</div>
</div>
</div>
</div>
<script type="application/javascript;version=1.8" src="chrome://browser/content/aboutAccounts.js"></script>
</body>
</html>

View file

@ -489,20 +489,6 @@ var BrowserApp = {
let mm = window.getGroupMessageManager("browsers");
mm.loadFrameScript("chrome://browser/content/content.js", true);
// We can't delay registering WebChannel listeners: if the first page is
// about:accounts, which can happen when starting the Firefox Account flow
// from the first run experience, or via the Firefox Account Status
// Activity, we can and do miss messages from the fxa-content-server.
// However, we never allow suitably restricted profiles from listening to
// fxa-content-server messages.
if (ParentalControls.isAllowed(ParentalControls.MODIFY_ACCOUNTS)) {
console.log("browser.js: loading Firefox Accounts WebChannel");
Cu.import("resource://gre/modules/FxAccountsWebChannel.jsm");
EnsureFxAccountsWebChannel();
} else {
console.log("browser.js: not loading Firefox Accounts WebChannel; this profile cannot connect to Firefox Accounts.");
}
// Notify Java that Gecko has loaded.
Messaging.sendRequest({ type: "Gecko:Ready" });

View file

@ -56,8 +56,6 @@ chrome.jar:
content/aboutHealthReport.xhtml (content/aboutHealthReport.xhtml)
content/aboutHealthReport.js (content/aboutHealthReport.js)
#endif
content/aboutAccounts.xhtml (content/aboutAccounts.xhtml)
content/aboutAccounts.js (content/aboutAccounts.js)
content/aboutLogins.xhtml (content/aboutLogins.xhtml)
content/aboutLogins.js (content/aboutLogins.js)
#ifndef RELEASE_OR_BETA

View file

@ -1,164 +0,0 @@
/* jshint moz: true, esnext: true */
/* 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/. */
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Messaging.jsm");
const {
PushCrypto,
getCryptoParams,
} = Cu.import("resource://gre/modules/PushCrypto.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "PushService",
"@mozilla.org/push/Service;1", "nsIPushService");
XPCOMUtils.defineLazyGetter(this, "_decoder", () => new TextDecoder());
const FXA_PUSH_SCOPE = "chrome://fxa-push";
const Log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.bind("FxAccountsPush");
function FxAccountsPush() {
Services.obs.addObserver(this, "FxAccountsPush:ReceivedPushMessageToDecode", false);
Messaging.sendRequestForResult({
type: "FxAccountsPush:Initialized"
});
}
FxAccountsPush.prototype = {
observe: function (subject, topic, data) {
switch (topic) {
case "android-push-service":
if (data === "android-fxa-subscribe") {
this._subscribe();
} else if (data === "android-fxa-unsubscribe") {
this._unsubscribe();
}
break;
case "FxAccountsPush:ReceivedPushMessageToDecode":
this._decodePushMessage(data);
break;
}
},
_subscribe() {
Log.i("FxAccountsPush _subscribe");
return new Promise((resolve, reject) => {
PushService.subscribe(FXA_PUSH_SCOPE,
Services.scriptSecurityManager.getSystemPrincipal(),
(result, subscription) => {
if (Components.isSuccessCode(result)) {
Log.d("FxAccountsPush got subscription");
resolve(subscription);
} else {
Log.w("FxAccountsPush failed to subscribe", result);
reject(new Error("FxAccountsPush failed to subscribe"));
}
});
})
.then(subscription => {
Messaging.sendRequest({
type: "FxAccountsPush:Subscribe:Response",
subscription: {
pushCallback: subscription.endpoint,
pushPublicKey: urlsafeBase64Encode(subscription.getKey('p256dh')),
pushAuthKey: urlsafeBase64Encode(subscription.getKey('auth'))
}
});
})
.catch(err => {
Log.i("Error when registering FxA push endpoint " + err);
});
},
_unsubscribe() {
Log.i("FxAccountsPush _unsubscribe");
return new Promise((resolve) => {
PushService.unsubscribe(FXA_PUSH_SCOPE,
Services.scriptSecurityManager.getSystemPrincipal(),
(result, ok) => {
if (Components.isSuccessCode(result)) {
if (ok === true) {
Log.d("FxAccountsPush unsubscribed");
} else {
Log.d("FxAccountsPush had no subscription to unsubscribe");
}
} else {
Log.w("FxAccountsPush failed to unsubscribe", result);
}
return resolve(ok);
});
}).catch(err => {
Log.e("Error during unsubscribe", err);
});
},
_decodePushMessage(data) {
Log.i("FxAccountsPush _decodePushMessage");
data = JSON.parse(data);
let { headers, message } = this._messageAndHeaders(data);
return new Promise((resolve, reject) => {
PushService.getSubscription(FXA_PUSH_SCOPE,
Services.scriptSecurityManager.getSystemPrincipal(),
(result, subscription) => {
if (!subscription) {
return reject(new Error("No subscription found"));
}
return resolve(subscription);
});
}).then(subscription => {
return PushCrypto.decrypt(subscription.p256dhPrivateKey,
new Uint8Array(subscription.getKey("p256dh")),
new Uint8Array(subscription.getKey("auth")),
headers, message);
})
.then(plaintext => {
let decryptedMessage = plaintext ? _decoder.decode(plaintext) : "";
Messaging.sendRequestForResult({
type: "FxAccountsPush:ReceivedPushMessageToDecode:Response",
message: decryptedMessage
});
})
.catch(err => {
Log.d("Error while decoding incoming message : " + err);
});
},
// Copied from PushServiceAndroidGCM
_messageAndHeaders(data) {
// Default is no data (and no encryption).
let message = null;
let headers = null;
if (data.message && data.enc && (data.enckey || data.cryptokey)) {
headers = {
encryption_key: data.enckey,
crypto_key: data.cryptokey,
encryption: data.enc,
encoding: data.con,
};
// Ciphertext is (urlsafe) Base 64 encoded.
message = ChromeUtils.base64URLDecode(data.message, {
// The Push server may append padding.
padding: "ignore",
});
}
return { headers, message };
},
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
classID: Components.ID("{d1bbb0fd-1d47-4134-9c12-d7b1be20b721}")
};
function urlsafeBase64Encode(key) {
return ChromeUtils.base64URLEncode(new Uint8Array(key), { pad: false });
}
var components = [ FxAccountsPush ];
this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);

View file

@ -100,11 +100,6 @@ contract @mozilla.org/dom/site-specific-user-agent;1 {d5234c9d-0ee2-4b3c-9da3-18
component {18a4e042-7c7c-424b-a583-354e68553a7f} FilePicker.js
contract @mozilla.org/filepicker;1 {18a4e042-7c7c-424b-a583-354e68553a7f}
# FxAccountsPush.js
component {d1bbb0fd-1d47-4134-9c12-d7b1be20b721} FxAccountsPush.js
contract @mozilla.org/fxa-push;1 {d1bbb0fd-1d47-4134-9c12-d7b1be20b721}
category android-push-service FxAccountsPush @mozilla.org/fxa-push;1
#ifndef RELEASE_OR_BETA
# TabSource.js
component {5850c76e-b916-4218-b99a-31f004e0a7e7} TabSource.js

View file

@ -20,7 +20,6 @@ EXTRA_COMPONENTS += [
'ContentPermissionPrompt.js',
'DirectoryProvider.js',
'FilePicker.js',
'FxAccountsPush.js',
'HelperAppDialog.js',
'ImageBlockingPolicy.js',
'LoginManagerPrompter.js',

View file

@ -16,8 +16,6 @@
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.preference.Preference
-keep public class * extends org.mozilla.gecko.fxa.sync.FxAccountSyncAdapter
-keep class org.mozilla.gecko.fxa.sync.FxAccountSyncAdapter
-keep public class * extends android.support.v4.app.Fragment

View file

@ -57,4 +57,4 @@ MOZ_WEBGL_CONFORMANT=1
export JS_GC_SMALL_CHUNK_SIZE=1
# Enable checking that add-ons are signed by the trusted root
MOZ_ADDON_SIGNING=1
MOZ_ADDON_SIGNING=

View file

@ -503,7 +503,6 @@
@BINPATH@/components/ImageBlockingPolicy.js
@BINPATH@/components/DirectoryProvider.js
@BINPATH@/components/FilePicker.js
@BINPATH@/components/FxAccountsPush.js
@BINPATH@/components/HelperAppDialog.js
@BINPATH@/components/LoginManagerPrompter.js
@BINPATH@/components/MobileComponents.manifest

View file

@ -40,14 +40,8 @@ var Accounts = Object.freeze({
}).then(data => data.exists);
},
firefoxAccountsExist: function () {
return this._accountsExist("fxa");
},
syncAccountsExist: function () {
Deprecated.warning("The legacy Sync account type has been removed from Firefox for Android. " +
"Please use `firefoxAccountsExist` instead.",
"https://developer.mozilla.org/en-US/Add-ons/Firefox_for_Android/API/Accounts.jsm");
Deprecated.warning("The legacy Sync account type has been removed from Firefox for Android.");
return Promise.resolve(false);
},
@ -73,106 +67,13 @@ var Accounts = Object.freeze({
},
_addDefaultEndpoints: function (json) {
// Empty without FxA
let newData = Cu.cloneInto(json, {}, { cloneFunctions: false });
let associations = {
authServerEndpoint: 'identity.fxaccounts.auth.uri',
profileServerEndpoint: 'identity.fxaccounts.remote.profile.uri',
tokenServerEndpoint: 'identity.sync.tokenserver.uri'
};
for (let key in associations) {
newData[key] = newData[key] || Services.urlFormatter.formatURLPref(associations[key]);
}
return newData;
},
/**
* Create a new Android Account corresponding to the given
* fxa-content-server "login" JSON datum. The new account will be
* in the "Engaged" state, and will start syncing immediately.
*
* It is an error if an Android Account already exists.
*
* Returns a Promise that resolves to a boolean indicating success.
*/
createFirefoxAccountFromJSON: function (json) {
return Messaging.sendRequestForResult({
type: "Accounts:CreateFirefoxAccountFromJSON",
json: this._addDefaultEndpoints(json)
});
},
/**
* Move an existing Android Account to the "Engaged" state with the given
* fxa-content-server "login" JSON datum. The account will (re)start
* syncing immediately, unless the user has manually configured the account
* to not Sync.
*
* It is an error if no Android Account exists.
*
* Returns a Promise that resolves to a boolean indicating success.
*/
updateFirefoxAccountFromJSON: function (json) {
return Messaging.sendRequestForResult({
type: "Accounts:UpdateFirefoxAccountFromJSON",
json: this._addDefaultEndpoints(json)
});
},
/**
* Notify that profile for Android Account has updated.
* The account will re-fetch the profile image.
*
* It is an error if no Android Account exists.
*
* There is no return value from this method.
*/
notifyFirefoxAccountProfileChanged: function () {
Messaging.sendRequest({
type: "Accounts:ProfileUpdated",
});
},
/**
* Fetch information about an existing Android Firefox Account.
*
* Returns a Promise that resolves to null if no Android Firefox Account
* exists, or an object including at least a string-valued 'email' key.
*/
getFirefoxAccount: function () {
return Messaging.sendRequestForResult({
type: "Accounts:Exist",
kind: "fxa",
}).then(data => {
if (!data || !data.exists) {
return null;
}
delete data.exists;
return data;
});
},
/**
* Delete an existing Android Firefox Account.
*
* It is an error if no Android Account exists.
*
* Returns a Promise that resolves to a boolean indicating success.
*/
deleteFirefoxAccount: function () {
return Messaging.sendRequestForResult({
type: "Accounts:DeleteFirefoxAccount",
});
},
showSyncPreferences: function () {
// Only show Sync preferences of an existing Android Account.
return Accounts.getFirefoxAccount().then(account => {
if (!account) {
throw new Error("Can't show Sync preferences of non-existent Firefox Account!");
}
return Messaging.sendRequestForResult({
type: "Accounts:ShowSyncPreferences"
});
});
throw new Error("Can't show Sync preferences without accounts!");
}
});

View file

@ -1,394 +0,0 @@
// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
/* 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/. */
/**
* Firefox Accounts Web Channel.
*
* Use the WebChannel component to receive messages about account
* state changes.
*/
this.EXPORTED_SYMBOLS = ["EnsureFxAccountsWebChannel"];
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; /*global Components */
Cu.import("resource://gre/modules/Accounts.jsm"); /*global Accounts */
Cu.import("resource://gre/modules/Services.jsm"); /*global Services */
Cu.import("resource://gre/modules/WebChannel.jsm"); /*global WebChannel */
Cu.import("resource://gre/modules/XPCOMUtils.jsm"); /*global XPCOMUtils */
const log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.bind("FxAccounts");
const WEBCHANNEL_ID = "account_updates";
const COMMAND_LOADED = "fxaccounts:loaded";
const COMMAND_CAN_LINK_ACCOUNT = "fxaccounts:can_link_account";
const COMMAND_LOGIN = "fxaccounts:login";
const COMMAND_CHANGE_PASSWORD = "fxaccounts:change_password";
const COMMAND_DELETE_ACCOUNT = "fxaccounts:delete_account";
const COMMAND_PROFILE_CHANGE = "profile:change";
const COMMAND_SYNC_PREFERENCES = "fxaccounts:sync_preferences";
const PREF_LAST_FXA_USER = "identity.fxaccounts.lastSignedInUserHash";
XPCOMUtils.defineLazyGetter(this, "strings",
() => Services.strings.createBundle("chrome://browser/locale/aboutAccounts.properties")); /*global strings */
XPCOMUtils.defineLazyModuleGetter(this, "Snackbars", "resource://gre/modules/Snackbars.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Prompt", "resource://gre/modules/Prompt.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry", "resource://gre/modules/UITelemetry.jsm");
this.FxAccountsWebChannelHelpers = function() {
};
this.FxAccountsWebChannelHelpers.prototype = {
/**
* Get the hash of account name of the previously signed in account.
*/
getPreviousAccountNameHashPref() {
try {
return Services.prefs.getComplexValue(PREF_LAST_FXA_USER, Ci.nsISupportsString).data;
} catch (_) {
return "";
}
},
/**
* Given an account name, set the hash of the previously signed in account.
*
* @param acctName the account name of the user's account.
*/
setPreviousAccountNameHashPref(acctName) {
let string = Cc["@mozilla.org/supports-string;1"]
.createInstance(Ci.nsISupportsString);
string.data = this.sha256(acctName);
Services.prefs.setComplexValue(PREF_LAST_FXA_USER, Ci.nsISupportsString, string);
},
/**
* Given a string, returns the SHA265 hash in base64.
*/
sha256(str) {
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
// Data is an array of bytes.
let data = converter.convertToByteArray(str, {});
let hasher = Cc["@mozilla.org/security/hash;1"]
.createInstance(Ci.nsICryptoHash);
hasher.init(hasher.SHA256);
hasher.update(data, data.length);
return hasher.finish(true);
},
};
/**
* Create a new FxAccountsWebChannel to listen for account updates.
*
* @param {Object} options Options
* @param {Object} options
* @param {String} options.content_uri
* The FxA Content server uri
* @param {String} options.channel_id
* The ID of the WebChannel
* @param {String} options.helpers
* Helpers functions. Should only be passed in for testing.
* @constructor
*/
this.FxAccountsWebChannel = function(options) {
if (!options) {
throw new Error("Missing configuration options");
}
if (!options["content_uri"]) {
throw new Error("Missing 'content_uri' option");
}
this._contentUri = options.content_uri;
if (!options["channel_id"]) {
throw new Error("Missing 'channel_id' option");
}
this._webChannelId = options.channel_id;
// options.helpers is only specified by tests.
this._helpers = options.helpers || new FxAccountsWebChannelHelpers(options);
this._setupChannel();
};
this.FxAccountsWebChannel.prototype = {
/**
* WebChannel that is used to communicate with content page
*/
_channel: null,
/**
* WebChannel ID.
*/
_webChannelId: null,
/**
* WebChannel origin, used to validate origin of messages
*/
_webChannelOrigin: null,
/**
* Release all resources that are in use.
*/
tearDown() {
this._channel.stopListening();
this._channel = null;
this._channelCallback = null;
},
/**
* Configures and registers a new WebChannel
*
* @private
*/
_setupChannel() {
// if this.contentUri is present but not a valid URI, then this will throw an error.
try {
this._webChannelOrigin = Services.io.newURI(this._contentUri, null, null);
this._registerChannel();
} catch (e) {
log.e(e.toString());
throw e;
}
},
/**
* Create a new channel with the WebChannelBroker, setup a callback listener
* @private
*/
_registerChannel() {
/**
* Processes messages that are called back from the FxAccountsChannel
*
* @param webChannelId {String}
* Command webChannelId
* @param message {Object}
* Command message
* @param sendingContext {Object}
* Message sending context.
* @param sendingContext.browser {browser}
* The <browser> object that captured the
* WebChannelMessageToChrome.
* @param sendingContext.eventTarget {EventTarget}
* The <EventTarget> where the message was sent.
* @param sendingContext.principal {Principal}
* The <Principal> of the EventTarget where the message was sent.
* @private
*
*/
let listener = (webChannelId, message, sendingContext) => {
if (message) {
let command = message.command;
let data = message.data;
log.d("FxAccountsWebChannel message received, command: " + command);
// Respond to the message with true or false.
let respond = (data) => {
let response = {
command: command,
messageId: message.messageId,
data: data
};
log.d("Sending response to command: " + command);
this._channel.send(response, sendingContext);
};
switch (command) {
case COMMAND_LOADED:
let mm = sendingContext.browser.docShell
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIContentFrameMessageManager);
mm.sendAsyncMessage(COMMAND_LOADED);
break;
case COMMAND_CAN_LINK_ACCOUNT:
Accounts.getFirefoxAccount().then(account => {
if (account) {
// If we /have/ an Android Account, we never allow the user to
// login to a different account. They need to manually delete
// the first Android Account and then create a new one.
if (account.email == data.email) {
// In future, we should use a UID for this comparison.
log.d("Relinking existing Android Account: email addresses agree.");
respond({ok: true});
} else {
log.w("Not relinking existing Android Account: email addresses disagree!");
let message = strings.GetStringFromName("relinkDenied.message");
let buttonLabel = strings.GetStringFromName("relinkDenied.openPrefs");
Snackbars.show(message, Snackbars.LENGTH_LONG, {
action: {
label: buttonLabel,
callback: () => {
// We have an account, so this opens Sync native preferences.
Accounts.launchSetup();
},
}
});
respond({ok: false});
}
} else {
// If we /don't have/ an Android Account, we warn if we're
// connecting to a new Account. This is to minimize surprise;
// we never did this when changing accounts via the native UI.
let prevAcctHash = this._helpers.getPreviousAccountNameHashPref();
let shouldShowWarning = prevAcctHash && (prevAcctHash != this._helpers.sha256(data.email));
if (shouldShowWarning) {
log.w("Warning about creating a new Android Account: previously linked to different email address!");
let message = strings.formatStringFromName("relinkVerify.message", [data.email], 1);
new Prompt({
title: strings.GetStringFromName("relinkVerify.title"),
message: message,
buttons: [
// This puts Cancel on the right.
strings.GetStringFromName("relinkVerify.cancel"),
strings.GetStringFromName("relinkVerify.continue"),
],
}).show(result => respond({ok: result && result.button == 1}));
} else {
log.d("Not warning about creating a new Android Account: no previously linked email address.");
respond({ok: true});
}
}
}).catch(e => {
log.e(e.toString());
respond({ok: false});
});
break;
case COMMAND_LOGIN:
// Either create a new Android Account or re-connect an existing
// Android Account here. There's not much to be done if we don't
// succeed or get an error.
Accounts.getFirefoxAccount().then(account => {
if (!account) {
return Accounts.createFirefoxAccountFromJSON(data).then(success => {
if (!success) {
throw new Error("Could not create Firefox Account!");
}
UITelemetry.addEvent("action.1", "content", null, "fxaccount-create");
return success;
});
} else {
return Accounts.updateFirefoxAccountFromJSON(data).then(success => {
if (!success) {
throw new Error("Could not update Firefox Account!");
}
UITelemetry.addEvent("action.1", "content", null, "fxaccount-login");
return success;
});
}
})
.then(success => {
if (!success) {
throw new Error("Could not create or update Firefox Account!");
}
// Remember who it is so we can show a relink warning when appropriate.
this._helpers.setPreviousAccountNameHashPref(data.email);
log.i("Created or updated Firefox Account.");
})
.catch(e => {
log.e(e.toString());
});
break;
case COMMAND_CHANGE_PASSWORD:
// Only update an existing Android Account.
Accounts.getFirefoxAccount().then(account => {
if (!account) {
throw new Error("Can't change password of non-existent Firefox Account!");
}
return Accounts.updateFirefoxAccountFromJSON(data);
})
.then(success => {
if (!success) {
throw new Error("Could not change Firefox Account password!");
}
UITelemetry.addEvent("action.1", "content", null, "fxaccount-changepassword");
log.i("Changed Firefox Account password.");
})
.catch(e => {
log.e(e.toString());
});
break;
case COMMAND_DELETE_ACCOUNT:
// The fxa-content-server has already confirmed the user's intent.
// Bombs away. There's no recovery from failure, and not even a
// real need to check an account exists (although we do, for error
// messaging only).
Accounts.getFirefoxAccount().then(account => {
if (!account) {
throw new Error("Can't delete non-existent Firefox Account!");
}
return Accounts.deleteFirefoxAccount().then(success => {
if (!success) {
throw new Error("Could not delete Firefox Account!");
}
UITelemetry.addEvent("action.1", "content", null, "fxaccount-delete");
log.i("Firefox Account deleted.");
});
}).catch(e => {
log.e(e.toString());
});
break;
case COMMAND_PROFILE_CHANGE:
// Only update an existing Android Account.
Accounts.getFirefoxAccount().then(account => {
if (!account) {
throw new Error("Can't change profile of non-existent Firefox Account!");
}
UITelemetry.addEvent("action.1", "content", null, "fxaccount-changeprofile");
return Accounts.notifyFirefoxAccountProfileChanged();
})
.catch(e => {
log.e(e.toString());
});
break;
case COMMAND_SYNC_PREFERENCES:
UITelemetry.addEvent("action.1", "content", null, "fxaccount-syncprefs");
Accounts.showSyncPreferences()
.catch(e => {
log.e(e.toString());
});
break;
default:
log.w("Ignoring unrecognized FxAccountsWebChannel command: " + JSON.stringify(command));
break;
}
}
};
this._channelCallback = listener;
this._channel = new WebChannel(this._webChannelId, this._webChannelOrigin);
this._channel.listen(listener);
log.d("FxAccountsWebChannel registered: " + this._webChannelId + " with origin " + this._webChannelOrigin.prePath);
}
};
var singleton;
// The entry-point for this module, which ensures only one of our channels is
// ever created - we require this because the WebChannel is global in scope and
// allowing multiple channels would cause such notifications to be sent multiple
// times.
this.EnsureFxAccountsWebChannel = function() {
if (!singleton) {
let contentUri = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.webchannel.uri");
// The FxAccountsWebChannel listens for events and updates the Java layer.
singleton = new this.FxAccountsWebChannel({
content_uri: contentUri,
channel_id: WEBCHANNEL_ID,
});
}
};

View file

@ -10,7 +10,6 @@ EXTRA_JS_MODULES += [
'dbg-browser-actors.js',
'DelayedInit.jsm',
'DownloadNotifications.jsm',
'FxAccountsWebChannel.jsm',
'HelperApps.jsm',
'Home.jsm',
'HomeProvider.jsm',

View file

@ -1 +0,0 @@
These files are managed in the android-sync repo. Do not modify directly, or your changes will be lost.

View file

@ -1,63 +0,0 @@
<activity
android:theme="@style/FxAccountTheme.FxAccountStatusActivity"
android:label="@string/fxaccount_status_activity_label"
android:clearTaskOnLaunch="true"
android:taskAffinity="@ANDROID_PACKAGE_NAME@.FXA"
android:name="org.mozilla.gecko.fxa.activities.FxAccountStatusActivity"
android:configChanges="locale|layoutDirection"
android:windowSoftInputMode="adjustResize">
<!-- Adding a launcher will make this activity appear on the
Apps screen, which we only want when testing. -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<!-- <category android:name="android.intent.category.LAUNCHER" /> -->
</intent-filter>
<intent-filter>
<action android:name="@ANDROID_PACKAGE_NAME@.ACTION_FXA_STATUS"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<receiver
android:name="org.mozilla.gecko.fxa.receivers.FxAccountUpgradeReceiver">
<intent-filter>
<action android:name="android.intent.action.PACKAGE_REPLACED" />
<data android:scheme="package"/>
</intent-filter>
</receiver>
<activity
android:exported="false"
android:name="org.mozilla.gecko.fxa.activities.FxAccountGetStartedActivityWeb">
<intent-filter>
<action android:name="@ANDROID_PACKAGE_NAME@.ACTION_FXA_GET_STARTED"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<activity
android:exported="false"
android:name="org.mozilla.gecko.fxa.activities.FxAccountUpdateCredentialsActivityWeb">
<intent-filter>
<action android:name="@ANDROID_PACKAGE_NAME@.ACTION_FXA_UPDATE_CREDENTIALS"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<activity
android:exported="false"
android:name="org.mozilla.gecko.fxa.activities.FxAccountFinishMigratingActivityWeb">
<intent-filter>
<action android:name="@ANDROID_PACKAGE_NAME@.ACTION_FXA_FINISH_MIGRATING"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<activity
android:exported="false"
android:name="org.mozilla.gecko.fxa.activities.FxAccountConfirmAccountActivityWeb">
<intent-filter>
<action android:name="@ANDROID_PACKAGE_NAME@.ACTION_FXA_CONFIRM_ACCOUNT"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>

View file

@ -1,18 +0,0 @@
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<uses-permission android:name="android.permission.READ_SYNC_STATS" />
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<!-- A signature level permission granted only to the Firefox
channels sharing an Android Account type. -->
<permission
android:name="@ANDROID_PACKAGE_NAME@_fxaccount.permission.PER_ACCOUNT_TYPE"
android:protectionLevel="signature">
</permission>
<uses-permission android:name="@ANDROID_PACKAGE_NAME@_fxaccount.permission.PER_ACCOUNT_TYPE" />

View file

@ -1,34 +0,0 @@
<service
android:exported="true"
android:name="org.mozilla.gecko.fxa.authenticator.FxAccountAuthenticatorService" >
<intent-filter >
<action android:name="android.accounts.AccountAuthenticator" />
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/fxaccount_authenticator" />
</service>
<service
android:exported="false"
android:name="org.mozilla.gecko.fxa.receivers.FxAccountDeletedService" >
</service>
<service
android:exported="false"
android:name="org.mozilla.gecko.fxa.sync.FxAccountProfileService" >
</service>
<!-- Firefox Sync. -->
<service
android:exported="false"
android:name="org.mozilla.gecko.fxa.sync.FxAccountSyncService" >
<intent-filter >
<action android:name="android.content.SyncAdapter" />
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/fxaccount_syncadapter" />
</service>

View file

@ -1,23 +0,0 @@
/* 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/. */
package org.mozilla.gecko.background;
import org.mozilla.gecko.AppConstants;
/**
* This is in 'background' not 'reading' so that it's still usable even when the
* Reading List feature is build-time disabled.
*/
public class ReadingListConstants {
public static final String GLOBAL_LOG_TAG = "FxReadingList";
public static final String USER_AGENT = "Firefox-Android-FxReader/" + AppConstants.MOZ_APP_VERSION + " (" + AppConstants.MOZ_APP_UA_NAME + ")";
public static final String DEFAULT_DEV_ENDPOINT = "https://readinglist.dev.mozaws.net/v1/";
public static final String DEFAULT_PROD_ENDPOINT = "https://readinglist.services.mozilla.com/v1/";
public static final String OAUTH_SCOPE_READINGLIST = "readinglist";
public static final String AUTH_TOKEN_TYPE = "oauth::" + OAUTH_SCOPE_READINGLIST;
public static boolean DEBUG = false;
}

View file

@ -1,82 +0,0 @@
/* 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/. */
package org.mozilla.gecko.background.common;
import java.util.Set;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
public class EditorBranch implements Editor {
private final String prefix;
private Editor editor;
public EditorBranch(final SharedPreferences prefs, final String prefix) {
if (!prefix.endsWith(".")) {
throw new IllegalArgumentException("No trailing period in prefix.");
}
this.prefix = prefix;
this.editor = prefs.edit();
}
@Override
public void apply() {
this.editor.apply();
}
@Override
public Editor clear() {
this.editor = this.editor.clear();
return this;
}
@Override
public boolean commit() {
return this.editor.commit();
}
@Override
public Editor putBoolean(String key, boolean value) {
this.editor = this.editor.putBoolean(prefix + key, value);
return this;
}
@Override
public Editor putFloat(String key, float value) {
this.editor = this.editor.putFloat(prefix + key, value);
return this;
}
@Override
public Editor putInt(String key, int value) {
this.editor = this.editor.putInt(prefix + key, value);
return this;
}
@Override
public Editor putLong(String key, long value) {
this.editor = this.editor.putLong(prefix + key, value);
return this;
}
@Override
public Editor putString(String key, String value) {
this.editor = this.editor.putString(prefix + key, value);
return this;
}
// Not marking as Override, because Android <= 10 doesn't have
// putStringSet. Neither can we implement it.
public Editor putStringSet(String key, Set<String> value) {
throw new RuntimeException("putStringSet not available.");
}
@Override
public Editor remove(String key) {
this.editor = this.editor.remove(prefix + key);
return this;
}
}

View file

@ -1,90 +0,0 @@
/* 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/. */
package org.mozilla.gecko.background.common;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.AppConstants.Versions;
/**
* Constant values common to all Android services.
*/
public class GlobalConstants {
public static final String BROWSER_INTENT_PACKAGE = AppConstants.ANDROID_PACKAGE_NAME;
public static final String BROWSER_INTENT_CLASS = AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS;
public static final int SHARED_PREFERENCES_MODE = 0;
// Common time values.
public static final long MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
public static final long MILLISECONDS_PER_SIX_MONTHS = 180 * MILLISECONDS_PER_DAY;
// Acceptable cipher suites.
/**
* We support only a very limited range of strong cipher suites and protocols:
* no SSLv3 or TLSv1.0 (if we can), no DHE ciphers that might be vulnerable to Logjam
* (https://weakdh.org/), no RC4.
*
* Backstory: Bug 717691 (we no longer support Android 2.2, so the name
* workaround is unnecessary), Bug 1081953, Bug 1061273, Bug 1166839.
*
* See <http://developer.android.com/reference/javax/net/ssl/SSLSocket.html> for
* supported Android versions for each set of protocols and cipher suites.
*
* Note that currently we need to support connections to Sync 1.1 on Mozilla-hosted infra,
* as well as connections to FxA and Sync 1.5 on AWS.
*
* ELB cipher suites:
* <http://docs.aws.amazon.com/ElasticLoadBalancing/latest/DeveloperGuide/elb-security-policy-table.html>
*/
public static final String[] DEFAULT_CIPHER_SUITES;
public static final String[] DEFAULT_PROTOCOLS;
static {
// Prioritize 128 over 256 as a tradeoff between device CPU/battery and the minor
// increase in strength.
if (Versions.feature20Plus) {
DEFAULT_CIPHER_SUITES = new String[]
{
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", // 20+
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", // 20+
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", // 20+
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", // 11+
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", // 20+
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", // 20+
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", // 11+
// For Sync 1.1.
"TLS_DHE_RSA_WITH_AES_128_CBC_SHA", // 9+
"TLS_RSA_WITH_AES_128_CBC_SHA", // 9+
};
} else {
DEFAULT_CIPHER_SUITES = new String[]
{
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", // 11+
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", // 11+
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", // 11+
// For Sync 1.1.
"TLS_DHE_RSA_WITH_AES_128_CBC_SHA", // 9+
"TLS_RSA_WITH_AES_128_CBC_SHA", // 9+
};
}
if (Versions.feature16Plus) {
DEFAULT_PROTOCOLS = new String[]
{
"TLSv1.2",
"TLSv1.1",
"TLSv1", // We would like to remove this, and will do so when we can.
};
} else {
// Fall back to TLSv1 if there's nothing better.
DEFAULT_PROTOCOLS = new String[]
{
"TLSv1",
};
}
}
}

View file

@ -1,83 +0,0 @@
/* 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/. */
package org.mozilla.gecko.background.common;
import java.util.Map;
import java.util.Set;
import android.content.SharedPreferences;
/**
* A wrapper around a portion of the SharedPreferences space.
*/
public class PrefsBranch implements SharedPreferences {
private final SharedPreferences prefs;
private final String prefix; // Including trailing period.
public PrefsBranch(SharedPreferences prefs, String prefix) {
if (!prefix.endsWith(".")) {
throw new IllegalArgumentException("No trailing period in prefix.");
}
this.prefs = prefs;
this.prefix = prefix;
}
@Override
public boolean contains(String key) {
return prefs.contains(prefix + key);
}
@Override
public Editor edit() {
return new EditorBranch(prefs, prefix);
}
@Override
public Map<String, ?> getAll() {
// Not implemented. TODO
return null;
}
@Override
public boolean getBoolean(String key, boolean defValue) {
return prefs.getBoolean(prefix + key, defValue);
}
@Override
public float getFloat(String key, float defValue) {
return prefs.getFloat(prefix + key, defValue);
}
@Override
public int getInt(String key, int defValue) {
return prefs.getInt(prefix + key, defValue);
}
@Override
public long getLong(String key, long defValue) {
return prefs.getLong(prefix + key, defValue);
}
@Override
public String getString(String key, String defValue) {
return prefs.getString(prefix + key, defValue);
}
// Not marking as Override, because Android <= 10 doesn't have
// getStringSet. Neither can we implement it.
public Set<String> getStringSet(String key, Set<String> defValue) {
throw new RuntimeException("getStringSet not available.");
}
@Override
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
prefs.registerOnSharedPreferenceChangeListener(listener);
}
@Override
public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
prefs.unregisterOnSharedPreferenceChangeListener(listener);
}
}

View file

@ -1,232 +0,0 @@
/* 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/. */
package org.mozilla.gecko.background.common.log;
import java.io.PrintWriter;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;
import org.mozilla.gecko.background.common.GlobalConstants;
import org.mozilla.gecko.background.common.log.writers.AndroidLevelCachingLogWriter;
import org.mozilla.gecko.background.common.log.writers.AndroidLogWriter;
import org.mozilla.gecko.background.common.log.writers.LogWriter;
import org.mozilla.gecko.background.common.log.writers.PrintLogWriter;
import org.mozilla.gecko.background.common.log.writers.SimpleTagLogWriter;
import org.mozilla.gecko.background.common.log.writers.ThreadLocalTagLogWriter;
import android.util.Log;
/**
* Logging helper class. Serializes all log operations (by synchronizing).
*/
public class Logger {
public static final String LOGGER_TAG = "Logger";
public static final String DEFAULT_LOG_TAG = "GeckoLogger";
// For extra debugging.
public static boolean LOG_PERSONAL_INFORMATION = false;
/**
* Allow each thread to use its own global log tag. This allows
* independent services to log as different sources.
*
* When your thread sets up logging, it should do something like the following:
*
* Logger.setThreadLogTag("MyTag");
*
* The value is inheritable, so worker threads and such do not need to
* set the same log tag as their parent.
*/
private static final InheritableThreadLocal<String> logTag = new InheritableThreadLocal<String>() {
@Override
protected String initialValue() {
return DEFAULT_LOG_TAG;
}
};
public static void setThreadLogTag(final String logTag) {
Logger.logTag.set(logTag);
}
public static String getThreadLogTag() {
return Logger.logTag.get();
}
/**
* Current set of writers to which we will log.
* <p>
* We want logging to be available while running tests, so we initialize
* this set statically.
*/
protected final static Set<LogWriter> logWriters;
static {
final Set<LogWriter> defaultWriters = Logger.defaultLogWriters();
logWriters = new LinkedHashSet<LogWriter>(defaultWriters);
}
/**
* Default set of log writers to log to.
*/
public final static Set<LogWriter> defaultLogWriters() {
final String processedPackage = GlobalConstants.BROWSER_INTENT_PACKAGE.replace("org.mozilla.", "");
final Set<LogWriter> defaultLogWriters = new LinkedHashSet<LogWriter>();
final LogWriter log = new AndroidLogWriter();
final LogWriter cache = new AndroidLevelCachingLogWriter(log);
final LogWriter single = new SimpleTagLogWriter(processedPackage, new ThreadLocalTagLogWriter(Logger.logTag, cache));
defaultLogWriters.add(single);
return defaultLogWriters;
}
public static synchronized void startLoggingTo(LogWriter logWriter) {
logWriters.add(logWriter);
}
public static synchronized void startLoggingToWriters(Set<LogWriter> writers) {
logWriters.addAll(writers);
}
public static synchronized void stopLoggingTo(LogWriter logWriter) {
try {
logWriter.close();
} catch (Exception e) {
Log.e(LOGGER_TAG, "Got exception closing and removing LogWriter " + logWriter + ".", e);
}
logWriters.remove(logWriter);
}
public static synchronized void stopLoggingToAll() {
for (LogWriter logWriter : logWriters) {
try {
logWriter.close();
} catch (Exception e) {
Log.e(LOGGER_TAG, "Got exception closing and removing LogWriter " + logWriter + ".", e);
}
}
logWriters.clear();
}
/**
* Write to only the default log writers.
*/
public static synchronized void resetLogging() {
stopLoggingToAll();
logWriters.addAll(Logger.defaultLogWriters());
}
/**
* Start writing log output to stdout.
* <p>
* Use <code>resetLogging</code> to stop logging to stdout.
*/
public static synchronized void startLoggingToConsole() {
setThreadLogTag("Test");
startLoggingTo(new PrintLogWriter(new PrintWriter(System.out, true)));
}
// Synchronized version for other classes to use.
public static synchronized boolean shouldLogVerbose(String logTag) {
for (LogWriter logWriter : logWriters) {
if (logWriter.shouldLogVerbose(logTag)) {
return true;
}
}
return false;
}
public static void error(String tag, String message) {
Logger.error(tag, message, null);
}
public static void warn(String tag, String message) {
Logger.warn(tag, message, null);
}
public static void info(String tag, String message) {
Logger.info(tag, message, null);
}
public static void debug(String tag, String message) {
Logger.debug(tag, message, null);
}
public static void trace(String tag, String message) {
Logger.trace(tag, message, null);
}
public static void pii(String tag, String message) {
if (LOG_PERSONAL_INFORMATION) {
Logger.debug(tag, "$$PII$$: " + message);
}
}
public static synchronized void error(String tag, String message, Throwable error) {
Iterator<LogWriter> it = logWriters.iterator();
while (it.hasNext()) {
LogWriter writer = it.next();
try {
writer.error(tag, message, error);
} catch (Exception e) {
Log.e(LOGGER_TAG, "Got exception logging; removing LogWriter " + writer + ".", e);
it.remove();
}
}
}
public static synchronized void warn(String tag, String message, Throwable error) {
Iterator<LogWriter> it = logWriters.iterator();
while (it.hasNext()) {
LogWriter writer = it.next();
try {
writer.warn(tag, message, error);
} catch (Exception e) {
Log.e(LOGGER_TAG, "Got exception logging; removing LogWriter " + writer + ".", e);
it.remove();
}
}
}
public static synchronized void info(String tag, String message, Throwable error) {
Iterator<LogWriter> it = logWriters.iterator();
while (it.hasNext()) {
LogWriter writer = it.next();
try {
writer.info(tag, message, error);
} catch (Exception e) {
Log.e(LOGGER_TAG, "Got exception logging; removing LogWriter " + writer + ".", e);
it.remove();
}
}
}
public static synchronized void debug(String tag, String message, Throwable error) {
Iterator<LogWriter> it = logWriters.iterator();
while (it.hasNext()) {
LogWriter writer = it.next();
try {
writer.debug(tag, message, error);
} catch (Exception e) {
Log.e(LOGGER_TAG, "Got exception logging; removing LogWriter " + writer + ".", e);
it.remove();
}
}
}
public static synchronized void trace(String tag, String message, Throwable error) {
Iterator<LogWriter> it = logWriters.iterator();
while (it.hasNext()) {
LogWriter writer = it.next();
try {
writer.trace(tag, message, error);
} catch (Exception e) {
Log.e(LOGGER_TAG, "Got exception logging; removing LogWriter " + writer + ".", e);
it.remove();
}
}
}
}

View file

@ -1,132 +0,0 @@
/* 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/. */
package org.mozilla.gecko.background.common.log.writers;
import java.util.IdentityHashMap;
import java.util.Map;
import android.util.Log;
/**
* Make a <code>LogWriter</code> only log when the Android log system says to.
*/
public class AndroidLevelCachingLogWriter extends LogWriter {
protected final LogWriter inner;
public AndroidLevelCachingLogWriter(LogWriter inner) {
this.inner = inner;
}
// I can't believe we have to implement this ourselves.
// These aren't synchronized (and neither are the setters) because
// the logging calls themselves are synchronized.
private Map<String, Boolean> isErrorLoggable = new IdentityHashMap<String, Boolean>();
private Map<String, Boolean> isWarnLoggable = new IdentityHashMap<String, Boolean>();
private Map<String, Boolean> isInfoLoggable = new IdentityHashMap<String, Boolean>();
private Map<String, Boolean> isDebugLoggable = new IdentityHashMap<String, Boolean>();
private Map<String, Boolean> isVerboseLoggable = new IdentityHashMap<String, Boolean>();
/**
* Empty the caches of log levels.
*/
public void refreshLogLevels() {
isErrorLoggable = new IdentityHashMap<String, Boolean>();
isWarnLoggable = new IdentityHashMap<String, Boolean>();
isInfoLoggable = new IdentityHashMap<String, Boolean>();
isDebugLoggable = new IdentityHashMap<String, Boolean>();
isVerboseLoggable = new IdentityHashMap<String, Boolean>();
}
private boolean shouldLogError(String logTag) {
Boolean out = isErrorLoggable.get(logTag);
if (out != null) {
return out;
}
out = Log.isLoggable(logTag, Log.ERROR);
isErrorLoggable.put(logTag, out);
return out;
}
private boolean shouldLogWarn(String logTag) {
Boolean out = isWarnLoggable.get(logTag);
if (out != null) {
return out;
}
out = Log.isLoggable(logTag, Log.WARN);
isWarnLoggable.put(logTag, out);
return out;
}
private boolean shouldLogInfo(String logTag) {
Boolean out = isInfoLoggable.get(logTag);
if (out != null) {
return out;
}
out = Log.isLoggable(logTag, Log.INFO);
isInfoLoggable.put(logTag, out);
return out;
}
private boolean shouldLogDebug(String logTag) {
Boolean out = isDebugLoggable.get(logTag);
if (out != null) {
return out;
}
out = Log.isLoggable(logTag, Log.DEBUG);
isDebugLoggable.put(logTag, out);
return out;
}
@Override
public boolean shouldLogVerbose(String logTag) {
Boolean out = isVerboseLoggable.get(logTag);
if (out != null) {
return out;
}
out = Log.isLoggable(logTag, Log.VERBOSE);
isVerboseLoggable.put(logTag, out);
return out;
}
@Override
public void error(String tag, String message, Throwable error) {
if (shouldLogError(tag)) {
inner.error(tag, message, error);
}
}
@Override
public void warn(String tag, String message, Throwable error) {
if (shouldLogWarn(tag)) {
inner.warn(tag, message, error);
}
}
@Override
public void info(String tag, String message, Throwable error) {
if (shouldLogInfo(tag)) {
inner.info(tag, message, error);
}
}
@Override
public void debug(String tag, String message, Throwable error) {
if (shouldLogDebug(tag)) {
inner.debug(tag, message, error);
}
}
@Override
public void trace(String tag, String message, Throwable error) {
if (shouldLogVerbose(tag)) {
inner.trace(tag, message, error);
}
}
@Override
public void close() {
inner.close();
}
}

View file

@ -1,46 +0,0 @@
/* 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/. */
package org.mozilla.gecko.background.common.log.writers;
import android.util.Log;
/**
* Log to the Android log.
*/
public class AndroidLogWriter extends LogWriter {
@Override
public boolean shouldLogVerbose(String logTag) {
return true;
}
@Override
public void error(String tag, String message, Throwable error) {
Log.e(tag, message, error);
}
@Override
public void warn(String tag, String message, Throwable error) {
Log.w(tag, message, error);
}
@Override
public void info(String tag, String message, Throwable error) {
Log.i(tag, message, error);
}
@Override
public void debug(String tag, String message, Throwable error) {
Log.d(tag, message, error);
}
@Override
public void trace(String tag, String message, Throwable error) {
Log.v(tag, message, error);
}
@Override
public void close() {
}
}

View file

@ -1,67 +0,0 @@
/* 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/. */
package org.mozilla.gecko.background.common.log.writers;
import android.util.Log;
/**
* A LogWriter that logs only if the message is as important as the specified
* level. For example, if the specified level is <code>Log.WARN</code>, only
* <code>warn</code> and <code>error</code> will log.
*/
public class LevelFilteringLogWriter extends LogWriter {
protected final LogWriter inner;
protected final int logLevel;
public LevelFilteringLogWriter(int logLevel, LogWriter inner) {
this.inner = inner;
this.logLevel = logLevel;
}
@Override
public void close() {
inner.close();
}
@Override
public void error(String tag, String message, Throwable error) {
if (logLevel <= Log.ERROR) {
inner.error(tag, message, error);
}
}
@Override
public void warn(String tag, String message, Throwable error) {
if (logLevel <= Log.WARN) {
inner.warn(tag, message, error);
}
}
@Override
public void info(String tag, String message, Throwable error) {
if (logLevel <= Log.INFO) {
inner.info(tag, message, error);
}
}
@Override
public void debug(String tag, String message, Throwable error) {
if (logLevel <= Log.DEBUG) {
inner.debug(tag, message, error);
}
}
@Override
public void trace(String tag, String message, Throwable error) {
if (logLevel <= Log.VERBOSE) {
inner.trace(tag, message, error);
}
}
@Override
public boolean shouldLogVerbose(String tag) {
return logLevel <= Log.VERBOSE;
}
}

View file

@ -1,29 +0,0 @@
/* 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/. */
package org.mozilla.gecko.background.common.log.writers;
/**
* An abstract object that logs information in some way.
* <p>
* Intended to be composed with other log writers, for example a log
* writer could make all log entries have the same single log tag, or
* could ignore certain log levels, before delegating to an inner log
* writer.
*/
public abstract class LogWriter {
public abstract void error(String tag, String message, Throwable error);
public abstract void warn(String tag, String message, Throwable error);
public abstract void info(String tag, String message, Throwable error);
public abstract void debug(String tag, String message, Throwable error);
public abstract void trace(String tag, String message, Throwable error);
/**
* We expect <code>close</code> to be called only by static
* synchronized methods in class <code>Logger</code>.
*/
public abstract void close();
public abstract boolean shouldLogVerbose(String tag);
}

View file

@ -1,77 +0,0 @@
/* 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/. */
package org.mozilla.gecko.background.common.log.writers;
import java.io.PrintWriter;
/**
* Log to a <code>PrintWriter</code>.
*/
public class PrintLogWriter extends LogWriter {
protected final PrintWriter pw;
protected boolean closed = false;
public static final String ERROR = " :: E :: ";
public static final String WARN = " :: W :: ";
public static final String INFO = " :: I :: ";
public static final String DEBUG = " :: D :: ";
public static final String VERBOSE = " :: V :: ";
public PrintLogWriter(PrintWriter pw) {
this.pw = pw;
}
protected void log(String tag, String message, Throwable error) {
if (closed) {
return;
}
pw.println(tag + message);
if (error != null) {
error.printStackTrace(pw);
}
}
@Override
public void error(String tag, String message, Throwable error) {
log(tag, ERROR + message, error);
}
@Override
public void warn(String tag, String message, Throwable error) {
log(tag, WARN + message, error);
}
@Override
public void info(String tag, String message, Throwable error) {
log(tag, INFO + message, error);
}
@Override
public void debug(String tag, String message, Throwable error) {
log(tag, DEBUG + message, error);
}
@Override
public void trace(String tag, String message, Throwable error) {
log(tag, VERBOSE + message, error);
}
@Override
public boolean shouldLogVerbose(String tag) {
return true;
}
@Override
public void close() {
if (closed) {
return;
}
if (pw != null) {
pw.close();
}
closed = true;
}
}

View file

@ -1,21 +0,0 @@
/* 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/. */
package org.mozilla.gecko.background.common.log.writers;
/**
* Make a <code>LogWriter</code> only log with a single string tag.
*/
public class SimpleTagLogWriter extends TagLogWriter {
final String tag;
public SimpleTagLogWriter(String tag, LogWriter inner) {
super(inner);
this.tag = tag;
}
@Override
protected String getMainTag() {
return tag;
}
}

View file

@ -1,57 +0,0 @@
/* 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/. */
package org.mozilla.gecko.background.common.log.writers;
import java.io.PrintWriter;
import java.io.StringWriter;
public class StringLogWriter extends LogWriter {
protected final StringWriter sw;
protected final PrintLogWriter inner;
public StringLogWriter() {
sw = new StringWriter();
inner = new PrintLogWriter(new PrintWriter(sw));
}
public String toString() {
return sw.toString();
}
@Override
public boolean shouldLogVerbose(String tag) {
return true;
}
@Override
public void error(String tag, String message, Throwable error) {
inner.error(tag, message, error);
}
@Override
public void warn(String tag, String message, Throwable error) {
inner.warn(tag, message, error);
}
@Override
public void info(String tag, String message, Throwable error) {
inner.info(tag, message, error);
}
@Override
public void debug(String tag, String message, Throwable error) {
inner.debug(tag, message, error);
}
@Override
public void trace(String tag, String message, Throwable error) {
inner.trace(tag, message, error);
}
@Override
public void close() {
inner.close();
}
}

View file

@ -1,55 +0,0 @@
/* 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/. */
package org.mozilla.gecko.background.common.log.writers;
/**
* A @link{LogWriter} that logs each message under a parent tag.
*/
public abstract class TagLogWriter extends LogWriter {
protected final LogWriter inner;
public TagLogWriter(final LogWriter inner) {
super();
this.inner = inner;
}
protected abstract String getMainTag();
@Override
public void error(String tag, String message, Throwable error) {
inner.error(this.getMainTag(), tag + " :: " + message, error);
}
@Override
public void warn(String tag, String message, Throwable error) {
inner.warn(this.getMainTag(), tag + " :: " + message, error);
}
@Override
public void info(String tag, String message, Throwable error) {
inner.info(this.getMainTag(), tag + " :: " + message, error);
}
@Override
public void debug(String tag, String message, Throwable error) {
inner.debug(this.getMainTag(), tag + " :: " + message, error);
}
@Override
public void trace(String tag, String message, Throwable error) {
inner.trace(this.getMainTag(), tag + " :: " + message, error);
}
@Override
public boolean shouldLogVerbose(String tag) {
return inner.shouldLogVerbose(this.getMainTag());
}
@Override
public void close() {
inner.close();
}
}

View file

@ -1,25 +0,0 @@
/* 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/. */
package org.mozilla.gecko.background.common.log.writers;
/**
* Log with a single global tag but that tag can be different for each thread.
*
* Takes a @link{ThreadLocal} as a constructor parameter.
*/
public class ThreadLocalTagLogWriter extends TagLogWriter {
private final ThreadLocal<String> tag;
public ThreadLocalTagLogWriter(ThreadLocal<String> tag, LogWriter inner) {
super(inner);
this.tag = tag;
}
@Override
protected String getMainTag() {
return this.tag.get();
}
}

View file

@ -1,56 +0,0 @@
/* 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/. */
package org.mozilla.gecko.background.common.telemetry;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import org.mozilla.gecko.background.common.log.Logger;
/**
* Android Background Services are normally built into Fennec, but can also be
* built as a stand-alone APK for rapid local development. The current Telemetry
* implementation is coupled to Gecko, and Background Services should not
* interact with Gecko directly. To maintain this independence, Background
* Services lazily introspects the relevant Telemetry class from the enclosing
* package, warning but otherwise ignoring failures during introspection or
* invocation.
* <p>
* It is possible that Background Services will introspect and invoke the
* Telemetry implementation while Gecko is not running. In this case, the Fennec
* process itself buffers Telemetry events until such time as they can be
* flushed to disk and uploaded. <b>There is no guarantee that all Telemetry
* events will be uploaded!</b> Depending on the volume of data and the
* application lifecycle, Telemetry events may be dropped.
*/
public class TelemetryWrapper {
private static final String LOG_TAG = TelemetryWrapper.class.getSimpleName();
// Marking this volatile maintains thread safety cheaply.
private static volatile Method mAddToHistogram;
public static void addToHistogram(String key, int value) {
if (mAddToHistogram == null) {
try {
final Class<?> telemetry = Class.forName("org.mozilla.gecko.Telemetry");
mAddToHistogram = telemetry.getMethod("addToHistogram", String.class, int.class);
} catch (ClassNotFoundException e) {
Logger.warn(LOG_TAG, "org.mozilla.gecko.Telemetry class found!");
return;
} catch (NoSuchMethodException e) {
Logger.warn(LOG_TAG, "org.mozilla.gecko.Telemetry.addToHistogram(String, int) method not found!");
return;
}
}
if (mAddToHistogram != null) {
try {
mAddToHistogram.invoke(null, key, value);
} catch (IllegalArgumentException | InvocationTargetException | IllegalAccessException e) {
Logger.warn(LOG_TAG, "Got exception invoking telemetry!");
}
}
}
}

View file

@ -1,99 +0,0 @@
/* 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/. */
package org.mozilla.gecko.background.db;
import android.database.Cursor;
/**
* A utility for dumping a cursor the debug log.
* <p>
* <b>For debugging only!</p>
*/
public class CursorDumper {
protected static String fixedWidth(int width, String s) {
if (s == null) {
return spaces(width);
}
int length = s.length();
if (width == length) {
return s;
}
if (width > length) {
return s + spaces(width - length);
}
return s.substring(0, width);
}
protected static String spaces(int i) {
return " ".substring(0, i);
}
protected static String dashes(int i) {
return "-------------------------------------".substring(0, i);
}
/**
* Dump a cursor to the debug log, ignoring any log level settings.
* <p>
* The position in the cursor is maintained. Caller is responsible for opening
* and closing cursor.
*
* @param cursor
* to dump.
*/
public static void dumpCursor(Cursor cursor) {
dumpCursor(cursor, 18, "records");
}
/**
* Dump a cursor to the debug log, ignoring any log level settings.
* <p>
* The position in the cursor is maintained. Caller is responsible for opening
* and closing cursor.
*
* @param cursor
* to dump.
* @param columnWidth
* how many characters per cursor column.
* @param tags
* a descriptor, printed like "(10 tags)", in the header row.
*/
protected static void dumpCursor(Cursor cursor, int columnWidth, String tags) {
int originalPosition = cursor.getPosition();
try {
String[] columnNames = cursor.getColumnNames();
int columnCount = cursor.getColumnCount();
for (int i = 0; i < columnCount; ++i) {
System.out.print(fixedWidth(columnWidth, columnNames[i]) + " | ");
}
System.out.println("(" + cursor.getCount() + " " + tags + ")");
for (int i = 0; i < columnCount; ++i) {
System.out.print(dashes(columnWidth) + " | ");
}
System.out.println("");
if (!cursor.moveToFirst()) {
System.out.println("EMPTY");
return;
}
cursor.moveToFirst();
while (!cursor.isAfterLast()) {
for (int i = 0; i < columnCount; ++i) {
System.out.print(fixedWidth(columnWidth, cursor.getString(i)) + " | ");
}
System.out.println("");
cursor.moveToNext();
}
for (int i = 0; i < columnCount-1; ++i) {
System.out.print(dashes(columnWidth + 3));
}
System.out.print(dashes(columnWidth + 3 - 1));
System.out.println("");
} finally {
cursor.moveToPosition(originalPosition);
}
}
}

View file

@ -1,86 +0,0 @@
/* 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/. */
package org.mozilla.gecko.background.db;
import org.json.simple.JSONArray;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.db.BrowserContract.Tabs;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.repositories.android.RepoUtils;
import android.content.ContentValues;
import android.database.Cursor;
// Immutable.
public class Tab {
public final String title;
public final String icon;
public final JSONArray history;
public final long lastUsed;
public Tab(String title, String icon, JSONArray history, long lastUsed) {
this.title = title;
this.icon = icon;
this.history = history;
this.lastUsed = lastUsed;
}
public ContentValues toContentValues(String clientGUID, int position) {
ContentValues out = new ContentValues();
out.put(BrowserContract.Tabs.POSITION, position);
out.put(BrowserContract.Tabs.CLIENT_GUID, clientGUID);
out.put(BrowserContract.Tabs.FAVICON, this.icon);
out.put(BrowserContract.Tabs.LAST_USED, this.lastUsed);
out.put(BrowserContract.Tabs.TITLE, this.title);
out.put(BrowserContract.Tabs.URL, (String) this.history.get(0));
out.put(BrowserContract.Tabs.HISTORY, this.history.toJSONString());
return out;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Tab)) {
return false;
}
final Tab other = (Tab) o;
if (!RepoUtils.stringsEqual(this.title, other.title)) {
return false;
}
if (!RepoUtils.stringsEqual(this.icon, other.icon)) {
return false;
}
if (!(this.lastUsed == other.lastUsed)) {
return false;
}
return Utils.sameArrays(this.history, other.history);
}
@Override
public int hashCode() {
return super.hashCode();
}
/**
* Extract a <code>Tab</code> from a cursor row.
* <p>
* Caller is responsible for creating, positioning, and closing the cursor.
*
* @param cursor
* to inspect.
* @return <code>Tab</code> instance.
*/
public static Tab fromCursor(final Cursor cursor) {
final String title = RepoUtils.getStringFromCursor(cursor, Tabs.TITLE);
final String icon = RepoUtils.getStringFromCursor(cursor, Tabs.FAVICON);
final JSONArray history = RepoUtils.getJSONArrayFromCursor(cursor, Tabs.HISTORY);
final long lastUsed = RepoUtils.getLongFromCursor(cursor, Tabs.LAST_USED);
return new Tab(title, icon, history, lastUsed);
}
}

View file

@ -1,52 +0,0 @@
/* 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/. */
package org.mozilla.gecko.background.fxa;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.Utils;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
public class FxAccount20CreateDelegate {
protected final byte[] emailUTF8;
protected final byte[] authPW;
protected final boolean preVerified;
/**
* Make a new "create account" delegate.
*
* @param emailUTF8
* email as UTF-8 bytes.
* @param quickStretchedPW
* quick stretched password as bytes.
* @param preVerified
* true if account should be marked already verified; only effective
* for non-production auth servers.
* @throws UnsupportedEncodingException
* @throws GeneralSecurityException
*/
public FxAccount20CreateDelegate(byte[] emailUTF8, byte[] quickStretchedPW, boolean preVerified) throws UnsupportedEncodingException, GeneralSecurityException {
this.emailUTF8 = emailUTF8;
this.authPW = FxAccountUtils.generateAuthPW(quickStretchedPW);
this.preVerified = preVerified;
}
public ExtendedJSONObject getCreateBody() throws FxAccountClientException {
final ExtendedJSONObject body = new ExtendedJSONObject();
try {
body.put("email", new String(emailUTF8, "UTF-8"));
body.put("authPW", Utils.byte2Hex(authPW));
if (preVerified) {
// Production endpoints do not allow preVerified; this assumes we only
// set it when it's okay to send it.
body.put("preVerified", preVerified);
}
return body;
} catch (UnsupportedEncodingException e) {
throw new FxAccountClientException(e);
}
}
}

View file

@ -1,36 +0,0 @@
/* 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/. */
package org.mozilla.gecko.background.fxa;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.Utils;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
/**
* An abstraction around providing an email and authorization token to the auth
* server.
*/
public class FxAccount20LoginDelegate {
protected final byte[] emailUTF8;
protected final byte[] authPW;
public FxAccount20LoginDelegate(byte[] emailUTF8, byte[] quickStretchedPW) throws UnsupportedEncodingException, GeneralSecurityException {
this.emailUTF8 = emailUTF8;
this.authPW = FxAccountUtils.generateAuthPW(quickStretchedPW);
}
public ExtendedJSONObject getCreateBody() throws FxAccountClientException {
final ExtendedJSONObject body = new ExtendedJSONObject();
try {
body.put("email", new String(emailUTF8, "UTF-8"));
body.put("authPW", Utils.byte2Hex(authPW));
return body;
} catch (UnsupportedEncodingException e) {
throw new FxAccountClientException(e);
}
}
}

View file

@ -1,24 +0,0 @@
/* 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/. */
package org.mozilla.gecko.background.fxa;
import org.mozilla.gecko.background.fxa.FxAccountClient20.AccountStatusResponse;
import org.mozilla.gecko.background.fxa.FxAccountClient20.RecoveryEmailStatusResponse;
import org.mozilla.gecko.background.fxa.FxAccountClient20.RequestDelegate;
import org.mozilla.gecko.background.fxa.FxAccountClient20.TwoKeys;
import org.mozilla.gecko.fxa.FxAccountDevice;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import java.util.List;
public interface FxAccountClient {
public void accountStatus(String uid, RequestDelegate<AccountStatusResponse> requestDelegate);
public void recoveryEmailStatus(byte[] sessionToken, RequestDelegate<RecoveryEmailStatusResponse> requestDelegate);
public void keys(byte[] keyFetchToken, RequestDelegate<TwoKeys> requestDelegate);
public void sign(byte[] sessionToken, ExtendedJSONObject publicKey, long certificateDurationInMilliseconds, RequestDelegate<String> requestDelegate);
public void registerOrUpdateDevice(byte[] sessionToken, FxAccountDevice device, RequestDelegate<FxAccountDevice> requestDelegate);
public void deviceList(byte[] sessionToken, RequestDelegate<FxAccountDevice[]> requestDelegate);
public void notifyDevices(byte[] sessionToken, List<String> deviceIds, ExtendedJSONObject payload, Long TTL, RequestDelegate<ExtendedJSONObject> requestDelegate);
}

View file

@ -1,914 +0,0 @@
/* 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/. */
package org.mozilla.gecko.background.fxa;
import android.support.annotation.NonNull;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientMalformedResponseException;
import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.Locales;
import org.mozilla.gecko.fxa.FxAccountDevice;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.crypto.HKDF;
import org.mozilla.gecko.sync.net.AuthHeaderProvider;
import org.mozilla.gecko.sync.net.BaseResource;
import org.mozilla.gecko.sync.net.BaseResourceDelegate;
import org.mozilla.gecko.sync.net.HawkAuthHeaderProvider;
import org.mozilla.gecko.sync.net.Resource;
import org.mozilla.gecko.sync.net.SyncResponse;
import org.mozilla.gecko.sync.net.SyncStorageResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.Executor;
import javax.crypto.Mac;
import ch.boye.httpclientandroidlib.HttpEntity;
import ch.boye.httpclientandroidlib.HttpHeaders;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.client.ClientProtocolException;
import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
/**
* An HTTP client for talking to an FxAccount server.
* <p>
* <p>
* The delegate structure used is a little different from the rest of the code
* base. We add a <code>RequestDelegate</code> layer that processes a typed
* value extracted from the body of a successful response.
*/
public class FxAccountClient20 implements FxAccountClient {
protected static final String LOG_TAG = FxAccountClient20.class.getSimpleName();
protected static final String ACCEPT_HEADER = "application/json;charset=utf-8";
public static final String JSON_KEY_EMAIL = "email";
public static final String JSON_KEY_KEYFETCHTOKEN = "keyFetchToken";
public static final String JSON_KEY_SESSIONTOKEN = "sessionToken";
public static final String JSON_KEY_UID = "uid";
public static final String JSON_KEY_VERIFIED = "verified";
public static final String JSON_KEY_ERROR = "error";
public static final String JSON_KEY_MESSAGE = "message";
public static final String JSON_KEY_INFO = "info";
public static final String JSON_KEY_CODE = "code";
public static final String JSON_KEY_ERRNO = "errno";
public static final String JSON_KEY_EXISTS = "exists";
protected static final String[] requiredErrorStringFields = { JSON_KEY_ERROR, JSON_KEY_MESSAGE, JSON_KEY_INFO };
protected static final String[] requiredErrorLongFields = { JSON_KEY_CODE, JSON_KEY_ERRNO };
/**
* The server's URI.
* <p>
* We assume throughout that this ends with a trailing slash (and guarantee as
* much in the constructor).
*/
protected final String serverURI;
protected final Executor executor;
public FxAccountClient20(String serverURI, Executor executor) {
if (serverURI == null) {
throw new IllegalArgumentException("Must provide a server URI.");
}
if (executor == null) {
throw new IllegalArgumentException("Must provide a non-null executor.");
}
this.serverURI = serverURI.endsWith("/") ? serverURI : serverURI + "/";
if (!this.serverURI.endsWith("/")) {
throw new IllegalArgumentException("Constructed serverURI must end with a trailing slash: " + this.serverURI);
}
this.executor = executor;
}
protected BaseResource getBaseResource(String path, Map<String, String> queryParameters) throws UnsupportedEncodingException, URISyntaxException {
if (queryParameters == null || queryParameters.isEmpty()) {
return getBaseResource(path);
}
final String[] array = new String[2 * queryParameters.size()];
int i = 0;
for (Entry<String, String> entry : queryParameters.entrySet()) {
array[i++] = entry.getKey();
array[i++] = entry.getValue();
}
return getBaseResource(path, array);
}
/**
* Create <code>BaseResource</code>, encoding query parameters carefully.
* <p>
* This is equivalent to <code>android.net.Uri.Builder</code>, which is not
* present in our JUnit 4 tests.
*
* @param path fragment.
* @param queryParameters list of key/value query parameter pairs. Must be even length!
* @return <code>BaseResource<instance>
* @throws URISyntaxException
* @throws UnsupportedEncodingException
*/
protected BaseResource getBaseResource(String path, String... queryParameters) throws URISyntaxException, UnsupportedEncodingException {
final StringBuilder sb = new StringBuilder(serverURI);
sb.append(path);
if (queryParameters != null) {
int i = 0;
while (i < queryParameters.length) {
sb.append(i > 0 ? "&" : "?");
final String key = queryParameters[i++];
final String val = queryParameters[i++];
sb.append(URLEncoder.encode(key, "UTF-8"));
sb.append("=");
sb.append(URLEncoder.encode(val, "UTF-8"));
}
}
return new BaseResource(new URI(sb.toString()));
}
/**
* Process a typed value extracted from a successful response (in an
* endpoint-dependent way).
*/
public interface RequestDelegate<T> {
public void handleError(Exception e);
public void handleFailure(FxAccountClientRemoteException e);
public void handleSuccess(T result);
}
/**
* Thin container for two cryptographic keys.
*/
public static class TwoKeys {
public final byte[] kA;
public final byte[] wrapkB;
public TwoKeys(byte[] kA, byte[] wrapkB) {
this.kA = kA;
this.wrapkB = wrapkB;
}
}
protected <T> void invokeHandleError(final RequestDelegate<T> delegate, final Exception e) {
executor.execute(new Runnable() {
@Override
public void run() {
delegate.handleError(e);
}
});
}
enum ResponseType {
JSON_ARRAY,
JSON_OBJECT
}
/**
* Translate resource callbacks into request callbacks invoked on the provided
* executor.
* <p>
* Override <code>handleSuccess</code> to parse the body of the resource
* request and call the request callback. <code>handleSuccess</code> is
* invoked via the executor, so you don't need to delegate further.
*/
protected abstract class ResourceDelegate<T> extends BaseResourceDelegate {
protected void handleSuccess(final int status, HttpResponse response, final ExtendedJSONObject body) throws Exception {
throw new UnsupportedOperationException();
}
protected void handleSuccess(final int status, HttpResponse response, final JSONArray body) throws Exception {
throw new UnsupportedOperationException();
}
protected final RequestDelegate<T> delegate;
protected final byte[] tokenId;
protected final byte[] reqHMACKey;
protected final SkewHandler skewHandler;
protected final ResponseType responseType;
/**
* Create a delegate for an un-authenticated resource.
*/
public ResourceDelegate(final Resource resource, final RequestDelegate<T> delegate, ResponseType responseType) {
this(resource, delegate, responseType, null, null);
}
/**
* Create a delegate for a Hawk-authenticated resource.
* <p>
* Every Hawk request that encloses an entity (PATCH, POST, and PUT) will
* include the payload verification hash.
*/
public ResourceDelegate(final Resource resource, final RequestDelegate<T> delegate, ResponseType responseType, final byte[] tokenId, final byte[] reqHMACKey) {
super(resource);
this.delegate = delegate;
this.reqHMACKey = reqHMACKey;
this.tokenId = tokenId;
this.skewHandler = SkewHandler.getSkewHandlerForResource(resource);
this.responseType = responseType;
}
@Override
public AuthHeaderProvider getAuthHeaderProvider() {
if (tokenId != null && reqHMACKey != null) {
// We always include the payload verification hash for FxA Hawk-authenticated requests.
final boolean includePayloadVerificationHash = true;
return new HawkAuthHeaderProvider(Utils.byte2Hex(tokenId), reqHMACKey, includePayloadVerificationHash, skewHandler.getSkewInSeconds());
}
return super.getAuthHeaderProvider();
}
@Override
public String getUserAgent() {
return FxAccountConstants.USER_AGENT;
}
@Override
public void handleHttpResponse(HttpResponse response) {
try {
final int status = validateResponse(response);
skewHandler.updateSkew(response, now());
invokeHandleSuccess(status, response);
} catch (FxAccountClientRemoteException e) {
if (!skewHandler.updateSkew(response, now())) {
// If we couldn't update skew, but we got a failure, let's try clearing the skew.
skewHandler.resetSkew();
}
invokeHandleFailure(e);
}
}
protected void invokeHandleFailure(final FxAccountClientRemoteException e) {
executor.execute(new Runnable() {
@Override
public void run() {
delegate.handleFailure(e);
}
});
}
protected void invokeHandleSuccess(final int status, final HttpResponse response) {
executor.execute(new Runnable() {
@Override
public void run() {
try {
SyncResponse syncResponse = new SyncResponse(response);
if (responseType == ResponseType.JSON_ARRAY) {
JSONArray body = syncResponse.jsonArrayBody();
ResourceDelegate.this.handleSuccess(status, response, body);
} else {
ExtendedJSONObject body = syncResponse.jsonObjectBody();
ResourceDelegate.this.handleSuccess(status, response, body);
}
} catch (Exception e) {
delegate.handleError(e);
}
}
});
}
@Override
public void handleHttpProtocolException(final ClientProtocolException e) {
invokeHandleError(delegate, e);
}
@Override
public void handleHttpIOException(IOException e) {
invokeHandleError(delegate, e);
}
@Override
public void handleTransportException(GeneralSecurityException e) {
invokeHandleError(delegate, e);
}
@Override
public void addHeaders(HttpRequestBase request, DefaultHttpClient client) {
super.addHeaders(request, client);
// The basics.
final Locale locale = Locale.getDefault();
request.addHeader(HttpHeaders.ACCEPT_LANGUAGE, Locales.getLanguageTag(locale));
request.addHeader(HttpHeaders.ACCEPT, ACCEPT_HEADER);
}
}
protected <T> void post(BaseResource resource, final ExtendedJSONObject requestBody) {
if (requestBody == null) {
resource.post((HttpEntity) null);
} else {
resource.post(requestBody);
}
}
@SuppressWarnings("static-method")
public long now() {
return System.currentTimeMillis();
}
/**
* Intepret a response from the auth server.
* <p>
* Throw an appropriate exception on errors; otherwise, return the response's
* status code.
*
* @return response's HTTP status code.
* @throws FxAccountClientException
*/
public static int validateResponse(HttpResponse response) throws FxAccountClientRemoteException {
final int status = response.getStatusLine().getStatusCode();
if (status == 200) {
return status;
}
int code;
int errno;
String error;
String message;
String info;
ExtendedJSONObject body;
try {
body = new SyncStorageResponse(response).jsonObjectBody();
body.throwIfFieldsMissingOrMisTyped(requiredErrorStringFields, String.class);
body.throwIfFieldsMissingOrMisTyped(requiredErrorLongFields, Long.class);
code = body.getLong(JSON_KEY_CODE).intValue();
errno = body.getLong(JSON_KEY_ERRNO).intValue();
error = body.getString(JSON_KEY_ERROR);
message = body.getString(JSON_KEY_MESSAGE);
info = body.getString(JSON_KEY_INFO);
} catch (Exception e) {
throw new FxAccountClientMalformedResponseException(response);
}
throw new FxAccountClientRemoteException(response, code, errno, error, message, info, body);
}
/**
* Don't call this directly. Use <code>unbundleBody</code> instead.
*/
protected void unbundleBytes(byte[] bundleBytes, byte[] respHMACKey, byte[] respXORKey, byte[]... rest)
throws InvalidKeyException, NoSuchAlgorithmException, FxAccountClientException {
if (bundleBytes.length < 32) {
throw new IllegalArgumentException("input bundle must include HMAC");
}
int len = respXORKey.length;
if (bundleBytes.length != len + 32) {
throw new IllegalArgumentException("input bundle and XOR key with HMAC have different lengths");
}
int left = len;
for (byte[] array : rest) {
left -= array.length;
}
if (left != 0) {
throw new IllegalArgumentException("XOR key and total output arrays have different lengths");
}
byte[] ciphertext = new byte[len];
byte[] HMAC = new byte[32];
System.arraycopy(bundleBytes, 0, ciphertext, 0, len);
System.arraycopy(bundleBytes, len, HMAC, 0, 32);
Mac hmacHasher = HKDF.makeHMACHasher(respHMACKey);
byte[] computedHMAC = hmacHasher.doFinal(ciphertext);
if (!Arrays.equals(computedHMAC, HMAC)) {
throw new FxAccountClientException("Bad message HMAC");
}
int offset = 0;
for (byte[] array : rest) {
for (int i = 0; i < array.length; i++) {
array[i] = (byte) (respXORKey[offset + i] ^ ciphertext[offset + i]);
}
offset += array.length;
}
}
protected void unbundleBody(ExtendedJSONObject body, byte[] requestKey, byte[] ctxInfo, byte[]... rest) throws Exception {
int length = 0;
for (byte[] array : rest) {
length += array.length;
}
if (body == null) {
throw new FxAccountClientException("body must be non-null");
}
String bundle = body.getString("bundle");
if (bundle == null) {
throw new FxAccountClientException("bundle must be a non-null string");
}
byte[] bundleBytes = Utils.hex2Byte(bundle);
final byte[] respHMACKey = new byte[32];
final byte[] respXORKey = new byte[length];
HKDF.deriveMany(requestKey, new byte[0], ctxInfo, respHMACKey, respXORKey);
unbundleBytes(bundleBytes, respHMACKey, respXORKey, rest);
}
public void keys(byte[] keyFetchToken, final RequestDelegate<TwoKeys> delegate) {
final byte[] tokenId = new byte[32];
final byte[] reqHMACKey = new byte[32];
final byte[] requestKey = new byte[32];
try {
HKDF.deriveMany(keyFetchToken, new byte[0], FxAccountUtils.KW("keyFetchToken"), tokenId, reqHMACKey, requestKey);
} catch (Exception e) {
invokeHandleError(delegate, e);
return;
}
BaseResource resource;
try {
resource = getBaseResource("account/keys");
} catch (URISyntaxException | UnsupportedEncodingException e) {
invokeHandleError(delegate, e);
return;
}
resource.delegate = new ResourceDelegate<TwoKeys>(resource, delegate, ResponseType.JSON_OBJECT, tokenId, reqHMACKey) {
@Override
public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) throws Exception {
byte[] kA = new byte[FxAccountUtils.CRYPTO_KEY_LENGTH_BYTES];
byte[] wrapkB = new byte[FxAccountUtils.CRYPTO_KEY_LENGTH_BYTES];
unbundleBody(body, requestKey, FxAccountUtils.KW("account/keys"), kA, wrapkB);
delegate.handleSuccess(new TwoKeys(kA, wrapkB));
}
};
resource.get();
}
/**
* Thin container for account status response.
*/
public static class AccountStatusResponse {
public final boolean exists;
public AccountStatusResponse(boolean exists) {
this.exists = exists;
}
}
/**
* Query the account status of an account given a uid.
*
* @param uid to query.
* @param delegate to invoke callbacks.
*/
public void accountStatus(String uid, final RequestDelegate<AccountStatusResponse> delegate) {
final BaseResource resource;
try {
final Map<String, String> params = new HashMap<>(1);
params.put("uid", uid);
resource = getBaseResource("account/status", params);
} catch (URISyntaxException | UnsupportedEncodingException e) {
invokeHandleError(delegate, e);
return;
}
resource.delegate = new ResourceDelegate<AccountStatusResponse>(resource, delegate, ResponseType.JSON_OBJECT) {
@Override
public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) throws Exception {
boolean exists = body.getBoolean(JSON_KEY_EXISTS);
delegate.handleSuccess(new AccountStatusResponse(exists));
}
};
resource.get();
}
/**
* Thin container for recovery email status response.
*/
public static class RecoveryEmailStatusResponse {
public final String email;
public final boolean verified;
public RecoveryEmailStatusResponse(String email, boolean verified) {
this.email = email;
this.verified = verified;
}
}
/**
* Query the recovery email status of an account given a valid session token.
* <p>
* This API is a little odd: the auth server returns the email and
* verification state of the account that corresponds to the (opaque) session
* token. It might fail if the session token is unknown (or invalid, or
* revoked).
*
* @param sessionToken
* to query.
* @param delegate
* to invoke callbacks.
*/
public void recoveryEmailStatus(byte[] sessionToken, final RequestDelegate<RecoveryEmailStatusResponse> delegate) {
final byte[] tokenId = new byte[32];
final byte[] reqHMACKey = new byte[32];
final byte[] requestKey = new byte[32];
try {
HKDF.deriveMany(sessionToken, new byte[0], FxAccountUtils.KW("sessionToken"), tokenId, reqHMACKey, requestKey);
} catch (Exception e) {
invokeHandleError(delegate, e);
return;
}
BaseResource resource;
try {
resource = getBaseResource("recovery_email/status");
} catch (URISyntaxException | UnsupportedEncodingException e) {
invokeHandleError(delegate, e);
return;
}
resource.delegate = new ResourceDelegate<RecoveryEmailStatusResponse>(resource, delegate, ResponseType.JSON_OBJECT, tokenId, reqHMACKey) {
@Override
public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) throws Exception {
String[] requiredStringFields = new String[] { JSON_KEY_EMAIL };
body.throwIfFieldsMissingOrMisTyped(requiredStringFields, String.class);
String email = body.getString(JSON_KEY_EMAIL);
Boolean verified = body.getBoolean(JSON_KEY_VERIFIED);
delegate.handleSuccess(new RecoveryEmailStatusResponse(email, verified));
}
};
resource.get();
}
@SuppressWarnings("unchecked")
public void sign(final byte[] sessionToken, final ExtendedJSONObject publicKey, long durationInMilliseconds, final RequestDelegate<String> delegate) {
final ExtendedJSONObject body = new ExtendedJSONObject();
body.put("publicKey", publicKey);
body.put("duration", durationInMilliseconds);
final byte[] tokenId = new byte[32];
final byte[] reqHMACKey = new byte[32];
try {
HKDF.deriveMany(sessionToken, new byte[0], FxAccountUtils.KW("sessionToken"), tokenId, reqHMACKey);
} catch (Exception e) {
invokeHandleError(delegate, e);
return;
}
BaseResource resource;
try {
resource = getBaseResource("certificate/sign");
} catch (URISyntaxException | UnsupportedEncodingException e) {
invokeHandleError(delegate, e);
return;
}
resource.delegate = new ResourceDelegate<String>(resource, delegate, ResponseType.JSON_OBJECT, tokenId, reqHMACKey) {
@Override
public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) throws Exception {
String cert = body.getString("cert");
if (cert == null) {
delegate.handleError(new FxAccountClientException("cert must be a non-null string"));
return;
}
delegate.handleSuccess(cert);
}
};
post(resource, body);
}
protected static final String[] LOGIN_RESPONSE_REQUIRED_STRING_FIELDS = new String[] { JSON_KEY_UID, JSON_KEY_SESSIONTOKEN };
protected static final String[] LOGIN_RESPONSE_REQUIRED_STRING_FIELDS_KEYS = new String[] { JSON_KEY_UID, JSON_KEY_SESSIONTOKEN, JSON_KEY_KEYFETCHTOKEN, };
protected static final String[] LOGIN_RESPONSE_REQUIRED_BOOLEAN_FIELDS = new String[] { JSON_KEY_VERIFIED };
/**
* Thin container for login response.
* <p>
* The <code>remoteEmail</code> field is the email address as normalized by the
* server, and is <b>not necessarily</b> the email address delivered to the
* <code>login</code> or <code>create</code> call.
*/
public static class LoginResponse {
public final String remoteEmail;
public final String uid;
public final byte[] sessionToken;
public final boolean verified;
public final byte[] keyFetchToken;
public LoginResponse(String remoteEmail, String uid, boolean verified, byte[] sessionToken, byte[] keyFetchToken) {
this.remoteEmail = remoteEmail;
this.uid = uid;
this.verified = verified;
this.sessionToken = sessionToken;
this.keyFetchToken = keyFetchToken;
}
}
// Public for testing only; prefer login and loginAndGetKeys (without boolean parameter).
public void login(final byte[] emailUTF8, final byte[] quickStretchedPW, final boolean getKeys,
final Map<String, String> queryParameters,
final RequestDelegate<LoginResponse> delegate) {
final BaseResource resource;
final ExtendedJSONObject body;
try {
final String path = "account/login";
final Map<String, String> modifiedParameters = new HashMap<>();
if (queryParameters != null) {
modifiedParameters.putAll(queryParameters);
}
if (getKeys) {
modifiedParameters.put("keys", "true");
}
resource = getBaseResource(path, modifiedParameters);
body = new FxAccount20LoginDelegate(emailUTF8, quickStretchedPW).getCreateBody();
} catch (Exception e) {
invokeHandleError(delegate, e);
return;
}
resource.delegate = new ResourceDelegate<LoginResponse>(resource, delegate, ResponseType.JSON_OBJECT) {
@Override
public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) throws Exception {
final String[] requiredStringFields = getKeys ? LOGIN_RESPONSE_REQUIRED_STRING_FIELDS_KEYS : LOGIN_RESPONSE_REQUIRED_STRING_FIELDS;
body.throwIfFieldsMissingOrMisTyped(requiredStringFields, String.class);
final String[] requiredBooleanFields = LOGIN_RESPONSE_REQUIRED_BOOLEAN_FIELDS;
body.throwIfFieldsMissingOrMisTyped(requiredBooleanFields, Boolean.class);
String uid = body.getString(JSON_KEY_UID);
boolean verified = body.getBoolean(JSON_KEY_VERIFIED);
byte[] sessionToken = Utils.hex2Byte(body.getString(JSON_KEY_SESSIONTOKEN));
byte[] keyFetchToken = null;
if (getKeys) {
keyFetchToken = Utils.hex2Byte(body.getString(JSON_KEY_KEYFETCHTOKEN));
}
LoginResponse loginResponse = new LoginResponse(new String(emailUTF8, "UTF-8"), uid, verified, sessionToken, keyFetchToken);
delegate.handleSuccess(loginResponse);
}
};
post(resource, body);
}
public void createAccount(final byte[] emailUTF8, final byte[] quickStretchedPW,
final boolean getKeys,
final boolean preVerified,
final Map<String, String> queryParameters,
final RequestDelegate<LoginResponse> delegate) {
final BaseResource resource;
final ExtendedJSONObject body;
try {
final String path = "account/create";
final Map<String, String> modifiedParameters = new HashMap<>();
if (queryParameters != null) {
modifiedParameters.putAll(queryParameters);
}
if (getKeys) {
modifiedParameters.put("keys", "true");
}
resource = getBaseResource(path, modifiedParameters);
body = new FxAccount20CreateDelegate(emailUTF8, quickStretchedPW, preVerified).getCreateBody();
} catch (Exception e) {
invokeHandleError(delegate, e);
return;
}
// This is very similar to login, except verified is not required.
resource.delegate = new ResourceDelegate<LoginResponse>(resource, delegate, ResponseType.JSON_OBJECT) {
@Override
public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) throws Exception {
final String[] requiredStringFields = getKeys ? LOGIN_RESPONSE_REQUIRED_STRING_FIELDS_KEYS : LOGIN_RESPONSE_REQUIRED_STRING_FIELDS;
body.throwIfFieldsMissingOrMisTyped(requiredStringFields, String.class);
String uid = body.getString(JSON_KEY_UID);
boolean verified = false; // In production, we're definitely not verified immediately upon creation.
Boolean tempVerified = body.getBoolean(JSON_KEY_VERIFIED);
if (tempVerified != null) {
verified = tempVerified;
}
byte[] sessionToken = Utils.hex2Byte(body.getString(JSON_KEY_SESSIONTOKEN));
byte[] keyFetchToken = null;
if (getKeys) {
keyFetchToken = Utils.hex2Byte(body.getString(JSON_KEY_KEYFETCHTOKEN));
}
LoginResponse loginResponse = new LoginResponse(new String(emailUTF8, "UTF-8"), uid, verified, sessionToken, keyFetchToken);
delegate.handleSuccess(loginResponse);
}
};
post(resource, body);
}
/**
* We want users to be able to enter their email address case-insensitively.
* We stretch the password locally using the email address as a salt, to make
* dictionary attacks more expensive. This means that a client with a
* case-differing email address is unable to produce the correct
* authorization, even though it knows the password. In this case, the server
* returns the email that the account was created with, so that the client can
* re-stretch the password locally with the correct email salt. This version
* of <code>login</code> retries at most one time with a server provided email
* address.
* <p>
* Be aware that consumers will not see the initial error response from the
* server providing an alternate email (if there is one).
*
* @param emailUTF8
* user entered email address.
* @param stretcher
* delegate to stretch and re-stretch password.
* @param getKeys
* true if a <code>keyFetchToken</code> should be returned (in
* addition to the standard <code>sessionToken</code>).
* @param queryParameters
* @param delegate
* to invoke callbacks.
*/
public void login(final byte[] emailUTF8, final PasswordStretcher stretcher, final boolean getKeys,
final Map<String, String> queryParameters,
final RequestDelegate<LoginResponse> delegate) {
byte[] quickStretchedPW;
try {
FxAccountUtils.pii(LOG_TAG, "Trying user provided email: '" + new String(emailUTF8, "UTF-8") + "'" );
quickStretchedPW = stretcher.getQuickStretchedPW(emailUTF8);
} catch (Exception e) {
delegate.handleError(e);
return;
}
this.login(emailUTF8, quickStretchedPW, getKeys, queryParameters, new RequestDelegate<LoginResponse>() {
@Override
public void handleSuccess(LoginResponse result) {
delegate.handleSuccess(result);
}
@Override
public void handleError(Exception e) {
delegate.handleError(e);
}
@Override
public void handleFailure(FxAccountClientRemoteException e) {
String alternateEmail = e.body.getString(JSON_KEY_EMAIL);
if (!e.isBadEmailCase() || alternateEmail == null) {
delegate.handleFailure(e);
return;
};
Logger.info(LOG_TAG, "Server returned alternate email; retrying login with provided email.");
FxAccountUtils.pii(LOG_TAG, "Trying server provided email: '" + alternateEmail + "'" );
try {
// Nota bene: this is not recursive, since we call the fixed password
// signature here, which invokes a non-retrying version.
byte[] alternateEmailUTF8 = alternateEmail.getBytes("UTF-8");
byte[] alternateQuickStretchedPW = stretcher.getQuickStretchedPW(alternateEmailUTF8);
login(alternateEmailUTF8, alternateQuickStretchedPW, getKeys, queryParameters, delegate);
} catch (Exception innerException) {
delegate.handleError(innerException);
return;
}
}
});
}
/**
* Registers a device given a valid session token.
*
* @param sessionToken to query.
* @param delegate to invoke callbacks.
*/
@Override
public void registerOrUpdateDevice(byte[] sessionToken, FxAccountDevice device, RequestDelegate<FxAccountDevice> delegate) {
final byte[] tokenId = new byte[32];
final byte[] reqHMACKey = new byte[32];
final byte[] requestKey = new byte[32];
try {
HKDF.deriveMany(sessionToken, new byte[0], FxAccountUtils.KW("sessionToken"), tokenId, reqHMACKey, requestKey);
} catch (Exception e) {
invokeHandleError(delegate, e);
return;
}
final BaseResource resource;
final ExtendedJSONObject body;
try {
resource = getBaseResource("account/device");
body = device.toJson();
} catch (URISyntaxException | UnsupportedEncodingException e) {
invokeHandleError(delegate, e);
return;
}
resource.delegate = new ResourceDelegate<FxAccountDevice>(resource, delegate, ResponseType.JSON_OBJECT, tokenId, reqHMACKey) {
@Override
public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
try {
delegate.handleSuccess(FxAccountDevice.fromJson(body));
} catch (Exception e) {
delegate.handleError(e);
}
}
};
post(resource, body);
}
@Override
public void deviceList(byte[] sessionToken, RequestDelegate<FxAccountDevice[]> delegate) {
final byte[] tokenId = new byte[32];
final byte[] reqHMACKey = new byte[32];
final byte[] requestKey = new byte[32];
try {
HKDF.deriveMany(sessionToken, new byte[0], FxAccountUtils.KW("sessionToken"), tokenId, reqHMACKey, requestKey);
} catch (Exception e) {
invokeHandleError(delegate, e);
return;
}
final BaseResource resource;
try {
resource = getBaseResource("account/devices");
} catch (URISyntaxException | UnsupportedEncodingException e) {
invokeHandleError(delegate, e);
return;
}
resource.delegate = new ResourceDelegate<FxAccountDevice[]>(resource, delegate, ResponseType.JSON_ARRAY, tokenId, reqHMACKey) {
@Override
public void handleSuccess(int status, HttpResponse response, JSONArray devicesJson) {
try {
FxAccountDevice[] devices = new FxAccountDevice[devicesJson.size()];
for (int i = 0; i < devices.length; i++) {
ExtendedJSONObject deviceJson = new ExtendedJSONObject((JSONObject) devicesJson.get(i));
devices[i] = FxAccountDevice.fromJson(deviceJson);
}
delegate.handleSuccess(devices);
} catch (Exception e) {
delegate.handleError(e);
}
}
};
resource.get();
}
@Override
public void notifyDevices(@NonNull byte[] sessionToken, @NonNull List<String> deviceIds, ExtendedJSONObject payload, Long TTL, RequestDelegate<ExtendedJSONObject> delegate) {
final byte[] tokenId = new byte[32];
final byte[] reqHMACKey = new byte[32];
final byte[] requestKey = new byte[32];
try {
HKDF.deriveMany(sessionToken, new byte[0], FxAccountUtils.KW("sessionToken"), tokenId, reqHMACKey, requestKey);
} catch (Exception e) {
invokeHandleError(delegate, e);
return;
}
final BaseResource resource;
final ExtendedJSONObject body = createNotifyDevicesBody(deviceIds, payload, TTL);
try {
resource = getBaseResource("account/devices/notify");
} catch (URISyntaxException | UnsupportedEncodingException e) {
invokeHandleError(delegate, e);
return;
}
resource.delegate = new ResourceDelegate<ExtendedJSONObject>(resource, delegate, ResponseType.JSON_OBJECT, tokenId, reqHMACKey) {
@Override
public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
try {
delegate.handleSuccess(body);
} catch (Exception e) {
delegate.handleError(e);
}
}
};
post(resource, body);
}
@NonNull
@SuppressWarnings("unchecked")
private ExtendedJSONObject createNotifyDevicesBody(@NonNull List<String> deviceIds, ExtendedJSONObject payload, Long TTL) {
final ExtendedJSONObject body = new ExtendedJSONObject();
final JSONArray to = new JSONArray();
to.addAll(deviceIds);
body.put("to", to);
if (payload != null) {
body.put("payload", payload);
}
if (TTL != null) {
body.put("TTL", TTL);
}
return body;
}
}

View file

@ -1,133 +0,0 @@
/* 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/. */
package org.mozilla.gecko.background.fxa;
import org.mozilla.gecko.R;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.HTTPFailureException;
import org.mozilla.gecko.sync.net.SyncStorageResponse;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.HttpStatus;
/**
* From <a href="https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md">https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md</a>.
*/
public class FxAccountClientException extends Exception {
private static final long serialVersionUID = 7953459541558266597L;
public FxAccountClientException(String detailMessage) {
super(detailMessage);
}
public FxAccountClientException(Exception e) {
super(e);
}
public static class FxAccountClientRemoteException extends FxAccountClientException {
private static final long serialVersionUID = 2209313149952001097L;
public final HttpResponse response;
public final long httpStatusCode;
public final long apiErrorNumber;
public final String error;
public final String message;
public final String info;
public final ExtendedJSONObject body;
public FxAccountClientRemoteException(HttpResponse response, long httpStatusCode, long apiErrorNumber, String error, String message, String info, ExtendedJSONObject body) {
super(new HTTPFailureException(new SyncStorageResponse(response)));
if (body == null) {
throw new IllegalArgumentException("body must not be null");
}
this.response = response;
this.httpStatusCode = httpStatusCode;
this.apiErrorNumber = apiErrorNumber;
this.error = error;
this.message = message;
this.info = info;
this.body = body;
}
@Override
public String toString() {
return "<FxAccountClientRemoteException " + this.httpStatusCode + " [" + this.apiErrorNumber + "]: " + this.message + ">";
}
public boolean isInvalidAuthentication() {
return httpStatusCode == HttpStatus.SC_UNAUTHORIZED;
}
public boolean isAccountAlreadyExists() {
return apiErrorNumber == FxAccountRemoteError.ATTEMPT_TO_CREATE_AN_ACCOUNT_THAT_ALREADY_EXISTS;
}
public boolean isAccountDoesNotExist() {
return apiErrorNumber == FxAccountRemoteError.ATTEMPT_TO_ACCESS_AN_ACCOUNT_THAT_DOES_NOT_EXIST;
}
public boolean isBadPassword() {
return apiErrorNumber == FxAccountRemoteError.INCORRECT_PASSWORD;
}
public boolean isUnverified() {
return apiErrorNumber == FxAccountRemoteError.ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT;
}
public boolean isUpgradeRequired() {
return
apiErrorNumber == FxAccountRemoteError.ENDPOINT_IS_NO_LONGER_SUPPORTED ||
apiErrorNumber == FxAccountRemoteError.INCORRECT_LOGIN_METHOD_FOR_THIS_ACCOUNT ||
apiErrorNumber == FxAccountRemoteError.INCORRECT_KEY_RETRIEVAL_METHOD_FOR_THIS_ACCOUNT ||
apiErrorNumber == FxAccountRemoteError.INCORRECT_API_VERSION_FOR_THIS_ACCOUNT;
}
public boolean isTooManyRequests() {
return apiErrorNumber == FxAccountRemoteError.CLIENT_HAS_SENT_TOO_MANY_REQUESTS;
}
public boolean isServerUnavailable() {
return apiErrorNumber == FxAccountRemoteError.SERVICE_TEMPORARILY_UNAVAILABLE_DUE_TO_HIGH_LOAD;
}
public boolean isBadEmailCase() {
return apiErrorNumber == FxAccountRemoteError.INCORRECT_EMAIL_CASE;
}
public boolean isAccountLocked() {
return apiErrorNumber == FxAccountRemoteError.ACCOUNT_LOCKED;
}
public int getErrorMessageStringResource() {
if (isUpgradeRequired()) {
return R.string.fxaccount_remote_error_UPGRADE_REQUIRED;
} else if (isAccountAlreadyExists()) {
return R.string.fxaccount_remote_error_ATTEMPT_TO_CREATE_AN_ACCOUNT_THAT_ALREADY_EXISTS;
} else if (isAccountDoesNotExist()) {
return R.string.fxaccount_remote_error_ATTEMPT_TO_ACCESS_AN_ACCOUNT_THAT_DOES_NOT_EXIST;
} else if (isBadPassword()) {
return R.string.fxaccount_remote_error_INCORRECT_PASSWORD;
} else if (isUnverified()) {
return R.string.fxaccount_remote_error_ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT;
} else if (isTooManyRequests()) {
return R.string.fxaccount_remote_error_CLIENT_HAS_SENT_TOO_MANY_REQUESTS;
} else if (isServerUnavailable()) {
return R.string.fxaccount_remote_error_SERVICE_TEMPORARILY_UNAVAILABLE_TO_DUE_HIGH_LOAD;
} else if (isAccountLocked()) {
return R.string.fxaccount_remote_error_ACCOUNT_LOCKED;
} else {
return R.string.fxaccount_remote_error_UNKNOWN_ERROR;
}
}
}
public static class FxAccountClientMalformedResponseException extends FxAccountClientRemoteException {
private static final long serialVersionUID = 2209313149952001098L;
public FxAccountClientMalformedResponseException(HttpResponse response) {
super(response, 0, FxAccountRemoteError.UNKNOWN_ERROR, "Response malformed", "Response malformed", "Response malformed", new ExtendedJSONObject());
}
}
}

View file

@ -1,33 +0,0 @@
/* 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/. */
package org.mozilla.gecko.background.fxa;
public interface FxAccountRemoteError {
public static final int ATTEMPT_TO_CREATE_AN_ACCOUNT_THAT_ALREADY_EXISTS = 101;
public static final int ATTEMPT_TO_ACCESS_AN_ACCOUNT_THAT_DOES_NOT_EXIST = 102;
public static final int INCORRECT_PASSWORD = 103;
public static final int ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT = 104;
public static final int INVALID_VERIFICATION_CODE = 105;
public static final int REQUEST_BODY_WAS_NOT_VALID_JSON = 106;
public static final int REQUEST_BODY_CONTAINS_INVALID_PARAMETERS = 107;
public static final int REQUEST_BODY_MISSING_REQUIRED_PARAMETERS = 108;
public static final int INVALID_REQUEST_SIGNATURE = 109;
public static final int INVALID_AUTHENTICATION_TOKEN = 110;
public static final int INVALID_AUTHENTICATION_TIMESTAMP = 111;
public static final int CONTENT_LENGTH_HEADER_WAS_NOT_PROVIDED = 112;
public static final int REQUEST_BODY_TOO_LARGE = 113;
public static final int CLIENT_HAS_SENT_TOO_MANY_REQUESTS = 114;
public static final int INVALID_NONCE_IN_REQUEST_SIGNATURE = 115;
public static final int ENDPOINT_IS_NO_LONGER_SUPPORTED = 116;
public static final int INCORRECT_LOGIN_METHOD_FOR_THIS_ACCOUNT = 117;
public static final int INCORRECT_KEY_RETRIEVAL_METHOD_FOR_THIS_ACCOUNT = 118;
public static final int INCORRECT_API_VERSION_FOR_THIS_ACCOUNT = 119;
public static final int INCORRECT_EMAIL_CASE = 120;
public static final int ACCOUNT_LOCKED = 121;
public static final int UNKNOWN_DEVICE = 123;
public static final int DEVICE_SESSION_CONFLICT = 124;
public static final int SERVICE_TEMPORARILY_UNAVAILABLE_DUE_TO_HIGH_LOAD = 201;
public static final int UNKNOWN_ERROR = 999;
}

View file

@ -1,217 +0,0 @@
/* 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/. */
package org.mozilla.gecko.background.fxa;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.R;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.nativecode.NativeCrypto;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.crypto.HKDF;
import org.mozilla.gecko.sync.crypto.KeyBundle;
import org.mozilla.gecko.sync.crypto.PBKDF2;
import android.content.Context;
public class FxAccountUtils {
private static final String LOG_TAG = FxAccountUtils.class.getSimpleName();
public static final int SALT_LENGTH_BYTES = 32;
public static final int SALT_LENGTH_HEX = 2 * SALT_LENGTH_BYTES;
public static final int HASH_LENGTH_BYTES = 16;
public static final int HASH_LENGTH_HEX = 2 * HASH_LENGTH_BYTES;
public static final int CRYPTO_KEY_LENGTH_BYTES = 32;
public static final int CRYPTO_KEY_LENGTH_HEX = 2 * CRYPTO_KEY_LENGTH_BYTES;
public static final String KW_VERSION_STRING = "identity.mozilla.com/picl/v1/";
public static final int NUMBER_OF_QUICK_STRETCH_ROUNDS = 1000;
// For extra debugging. Not final so it can be changed from Fennec, or from
// an add-on.
public static boolean LOG_PERSONAL_INFORMATION = false;
public static void pii(String tag, String message) {
if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
Logger.info(tag, "$$FxA PII$$: " + message);
}
}
public static String bytes(String string) throws UnsupportedEncodingException {
return Utils.byte2Hex(string.getBytes("UTF-8"));
}
public static byte[] KW(String name) throws UnsupportedEncodingException {
return Utils.concatAll(
KW_VERSION_STRING.getBytes("UTF-8"),
name.getBytes("UTF-8"));
}
public static byte[] KWE(String name, byte[] emailUTF8) throws UnsupportedEncodingException {
return Utils.concatAll(
KW_VERSION_STRING.getBytes("UTF-8"),
name.getBytes("UTF-8"),
":".getBytes("UTF-8"),
emailUTF8);
}
/**
* Calculate the SRP verifier <tt>x</tt> value.
*/
public static BigInteger srpVerifierLowercaseX(byte[] emailUTF8, byte[] srpPWBytes, byte[] srpSaltBytes)
throws NoSuchAlgorithmException, UnsupportedEncodingException {
byte[] inner = Utils.sha256(Utils.concatAll(emailUTF8, ":".getBytes("UTF-8"), srpPWBytes));
byte[] outer = Utils.sha256(Utils.concatAll(srpSaltBytes, inner));
return new BigInteger(1, outer);
}
/**
* Calculate the SRP verifier <tt>v</tt> value.
*/
public static BigInteger srpVerifierLowercaseV(byte[] emailUTF8, byte[] srpPWBytes, byte[] srpSaltBytes, BigInteger g, BigInteger N)
throws NoSuchAlgorithmException, UnsupportedEncodingException {
BigInteger x = srpVerifierLowercaseX(emailUTF8, srpPWBytes, srpSaltBytes);
BigInteger v = g.modPow(x, N);
return v;
}
/**
* Format x modulo N in hexadecimal, using as many characters as N takes (in hexadecimal).
* @param x to format.
* @param N modulus.
* @return x modulo N in hexadecimal.
*/
public static String hexModN(BigInteger x, BigInteger N) {
int byteLength = (N.bitLength() + 7) / 8;
int hexLength = 2 * byteLength;
return Utils.byte2Hex(Utils.hex2Byte((x.mod(N)).toString(16), byteLength), hexLength);
}
/**
* The first engineering milestone of PICL (Profile-in-the-Cloud) was
* comprised of Sync 1.1 fronted by a Firefox Account. The sync key was
* generated from the Firefox Account password-derived kB value using this
* method.
*/
public static KeyBundle generateSyncKeyBundle(final byte[] kB) throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException {
byte[] encryptionKey = new byte[32];
byte[] hmacKey = new byte[32];
byte[] derived = HKDF.derive(kB, new byte[0], FxAccountUtils.KW("oldsync"), 2*32);
System.arraycopy(derived, 0*32, encryptionKey, 0, 1*32);
System.arraycopy(derived, 1*32, hmacKey, 0, 1*32);
return new KeyBundle(encryptionKey, hmacKey);
}
/**
* Firefox Accounts are password authenticated, but clients should not store
* the plain-text password for any amount of time. Equivalent, but slightly
* more secure, is the quickly client-side stretched password.
* <p>
* We separate this since multiple login-time operations want it, and the
* PBKDF2 operation is computationally expensive.
*/
public static byte[] generateQuickStretchedPW(byte[] emailUTF8, byte[] passwordUTF8) throws GeneralSecurityException, UnsupportedEncodingException {
byte[] S = FxAccountUtils.KWE("quickStretch", emailUTF8);
try {
return NativeCrypto.pbkdf2SHA256(passwordUTF8, S, NUMBER_OF_QUICK_STRETCH_ROUNDS, 32);
} catch (final LinkageError e) {
// This will throw UnsatisfiedLinkError (missing mozglue) the first time it is called, and
// ClassNotDefFoundError, for the uninitialized NativeCrypto class, each subsequent time this
// is called; LinkageError is their common ancestor.
Logger.warn(LOG_TAG, "Got throwable stretching password using native pbkdf2SHA256 " +
"implementation; ignoring and using Java implementation.", e);
return PBKDF2.pbkdf2SHA256(passwordUTF8, S, NUMBER_OF_QUICK_STRETCH_ROUNDS, 32);
}
}
/**
* The password-derived credential used to authenticate to the Firefox Account
* auth server.
*/
public static byte[] generateAuthPW(byte[] quickStretchedPW) throws GeneralSecurityException, UnsupportedEncodingException {
return HKDF.derive(quickStretchedPW, new byte[0], FxAccountUtils.KW("authPW"), 32);
}
/**
* The password-derived credential used to unwrap keys managed by the Firefox
* Account auth server.
*/
public static byte[] generateUnwrapBKey(byte[] quickStretchedPW) throws GeneralSecurityException, UnsupportedEncodingException {
return HKDF.derive(quickStretchedPW, new byte[0], FxAccountUtils.KW("unwrapBkey"), 32);
}
public static byte[] unwrapkB(byte[] unwrapkB, byte[] wrapkB) {
if (unwrapkB == null) {
throw new IllegalArgumentException("unwrapkB must not be null");
}
if (wrapkB == null) {
throw new IllegalArgumentException("wrapkB must not be null");
}
if (unwrapkB.length != CRYPTO_KEY_LENGTH_BYTES || wrapkB.length != CRYPTO_KEY_LENGTH_BYTES) {
throw new IllegalArgumentException("unwrapkB and wrapkB must be " + CRYPTO_KEY_LENGTH_BYTES + " bytes long");
}
byte[] kB = new byte[CRYPTO_KEY_LENGTH_BYTES];
for (int i = 0; i < wrapkB.length; i++) {
kB[i] = (byte) (wrapkB[i] ^ unwrapkB[i]);
}
return kB;
}
/**
* The token server accepts an X-Client-State header, which is the
* lowercase-hex-encoded first 16 bytes of the SHA-256 hash of the
* bytes of kB.
* @param kB a byte array, expected to be 32 bytes long.
* @return a 32-character string.
* @throws NoSuchAlgorithmException
*/
public static String computeClientState(byte[] kB) throws NoSuchAlgorithmException {
if (kB == null ||
kB.length != 32) {
throw new IllegalArgumentException("Unexpected kB.");
}
byte[] sha256 = Utils.sha256(kB);
byte[] truncated = new byte[16];
System.arraycopy(sha256, 0, truncated, 0, 16);
return Utils.byte2Hex(truncated); // This is automatically lowercase.
}
/**
* Given an endpoint, calculate the corresponding BrowserID audience.
* <p>
* This is the domain, in web parlance.
*
* @param serverURI endpoint.
* @return BrowserID audience.
* @throws URISyntaxException
*/
public static String getAudienceForURL(String serverURI) throws URISyntaxException {
URI uri = new URI(serverURI);
return new URI(uri.getScheme(), null, uri.getHost(), uri.getPort(), null, null, null).toString();
}
public static String defaultClientName(Context context) {
String name = AppConstants.MOZ_APP_DISPLAYNAME; // The display name is never translated.
// Change "Firefox Aurora" or similar into "Aurora".
if (name.contains("Aurora")) {
name = "Aurora";
} else if (name.contains("Beta")) {
name = "Beta";
} else if (name.contains("Nightly")) {
name = "Nightly";
}
return context.getResources().getString(R.string.sync_default_client_name, name, android.os.Build.MODEL);
}
}

View file

@ -1,12 +0,0 @@
/* 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/. */
package org.mozilla.gecko.background.fxa;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
public interface PasswordStretcher {
public byte[] getQuickStretchedPW(byte[] emailUTF8) throws UnsupportedEncodingException, GeneralSecurityException;
}

View file

@ -1,35 +0,0 @@
/* 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/. */
package org.mozilla.gecko.background.fxa;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.util.HashMap;
import java.util.Map;
import org.mozilla.gecko.sync.Utils;
public class QuickPasswordStretcher implements PasswordStretcher {
protected final String password;
protected final Map<String, String> cache = new HashMap<String, String>();
public QuickPasswordStretcher(String password) {
this.password = password;
}
@Override
public synchronized byte[] getQuickStretchedPW(byte[] emailUTF8) throws UnsupportedEncodingException, GeneralSecurityException {
if (emailUTF8 == null) {
throw new IllegalArgumentException("emailUTF8 must not be null");
}
String key = Utils.byte2Hex(emailUTF8);
if (!cache.containsKey(key)) {
byte[] value = FxAccountUtils.generateQuickStretchedPW(emailUTF8, password.getBytes("UTF-8"));
cache.put(key, Utils.byte2Hex(value));
return value;
}
return Utils.hex2Byte(cache.get(key));
}
}

View file

@ -1,111 +0,0 @@
/* 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/. */
package org.mozilla.gecko.background.fxa;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.sync.net.Resource;
import ch.boye.httpclientandroidlib.Header;
import ch.boye.httpclientandroidlib.HttpHeaders;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.impl.cookie.DateParseException;
import ch.boye.httpclientandroidlib.impl.cookie.DateUtils;
public class SkewHandler {
private static final String LOG_TAG = "SkewHandler";
protected volatile long skewMillis = 0L;
protected final String hostname;
private static final HashMap<String, SkewHandler> skewHandlers = new HashMap<String, SkewHandler>();
public static SkewHandler getSkewHandlerForResource(final Resource resource) {
return getSkewHandlerForHostname(resource.getHostname());
}
public static SkewHandler getSkewHandlerFromEndpointString(final String url) throws URISyntaxException {
if (url == null) {
throw new IllegalArgumentException("url must not be null.");
}
URI u = new URI(url);
return getSkewHandlerForHostname(u.getHost());
}
public static synchronized SkewHandler getSkewHandlerForHostname(final String hostname) {
SkewHandler handler = skewHandlers.get(hostname);
if (handler == null) {
handler = new SkewHandler(hostname);
skewHandlers.put(hostname, handler);
}
return handler;
}
public static synchronized void clearSkewHandlers() {
skewHandlers.clear();
}
public SkewHandler(final String hostname) {
this.hostname = hostname;
}
public boolean updateSkewFromServerMillis(long millis, long now) {
skewMillis = millis - now;
Logger.debug(LOG_TAG, "Updated skew: " + skewMillis + "ms for hostname " + this.hostname);
return true;
}
public boolean updateSkewFromHTTPDateString(String date, long now) {
try {
final long millis = DateUtils.parseDate(date).getTime();
return updateSkewFromServerMillis(millis, now);
} catch (DateParseException e) {
Logger.warn(LOG_TAG, "Unexpected: invalid Date header from " + this.hostname);
return false;
}
}
public boolean updateSkewFromDateHeader(Header header, long now) {
String date = header.getValue();
if (null == date) {
Logger.warn(LOG_TAG, "Unexpected: null Date header from " + this.hostname);
return false;
}
return updateSkewFromHTTPDateString(date, now);
}
/**
* Update our tracked skew value to account for the local clock differing from
* the server's.
*
* @param response
* the received HTTP response.
* @param now
* the current time in milliseconds.
* @return true if the skew value was updated, false otherwise.
*/
public boolean updateSkew(HttpResponse response, long now) {
Header header = response.getFirstHeader(HttpHeaders.DATE);
if (null == header) {
Logger.warn(LOG_TAG, "Unexpected: missing Date header from " + this.hostname);
return false;
}
return updateSkewFromDateHeader(header, now);
}
public long getSkewInMillis() {
return skewMillis;
}
public long getSkewInSeconds() {
return skewMillis / 1000;
}
public void resetSkew() {
skewMillis = 0L;
}
}

View file

@ -1,224 +0,0 @@
/* 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/. */
package org.mozilla.gecko.background.fxa.oauth;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.Locale;
import java.util.concurrent.Executor;
import org.mozilla.gecko.background.fxa.FxAccountClientException;
import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClientException.FxAccountAbstractClientMalformedResponseException;
import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClientException.FxAccountAbstractClientRemoteException;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.Locales;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.net.AuthHeaderProvider;
import org.mozilla.gecko.sync.net.BaseResource;
import org.mozilla.gecko.sync.net.BaseResourceDelegate;
import org.mozilla.gecko.sync.net.Resource;
import org.mozilla.gecko.sync.net.SyncResponse;
import org.mozilla.gecko.sync.net.SyncStorageResponse;
import ch.boye.httpclientandroidlib.HttpEntity;
import ch.boye.httpclientandroidlib.HttpHeaders;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.client.ClientProtocolException;
import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
public abstract class FxAccountAbstractClient {
protected static final String LOG_TAG = FxAccountAbstractClient.class.getSimpleName();
protected static final String ACCEPT_HEADER = "application/json;charset=utf-8";
protected static final String AUTHORIZATION_RESPONSE_TYPE = "token";
public static final String JSON_KEY_ERROR = "error";
public static final String JSON_KEY_MESSAGE = "message";
public static final String JSON_KEY_CODE = "code";
public static final String JSON_KEY_ERRNO = "errno";
protected static final String[] requiredErrorStringFields = { JSON_KEY_ERROR, JSON_KEY_MESSAGE };
protected static final String[] requiredErrorLongFields = { JSON_KEY_CODE, JSON_KEY_ERRNO };
/**
* The server's URI.
* <p>
* We assume throughout that this ends with a trailing slash (and guarantee as
* much in the constructor).
*/
protected final String serverURI;
protected final Executor executor;
public FxAccountAbstractClient(String serverURI, Executor executor) {
if (serverURI == null) {
throw new IllegalArgumentException("Must provide a server URI.");
}
if (executor == null) {
throw new IllegalArgumentException("Must provide a non-null executor.");
}
this.serverURI = serverURI.endsWith("/") ? serverURI : serverURI + "/";
if (!this.serverURI.endsWith("/")) {
throw new IllegalArgumentException("Constructed serverURI must end with a trailing slash: " + this.serverURI);
}
this.executor = executor;
}
/**
* Process a typed value extracted from a successful response (in an
* endpoint-dependent way).
*/
public interface RequestDelegate<T> {
public void handleError(Exception e);
public void handleFailure(FxAccountAbstractClientRemoteException e);
public void handleSuccess(T result);
}
/**
* Intepret a response from the auth server.
* <p>
* Throw an appropriate exception on errors; otherwise, return the response's
* status code.
*
* @return response's HTTP status code.
* @throws FxAccountClientException
*/
public static int validateResponse(HttpResponse response) throws FxAccountAbstractClientRemoteException {
final int status = response.getStatusLine().getStatusCode();
if (status == 200) {
return status;
}
int code;
int errno;
String error;
String message;
ExtendedJSONObject body;
try {
body = new SyncStorageResponse(response).jsonObjectBody();
body.throwIfFieldsMissingOrMisTyped(requiredErrorStringFields, String.class);
body.throwIfFieldsMissingOrMisTyped(requiredErrorLongFields, Long.class);
code = body.getLong(JSON_KEY_CODE).intValue();
errno = body.getLong(JSON_KEY_ERRNO).intValue();
error = body.getString(JSON_KEY_ERROR);
message = body.getString(JSON_KEY_MESSAGE);
} catch (Exception e) {
throw new FxAccountAbstractClientMalformedResponseException(response);
}
throw new FxAccountAbstractClientRemoteException(response, code, errno, error, message, body);
}
protected <T> void invokeHandleError(final RequestDelegate<T> delegate, final Exception e) {
executor.execute(new Runnable() {
@Override
public void run() {
delegate.handleError(e);
}
});
}
protected <T> void post(BaseResource resource, final ExtendedJSONObject requestBody, final RequestDelegate<T> delegate) {
try {
if (requestBody == null) {
resource.post((HttpEntity) null);
} else {
resource.post(requestBody);
}
} catch (Exception e) {
invokeHandleError(delegate, e);
return;
}
}
/**
* Translate resource callbacks into request callbacks invoked on the provided
* executor.
* <p>
* Override <code>handleSuccess</code> to parse the body of the resource
* request and call the request callback. <code>handleSuccess</code> is
* invoked via the executor, so you don't need to delegate further.
*/
protected abstract class ResourceDelegate<T> extends BaseResourceDelegate {
protected abstract void handleSuccess(final int status, HttpResponse response, final ExtendedJSONObject body);
protected final RequestDelegate<T> delegate;
/**
* Create a delegate for an un-authenticated resource.
*/
public ResourceDelegate(final Resource resource, final RequestDelegate<T> delegate) {
super(resource);
this.delegate = delegate;
}
@Override
public AuthHeaderProvider getAuthHeaderProvider() {
return super.getAuthHeaderProvider();
}
@Override
public String getUserAgent() {
return FxAccountConstants.USER_AGENT;
}
@Override
public void handleHttpResponse(HttpResponse response) {
try {
final int status = validateResponse(response);
invokeHandleSuccess(status, response);
} catch (FxAccountAbstractClientRemoteException e) {
invokeHandleFailure(e);
}
}
protected void invokeHandleFailure(final FxAccountAbstractClientRemoteException e) {
executor.execute(new Runnable() {
@Override
public void run() {
delegate.handleFailure(e);
}
});
}
protected void invokeHandleSuccess(final int status, final HttpResponse response) {
executor.execute(new Runnable() {
@Override
public void run() {
try {
ExtendedJSONObject body = new SyncResponse(response).jsonObjectBody();
ResourceDelegate.this.handleSuccess(status, response, body);
} catch (Exception e) {
delegate.handleError(e);
}
}
});
}
@Override
public void handleHttpProtocolException(final ClientProtocolException e) {
invokeHandleError(delegate, e);
}
@Override
public void handleHttpIOException(IOException e) {
invokeHandleError(delegate, e);
}
@Override
public void handleTransportException(GeneralSecurityException e) {
invokeHandleError(delegate, e);
}
@Override
public void addHeaders(HttpRequestBase request, DefaultHttpClient client) {
super.addHeaders(request, client);
// The basics.
final Locale locale = Locale.getDefault();
request.addHeader(HttpHeaders.ACCEPT_LANGUAGE, Locales.getLanguageTag(locale));
request.addHeader(HttpHeaders.ACCEPT, ACCEPT_HEADER);
}
}
}

View file

@ -1,68 +0,0 @@
/* 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/. */
package org.mozilla.gecko.background.fxa.oauth;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.HTTPFailureException;
import org.mozilla.gecko.sync.net.SyncStorageResponse;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.HttpStatus;
/**
* From <a href="https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md">https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md</a>.
*/
public class FxAccountAbstractClientException extends Exception {
private static final long serialVersionUID = 1953459541558266597L;
public FxAccountAbstractClientException(String detailMessage) {
super(detailMessage);
}
public FxAccountAbstractClientException(Exception e) {
super(e);
}
public static class FxAccountAbstractClientRemoteException extends FxAccountAbstractClientException {
private static final long serialVersionUID = 1209313149952001097L;
public final HttpResponse response;
public final long httpStatusCode;
public final long apiErrorNumber;
public final String error;
public final String message;
public final ExtendedJSONObject body;
public FxAccountAbstractClientRemoteException(HttpResponse response, long httpStatusCode, long apiErrorNumber, String error, String message, ExtendedJSONObject body) {
super(new HTTPFailureException(new SyncStorageResponse(response)));
if (body == null) {
throw new IllegalArgumentException("body must not be null");
}
this.response = response;
this.httpStatusCode = httpStatusCode;
this.apiErrorNumber = apiErrorNumber;
this.error = error;
this.message = message;
this.body = body;
}
@Override
public String toString() {
return "<FxAccountAbstractClientRemoteException " + this.httpStatusCode + " [" + this.apiErrorNumber + "]: " + this.message + ">";
}
public boolean isInvalidAuthentication() {
return this.httpStatusCode == HttpStatus.SC_UNAUTHORIZED;
}
}
public static class FxAccountAbstractClientMalformedResponseException extends FxAccountAbstractClientRemoteException {
private static final long serialVersionUID = 1209313149952001098L;
public FxAccountAbstractClientMalformedResponseException(HttpResponse response) {
super(response, 0, FxAccountOAuthRemoteError.UNKNOWN_ERROR, "Response malformed", "Response malformed", new ExtendedJSONObject());
}
}
}

View file

@ -1,129 +0,0 @@
/* 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/. */
package org.mozilla.gecko.background.fxa.oauth;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.concurrent.Executor;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.net.BaseResource;
import ch.boye.httpclientandroidlib.HttpResponse;
/**
* Talk to an fxa-oauth-server to get "implicitly granted" OAuth tokens.
* <p>
* To use this client, you will need a pre-allocated fxa-oauth-server
* "client_id" with special "implicit grant" permissions.
* <p>
* This client was written against the API documented at <a href="https://github.com/mozilla/fxa-oauth-server/blob/41538990df9e91158558ae5a8115194383ac3b05/docs/api.md">https://github.com/mozilla/fxa-oauth-server/blob/41538990df9e91158558ae5a8115194383ac3b05/docs/api.md</a>.
*/
public class FxAccountOAuthClient10 extends FxAccountAbstractClient {
protected static final String LOG_TAG = FxAccountOAuthClient10.class.getSimpleName();
protected static final String AUTHORIZATION_RESPONSE_TYPE = "token";
protected static final String JSON_KEY_ACCESS_TOKEN = "access_token";
protected static final String JSON_KEY_ASSERTION = "assertion";
protected static final String JSON_KEY_CLIENT_ID = "client_id";
protected static final String JSON_KEY_RESPONSE_TYPE = "response_type";
protected static final String JSON_KEY_SCOPE = "scope";
protected static final String JSON_KEY_STATE = "state";
protected static final String JSON_KEY_TOKEN = "token";
protected static final String JSON_KEY_TOKEN_TYPE = "token_type";
// access_token: A string that can be used for authorized requests to service providers.
// scope: A string of space-separated permissions that this token has. May differ from requested scopes, since user can deny permissions.
// token_type: A string representing the token type. Currently will always be "bearer".
protected static final String[] AUTHORIZATION_RESPONSE_REQUIRED_STRING_FIELDS = new String[] { JSON_KEY_ACCESS_TOKEN, JSON_KEY_SCOPE, JSON_KEY_TOKEN_TYPE };
public FxAccountOAuthClient10(String serverURI, Executor executor) {
super(serverURI, executor);
}
/**
* Thin container for an authorization response.
*/
public static class AuthorizationResponse {
public final String access_token;
public final String token_type;
public final String scope;
public AuthorizationResponse(String access_token, String token_type, String scope) {
this.access_token = access_token;
this.token_type = token_type;
this.scope = scope;
}
}
public void authorization(String client_id, String assertion, String state, String scope,
RequestDelegate<AuthorizationResponse> delegate) {
final BaseResource resource;
try {
resource = new BaseResource(new URI(serverURI + "authorization"));
} catch (URISyntaxException e) {
invokeHandleError(delegate, e);
return;
}
resource.delegate = new ResourceDelegate<AuthorizationResponse>(resource, delegate) {
@Override
public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
try {
body.throwIfFieldsMissingOrMisTyped(AUTHORIZATION_RESPONSE_REQUIRED_STRING_FIELDS, String.class);
String access_token = body.getString(JSON_KEY_ACCESS_TOKEN);
String token_type = body.getString(JSON_KEY_TOKEN_TYPE);
String scope = body.getString(JSON_KEY_SCOPE);
delegate.handleSuccess(new AuthorizationResponse(access_token, token_type, scope));
return;
} catch (Exception e) {
delegate.handleError(e);
return;
}
}
};
final ExtendedJSONObject requestBody = new ExtendedJSONObject();
requestBody.put(JSON_KEY_RESPONSE_TYPE, AUTHORIZATION_RESPONSE_TYPE);
requestBody.put(JSON_KEY_CLIENT_ID, client_id);
requestBody.put(JSON_KEY_ASSERTION, assertion);
if (scope != null) {
requestBody.put(JSON_KEY_SCOPE, scope);
}
if (state != null) {
requestBody.put(JSON_KEY_STATE, state);
}
post(resource, requestBody, delegate);
}
public void deleteToken(final String token, final RequestDelegate<Void> delegate) {
final BaseResource resource;
try {
resource = new BaseResource(new URI(serverURI + "destroy"));
} catch (URISyntaxException e) {
invokeHandleError(delegate, e);
return;
}
resource.delegate = new ResourceDelegate<Void>(resource, delegate) {
@Override
public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
try {
delegate.handleSuccess(null);
return;
} catch (Exception e) {
delegate.handleError(e);
return;
}
}
};
final ExtendedJSONObject requestBody = new ExtendedJSONObject();
requestBody.put(JSON_KEY_TOKEN, token);
post(resource, requestBody, delegate);
}
}

View file

@ -1,19 +0,0 @@
/* 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/. */
package org.mozilla.gecko.background.fxa.oauth;
public interface FxAccountOAuthRemoteError {
public static final int ATTEMPT_TO_CREATE_AN_ACCOUNT_THAT_ALREADY_EXISTS = 101;
public static final int UNKNOWN_CLIENT_ID = 101;
public static final int INCORRECT_CLIENT_SECRET = 102;
public static final int REDIRECT_URI_DOES_NOT_MATCH_REGISTERED_VALUE = 103;
public static final int INVALID_FXA_ASSERTION = 104;
public static final int UNKNOWN_CODE = 105;
public static final int INCORRECT_CODE = 106;
public static final int EXPIRED_CODE = 107;
public static final int INVALID_TOKEN = 108;
public static final int INVALID_REQUEST_PARAMETER = 109;
public static final int UNKNOWN_ERROR = 999;
}

View file

@ -1,59 +0,0 @@
/* 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/. */
package org.mozilla.gecko.background.fxa.profile;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.concurrent.Executor;
import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClient;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.net.AuthHeaderProvider;
import org.mozilla.gecko.sync.net.BaseResource;
import org.mozilla.gecko.sync.net.BearerAuthHeaderProvider;
import ch.boye.httpclientandroidlib.HttpResponse;
/**
* Talk to an fxa-profile-server to get profile information like name, age, gender, and avatar image.
* <p>
* This client was written against the API documented at <a href="https://github.com/mozilla/fxa-profile-server/blob/0c065619f5a2e867f813a343b4c67da3fe2c82a4/docs/API.md">https://github.com/mozilla/fxa-profile-server/blob/0c065619f5a2e867f813a343b4c67da3fe2c82a4/docs/API.md</a>.
*/
public class FxAccountProfileClient10 extends FxAccountAbstractClient {
public FxAccountProfileClient10(String serverURI, Executor executor) {
super(serverURI, executor);
}
public void profile(final String token, RequestDelegate<ExtendedJSONObject> delegate) {
BaseResource resource;
try {
resource = new BaseResource(new URI(serverURI + "profile"));
} catch (URISyntaxException e) {
invokeHandleError(delegate, e);
return;
}
resource.delegate = new ResourceDelegate<ExtendedJSONObject>(resource, delegate) {
@Override
public AuthHeaderProvider getAuthHeaderProvider() {
return new BearerAuthHeaderProvider(token);
}
@Override
public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
try {
delegate.handleSuccess(body);
return;
} catch (Exception e) {
delegate.handleError(e);
return;
}
}
};
resource.get();
}
}

View file

@ -1,60 +0,0 @@
/* 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/. */
package org.mozilla.gecko.background.nativecode;
import java.security.GeneralSecurityException;
import org.mozilla.gecko.annotation.RobocopTarget;
import org.mozilla.gecko.AppConstants;
import android.util.Log;
@RobocopTarget
public class NativeCrypto {
static {
try {
System.loadLibrary("mozglue");
} catch (UnsatisfiedLinkError e) {
Log.wtf("NativeCrypto", "Couldn't load mozglue. Trying /data/app-lib path.");
try {
System.load("/data/app-lib/" + AppConstants.ANDROID_PACKAGE_NAME + "/libmozglue.so");
} catch (Throwable ee) {
try {
Log.wtf("NativeCrypto", "Couldn't load mozglue: " + ee + ". Trying /data/data path.");
System.load("/data/data/" + AppConstants.ANDROID_PACKAGE_NAME + "/lib/libmozglue.so");
} catch (UnsatisfiedLinkError eee) {
Log.wtf("NativeCrypto", "Failed every attempt to load mozglue. Giving up.");
throw new RuntimeException("Unable to load mozglue", eee);
}
}
}
}
/**
* Wrapper to perform PBKDF2-HMAC-SHA-256 in native code.
*/
public native static byte[] pbkdf2SHA256(byte[] password, byte[] salt, int c, int dkLen)
throws GeneralSecurityException;
/**
* Wrapper to perform SHA-1 in native code.
*/
public native static byte[] sha1(byte[] str);
/**
* Wrapper to perform SHA-256 init in native code. Returns a SHA-256 context.
*/
public native static byte[] sha256init();
/**
* Wrapper to update a SHA-256 context in native code.
*/
public native static void sha256update(byte[] ctx, byte[] str, int len);
/**
* Wrapper to finalize a SHA-256 context in native code. Returns digest.
*/
public native static byte[] sha256finalize(byte[] ctx);
}

View file

@ -1,326 +0,0 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mozilla.gecko.background.preferences;
import org.mozilla.gecko.R;
import org.mozilla.gecko.util.WeakReferenceHandler;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.preference.Preference;
import android.preference.PreferenceGroup;
import android.preference.PreferenceManager;
import android.preference.PreferenceScreen;
import android.support.v4.app.Fragment;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnKeyListener;
import android.view.ViewGroup;
import android.widget.ListView;
public abstract class PreferenceFragment extends Fragment implements PreferenceManagerCompat.OnPreferenceTreeClickListener {
private static final String PREFERENCES_TAG = "android:preferences";
private PreferenceManager mPreferenceManager;
private ListView mList;
private boolean mHavePrefs;
private boolean mInitDone;
/**
* The starting request code given out to preference framework.
*/
private static final int FIRST_REQUEST_CODE = 100;
private static final int MSG_BIND_PREFERENCES = 1;
private static class PreferenceFragmentHandler extends WeakReferenceHandler<PreferenceFragment> {
public PreferenceFragmentHandler(final PreferenceFragment that) {
super(that);
}
@Override
public void handleMessage(Message msg) {
final PreferenceFragment that = mTarget.get();
if (that == null) {
return;
}
switch (msg.what) {
case MSG_BIND_PREFERENCES:
that.bindPreferences();
break;
}
}
}
private final Handler mHandler = new PreferenceFragmentHandler(this);
final private Runnable mRequestFocus = new Runnable() {
@Override
public void run() {
mList.focusableViewAvailable(mList);
}
};
/**
* Interface that PreferenceFragment's containing activity should
* implement to be able to process preference items that wish to
* switch to a new fragment.
*/
public interface OnPreferenceStartFragmentCallback {
/**
* Called when the user has clicked on a Preference that has
* a fragment class name associated with it. The implementation
* to should instantiate and switch to an instance of the given
* fragment.
*/
boolean onPreferenceStartFragment(PreferenceFragment caller, Preference pref);
}
@Override
public void onCreate(Bundle paramBundle) {
super.onCreate(paramBundle);
mPreferenceManager = PreferenceManagerCompat.newInstance(getActivity(), FIRST_REQUEST_CODE);
PreferenceManagerCompat.setFragment(mPreferenceManager, this);
}
@Override
public View onCreateView(LayoutInflater paramLayoutInflater, ViewGroup paramViewGroup, Bundle paramBundle) {
return paramLayoutInflater.inflate(R.layout.fxaccount_preference_list_fragment, paramViewGroup,
false);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (mHavePrefs) {
bindPreferences();
}
mInitDone = true;
if (savedInstanceState != null) {
Bundle container = savedInstanceState.getBundle(PREFERENCES_TAG);
if (container != null) {
final PreferenceScreen preferenceScreen = getPreferenceScreen();
if (preferenceScreen != null) {
preferenceScreen.restoreHierarchyState(container);
}
}
}
}
@Override
public void onStart() {
super.onStart();
PreferenceManagerCompat.setOnPreferenceTreeClickListener(mPreferenceManager, this);
}
@Override
public void onStop() {
super.onStop();
PreferenceManagerCompat.dispatchActivityStop(mPreferenceManager);
PreferenceManagerCompat.setOnPreferenceTreeClickListener(mPreferenceManager, null);
}
@Override
public void onDestroyView() {
mList = null;
mHandler.removeCallbacks(mRequestFocus);
mHandler.removeMessages(MSG_BIND_PREFERENCES);
super.onDestroyView();
}
@Override
public void onDestroy() {
super.onDestroy();
PreferenceManagerCompat.dispatchActivityDestroy(mPreferenceManager);
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
final PreferenceScreen preferenceScreen = getPreferenceScreen();
if (preferenceScreen != null) {
Bundle container = new Bundle();
preferenceScreen.saveHierarchyState(container);
outState.putBundle(PREFERENCES_TAG, container);
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
PreferenceManagerCompat.dispatchActivityResult(mPreferenceManager, requestCode, resultCode, data);
}
/**
* Returns the {@link PreferenceManager} used by this fragment.
* @return The {@link PreferenceManager}.
*/
public PreferenceManager getPreferenceManager() {
return mPreferenceManager;
}
/**
* Sets the root of the preference hierarchy that this fragment is showing.
*
* @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy.
*/
public void setPreferenceScreen(PreferenceScreen preferenceScreen) {
if (PreferenceManagerCompat.setPreferences(mPreferenceManager, preferenceScreen) && preferenceScreen != null) {
mHavePrefs = true;
if (mInitDone) {
postBindPreferences();
}
}
}
/**
* Gets the root of the preference hierarchy that this fragment is showing.
*
* @return The {@link PreferenceScreen} that is the root of the preference
* hierarchy.
*/
public PreferenceScreen getPreferenceScreen() {
return PreferenceManagerCompat.getPreferenceScreen(mPreferenceManager);
}
/**
* Adds preferences from activities that match the given {@link Intent}.
*
* @param intent The {@link Intent} to query activities.
*/
public void addPreferencesFromIntent(Intent intent) {
requirePreferenceManager();
setPreferenceScreen(PreferenceManagerCompat.inflateFromIntent(mPreferenceManager, intent, getPreferenceScreen()));
}
/**
* Inflates the given XML resource and adds the preference hierarchy to the current
* preference hierarchy.
*
* @param preferencesResId The XML resource ID to inflate.
*/
public void addPreferencesFromResource(int preferencesResId) {
requirePreferenceManager();
setPreferenceScreen(PreferenceManagerCompat.inflateFromResource(mPreferenceManager, getActivity(),
preferencesResId, getPreferenceScreen()));
}
/**
* {@inheritDoc}
*/
@Override
public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen,
Preference preference) {
//if (preference.getFragment() != null &&
if (
getActivity() instanceof OnPreferenceStartFragmentCallback) {
return ((OnPreferenceStartFragmentCallback)getActivity()).onPreferenceStartFragment(
this, preference);
}
return false;
}
/**
* Finds a {@link Preference} based on its key.
*
* @param key The key of the preference to retrieve.
* @return The {@link Preference} with the key, or null.
* @see PreferenceGroup#findPreference(CharSequence)
*/
public Preference findPreference(CharSequence key) {
if (mPreferenceManager == null) {
return null;
}
return mPreferenceManager.findPreference(key);
}
private void requirePreferenceManager() {
if (mPreferenceManager == null) {
throw new RuntimeException("This should be called after super.onCreate.");
}
}
private void postBindPreferences() {
if (mHandler.hasMessages(MSG_BIND_PREFERENCES)) return;
mHandler.obtainMessage(MSG_BIND_PREFERENCES).sendToTarget();
}
private void bindPreferences() {
final PreferenceScreen preferenceScreen = getPreferenceScreen();
if (preferenceScreen != null) {
preferenceScreen.bind(getListView());
}
}
public ListView getListView() {
ensureList();
return mList;
}
private void ensureList() {
if (mList != null) {
return;
}
View root = getView();
if (root == null) {
throw new IllegalStateException("Content view not yet created");
}
View rawListView = root.findViewById(android.R.id.list);
if (!(rawListView instanceof ListView)) {
throw new RuntimeException(
"Content has view with id attribute 'android.R.id.list' "
+ "that is not a ListView class");
}
mList = (ListView)rawListView;
if (mList == null) {
throw new RuntimeException(
"Your content must have a ListView whose id attribute is " +
"'android.R.id.list'");
}
mList.setOnKeyListener(mListOnKeyListener);
mHandler.post(mRequestFocus);
}
private final OnKeyListener mListOnKeyListener = new OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
Object selectedItem = mList.getSelectedItem();
if (selectedItem instanceof Preference) {
@SuppressWarnings("unused")
View selectedView = mList.getSelectedView();
//return ((Preference)selectedItem).onKey(
// selectedView, keyCode, event);
return false;
}
return false;
}
};
}

View file

@ -1,226 +0,0 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mozilla.gecko.background.preferences;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.preference.Preference;
import android.preference.PreferenceManager;
import android.preference.PreferenceScreen;
import android.util.Log;
public class PreferenceManagerCompat {
private static final String TAG = PreferenceManagerCompat.class.getSimpleName();
/**
* Interface definition for a callback to be invoked when a {@link Preference} in the hierarchy
* rooted at this {@link PreferenceScreen} is clicked.
*/
interface OnPreferenceTreeClickListener {
/**
* Called when a preference in the tree rooted at this {@link PreferenceScreen} has been
* clicked.
*
* @param preferenceScreen The {@link PreferenceScreen} that the preference is located in.
* @param preference The preference that was clicked.
*
* @return Whether the click was handled.
*/
boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference);
}
static PreferenceManager newInstance(Activity activity, int firstRequestCode) {
try {
Constructor<PreferenceManager> c = PreferenceManager.class.getDeclaredConstructor(Activity.class, int.class);
c.setAccessible(true);
return c.newInstance(activity, firstRequestCode);
} catch (Exception e) {
Log.w(TAG, "Couldn't call constructor PreferenceManager by reflection", e);
}
return null;
}
/**
* Sets the owning preference fragment
*/
static void setFragment(PreferenceManager manager, PreferenceFragment fragment) {
// stub
}
/**
* Sets the callback to be invoked when a {@link Preference} in the hierarchy rooted at this
* {@link PreferenceManager} is clicked.
*
* @param listener The callback to be invoked.
*/
static void setOnPreferenceTreeClickListener(PreferenceManager manager, final OnPreferenceTreeClickListener listener) {
try {
Field onPreferenceTreeClickListener = PreferenceManager.class.getDeclaredField("mOnPreferenceTreeClickListener");
onPreferenceTreeClickListener.setAccessible(true);
if (listener != null) {
Object proxy = Proxy.newProxyInstance(
onPreferenceTreeClickListener.getType().getClassLoader(),
new Class<?>[] { onPreferenceTreeClickListener.getType() },
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) {
if (method.getName().equals("onPreferenceTreeClick")) {
return listener.onPreferenceTreeClick((PreferenceScreen) args[0], (Preference) args[1]);
} else {
return null;
}
}
});
onPreferenceTreeClickListener.set(manager, proxy);
} else {
onPreferenceTreeClickListener.set(manager, null);
}
} catch (Exception e) {
Log.w(TAG, "Couldn't set PreferenceManager.mOnPreferenceTreeClickListener by reflection", e);
}
}
/**
* Inflates a preference hierarchy from the preference hierarchies of {@link Activity Activities}
* that match the given {@link Intent}. An {@link Activity} defines its preference hierarchy with
* meta-data using the {@link #METADATA_KEY_PREFERENCES} key.
* <p/>
* If a preference hierarchy is given, the new preference hierarchies will be merged in.
*
* @param queryIntent The intent to match activities.
* @param rootPreferences Optional existing hierarchy to merge the new hierarchies into.
*
* @return The root hierarchy (if one was not provided, the new hierarchy's root).
*/
static PreferenceScreen inflateFromIntent(PreferenceManager manager, Intent intent, PreferenceScreen screen) {
try {
Method m = PreferenceManager.class.getDeclaredMethod("inflateFromIntent", Intent.class, PreferenceScreen.class);
m.setAccessible(true);
PreferenceScreen prefScreen = (PreferenceScreen) m.invoke(manager, intent, screen);
return prefScreen;
} catch (Exception e) {
Log.w(TAG, "Couldn't call PreferenceManager.inflateFromIntent by reflection", e);
}
return null;
}
/**
* Inflates a preference hierarchy from XML. If a preference hierarchy is given, the new
* preference hierarchies will be merged in.
*
* @param context The context of the resource.
* @param resId The resource ID of the XML to inflate.
* @param rootPreferences Optional existing hierarchy to merge the new hierarchies into.
*
* @return The root hierarchy (if one was not provided, the new hierarchy's root).
*
* @hide
*/
static PreferenceScreen inflateFromResource(PreferenceManager manager, Activity activity, int resId, PreferenceScreen screen) {
try {
Method m = PreferenceManager.class.getDeclaredMethod("inflateFromResource", Context.class, int.class, PreferenceScreen.class);
m.setAccessible(true);
PreferenceScreen prefScreen = (PreferenceScreen) m.invoke(manager, activity, resId, screen);
return prefScreen;
} catch (Exception e) {
Log.w(TAG, "Couldn't call PreferenceManager.inflateFromResource by reflection", e);
}
return null;
}
/**
* Returns the root of the preference hierarchy managed by this class.
*
* @return The {@link PreferenceScreen} object that is at the root of the hierarchy.
*/
static PreferenceScreen getPreferenceScreen(PreferenceManager manager) {
try {
Method m = PreferenceManager.class.getDeclaredMethod("getPreferenceScreen");
m.setAccessible(true);
return (PreferenceScreen) m.invoke(manager);
} catch (Exception e) {
Log.w(TAG, "Couldn't call PreferenceManager.getPreferenceScreen by reflection", e);
}
return null;
}
/**
* Called by the {@link PreferenceManager} to dispatch a subactivity result.
*/
static void dispatchActivityResult(PreferenceManager manager, int requestCode, int resultCode, Intent data) {
try {
Method m = PreferenceManager.class.getDeclaredMethod("dispatchActivityResult", int.class, int.class, Intent.class);
m.setAccessible(true);
m.invoke(manager, requestCode, resultCode, data);
} catch (Exception e) {
Log.w(TAG, "Couldn't call PreferenceManager.dispatchActivityResult by reflection", e);
}
}
/**
* Called by the {@link PreferenceManager} to dispatch the activity stop event.
*/
static void dispatchActivityStop(PreferenceManager manager) {
try {
Method m = PreferenceManager.class.getDeclaredMethod("dispatchActivityStop");
m.setAccessible(true);
m.invoke(manager);
} catch (Exception e) {
Log.w(TAG, "Couldn't call PreferenceManager.dispatchActivityStop by reflection", e);
}
}
/**
* Called by the {@link PreferenceManager} to dispatch the activity destroy event.
*/
static void dispatchActivityDestroy(PreferenceManager manager) {
try {
Method m = PreferenceManager.class.getDeclaredMethod("dispatchActivityDestroy");
m.setAccessible(true);
m.invoke(manager);
} catch (Exception e) {
Log.w(TAG, "Couldn't call PreferenceManager.dispatchActivityDestroy by reflection", e);
}
}
/**
* Sets the root of the preference hierarchy.
*
* @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy.
*
* @return Whether the {@link PreferenceScreen} given is different than the previous.
*/
static boolean setPreferences(PreferenceManager manager, PreferenceScreen screen) {
try {
Method m = PreferenceManager.class.getDeclaredMethod("setPreferences", PreferenceScreen.class);
m.setAccessible(true);
return ((Boolean) m.invoke(manager, screen));
} catch (Exception e) {
Log.w(TAG, "Couldn't call PreferenceManager.setPreferences by reflection", e);
}
return false;
}
}

View file

@ -1,82 +0,0 @@
/* 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/. */
package org.mozilla.gecko.browserid;
/**
* Java produces signature in ASN.1 format. Here's some hard-coded encoding and decoding
* code, courtesy of a comment in
* <a href="http://stackoverflow.com/questions/10921733/how-sign-method-of-the-digital-signature-combines-the-r-s-values-in-to-array">http://stackoverflow.com/questions/10921733/how-sign-method-of-the-digital-signature-combines-the-r-s-values-in-to-array</a>.
*/
public class ASNUtils {
/**
* Decode two short arrays from ASN.1 bytes.
* @param input to extract.
* @return length 2 array of byte arrays.
*/
public static byte[][] decodeTwoArraysFromASN1(byte[] input) throws IllegalArgumentException {
if (input == null) {
throw new IllegalArgumentException("input must not be null");
}
if (input.length <= 3)
throw new IllegalArgumentException("bad length");
if (input[0] != 0x30)
throw new IllegalArgumentException("bad encoding");
if ((input[1] & ((byte) 0x80)) != 0)
throw new IllegalArgumentException("bad length encoding");
if (input[2] != 0x02)
throw new IllegalArgumentException("bad encoding");
if ((input[3] & ((byte) 0x80)) != 0)
throw new IllegalArgumentException("bad length encoding");
byte rLength = input[3];
if (input.length <= 5 + rLength)
throw new IllegalArgumentException("bad length");
if (input[4 + rLength] != 0x02)
throw new IllegalArgumentException("bad encoding");
if ((input[5 + rLength] & (byte) 0x80) !=0)
throw new IllegalArgumentException("bad length encoding");
byte sLength = input[5 + rLength];
if (input.length != 6 + sLength + rLength)
throw new IllegalArgumentException("bad length");
byte[] rArr = new byte[rLength];
byte[] sArr = new byte[sLength];
System.arraycopy(input, 4, rArr, 0, rLength);
System.arraycopy(input, 6 + rLength, sArr, 0, sLength);
return new byte[][] { rArr, sArr };
}
/**
* Encode two short arrays into ASN.1 bytes.
* @param first array to encode.
* @param second array to encode.
* @return array.
*/
public static byte[] encodeTwoArraysToASN1(byte[] first, byte[] second) throws IllegalArgumentException {
if (first == null) {
throw new IllegalArgumentException("first must not be null");
}
if (second == null) {
throw new IllegalArgumentException("second must not be null");
}
byte[] output = new byte[6 + first.length + second.length];
output[0] = 0x30;
if (4 + first.length + second.length > 255)
throw new IllegalArgumentException("bad length");
output[1] = (byte) (4 + first.length + second.length);
if ((output[1] & ((byte) 0x80)) != 0)
throw new IllegalArgumentException("bad length encoding");
output[2] = 0x02;
output[3] = (byte) first.length;
if ((output[3] & ((byte) 0x80)) != 0)
throw new IllegalArgumentException("bad length encoding");
System.arraycopy(first, 0, output, 4, first.length);
output[4 + first.length] = 0x02;
output[5 + first.length] = (byte) second.length;
if ((output[5 + first.length] & ((byte) 0x80)) != 0)
throw new IllegalArgumentException("bad length encoding");
System.arraycopy(second, 0, output, 6 + first.length, second.length);
return output;
}
}

View file

@ -1,35 +0,0 @@
/* 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/. */
package org.mozilla.gecko.browserid;
import org.mozilla.gecko.sync.ExtendedJSONObject;
public class BrowserIDKeyPair {
public static final String JSON_KEY_PRIVATEKEY = "privateKey";
public static final String JSON_KEY_PUBLICKEY = "publicKey";
protected final SigningPrivateKey privateKey;
protected final VerifyingPublicKey publicKey;
public BrowserIDKeyPair(SigningPrivateKey privateKey, VerifyingPublicKey publicKey) {
this.privateKey = privateKey;
this.publicKey = publicKey;
}
public SigningPrivateKey getPrivate() {
return this.privateKey;
}
public VerifyingPublicKey getPublic() {
return this.publicKey;
}
public ExtendedJSONObject toJSONObject() {
ExtendedJSONObject o = new ExtendedJSONObject();
o.put(JSON_KEY_PRIVATEKEY, privateKey.toJSONObject());
o.put(JSON_KEY_PUBLICKEY, publicKey.toJSONObject());
return o;
}
}

View file

@ -1,255 +0,0 @@
/* 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/. */
package org.mozilla.gecko.browserid;
import android.annotation.SuppressLint;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.NonObjectJSONException;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.util.PRNGFixes;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
import java.security.interfaces.DSAParams;
import java.security.interfaces.DSAPrivateKey;
import java.security.interfaces.DSAPublicKey;
import java.security.spec.DSAPrivateKeySpec;
import java.security.spec.DSAPublicKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
public class DSACryptoImplementation {
private static final String LOG_TAG = DSACryptoImplementation.class.getSimpleName();
public static final String SIGNATURE_ALGORITHM = "SHA1withDSA";
public static final int SIGNATURE_LENGTH_BYTES = 40; // DSA signatures are always 40 bytes long.
/**
* Parameters are serialized as hex strings. Hex-versus-decimal was
* reverse-engineered from what the Persona public verifier accepted. We
* expect to follow the JOSE/JWT spec as it solidifies, and that will probably
* mean unifying this base.
*/
protected static final int SERIALIZATION_BASE = 16;
protected static class DSAVerifyingPublicKey implements VerifyingPublicKey {
protected final DSAPublicKey publicKey;
public DSAVerifyingPublicKey(DSAPublicKey publicKey) {
this.publicKey = publicKey;
}
/**
* Serialize to a JSON object.
* <p>
* Parameters are serialized as hex strings. Hex-versus-decimal was
* reverse-engineered from what the Persona public verifier accepted.
*/
@Override
public ExtendedJSONObject toJSONObject() {
DSAParams params = publicKey.getParams();
ExtendedJSONObject o = new ExtendedJSONObject();
o.put("algorithm", "DS");
o.put("y", publicKey.getY().toString(SERIALIZATION_BASE));
o.put("g", params.getG().toString(SERIALIZATION_BASE));
o.put("p", params.getP().toString(SERIALIZATION_BASE));
o.put("q", params.getQ().toString(SERIALIZATION_BASE));
return o;
}
@Override
public boolean verifyMessage(byte[] bytes, byte[] signature)
throws GeneralSecurityException {
if (bytes == null) {
throw new IllegalArgumentException("bytes must not be null");
}
if (signature == null) {
throw new IllegalArgumentException("signature must not be null");
}
if (signature.length != SIGNATURE_LENGTH_BYTES) {
return false;
}
byte[] first = new byte[signature.length / 2];
byte[] second = new byte[signature.length / 2];
System.arraycopy(signature, 0, first, 0, first.length);
System.arraycopy(signature, first.length, second, 0, second.length);
BigInteger r = new BigInteger(Utils.byte2Hex(first), 16);
BigInteger s = new BigInteger(Utils.byte2Hex(second), 16);
// This is awful, but encoding an extra 0 byte works better on devices.
byte[] encoded = ASNUtils.encodeTwoArraysToASN1(
Utils.hex2Byte(r.toString(16), 1 + SIGNATURE_LENGTH_BYTES / 2),
Utils.hex2Byte(s.toString(16), 1 + SIGNATURE_LENGTH_BYTES / 2));
final Signature signer = Signature.getInstance(SIGNATURE_ALGORITHM);
signer.initVerify(publicKey);
signer.update(bytes);
return signer.verify(encoded);
}
}
protected static class DSASigningPrivateKey implements SigningPrivateKey {
protected final DSAPrivateKey privateKey;
public DSASigningPrivateKey(DSAPrivateKey privateKey) {
this.privateKey = privateKey;
}
@Override
public String getAlgorithm() {
return "DS" + (privateKey.getParams().getP().bitLength() + 7)/8;
}
/**
* Serialize to a JSON object.
* <p>
* Parameters are serialized as decimal strings. Hex-versus-decimal was
* reverse-engineered from what the Persona public verifier accepted.
*/
@Override
public ExtendedJSONObject toJSONObject() {
DSAParams params = privateKey.getParams();
ExtendedJSONObject o = new ExtendedJSONObject();
o.put("algorithm", "DS");
o.put("x", privateKey.getX().toString(SERIALIZATION_BASE));
o.put("g", params.getG().toString(SERIALIZATION_BASE));
o.put("p", params.getP().toString(SERIALIZATION_BASE));
o.put("q", params.getQ().toString(SERIALIZATION_BASE));
return o;
}
@SuppressLint("TrulyRandom")
@Override
public byte[] signMessage(byte[] bytes)
throws GeneralSecurityException {
if (bytes == null) {
throw new IllegalArgumentException("bytes must not be null");
}
try {
PRNGFixes.apply();
} catch (Exception e) {
// Not much to be done here: it was weak before, and we couldn't patch it, so it's weak now. Not worth aborting.
Logger.error(LOG_TAG, "Got exception applying PRNGFixes! Cryptographic data produced on this device may be weak. Ignoring.", e);
}
final Signature signer = Signature.getInstance(SIGNATURE_ALGORITHM);
signer.initSign(privateKey);
signer.update(bytes);
final byte[] signature = signer.sign();
final byte[][] arrays = ASNUtils.decodeTwoArraysFromASN1(signature);
BigInteger r = new BigInteger(arrays[0]);
BigInteger s = new BigInteger(arrays[1]);
// This is awful, but signatures are always 40 bytes long.
byte[] decoded = Utils.concatAll(
Utils.hex2Byte(r.toString(16), SIGNATURE_LENGTH_BYTES / 2),
Utils.hex2Byte(s.toString(16), SIGNATURE_LENGTH_BYTES / 2));
return decoded;
}
}
public static BrowserIDKeyPair generateKeyPair(int keysize)
throws NoSuchAlgorithmException {
final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA");
keyPairGenerator.initialize(keysize);
final KeyPair keyPair = keyPairGenerator.generateKeyPair();
DSAPrivateKey privateKey = (DSAPrivateKey) keyPair.getPrivate();
DSAPublicKey publicKey = (DSAPublicKey) keyPair.getPublic();
return new BrowserIDKeyPair(new DSASigningPrivateKey(privateKey), new DSAVerifyingPublicKey(publicKey));
}
public static SigningPrivateKey createPrivateKey(BigInteger x, BigInteger p, BigInteger q, BigInteger g) throws NoSuchAlgorithmException, InvalidKeySpecException {
if (x == null) {
throw new IllegalArgumentException("x must not be null");
}
if (p == null) {
throw new IllegalArgumentException("p must not be null");
}
if (q == null) {
throw new IllegalArgumentException("q must not be null");
}
if (g == null) {
throw new IllegalArgumentException("g must not be null");
}
KeySpec keySpec = new DSAPrivateKeySpec(x, p, q, g);
KeyFactory keyFactory = KeyFactory.getInstance("DSA");
DSAPrivateKey privateKey = (DSAPrivateKey) keyFactory.generatePrivate(keySpec);
return new DSASigningPrivateKey(privateKey);
}
public static VerifyingPublicKey createPublicKey(BigInteger y, BigInteger p, BigInteger q, BigInteger g) throws NoSuchAlgorithmException, InvalidKeySpecException {
if (y == null) {
throw new IllegalArgumentException("n must not be null");
}
if (p == null) {
throw new IllegalArgumentException("p must not be null");
}
if (q == null) {
throw new IllegalArgumentException("q must not be null");
}
if (g == null) {
throw new IllegalArgumentException("g must not be null");
}
KeySpec keySpec = new DSAPublicKeySpec(y, p, q, g);
KeyFactory keyFactory = KeyFactory.getInstance("DSA");
DSAPublicKey publicKey = (DSAPublicKey) keyFactory.generatePublic(keySpec);
return new DSAVerifyingPublicKey(publicKey);
}
public static SigningPrivateKey createPrivateKey(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException {
String algorithm = o.getString("algorithm");
if (!"DS".equals(algorithm)) {
throw new InvalidKeySpecException("algorithm must equal DS, was " + algorithm);
}
try {
BigInteger x = new BigInteger(o.getString("x"), SERIALIZATION_BASE);
BigInteger p = new BigInteger(o.getString("p"), SERIALIZATION_BASE);
BigInteger q = new BigInteger(o.getString("q"), SERIALIZATION_BASE);
BigInteger g = new BigInteger(o.getString("g"), SERIALIZATION_BASE);
return createPrivateKey(x, p, q, g);
} catch (NullPointerException | NumberFormatException e) {
throw new InvalidKeySpecException("x, p, q, and g must be integers encoded as strings, base " + SERIALIZATION_BASE);
}
}
public static VerifyingPublicKey createPublicKey(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException {
String algorithm = o.getString("algorithm");
if (!"DS".equals(algorithm)) {
throw new InvalidKeySpecException("algorithm must equal DS, was " + algorithm);
}
try {
BigInteger y = new BigInteger(o.getString("y"), SERIALIZATION_BASE);
BigInteger p = new BigInteger(o.getString("p"), SERIALIZATION_BASE);
BigInteger q = new BigInteger(o.getString("q"), SERIALIZATION_BASE);
BigInteger g = new BigInteger(o.getString("g"), SERIALIZATION_BASE);
return createPublicKey(y, p, q, g);
} catch (NullPointerException | NumberFormatException e) {
throw new InvalidKeySpecException("y, p, q, and g must be integers encoded as strings, base " + SERIALIZATION_BASE);
}
}
public static BrowserIDKeyPair fromJSONObject(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException {
try {
ExtendedJSONObject privateKey = o.getObject(BrowserIDKeyPair.JSON_KEY_PRIVATEKEY);
ExtendedJSONObject publicKey = o.getObject(BrowserIDKeyPair.JSON_KEY_PUBLICKEY);
if (privateKey == null) {
throw new InvalidKeySpecException("privateKey must not be null");
}
if (publicKey == null) {
throw new InvalidKeySpecException("publicKey must not be null");
}
return new BrowserIDKeyPair(createPrivateKey(privateKey), createPublicKey(publicKey));
} catch (NonObjectJSONException e) {
throw new InvalidKeySpecException("privateKey and publicKey must be JSON objects");
}
}
}

View file

@ -1,245 +0,0 @@
/* 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/. */
package org.mozilla.gecko.browserid;
import org.json.simple.JSONObject;
import org.mozilla.apache.commons.codec.binary.Base64;
import org.mozilla.apache.commons.codec.binary.StringUtils;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.NonObjectJSONException;
import org.mozilla.gecko.sync.Utils;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.TreeMap;
/**
* Encode and decode JSON Web Tokens.
* <p>
* Reverse-engineered from the Node.js jwcrypto library at
* <a href="https://github.com/mozilla/jwcrypto">https://github.com/mozilla/jwcrypto</a>
* and informed by the informal draft standard "JSON Web Token (JWT)" at
* <a href="http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html">http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html</a>.
*/
public class JSONWebTokenUtils {
public static final long DEFAULT_CERTIFICATE_DURATION_IN_MILLISECONDS = 60 * 60 * 1000;
public static final long DEFAULT_ASSERTION_DURATION_IN_MILLISECONDS = 60 * 60 * 1000;
public static final long DEFAULT_FUTURE_EXPIRES_AT_IN_MILLISECONDS = 9999999999999L;
public static final String DEFAULT_CERTIFICATE_ISSUER = "127.0.0.1";
public static final String DEFAULT_ASSERTION_ISSUER = "127.0.0.1";
public static String encode(String payload, SigningPrivateKey privateKey) throws UnsupportedEncodingException, GeneralSecurityException {
final ExtendedJSONObject header = new ExtendedJSONObject();
header.put("alg", privateKey.getAlgorithm());
String encodedHeader = Base64.encodeBase64URLSafeString(header.toJSONString().getBytes("UTF-8"));
String encodedPayload = Base64.encodeBase64URLSafeString(payload.getBytes("UTF-8"));
ArrayList<String> segments = new ArrayList<String>();
segments.add(encodedHeader);
segments.add(encodedPayload);
byte[] message = Utils.toDelimitedString(".", segments).getBytes("UTF-8");
byte[] signature = privateKey.signMessage(message);
segments.add(Base64.encodeBase64URLSafeString(signature));
return Utils.toDelimitedString(".", segments);
}
public static String decode(String token, VerifyingPublicKey publicKey) throws GeneralSecurityException, UnsupportedEncodingException {
if (token == null) {
throw new IllegalArgumentException("token must not be null");
}
String[] segments = token.split("\\.");
if (segments == null || segments.length != 3) {
throw new GeneralSecurityException("malformed token");
}
byte[] message = (segments[0] + "." + segments[1]).getBytes("UTF-8");
byte[] signature = Base64.decodeBase64(segments[2]);
boolean verifies = publicKey.verifyMessage(message, signature);
if (!verifies) {
throw new GeneralSecurityException("bad signature");
}
String payload = StringUtils.newStringUtf8(Base64.decodeBase64(segments[1]));
return payload;
}
/**
* Public for testing.
*/
@SuppressWarnings("unchecked")
public static String getPayloadString(String payloadString, String audience, String issuer,
Long issuedAt, long expiresAt) throws NonObjectJSONException, IOException {
ExtendedJSONObject payload;
if (payloadString != null) {
payload = new ExtendedJSONObject(payloadString);
} else {
payload = new ExtendedJSONObject();
}
if (audience != null) {
payload.put("aud", audience);
}
payload.put("iss", issuer);
if (issuedAt != null) {
payload.put("iat", issuedAt);
}
payload.put("exp", expiresAt);
// TreeMap so that keys are sorted. A small attempt to keep output stable over time.
return JSONObject.toJSONString(new TreeMap<Object, Object>(payload.object));
}
protected static String getCertificatePayloadString(VerifyingPublicKey publicKeyToSign, String email) throws NonObjectJSONException, IOException {
ExtendedJSONObject payload = new ExtendedJSONObject();
ExtendedJSONObject principal = new ExtendedJSONObject();
principal.put("email", email);
payload.put("principal", principal);
payload.put("public-key", publicKeyToSign.toJSONObject());
return payload.toJSONString();
}
public static String createCertificate(VerifyingPublicKey publicKeyToSign, String email,
String issuer, long issuedAt, long expiresAt, SigningPrivateKey privateKey) throws NonObjectJSONException, IOException, GeneralSecurityException {
String certificatePayloadString = getCertificatePayloadString(publicKeyToSign, email);
String payloadString = getPayloadString(certificatePayloadString, null, issuer, issuedAt, expiresAt);
return JSONWebTokenUtils.encode(payloadString, privateKey);
}
/**
* Create a Browser ID assertion.
*
* @param privateKeyToSignWith
* private key to sign assertion with.
* @param certificate
* to include in assertion; no attempt is made to ensure the
* certificate is valid, or corresponds to the private key, or any
* other condition.
* @param audience
* to produce assertion for.
* @param issuer
* to produce assertion for.
* @param issuedAt
* timestamp for assertion, in milliseconds since the epoch; if null,
* no timestamp is included.
* @param expiresAt
* expiration timestamp for assertion, in milliseconds since the epoch.
* @return assertion.
* @throws NonObjectJSONException
* @throws IOException
* @throws GeneralSecurityException
*/
public static String createAssertion(SigningPrivateKey privateKeyToSignWith, String certificate, String audience,
String issuer, Long issuedAt, long expiresAt) throws NonObjectJSONException, IOException, GeneralSecurityException {
String emptyAssertionPayloadString = "{}";
String payloadString = getPayloadString(emptyAssertionPayloadString, audience, issuer, issuedAt, expiresAt);
String signature = JSONWebTokenUtils.encode(payloadString, privateKeyToSignWith);
return certificate + "~" + signature;
}
/**
* For debugging only!
*
* @param input
* certificate to dump.
* @return non-null object with keys header, payload, signature if the
* certificate is well-formed.
*/
public static ExtendedJSONObject parseCertificate(String input) {
try {
String[] parts = input.split("\\.");
if (parts.length != 3) {
return null;
}
String cHeader = new String(Base64.decodeBase64(parts[0]));
String cPayload = new String(Base64.decodeBase64(parts[1]));
String cSignature = Utils.byte2Hex(Base64.decodeBase64(parts[2]));
ExtendedJSONObject o = new ExtendedJSONObject();
o.put("header", new ExtendedJSONObject(cHeader));
o.put("payload", new ExtendedJSONObject(cPayload));
o.put("signature", cSignature);
return o;
} catch (Exception e) {
return null;
}
}
/**
* For debugging only!
*
* @param input certificate to dump.
* @return true if the certificate is well-formed.
*/
public static boolean dumpCertificate(String input) {
ExtendedJSONObject c = parseCertificate(input);
try {
if (c == null) {
System.out.println("Malformed certificate -- got exception trying to dump contents.");
return false;
}
System.out.println("certificate header: " + c.getObject("header").toJSONString());
System.out.println("certificate payload: " + c.getObject("payload").toJSONString());
System.out.println("certificate signature: " + c.getString("signature"));
return true;
} catch (Exception e) {
System.out.println("Malformed certificate -- got exception trying to dump contents.");
return false;
}
}
/**
* For debugging only!
*
* @param input assertion to dump.
* @return true if the assertion is well-formed.
*/
public static ExtendedJSONObject parseAssertion(String input) {
try {
String[] parts = input.split("~");
if (parts.length != 2) {
return null;
}
String certificate = parts[0];
String assertion = parts[1];
parts = assertion.split("\\.");
if (parts.length != 3) {
return null;
}
String aHeader = new String(Base64.decodeBase64(parts[0]));
String aPayload = new String(Base64.decodeBase64(parts[1]));
String aSignature = Utils.byte2Hex(Base64.decodeBase64(parts[2]));
// We do all the assertion parsing *before* dumping the certificate in
// case there's a malformed assertion.
ExtendedJSONObject o = new ExtendedJSONObject();
o.put("header", new ExtendedJSONObject(aHeader));
o.put("payload", new ExtendedJSONObject(aPayload));
o.put("signature", aSignature);
o.put("certificate", certificate);
return o;
} catch (Exception e) {
return null;
}
}
/**
* For debugging only!
*
* @param input assertion to dump.
* @return true if the assertion is well-formed.
*/
public static boolean dumpAssertion(String input) {
ExtendedJSONObject a = parseAssertion(input);
try {
if (a == null) {
System.out.println("Malformed assertion -- got exception trying to dump contents.");
return false;
}
dumpCertificate(a.getString("certificate"));
System.out.println("assertion header: " + a.getObject("header").toJSONString());
System.out.println("assertion payload: " + a.getObject("payload").toJSONString());
System.out.println("assertion signature: " + a.getString("signature"));
return true;
} catch (Exception e) {
System.out.println("Malformed assertion -- got exception trying to dump contents.");
return false;
}
}
}

View file

@ -1,128 +0,0 @@
/* 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/. */
package org.mozilla.gecko.browserid;
import java.math.BigInteger;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
/**
* Generate certificates and assertions backed by mockmyid.com's private key.
* <p>
* These artifacts are for testing only.
*/
public class MockMyIDTokenFactory {
public static final BigInteger MOCKMYID_x = new BigInteger("385cb3509f086e110c5e24bdd395a84b335a09ae", 16);
public static final BigInteger MOCKMYID_y = new BigInteger("738ec929b559b604a232a9b55a5295afc368063bb9c20fac4e53a74970a4db7956d48e4c7ed523405f629b4cc83062f13029c4d615bbacb8b97f5e56f0c7ac9bc1d4e23809889fa061425c984061fca1826040c399715ce7ed385c4dd0d402256912451e03452d3c961614eb458f188e3e8d2782916c43dbe2e571251ce38262", 16);
public static final BigInteger MOCKMYID_p = new BigInteger("ff600483db6abfc5b45eab78594b3533d550d9f1bf2a992a7a8daa6dc34f8045ad4e6e0c429d334eeeaaefd7e23d4810be00e4cc1492cba325ba81ff2d5a5b305a8d17eb3bf4a06a349d392e00d329744a5179380344e82a18c47933438f891e22aeef812d69c8f75e326cb70ea000c3f776dfdbd604638c2ef717fc26d02e17", 16);
public static final BigInteger MOCKMYID_q = new BigInteger("e21e04f911d1ed7991008ecaab3bf775984309c3", 16);
public static final BigInteger MOCKMYID_g = new BigInteger("c52a4a0ff3b7e61fdf1867ce84138369a6154f4afa92966e3c827e25cfa6cf508b90e5de419e1337e07a2e9e2a3cd5dea704d175f8ebf6af397d69e110b96afb17c7a03259329e4829b0d03bbc7896b15b4ade53e130858cc34d96269aa89041f409136c7242a38895c9d5bccad4f389af1d7a4bd1398bd072dffa896233397a", 16);
// Computed lazily by static <code>getMockMyIDPrivateKey</code>.
protected static SigningPrivateKey cachedMockMyIDPrivateKey;
public static SigningPrivateKey getMockMyIDPrivateKey() throws NoSuchAlgorithmException, InvalidKeySpecException {
if (cachedMockMyIDPrivateKey == null) {
cachedMockMyIDPrivateKey = DSACryptoImplementation.createPrivateKey(MOCKMYID_x, MOCKMYID_p, MOCKMYID_q, MOCKMYID_g);
}
return cachedMockMyIDPrivateKey;
}
/**
* Sign a public key asserting ownership of username@mockmyid.com with
* mockmyid.com's private key.
*
* @param publicKeyToSign
* public key to sign.
* @param username
* sign username@mockmyid.com
* @param issuedAt
* timestamp for certificate, in milliseconds since the epoch.
* @param expiresAt
* expiration timestamp for certificate, in milliseconds since the epoch.
* @return encoded certificate string.
* @throws Exception
*/
public String createMockMyIDCertificate(final VerifyingPublicKey publicKeyToSign, String username,
final long issuedAt, final long expiresAt)
throws Exception {
if (!username.endsWith("@mockmyid.com")) {
username = username + "@mockmyid.com";
}
SigningPrivateKey mockMyIdPrivateKey = getMockMyIDPrivateKey();
return JSONWebTokenUtils.createCertificate(publicKeyToSign, username, "mockmyid.com", issuedAt, expiresAt, mockMyIdPrivateKey);
}
/**
* Sign a public key asserting ownership of username@mockmyid.com with
* mockmyid.com's private key.
*
* @param publicKeyToSign
* public key to sign.
* @param username
* sign username@mockmyid.com
* @return encoded certificate string.
* @throws Exception
*/
public String createMockMyIDCertificate(final VerifyingPublicKey publicKeyToSign, final String username)
throws Exception {
long ciat = System.currentTimeMillis();
long cexp = ciat + JSONWebTokenUtils.DEFAULT_CERTIFICATE_DURATION_IN_MILLISECONDS;
return createMockMyIDCertificate(publicKeyToSign, username, ciat, cexp);
}
/**
* Generate an assertion asserting ownership of username@mockmyid.com to a
* relying party. The underlying certificate is signed by mockymid.com's
* private key.
*
* @param keyPair
* to sign with.
* @param username
* sign username@mockmyid.com.
* @param certificateIssuedAt
* timestamp for certificate, in milliseconds since the epoch.
* @param certificateExpiresAt
* expiration timestamp for certificate, in milliseconds since the epoch.
* @param assertionIssuedAt
* timestamp for assertion, in milliseconds since the epoch; if null,
* no timestamp is included.
* @param assertionExpiresAt
* expiration timestamp for assertion, in milliseconds since the epoch.
* @return encoded assertion string.
* @throws Exception
*/
public String createMockMyIDAssertion(BrowserIDKeyPair keyPair, String username, String audience,
long certificateIssuedAt, long certificateExpiresAt,
Long assertionIssuedAt, long assertionExpiresAt)
throws Exception {
String certificate = createMockMyIDCertificate(keyPair.getPublic(), username,
certificateIssuedAt, certificateExpiresAt);
return JSONWebTokenUtils.createAssertion(keyPair.getPrivate(), certificate, audience,
JSONWebTokenUtils.DEFAULT_ASSERTION_ISSUER, assertionIssuedAt, assertionExpiresAt);
}
/**
* Generate an assertion asserting ownership of username@mockmyid.com to a
* relying party. The underlying certificate is signed by mockymid.com's
* private key.
*
* @param keyPair
* to sign with.
* @param username
* sign username@mockmyid.com.
* @return encoded assertion string.
* @throws Exception
*/
public String createMockMyIDAssertion(BrowserIDKeyPair keyPair, String username, String audience)
throws Exception {
long ciat = System.currentTimeMillis();
long cexp = ciat + JSONWebTokenUtils.DEFAULT_CERTIFICATE_DURATION_IN_MILLISECONDS;
long aiat = ciat + 1;
long aexp = aiat + JSONWebTokenUtils.DEFAULT_ASSERTION_DURATION_IN_MILLISECONDS;
return createMockMyIDAssertion(keyPair, username, audience,
ciat, cexp, aiat, aexp);
}
}

View file

@ -1,182 +0,0 @@
/* 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/. */
package org.mozilla.gecko.browserid;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.security.spec.RSAPrivateKeySpec;
import java.security.spec.RSAPublicKeySpec;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.NonObjectJSONException;
public class RSACryptoImplementation {
public static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
/**
* Parameters are serialized as decimal strings. Hex-versus-decimal was
* reverse-engineered from what the Persona public verifier accepted. We
* expect to follow the JOSE/JWT spec as it solidifies, and that will probably
* mean unifying this base.
*/
protected static final int SERIALIZATION_BASE = 10;
protected static class RSAVerifyingPublicKey implements VerifyingPublicKey {
protected final RSAPublicKey publicKey;
public RSAVerifyingPublicKey(RSAPublicKey publicKey) {
this.publicKey = publicKey;
}
/**
* Serialize to a JSON object.
* <p>
* Parameters are serialized as decimal strings. Hex-versus-decimal was
* reverse-engineered from what the Persona public verifier accepted.
*/
@Override
public ExtendedJSONObject toJSONObject() {
ExtendedJSONObject o = new ExtendedJSONObject();
o.put("algorithm", "RS");
o.put("n", publicKey.getModulus().toString(SERIALIZATION_BASE));
o.put("e", publicKey.getPublicExponent().toString(SERIALIZATION_BASE));
return o;
}
@Override
public boolean verifyMessage(byte[] bytes, byte[] signature)
throws GeneralSecurityException {
final Signature signer = Signature.getInstance(SIGNATURE_ALGORITHM);
signer.initVerify(publicKey);
signer.update(bytes);
return signer.verify(signature);
}
}
protected static class RSASigningPrivateKey implements SigningPrivateKey {
protected final RSAPrivateKey privateKey;
public RSASigningPrivateKey(RSAPrivateKey privateKey) {
this.privateKey = privateKey;
}
@Override
public String getAlgorithm() {
return "RS" + (privateKey.getModulus().bitLength() + 7)/8;
}
/**
* Serialize to a JSON object.
* <p>
* Parameters are serialized as decimal strings. Hex-versus-decimal was
* reverse-engineered from what the Persona public verifier accepted.
*/
@Override
public ExtendedJSONObject toJSONObject() {
ExtendedJSONObject o = new ExtendedJSONObject();
o.put("algorithm", "RS");
o.put("n", privateKey.getModulus().toString(SERIALIZATION_BASE));
o.put("d", privateKey.getPrivateExponent().toString(SERIALIZATION_BASE));
return o;
}
@Override
public byte[] signMessage(byte[] bytes)
throws GeneralSecurityException {
final Signature signer = Signature.getInstance(SIGNATURE_ALGORITHM);
signer.initSign(privateKey);
signer.update(bytes);
return signer.sign();
}
}
public static BrowserIDKeyPair generateKeyPair(final int keysize) throws NoSuchAlgorithmException {
final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(keysize);
final KeyPair keyPair = keyPairGenerator.generateKeyPair();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
return new BrowserIDKeyPair(new RSASigningPrivateKey(privateKey), new RSAVerifyingPublicKey(publicKey));
}
public static SigningPrivateKey createPrivateKey(BigInteger n, BigInteger d) throws NoSuchAlgorithmException, InvalidKeySpecException {
if (n == null) {
throw new IllegalArgumentException("n must not be null");
}
if (d == null) {
throw new IllegalArgumentException("d must not be null");
}
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
KeySpec keySpec = new RSAPrivateKeySpec(n, d);
RSAPrivateKey privateKey = (RSAPrivateKey) keyFactory.generatePrivate(keySpec);
return new RSASigningPrivateKey(privateKey);
}
public static VerifyingPublicKey createPublicKey(BigInteger n, BigInteger e) throws NoSuchAlgorithmException, InvalidKeySpecException {
if (n == null) {
throw new IllegalArgumentException("n must not be null");
}
if (e == null) {
throw new IllegalArgumentException("e must not be null");
}
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
KeySpec keySpec = new RSAPublicKeySpec(n, e);
RSAPublicKey publicKey = (RSAPublicKey) keyFactory.generatePublic(keySpec);
return new RSAVerifyingPublicKey(publicKey);
}
public static SigningPrivateKey createPrivateKey(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException {
String algorithm = o.getString("algorithm");
if (!"RS".equals(algorithm)) {
throw new InvalidKeySpecException("algorithm must equal RS, was " + algorithm);
}
try {
BigInteger n = new BigInteger(o.getString("n"), SERIALIZATION_BASE);
BigInteger d = new BigInteger(o.getString("d"), SERIALIZATION_BASE);
return createPrivateKey(n, d);
} catch (NullPointerException | NumberFormatException e) {
throw new InvalidKeySpecException("n and d must be integers encoded as strings, base " + SERIALIZATION_BASE);
}
}
public static VerifyingPublicKey createPublicKey(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException {
String algorithm = o.getString("algorithm");
if (!"RS".equals(algorithm)) {
throw new InvalidKeySpecException("algorithm must equal RS, was " + algorithm);
}
try {
BigInteger n = new BigInteger(o.getString("n"), SERIALIZATION_BASE);
BigInteger e = new BigInteger(o.getString("e"), SERIALIZATION_BASE);
return createPublicKey(n, e);
} catch (NullPointerException | NumberFormatException e) {
throw new InvalidKeySpecException("n and e must be integers encoded as strings, base " + SERIALIZATION_BASE);
}
}
public static BrowserIDKeyPair fromJSONObject(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException {
try {
ExtendedJSONObject privateKey = o.getObject(BrowserIDKeyPair.JSON_KEY_PRIVATEKEY);
ExtendedJSONObject publicKey = o.getObject(BrowserIDKeyPair.JSON_KEY_PUBLICKEY);
if (privateKey == null) {
throw new InvalidKeySpecException("privateKey must not be null");
}
if (publicKey == null) {
throw new InvalidKeySpecException("publicKey must not be null");
}
return new BrowserIDKeyPair(createPrivateKey(privateKey), createPublicKey(publicKey));
} catch (NonObjectJSONException e) {
throw new InvalidKeySpecException("privateKey and publicKey must be JSON objects");
}
}
}

View file

@ -1,41 +0,0 @@
/* 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/. */
package org.mozilla.gecko.browserid;
import java.security.GeneralSecurityException;
import org.mozilla.gecko.sync.ExtendedJSONObject;
public interface SigningPrivateKey {
/**
* Return the JSON Web Token "alg" header corresponding to this private key.
* <p>
* The header is used when formatting web tokens, and generally denotes the
* algorithm and an ad-hoc encoding of the key size.
*
* @return header.
*/
public String getAlgorithm();
/**
* Generate a JSON representation of a private key.
* <p>
* <b>This should only be used for debugging. No private keys should go over
* the wire at any time.</b>
*
* @param privateKey
* to represent.
* @return JSON representation.
*/
public ExtendedJSONObject toJSONObject();
/**
* Sign a message.
* @param message to sign.
* @return signature.
* @throws GeneralSecurityException
*/
public byte[] signMessage(byte[] message) throws GeneralSecurityException;
}

View file

@ -1,34 +0,0 @@
/* 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/. */
package org.mozilla.gecko.browserid;
import java.security.GeneralSecurityException;
import org.mozilla.gecko.sync.ExtendedJSONObject;
public interface VerifyingPublicKey {
/**
* Generate a JSON representation of a public key.
*
* @param publicKey
* to represent.
* @return JSON representation.
*/
public ExtendedJSONObject toJSONObject();
/**
* Verify a signature.
*
* @param message
* to verify signature of.
* @param signature
* to verify.
* @return true if signature is a signature of message produced by the private
* key corresponding to this public key.
* @throws GeneralSecurityException
*/
public boolean verifyMessage(byte[] message, byte[] signature) throws GeneralSecurityException;
}

View file

@ -1,95 +0,0 @@
/* 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/. */
package org.mozilla.gecko.browserid.verifier;
import java.io.IOException;
import java.net.URI;
import java.security.GeneralSecurityException;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.browserid.verifier.BrowserIDVerifierException.BrowserIDVerifierErrorResponseException;
import org.mozilla.gecko.browserid.verifier.BrowserIDVerifierException.BrowserIDVerifierMalformedResponseException;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.net.BaseResourceDelegate;
import org.mozilla.gecko.sync.net.Resource;
import org.mozilla.gecko.sync.net.SyncResponse;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.client.ClientProtocolException;
public abstract class AbstractBrowserIDRemoteVerifierClient implements BrowserIDVerifierClient {
public static final String LOG_TAG = AbstractBrowserIDRemoteVerifierClient.class.getSimpleName();
protected static class RemoteVerifierResourceDelegate extends BaseResourceDelegate {
private final BrowserIDVerifierDelegate delegate;
protected RemoteVerifierResourceDelegate(Resource resource, BrowserIDVerifierDelegate delegate) {
super(resource);
this.delegate = delegate;
}
@Override
public String getUserAgent() {
return null;
}
@Override
public void handleHttpResponse(HttpResponse response) {
SyncResponse res = new SyncResponse(response);
int statusCode = res.getStatusCode();
Logger.debug(LOG_TAG, "Got response with status code " + statusCode + ".");
if (statusCode != 200) {
delegate.handleError(new BrowserIDVerifierErrorResponseException("Expected status code 200."));
return;
}
ExtendedJSONObject o = null;
try {
o = res.jsonObjectBody();
} catch (Exception e) {
delegate.handleError(new BrowserIDVerifierMalformedResponseException(e));
return;
}
String status = o.getString("status");
if ("failure".equals(status)) {
delegate.handleFailure(o);
return;
}
if (!("okay".equals(status))) {
delegate.handleError(new BrowserIDVerifierMalformedResponseException("Expected status okay, got '" + status + "'."));
return;
}
delegate.handleSuccess(o);
}
@Override
public void handleTransportException(GeneralSecurityException e) {
Logger.warn(LOG_TAG, "Got transport exception.", e);
delegate.handleError(e);
}
@Override
public void handleHttpProtocolException(ClientProtocolException e) {
Logger.warn(LOG_TAG, "Got protocol exception.", e);
delegate.handleError(e);
}
@Override
public void handleHttpIOException(IOException e) {
Logger.warn(LOG_TAG, "Got IO exception.", e);
delegate.handleError(e);
}
}
protected final URI verifierUri;
public AbstractBrowserIDRemoteVerifierClient(URI verifierUri) {
this.verifierUri = verifierUri;
}
}

View file

@ -1,62 +0,0 @@
/* 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/. */
package org.mozilla.gecko.browserid.verifier;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.List;
import org.mozilla.gecko.sync.net.BaseResource;
import ch.boye.httpclientandroidlib.NameValuePair;
import ch.boye.httpclientandroidlib.client.entity.UrlEncodedFormEntity;
import ch.boye.httpclientandroidlib.message.BasicNameValuePair;
/**
* The verifier protocol changed: version 1 posts form-encoded data; version 2
* posts JSON data.
*/
public class BrowserIDRemoteVerifierClient10 extends AbstractBrowserIDRemoteVerifierClient {
public static final String LOG_TAG = BrowserIDRemoteVerifierClient10.class.getSimpleName();
public static final String DEFAULT_VERIFIER_URL = "https://verifier.login.persona.org/verify";
public BrowserIDRemoteVerifierClient10() throws URISyntaxException {
super(new URI(DEFAULT_VERIFIER_URL));
}
public BrowserIDRemoteVerifierClient10(URI verifierUri) {
super(verifierUri);
}
@Override
public void verify(String audience, String assertion, final BrowserIDVerifierDelegate delegate) {
if (audience == null) {
throw new IllegalArgumentException("audience cannot be null.");
}
if (assertion == null) {
throw new IllegalArgumentException("assertion cannot be null.");
}
if (delegate == null) {
throw new IllegalArgumentException("delegate cannot be null.");
}
BaseResource r = new BaseResource(verifierUri);
r.delegate = new RemoteVerifierResourceDelegate(r, delegate);
List<NameValuePair> nvps = Arrays.asList(new NameValuePair[] {
new BasicNameValuePair("audience", audience),
new BasicNameValuePair("assertion", assertion) });
try {
r.post(new UrlEncodedFormEntity(nvps, "UTF-8"));
} catch (UnsupportedEncodingException e) {
delegate.handleError(e);
}
}
}

View file

@ -1,58 +0,0 @@
/* 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/. */
package org.mozilla.gecko.browserid.verifier;
import java.net.URI;
import java.net.URISyntaxException;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.net.BaseResource;
/**
* The verifier protocol changed: version 1 posts form-encoded data; version 2
* posts JSON data.
*/
public class BrowserIDRemoteVerifierClient20 extends AbstractBrowserIDRemoteVerifierClient {
public static final String LOG_TAG = BrowserIDRemoteVerifierClient20.class.getSimpleName();
public static final String DEFAULT_VERIFIER_URL = "https://verifier.accounts.firefox.com/v2";
protected static final String JSON_KEY_ASSERTION = "assertion";
protected static final String JSON_KEY_AUDIENCE = "audience";
public BrowserIDRemoteVerifierClient20() throws URISyntaxException {
super(new URI(DEFAULT_VERIFIER_URL));
}
public BrowserIDRemoteVerifierClient20(URI verifierUri) {
super(verifierUri);
}
@Override
public void verify(String audience, String assertion, final BrowserIDVerifierDelegate delegate) {
if (audience == null) {
throw new IllegalArgumentException("audience cannot be null.");
}
if (assertion == null) {
throw new IllegalArgumentException("assertion cannot be null.");
}
if (delegate == null) {
throw new IllegalArgumentException("delegate cannot be null.");
}
BaseResource r = new BaseResource(verifierUri);
r.delegate = new RemoteVerifierResourceDelegate(r, delegate);
final ExtendedJSONObject requestBody = new ExtendedJSONObject();
requestBody.put(JSON_KEY_AUDIENCE, audience);
requestBody.put(JSON_KEY_ASSERTION, assertion);
try {
r.post(requestBody);
} catch (Exception e) {
delegate.handleError(e);
}
}
}

View file

@ -1,9 +0,0 @@
/* 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/. */
package org.mozilla.gecko.browserid.verifier;
public interface BrowserIDVerifierClient {
public abstract void verify(String audience, String assertion, BrowserIDVerifierDelegate delegate);
}

View file

@ -1,13 +0,0 @@
/* 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/. */
package org.mozilla.gecko.browserid.verifier;
import org.mozilla.gecko.sync.ExtendedJSONObject;
public interface BrowserIDVerifierDelegate {
void handleSuccess(ExtendedJSONObject response);
void handleFailure(ExtendedJSONObject response);
void handleError(Exception e);
}

View file

@ -1,41 +0,0 @@
/* 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/. */
package org.mozilla.gecko.browserid.verifier;
public class BrowserIDVerifierException extends Exception {
private static final long serialVersionUID = 2228946910754889975L;
public BrowserIDVerifierException(String detailMessage) {
super(detailMessage);
}
public BrowserIDVerifierException(Throwable throwable) {
super(throwable);
}
public static class BrowserIDVerifierMalformedResponseException extends BrowserIDVerifierException {
private static final long serialVersionUID = 115377527009652839L;
public BrowserIDVerifierMalformedResponseException(String detailMessage) {
super(detailMessage);
}
public BrowserIDVerifierMalformedResponseException(Throwable throwable) {
super(throwable);
}
}
public static class BrowserIDVerifierErrorResponseException extends BrowserIDVerifierException {
private static final long serialVersionUID = 115377527009652840L;
public BrowserIDVerifierErrorResponseException(String detailMessage) {
super(detailMessage);
}
public BrowserIDVerifierErrorResponseException(Throwable throwable) {
super(throwable);
}
}
}

View file

@ -1,227 +0,0 @@
/* 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/. */
package org.mozilla.gecko.fxa;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Handler;
import android.os.Looper;
import android.content.AsyncTaskLoader;
import android.support.v4.content.LocalBroadcastManager;
import java.lang.ref.WeakReference;
/**
* A Loader that queries and updates based on the existence of Firefox and
* legacy Sync Android Accounts.
*
* The loader returns an Android Account (of either Account type) if an account
* exists, and null to indicate no Account is present.
*
* The loader listens for Accounts added and deleted, and also Accounts being
* updated by Sync or another Activity, via the use of
* {@link AndroidFxAccount#setState(org.mozilla.gecko.fxa.login.State)}.
* Be careful of message loops if you update the account state from an activity
* that uses this loader.
*
* This implementation is based on
* <a href="http://www.androiddesignpatterns.com/2012/08/implementing-loaders.html">http://www.androiddesignpatterns.com/2012/08/implementing-loaders.html</a>.
*/
public class AccountLoader extends AsyncTaskLoader<Account> {
protected Account account = null;
protected BroadcastReceiver broadcastReceiver = null;
// Hold a weak reference to AccountLoader instance in this Runnable to avoid potentially leaking it
// after posting to a Handler in the BroadcastReceiver returned from makeNewObserver.
private final BroadcastReceiverRunnable broadcastReceiverRunnable = new BroadcastReceiverRunnable(this);
public AccountLoader(final Context context) {
super(context);
}
// Task that performs the asynchronous load.
@Override
public Account loadInBackground() {
return FirefoxAccounts.getFirefoxAccount(getContext());
}
// Deliver the results to the registered listener.
@Override
public void deliverResult(Account data) {
if (isReset()) {
// The Loader has been reset; ignore the result and invalidate the data.
releaseResources(data);
return;
}
// Hold a reference to the old data so it doesn't get garbage collected.
// We must protect it until the new data has been delivered.
Account oldData = account;
account = data;
if (isStarted()) {
// If the Loader is in a started state, deliver the results to the
// client. The superclass method does this for us.
super.deliverResult(data);
}
// Invalidate the old data as we don't need it any more.
if (oldData != null && oldData != data) {
releaseResources(oldData);
}
}
// The Loaders state-dependent behavior.
@Override
protected void onStartLoading() {
if (account != null) {
// Deliver any previously loaded data immediately.
deliverResult(account);
}
// Begin monitoring the underlying data source.
if (broadcastReceiver == null) {
broadcastReceiver = makeNewObserver();
registerLocalObserver(getContext(), broadcastReceiver);
registerSystemObserver(getContext(), broadcastReceiver);
}
if (takeContentChanged() || account == null) {
// When the observer detects a change, it should call onContentChanged()
// on the Loader, which will cause the next call to takeContentChanged()
// to return true. If this is ever the case (or if the current data is
// null), we force a new load.
forceLoad();
}
}
@Override
protected void onStopLoading() {
// The Loader is in a stopped state, so we should attempt to cancel the
// current load (if there is one).
cancelLoad();
// Note that we leave the observer as is. Loaders in a stopped state
// should still monitor the data source for changes so that the Loader
// will know to force a new load if it is ever started again.
}
@Override
protected void onReset() {
// Ensure the loader has been stopped. In CursorLoader and the template
// this code follows (see the class comment), this is onStopLoading, which
// appears to not set the started flag (see Loader itself).
stopLoading();
// At this point we can release the resources associated with 'mData'.
if (account != null) {
releaseResources(account);
account = null;
}
// The Loader is being reset, so we should stop monitoring for changes.
if (broadcastReceiver != null) {
final BroadcastReceiver observer = broadcastReceiver;
broadcastReceiver = null;
unregisterObserver(getContext(), observer);
}
}
@Override
public void onCanceled(final Account data) {
// Attempt to cancel the current asynchronous load.
super.onCanceled(data);
// The load has been canceled, so we should release the resources
// associated with 'data'.
releaseResources(data);
}
// Observer which receives notifications when the data changes.
protected BroadcastReceiver makeNewObserver() {
return new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// onContentChanged must be called on the main thread.
// If we're already on the main thread, call it directly.
if (Looper.myLooper() == Looper.getMainLooper()) {
onContentChanged();
return;
}
// Otherwise, post a Runnable to a Handler bound to the main thread's message loop.
final Handler mainHandler = new Handler(Looper.getMainLooper());
mainHandler.post(broadcastReceiverRunnable);
}
};
}
private static class BroadcastReceiverRunnable implements Runnable {
private final WeakReference<AccountLoader> accountLoaderWeakReference;
public BroadcastReceiverRunnable(final AccountLoader accountLoader) {
accountLoaderWeakReference = new WeakReference<>(accountLoader);
}
@Override
public void run() {
final AccountLoader accountLoader = accountLoaderWeakReference.get();
if (accountLoader != null) {
accountLoader.onContentChanged();
}
}
}
private void releaseResources(Account data) {
// For a simple List, there is nothing to do. For something like a Cursor, we
// would close it in this method. All resources associated with the Loader
// should be released here.
}
/**
* Register provided observer with the LocalBroadcastManager to listen for internal events.
*
* @param context <code>Context</code> to use for obtaining LocalBroadcastManager instance.
* @param observer <code>BroadcastReceiver</code> which will handle local events.
*/
protected static void registerLocalObserver(final Context context, final BroadcastReceiver observer) {
final IntentFilter intentFilter = new IntentFilter();
// Firefox Account internal state changed.
intentFilter.addAction(FxAccountConstants.ACCOUNT_STATE_CHANGED_ACTION);
// Firefox Account profile state changed.
intentFilter.addAction(FxAccountConstants.ACCOUNT_PROFILE_JSON_UPDATED_ACTION);
LocalBroadcastManager.getInstance(context).registerReceiver(observer, intentFilter);
}
/**
* Register provided observer for handling system-wide broadcasts.
*
* @param context <code>Context</code> to use for registering a receiver.
* @param observer <code>BroadcastReceiver</code> which will handle system events.
*/
protected static void registerSystemObserver(final Context context, final BroadcastReceiver observer) {
context.registerReceiver(observer,
// Android Account added or removed.
new IntentFilter(AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION),
// No broadcast permissions required.
null,
// Null handler ensures that broadcasts will be handled on the main thread.
null
);
}
protected static void unregisterObserver(final Context context, final BroadcastReceiver observer) {
LocalBroadcastManager.getInstance(context).unregisterReceiver(observer);
context.unregisterReceiver(observer);
}
}

View file

@ -1,222 +0,0 @@
/* 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/. */
package org.mozilla.gecko.fxa;
import java.io.File;
import java.util.concurrent.CountDownLatch;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.fxa.authenticator.AccountPickler;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.fxa.sync.FxAccountSyncStatusHelper;
import org.mozilla.gecko.sync.ThreadPool;
import org.mozilla.gecko.sync.Utils;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.ContentResolver;
import android.content.Context;
import android.os.Bundle;
/**
* Simple public accessors for Firefox account objects.
*/
public class FirefoxAccounts {
private static final String LOG_TAG = FirefoxAccounts.class.getSimpleName();
/**
* Returns true if a FirefoxAccount exists, false otherwise.
*
* @param context Android context.
* @return true if at least one Firefox account exists.
*/
public static boolean firefoxAccountsExist(final Context context) {
return getFirefoxAccounts(context).length > 0;
}
/**
* Return Firefox accounts.
* <p>
* If no accounts exist in the AccountManager, one may be created
* via a pickled FirefoxAccount, if available, and that account
* will be added to the AccountManager and returned.
* <p>
* Note that this can be called from any thread.
*
* @param context Android context.
* @return Firefox account objects.
*/
public static Account[] getFirefoxAccounts(final Context context) {
final Account[] accounts =
AccountManager.get(context).getAccountsByType(FxAccountConstants.ACCOUNT_TYPE);
if (accounts.length > 0) {
return accounts;
}
final Account pickledAccount = getPickledAccount(context);
return (pickledAccount != null) ? new Account[] {pickledAccount} : new Account[0];
}
private static Account getPickledAccount(final Context context) {
// To avoid a StrictMode violation for disk access, we call this from a background thread.
// We do this every time, so the caller doesn't have to care.
final CountDownLatch latch = new CountDownLatch(1);
final Account[] accounts = new Account[1];
ThreadPool.run(new Runnable() {
@Override
public void run() {
try {
final File file = context.getFileStreamPath(FxAccountConstants.ACCOUNT_PICKLE_FILENAME);
if (!file.exists()) {
accounts[0] = null;
return;
}
// There is a small race window here: if the user creates a new Firefox account
// between our checks, this could erroneously report that no Firefox accounts
// exist.
final AndroidFxAccount fxAccount =
AccountPickler.unpickle(context, FxAccountConstants.ACCOUNT_PICKLE_FILENAME);
accounts[0] = fxAccount != null ? fxAccount.getAndroidAccount() : null;
} finally {
latch.countDown();
}
}
});
try {
latch.await(); // Wait for the background thread to return.
} catch (InterruptedException e) {
Logger.warn(LOG_TAG,
"Foreground thread unexpectedly interrupted while getting pickled account", e);
return null;
}
return accounts[0];
}
/**
* @param context Android context.
* @return the configured Firefox account if one exists, or null otherwise.
*/
public static Account getFirefoxAccount(final Context context) {
Account[] accounts = getFirefoxAccounts(context);
if (accounts.length > 0) {
return accounts[0];
}
return null;
}
/**
* @return
* the {@link State} instance associated with the current account, or <code>null</code> if
* no accounts exist.
*/
public static State getFirefoxAccountState(final Context context) {
final Account account = getFirefoxAccount(context);
if (account == null) {
return null;
}
final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
try {
return fxAccount.getState();
} catch (final Exception ex) {
Logger.warn(LOG_TAG, "Could not get FX account state.", ex);
return null;
}
}
/*
* @param context Android context
* @return the email address associated with the configured Firefox account if one exists; null otherwise.
*/
public static String getFirefoxAccountEmail(final Context context) {
final Account account = getFirefoxAccount(context);
if (account == null) {
return null;
}
return account.name;
}
public static void logSyncOptions(Bundle syncOptions) {
final boolean scheduleNow = syncOptions.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false);
Logger.info(LOG_TAG, "Sync options -- scheduling now: " + scheduleNow);
}
public static void requestImmediateSync(final Account account, String[] stagesToSync, String[] stagesToSkip) {
final Bundle syncOptions = new Bundle();
syncOptions.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, true);
syncOptions.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
requestSync(account, syncOptions, stagesToSync, stagesToSkip);
}
public static void requestEventualSync(final Account account, String[] stagesToSync, String[] stagesToSkip) {
requestSync(account, Bundle.EMPTY, stagesToSync, stagesToSkip);
}
/**
* Request a sync for the given Android Account.
* <p>
* Any hints are strictly optional: the actual requested sync is scheduled by
* the Android sync scheduler, and the sync mechanism may ignore hints as it
* sees fit.
* <p>
* It is safe to call this method from any thread.
*
* @param account to sync.
* @param syncOptions to pass to sync.
* @param stagesToSync stage names to sync.
* @param stagesToSkip stage names to skip.
*/
protected static void requestSync(final Account account, final Bundle syncOptions, String[] stagesToSync, String[] stagesToSkip) {
if (account == null) {
throw new IllegalArgumentException("account must not be null");
}
if (syncOptions == null) {
throw new IllegalArgumentException("syncOptions must not be null");
}
Utils.putStageNamesToSync(syncOptions, stagesToSync, stagesToSkip);
Logger.info(LOG_TAG, "Requesting sync.");
logSyncOptions(syncOptions);
// We get strict mode warnings on some devices, so make the request on a
// background thread.
ThreadPool.run(new Runnable() {
@Override
public void run() {
for (String authority : AndroidFxAccount.DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP.keySet()) {
ContentResolver.requestSync(account, authority, syncOptions);
}
}
});
}
/**
* Start notifying <code>syncStatusListener</code> of sync status changes.
* <p>
* Only a weak reference to <code>syncStatusListener</code> is held.
*
* @param syncStatusListener to start notifying.
*/
public static void addSyncStatusListener(SyncStatusListener syncStatusListener) {
// startObserving null-checks its argument.
FxAccountSyncStatusHelper.getInstance().startObserving(syncStatusListener);
}
/**
* Stop notifying <code>syncStatusListener</code> of sync status changes.
*
* @param syncStatusListener to stop notifying.
*/
public static void removeSyncStatusListener(SyncStatusListener syncStatusListener) {
// stopObserving null-checks its argument.
FxAccountSyncStatusHelper.getInstance().stopObserving(syncStatusListener);
}
}

View file

@ -1,75 +0,0 @@
/* 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/. */
package org.mozilla.gecko.fxa;
import org.mozilla.gecko.AppConstants;
public class FxAccountConstants {
public static final String GLOBAL_LOG_TAG = "FxAccounts";
public static final String ACCOUNT_TYPE = AppConstants.MOZ_ANDROID_SHARED_FXACCOUNT_TYPE;
// Must be a client ID allocated with "canGrant" privileges!
public static final String OAUTH_CLIENT_ID_FENNEC = "3332a18d142636cb";
public static final String DEFAULT_AUTH_SERVER_ENDPOINT = "https://api.accounts.firefox.com/v1";
public static final String DEFAULT_TOKEN_SERVER_ENDPOINT = "https://token.services.mozilla.com/1.0/sync/1.5";
public static final String DEFAULT_OAUTH_SERVER_ENDPOINT = "https://oauth.accounts.firefox.com/v1";
public static final String DEFAULT_PROFILE_SERVER_ENDPOINT = "https://profile.accounts.firefox.com/v1";
public static final String STAGE_AUTH_SERVER_ENDPOINT = "https://stable.dev.lcip.org/auth/v1";
public static final String STAGE_TOKEN_SERVER_ENDPOINT = "https://stable.dev.lcip.org/syncserver/token/1.0/sync/1.5";
public static final String STAGE_OAUTH_SERVER_ENDPOINT = "https://oauth-stable.dev.lcip.org/v1";
public static final String STAGE_PROFILE_SERVER_ENDPOINT = "https://latest.dev.lcip.org/profile/v1";
// Action to update on cached profile information.
public static final String ACCOUNT_PROFILE_JSON_UPDATED_ACTION = "org.mozilla.gecko.fxa.profile.JSON.updated";
// You must be at least 13 years old, on the day of creation, to create a Firefox Account.
public static final int MINIMUM_AGE_TO_CREATE_AN_ACCOUNT = 13;
// Key for avatar URI in profile JSON.
public static final String KEY_PROFILE_JSON_AVATAR = "avatar";
// Key for username in profile JSON.
public static final String KEY_PROFILE_JSON_USERNAME = "displayName";
// You must wait 15 minutes after failing an age check before trying to create a different account.
public static final long MINIMUM_TIME_TO_WAIT_AFTER_AGE_CHECK_FAILED_IN_MILLISECONDS = 15 * 60 * 1000;
public static final String USER_AGENT = "Firefox-Android-FxAccounts/" + AppConstants.MOZ_APP_VERSION + " (" + AppConstants.MOZ_APP_UA_NAME + ")";
public static final String ACCOUNT_PICKLE_FILENAME = "fxa.account.json";
/**
* Version number of contents of SYNC_ACCOUNT_DELETED_ACTION intent.
*/
public static final long ACCOUNT_DELETED_INTENT_VERSION = 1;
public static final String ACCOUNT_DELETED_INTENT_VERSION_KEY = "account_deleted_intent_version";
public static final String ACCOUNT_DELETED_INTENT_ACCOUNT_KEY = "account_deleted_intent_account";
public static final String ACCOUNT_DELETED_INTENT_ACCOUNT_PROFILE = "account_deleted_intent_profile";
public static final String ACCOUNT_OAUTH_SERVICE_ENDPOINT_KEY = "account_oauth_service_endpoint";
public static final String ACCOUNT_DELETED_INTENT_ACCOUNT_AUTH_TOKENS = "account_deleted_intent_auth_tokens";
/**
* This action is broadcast when an Android Firefox Account's internal state
* is changed.
* <p>
* It is protected by signing-level permission PER_ACCOUNT_TYPE_PERMISSION and
* can be received only by Firefox versions sharing the same Android Firefox
* Account type.
*/
public static final String ACCOUNT_STATE_CHANGED_ACTION = AppConstants.MOZ_ANDROID_SHARED_FXACCOUNT_TYPE + ".accounts.ACCOUNT_STATE_CHANGED_ACTION";
public static final String ACTION_FXA_CONFIRM_ACCOUNT = AppConstants.ANDROID_PACKAGE_NAME + ".ACTION_FXA_CONFIRM_ACCOUNT";
public static final String ACTION_FXA_FINISH_MIGRATING = AppConstants.ANDROID_PACKAGE_NAME + ".ACTION_FXA_FINISH_MIGRATING";
public static final String ACTION_FXA_GET_STARTED = AppConstants.ANDROID_PACKAGE_NAME + ".ACTION_FXA_GET_STARTED";
public static final String ACTION_FXA_STATUS = AppConstants.ANDROID_PACKAGE_NAME + ".ACTION_FXA_STATUS";
public static final String ACTION_FXA_UPDATE_CREDENTIALS = AppConstants.ANDROID_PACKAGE_NAME + ".ACTION_FXA_UPDATE_CREDENTIALS";
public static final String ENDPOINT_PREFERENCES = "preferences";
public static final String ENDPOINT_NOTIFICATION = "notification";
public static final String ENDPOINT_FIRSTRUN = "firstrun";
}

View file

@ -1,81 +0,0 @@
/* 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/. */
package org.mozilla.gecko.fxa;
import org.mozilla.gecko.sync.ExtendedJSONObject;
public class FxAccountDevice {
public static final String JSON_KEY_NAME = "name";
public static final String JSON_KEY_ID = "id";
public static final String JSON_KEY_TYPE = "type";
public static final String JSON_KEY_ISCURRENTDEVICE = "isCurrentDevice";
public static final String JSON_KEY_PUSH_CALLBACK = "pushCallback";
public static final String JSON_KEY_PUSH_PUBLICKEY = "pushPublicKey";
public static final String JSON_KEY_PUSH_AUTHKEY = "pushAuthKey";
public final String id;
public final String name;
public final String type;
public final Boolean isCurrentDevice;
public final String pushCallback;
public final String pushPublicKey;
public final String pushAuthKey;
public FxAccountDevice(String name, String id, String type, Boolean isCurrentDevice,
String pushCallback, String pushPublicKey, String pushAuthKey) {
this.name = name;
this.id = id;
this.type = type;
this.isCurrentDevice = isCurrentDevice;
this.pushCallback = pushCallback;
this.pushPublicKey = pushPublicKey;
this.pushAuthKey = pushAuthKey;
}
public static FxAccountDevice forRegister(String name, String type, String pushCallback,
String pushPublicKey, String pushAuthKey) {
return new FxAccountDevice(name, null, type, null, pushCallback, pushPublicKey, pushAuthKey);
}
public static FxAccountDevice forUpdate(String id, String name, String pushCallback,
String pushPublicKey, String pushAuthKey) {
return new FxAccountDevice(name, id, null, null, pushCallback, pushPublicKey, pushAuthKey);
}
public static FxAccountDevice fromJson(ExtendedJSONObject json) {
String name = json.getString(JSON_KEY_NAME);
String id = json.getString(JSON_KEY_ID);
String type = json.getString(JSON_KEY_TYPE);
Boolean isCurrentDevice = json.getBoolean(JSON_KEY_ISCURRENTDEVICE);
String pushCallback = json.getString(JSON_KEY_PUSH_CALLBACK);
String pushPublicKey = json.getString(JSON_KEY_PUSH_PUBLICKEY);
String pushAuthKey = json.getString(JSON_KEY_PUSH_AUTHKEY);
return new FxAccountDevice(name, id, type, isCurrentDevice, pushCallback, pushPublicKey, pushAuthKey);
}
public ExtendedJSONObject toJson() {
final ExtendedJSONObject body = new ExtendedJSONObject();
if (this.name != null) {
body.put(JSON_KEY_NAME, this.name);
}
if (this.id != null) {
body.put(JSON_KEY_ID, this.id);
}
if (this.type != null) {
body.put(JSON_KEY_TYPE, this.type);
}
if (this.pushCallback != null) {
body.put(JSON_KEY_PUSH_CALLBACK, this.pushCallback);
}
if (this.pushPublicKey != null) {
body.put(JSON_KEY_PUSH_PUBLICKEY, this.pushPublicKey);
}
if (this.pushAuthKey != null) {
body.put(JSON_KEY_PUSH_AUTHKEY, this.pushAuthKey);
}
return body;
}
}

View file

@ -1,282 +0,0 @@
/* 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/. */
package org.mozilla.gecko.fxa;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountClient;
import org.mozilla.gecko.background.fxa.FxAccountClient20;
import org.mozilla.gecko.background.fxa.FxAccountClient20.AccountStatusResponse;
import org.mozilla.gecko.background.fxa.FxAccountClient20.RequestDelegate;
import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
import org.mozilla.gecko.background.fxa.FxAccountRemoteError;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount.InvalidFxAState;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate;
import org.mozilla.gecko.util.BundleEventListener;
import org.mozilla.gecko.util.EventCallback;
import java.io.UnsupportedEncodingException;
import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.GeneralSecurityException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/* This class provides a way to register the current device against FxA
* and also stores the registration details in the Android FxAccount.
* This should be used in a state where we possess a sessionToken, most likely the Married state.
*/
public class FxAccountDeviceRegistrator implements BundleEventListener {
private static final String LOG_TAG = "FxADeviceRegistrator";
// The current version of the device registration, we use this to re-register
// devices after we update what we send on device registration.
public static final Integer DEVICE_REGISTRATION_VERSION = 2;
private static FxAccountDeviceRegistrator instance;
private final WeakReference<Context> context;
private FxAccountDeviceRegistrator(Context appContext) {
this.context = new WeakReference<Context>(appContext);
}
private static FxAccountDeviceRegistrator getInstance(Context appContext) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
if (instance == null) {
FxAccountDeviceRegistrator tempInstance = new FxAccountDeviceRegistrator(appContext);
tempInstance.setupListeners(); // Set up listener for FxAccountPush:Subscribe:Response
instance = tempInstance;
}
return instance;
}
public static void register(Context context) {
Context appContext = context.getApplicationContext();
try {
getInstance(appContext).beginRegistration(appContext);
} catch (Exception e) {
Log.e(LOG_TAG, "Could not start FxA device registration", e);
}
}
private void beginRegistration(Context context) {
// Fire up gecko and send event
// We create the Intent ourselves instead of using GeckoService.getIntentToCreateServices
// because we can't import these modules (circular dependency between browser and services)
final Intent geckoIntent = new Intent();
geckoIntent.setAction("create-services");
geckoIntent.setClassName(context, "org.mozilla.gecko.GeckoService");
geckoIntent.putExtra("category", "android-push-service");
geckoIntent.putExtra("data", "android-fxa-subscribe");
final AndroidFxAccount fxAccount = AndroidFxAccount.fromContext(context);
geckoIntent.putExtra("org.mozilla.gecko.intent.PROFILE_NAME", fxAccount.getProfile());
context.startService(geckoIntent);
// -> handleMessage()
}
@Override
public void handleMessage(String event, Bundle message, EventCallback callback) {
if ("FxAccountsPush:Subscribe:Response".equals(event)) {
try {
doFxaRegistration(message.getBundle("subscription"));
} catch (InvalidFxAState e) {
Log.d(LOG_TAG, "Invalid state when trying to register with FxA ", e);
}
} else {
Log.e(LOG_TAG, "No action defined for " + event);
}
}
private void doFxaRegistration(Bundle subscription) throws InvalidFxAState {
final Context context = this.context.get();
if (this.context == null) {
throw new IllegalStateException("Application context has been gc'ed");
}
doFxaRegistration(context, subscription, true);
}
private static void doFxaRegistration(final Context context, final Bundle subscription, final boolean allowRecursion) throws InvalidFxAState {
String pushCallback = subscription.getString("pushCallback");
String pushPublicKey = subscription.getString("pushPublicKey");
String pushAuthKey = subscription.getString("pushAuthKey");
final AndroidFxAccount fxAccount = AndroidFxAccount.fromContext(context);
if (fxAccount == null) {
Log.e(LOG_TAG, "AndroidFxAccount is null");
return;
}
final byte[] sessionToken = fxAccount.getSessionToken();
final FxAccountDevice device;
String deviceId = fxAccount.getDeviceId();
String clientName = getClientName(fxAccount, context);
if (TextUtils.isEmpty(deviceId)) {
Log.i(LOG_TAG, "Attempting registration for a new device");
device = FxAccountDevice.forRegister(clientName, "mobile", pushCallback, pushPublicKey, pushAuthKey);
} else {
Log.i(LOG_TAG, "Attempting registration for an existing device");
Logger.pii(LOG_TAG, "Device ID: " + deviceId);
device = FxAccountDevice.forUpdate(deviceId, clientName, pushCallback, pushPublicKey, pushAuthKey);
}
ExecutorService executor = Executors.newSingleThreadExecutor(); // Not called often, it's okay to spawn another thread
final FxAccountClient20 fxAccountClient =
new FxAccountClient20(fxAccount.getAccountServerURI(), executor);
fxAccountClient.registerOrUpdateDevice(sessionToken, device, new RequestDelegate<FxAccountDevice>() {
@Override
public void handleError(Exception e) {
Log.e(LOG_TAG, "Error while updating a device registration: ", e);
}
@Override
public void handleFailure(FxAccountClientRemoteException error) {
Log.e(LOG_TAG, "Error while updating a device registration: ", error);
if (error.httpStatusCode == 400) {
if (error.apiErrorNumber == FxAccountRemoteError.UNKNOWN_DEVICE) {
recoverFromUnknownDevice(fxAccount);
} else if (error.apiErrorNumber == FxAccountRemoteError.DEVICE_SESSION_CONFLICT) {
recoverFromDeviceSessionConflict(error, fxAccountClient, sessionToken, fxAccount, context,
subscription, allowRecursion);
}
} else
if (error.httpStatusCode == 401
&& error.apiErrorNumber == FxAccountRemoteError.INVALID_AUTHENTICATION_TOKEN) {
handleTokenError(error, fxAccountClient, fxAccount);
} else {
logErrorAndResetDeviceRegistrationVersion(error, fxAccount);
}
}
@Override
public void handleSuccess(FxAccountDevice result) {
Log.i(LOG_TAG, "Device registration complete");
Logger.pii(LOG_TAG, "Registered device ID: " + result.id);
fxAccount.setFxAUserData(result.id, DEVICE_REGISTRATION_VERSION);
}
});
}
private static void logErrorAndResetDeviceRegistrationVersion(
final FxAccountClientRemoteException error, final AndroidFxAccount fxAccount) {
Log.e(LOG_TAG, "Device registration failed", error);
fxAccount.resetDeviceRegistrationVersion();
}
@Nullable
private static String getClientName(final AndroidFxAccount fxAccount, final Context context) {
try {
SharedPreferencesClientsDataDelegate clientsDataDelegate =
new SharedPreferencesClientsDataDelegate(fxAccount.getSyncPrefs(), context);
return clientsDataDelegate.getClientName();
} catch (UnsupportedEncodingException | GeneralSecurityException e) {
Log.e(LOG_TAG, "Unable to get client name.", e);
return null;
}
}
private static void handleTokenError(final FxAccountClientRemoteException error,
final FxAccountClient fxAccountClient,
final AndroidFxAccount fxAccount) {
Log.i(LOG_TAG, "Recovering from invalid token error: ", error);
logErrorAndResetDeviceRegistrationVersion(error, fxAccount);
fxAccountClient.accountStatus(fxAccount.getState().uid,
new RequestDelegate<AccountStatusResponse>() {
@Override
public void handleError(Exception e) {
}
@Override
public void handleFailure(FxAccountClientRemoteException e) {
}
@Override
public void handleSuccess(AccountStatusResponse result) {
State doghouseState = fxAccount.getState().makeDoghouseState();
if (!result.exists) {
Log.i(LOG_TAG, "token invalidated because the account no longer exists");
// TODO: Should be in a "I have an Android account, but the FxA is gone." State.
// This will do for now..
fxAccount.setState(doghouseState);
return;
}
Log.e(LOG_TAG, "sessionToken invalid");
fxAccount.setState(doghouseState);
}
});
}
private static void recoverFromUnknownDevice(final AndroidFxAccount fxAccount) {
Log.i(LOG_TAG, "unknown device id, clearing the cached device id");
fxAccount.setDeviceId(null);
}
/**
* Will call delegate#complete in all cases
*/
private static void recoverFromDeviceSessionConflict(final FxAccountClientRemoteException error,
final FxAccountClient fxAccountClient,
final byte[] sessionToken,
final AndroidFxAccount fxAccount,
final Context context,
final Bundle subscription,
final boolean allowRecursion) {
Log.w(LOG_TAG, "device session conflict, attempting to ascertain the correct device id");
fxAccountClient.deviceList(sessionToken, new RequestDelegate<FxAccountDevice[]>() {
private void onError() {
Log.e(LOG_TAG, "failed to recover from device-session conflict");
logErrorAndResetDeviceRegistrationVersion(error, fxAccount);
}
@Override
public void handleError(Exception e) {
onError();
}
@Override
public void handleFailure(FxAccountClientRemoteException e) {
onError();
}
@Override
public void handleSuccess(FxAccountDevice[] devices) {
for (FxAccountDevice device : devices) {
if (device.isCurrentDevice) {
fxAccount.setFxAUserData(device.id, 0); // Reset device registration version
if (!allowRecursion) {
Log.d(LOG_TAG, "Failure to register a device on the second try");
break;
}
try {
doFxaRegistration(context, subscription, false);
return;
} catch (InvalidFxAState e) {
Log.d(LOG_TAG, "Invalid state when trying to recover from a session conflict ", e);
break;
}
}
}
onError();
}
});
}
private void setupListeners() throws ClassNotFoundException, NoSuchMethodException,
InvocationTargetException, IllegalAccessException {
// We have no choice but to use reflection here, sorry :(
Class<?> eventDispatcher = Class.forName("org.mozilla.gecko.EventDispatcher");
Method getInstance = eventDispatcher.getMethod("getInstance");
Object instance = getInstance.invoke(null);
Method registerBackgroundThreadListener = eventDispatcher.getMethod("registerBackgroundThreadListener",
BundleEventListener.class, String[].class);
registerBackgroundThreadListener.invoke(instance, this, new String[] { "FxAccountsPush:Subscribe:Response" });
}
}

View file

@ -1,95 +0,0 @@
package org.mozilla.gecko.fxa;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
public class FxAccountPushHandler {
private static final String LOG_TAG = "FxAccountPush";
private static final String COMMAND_DEVICE_DISCONNECTED = "fxaccounts:device_disconnected";
private static final String COMMAND_COLLECTION_CHANGED = "sync:collection_changed";
private static final String CLIENTS_COLLECTION = "clients";
// Forbid instantiation
private FxAccountPushHandler() {}
public static void handleFxAPushMessage(Context context, Bundle bundle) {
Log.i(LOG_TAG, "Handling FxA Push Message");
String rawMessage = bundle.getString("message");
JSONObject message = null;
if (!TextUtils.isEmpty(rawMessage)) {
try {
message = new JSONObject(rawMessage);
} catch (JSONException e) {
Log.e(LOG_TAG, "Could not parse JSON", e);
return;
}
}
if (message == null) {
// An empty body means we should check the verification state of the account (FxA sends this
// when the account email is verified for example).
// TODO: We're only registering the push endpoint when we are in the Married state, that's why we're skipping the message :(
Log.d(LOG_TAG, "Skipping empty message");
return;
}
try {
String command = message.getString("command");
JSONObject data = message.getJSONObject("data");
switch (command) {
case COMMAND_DEVICE_DISCONNECTED:
handleDeviceDisconnection(context, data);
break;
case COMMAND_COLLECTION_CHANGED:
handleCollectionChanged(context, data);
break;
default:
Log.d(LOG_TAG, "No handler defined for FxA Push command " + command);
break;
}
} catch (JSONException e) {
Log.e(LOG_TAG, "Error while handling FxA push notification", e);
}
}
private static void handleCollectionChanged(Context context, JSONObject data) throws JSONException {
JSONArray collections = data.getJSONArray("collections");
int len = collections.length();
for (int i = 0; i < len; i++) {
if (collections.getString(i).equals(CLIENTS_COLLECTION)) {
final Account account = FirefoxAccounts.getFirefoxAccount(context);
if (account == null) {
Log.e(LOG_TAG, "The account does not exist anymore");
return;
}
final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
fxAccount.requestImmediateSync(new String[] { CLIENTS_COLLECTION }, null);
return;
}
}
}
private static void handleDeviceDisconnection(Context context, JSONObject data) throws JSONException {
final Account account = FirefoxAccounts.getFirefoxAccount(context);
if (account == null) {
Log.e(LOG_TAG, "The account does not exist anymore");
return;
}
final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
if (!fxAccount.getDeviceId().equals(data.getString("id"))) {
Log.e(LOG_TAG, "The device ID to disconnect doesn't match with the local device ID.\n"
+ "Local: " + fxAccount.getDeviceId() + ", ID to disconnect: " + data.getString("id"));
return;
}
AccountManager.get(context).removeAccount(account, null, null);
}
}

View file

@ -1,31 +0,0 @@
/* 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/. */
package org.mozilla.gecko.fxa;
import android.accounts.Account;
import android.content.Context;
import android.support.annotation.UiThread;
/**
* Interface definition for a callback to be invoked when an sync status change.
*/
public interface SyncStatusListener {
public Context getContext();
public Account getAccount();
/**
* Called when sync has started.
* This is always called in UiThread.
*/
@UiThread
public void onSyncStarted();
/**
* Called when sync has finished.
* This is always called in UiThread.
*/
@UiThread
public void onSyncFinished();
}

View file

@ -1,52 +0,0 @@
/* 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/. */
package org.mozilla.gecko.fxa.activities;
import org.mozilla.gecko.R;
import android.content.Context;
import android.content.res.TypedArray;
import android.preference.Preference;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
/**
* This preference is used to define custom colors for both title and summary texts.
* Color code #777777 (placeholder_grey) is used as the fallback color for both title and summary.
*/
public class CustomColorPreference extends Preference {
private int mTitleColor;
private int mSummaryColor;
public CustomColorPreference(Context context) {
super(context);
}
public CustomColorPreference(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public CustomColorPreference(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context, attrs);
}
public void init(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomColorPreference);
mTitleColor = a.getColor(R.styleable.CustomColorPreference_titleColor, R.color.placeholder_grey);
mSummaryColor = a.getColor(R.styleable.CustomColorPreference_summaryColor, R.color.placeholder_grey);
a.recycle();
}
@Override
protected void onBindView(View view) {
super.onBindView(view);
final TextView title = (TextView) view.findViewById(android.R.id.title);
final TextView summary = (TextView) view.findViewById(android.R.id.summary);
title.setTextColor(mTitleColor);
summary.setTextColor(mSummaryColor);
}
}

View file

@ -1,80 +0,0 @@
/* 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/. */
package org.mozilla.gecko.fxa.activities;
import android.accounts.Account;
import android.app.Activity;
import android.content.Intent;
import org.mozilla.gecko.Locales.LocaleAwareActivity;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.fxa.FxAccountConstants;
public abstract class FxAccountAbstractActivity extends LocaleAwareActivity {
private static final String LOG_TAG = FxAccountAbstractActivity.class.getSimpleName();
protected final boolean cannotResumeWhenAccountsExist;
protected final boolean cannotResumeWhenNoAccountsExist;
public static final int CAN_ALWAYS_RESUME = 0;
public static final int CANNOT_RESUME_WHEN_ACCOUNTS_EXIST = 1 << 0;
public static final int CANNOT_RESUME_WHEN_NO_ACCOUNTS_EXIST = 1 << 1;
public FxAccountAbstractActivity(int resume) {
super();
this.cannotResumeWhenAccountsExist = 0 != (resume & CANNOT_RESUME_WHEN_ACCOUNTS_EXIST);
this.cannotResumeWhenNoAccountsExist = 0 != (resume & CANNOT_RESUME_WHEN_NO_ACCOUNTS_EXIST);
}
/**
* Many Firefox Accounts activities shouldn't display if an account already
* exists. This function redirects as appropriate.
*
* @return true if redirected.
*/
protected boolean redirectIfAppropriate() {
if (cannotResumeWhenAccountsExist || cannotResumeWhenNoAccountsExist) {
final Account account = FirefoxAccounts.getFirefoxAccount(this);
if (cannotResumeWhenAccountsExist && account != null) {
redirectToAction(FxAccountConstants.ACTION_FXA_STATUS);
return true;
}
if (cannotResumeWhenNoAccountsExist && account == null) {
redirectToAction(FxAccountConstants.ACTION_FXA_GET_STARTED);
return true;
}
}
return false;
}
@Override
public void onResume() {
super.onResume();
redirectIfAppropriate();
}
@Override
public void onBackPressed() {
super.onBackPressed();
overridePendingTransition(0, 0);
}
protected void launchActivity(Class<? extends Activity> activityClass) {
Intent intent = new Intent(this, activityClass);
// Per http://stackoverflow.com/a/8992365, this triggers a known bug with
// the soft keyboard not being shown for the started activity. Why, Android, why?
intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
startActivity(intent);
}
protected void redirectToAction(final String action) {
final Intent intent = new Intent(action);
// Per http://stackoverflow.com/a/8992365, this triggers a known bug with
// the soft keyboard not being shown for the started activity. Why, Android, why?
intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
startActivity(intent);
finish();
}
}

View file

@ -1,11 +0,0 @@
/* 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/. */
package org.mozilla.gecko.fxa.activities;
public class FxAccountConfirmAccountActivityWeb extends FxAccountWebFlowActivity {
public FxAccountConfirmAccountActivityWeb() {
super(CANNOT_RESUME_WHEN_NO_ACCOUNTS_EXIST, "manage");
}
}

View file

@ -1,11 +0,0 @@
/* 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/. */
package org.mozilla.gecko.fxa.activities;
public class FxAccountFinishMigratingActivityWeb extends FxAccountWebFlowActivity {
public FxAccountFinishMigratingActivityWeb() {
super(CANNOT_RESUME_WHEN_NO_ACCOUNTS_EXIST, "signin", "migration=sync11");
}
}

View file

@ -1,11 +0,0 @@
/* 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/. */
package org.mozilla.gecko.fxa.activities;
public class FxAccountGetStartedActivityWeb extends FxAccountWebFlowActivity {
public FxAccountGetStartedActivityWeb() {
super(CANNOT_RESUME_WHEN_ACCOUNTS_EXIST, "signup");
}
}

View file

@ -1,228 +0,0 @@
/* 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/. */
package org.mozilla.gecko.fxa.activities;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.support.v7.app.ActionBar;
import android.support.v7.widget.Toolbar;
import android.util.TypedValue;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.Window;
import android.widget.Toast;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.Locales.LocaleAwareAppCompatActivity;
import org.mozilla.gecko.R;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.sync.Utils;
/**
* Activity which displays account status.
*/
public class FxAccountStatusActivity extends LocaleAwareAppCompatActivity {
private static final String LOG_TAG = FxAccountStatusActivity.class.getSimpleName();
protected FxAccountStatusFragment statusFragment;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Display the fragment as the content.
statusFragment = new FxAccountStatusFragment();
getSupportFragmentManager()
.beginTransaction()
.replace(android.R.id.content, statusFragment)
.commit();
maybeSetHomeButtonEnabled();
}
/**
* Sufficiently recent Android versions need additional code to receive taps
* on the status bar to go "up". See <a
* href="http://stackoverflow.com/a/8953148">this stackoverflow answer</a> for
* more information.
*/
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
protected void maybeSetHomeButtonEnabled() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
Logger.debug(LOG_TAG, "Not enabling home button; version too low.");
return;
}
final ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
Logger.debug(LOG_TAG, "Enabling home button.");
actionBar.setHomeButtonEnabled(true);
actionBar.setDisplayHomeAsUpEnabled(true);
return;
}
Logger.debug(LOG_TAG, "Not enabling home button.");
}
@Override
public void onResume() {
super.onResume();
final AndroidFxAccount fxAccount = getAndroidFxAccount();
if (fxAccount == null) {
Logger.warn(LOG_TAG, "Could not get Firefox Account.");
// Gracefully redirect to get started.
final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_GET_STARTED);
// Per http://stackoverflow.com/a/8992365, this triggers a known bug with
// the soft keyboard not being shown for the started activity. Why, Android, why?
intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
startActivity(intent);
setResult(RESULT_CANCELED);
finish();
return;
}
statusFragment.refresh(fxAccount);
}
/**
* Helper to fetch (unique) Android Firefox Account if one exists, or return null.
*/
protected AndroidFxAccount getAndroidFxAccount() {
Account account = FirefoxAccounts.getFirefoxAccount(this);
if (account == null) {
return null;
}
return new AndroidFxAccount(this, account);
}
/**
* Helper function to maybe remove the given Android account.
*/
@SuppressLint("InlinedApi")
public static void maybeDeleteAndroidAccount(final Activity activity, final Account account, final Intent intent) {
if (account == null) {
Logger.warn(LOG_TAG, "Trying to delete null account; ignoring request.");
return;
}
final AccountManagerCallback<Boolean> callback = new AccountManagerCallback<Boolean>() {
@Override
public void run(AccountManagerFuture<Boolean> future) {
Logger.info(LOG_TAG, "Account " + Utils.obfuscateEmail(account.name) + " removed.");
final String text = activity.getResources().getString(R.string.fxaccount_remove_account_toast, account.name);
Toast.makeText(activity, text, Toast.LENGTH_LONG).show();
if (intent != null) {
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
activity.startActivity(intent);
}
activity.finish();
}
};
/*
* Get the best dialog icon from the theme on v11+.
* See http://stackoverflow.com/questions/14910536/android-dialog-theme-makes-icon-too-light/14910945#14910945.
*/
final int icon;
final TypedValue typedValue = new TypedValue();
activity.getTheme().resolveAttribute(android.R.attr.alertDialogIcon, typedValue, true);
icon = typedValue.resourceId;
final AlertDialog dialog = new AlertDialog.Builder(activity)
.setTitle(R.string.fxaccount_remove_account_dialog_title)
.setIcon(icon)
.setMessage(R.string.fxaccount_remove_account_dialog_message)
.setPositiveButton(android.R.string.ok, new Dialog.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
AccountManager.get(activity).removeAccount(account, callback, null);
}
})
.setNegativeButton(android.R.string.cancel, new Dialog.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
}
})
.create();
dialog.show();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int itemId = item.getItemId();
if (itemId == android.R.id.home) {
finish();
return true;
}
if (itemId == R.id.enable_debug_mode) {
FxAccountUtils.LOG_PERSONAL_INFORMATION = !FxAccountUtils.LOG_PERSONAL_INFORMATION;
Toast.makeText(this, (FxAccountUtils.LOG_PERSONAL_INFORMATION ? "Enabled" : "Disabled") +
" Firefox Account personal information!", Toast.LENGTH_LONG).show();
item.setChecked(!item.isChecked());
// Display or hide debug options.
statusFragment.hardRefresh();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
final MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.fxaccount_status_menu, menu);
// !defined(MOZILLA_OFFICIAL) || defined(NIGHTLY_BUILD) || defined(MOZ_DEBUG)
boolean enabled = !AppConstants.MOZILLA_OFFICIAL || AppConstants.NIGHTLY_BUILD || AppConstants.DEBUG_BUILD;
if (!enabled) {
menu.removeItem(R.id.enable_debug_mode);
} else {
final MenuItem debugModeItem = menu.findItem(R.id.enable_debug_mode);
if (debugModeItem != null) {
// Update checked state based on internal flag.
menu.findItem(R.id.enable_debug_mode).setChecked(FxAccountUtils.LOG_PERSONAL_INFORMATION);
}
}
return super.onCreateOptionsMenu(menu);
};
@Override
public void openOptionsMenu() {
// This is a workaround of an Android bug:
// https://code.google.com/p/android/issues/detail?id=185217
// openOptionsMenu isn't overriden by WindowDecorActionBar, which is used by AppCompatActivity,
// meaning getSupportActionbar().openOptionsMenu doesn't work.
// Based loosely on the code in:
// http://androidxref.com/6.0.1_r10/xref/frameworks/support/v7/appcompat/src/android/support/v7/internal/app/WindowDecorActionBar.java#getDecorToolbar
final Window window = getWindow();
final View decor = window.getDecorView();
final View view = decor.findViewById(R.id.action_bar);
if (view instanceof Toolbar) {
final Toolbar toolbar = (Toolbar) view;
toolbar.showOverflowMenu();
}
}
}

View file

@ -1,949 +0,0 @@
/* 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/. */
package org.mozilla.gecko.fxa.activities;
import android.accounts.Account;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Handler;
import android.preference.CheckBoxPreference;
import android.preference.EditTextPreference;
import android.preference.Preference;
import android.preference.Preference.OnPreferenceChangeListener;
import android.preference.Preference.OnPreferenceClickListener;
import android.preference.PreferenceCategory;
import android.preference.PreferenceScreen;
import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;
import android.text.format.DateUtils;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.R;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.background.preferences.PreferenceFragment;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.SyncStatusListener;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.login.Married;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.fxa.sync.FxAccountSyncStatusHelper;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate;
import org.mozilla.gecko.sync.SyncConfiguration;
import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
import org.mozilla.gecko.util.HardwareUtils;
import org.mozilla.gecko.util.ThreadUtils;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* A fragment that displays the status of an AndroidFxAccount.
* <p>
* The owning activity is responsible for providing an AndroidFxAccount at
* appropriate times.
*/
public class FxAccountStatusFragment
extends PreferenceFragment
implements OnPreferenceClickListener, OnPreferenceChangeListener {
private static final String LOG_TAG = FxAccountStatusFragment.class.getSimpleName();
/**
* If a device claims to have synced before this date, we will assume it has never synced.
*/
private static final Date EARLIEST_VALID_SYNCED_DATE;
static {
final Calendar c = GregorianCalendar.getInstance();
c.set(2000, Calendar.JANUARY, 1, 0, 0, 0);
EARLIEST_VALID_SYNCED_DATE = c.getTime();
}
// When a checkbox is toggled, wait 5 seconds (for other checkbox actions)
// before trying to sync. Should we kill off the fragment before the sync
// request happens, that's okay: the runnable will run if the UI thread is
// still around to service it, and since we're not updating any UI, we'll just
// schedule the sync as usual. See also comment below about garbage
// collection.
private static final long DELAY_IN_MILLISECONDS_BEFORE_REQUESTING_SYNC = 5 * 1000;
private static final long LAST_SYNCED_TIME_UPDATE_INTERVAL_IN_MILLISECONDS = 60 * 1000;
private static final long PROFILE_FETCH_RETRY_INTERVAL_IN_MILLISECONDS = 60 * 1000;
private static final String[] STAGES_TO_SYNC_ON_DEVICE_NAME_CHANGE = new String[] { "clients" };
// By default, the auth/account server preference is only shown when the
// account is configured to use a custom server. In debug mode, this is set.
private static boolean ALWAYS_SHOW_AUTH_SERVER = false;
// By default, the Sync server preference is only shown when the account is
// configured to use a custom Sync server. In debug mode, this is set.
private static boolean ALWAYS_SHOW_SYNC_SERVER = false;
protected PreferenceCategory accountCategory;
protected Preference profilePreference;
protected Preference manageAccountPreference;
protected Preference authServerPreference;
protected Preference removeAccountPreference;
protected Preference needsPasswordPreference;
protected Preference needsUpgradePreference;
protected Preference needsVerificationPreference;
protected Preference needsMasterSyncAutomaticallyEnabledPreference;
protected Preference needsFinishMigratingPreference;
protected PreferenceCategory syncCategory;
protected CheckBoxPreference bookmarksPreference;
protected CheckBoxPreference historyPreference;
protected CheckBoxPreference tabsPreference;
protected CheckBoxPreference passwordsPreference;
protected CheckBoxPreference readingListPreference;
protected EditTextPreference deviceNamePreference;
protected Preference syncServerPreference;
protected Preference morePreference;
protected Preference syncNowPreference;
protected volatile AndroidFxAccount fxAccount;
// The contract is: when fxAccount is non-null, then clientsDataDelegate is
// non-null. If violated then an IllegalStateException is thrown.
protected volatile SharedPreferencesClientsDataDelegate clientsDataDelegate;
// Used to post delayed sync requests.
protected Handler handler;
// Member variable so that re-posting pushes back the already posted instance.
// This Runnable references the fxAccount above, but it is not specific to a
// single account. (That is, it does not capture a single account instance.)
protected Runnable requestSyncRunnable;
// Runnable to update last synced time.
protected Runnable lastSyncedTimeUpdateRunnable;
// Broadcast Receiver to update profile Information.
protected FxAccountProfileInformationReceiver accountProfileInformationReceiver;
protected final InnerSyncStatusDelegate syncStatusDelegate = new InnerSyncStatusDelegate();
private Target profileAvatarTarget;
protected Preference ensureFindPreference(String key) {
Preference preference = findPreference(key);
if (preference == null) {
throw new IllegalStateException("Could not find preference with key: " + key);
}
return preference;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// We need to do this before we can query the hardware menu button state.
// We're guaranteed to have an activity at this point (onAttach is called
// before onCreate). It's okay to call this multiple times (with different
// contexts).
HardwareUtils.init(getActivity());
addPreferences();
}
protected void addPreferences() {
addPreferencesFromResource(R.xml.fxaccount_status_prefscreen);
accountCategory = (PreferenceCategory) ensureFindPreference("signed_in_as_category");
profilePreference = ensureFindPreference("profile");
manageAccountPreference = ensureFindPreference("manage_account");
authServerPreference = ensureFindPreference("auth_server");
removeAccountPreference = ensureFindPreference("remove_account");
needsPasswordPreference = ensureFindPreference("needs_credentials");
needsUpgradePreference = ensureFindPreference("needs_upgrade");
needsVerificationPreference = ensureFindPreference("needs_verification");
needsMasterSyncAutomaticallyEnabledPreference = ensureFindPreference("needs_master_sync_automatically_enabled");
needsFinishMigratingPreference = ensureFindPreference("needs_finish_migrating");
syncCategory = (PreferenceCategory) ensureFindPreference("sync_category");
bookmarksPreference = (CheckBoxPreference) ensureFindPreference("bookmarks");
historyPreference = (CheckBoxPreference) ensureFindPreference("history");
tabsPreference = (CheckBoxPreference) ensureFindPreference("tabs");
passwordsPreference = (CheckBoxPreference) ensureFindPreference("passwords");
if (!FxAccountUtils.LOG_PERSONAL_INFORMATION) {
removeDebugButtons();
} else {
connectDebugButtons();
ALWAYS_SHOW_AUTH_SERVER = true;
ALWAYS_SHOW_SYNC_SERVER = true;
}
profilePreference.setOnPreferenceClickListener(this);
manageAccountPreference.setOnPreferenceClickListener(this);
removeAccountPreference.setOnPreferenceClickListener(this);
needsPasswordPreference.setOnPreferenceClickListener(this);
needsVerificationPreference.setOnPreferenceClickListener(this);
needsFinishMigratingPreference.setOnPreferenceClickListener(this);
bookmarksPreference.setOnPreferenceClickListener(this);
historyPreference.setOnPreferenceClickListener(this);
tabsPreference.setOnPreferenceClickListener(this);
passwordsPreference.setOnPreferenceClickListener(this);
deviceNamePreference = (EditTextPreference) ensureFindPreference("device_name");
deviceNamePreference.setOnPreferenceChangeListener(this);
syncServerPreference = ensureFindPreference("sync_server");
morePreference = ensureFindPreference("more");
morePreference.setOnPreferenceClickListener(this);
syncNowPreference = ensureFindPreference("sync_now");
syncNowPreference.setEnabled(true);
syncNowPreference.setOnPreferenceClickListener(this);
ensureFindPreference("linktos").setOnPreferenceClickListener(this);
ensureFindPreference("linkprivacy").setOnPreferenceClickListener(this);
}
/**
* We intentionally don't refresh here. Our owning activity is responsible for
* providing an AndroidFxAccount to our refresh method in its onResume method.
*/
@Override
public void onResume() {
super.onResume();
}
@Override
public boolean onPreferenceClick(Preference preference) {
if (preference == profilePreference) {
ActivityUtils.openURLInFennec(getActivity().getApplicationContext(), "about:accounts?action=avatar");
return true;
}
if (preference == manageAccountPreference) {
ActivityUtils.openURLInFennec(getActivity().getApplicationContext(), "about:accounts?action=manage");
return true;
}
if (preference == removeAccountPreference) {
FxAccountStatusActivity.maybeDeleteAndroidAccount(getActivity(), fxAccount.getAndroidAccount(), null);
return true;
}
if (preference == needsPasswordPreference) {
final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_UPDATE_CREDENTIALS);
intent.putExtra(FxAccountWebFlowActivity.EXTRA_ENDPOINT, FxAccountConstants.ENDPOINT_PREFERENCES);
// Per http://stackoverflow.com/a/8992365, this triggers a known bug with
// the soft keyboard not being shown for the started activity. Why, Android, why?
intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
startActivity(intent);
return true;
}
if (preference == needsFinishMigratingPreference) {
final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_FINISH_MIGRATING);
intent.putExtra(FxAccountWebFlowActivity.EXTRA_ENDPOINT, FxAccountConstants.ENDPOINT_PREFERENCES);
// Per http://stackoverflow.com/a/8992365, this triggers a known bug with
// the soft keyboard not being shown for the started activity. Why, Android, why?
intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
startActivity(intent);
return true;
}
if (preference == needsVerificationPreference) {
final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_CONFIRM_ACCOUNT);
// Per http://stackoverflow.com/a/8992365, this triggers a known bug with
// the soft keyboard not being shown for the started activity. Why, Android, why?
intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
intent.putExtra(FxAccountWebFlowActivity.EXTRA_ENDPOINT, FxAccountConstants.ENDPOINT_PREFERENCES);
startActivity(intent);
return true;
}
if (preference == bookmarksPreference ||
preference == historyPreference ||
preference == passwordsPreference ||
preference == tabsPreference) {
saveEngineSelections();
return true;
}
if (preference == morePreference) {
getActivity().openOptionsMenu();
return true;
}
if (preference == syncNowPreference) {
if (fxAccount != null) {
fxAccount.requestImmediateSync(null, null);
}
return true;
}
if (TextUtils.equals("linktos", preference.getKey())) {
ActivityUtils.openURLInFennec(getActivity().getApplicationContext(), getResources().getString(R.string.fxaccount_link_tos));
return true;
}
if (TextUtils.equals("linkprivacy", preference.getKey())) {
ActivityUtils.openURLInFennec(getActivity().getApplicationContext(), getResources().getString(R.string.fxaccount_link_pn));
return true;
}
return false;
}
protected void setCheckboxesEnabled(boolean enabled) {
bookmarksPreference.setEnabled(enabled);
historyPreference.setEnabled(enabled);
tabsPreference.setEnabled(enabled);
passwordsPreference.setEnabled(enabled);
// Since we can't sync, we can't update our remote client record.
deviceNamePreference.setEnabled(enabled);
syncNowPreference.setEnabled(enabled);
}
/**
* Show at most one error preference, hiding all others.
*
* @param errorPreferenceToShow
* single error preference to show; if null, hide all error preferences
*/
protected void showOnlyOneErrorPreference(Preference errorPreferenceToShow) {
final Preference[] errorPreferences = new Preference[] {
this.needsPasswordPreference,
this.needsUpgradePreference,
this.needsVerificationPreference,
this.needsMasterSyncAutomaticallyEnabledPreference,
this.needsFinishMigratingPreference,
};
for (Preference errorPreference : errorPreferences) {
final boolean currentlyShown = null != findPreference(errorPreference.getKey());
final boolean shouldBeShown = errorPreference == errorPreferenceToShow;
if (currentlyShown == shouldBeShown) {
continue;
}
if (shouldBeShown) {
syncCategory.addPreference(errorPreference);
} else {
syncCategory.removePreference(errorPreference);
}
}
}
protected void showNeedsPassword() {
syncCategory.setTitle(R.string.fxaccount_status_sync);
showOnlyOneErrorPreference(needsPasswordPreference);
setCheckboxesEnabled(false);
}
protected void showNeedsUpgrade() {
syncCategory.setTitle(R.string.fxaccount_status_sync);
showOnlyOneErrorPreference(needsUpgradePreference);
setCheckboxesEnabled(false);
}
protected void showNeedsVerification() {
syncCategory.setTitle(R.string.fxaccount_status_sync);
showOnlyOneErrorPreference(needsVerificationPreference);
setCheckboxesEnabled(false);
}
protected void showNeedsMasterSyncAutomaticallyEnabled() {
syncCategory.setTitle(R.string.fxaccount_status_sync);
needsMasterSyncAutomaticallyEnabledPreference.setTitle(AppConstants.Versions.preLollipop ?
R.string.fxaccount_status_needs_master_sync_automatically_enabled :
R.string.fxaccount_status_needs_master_sync_automatically_enabled_v21);
showOnlyOneErrorPreference(needsMasterSyncAutomaticallyEnabledPreference);
setCheckboxesEnabled(false);
}
protected void showNeedsFinishMigrating() {
syncCategory.setTitle(R.string.fxaccount_status_sync);
showOnlyOneErrorPreference(needsFinishMigratingPreference);
setCheckboxesEnabled(false);
}
protected void showConnected() {
syncCategory.setTitle(R.string.fxaccount_status_sync_enabled);
showOnlyOneErrorPreference(null);
setCheckboxesEnabled(true);
}
protected class InnerSyncStatusDelegate implements SyncStatusListener {
protected final Runnable refreshRunnable = new Runnable() {
@Override
public void run() {
refresh();
}
};
@Override
public Context getContext() {
return FxAccountStatusFragment.this.getActivity();
}
@Override
public Account getAccount() {
return fxAccount.getAndroidAccount();
}
@Override
public void onSyncStarted() {
if (fxAccount == null) {
return;
}
Logger.info(LOG_TAG, "Got sync started message; refreshing.");
getActivity().runOnUiThread(refreshRunnable);
}
@Override
public void onSyncFinished() {
if (fxAccount == null) {
return;
}
Logger.info(LOG_TAG, "Got sync finished message; refreshing.");
getActivity().runOnUiThread(refreshRunnable);
}
}
/**
* Notify the fragment that a new AndroidFxAccount instance is current.
* <p>
* <b>Important:</b> call this method on the UI thread!
* <p>
* In future, this might be a Loader.
*
* @param fxAccount new instance.
*/
public void refresh(AndroidFxAccount fxAccount) {
if (fxAccount == null) {
throw new IllegalArgumentException("fxAccount must not be null");
}
this.fxAccount = fxAccount;
try {
this.clientsDataDelegate = new SharedPreferencesClientsDataDelegate(fxAccount.getSyncPrefs(), getActivity().getApplicationContext());
} catch (Exception e) {
Logger.error(LOG_TAG, "Got exception fetching Sync prefs associated to Firefox Account; aborting.", e);
// Something is terribly wrong; best to get a stack trace rather than
// continue with a null clients delegate.
throw new IllegalStateException(e);
}
handler = new Handler(); // Attached to current (assumed to be UI) thread.
// Runnable is not specific to one Firefox Account. This runnable will keep
// a reference to this fragment alive, but we expect posted runnables to be
// serviced very quickly, so this is not an issue.
requestSyncRunnable = new RequestSyncRunnable();
lastSyncedTimeUpdateRunnable = new LastSyncTimeUpdateRunnable();
// We would very much like register these status observers in bookended
// onResume/onPause calls, but because the Fragment gets onResume during the
// Activity's super.onResume, it hasn't yet been told its Firefox Account.
// So we register the observer here (and remove it in onPause), and open
// ourselves to the possibility that we don't have properly paired
// register/unregister calls.
FxAccountSyncStatusHelper.getInstance().startObserving(syncStatusDelegate);
// Register a local broadcast receiver to get profile cached notification.
final IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(FxAccountConstants.ACCOUNT_PROFILE_JSON_UPDATED_ACTION);
accountProfileInformationReceiver = new FxAccountProfileInformationReceiver();
LocalBroadcastManager.getInstance(getActivity()).registerReceiver(accountProfileInformationReceiver, intentFilter);
// profilePreference is set during onCreate, so it's definitely not null here.
final float cornerRadius = getResources().getDimension(R.dimen.fxaccount_profile_image_width) / 2;
profileAvatarTarget = new PicassoPreferenceIconTarget(getResources(), profilePreference, cornerRadius);
refresh();
}
@Override
public void onPause() {
super.onPause();
FxAccountSyncStatusHelper.getInstance().stopObserving(syncStatusDelegate);
// Focus lost, remove scheduled update if any.
if (lastSyncedTimeUpdateRunnable != null) {
handler.removeCallbacks(lastSyncedTimeUpdateRunnable);
}
// Focus lost, unregister broadcast receiver.
if (accountProfileInformationReceiver != null) {
LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(accountProfileInformationReceiver);
}
if (profileAvatarTarget != null) {
Picasso.with(getActivity()).cancelRequest(profileAvatarTarget);
profileAvatarTarget = null;
}
}
protected void hardRefresh() {
// This is the only way to guarantee that the EditText dialogs created by
// EditTextPreferences are re-created. This works around the issue described
// at http://androiddev.orkitra.com/?p=112079.
final PreferenceScreen statusScreen = (PreferenceScreen) ensureFindPreference("status_screen");
statusScreen.removeAll();
addPreferences();
refresh();
}
protected void refresh() {
// refresh is called from our onResume, which can happen before the owning
// Activity tells us about an account (via our public
// refresh(AndroidFxAccount) method).
if (fxAccount == null) {
throw new IllegalArgumentException("fxAccount must not be null");
}
updateProfileInformation();
updateAuthServerPreference();
updateSyncServerPreference();
try {
// There are error states determined by Android, not the login state
// machine, and we have a chance to present these states here. We handle
// them specially, since we can't surface these states as part of syncing,
// because they generally stop syncs from happening regularly. Right now
// there are no such states.
// Interrogate the Firefox Account's state.
State state = fxAccount.getState();
switch (state.getNeededAction()) {
case NeedsUpgrade:
showNeedsUpgrade();
break;
case NeedsPassword:
showNeedsPassword();
break;
case NeedsVerification:
showNeedsVerification();
break;
case NeedsFinishMigrating:
showNeedsFinishMigrating();
break;
case None:
showConnected();
break;
}
// We check for the master setting last, since it is not strictly
// necessary for the user to address this error state: it's really a
// warning state. We surface it for the user's convenience, and to prevent
// confused folks wondering why Sync is not working at all.
final boolean masterSyncAutomatically = ContentResolver.getMasterSyncAutomatically();
if (!masterSyncAutomatically) {
showNeedsMasterSyncAutomaticallyEnabled();
return;
}
} finally {
// No matter our state, we should update the checkboxes.
updateSelectedEngines();
}
final String clientName = clientsDataDelegate.getClientName();
deviceNamePreference.setSummary(clientName);
deviceNamePreference.setText(clientName);
updateSyncNowPreference();
}
// This is a helper function similar to TabsAccessor.getLastSyncedString() to calculate relative "Last synced" time span.
private String getLastSyncedString(final long startTime) {
if (new Date(startTime).before(EARLIEST_VALID_SYNCED_DATE)) {
return getActivity().getString(R.string.fxaccount_status_never_synced);
}
final CharSequence relativeTimeSpanString = DateUtils.getRelativeTimeSpanString(startTime);
return getActivity().getResources().getString(R.string.fxaccount_status_last_synced, relativeTimeSpanString);
}
protected void updateSyncNowPreference() {
final boolean currentlySyncing = fxAccount.isCurrentlySyncing();
syncNowPreference.setEnabled(!currentlySyncing);
if (currentlySyncing) {
syncNowPreference.setTitle(R.string.fxaccount_status_syncing);
} else {
syncNowPreference.setTitle(R.string.fxaccount_status_sync_now);
}
scheduleAndUpdateLastSyncedTime();
}
private void updateProfileInformation() {
final ExtendedJSONObject profileJSON = fxAccount.getProfileJSON();
if (profileJSON == null) {
// Update the profile title with email as the fallback.
// Profile icon by default use the default avatar as the fallback.
profilePreference.setTitle(fxAccount.getEmail());
return;
}
updateProfileInformation(profileJSON);
}
/**
* Update profile information from json on UI thread.
*
* @param profileJSON json fetched from server.
*/
protected void updateProfileInformation(final ExtendedJSONObject profileJSON) {
// View changes must always be done on UI thread.
ThreadUtils.assertOnUiThread();
FxAccountUtils.pii(LOG_TAG, "Profile JSON is: " + profileJSON.toJSONString());
final String userName = profileJSON.getString(FxAccountConstants.KEY_PROFILE_JSON_USERNAME);
// Update the profile username and email if available.
if (!TextUtils.isEmpty(userName)) {
profilePreference.setTitle(userName);
profilePreference.setSummary(fxAccount.getEmail());
} else {
profilePreference.setTitle(fxAccount.getEmail());
}
// Avatar URI empty, skip profile image fetch.
final String avatarURI = profileJSON.getString(FxAccountConstants.KEY_PROFILE_JSON_AVATAR);
if (TextUtils.isEmpty(avatarURI)) {
Logger.info(LOG_TAG, "AvatarURI is empty, skipping profile image fetch.");
return;
}
// Using noPlaceholder would avoid a pop of the default image, but it's not available in the version of Picasso
// we ship in the tree.
Picasso
.with(getActivity())
.load(avatarURI)
.centerInside()
.resizeDimen(R.dimen.fxaccount_profile_image_width, R.dimen.fxaccount_profile_image_height)
.placeholder(R.drawable.sync_avatar_default)
.error(R.drawable.sync_avatar_default)
.into(profileAvatarTarget);
}
private void scheduleAndUpdateLastSyncedTime() {
final String lastSynced = getLastSyncedString(fxAccount.getLastSyncedTimestamp());
syncNowPreference.setSummary(lastSynced);
handler.postDelayed(lastSyncedTimeUpdateRunnable, LAST_SYNCED_TIME_UPDATE_INTERVAL_IN_MILLISECONDS);
}
protected void updateAuthServerPreference() {
final String authServer = fxAccount.getAccountServerURI();
final boolean shouldBeShown = ALWAYS_SHOW_AUTH_SERVER || !FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT.equals(authServer);
final boolean currentlyShown = null != findPreference(authServerPreference.getKey());
if (currentlyShown != shouldBeShown) {
if (shouldBeShown) {
accountCategory.addPreference(authServerPreference);
} else {
accountCategory.removePreference(authServerPreference);
}
}
// Always set the summary, because on first run, the preference is visible,
// and the above block will be skipped if there is a custom value.
authServerPreference.setSummary(authServer);
}
protected void updateSyncServerPreference() {
final String syncServer = fxAccount.getTokenServerURI();
final boolean shouldBeShown = ALWAYS_SHOW_SYNC_SERVER || !FxAccountConstants.DEFAULT_TOKEN_SERVER_ENDPOINT.equals(syncServer);
final boolean currentlyShown = null != findPreference(syncServerPreference.getKey());
if (currentlyShown != shouldBeShown) {
if (shouldBeShown) {
syncCategory.addPreference(syncServerPreference);
} else {
syncCategory.removePreference(syncServerPreference);
}
}
// Always set the summary, because on first run, the preference is visible,
// and the above block will be skipped if there is a custom value.
syncServerPreference.setSummary(syncServer);
}
/**
* Query shared prefs for the current engine state, and update the UI
* accordingly.
* <p>
* In future, we might want this to be on a background thread, or implemented
* as a Loader.
*/
protected void updateSelectedEngines() {
try {
SharedPreferences syncPrefs = fxAccount.getSyncPrefs();
Map<String, Boolean> engines = SyncConfiguration.getUserSelectedEngines(syncPrefs);
if (engines != null) {
bookmarksPreference.setChecked(engines.containsKey("bookmarks") && engines.get("bookmarks"));
historyPreference.setChecked(engines.containsKey("history") && engines.get("history"));
passwordsPreference.setChecked(engines.containsKey("passwords") && engines.get("passwords"));
tabsPreference.setChecked(engines.containsKey("tabs") && engines.get("tabs"));
return;
}
// We don't have user specified preferences. Perhaps we have seen a meta/global?
Set<String> enabledNames = SyncConfiguration.getEnabledEngineNames(syncPrefs);
if (enabledNames != null) {
bookmarksPreference.setChecked(enabledNames.contains("bookmarks"));
historyPreference.setChecked(enabledNames.contains("history"));
passwordsPreference.setChecked(enabledNames.contains("passwords"));
tabsPreference.setChecked(enabledNames.contains("tabs"));
return;
}
// Okay, we don't have userSelectedEngines or enabledEngines. That means
// the user hasn't specified to begin with, we haven't specified here, and
// we haven't already seen, Sync engines. We don't know our state, so
// let's check everything (the default) and disable everything.
bookmarksPreference.setChecked(true);
historyPreference.setChecked(true);
passwordsPreference.setChecked(true);
tabsPreference.setChecked(true);
setCheckboxesEnabled(false);
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception getting engines to select; ignoring.", e);
return;
}
}
/**
* Persist engine selections to local shared preferences, and request a sync
* to persist selections to remote storage.
*/
protected void saveEngineSelections() {
final Map<String, Boolean> engineSelections = new HashMap<String, Boolean>();
engineSelections.put("bookmarks", bookmarksPreference.isChecked());
engineSelections.put("history", historyPreference.isChecked());
engineSelections.put("passwords", passwordsPreference.isChecked());
engineSelections.put("tabs", tabsPreference.isChecked());
// No GlobalSession.config, so store directly to shared prefs. We do this on
// a background thread to avoid IO on the main thread and strict mode
// warnings.
new Thread(new PersistEngineSelectionsRunnable(engineSelections)).start();
}
protected void requestDelayedSync() {
Logger.info(LOG_TAG, "Posting a delayed request for a sync sometime soon.");
handler.removeCallbacks(requestSyncRunnable);
handler.postDelayed(requestSyncRunnable, DELAY_IN_MILLISECONDS_BEFORE_REQUESTING_SYNC);
}
/**
* Remove all traces of debug buttons. By default, no debug buttons are shown.
*/
protected void removeDebugButtons() {
final PreferenceScreen statusScreen = (PreferenceScreen) ensureFindPreference("status_screen");
final PreferenceCategory debugCategory = (PreferenceCategory) ensureFindPreference("debug_category");
statusScreen.removePreference(debugCategory);
}
/**
* A Runnable that persists engine selections to shared prefs, and then
* requests a delayed sync.
* <p>
* References the member <code>fxAccount</code> and is specific to the Android
* account associated to that account.
*/
protected class PersistEngineSelectionsRunnable implements Runnable {
private final Map<String, Boolean> engineSelections;
protected PersistEngineSelectionsRunnable(Map<String, Boolean> engineSelections) {
this.engineSelections = engineSelections;
}
@Override
public void run() {
try {
// Name shadowing -- do you like it, or do you love it?
AndroidFxAccount fxAccount = FxAccountStatusFragment.this.fxAccount;
if (fxAccount == null) {
return;
}
Logger.info(LOG_TAG, "Persisting engine selections: " + engineSelections.toString());
SyncConfiguration.storeSelectedEnginesToPrefs(fxAccount.getSyncPrefs(), engineSelections);
requestDelayedSync();
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception persisting selected engines; ignoring.", e);
return;
}
}
}
/**
* A Runnable that requests a sync.
* <p>
* References the member <code>fxAccount</code>, but is not specific to the
* Android account associated to that account.
*/
protected class RequestSyncRunnable implements Runnable {
@Override
public void run() {
// Name shadowing -- do you like it, or do you love it?
AndroidFxAccount fxAccount = FxAccountStatusFragment.this.fxAccount;
if (fxAccount == null) {
return;
}
Logger.info(LOG_TAG, "Requesting a sync sometime soon.");
fxAccount.requestEventualSync(null, null);
}
}
/**
* The Runnable that schedules a future update and updates the last synced time.
*/
protected class LastSyncTimeUpdateRunnable implements Runnable {
@Override
public void run() {
scheduleAndUpdateLastSyncedTime();
}
}
/**
* Broadcast receiver to receive updates for the cached profile action.
*/
public class FxAccountProfileInformationReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (!intent.getAction().equals(FxAccountConstants.ACCOUNT_PROFILE_JSON_UPDATED_ACTION)) {
return;
}
Logger.info(LOG_TAG, "Profile avatar cache update action broadcast received.");
// Update the UI from cached profile json on the main thread.
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
updateProfileInformation();
}
});
}
}
/**
* A separate listener to separate debug logic from main code paths.
*/
protected class DebugPreferenceClickListener implements OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {
final String key = preference.getKey();
if ("debug_refresh".equals(key)) {
Logger.info(LOG_TAG, "Refreshing.");
refresh();
} else if ("debug_dump".equals(key)) {
fxAccount.dump();
} else if ("debug_force_sync".equals(key)) {
Logger.info(LOG_TAG, "Force syncing.");
fxAccount.requestImmediateSync(null, null);
// No sense refreshing, since the sync will complete in the future.
} else if ("debug_forget_certificate".equals(key)) {
State state = fxAccount.getState();
try {
Married married = (Married) state;
Logger.info(LOG_TAG, "Moving to Cohabiting state: Forgetting certificate.");
fxAccount.setState(married.makeCohabitingState());
refresh();
} catch (ClassCastException e) {
Logger.info(LOG_TAG, "Not in Married state; can't forget certificate.");
// Ignore.
}
} else if ("debug_invalidate_certificate".equals(key)) {
State state = fxAccount.getState();
try {
Married married = (Married) state;
Logger.info(LOG_TAG, "Invalidating certificate.");
fxAccount.setState(married.makeCohabitingState().withCertificate("INVALID CERTIFICATE"));
refresh();
} catch (ClassCastException e) {
Logger.info(LOG_TAG, "Not in Married state; can't invalidate certificate.");
// Ignore.
}
} else if ("debug_require_password".equals(key)) {
Logger.info(LOG_TAG, "Moving to Separated state: Forgetting password.");
State state = fxAccount.getState();
fxAccount.setState(state.makeSeparatedState());
refresh();
} else if ("debug_require_upgrade".equals(key)) {
Logger.info(LOG_TAG, "Moving to Doghouse state: Requiring upgrade.");
State state = fxAccount.getState();
fxAccount.setState(state.makeDoghouseState());
refresh();
} else if ("debug_migrated_from_sync11".equals(key)) {
Logger.info(LOG_TAG, "Moving to MigratedFromSync11 state: Requiring password.");
State state = fxAccount.getState();
fxAccount.setState(state.makeMigratedFromSync11State(null));
refresh();
} else if ("debug_make_account_stage".equals(key)) {
Logger.info(LOG_TAG, "Moving Account endpoints, in place, to stage. Deleting Sync and RL prefs and requiring password.");
fxAccount.unsafeTransitionToStageEndpoints();
refresh();
} else if ("debug_make_account_default".equals(key)) {
Logger.info(LOG_TAG, "Moving Account endpoints, in place, to default (production). Deleting Sync and RL prefs and requiring password.");
fxAccount.unsafeTransitionToDefaultEndpoints();
refresh();
} else {
return false;
}
return true;
}
}
/**
* Iterate through debug buttons, adding a special debug preference click
* listener to each of them.
*/
protected void connectDebugButtons() {
// Separate listener to really separate debug logic from main code paths.
final OnPreferenceClickListener listener = new DebugPreferenceClickListener();
// We don't want to use Android resource strings for debug UI, so we just
// use the keys throughout.
final PreferenceCategory debugCategory = (PreferenceCategory) ensureFindPreference("debug_category");
debugCategory.setTitle(debugCategory.getKey());
for (int i = 0; i < debugCategory.getPreferenceCount(); i++) {
final Preference button = debugCategory.getPreference(i);
button.setTitle(button.getKey()); // Not very friendly, but this is for debugging only!
button.setOnPreferenceClickListener(listener);
}
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
if (preference == deviceNamePreference) {
String newClientName = (String) newValue;
if (TextUtils.isEmpty(newClientName)) {
newClientName = clientsDataDelegate.getDefaultClientName();
}
final long now = System.currentTimeMillis();
clientsDataDelegate.setClientName(newClientName, now);
// Force sync the client record, we want the user to see the device name change immediately
// on the FxA Device Manager if possible ( = we are online) to avoid confusion
// ("I changed my Android's device name but I don't see it on my computer").
fxAccount.requestImmediateSync(STAGES_TO_SYNC_ON_DEVICE_NAME_CHANGE, null);
hardRefresh(); // Updates the value displayed to the user, among other things.
return true;
}
// For everything else, accept the change.
return true;
}
}

View file

@ -1,11 +0,0 @@
/* 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/. */
package org.mozilla.gecko.fxa.activities;
public class FxAccountUpdateCredentialsActivityWeb extends FxAccountWebFlowActivity {
public FxAccountUpdateCredentialsActivityWeb() {
super(CANNOT_RESUME_WHEN_NO_ACCOUNTS_EXIST, "force_auth");
}
}

View file

@ -1,91 +0,0 @@
/* 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/. */
package org.mozilla.gecko.fxa.activities;
import android.content.Intent;
import android.os.Bundle;
import org.mozilla.gecko.Locales;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
/**
* Activity which shows the status activity or passes through to web flow.
*/
public abstract class FxAccountWebFlowActivity extends FxAccountAbstractActivity {
protected static final String LOG_TAG = FxAccountWebFlowActivity.class.getSimpleName();
protected static final String ABOUT_ACCOUNTS = "about:accounts";
public static final String EXTRA_ENDPOINT = "entrypoint";
protected static final String[] EXTRAS_TO_PASSTHROUGH = new String[] {
EXTRA_ENDPOINT,
};
private final String action;
private final String extras;
public FxAccountWebFlowActivity(int resume, String action) {
this(resume, action, null);
}
public FxAccountWebFlowActivity(int resume, String action, String extras) {
super(resume);
this.action = action;
this.extras = (extras != null) ? ("&" + extras) : "";
}
/**
* {@inheritDoc}
*/
@Override
public void onCreate(Bundle icicle) {
Logger.setThreadLogTag(FxAccountConstants.GLOBAL_LOG_TAG);
Logger.debug(LOG_TAG, "onCreate(" + icicle + ")");
Locales.initializeLocale(getApplicationContext());
super.onCreate(icicle);
}
protected boolean redirectIfAppropriate() {
final boolean redirected = super.redirectIfAppropriate();
if (redirected) {
return true;
}
final StringBuilder sb = new StringBuilder();
sb.append(ABOUT_ACCOUNTS);
sb.append("?action=");
sb.append(action);
sb.append(extras);
// Pass through a set of known string values from intent extras to about:accounts.
final Intent intent = getIntent();
if (intent != null) {
for (String key : EXTRAS_TO_PASSTHROUGH) {
final String value = intent.getStringExtra(key);
if (value != null) {
sb.append("&");
sb.append(key);
sb.append("=");
sb.append(value);
}
}
}
ActivityUtils.openURLInFennec(getApplicationContext(), sb.toString());
return true;
}
@Override
public void onResume() {
super.onResume();
// We are always redirected.
this.finish();
}
}

View file

@ -1,63 +0,0 @@
/* 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/. */
package org.mozilla.gecko.fxa.activities;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.preference.Preference;
import android.support.v4.graphics.drawable.RoundedBitmapDrawable;
import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target;
import org.mozilla.gecko.AppConstants;
/**
* A Picasso Target that updates a preference icon.
*
* Nota bene: Android grew support for updating preference icons programatically
* only in API 11. This class silently ignores requests before API 11.
*/
public class PicassoPreferenceIconTarget implements Target {
private final Preference preference;
private final Resources resources;
private final float cornerRadius;
public PicassoPreferenceIconTarget(Resources resources, Preference preference) {
this(resources, preference, 0);
}
public PicassoPreferenceIconTarget(Resources resources, Preference preference, float cornerRadius) {
this.resources = resources;
this.preference = preference;
this.cornerRadius = cornerRadius;
}
@Override
public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
final Drawable drawable;
if (cornerRadius > 0) {
final RoundedBitmapDrawable roundedBitmapDrawable;
roundedBitmapDrawable = RoundedBitmapDrawableFactory.create(resources, bitmap);
roundedBitmapDrawable.setCornerRadius(cornerRadius);
roundedBitmapDrawable.setAntiAlias(true);
drawable = roundedBitmapDrawable;
} else {
drawable = new BitmapDrawable(resources, bitmap);
}
preference.setIcon(drawable);
}
@Override
public void onBitmapFailed(Drawable errorDrawable) {
preference.setIcon(errorDrawable);
}
@Override
public void onPrepareLoad(Drawable placeHolderDrawable) {
preference.setIcon(placeHolderDrawable);
}
}

View file

@ -1,362 +0,0 @@
/* 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/. */
package org.mozilla.gecko.fxa.authenticator;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.fxa.login.State.StateLabel;
import org.mozilla.gecko.fxa.login.StateFactory;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.NonObjectJSONException;
import org.mozilla.gecko.sync.Utils;
import android.content.Context;
/**
* Android deletes Account objects when the Authenticator that owns the Account
* disappears. This happens when an App is installed to the SD card and the SD
* card is un-mounted or the device is rebooted.
* <p>
* We work around this by pickling the current Firefox account data every sync
* and unpickling when we check if Firefox accounts exist (called from Fennec).
* <p>
* Android just doesn't support installing Apps that define long-lived Services
* and/or own Account types onto the SD card. The documentation says not to do
* it. There are hordes of developers who want to do it, and have tried to
* register for almost every "package installation changed" broadcast intent
* that Android supports. They all explicitly state that the package that has
* changed does *not* receive the broadcast intent, thereby preventing an App
* from re-establishing its state.
* <p>
* <a href="http://developer.android.com/guide/topics/data/install-location.html">Reference.</a>
* <p>
* <b>Quote</b>: Your AbstractThreadedSyncAdapter and all its sync functionality
* will not work until external storage is remounted.
* <p>
* <b>Quote</b>: Your running Service will be killed and will not be restarted
* when external storage is remounted. You can, however, register for the
* ACTION_EXTERNAL_APPLICATIONS_AVAILABLE broadcast Intent, which will notify
* your application when applications installed on external storage have become
* available to the system again. At which time, you can restart your Service.
* <p>
* Problem: <a href="http://code.google.com/p/android/issues/detail?id=8485">that intent doesn't work</a>!
* <p>
* See bug 768102 for more information in the context of Sync.
*/
public class AccountPickler {
public static final String LOG_TAG = AccountPickler.class.getSimpleName();
public static final long PICKLE_VERSION = 3;
public static final String KEY_PICKLE_VERSION = "pickle_version";
public static final String KEY_PICKLE_TIMESTAMP = "pickle_timestamp";
public static final String KEY_ACCOUNT_VERSION = "account_version";
public static final String KEY_ACCOUNT_TYPE = "account_type";
public static final String KEY_EMAIL = "email";
public static final String KEY_PROFILE = "profile";
public static final String KEY_IDP_SERVER_URI = "idpServerURI";
public static final String KEY_TOKEN_SERVER_URI = "tokenServerURI";
public static final String KEY_PROFILE_SERVER_URI = "profileServerURI";
public static final String KEY_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP = "authoritiesToSyncAutomaticallyMap";
// Deprecated, but maintained for migration purposes.
public static final String KEY_IS_SYNCING_ENABLED = "isSyncingEnabled";
public static final String KEY_BUNDLE = "bundle";
/**
* Remove Firefox account persisted to disk.
* This operation is synchronized to avoid race condition while deleting the account.
*
* @param context Android context.
* @param filename name of persisted pickle file; must not contain path separators.
* @return <code>true</code> if given pickle existed and was successfully deleted.
*/
public synchronized static boolean deletePickle(final Context context, final String filename) {
return context.deleteFile(filename);
}
public static ExtendedJSONObject toJSON(final AndroidFxAccount account, final long now) {
final ExtendedJSONObject o = new ExtendedJSONObject();
o.put(KEY_PICKLE_VERSION, PICKLE_VERSION);
o.put(KEY_PICKLE_TIMESTAMP, now);
o.put(KEY_ACCOUNT_VERSION, AndroidFxAccount.CURRENT_ACCOUNT_VERSION);
o.put(KEY_ACCOUNT_TYPE, FxAccountConstants.ACCOUNT_TYPE);
o.put(KEY_EMAIL, account.getEmail());
o.put(KEY_PROFILE, account.getProfile());
o.put(KEY_IDP_SERVER_URI, account.getAccountServerURI());
o.put(KEY_TOKEN_SERVER_URI, account.getTokenServerURI());
o.put(KEY_PROFILE_SERVER_URI, account.getProfileServerURI());
final ExtendedJSONObject p = new ExtendedJSONObject();
for (Entry<String, Boolean> pair : account.getAuthoritiesToSyncAutomaticallyMap().entrySet()) {
p.put(pair.getKey(), pair.getValue());
}
o.put(KEY_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP, p);
// TODO: If prefs version changes under us, SyncPrefsPath will change, "clearing" prefs.
final ExtendedJSONObject bundle = account.unbundle();
if (bundle == null) {
Logger.warn(LOG_TAG, "Unable to obtain account bundle; aborting.");
return null;
}
o.put(KEY_BUNDLE, bundle);
return o;
}
/**
* Persist Firefox account to disk as a JSON object.
* This operation is synchronized to avoid race condition while deleting the account.
*
* @param account the AndroidFxAccount to persist to disk
* @param filename name of file to persist to; must not contain path separators.
*/
public synchronized static void pickle(final AndroidFxAccount account, final String filename) {
final ExtendedJSONObject o = toJSON(account, System.currentTimeMillis());
writeToDisk(account.context, filename, o);
}
private static void writeToDisk(final Context context, final String filename,
final ExtendedJSONObject pickle) {
try {
final FileOutputStream fos = context.openFileOutput(filename, Context.MODE_PRIVATE);
try {
final PrintStream ps = new PrintStream(fos);
try {
ps.print(pickle.toJSONString());
Logger.debug(LOG_TAG, "Persisted " + pickle.keySet().size() +
" account settings to " + filename + ".");
} finally {
ps.close();
}
} finally {
fos.close();
}
} catch (Exception e) {
Logger.warn(LOG_TAG, "Caught exception persisting account settings to " + filename +
"; ignoring.", e);
}
}
/**
* Create Android account from saved JSON object. Assumes that an account does not exist.
* This operation is synchronized to avoid race condition while deleting the account.
*
* @param context
* Android context.
* @param filename
* name of file to read from; must not contain path separators.
* @return created Android account, or null on error.
*/
public synchronized static AndroidFxAccount unpickle(final Context context, final String filename) {
final String jsonString = Utils.readFile(context, filename);
if (jsonString == null) {
Logger.info(LOG_TAG, "Pickle file '" + filename + "' not found; aborting.");
return null;
}
ExtendedJSONObject json = null;
try {
json = new ExtendedJSONObject(jsonString);
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception reading pickle file '" + filename + "'; aborting.", e);
return null;
}
final UnpickleParams params;
try {
params = UnpickleParams.fromJSON(json);
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception extracting unpickle json; aborting.", e);
return null;
}
final AndroidFxAccount account;
try {
account = AndroidFxAccount.addAndroidAccount(context, params.email, params.profile,
params.authServerURI, params.tokenServerURI, params.profileServerURI, params.state,
params.authoritiesToSyncAutomaticallyMap,
params.accountVersion,
true, params.bundle);
} catch (Exception e) {
Logger.warn(LOG_TAG, "Exception when adding Android Account; aborting.", e);
return null;
}
if (account == null) {
Logger.warn(LOG_TAG, "Failed to add Android Account; aborting.");
return null;
}
Long timestamp = json.getLong(KEY_PICKLE_TIMESTAMP);
if (timestamp == null) {
Logger.warn(LOG_TAG, "Did not find timestamp in pickle file; ignoring.");
timestamp = -1L;
}
Logger.info(LOG_TAG, "Un-pickled Android account named " + params.email + " (version " +
params.pickleVersion + ", pickled at " + timestamp + ").");
return account;
}
private static class UnpickleParams {
private Long pickleVersion;
private int accountVersion;
private String email;
private String profile;
private String authServerURI;
private String tokenServerURI;
private String profileServerURI;
private final Map<String, Boolean> authoritiesToSyncAutomaticallyMap = new HashMap<>();
private ExtendedJSONObject bundle;
private State state;
private UnpickleParams() {
}
private static UnpickleParams fromJSON(final ExtendedJSONObject json)
throws InvalidKeySpecException, NoSuchAlgorithmException, NonObjectJSONException {
final UnpickleParams params = new UnpickleParams();
params.pickleVersion = json.getLong(KEY_PICKLE_VERSION);
if (params.pickleVersion == null) {
throw new IllegalStateException("Pickle version not found.");
}
/*
* Version 1 and version 2 are identical, except version 2 throws if the
* internal Android Account type has changed. Version 1 used to throw in
* this case, but we intentionally used the pickle file to migrate across
* Account types, bumping the version simultaneously.
*
* Version 3 replaces "isSyncEnabled" with a map (String -> Boolean)
* associating Android authorities to whether or not they are configured
* to sync automatically.
*/
switch (params.pickleVersion.intValue()) {
case 3: {
// Sanity check.
final String accountType = json.getString(KEY_ACCOUNT_TYPE);
if (!FxAccountConstants.ACCOUNT_TYPE.equals(accountType)) {
throw new IllegalStateException("Account type has changed from " + accountType + " to " + FxAccountConstants.ACCOUNT_TYPE + ".");
}
params.unpickleV3(json);
}
break;
case 2: {
// Sanity check.
final String accountType = json.getString(KEY_ACCOUNT_TYPE);
if (!FxAccountConstants.ACCOUNT_TYPE.equals(accountType)) {
throw new IllegalStateException("Account type has changed from " + accountType + " to " + FxAccountConstants.ACCOUNT_TYPE + ".");
}
params.unpickleV1(json);
}
break;
case 1: {
// Warn about account type changing, but don't throw over it.
final String accountType = json.getString(KEY_ACCOUNT_TYPE);
if (!FxAccountConstants.ACCOUNT_TYPE.equals(accountType)) {
Logger.warn(LOG_TAG, "Account type has changed from " + accountType + " to " + FxAccountConstants.ACCOUNT_TYPE + "; ignoring.");
}
params.unpickleV1(json);
}
break;
default:
throw new IllegalStateException("Unknown pickle version, " + params.pickleVersion + ".");
}
return params;
}
private void unpickleV1(final ExtendedJSONObject json)
throws NonObjectJSONException, NoSuchAlgorithmException, InvalidKeySpecException {
this.accountVersion = json.getIntegerSafely(KEY_ACCOUNT_VERSION);
this.email = json.getString(KEY_EMAIL);
this.profile = json.getString(KEY_PROFILE);
this.authServerURI = json.getString(KEY_IDP_SERVER_URI);
this.tokenServerURI = json.getString(KEY_TOKEN_SERVER_URI);
this.profileServerURI = json.getString(KEY_PROFILE_SERVER_URI);
// Fallback to default value when profile server URI was not pickled.
if (this.profileServerURI == null) {
this.profileServerURI = FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT.equals(this.authServerURI)
? FxAccountConstants.DEFAULT_PROFILE_SERVER_ENDPOINT
: FxAccountConstants.STAGE_PROFILE_SERVER_ENDPOINT;
}
// We get the default value for everything except syncing browser data.
this.authoritiesToSyncAutomaticallyMap.put(BrowserContract.AUTHORITY, json.getBoolean(KEY_IS_SYNCING_ENABLED));
this.bundle = json.getObject(KEY_BUNDLE);
if (bundle == null) {
throw new IllegalStateException("Pickle bundle is null.");
}
this.state = getState(bundle);
}
private void unpickleV3(final ExtendedJSONObject json)
throws NonObjectJSONException, NoSuchAlgorithmException, InvalidKeySpecException {
// We'll overwrite the extracted sync automatically map.
unpickleV1(json);
// Extract the map of authorities to sync automatically.
authoritiesToSyncAutomaticallyMap.clear();
final ExtendedJSONObject o = json.getObject(KEY_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP);
if (o == null) {
return;
}
for (String key : o.keySet()) {
final Boolean enabled = o.getBoolean(key);
if (enabled != null) {
authoritiesToSyncAutomaticallyMap.put(key, enabled);
}
}
}
private State getState(final ExtendedJSONObject bundle) throws InvalidKeySpecException,
NonObjectJSONException, NoSuchAlgorithmException {
// TODO: Should copy-pasta BUNDLE_KEY_STATE & LABEL to this file to ensure we maintain
// old versions?
final StateLabel stateLabelString = StateLabel.valueOf(
bundle.getString(AndroidFxAccount.BUNDLE_KEY_STATE_LABEL));
final String stateString = bundle.getString(AndroidFxAccount.BUNDLE_KEY_STATE);
if (stateLabelString == null || stateString == null) {
throw new IllegalStateException("stateLabel and stateString must not be null, but: " +
"(stateLabel == null) = " + (stateLabelString == null) +
" and (stateString == null) = " + (stateString == null));
}
try {
return StateFactory.fromJSONObject(stateLabelString, new ExtendedJSONObject(stateString));
} catch (Exception e) {
throw new IllegalStateException("could not get state", e);
}
}
}
}

View file

@ -1,929 +0,0 @@
/* 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/. */
package org.mozilla.gecko.fxa.authenticator;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Handler;
import android.os.ResultReceiver;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;
import android.util.Log;
import org.mozilla.gecko.background.common.GlobalConstants;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.fxa.login.State.StateLabel;
import org.mozilla.gecko.fxa.login.StateFactory;
import org.mozilla.gecko.fxa.login.TokensAndKeysState;
import org.mozilla.gecko.fxa.sync.FxAccountProfileService;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.setup.Constants;
import org.mozilla.gecko.util.ThreadUtils;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Semaphore;
/**
* A Firefox Account that stores its details and state as user data attached to
* an Android Account instance.
* <p>
* Account user data is accessible only to the Android App(s) that own the
* Account type. Account user data is not removed when the App's private data is
* cleared.
*/
public class AndroidFxAccount {
protected static final String LOG_TAG = AndroidFxAccount.class.getSimpleName();
public static final int CURRENT_SYNC_PREFS_VERSION = 1;
public static final int CURRENT_RL_PREFS_VERSION = 1;
// When updating the account, do not forget to update AccountPickler.
public static final int CURRENT_ACCOUNT_VERSION = 3;
public static final String ACCOUNT_KEY_ACCOUNT_VERSION = "version";
public static final String ACCOUNT_KEY_PROFILE = "profile";
public static final String ACCOUNT_KEY_IDP_SERVER = "idpServerURI";
private static final String ACCOUNT_KEY_PROFILE_SERVER = "profileServerURI";
public static final String ACCOUNT_KEY_TOKEN_SERVER = "tokenServerURI"; // Sync-specific.
public static final String ACCOUNT_KEY_DESCRIPTOR = "descriptor";
public static final int CURRENT_BUNDLE_VERSION = 2;
public static final String BUNDLE_KEY_BUNDLE_VERSION = "version";
public static final String BUNDLE_KEY_STATE_LABEL = "stateLabel";
public static final String BUNDLE_KEY_STATE = "state";
public static final String BUNDLE_KEY_PROFILE_JSON = "profile";
public static final String ACCOUNT_KEY_DEVICE_ID = "deviceId";
public static final String ACCOUNT_KEY_DEVICE_REGISTRATION_VERSION = "deviceRegistrationVersion";
// Account authentication token type for fetching account profile.
public static final String PROFILE_OAUTH_TOKEN_TYPE = "oauth::profile";
// Services may request OAuth tokens from the Firefox Account dynamically.
// Each such token is prefixed with "oauth::" and a service-dependent scope.
// Such tokens should be destroyed when the account is removed from the device.
// This list collects all the known "oauth::" token types in order to delete them when necessary.
private static final List<String> KNOWN_OAUTH_TOKEN_TYPES;
static {
final List<String> list = new ArrayList<>();
list.add(PROFILE_OAUTH_TOKEN_TYPE);
KNOWN_OAUTH_TOKEN_TYPES = Collections.unmodifiableList(list);
}
public static final Map<String, Boolean> DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP;
static {
final HashMap<String, Boolean> m = new HashMap<String, Boolean>();
// By default, Firefox Sync is enabled.
m.put(BrowserContract.AUTHORITY, true);
DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP = Collections.unmodifiableMap(m);
}
private static final String PREF_KEY_LAST_SYNCED_TIMESTAMP = "lastSyncedTimestamp";
protected final Context context;
protected final AccountManager accountManager;
protected final Account account;
/**
* A cache associating Account name (email address) to a representation of the
* account's internal bundle.
* <p>
* The cache is invalidated entirely when <it>any</it> new Account is added,
* because there is no reliable way to know that an Account has been removed
* and then re-added.
*/
protected static final ConcurrentHashMap<String, ExtendedJSONObject> perAccountBundleCache =
new ConcurrentHashMap<>();
public static void invalidateCaches() {
perAccountBundleCache.clear();
}
/**
* Create an Android Firefox Account instance backed by an Android Account
* instance.
* <p>
* We expect a long-lived application context to avoid life-cycle issues that
* might arise if the internally cached AccountManager instance surfaces UI.
* <p>
* We take care to not install any listeners or observers that might outlive
* the AccountManager; and Android ensures the AccountManager doesn't outlive
* the associated context.
*
* @param applicationContext
* to use as long-lived ambient Android context.
* @param account
* Android account to use for storage.
*/
public AndroidFxAccount(Context applicationContext, Account account) {
this.context = applicationContext;
this.account = account;
this.accountManager = AccountManager.get(this.context);
}
public static AndroidFxAccount fromContext(Context context) {
context = context.getApplicationContext();
Account account = FirefoxAccounts.getFirefoxAccount(context);
if (account == null) {
return null;
}
return new AndroidFxAccount(context, account);
}
/**
* Persist the Firefox account to disk as a JSON object. Note that this is a wrapper around
* {@link AccountPickler#pickle}, and is identical to calling it directly.
* <p>
* Note that pickling is different from bundling, which involves operations on a
* {@link android.os.Bundle Bundle} object of miscellaneous data associated with the account.
* See {@link #persistBundle} and {@link #unbundle} for more.
*/
public void pickle(final String filename) {
AccountPickler.pickle(this, filename);
}
public Account getAndroidAccount() {
return this.account;
}
protected int getAccountVersion() {
String v = accountManager.getUserData(account, ACCOUNT_KEY_ACCOUNT_VERSION);
if (v == null) {
return 0; // Implicit.
}
try {
return Integer.parseInt(v, 10);
} catch (NumberFormatException ex) {
return 0;
}
}
/**
* Saves the given data as the internal bundle associated with this account.
* @param bundle to write to account.
*/
protected synchronized void persistBundle(ExtendedJSONObject bundle) {
perAccountBundleCache.put(account.name, bundle);
accountManager.setUserData(account, ACCOUNT_KEY_DESCRIPTOR, bundle.toJSONString());
}
protected ExtendedJSONObject unbundle() {
return unbundle(true);
}
/**
* Retrieve the internal bundle associated with this account.
* @return bundle associated with account.
*/
protected synchronized ExtendedJSONObject unbundle(boolean allowCachedBundle) {
if (allowCachedBundle) {
final ExtendedJSONObject cachedBundle = perAccountBundleCache.get(account.name);
if (cachedBundle != null) {
Logger.debug(LOG_TAG, "Returning cached account bundle.");
return cachedBundle;
}
}
final int version = getAccountVersion();
if (version < CURRENT_ACCOUNT_VERSION) {
// Needs upgrade. For now, do nothing. We'd like to just put your account
// into the Separated state here and have you update your credentials.
return null;
}
if (version > CURRENT_ACCOUNT_VERSION) {
// Oh dear.
return null;
}
String bundleString = accountManager.getUserData(account, ACCOUNT_KEY_DESCRIPTOR);
if (bundleString == null) {
return null;
}
final ExtendedJSONObject bundle = unbundleAccountV2(bundleString);
perAccountBundleCache.put(account.name, bundle);
Logger.info(LOG_TAG, "Account bundle persisted to cache.");
return bundle;
}
protected String getBundleData(String key) {
ExtendedJSONObject o = unbundle();
if (o == null) {
return null;
}
return o.getString(key);
}
protected boolean getBundleDataBoolean(String key, boolean def) {
ExtendedJSONObject o = unbundle();
if (o == null) {
return def;
}
Boolean b = o.getBoolean(key);
if (b == null) {
return def;
}
return b;
}
protected byte[] getBundleDataBytes(String key) {
ExtendedJSONObject o = unbundle();
if (o == null) {
return null;
}
return o.getByteArrayHex(key);
}
protected void updateBundleValues(String key, String value, String... more) {
if (more.length % 2 != 0) {
throw new IllegalArgumentException("more must be a list of key, value pairs");
}
ExtendedJSONObject descriptor = unbundle();
if (descriptor == null) {
return;
}
descriptor.put(key, value);
for (int i = 0; i + 1 < more.length; i += 2) {
descriptor.put(more[i], more[i+1]);
}
persistBundle(descriptor);
}
private ExtendedJSONObject unbundleAccountV1(String bundle) {
ExtendedJSONObject o;
try {
o = new ExtendedJSONObject(bundle);
} catch (Exception e) {
return null;
}
if (CURRENT_BUNDLE_VERSION == o.getIntegerSafely(BUNDLE_KEY_BUNDLE_VERSION)) {
return o;
}
return null;
}
private ExtendedJSONObject unbundleAccountV2(String bundle) {
return unbundleAccountV1(bundle);
}
/**
* Note that if the user clears data, an account will be left pointing to a
* deleted profile. Such is life.
*/
public String getProfile() {
return accountManager.getUserData(account, ACCOUNT_KEY_PROFILE);
}
public String getAccountServerURI() {
return accountManager.getUserData(account, ACCOUNT_KEY_IDP_SERVER);
}
public String getTokenServerURI() {
return accountManager.getUserData(account, ACCOUNT_KEY_TOKEN_SERVER);
}
public String getProfileServerURI() {
String profileURI = accountManager.getUserData(account, ACCOUNT_KEY_PROFILE_SERVER);
if (profileURI == null) {
if (isStaging()) {
return FxAccountConstants.STAGE_PROFILE_SERVER_ENDPOINT;
}
return FxAccountConstants.DEFAULT_PROFILE_SERVER_ENDPOINT;
}
return profileURI;
}
public String getOAuthServerURI() {
// Allow testing against stage.
if (isStaging()) {
return FxAccountConstants.STAGE_OAUTH_SERVER_ENDPOINT;
} else {
return FxAccountConstants.DEFAULT_OAUTH_SERVER_ENDPOINT;
}
}
private boolean isStaging() {
return FxAccountConstants.STAGE_AUTH_SERVER_ENDPOINT.equals(getAccountServerURI());
}
private String constructPrefsPath(String product, long version, String extra) throws GeneralSecurityException, UnsupportedEncodingException {
String profile = getProfile();
String username = account.name;
if (profile == null) {
throw new IllegalStateException("Missing profile. Cannot fetch prefs.");
}
if (username == null) {
throw new IllegalStateException("Missing username. Cannot fetch prefs.");
}
final String fxaServerURI = getAccountServerURI();
if (fxaServerURI == null) {
throw new IllegalStateException("No account server URI. Cannot fetch prefs.");
}
// This is unique for each syncing 'view' of the account.
final String serverURLThing = fxaServerURI + "!" + extra;
return Utils.getPrefsPath(product, username, serverURLThing, profile, version);
}
/**
* This needs to return a string because of the tortured prefs access in GlobalSession.
*/
public String getSyncPrefsPath() throws GeneralSecurityException, UnsupportedEncodingException {
final String tokenServerURI = getTokenServerURI();
if (tokenServerURI == null) {
throw new IllegalStateException("No token server URI. Cannot fetch prefs.");
}
final String product = GlobalConstants.BROWSER_INTENT_PACKAGE + ".fxa";
final long version = CURRENT_SYNC_PREFS_VERSION;
return constructPrefsPath(product, version, tokenServerURI);
}
public String getReadingListPrefsPath() throws GeneralSecurityException, UnsupportedEncodingException {
final String product = GlobalConstants.BROWSER_INTENT_PACKAGE + ".reading";
final long version = CURRENT_RL_PREFS_VERSION;
return constructPrefsPath(product, version, "");
}
public SharedPreferences getSyncPrefs() throws UnsupportedEncodingException, GeneralSecurityException {
return context.getSharedPreferences(getSyncPrefsPath(), Utils.SHARED_PREFERENCES_MODE);
}
public SharedPreferences getReadingListPrefs() throws UnsupportedEncodingException, GeneralSecurityException {
return context.getSharedPreferences(getReadingListPrefsPath(), Utils.SHARED_PREFERENCES_MODE);
}
/**
* Extract a JSON dictionary of the string values associated to this account.
* <p>
* <b>For debugging use only!</b> The contents of this JSON object completely
* determine the user's Firefox Account status and yield access to whatever
* user data the device has access to.
*
* @return JSON-object of Strings.
*/
public ExtendedJSONObject toJSONObject() {
ExtendedJSONObject o = unbundle();
o.put("email", account.name);
try {
o.put("emailUTF8", Utils.byte2Hex(account.name.getBytes("UTF-8")));
} catch (UnsupportedEncodingException e) {
// Ignore.
}
o.put("fxaDeviceId", getDeviceId());
o.put("fxaDeviceRegistrationVersion", getDeviceRegistrationVersion());
return o;
}
public static AndroidFxAccount addAndroidAccount(
Context context,
String email,
String profile,
String idpServerURI,
String tokenServerURI,
String profileServerURI,
State state,
final Map<String, Boolean> authoritiesToSyncAutomaticallyMap)
throws UnsupportedEncodingException, GeneralSecurityException, URISyntaxException {
return addAndroidAccount(context, email, profile, idpServerURI, tokenServerURI, profileServerURI, state,
authoritiesToSyncAutomaticallyMap,
CURRENT_ACCOUNT_VERSION, false, null);
}
public static AndroidFxAccount addAndroidAccount(
Context context,
String email,
String profile,
String idpServerURI,
String tokenServerURI,
String profileServerURI,
State state,
final Map<String, Boolean> authoritiesToSyncAutomaticallyMap,
final int accountVersion,
final boolean fromPickle,
ExtendedJSONObject bundle)
throws UnsupportedEncodingException, GeneralSecurityException, URISyntaxException {
if (email == null) {
throw new IllegalArgumentException("email must not be null");
}
if (profile == null) {
throw new IllegalArgumentException("profile must not be null");
}
if (idpServerURI == null) {
throw new IllegalArgumentException("idpServerURI must not be null");
}
if (tokenServerURI == null) {
throw new IllegalArgumentException("tokenServerURI must not be null");
}
if (profileServerURI == null) {
throw new IllegalArgumentException("profileServerURI must not be null");
}
if (state == null) {
throw new IllegalArgumentException("state must not be null");
}
// TODO: Add migration code.
if (accountVersion != CURRENT_ACCOUNT_VERSION) {
throw new IllegalStateException("Could not create account of version " + accountVersion +
". Current version is " + CURRENT_ACCOUNT_VERSION + ".");
}
// Android has internal restrictions that require all values in this
// bundle to be strings. *sigh*
Bundle userdata = new Bundle();
userdata.putString(ACCOUNT_KEY_ACCOUNT_VERSION, "" + CURRENT_ACCOUNT_VERSION);
userdata.putString(ACCOUNT_KEY_IDP_SERVER, idpServerURI);
userdata.putString(ACCOUNT_KEY_TOKEN_SERVER, tokenServerURI);
userdata.putString(ACCOUNT_KEY_PROFILE_SERVER, profileServerURI);
userdata.putString(ACCOUNT_KEY_PROFILE, profile);
if (bundle == null) {
bundle = new ExtendedJSONObject();
// TODO: How to upgrade?
bundle.put(BUNDLE_KEY_BUNDLE_VERSION, CURRENT_BUNDLE_VERSION);
}
bundle.put(BUNDLE_KEY_STATE_LABEL, state.getStateLabel().name());
bundle.put(BUNDLE_KEY_STATE, state.toJSONObject().toJSONString());
userdata.putString(ACCOUNT_KEY_DESCRIPTOR, bundle.toJSONString());
Account account = new Account(email, FxAccountConstants.ACCOUNT_TYPE);
AccountManager accountManager = AccountManager.get(context);
// We don't set an Android password, because we don't want to persist the
// password (or anything else as powerful as the password). Instead, we
// internally manage a sessionToken with a remotely owned lifecycle.
boolean added = accountManager.addAccountExplicitly(account, null, userdata);
if (!added) {
return null;
}
// Try to work around an intermittent issue described at
// http://stackoverflow.com/a/11698139. What happens is that tests that
// delete and re-create the same account frequently will find the account
// missing all or some of the userdata bundle, possibly due to an Android
// AccountManager caching bug.
for (String key : userdata.keySet()) {
accountManager.setUserData(account, key, userdata.getString(key));
}
AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
if (!fromPickle) {
fxAccount.clearSyncPrefs();
}
fxAccount.setAuthoritiesToSyncAutomaticallyMap(authoritiesToSyncAutomaticallyMap);
return fxAccount;
}
public void clearSyncPrefs() throws UnsupportedEncodingException, GeneralSecurityException {
getSyncPrefs().edit().clear().commit();
}
public void setAuthoritiesToSyncAutomaticallyMap(Map<String, Boolean> authoritiesToSyncAutomaticallyMap) {
if (authoritiesToSyncAutomaticallyMap == null) {
throw new IllegalArgumentException("authoritiesToSyncAutomaticallyMap must not be null");
}
for (String authority : DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP.keySet()) {
boolean authorityEnabled = DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP.get(authority);
final Boolean enabled = authoritiesToSyncAutomaticallyMap.get(authority);
if (enabled != null) {
authorityEnabled = enabled.booleanValue();
}
// Accounts are always capable of being synced ...
ContentResolver.setIsSyncable(account, authority, 1);
// ... but not always automatically synced.
ContentResolver.setSyncAutomatically(account, authority, authorityEnabled);
}
}
public Map<String, Boolean> getAuthoritiesToSyncAutomaticallyMap() {
final Map<String, Boolean> authoritiesToSync = new HashMap<>();
for (String authority : DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP.keySet()) {
final boolean enabled = ContentResolver.getSyncAutomatically(account, authority);
authoritiesToSync.put(authority, enabled);
}
return authoritiesToSync;
}
/**
* Is a sync currently in progress?
*
* @return true if Android is currently syncing the underlying Android Account.
*/
public boolean isCurrentlySyncing() {
boolean active = false;
for (String authority : AndroidFxAccount.DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP.keySet()) {
active |= ContentResolver.isSyncActive(account, authority);
}
return active;
}
/**
* Request an immediate sync. Use this to sync as soon as possible in response to user action.
*
* @param stagesToSync stage names to sync; can be null to sync <b>all</b> known stages.
* @param stagesToSkip stage names to skip; can be null to skip <b>no</b> known stages.
*/
public void requestImmediateSync(String[] stagesToSync, String[] stagesToSkip) {
FirefoxAccounts.requestImmediateSync(getAndroidAccount(), stagesToSync, stagesToSkip);
}
/**
* Request an eventual sync. Use this to request the system queue a sync for some time in the
* future.
*
* @param stagesToSync stage names to sync; can be null to sync <b>all</b> known stages.
* @param stagesToSkip stage names to skip; can be null to skip <b>no</b> known stages.
*/
public void requestEventualSync(String[] stagesToSync, String[] stagesToSkip) {
FirefoxAccounts.requestEventualSync(getAndroidAccount(), stagesToSync, stagesToSkip);
}
public synchronized void setState(State state) {
if (state == null) {
throw new IllegalArgumentException("state must not be null");
}
Logger.info(LOG_TAG, "Moving account named like " + getObfuscatedEmail() +
" to state " + state.getStateLabel().toString());
updateBundleValues(
BUNDLE_KEY_STATE_LABEL, state.getStateLabel().name(),
BUNDLE_KEY_STATE, state.toJSONObject().toJSONString());
broadcastAccountStateChangedIntent();
}
protected void broadcastAccountStateChangedIntent() {
final Intent intent = new Intent(FxAccountConstants.ACCOUNT_STATE_CHANGED_ACTION);
intent.putExtra(Constants.JSON_KEY_ACCOUNT, account.name);
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
}
public synchronized State getState() {
String stateLabelString = getBundleData(BUNDLE_KEY_STATE_LABEL);
String stateString = getBundleData(BUNDLE_KEY_STATE);
if (stateLabelString == null || stateString == null) {
throw new IllegalStateException("stateLabelString and stateString must not be null, but: " +
"(stateLabelString == null) = " + (stateLabelString == null) +
" and (stateString == null) = " + (stateString == null));
}
try {
StateLabel stateLabel = StateLabel.valueOf(stateLabelString);
Logger.debug(LOG_TAG, "Account is in state " + stateLabel);
return StateFactory.fromJSONObject(stateLabel, new ExtendedJSONObject(stateString));
} catch (Exception e) {
throw new IllegalStateException("could not get state", e);
}
}
public byte[] getSessionToken() throws InvalidFxAState {
State state = getState();
StateLabel stateLabel = state.getStateLabel();
if (stateLabel == StateLabel.Cohabiting || stateLabel == StateLabel.Married) {
TokensAndKeysState tokensAndKeysState = (TokensAndKeysState) state;
return tokensAndKeysState.getSessionToken();
}
throw new InvalidFxAState("Cannot get sessionToken: not in a TokensAndKeysState state");
}
public static class InvalidFxAState extends Exception {
private static final long serialVersionUID = -8537626959811195978L;
public InvalidFxAState(String message) {
super(message);
}
}
/**
* <b>For debugging only!</b>
*/
public void dump() {
if (!FxAccountUtils.LOG_PERSONAL_INFORMATION) {
return;
}
ExtendedJSONObject o = toJSONObject();
ArrayList<String> list = new ArrayList<String>(o.keySet());
Collections.sort(list);
for (String key : list) {
FxAccountUtils.pii(LOG_TAG, key + ": " + o.get(key));
}
}
/**
* Return the Firefox Account's local email address.
* <p>
* It is important to note that this is the local email address, and not
* necessarily the normalized remote email address that the server expects.
*
* @return local email address.
*/
public String getEmail() {
return account.name;
}
/**
* Return the Firefox Account's local email address, obfuscated.
* <p>
* Use this when logging.
*
* @return local email address, obfuscated.
*/
public String getObfuscatedEmail() {
return Utils.obfuscateEmail(account.name);
}
/**
* Populate an intent used for starting FxAccountDeletedService service.
*
* @param intent Intent to populate with necessary extras
* @return <code>Intent</code> with a deleted action and account/OAuth information extras
*/
public Intent populateDeletedAccountIntent(final Intent intent) {
final List<String> tokens = new ArrayList<>();
intent.putExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_VERSION_KEY,
Long.valueOf(FxAccountConstants.ACCOUNT_DELETED_INTENT_VERSION));
intent.putExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_KEY, account.name);
intent.putExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_PROFILE, getProfile());
// Get the tokens from AccountManager. Note: currently, only reading list service supports OAuth. The following logic will
// be extended in future to support OAuth for other services.
for (String tokenKey : KNOWN_OAUTH_TOKEN_TYPES) {
final String authToken = accountManager.peekAuthToken(account, tokenKey);
if (authToken != null) {
tokens.add(authToken);
}
}
// Update intent with tokens and service URI.
intent.putExtra(FxAccountConstants.ACCOUNT_OAUTH_SERVICE_ENDPOINT_KEY, getOAuthServerURI());
// Deleted broadcasts are package-private, so there's no security risk include the tokens in the extras
intent.putExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_AUTH_TOKENS, tokens.toArray(new String[tokens.size()]));
return intent;
}
/**
* Create an intent announcing that the profile JSON attached to this Firefox Account has been updated.
* <p>
* It is not guaranteed that the profile JSON has changed.
*
* @return <code>Intent</code> to broadcast.
*/
private Intent makeProfileJSONUpdatedIntent() {
final Intent intent = new Intent();
intent.setAction(FxAccountConstants.ACCOUNT_PROFILE_JSON_UPDATED_ACTION);
return intent;
}
public void setLastSyncedTimestamp(long now) {
try {
getSyncPrefs().edit().putLong(PREF_KEY_LAST_SYNCED_TIMESTAMP, now).commit();
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception setting last synced time; ignoring.", e);
}
}
public long getLastSyncedTimestamp() {
final long neverSynced = -1L;
try {
return getSyncPrefs().getLong(PREF_KEY_LAST_SYNCED_TIMESTAMP, neverSynced);
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception getting last synced time; ignoring.", e);
return neverSynced;
}
}
// Debug only! This is dangerous!
public void unsafeTransitionToDefaultEndpoints() {
unsafeTransitionToStageEndpoints(
FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT,
FxAccountConstants.DEFAULT_TOKEN_SERVER_ENDPOINT,
FxAccountConstants.DEFAULT_PROFILE_SERVER_ENDPOINT);
}
// Debug only! This is dangerous!
public void unsafeTransitionToStageEndpoints() {
unsafeTransitionToStageEndpoints(
FxAccountConstants.STAGE_AUTH_SERVER_ENDPOINT,
FxAccountConstants.STAGE_TOKEN_SERVER_ENDPOINT,
FxAccountConstants.STAGE_PROFILE_SERVER_ENDPOINT);
}
protected void unsafeTransitionToStageEndpoints(String authServerEndpoint, String tokenServerEndpoint, String profileServerEndpoint) {
try {
getReadingListPrefs().edit().clear().commit();
} catch (UnsupportedEncodingException | GeneralSecurityException e) {
// Ignore.
}
try {
getSyncPrefs().edit().clear().commit();
} catch (UnsupportedEncodingException | GeneralSecurityException e) {
// Ignore.
}
State state = getState();
setState(state.makeSeparatedState());
accountManager.setUserData(account, ACCOUNT_KEY_IDP_SERVER, authServerEndpoint);
accountManager.setUserData(account, ACCOUNT_KEY_TOKEN_SERVER, tokenServerEndpoint);
accountManager.setUserData(account, ACCOUNT_KEY_PROFILE_SERVER, profileServerEndpoint);
ContentResolver.setIsSyncable(account, BrowserContract.READING_LIST_AUTHORITY, 1);
}
/**
* Returns the current profile JSON if available, or null.
*
* @return profile JSON object.
*/
public ExtendedJSONObject getProfileJSON() {
final String profileString = getBundleData(BUNDLE_KEY_PROFILE_JSON);
if (profileString == null) {
return null;
}
try {
return new ExtendedJSONObject(profileString);
} catch (Exception e) {
Logger.error(LOG_TAG, "Failed to parse profile JSON; ignoring and returning null.", e);
}
return null;
}
/**
* Fetch the profile JSON associated to the underlying Firefox Account from the server and update the local store.
* <p>
* The LocalBroadcastManager is used to notify the receivers asynchronously after a successful fetch.
*/
public void fetchProfileJSON() {
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
// Fetch profile information from server.
String authToken;
try {
authToken = accountManager.blockingGetAuthToken(account, AndroidFxAccount.PROFILE_OAUTH_TOKEN_TYPE, true);
if (authToken == null) {
throw new RuntimeException("Couldn't get oauth token! Aborting profile fetch.");
}
} catch (Exception e) {
Logger.error(LOG_TAG, "Error fetching profile information; ignoring.", e);
return;
}
Logger.info(LOG_TAG, "Intent service launched to fetch profile.");
final Intent intent = new Intent(context, FxAccountProfileService.class);
intent.putExtra(FxAccountProfileService.KEY_AUTH_TOKEN, authToken);
intent.putExtra(FxAccountProfileService.KEY_PROFILE_SERVER_URI, getProfileServerURI());
intent.putExtra(FxAccountProfileService.KEY_RESULT_RECEIVER, new ProfileResultReceiver(new Handler()));
context.startService(intent);
}
});
}
@Nullable
public synchronized String getDeviceId() {
return accountManager.getUserData(account, ACCOUNT_KEY_DEVICE_ID);
}
@NonNull
public synchronized int getDeviceRegistrationVersion() {
String versionStr = accountManager.getUserData(account, ACCOUNT_KEY_DEVICE_REGISTRATION_VERSION);
if (TextUtils.isEmpty(versionStr)) {
return 0;
} else {
try {
return Integer.parseInt(versionStr);
} catch (NumberFormatException ex) {
return 0;
}
}
}
public synchronized void setDeviceId(String id) {
accountManager.setUserData(account, ACCOUNT_KEY_DEVICE_ID, id);
}
public synchronized void setDeviceRegistrationVersion(int deviceRegistrationVersion) {
accountManager.setUserData(account, ACCOUNT_KEY_DEVICE_REGISTRATION_VERSION,
Integer.toString(deviceRegistrationVersion));
}
public synchronized void resetDeviceRegistrationVersion() {
setDeviceRegistrationVersion(0);
}
public synchronized void setFxAUserData(String id, int deviceRegistrationVersion) {
accountManager.setUserData(account, ACCOUNT_KEY_DEVICE_ID, id);
accountManager.setUserData(account, ACCOUNT_KEY_DEVICE_REGISTRATION_VERSION,
Integer.toString(deviceRegistrationVersion));
}
@SuppressLint("ParcelCreator") // The CREATOR field is defined in the super class.
private class ProfileResultReceiver extends ResultReceiver {
public ProfileResultReceiver(Handler handler) {
super(handler);
}
@Override
protected void onReceiveResult(int resultCode, Bundle bundle) {
super.onReceiveResult(resultCode, bundle);
switch (resultCode) {
case Activity.RESULT_OK:
final String resultData = bundle.getString(FxAccountProfileService.KEY_RESULT_STRING);
updateBundleValues(BUNDLE_KEY_PROFILE_JSON, resultData);
Logger.info(LOG_TAG, "Profile JSON fetch succeeeded!");
FxAccountUtils.pii(LOG_TAG, "Profile JSON fetch returned: " + resultData);
LocalBroadcastManager.getInstance(context).sendBroadcast(makeProfileJSONUpdatedIntent());
break;
case Activity.RESULT_CANCELED:
Logger.warn(LOG_TAG, "Failed to fetch profile JSON; ignoring.");
break;
default:
Logger.warn(LOG_TAG, "Invalid result code received; ignoring.");
break;
}
}
}
/**
* Take the lock to own updating any Firefox Account's internal state.
*
* We use a <code>Semaphore</code> rather than a <code>ReentrantLock</code>
* because the callback that needs to release the lock may not be invoked on
* the thread that initially acquired the lock. Be aware!
*/
protected static final Semaphore sLock = new Semaphore(1, true /* fair */);
// Which consumer took the lock?
// Synchronized by this.
protected String lockTag = null;
// Are we locked? (It's not easy to determine who took the lock dynamically,
// so we maintain this flag internally.)
// Synchronized by this.
protected boolean locked = false;
// Block until we can take the shared state lock.
public synchronized void acquireSharedAccountStateLock(final String tag) throws InterruptedException {
final long id = Thread.currentThread().getId();
this.lockTag = tag;
Log.d(Logger.DEFAULT_LOG_TAG, "Thread with tag and thread id acquiring lock: " + lockTag + ", " + id + " ...");
sLock.acquire();
locked = true;
Log.d(Logger.DEFAULT_LOG_TAG, "Thread with tag and thread id acquiring lock: " + lockTag + ", " + id + " ... ACQUIRED");
}
// If we hold the shared state lock, release it. Otherwise, ignore the request.
public synchronized void releaseSharedAccountStateLock() {
final long id = Thread.currentThread().getId();
Log.d(Logger.DEFAULT_LOG_TAG, "Thread with tag and thread id releasing lock: " + lockTag + ", " + id + " ...");
if (locked) {
sLock.release();
locked = false;
Log.d(Logger.DEFAULT_LOG_TAG, "Thread with tag and thread id releasing lock: " + lockTag + ", " + id + " ... RELEASED");
} else {
Log.d(Logger.DEFAULT_LOG_TAG, "Thread with tag and thread id releasing lock: " + lockTag + ", " + id + " ... NOT LOCKED");
}
}
@Override
protected synchronized void finalize() {
if (locked) {
// Should never happen, but...
sLock.release();
locked = false;
final long id = Thread.currentThread().getId();
Log.e(Logger.DEFAULT_LOG_TAG, "Thread with tag and thread id releasing lock: " + lockTag + ", " + id + " ... RELEASED DURING FINALIZE");
}
}
}

View file

@ -1,84 +0,0 @@
/* 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/. */
package org.mozilla.gecko.fxa.authenticator;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountClient;
import org.mozilla.gecko.background.fxa.FxAccountClient20;
import org.mozilla.gecko.browserid.BrowserIDKeyPair;
import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.LoginStateMachineDelegate;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition;
import org.mozilla.gecko.fxa.login.Married;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.fxa.login.State.StateLabel;
import org.mozilla.gecko.fxa.login.StateFactory;
import org.mozilla.gecko.fxa.sync.FxAccountNotificationManager;
import org.mozilla.gecko.fxa.sync.FxAccountSyncAdapter;
import android.content.Context;
public abstract class FxADefaultLoginStateMachineDelegate implements LoginStateMachineDelegate {
protected final static String LOG_TAG = LoginStateMachineDelegate.class.getSimpleName();
protected final Context context;
protected final AndroidFxAccount fxAccount;
protected final Executor executor;
protected final FxAccountClient client;
public FxADefaultLoginStateMachineDelegate(Context context, AndroidFxAccount fxAccount) {
this.context = context;
this.fxAccount = fxAccount;
this.executor = Executors.newSingleThreadExecutor();
this.client = new FxAccountClient20(fxAccount.getAccountServerURI(), executor);
}
abstract public void handleNotMarried(State notMarried);
abstract public void handleMarried(Married married);
@Override
public FxAccountClient getClient() {
return client;
}
@Override
public long getCertificateDurationInMilliseconds() {
return 12 * 60 * 60 * 1000;
}
@Override
public long getAssertionDurationInMilliseconds() {
return 15 * 60 * 1000;
}
@Override
public BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException {
return StateFactory.generateKeyPair();
}
@Override
public void handleTransition(Transition transition, State state) {
Logger.info(LOG_TAG, "handleTransition: " + transition + " to " + state.getStateLabel());
}
@Override
public void handleFinal(State state) {
Logger.info(LOG_TAG, "handleFinal: in " + state.getStateLabel());
fxAccount.setState(state);
// Update any notifications displayed.
final FxAccountNotificationManager notificationManager = new FxAccountNotificationManager(FxAccountSyncAdapter.NOTIFICATION_ID);
notificationManager.update(context, fxAccount);
if (state.getStateLabel() != StateLabel.Married) {
handleNotMarried(state);
return;
} else {
handleMarried((Married) state);
}
}
}

View file

@ -1,385 +0,0 @@
/* 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/. */
package org.mozilla.gecko.fxa.authenticator;
import android.accounts.AbstractAccountAuthenticator;
import android.accounts.Account;
import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager;
import android.accounts.NetworkErrorException;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountClient;
import org.mozilla.gecko.background.fxa.FxAccountClient20;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClient.RequestDelegate;
import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClientException.FxAccountAbstractClientRemoteException;
import org.mozilla.gecko.background.fxa.oauth.FxAccountOAuthClient10;
import org.mozilla.gecko.background.fxa.oauth.FxAccountOAuthClient10.AuthorizationResponse;
import org.mozilla.gecko.browserid.BrowserIDKeyPair;
import org.mozilla.gecko.browserid.JSONWebTokenUtils;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine;
import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.LoginStateMachineDelegate;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition;
import org.mozilla.gecko.fxa.login.Married;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.fxa.login.State.StateLabel;
import org.mozilla.gecko.fxa.login.StateFactory;
import org.mozilla.gecko.fxa.receivers.FxAccountDeletedService;
import org.mozilla.gecko.fxa.sync.FxAccountNotificationManager;
import org.mozilla.gecko.fxa.sync.FxAccountSyncAdapter;
import org.mozilla.gecko.util.ThreadUtils;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class FxAccountAuthenticator extends AbstractAccountAuthenticator {
public static final String LOG_TAG = FxAccountAuthenticator.class.getSimpleName();
public static final int UNKNOWN_ERROR_CODE = 999;
protected final Context context;
protected final AccountManager accountManager;
public FxAccountAuthenticator(Context context) {
super(context);
this.context = context;
this.accountManager = AccountManager.get(context);
}
@Override
public Bundle addAccount(AccountAuthenticatorResponse response,
String accountType, String authTokenType, String[] requiredFeatures,
Bundle options)
throws NetworkErrorException {
Logger.debug(LOG_TAG, "addAccount");
// The data associated to each Account should be invalidated when we change
// the set of Firefox Accounts on the system.
AndroidFxAccount.invalidateCaches();
final Bundle res = new Bundle();
if (!FxAccountConstants.ACCOUNT_TYPE.equals(accountType)) {
res.putInt(AccountManager.KEY_ERROR_CODE, -1);
res.putString(AccountManager.KEY_ERROR_MESSAGE, "Not adding unknown account type.");
return res;
}
final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_GET_STARTED);
res.putParcelable(AccountManager.KEY_INTENT, intent);
return res;
}
@Override
public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options)
throws NetworkErrorException {
Logger.debug(LOG_TAG, "confirmCredentials");
return null;
}
@Override
public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
Logger.debug(LOG_TAG, "editProperties");
return null;
}
protected static class Responder {
final AccountAuthenticatorResponse response;
final AndroidFxAccount fxAccount;
public Responder(AccountAuthenticatorResponse response, AndroidFxAccount fxAccount) {
this.response = response;
this.fxAccount = fxAccount;
}
public void fail(Exception e) {
Logger.warn(LOG_TAG, "Responding with error!", e);
fxAccount.releaseSharedAccountStateLock();
final Bundle result = new Bundle();
result.putInt(AccountManager.KEY_ERROR_CODE, UNKNOWN_ERROR_CODE);
result.putString(AccountManager.KEY_ERROR_MESSAGE, e.toString());
response.onResult(result);
}
public void succeed(String authToken) {
Logger.info(LOG_TAG, "Responding with success!");
fxAccount.releaseSharedAccountStateLock();
final Bundle result = new Bundle();
result.putString(AccountManager.KEY_ACCOUNT_NAME, fxAccount.account.name);
result.putString(AccountManager.KEY_ACCOUNT_TYPE, fxAccount.account.type);
result.putString(AccountManager.KEY_AUTHTOKEN, authToken);
response.onResult(result);
}
}
public abstract static class FxADefaultLoginStateMachineDelegate implements LoginStateMachineDelegate {
protected final Context context;
protected final AndroidFxAccount fxAccount;
protected final Executor executor;
protected final FxAccountClient client;
public FxADefaultLoginStateMachineDelegate(Context context, AndroidFxAccount fxAccount) {
this.context = context;
this.fxAccount = fxAccount;
this.executor = Executors.newSingleThreadExecutor();
this.client = new FxAccountClient20(fxAccount.getAccountServerURI(), executor);
}
@Override
public FxAccountClient getClient() {
return client;
}
@Override
public long getCertificateDurationInMilliseconds() {
return 12 * 60 * 60 * 1000;
}
@Override
public long getAssertionDurationInMilliseconds() {
return 15 * 60 * 1000;
}
@Override
public BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException {
return StateFactory.generateKeyPair();
}
@Override
public void handleTransition(Transition transition, State state) {
Logger.info(LOG_TAG, "handleTransition: " + transition + " to " + state.getStateLabel());
}
abstract public void handleNotMarried(State notMarried);
abstract public void handleMarried(Married married);
@Override
public void handleFinal(State state) {
Logger.info(LOG_TAG, "handleFinal: in " + state.getStateLabel());
fxAccount.setState(state);
// Update any notifications displayed.
final FxAccountNotificationManager notificationManager = new FxAccountNotificationManager(FxAccountSyncAdapter.NOTIFICATION_ID);
notificationManager.update(context, fxAccount);
if (state.getStateLabel() != StateLabel.Married) {
handleNotMarried(state);
return;
} else {
handleMarried((Married) state);
}
}
}
protected void getOAuthToken(final AccountAuthenticatorResponse response, final AndroidFxAccount fxAccount, final String scope) throws NetworkErrorException {
Logger.info(LOG_TAG, "Fetching oauth token with scope: " + scope);
final Responder responder = new Responder(response, fxAccount);
final String oauthServerUri = fxAccount.getOAuthServerURI();
final String audience;
try {
audience = FxAccountUtils.getAudienceForURL(oauthServerUri); // The assertion gets traded in for an oauth bearer token.
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception fetching oauth token.", e);
responder.fail(e);
return;
}
final FxAccountLoginStateMachine stateMachine = new FxAccountLoginStateMachine();
stateMachine.advance(fxAccount.getState(), StateLabel.Married, new FxADefaultLoginStateMachineDelegate(context, fxAccount) {
@Override
public void handleNotMarried(State state) {
final String message = "Cannot fetch oauth token from state: " + state.getStateLabel();
Logger.warn(LOG_TAG, message);
responder.fail(new RuntimeException(message));
}
@Override
public void handleMarried(final Married married) {
final String assertion;
try {
assertion = married.generateAssertion(audience, JSONWebTokenUtils.DEFAULT_ASSERTION_ISSUER);
if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
JSONWebTokenUtils.dumpAssertion(assertion);
}
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception fetching oauth token.", e);
responder.fail(e);
return;
}
final FxAccountOAuthClient10 oauthClient = new FxAccountOAuthClient10(oauthServerUri, executor);
Logger.debug(LOG_TAG, "OAuth fetch for scope: " + scope);
oauthClient.authorization(FxAccountConstants.OAUTH_CLIENT_ID_FENNEC, assertion, null, scope, new RequestDelegate<FxAccountOAuthClient10.AuthorizationResponse>() {
@Override
public void handleSuccess(AuthorizationResponse result) {
Logger.debug(LOG_TAG, "OAuth success.");
FxAccountUtils.pii(LOG_TAG, "Fetched oauth token: " + result.access_token);
responder.succeed(result.access_token);
}
@Override
public void handleFailure(FxAccountAbstractClientRemoteException e) {
Logger.error(LOG_TAG, "OAuth failure.", e);
if (e.isInvalidAuthentication()) {
// We were married, generated an assertion, and our assertion was rejected by the
// oauth client. If it's a 401, we probably have a stale certificate. If instead of
// a stale certificate we have bad credentials, the state machine will fail to sign
// our public key and drive us back to Separated.
fxAccount.setState(married.makeCohabitingState());
}
responder.fail(e);
}
@Override
public void handleError(Exception e) {
Logger.error(LOG_TAG, "OAuth error.", e);
responder.fail(e);
}
});
}
});
}
@Override
public Bundle getAuthToken(final AccountAuthenticatorResponse response,
final Account account, final String authTokenType, final Bundle options)
throws NetworkErrorException {
Logger.debug(LOG_TAG, "getAuthToken: " + authTokenType);
// If we have a cached authToken, hand it over.
final String cachedAuthToken = AccountManager.get(context).peekAuthToken(account, authTokenType);
if (cachedAuthToken != null && !cachedAuthToken.isEmpty()) {
Logger.info(LOG_TAG, "Return cached token.");
final Bundle result = new Bundle();
result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type);
result.putString(AccountManager.KEY_AUTHTOKEN, cachedAuthToken);
return result;
}
// If we're asked for an oauth::scope token, try to generate one.
final String oauthPrefix = "oauth::";
if (authTokenType != null && authTokenType.startsWith(oauthPrefix)) {
final String scope = authTokenType.substring(oauthPrefix.length());
final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
try {
fxAccount.acquireSharedAccountStateLock(LOG_TAG);
} catch (InterruptedException e) {
Logger.warn(LOG_TAG, "Could not acquire account state lock; return error bundle.");
final Bundle bundle = new Bundle();
bundle.putInt(AccountManager.KEY_ERROR_CODE, 1);
bundle.putString(AccountManager.KEY_ERROR_MESSAGE, "Could not acquire account state lock.");
return bundle;
}
getOAuthToken(response, fxAccount, scope);
return null;
}
// Otherwise, fail.
Logger.warn(LOG_TAG, "Returning error bundle for getAuthToken with unknown token type.");
final Bundle bundle = new Bundle();
bundle.putInt(AccountManager.KEY_ERROR_CODE, 2);
bundle.putString(AccountManager.KEY_ERROR_MESSAGE, "Unknown token type: " + authTokenType);
return bundle;
}
@Override
public String getAuthTokenLabel(String authTokenType) {
Logger.debug(LOG_TAG, "getAuthTokenLabel");
return null;
}
@Override
public Bundle hasFeatures(AccountAuthenticatorResponse response,
Account account, String[] features) throws NetworkErrorException {
Logger.debug(LOG_TAG, "hasFeatures");
return null;
}
@Override
public Bundle updateCredentials(AccountAuthenticatorResponse response,
Account account, String authTokenType, Bundle options)
throws NetworkErrorException {
Logger.debug(LOG_TAG, "updateCredentials");
return null;
}
/**
* If the account is going to be removed, broadcast an "account deleted"
* intent. This allows us to clean up the account.
* <p>
* It is preferable to receive Android's LOGIN_ACCOUNTS_CHANGED_ACTION broadcast
* than to create our own hacky broadcast here, but that doesn't include enough
* information about which Accounts changed to correctly identify whether a Sync
* account has been removed (when some Firefox channels are installed on the SD
* card). We can work around this by storing additional state but it's both messy
* and expensive because the broadcast is noisy.
* <p>
* Note that this is <b>not</b> called when an Android Account is blown away
* due to the SD card being unmounted.
*/
@Override
public Bundle getAccountRemovalAllowed(final AccountAuthenticatorResponse response, Account account)
throws NetworkErrorException {
Bundle result = super.getAccountRemovalAllowed(response, account);
if (result == null ||
!result.containsKey(AccountManager.KEY_BOOLEAN_RESULT) ||
result.containsKey(AccountManager.KEY_INTENT)) {
return result;
}
final boolean removalAllowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT);
if (!removalAllowed) {
return result;
}
// Broadcast a message to all Firefox channels sharing this Android
// Account type telling that this Firefox account has been deleted.
//
// Broadcast intents protected with permissions are secure, so it's okay
// to include private information such as a password.
final AndroidFxAccount androidFxAccount = new AndroidFxAccount(context, account);
// Deleting the pickle file in a blocking manner will avoid race conditions that might happen when
// an account is unpickled while an FxAccount is being deleted.
// Also we have an assumption that this method is always called from a background thread, so we delete
// the pickle file directly without being afraid from a StrictMode violation.
ThreadUtils.assertNotOnUiThread();
final Intent serviceIntent = androidFxAccount.populateDeletedAccountIntent(
new Intent(context, FxAccountDeletedService.class)
);
Logger.info(LOG_TAG, "Account named " + account.name + " being removed; " +
"starting FxAccountDeletedService with action: " + serviceIntent.getAction() + ".");
context.startService(serviceIntent);
Logger.info(LOG_TAG, "Firefox account named " + account.name + " being removed; " +
"deleting saved pickle file '" + FxAccountConstants.ACCOUNT_PICKLE_FILENAME + "'.");
deletePickle();
return result;
}
private void deletePickle() {
try {
AccountPickler.deletePickle(context, FxAccountConstants.ACCOUNT_PICKLE_FILENAME);
} catch (Exception e) {
// This should never happen, but we really don't want to die in a background thread.
Logger.warn(LOG_TAG, "Got exception deleting saved pickle file; ignoring.", e);
}
}
}

View file

@ -1,55 +0,0 @@
/* 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/. */
package org.mozilla.gecko.fxa.authenticator;
import org.mozilla.gecko.background.common.log.Logger;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
public class FxAccountAuthenticatorService extends Service {
public static final String LOG_TAG = FxAccountAuthenticatorService.class.getSimpleName();
// Lazily initialized by <code>getAuthenticator</code>.
protected FxAccountAuthenticator accountAuthenticator;
protected synchronized FxAccountAuthenticator getAuthenticator() {
if (accountAuthenticator == null) {
accountAuthenticator = new FxAccountAuthenticator(this);
}
return accountAuthenticator;
}
@Override
public void onCreate() {
Logger.debug(LOG_TAG, "onCreate");
accountAuthenticator = getAuthenticator();
}
@Override
public IBinder onBind(Intent intent) {
Logger.debug(LOG_TAG, "onBind");
if (intent == null) {
// Should never happen, but can -- Bug 1025937.
return null;
}
if (!android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT.equals(intent.getAction())) {
return null;
}
final FxAccountAuthenticator authenticator = getAuthenticator();
if (authenticator == null) {
// Should never happen.
return null;
}
return authenticator.getIBinder();
}
}

View file

@ -1,26 +0,0 @@
/* 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/. */
package org.mozilla.gecko.fxa.authenticator;
/**
* Abstraction around things that might need to be signalled to the user via UI,
* such as:
* <ul>
* <li>account not yet verified;</li>
* <li>account password needs to be updated;</li>
* <li>account key management required or changed;</li>
* <li>auth protocol has changed and Firefox needs to be upgraded;</li>
* </ul>
* etc.
* <p>
* Consumers of this code should differentiate error classes based on the types
* of the exceptions thrown. Exceptions that do not have special meaning are of
* type <code>FxAccountLoginException</code> with an appropriate
* <code>cause</code> inner exception.
*/
public interface FxAccountLoginDelegate {
public void handleError(FxAccountLoginException e);
public void handleSuccess(String assertion);
}

View file

@ -1,33 +0,0 @@
/* 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/. */
package org.mozilla.gecko.fxa.authenticator;
public class FxAccountLoginException extends Exception {
public FxAccountLoginException(String string) {
super(string);
}
public FxAccountLoginException(Exception e) {
super(e);
}
private static final long serialVersionUID = 397685959625820798L;
public static class FxAccountLoginBadPasswordException extends FxAccountLoginException {
public FxAccountLoginBadPasswordException(String string) {
super(string);
}
private static final long serialVersionUID = 397685959625820799L;
}
public static class FxAccountLoginAccountNotVerifiedException extends FxAccountLoginException {
public FxAccountLoginAccountNotVerifiedException(String string) {
super(string);
}
private static final long serialVersionUID = 397685959625820800L;
}
}

View file

@ -1,49 +0,0 @@
/* 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/. */
package org.mozilla.gecko.fxa.login;
import org.mozilla.gecko.background.fxa.FxAccountClient20;
import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.AccountNeedsVerification;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LocalError;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.RemoteError;
public abstract class BaseRequestDelegate<T> implements FxAccountClient20.RequestDelegate<T> {
protected final ExecuteDelegate delegate;
protected final State state;
public BaseRequestDelegate(State state, ExecuteDelegate delegate) {
this.delegate = delegate;
this.state = state;
}
@Override
public void handleFailure(FxAccountClientRemoteException e) {
// Order matters here: we don't want to ignore upgrade required responses
// even if the server tells us something else as well. We don't go directly
// to the Doghouse on upgrade required; we want the user to try to update
// their credentials, and then display UI telling them they need to upgrade.
// Then they go to the Doghouse.
if (e.isUpgradeRequired()) {
delegate.handleTransition(new RemoteError(e), new Separated(state.email, state.uid, state.verified));
return;
}
if (e.isInvalidAuthentication()) {
delegate.handleTransition(new RemoteError(e), new Separated(state.email, state.uid, state.verified));
return;
}
if (e.isUnverified()) {
delegate.handleTransition(new AccountNeedsVerification(), state);
return;
}
delegate.handleTransition(new RemoteError(e), state);
}
@Override
public void handleError(Exception e) {
delegate.handleTransition(new LocalError(e), state);
}
}

View file

@ -1,50 +0,0 @@
/* 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/. */
package org.mozilla.gecko.fxa.login;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.browserid.BrowserIDKeyPair;
import org.mozilla.gecko.browserid.JSONWebTokenUtils;
import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LogMessage;
import org.mozilla.gecko.sync.ExtendedJSONObject;
public class Cohabiting extends TokensAndKeysState {
private static final String LOG_TAG = Cohabiting.class.getSimpleName();
public Cohabiting(String email, String uid, byte[] sessionToken, byte[] kA, byte[] kB, BrowserIDKeyPair keyPair) {
super(StateLabel.Cohabiting, email, uid, sessionToken, kA, kB, keyPair);
}
public Married withCertificate(String certificate) {
return new Married(email, uid, sessionToken, kA, kB, keyPair, certificate);
}
@Override
public void execute(final ExecuteDelegate delegate) {
delegate.getClient().sign(sessionToken, keyPair.getPublic().toJSONObject(), delegate.getCertificateDurationInMilliseconds(),
new BaseRequestDelegate<String>(this, delegate) {
@Override
public void handleSuccess(String certificate) {
if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
try {
FxAccountUtils.pii(LOG_TAG, "Fetched certificate: " + certificate);
ExtendedJSONObject c = JSONWebTokenUtils.parseCertificate(certificate);
if (c != null) {
FxAccountUtils.pii(LOG_TAG, "Header : " + c.getObject("header"));
FxAccountUtils.pii(LOG_TAG, "Payload : " + c.getObject("payload"));
FxAccountUtils.pii(LOG_TAG, "Signature: " + c.getString("signature"));
} else {
FxAccountUtils.pii(LOG_TAG, "Could not parse certificate!");
}
} catch (Exception e) {
FxAccountUtils.pii(LOG_TAG, "Could not parse certificate!");
}
}
delegate.handleTransition(new LogMessage("sign succeeded"), withCertificate(certificate));
}
});
}
}

View file

@ -1,25 +0,0 @@
/* 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/. */
package org.mozilla.gecko.fxa.login;
import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LogMessage;
public class Doghouse extends State {
public Doghouse(String email, String uid, boolean verified) {
super(StateLabel.Doghouse, email, uid, verified);
}
@Override
public void execute(final ExecuteDelegate delegate) {
delegate.handleTransition(new LogMessage("Upgraded Firefox clients might know what to do here."), this);
}
@Override
public Action getNeededAction() {
return Action.NeedsUpgrade;
}
}

View file

@ -1,91 +0,0 @@
/* 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/. */
package org.mozilla.gecko.fxa.login;
import java.security.NoSuchAlgorithmException;
import org.mozilla.gecko.background.fxa.FxAccountClient20.TwoKeys;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.browserid.BrowserIDKeyPair;
import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.AccountVerified;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LocalError;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LogMessage;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.RemoteError;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.Utils;
public class Engaged extends State {
private static final String LOG_TAG = Engaged.class.getSimpleName();
protected final byte[] sessionToken;
protected final byte[] keyFetchToken;
protected final byte[] unwrapkB;
public Engaged(String email, String uid, boolean verified, byte[] unwrapkB, byte[] sessionToken, byte[] keyFetchToken) {
super(StateLabel.Engaged, email, uid, verified);
Utils.throwIfNull(unwrapkB, sessionToken, keyFetchToken);
this.unwrapkB = unwrapkB;
this.sessionToken = sessionToken;
this.keyFetchToken = keyFetchToken;
}
@Override
public ExtendedJSONObject toJSONObject() {
ExtendedJSONObject o = super.toJSONObject();
// Fields are non-null by constructor.
o.put("unwrapkB", Utils.byte2Hex(unwrapkB));
o.put("sessionToken", Utils.byte2Hex(sessionToken));
o.put("keyFetchToken", Utils.byte2Hex(keyFetchToken));
return o;
}
@Override
public void execute(final ExecuteDelegate delegate) {
BrowserIDKeyPair theKeyPair;
try {
theKeyPair = delegate.generateKeyPair();
} catch (NoSuchAlgorithmException e) {
delegate.handleTransition(new LocalError(e), new Doghouse(email, uid, verified));
return;
}
final BrowserIDKeyPair keyPair = theKeyPair;
delegate.getClient().keys(keyFetchToken, new BaseRequestDelegate<TwoKeys>(this, delegate) {
@Override
public void handleSuccess(TwoKeys result) {
byte[] kB;
try {
kB = FxAccountUtils.unwrapkB(unwrapkB, result.wrapkB);
if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
FxAccountUtils.pii(LOG_TAG, "Fetched kA: " + Utils.byte2Hex(result.kA));
FxAccountUtils.pii(LOG_TAG, "And wrapkB: " + Utils.byte2Hex(result.wrapkB));
FxAccountUtils.pii(LOG_TAG, "Giving kB : " + Utils.byte2Hex(kB));
}
} catch (Exception e) {
delegate.handleTransition(new RemoteError(e), new Separated(email, uid, verified));
return;
}
Transition transition = verified
? new LogMessage("keys succeeded")
: new AccountVerified();
delegate.handleTransition(transition, new Cohabiting(email, uid, sessionToken, result.kA, kB, keyPair));
}
});
}
@Override
public Action getNeededAction() {
if (!verified) {
return Action.NeedsVerification;
}
return Action.None;
}
public byte[] getSessionToken() {
return sessionToken;
}
}

View file

@ -1,84 +0,0 @@
/* 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/. */
package org.mozilla.gecko.fxa.login;
import java.security.NoSuchAlgorithmException;
import java.util.EnumSet;
import java.util.Set;
import org.mozilla.gecko.background.fxa.FxAccountClient;
import org.mozilla.gecko.browserid.BrowserIDKeyPair;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition;
import org.mozilla.gecko.fxa.login.State.StateLabel;
public class FxAccountLoginStateMachine {
public static final String LOG_TAG = FxAccountLoginStateMachine.class.getSimpleName();
public interface LoginStateMachineDelegate {
public FxAccountClient getClient();
public long getCertificateDurationInMilliseconds();
public long getAssertionDurationInMilliseconds();
public void handleTransition(Transition transition, State state);
public void handleFinal(State state);
public BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException;
}
public static class ExecuteDelegate {
protected final LoginStateMachineDelegate delegate;
protected final StateLabel desiredStateLabel;
// It's as difficult to detect arbitrary cycles as repeated states.
protected final Set<StateLabel> stateLabelsSeen = EnumSet.noneOf(StateLabel.class);
protected ExecuteDelegate(StateLabel initialStateLabel, StateLabel desiredStateLabel, LoginStateMachineDelegate delegate) {
this.delegate = delegate;
this.desiredStateLabel = desiredStateLabel;
this.stateLabelsSeen.add(initialStateLabel);
}
public FxAccountClient getClient() {
return delegate.getClient();
}
public long getCertificateDurationInMilliseconds() {
return delegate.getCertificateDurationInMilliseconds();
}
public long getAssertionDurationInMilliseconds() {
return delegate.getAssertionDurationInMilliseconds();
}
public BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException {
return delegate.generateKeyPair();
}
public void handleTransition(Transition transition, State state) {
// Always trigger the transition callback.
delegate.handleTransition(transition, state);
// Possibly trigger the final callback. We trigger if we're at our desired
// state, or if we've seen this state before.
StateLabel stateLabel = state.getStateLabel();
if (stateLabel == desiredStateLabel || stateLabelsSeen.contains(stateLabel)) {
delegate.handleFinal(state);
return;
}
// If this wasn't the last state, leave a bread crumb and move on to the
// next state.
stateLabelsSeen.add(stateLabel);
state.execute(this);
}
}
public void advance(State initialState, final StateLabel desiredStateLabel, final LoginStateMachineDelegate delegate) {
if (initialState.getStateLabel() == desiredStateLabel) {
// We're already where we want to be!
delegate.handleFinal(initialState);
return;
}
ExecuteDelegate executeDelegate = new ExecuteDelegate(initialState.getStateLabel(), desiredStateLabel, delegate);
initialState.execute(executeDelegate);
}
}

View file

@ -1,68 +0,0 @@
/* 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/. */
package org.mozilla.gecko.fxa.login;
public class FxAccountLoginTransition {
public interface Transition {
}
public static class LogMessage implements Transition {
public final String detailMessage;
public LogMessage(String detailMessage) {
this.detailMessage = detailMessage;
}
@Override
public String toString() {
return getClass().getSimpleName() + (this.detailMessage == null ? "" : "('" + this.detailMessage + "')");
}
}
public static class AccountNeedsVerification extends LogMessage {
public AccountNeedsVerification() {
super(null);
}
}
public static class AccountVerified extends LogMessage {
public AccountVerified() {
super(null);
}
}
public static class PasswordRequired extends LogMessage {
public PasswordRequired() {
super(null);
}
}
public static class LocalError implements Transition {
public final Exception e;
public LocalError(Exception e) {
this.e = e;
}
@Override
public String toString() {
return "Log(" + this.e + ")";
}
}
public static class RemoteError implements Transition {
public final Exception e;
public RemoteError(Exception e) {
this.e = e;
}
@Override
public String toString() {
return "Log(" + (this.e == null ? "null" : this.e) + ")";
}
}
}

Some files were not shown because too many files have changed in this diff Show more