diff --git a/mobile/android/base/android-services.mozbuild b/mobile/android/base/android-services.mozbuild index c74bea63556..49a198a2119 100644 --- a/mobile/android/base/android-services.mozbuild +++ b/mobile/android/base/android-services.mozbuild @@ -503,6 +503,7 @@ sync_java_files = [ 'background/fxa/FxAccount10CreateDelegate.java', 'background/fxa/FxAccount20CreateDelegate.java', 'background/fxa/FxAccount20LoginDelegate.java', + 'background/fxa/FxAccountClient.java', 'background/fxa/FxAccountClient10.java', 'background/fxa/FxAccountClient20.java', 'background/fxa/FxAccountClientException.java', diff --git a/mobile/android/base/background/fxa/FxAccountClient.java b/mobile/android/base/background/fxa/FxAccountClient.java index 3f3e287f5b9..635b6a9d611 100644 --- a/mobile/android/base/background/fxa/FxAccountClient.java +++ b/mobile/android/base/background/fxa/FxAccountClient.java @@ -4,639 +4,15 @@ package org.mozilla.gecko.background.fxa; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.net.URISyntaxException; -import java.security.GeneralSecurityException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.util.Arrays; -import java.util.concurrent.Executor; - -import javax.crypto.Mac; - -import org.json.simple.JSONObject; +import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate; +import org.mozilla.gecko.background.fxa.FxAccountClient10.StatusResponse; +import org.mozilla.gecko.background.fxa.FxAccountClient10.TwoKeys; +import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse; 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 ch.boye.httpclientandroidlib.HttpEntity; -import ch.boye.httpclientandroidlib.HttpResponse; -import ch.boye.httpclientandroidlib.client.ClientProtocolException; - -/** - * An HTTP client for talking to an FxAccount server. - *

- * The reference server is developed at - * https://github.com/mozilla/picl-idp. - * This implementation was developed against - * https://github.com/mozilla/picl-idp/commit/c7a02a0cbbb43f332058dc060bd84a21e56ec208. - *

- * The delegate structure used is a little different from the rest of the code - * base. We add a RequestDelegate layer that processes a typed - * value extracted from the body of a successful response. - *

- * Further, we add internal CreateDelegate and - * AuthDelegate delegates to make it easier to modify the request - * bodies sent to the /create and /auth endpoints. - */ -public class FxAccountClient { - protected static final String LOG_TAG = FxAccountClient.class.getSimpleName(); - - protected static final String VERSION_FRAGMENT = "v1/"; - - 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"; - - protected final String serverURI; - protected final Executor executor; - - public FxAccountClient(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 + "/") + VERSION_FRAGMENT; - this.executor = executor; - } - - /** - * Process a typed value extracted from a successful response (in an - * endpoint-dependent way). - */ - public interface RequestDelegate { - public void handleError(Exception e); - public void handleFailure(int status, HttpResponse response); - public void handleSuccess(T result); - } - - /** - * A CreateDelegate produces the body of a /create request. - */ - public interface CreateDelegate { - public JSONObject getCreateBody() throws FxAccountClientException; - } - - /** - * A AuthDelegate produces the bodies of an /auth/{start,finish} - * request pair and exposes state generated by a successful response. - */ - public interface AuthDelegate { - public JSONObject getAuthStartBody() throws FxAccountClientException; - public void onAuthStartResponse(ExtendedJSONObject body) throws FxAccountClientException; - public JSONObject getAuthFinishBody() throws FxAccountClientException; - - public byte[] getSharedBytes() throws FxAccountClientException; - } - - /** - * Thin container for two access tokens. - */ - public static class TwoTokens { - public final byte[] keyFetchToken; - public final byte[] sessionToken; - public TwoTokens(byte[] keyFetchToken, byte[] sessionToken) { - this.keyFetchToken = keyFetchToken; - this.sessionToken = sessionToken; - } - } - - /** - * 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 void invokeHandleError(final RequestDelegate delegate, final Exception e) { - executor.execute(new Runnable() { - @Override - public void run() { - delegate.handleError(e); - } - }); - } - - /** - * Translate resource callbacks into request callbacks invoked on the provided - * executor. - *

- * Override handleSuccess to parse the body of the resource - * request and call the request callback. handleSuccess is - * invoked via the executor, so you don't need to delegate further. - */ - protected abstract class ResourceDelegate extends BaseResourceDelegate { - protected abstract void handleSuccess(final int status, HttpResponse response, final ExtendedJSONObject body); - - protected final RequestDelegate delegate; - - protected final byte[] tokenId; - protected final byte[] reqHMACKey; - protected final boolean payload; - - /** - * Create a delegate for an un-authenticated resource. - */ - public ResourceDelegate(final Resource resource, final RequestDelegate delegate) { - this(resource, delegate, null, null, false); - } - - /** - * Create a delegate for a Hawk-authenticated resource. - */ - public ResourceDelegate(final Resource resource, final RequestDelegate delegate, final byte[] tokenId, final byte[] reqHMACKey, final boolean authenticatePayload) { - super(resource); - this.delegate = delegate; - this.reqHMACKey = reqHMACKey; - this.tokenId = tokenId; - this.payload = authenticatePayload; - } - - @Override - public AuthHeaderProvider getAuthHeaderProvider() { - if (tokenId != null && reqHMACKey != null) { - return new HawkAuthHeaderProvider(Utils.byte2Hex(tokenId), reqHMACKey, payload); - } - return super.getAuthHeaderProvider(); - } - - @Override - public void handleHttpResponse(HttpResponse response) { - final int status = response.getStatusLine().getStatusCode(); - switch (status) { - case 200: - invokeHandleSuccess(status, response); - return; - default: - invokeHandleFailure(status, response); - return; - } - } - - protected void invokeHandleFailure(final int status, final HttpResponse response) { - executor.execute(new Runnable() { - @Override - public void run() { - delegate.handleFailure(status, response); - } - }); - } - - 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); - } - } - - protected void post(BaseResource resource, final JSONObject requestBody, final RequestDelegate delegate) { - try { - if (requestBody == null) { - resource.post((HttpEntity) null); - } else { - resource.post(requestBody); - } - } catch (UnsupportedEncodingException e) { - invokeHandleError(delegate, e); - return; - } - } - - public void createAccount(final String email, final byte[] stretchedPWBytes, - final String srpSalt, final String mainSalt, - final RequestDelegate delegate) { - try { - createAccount(new FxAccount10CreateDelegate(email, stretchedPWBytes, srpSalt, mainSalt), delegate); - } catch (final Exception e) { - invokeHandleError(delegate, e); - return; - } - } - - protected void createAccount(final CreateDelegate createDelegate, final RequestDelegate delegate) { - JSONObject body = null; - try { - body = createDelegate.getCreateBody(); - } catch (FxAccountClientException e) { - invokeHandleError(delegate, e); - return; - } - - BaseResource resource; - try { - resource = new BaseResource(new URI(serverURI + "account/create")); - } catch (URISyntaxException e) { - invokeHandleError(delegate, e); - return; - } - - resource.delegate = new ResourceDelegate(resource, delegate) { - @Override - public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) { - String uid = body.getString("uid"); - if (uid == null) { - delegate.handleError(new FxAccountClientException("uid must be a non-null string")); - return; - } - delegate.handleSuccess(uid); - } - }; - post(resource, body, delegate); - } - - protected void authStart(final AuthDelegate authDelegate, final RequestDelegate delegate) { - JSONObject body; - try { - body = authDelegate.getAuthStartBody(); - } catch (FxAccountClientException e) { - invokeHandleError(delegate, e); - return; - } - - BaseResource resource; - try { - resource = new BaseResource(new URI(serverURI + "auth/start")); - } catch (URISyntaxException e) { - invokeHandleError(delegate, e); - return; - } - - resource.delegate = new ResourceDelegate(resource, delegate) { - @Override - public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) { - try { - authDelegate.onAuthStartResponse(body); - delegate.handleSuccess(authDelegate); - } catch (Exception e) { - delegate.handleError(e); - return; - } - } - }; - post(resource, body, delegate); - } - - protected void authFinish(final AuthDelegate authDelegate, RequestDelegate delegate) { - JSONObject body; - try { - body = authDelegate.getAuthFinishBody(); - } catch (FxAccountClientException e) { - invokeHandleError(delegate, e); - return; - } - - BaseResource resource; - try { - resource = new BaseResource(new URI(serverURI + "auth/finish")); - } catch (URISyntaxException e) { - invokeHandleError(delegate, e); - return; - } - - resource.delegate = new ResourceDelegate(resource, delegate) { - @Override - public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) { - try { - byte[] authToken = new byte[32]; - unbundleBody(body, authDelegate.getSharedBytes(), FxAccountUtils.KW("auth/finish"), authToken); - delegate.handleSuccess(authToken); - } catch (Exception e) { - delegate.handleError(e); - return; - } - } - }; - post(resource, body, delegate); - } - - public void login(final String email, final byte[] stretchedPWBytes, final RequestDelegate delegate) { - login(new FxAccount10AuthDelegate(email, stretchedPWBytes), delegate); - } - - protected void login(final AuthDelegate authDelegate, final RequestDelegate delegate) { - authStart(authDelegate, new RequestDelegate() { - @Override - public void handleSuccess(AuthDelegate srpSession) { - authFinish(srpSession, delegate); - } - - @Override - public void handleError(final Exception e) { - invokeHandleError(delegate, e); - return; - } - - @Override - public void handleFailure(final int status, final HttpResponse response) { - executor.execute(new Runnable() { - @Override - public void run() { - delegate.handleFailure(status, response); - } - }); - } - }); - } - - public void sessionCreate(byte[] authToken, final RequestDelegate delegate) { - final byte[] tokenId = new byte[32]; - final byte[] reqHMACKey = new byte[32]; - final byte[] requestKey = new byte[32]; - try { - HKDF.deriveMany(authToken, new byte[0], FxAccountUtils.KW("authToken"), tokenId, reqHMACKey, requestKey); - } catch (Exception e) { - invokeHandleError(delegate, e); - return; - } - - BaseResource resource; - try { - resource = new BaseResource(new URI(serverURI + "session/create")); - } catch (URISyntaxException e) { - invokeHandleError(delegate, e); - return; - } - - resource.delegate = new ResourceDelegate(resource, delegate, tokenId, reqHMACKey, false) { - @Override - public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) { - try { - byte[] keyFetchToken = new byte[32]; - byte[] sessionToken = new byte[32]; - unbundleBody(body, requestKey, FxAccountUtils.KW("session/create"), keyFetchToken, sessionToken); - delegate.handleSuccess(new TwoTokens(keyFetchToken, sessionToken)); - return; - } catch (Exception e) { - delegate.handleError(e); - return; - } - } - }; - post(resource, null, delegate); - } - - public void sessionDestroy(byte[] sessionToken, final RequestDelegate delegate) { - 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 = new BaseResource(new URI(serverURI + "session/destroy")); - } catch (URISyntaxException e) { - invokeHandleError(delegate, e); - return; - } - - resource.delegate = new ResourceDelegate(resource, delegate, tokenId, reqHMACKey, false) { - @Override - public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) { - delegate.handleSuccess(null); - } - }; - post(resource, null, delegate); - } - - /** - * Don't call this directly. Use unbundleBody 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 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 = new BaseResource(new URI(serverURI + "account/keys")); - } catch (URISyntaxException e) { - invokeHandleError(delegate, e); - return; - } - - resource.delegate = new ResourceDelegate(resource, delegate, tokenId, reqHMACKey, false) { - @Override - public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) { - try { - byte[] kA = new byte[32]; - byte[] wrapkB = new byte[32]; - unbundleBody(body, requestKey, FxAccountUtils.KW("account/keys"), kA, wrapkB); - delegate.handleSuccess(new TwoKeys(kA, wrapkB)); - return; - } catch (Exception e) { - delegate.handleError(e); - return; - } - } - }; - resource.get(); - } - - /** - * Thin container for status response. - */ - public static class StatusResponse { - public final String email; - public final boolean verified; - public StatusResponse(String email, boolean verified) { - this.email = email; - this.verified = verified; - } - } - - /** - * Query the status of an account given a valid session token. - *

- * 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 status(byte[] sessionToken, final RequestDelegate 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 = new BaseResource(new URI(serverURI + "recovery_email/status")); - } catch (URISyntaxException e) { - invokeHandleError(delegate, e); - return; - } - - resource.delegate = new ResourceDelegate(resource, delegate, tokenId, reqHMACKey, false) { - @Override - public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) { - try { - 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 StatusResponse(email, verified)); - return; - } catch (Exception e) { - delegate.handleError(e); - return; - } - } - }; - resource.get(); - } - - @SuppressWarnings("unchecked") - public void sign(final byte[] sessionToken, final ExtendedJSONObject publicKey, long durationInSeconds, final RequestDelegate delegate) { - final JSONObject body = new JSONObject(); - body.put("publicKey", publicKey); - body.put("duration", durationInSeconds); - - 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 = new BaseResource(new URI(serverURI + "certificate/sign")); - } catch (URISyntaxException e) { - invokeHandleError(delegate, e); - return; - } - - resource.delegate = new ResourceDelegate(resource, delegate, tokenId, reqHMACKey, true) { - @Override - public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) { - 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, delegate); - } +public interface FxAccountClient { + public void loginAndGetKeys(final byte[] emailUTF8, final byte[] quickStretchedPW, final RequestDelegate requestDelegate); + public void status(byte[] sessionToken, RequestDelegate requestDelegate); + public void keys(byte[] keyFetchToken, RequestDelegate requestDelegate); + public void sign(byte[] sessionToken, ExtendedJSONObject publicKey, long certificateDurationInMilliseconds, RequestDelegate requestDelegate); } diff --git a/mobile/android/base/background/fxa/FxAccountClient20.java b/mobile/android/base/background/fxa/FxAccountClient20.java index 2c24fda2a0a..c805fbfa616 100644 --- a/mobile/android/base/background/fxa/FxAccountClient20.java +++ b/mobile/android/base/background/fxa/FxAccountClient20.java @@ -14,7 +14,7 @@ import org.mozilla.gecko.sync.net.BaseResource; import ch.boye.httpclientandroidlib.HttpResponse; -public class FxAccountClient20 extends FxAccountClient10 { +public class FxAccountClient20 extends FxAccountClient10 implements FxAccountClient { 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 }; diff --git a/mobile/android/base/fxa/FxAccountConstants.java.in b/mobile/android/base/fxa/FxAccountConstants.java.in index 41173353231..bd7cbd6d1b9 100644 --- a/mobile/android/base/fxa/FxAccountConstants.java.in +++ b/mobile/android/base/fxa/FxAccountConstants.java.in @@ -5,6 +5,8 @@ package org.mozilla.gecko.fxa; +import org.mozilla.gecko.background.common.log.Logger; + public class FxAccountConstants { public static final String GLOBAL_LOG_TAG = "FxAccounts"; public static final String ACCOUNT_TYPE = "@MOZ_ANDROID_SHARED_FXACCOUNT_TYPE@"; @@ -13,4 +15,14 @@ public class FxAccountConstants { public static final String DEFAULT_AUTH_ENDPOINT = "http://auth.oldsync.dev.lcip.org"; public static final String PREFS_PATH = "fxa.v1"; + + // For extra debugging. Not final so it can be changed from Fennec, or from + // an add-on. + public static boolean LOG_PERSONAL_INFORMATION = true; + + public static void pii(String tag, String message) { + if (LOG_PERSONAL_INFORMATION) { + Logger.info(tag, "$$FxA PII$$: " + message); + } + } } diff --git a/mobile/android/base/fxa/authenticator/AbstractFxAccount.java b/mobile/android/base/fxa/authenticator/AbstractFxAccount.java index e9abcbe39be..e2c06fb5406 100644 --- a/mobile/android/base/fxa/authenticator/AbstractFxAccount.java +++ b/mobile/android/base/fxa/authenticator/AbstractFxAccount.java @@ -40,11 +40,14 @@ public interface AbstractFxAccount { */ public String getServerURI(); + public boolean isValid(); + public void setInvalid(); + public byte[] getSessionToken(); public byte[] getKeyFetchToken(); - public void invalidateSessionToken(); - public void invalidateKeyFetchToken(); + public void setSessionToken(byte[] token); + public void setKeyFetchToken(byte[] token); /** * Return true if and only if this account is guaranteed to be verified. This @@ -77,4 +80,13 @@ public interface AbstractFxAccount { public void setWrappedKb(byte[] wrappedKb); BrowserIDKeyPair getAssertionKeyPair() throws GeneralSecurityException; + + public String getCertificate(); + public void setCertificate(String certificate); + + public String getAssertion(); + public void setAssertion(String assertion); + + public byte[] getEmailUTF8(); + public byte[] getQuickStretchedPW(); } diff --git a/mobile/android/base/fxa/authenticator/AndroidFxAccount.java b/mobile/android/base/fxa/authenticator/AndroidFxAccount.java index 619d53f0adf..88a7c899cd0 100644 --- a/mobile/android/base/fxa/authenticator/AndroidFxAccount.java +++ b/mobile/android/base/fxa/authenticator/AndroidFxAccount.java @@ -6,6 +6,8 @@ package org.mozilla.gecko.fxa.authenticator; import java.io.UnsupportedEncodingException; import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.Collections; import org.mozilla.gecko.background.common.log.Logger; import org.mozilla.gecko.background.fxa.FxAccountUtils; @@ -31,6 +33,9 @@ import android.os.Bundle; public class AndroidFxAccount implements AbstractFxAccount { protected static final String LOG_TAG = AndroidFxAccount.class.getSimpleName(); + public static final String ACCOUNT_KEY_ASSERTION = "assertion"; + public static final String ACCOUNT_KEY_CERTIFICATE = "certificate"; + public static final String ACCOUNT_KEY_INVALID = "invalid"; public static final String ACCOUNT_KEY_SERVERURI = "serverURI"; public static final String ACCOUNT_KEY_SESSION_TOKEN = "sessionToken"; public static final String ACCOUNT_KEY_KEY_FETCH_TOKEN = "keyFetchToken"; @@ -66,6 +71,21 @@ public class AndroidFxAccount implements AbstractFxAccount { this.accountManager = AccountManager.get(this.context); } + @Override + public byte[] getEmailUTF8() { + try { + return account.name.getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + // Ignore. + return null; + } + } + + @Override + public byte[] getQuickStretchedPW() { + return Utils.hex2Byte(accountManager.getPassword(account)); + } + @Override public String getServerURI() { return accountManager.getUserData(account, ACCOUNT_KEY_SERVERURI); @@ -90,13 +110,13 @@ public class AndroidFxAccount implements AbstractFxAccount { } @Override - public void invalidateSessionToken() { - accountManager.setUserData(account, ACCOUNT_KEY_SESSION_TOKEN, null); + public void setSessionToken(byte[] sessionToken) { + accountManager.setUserData(account, ACCOUNT_KEY_SESSION_TOKEN, sessionToken == null ? null : Utils.byte2Hex(sessionToken)); } @Override - public void invalidateKeyFetchToken() { - accountManager.setUserData(account, ACCOUNT_KEY_KEY_FETCH_TOKEN, null); + public void setKeyFetchToken(byte[] keyFetchToken) { + accountManager.setUserData(account, ACCOUNT_KEY_KEY_FETCH_TOKEN, keyFetchToken == null ? null : Utils.byte2Hex(keyFetchToken)); } @Override @@ -155,6 +175,26 @@ public class AndroidFxAccount implements AbstractFxAccount { return keyPair; } + @Override + public String getCertificate() { + return accountManager.getUserData(account, ACCOUNT_KEY_CERTIFICATE); + } + + @Override + public void setCertificate(String certificate) { + accountManager.setUserData(account, ACCOUNT_KEY_CERTIFICATE, certificate); + } + + @Override + public String getAssertion() { + return accountManager.getUserData(account, ACCOUNT_KEY_ASSERTION); + } + + @Override + public void setAssertion(String assertion) { + accountManager.setUserData(account, ACCOUNT_KEY_ASSERTION, assertion); + } + /** * Extract a JSON dictionary of the string values associated to this account. *

@@ -167,16 +207,27 @@ public class AndroidFxAccount implements AbstractFxAccount { public ExtendedJSONObject toJSONObject() { ExtendedJSONObject o = new ExtendedJSONObject(); for (String key : new String[] { + ACCOUNT_KEY_ASSERTION, + ACCOUNT_KEY_CERTIFICATE, ACCOUNT_KEY_SERVERURI, ACCOUNT_KEY_SESSION_TOKEN, + ACCOUNT_KEY_INVALID, ACCOUNT_KEY_KEY_FETCH_TOKEN, ACCOUNT_KEY_VERIFIED, ACCOUNT_KEY_KA, ACCOUNT_KEY_KB, ACCOUNT_KEY_UNWRAPKB, - ACCOUNT_KEY_ASSERTION_KEY_PAIR }) { + ACCOUNT_KEY_ASSERTION_KEY_PAIR, + }) { o.put(key, accountManager.getUserData(account, key)); } + o.put("email", account.name); + try { + o.put("emailUTF8", Utils.byte2Hex(account.name.getBytes("UTF-8"))); + } catch (UnsupportedEncodingException e) { + // Ignore. + } + o.put("quickStretchedPW", accountManager.getPassword(account)); return o; } @@ -208,8 +259,8 @@ public class AndroidFxAccount implements AbstractFxAccount { Bundle userdata = new Bundle(); userdata.putString(AndroidFxAccount.ACCOUNT_KEY_SERVERURI, serverURI); - userdata.putString(AndroidFxAccount.ACCOUNT_KEY_SESSION_TOKEN, Utils.byte2Hex(sessionToken)); - userdata.putString(AndroidFxAccount.ACCOUNT_KEY_KEY_FETCH_TOKEN, Utils.byte2Hex(keyFetchToken)); + userdata.putString(AndroidFxAccount.ACCOUNT_KEY_SESSION_TOKEN, sessionToken == null ? null : Utils.byte2Hex(sessionToken)); + userdata.putString(AndroidFxAccount.ACCOUNT_KEY_KEY_FETCH_TOKEN, keyFetchToken == null ? null : Utils.byte2Hex(keyFetchToken)); userdata.putString(AndroidFxAccount.ACCOUNT_KEY_VERIFIED, Boolean.valueOf(verified).toString()); userdata.putString(AndroidFxAccount.ACCOUNT_KEY_UNWRAPKB, Utils.byte2Hex(unwrapBkey)); @@ -222,4 +273,40 @@ public class AndroidFxAccount implements AbstractFxAccount { FxAccountAuthenticator.enableSyncing(context, account); return account; } + + @Override + public boolean isValid() { + // Boolean.valueOf only returns true for the string "true"; this errors in + // the direction of marking accounts valid. + boolean invalid = Boolean.valueOf(accountManager.getUserData(account, ACCOUNT_KEY_INVALID)).booleanValue(); + return !invalid; + } + + @Override + public void setInvalid() { + accountManager.setUserData(account, ACCOUNT_KEY_INVALID, Boolean.valueOf(true).toString()); + } + + /** + * For debugging only! + */ + public void dump() { + if (!FxAccountConstants.LOG_PERSONAL_INFORMATION) { + return; + } + ExtendedJSONObject o = toJSONObject(); + ArrayList list = new ArrayList(o.keySet()); + Collections.sort(list); + for (String key : list) { + FxAccountConstants.pii(LOG_TAG, key + ": " + o.getString(key)); + } + } + + /** + * For debugging only! + */ + public void resetAccountTokens() { + accountManager.setUserData(account, ACCOUNT_KEY_SESSION_TOKEN, null); + accountManager.setUserData(account, ACCOUNT_KEY_KEY_FETCH_TOKEN, null); + } } diff --git a/mobile/android/base/fxa/authenticator/FxAccountLoginPolicy.java b/mobile/android/base/fxa/authenticator/FxAccountLoginPolicy.java index 431db429e0d..364b3983aa6 100644 --- a/mobile/android/base/fxa/authenticator/FxAccountLoginPolicy.java +++ b/mobile/android/base/fxa/authenticator/FxAccountLoginPolicy.java @@ -4,23 +4,25 @@ package org.mozilla.gecko.fxa.authenticator; -import java.security.GeneralSecurityException; import java.util.LinkedList; import java.util.concurrent.Executor; import org.mozilla.gecko.background.common.log.Logger; +import org.mozilla.gecko.background.fxa.FxAccountClient; import org.mozilla.gecko.background.fxa.FxAccountClient10; import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate; import org.mozilla.gecko.background.fxa.FxAccountClient10.StatusResponse; import org.mozilla.gecko.background.fxa.FxAccountClient10.TwoKeys; import org.mozilla.gecko.background.fxa.FxAccountClient20; +import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse; import org.mozilla.gecko.browserid.BrowserIDKeyPair; import org.mozilla.gecko.browserid.JSONWebTokenUtils; -import org.mozilla.gecko.browserid.SigningPrivateKey; import org.mozilla.gecko.browserid.VerifyingPublicKey; +import org.mozilla.gecko.fxa.FxAccountConstants; import org.mozilla.gecko.fxa.authenticator.FxAccountLoginException.FxAccountLoginAccountNotVerifiedException; import org.mozilla.gecko.fxa.authenticator.FxAccountLoginException.FxAccountLoginBadPasswordException; import org.mozilla.gecko.sync.HTTPFailureException; +import org.mozilla.gecko.sync.Utils; import org.mozilla.gecko.sync.net.SyncStorageResponse; import android.content.Context; @@ -39,13 +41,130 @@ public class FxAccountLoginPolicy { this.executor = executor; } - protected void invokeHandleHardFailure(final FxAccountLoginDelegate delegate, final FxAccountLoginException e) { - executor.execute(new Runnable() { - @Override - public void run() { - delegate.handleError(e); - } - }); + public long certificateDurationInMilliseconds = JSONWebTokenUtils.DEFAULT_CERTIFICATE_DURATION_IN_MILLISECONDS; + public long assertionDurationInMilliseconds = JSONWebTokenUtils.DEFAULT_ASSERTION_DURATION_IN_MILLISECONDS; + + public long getCertificateDurationInMilliseconds() { + return certificateDurationInMilliseconds; + } + + public long getAssertionDurationInMilliseconds() { + return assertionDurationInMilliseconds; + } + + protected FxAccountClient makeFxAccountClient() { + String serverURI = fxAccount.getServerURI(); + return new FxAccountClient20(serverURI, executor); + } + + /** + * Check if this certificate is not worth generating an assertion from: for + * example, because it is not well-formed, or it is already expired. + * + * @param certificate + * to check. + * @return if it is definitely not worth generating an assertion from this + * certificate. + */ + protected boolean isInvalidCertificate(String certificate) { + return false; + } + + /** + * Check if this assertion is not worth presenting to the token server: for + * example, because it is not well-formed, or it is already expired. + * + * @param assertion + * to check. + * @return if assertion is definitely not worth presenting to the token + * server. + */ + protected boolean isInvalidAssertion(String assertion) { + return false; + } + + public enum AccountState { + Invalid, + NeedsSessionToken, + NeedsVerification, + NeedsKeys, + NeedsCertificate, + NeedsAssertion, + Valid, + }; + + public AccountState getAccountState(AbstractFxAccount fxAccount) { + String serverURI = fxAccount.getServerURI(); + byte[] emailUTF8 = fxAccount.getEmailUTF8(); + byte[] quickStretchedPW = fxAccount.getQuickStretchedPW(); + if (!fxAccount.isValid() || serverURI == null || emailUTF8 == null || quickStretchedPW == null) { + return AccountState.Invalid; + } + + byte[] sessionToken = fxAccount.getSessionToken(); + if (sessionToken == null) { + return AccountState.NeedsSessionToken; + } + + if (!fxAccount.isVerified()) { + return AccountState.NeedsVerification; + } + + // Verify against server? Tricky. + if (fxAccount.getKa() == null || fxAccount.getKb() == null) { + return AccountState.NeedsKeys; + } + + String certificate = fxAccount.getCertificate(); + if (certificate == null || isInvalidCertificate(certificate)) { + return AccountState.NeedsCertificate; + } + + String assertion = fxAccount.getAssertion(); + if (assertion == null || isInvalidAssertion(assertion)) { + return AccountState.NeedsAssertion; + } + + return AccountState.Valid; + } + + protected interface LoginStage { + public void execute(LoginStageDelegate delegate) throws Exception; + } + + protected LinkedList getStages(AccountState state) { + final LinkedList stages = new LinkedList(); + if (state == AccountState.Invalid) { + stages.addFirst(new FailStage()); + return stages; + } + + stages.addFirst(new SuccessStage()); + if (state == AccountState.Valid) { + return stages; + } + stages.addFirst(new EnsureAssertionStage()); + if (state == AccountState.NeedsAssertion) { + return stages; + } + stages.addFirst(new EnsureCertificateStage()); + if (state == AccountState.NeedsCertificate) { + return stages; + } + stages.addFirst(new EnsureKeysStage()); + stages.addFirst(new EnsureKeyFetchTokenStage()); + if (state == AccountState.NeedsKeys) { + return stages; + } + stages.addFirst(new EnsureVerificationStage()); + if (state == AccountState.NeedsVerification) { + return stages; + } + stages.addFirst(new EnsureSessionTokenStage()); + if (state == AccountState.NeedsSessionToken) { + return stages; + } + return stages; } /** @@ -58,121 +177,97 @@ public class FxAccountLoginPolicy { * @param delegate providing callbacks to invoke. */ public void login(final String audience, final FxAccountLoginDelegate delegate) { - final LinkedList stages = new LinkedList(); - stages.add(new CheckPreconditionsLoginStage()); - stages.add(new CheckVerifiedLoginStage()); - stages.add(new EnsureDerivedKeysLoginStage()); - stages.add(new FetchCertificateLoginStage()); + final AccountState initialState = getAccountState(fxAccount); + Logger.info(LOG_TAG, "Logging in account from initial state " + initialState + "."); - advance(audience, stages, delegate); + final LinkedList stages = getStages(initialState); + final LinkedList stageNames = new LinkedList(); + for (LoginStage stage : stages) { + stageNames.add(stage.getClass().getSimpleName()); + } + Logger.info(LOG_TAG, "Executing stages: [" + Utils.toCommaSeparatedString(stageNames) + "]"); + + LoginStageDelegate loginStageDelegate = new LoginStageDelegate(stages, audience, delegate); + loginStageDelegate.advance(); } - protected interface LoginStageDelegate { - public String getAssertionAudience(); - public void handleError(FxAccountLoginException e); - public void handleStageSuccess(); - public void handleLoginSuccess(String assertion); - } + protected class LoginStageDelegate { + public final LinkedList stages; + public final String audience; + public final FxAccountLoginDelegate delegate; + public final FxAccountClient client; - protected interface LoginStage { - public void execute(LoginStageDelegate delegate); - } + protected LoginStage currentStage = null; - /** - * Pop the next stage off stages and execute it. - *

- * This trades stack efficiency for implementation simplicity. - * - * @param delegate - * @param stages - */ - protected void advance(final String audience, final LinkedList stages, final FxAccountLoginDelegate delegate) { - LoginStage stage = stages.poll(); - if (stage == null) { - // No more stages. But we haven't seen an assertion. Failure! - Logger.info(LOG_TAG, "No more stages: login failed?"); - invokeHandleHardFailure(delegate, new FxAccountLoginException("No more stages, but no assertion: login failed?")); + public LoginStageDelegate(LinkedList stages, String audience, FxAccountLoginDelegate delegate) { + this.stages = stages; + this.audience = audience; + this.delegate = delegate; + this.client = makeFxAccountClient(); + } + + protected void invokeHandleHardFailure(final FxAccountLoginDelegate delegate, final FxAccountLoginException e) { + executor.execute(new Runnable() { + @Override + public void run() { + delegate.handleError(e); + } + }); + } + + public void advance() { + currentStage = stages.poll(); + if (currentStage == null) { + // No more stages. But we haven't seen an assertion. Failure! + Logger.info(LOG_TAG, "No more stages: login failed?"); + invokeHandleHardFailure(delegate, new FxAccountLoginException("No more stages, but no assertion: login failed?")); + return; + } + + try { + Logger.info(LOG_TAG, "Executing stage: " + currentStage.getClass().getSimpleName()); + currentStage.execute(this); + } catch (Exception e) { + Logger.info(LOG_TAG, "Got exception during stage.", e); + invokeHandleHardFailure(delegate, new FxAccountLoginException(e)); + return; + } + } + + public void handleStageSuccess() { + Logger.info(LOG_TAG, "Stage succeeded: " + currentStage.getClass().getSimpleName()); + advance(); + } + + public void handleLoginSuccess(final String assertion) { + Logger.info(LOG_TAG, "Login succeeded."); + executor.execute(new Runnable() { + @Override + public void run() { + delegate.handleSuccess(assertion); + } + }); return; } - stage.execute(new LoginStageDelegate() { - @Override - public void handleStageSuccess() { - Logger.info(LOG_TAG, "Stage succeeded."); - advance(audience, stages, delegate); - } - - @Override - public void handleLoginSuccess(final String assertion) { - Logger.info(LOG_TAG, "Login succeeded."); - executor.execute(new Runnable() { - @Override - public void run() { - delegate.handleSuccess(assertion); - } - }); - return; - } - - @Override - public void handleError(FxAccountLoginException e) { - invokeHandleHardFailure(delegate, e); - } - - @Override - public String getAssertionAudience() { - return audience; - } - }); - } - - /** - * Verify we have a valid server URI, session token, etc. If not, we have to - * prompt for credentials. - */ - public class CheckPreconditionsLoginStage implements LoginStage { - @Override - public void execute(final LoginStageDelegate delegate) { - final String audience = delegate.getAssertionAudience(); - if (audience == null) { - delegate.handleError(new FxAccountLoginException("Account has no audience.")); - return; - } - - String serverURI = fxAccount.getServerURI(); - if (serverURI == null) { - delegate.handleError(new FxAccountLoginException("Account has no server URI.")); - return; - } - - byte[] sessionToken = fxAccount.getSessionToken(); - if (sessionToken == null) { - delegate.handleError(new FxAccountLoginBadPasswordException("Account has no session token.")); - return; - } - - delegate.handleStageSuccess(); + public void handleError(FxAccountLoginException e) { + invokeHandleHardFailure(delegate, e); } } - /** - * Now that we have a server to talk to and a session token, we can use them - * to check that the account is verified. - */ - public class CheckVerifiedLoginStage implements LoginStage { + public class EnsureSessionTokenStage implements LoginStage { @Override - public void execute(final LoginStageDelegate delegate) { - if (fxAccount.isVerified()) { - Logger.info(LOG_TAG, "Account is already marked verified. Skipping remote status check."); - delegate.handleStageSuccess(); - return; + public void execute(final LoginStageDelegate delegate) throws Exception { + byte[] emailUTF8 = fxAccount.getEmailUTF8(); + if (emailUTF8 == null) { + throw new IllegalStateException("emailUTF8 must not be null"); + } + byte[] quickStretchedPW = fxAccount.getQuickStretchedPW(); + if (quickStretchedPW == null) { + throw new IllegalStateException("quickStretchedPW must not be null"); } - String serverURI = fxAccount.getServerURI(); - byte[] sessionToken = fxAccount.getSessionToken(); - final FxAccountClient20 client = new FxAccountClient20(serverURI, executor); - - client.status(sessionToken, new RequestDelegate() { + delegate.client.loginAndGetKeys(emailUTF8, quickStretchedPW, new RequestDelegate() { @Override public void handleError(Exception e) { delegate.handleError(new FxAccountLoginException(e)); @@ -184,6 +279,53 @@ public class FxAccountLoginPolicy { delegate.handleError(new FxAccountLoginException(new HTTPFailureException(new SyncStorageResponse(response)))); return; } + // We just got denied for a sessionToken. That's a problem with + // our email or password. Only thing to do is mark the account + // invalid and ask for user intervention. + fxAccount.setInvalid(); + delegate.handleError(new FxAccountLoginBadPasswordException("Auth server rejected email/password while fetching sessionToken.")); + } + + @Override + public void handleSuccess(LoginResponse result) { + fxAccount.setSessionToken(result.sessionToken); + fxAccount.setKeyFetchToken(result.keyFetchToken); + if (FxAccountConstants.LOG_PERSONAL_INFORMATION) { + FxAccountConstants.pii(LOG_TAG, "Fetched sessionToken : " + Utils.byte2Hex(result.sessionToken)); + FxAccountConstants.pii(LOG_TAG, "Fetched keyFetchToken: " + Utils.byte2Hex(result.keyFetchToken)); + } + delegate.handleStageSuccess(); + } + }); + } + } + + /** + * Now that we have a server to talk to and a session token, we can use them + * to check that the account is verified. + */ + public class EnsureVerificationStage implements LoginStage { + @Override + public void execute(final LoginStageDelegate delegate) { + byte[] sessionToken = fxAccount.getSessionToken(); + if (sessionToken == null) { + throw new IllegalArgumentException("sessionToken must not be null"); + } + + delegate.client.status(sessionToken, new RequestDelegate() { + @Override + public void handleError(Exception e) { + delegate.handleError(new FxAccountLoginException(e)); + } + + @Override + public void handleFailure(int status, HttpResponse response) { + if (status != 401) { + delegate.handleError(new FxAccountLoginException(new HTTPFailureException(new SyncStorageResponse(response)))); + return; + } + // We just got denied due to our sessionToken. Invalidate it. + fxAccount.setSessionToken(null); delegate.handleError(new FxAccountLoginBadPasswordException("Auth server rejected session token while fetching status.")); } @@ -202,31 +344,81 @@ public class FxAccountLoginPolicy { } } - /** - * Now we have a verified account, we can make sure that our local keys are - * consistent with the account's keys. - */ - public class EnsureDerivedKeysLoginStage implements LoginStage { + public static int[] DUMMY = null; + + public class EnsureKeyFetchTokenStage implements LoginStage { @Override - public void execute(final LoginStageDelegate delegate) { - byte[] kA = fxAccount.getKa(); - byte[] kB = fxAccount.getKb(); - if (kA != null && kB != null) { - Logger.info(LOG_TAG, "Account already has kA and kB. Skipping key derivation stage."); + public void execute(final LoginStageDelegate delegate) throws Exception { + byte[] emailUTF8 = fxAccount.getEmailUTF8(); + if (emailUTF8 == null) { + throw new IllegalStateException("emailUTF8 must not be null"); + } + byte[] quickStretchedPW = fxAccount.getQuickStretchedPW(); + if (quickStretchedPW == null) { + throw new IllegalStateException("quickStretchedPW must not be null"); + } + + boolean verified = fxAccount.isVerified(); + if (!verified) { + throw new IllegalStateException("must be verified"); + } + + // We might already have a valid keyFetchToken. If so, try it. If it's not + // valid, we'll invalidate it in EnsureKeysStage. + if (fxAccount.getKeyFetchToken() != null) { + Logger.info(LOG_TAG, "Using existing keyFetchToken."); delegate.handleStageSuccess(); return; } + delegate.client.loginAndGetKeys(emailUTF8, quickStretchedPW, new RequestDelegate() { + @Override + public void handleError(Exception e) { + delegate.handleError(new FxAccountLoginException(e)); + } + + @Override + public void handleFailure(int status, HttpResponse response) { + if (status != 401) { + delegate.handleError(new FxAccountLoginException(new HTTPFailureException(new SyncStorageResponse(response)))); + return; + } + // We just got denied for a keyFetchToken. That's a problem with + // our email or password. Only thing to do is mark the account + // invalid and ask for user intervention. + fxAccount.setInvalid(); + delegate.handleError(new FxAccountLoginBadPasswordException("Auth server rejected email/password while fetching keyFetchToken.")); + } + + @Override + public void handleSuccess(LoginResponse result) { + fxAccount.setKeyFetchToken(result.keyFetchToken); + if (FxAccountConstants.LOG_PERSONAL_INFORMATION) { + FxAccountConstants.pii(LOG_TAG, "Fetched keyFetchToken: " + Utils.byte2Hex(result.keyFetchToken)); + } + delegate.handleStageSuccess(); + } + }); + } + } + + /** + * Now we have a verified account, we can make sure that our local keys are + * consistent with the account's keys. + */ + public class EnsureKeysStage implements LoginStage { + @Override + public void execute(final LoginStageDelegate delegate) throws Exception { byte[] keyFetchToken = fxAccount.getKeyFetchToken(); if (keyFetchToken == null) { - // XXX this might mean something else? - delegate.handleError(new FxAccountLoginBadPasswordException("Account has no key fetch token.")); - return; + throw new IllegalStateException("keyFetchToken must not be null"); } - String serverURI = fxAccount.getServerURI(); - final FxAccountClient20 client = new FxAccountClient20(serverURI, executor); - client.keys(keyFetchToken, new RequestDelegate() { + // Make sure we don't use a keyFetchToken twice. This conveniently + // invalidates any invalid keyFetchToken we might try, too. + fxAccount.setKeyFetchToken(null); + + delegate.client.keys(keyFetchToken, new RequestDelegate() { @Override public void handleError(Exception e) { delegate.handleError(new FxAccountLoginException(e)); @@ -245,39 +437,35 @@ public class FxAccountLoginPolicy { public void handleSuccess(TwoKeys result) { fxAccount.setKa(result.kA); fxAccount.setWrappedKb(result.wrapkB); + if (FxAccountConstants.LOG_PERSONAL_INFORMATION) { + FxAccountConstants.pii(LOG_TAG, "Fetched kA: " + Utils.byte2Hex(result.kA)); + FxAccountConstants.pii(LOG_TAG, "And wrapkB: " + Utils.byte2Hex(result.wrapkB)); + FxAccountConstants.pii(LOG_TAG, "Giving kB : " + Utils.byte2Hex(fxAccount.getKb())); + } delegate.handleStageSuccess(); } }); } } - public class FetchCertificateLoginStage implements LoginStage { + public class EnsureCertificateStage implements LoginStage { @Override - public void execute(final LoginStageDelegate delegate) { - BrowserIDKeyPair keyPair; - try { - keyPair = fxAccount.getAssertionKeyPair(); - if (keyPair == null) { - Logger.info(LOG_TAG, "Account has no key pair."); - delegate.handleError(new FxAccountLoginException("Account has no key pair.")); - return; - } - } catch (GeneralSecurityException e) { - delegate.handleError(new FxAccountLoginException(e)); - return; + public void execute(final LoginStageDelegate delegate) throws Exception{ + byte[] sessionToken = fxAccount.getSessionToken(); + if (sessionToken == null) { + throw new IllegalStateException("keyPair must not be null"); + } + BrowserIDKeyPair keyPair = fxAccount.getAssertionKeyPair(); + if (keyPair == null) { + // If we can't fetch a keypair, we probably have some crypto + // configuration error on device, which we are never going to recover + // from. Mark the account invalid. + fxAccount.setInvalid(); + throw new IllegalStateException("keyPair must not be null"); } - - final SigningPrivateKey privateKey = keyPair.getPrivate(); final VerifyingPublicKey publicKey = keyPair.getPublic(); - byte[] sessionToken = fxAccount.getSessionToken(); - String serverURI = fxAccount.getServerURI(); - final FxAccountClient20 client = new FxAccountClient20(serverURI, executor); - - // TODO Make this duration configurable (that is, part of the policy). - long certificateDurationInMilliseconds = JSONWebTokenUtils.DEFAULT_CERTIFICATE_DURATION_IN_MILLISECONDS; - - client.sign(sessionToken, publicKey.toJSONObject(), certificateDurationInMilliseconds, new RequestDelegate() { + delegate.client.sign(sessionToken, publicKey.toJSONObject(), getCertificateDurationInMilliseconds(), new RequestDelegate() { @Override public void handleError(Exception e) { delegate.handleError(new FxAccountLoginException(e)); @@ -289,24 +477,78 @@ public class FxAccountLoginPolicy { delegate.handleError(new FxAccountLoginException(new HTTPFailureException(new SyncStorageResponse(response)))); return; } + // Our sessionToken was just rejected; we should get a new + // sessionToken. TODO: Make sure the exception below is fine + // enough grained. + // Since this is the place we'll see the majority of lifecylcle + // auth problems, we should be much more aggressive bumping the + // state machine out of this state when we don't get success. + fxAccount.setSessionToken(null); delegate.handleError(new FxAccountLoginBadPasswordException("Auth server rejected session token while fetching status.")); } @Override public void handleSuccess(String certificate) { - try { - String assertion = JSONWebTokenUtils.createAssertion(privateKey, certificate, delegate.getAssertionAudience()); - if (Logger.LOG_PERSONAL_INFORMATION) { - Logger.pii(LOG_TAG, "Generated assertion " + assertion); - JSONWebTokenUtils.dumpAssertion(assertion); - } - delegate.handleLoginSuccess(assertion); - } catch (Exception e) { - delegate.handleError(new FxAccountLoginException(e)); - return; + fxAccount.setCertificate(certificate); + if (FxAccountConstants.LOG_PERSONAL_INFORMATION) { + FxAccountConstants.pii(LOG_TAG, "Fetched certificate: " + certificate); + JSONWebTokenUtils.dumpCertificate(certificate); } + delegate.handleStageSuccess(); } }); } } + + public class EnsureAssertionStage implements LoginStage { + @Override + public void execute(final LoginStageDelegate delegate) throws Exception { + BrowserIDKeyPair keyPair = fxAccount.getAssertionKeyPair(); + if (keyPair == null) { + throw new IllegalStateException("keyPair must not be null"); + } + String certificate = fxAccount.getCertificate(); + if (certificate == null) { + throw new IllegalStateException("certificate must not be null"); + } + String assertion; + try { + long now = System.currentTimeMillis(); + assertion = JSONWebTokenUtils.createAssertion(keyPair.getPrivate(), certificate, delegate.audience, + JSONWebTokenUtils.DEFAULT_ASSERTION_ISSUER, now, getAssertionDurationInMilliseconds()); + } catch (Exception e) { + // If we can't sign an assertion, we probably have some crypto + // configuration error on device, which we are never going to recover + // from. Mark the account invalid before raising the exception. + fxAccount.setInvalid(); + throw e; + } + fxAccount.setAssertion(assertion); + if (FxAccountConstants.LOG_PERSONAL_INFORMATION) { + FxAccountConstants.pii(LOG_TAG, "Generated assertion: " + assertion); + JSONWebTokenUtils.dumpAssertion(assertion); + } + delegate.handleStageSuccess(); + } + } + + public class SuccessStage implements LoginStage { + @Override + public void execute(final LoginStageDelegate delegate) throws Exception { + String assertion = fxAccount.getAssertion(); + if (assertion == null) { + throw new IllegalStateException("assertion must not be null"); + } + delegate.handleLoginSuccess(assertion); + } + } + + public class FailStage implements LoginStage { + @Override + public void execute(final LoginStageDelegate delegate) { + AccountState finalState = getAccountState(fxAccount); + Logger.info(LOG_TAG, "Failed to login account; final state is " + finalState + "."); + delegate.handleError(new FxAccountLoginException("Failed to login.")); + } + } } diff --git a/mobile/android/base/fxa/sync/FxAccountGlobalSession.java b/mobile/android/base/fxa/sync/FxAccountGlobalSession.java index 5fb7850674f..d75c544a2ac 100644 --- a/mobile/android/base/fxa/sync/FxAccountGlobalSession.java +++ b/mobile/android/base/fxa/sync/FxAccountGlobalSession.java @@ -11,7 +11,7 @@ import java.util.Collections; import java.util.HashMap; import org.json.simple.parser.ParseException; -import org.mozilla.gecko.background.common.log.Logger; +import org.mozilla.gecko.fxa.FxAccountConstants; import org.mozilla.gecko.sync.GlobalSession; import org.mozilla.gecko.sync.NonObjectJSONException; import org.mozilla.gecko.sync.SyncConfigurationException; @@ -39,7 +39,7 @@ public class FxAccountGlobalSession extends GlobalSession { callback, context, extras, clientsDelegate, null); URI uri = new URI(storageEndpoint); this.config.clusterURL = new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(), "/", null, null); - Logger.warn(LOG_TAG, "storageEndpoint is " + uri + " and clusterURL is " + config.clusterURL); + FxAccountConstants.pii(LOG_TAG, "storageEndpoint is " + uri + " and clusterURL is " + config.clusterURL); } @Override diff --git a/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java b/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java index 63d3ac1c977..532867dc690 100644 --- a/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java +++ b/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java @@ -5,14 +5,14 @@ package org.mozilla.gecko.fxa.sync; import java.net.URI; -import java.util.ArrayList; -import java.util.Collections; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.mozilla.gecko.background.common.log.Logger; import org.mozilla.gecko.background.fxa.FxAccountUtils; +import org.mozilla.gecko.browserid.verifier.BrowserIDRemoteVerifierClient; +import org.mozilla.gecko.browserid.verifier.BrowserIDVerifierDelegate; import org.mozilla.gecko.fxa.FxAccountConstants; import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; import org.mozilla.gecko.fxa.authenticator.FxAccountAuthenticator; @@ -129,16 +129,16 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter { final AndroidFxAccount fxAccount = new AndroidFxAccount(getContext(), account); - if (Logger.LOG_PERSONAL_INFORMATION) { - ExtendedJSONObject o = new AndroidFxAccount(getContext(), account).toJSONObject(); - ArrayList list = new ArrayList(o.keySet()); - Collections.sort(list); - for (String key : list) { - Logger.pii(LOG_TAG, key + ": " + o.getString(key)); - } + if (FxAccountConstants.LOG_PERSONAL_INFORMATION) { + fxAccount.dump(); } + final SharedPreferences sharedPrefs = getContext().getSharedPreferences(FxAccountConstants.PREFS_PATH, Context.MODE_PRIVATE); // TODO Ensure preferences are per-Account. + final FxAccountLoginPolicy loginPolicy = new FxAccountLoginPolicy(getContext(), fxAccount, executor); + loginPolicy.certificateDurationInMilliseconds = 20 * 60 * 1000; + loginPolicy.assertionDurationInMilliseconds = 15 * 60 * 1000; + Logger.info(LOG_TAG, "Asking for certificates to expire after 20 minutes and assertions to expire after 15 minutes."); loginPolicy.login(authEndpoint, new FxAccountLoginDelegate() { @Override @@ -147,12 +147,12 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter { tokenServerclient.getTokenFromBrowserIDAssertion(assertion, true, new TokenServerClientDelegate() { @Override public void handleSuccess(final TokenServerToken token) { - Logger.pii(LOG_TAG, "Got token! uid is " + token.uid + " and endpoint is " + token.endpoint + "."); + FxAccountConstants.pii(LOG_TAG, "Got token! uid is " + token.uid + " and endpoint is " + token.endpoint + "."); + sharedPrefs.edit().putLong("tokenFailures", 0).commit(); final BaseGlobalSessionCallback callback = new SessionCallback(latch); FxAccountGlobalSession globalSession = null; try { - SharedPreferences sharedPrefs = getContext().getSharedPreferences(FxAccountConstants.PREFS_PATH, Context.MODE_PRIVATE); ClientsDataDelegate clientsDataDelegate = new SharedPreferencesClientsDataDelegate(sharedPrefs); final KeyBundle syncKeyBundle = FxAccountUtils.generateSyncKeyBundle(fxAccount.getKb()); // TODO Document this choice for deriving from kB. AuthHeaderProvider authHeaderProvider = new HawkAuthHeaderProvider(token.id, token.key.getBytes("UTF-8"), false); @@ -166,6 +166,21 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter { @Override public void handleFailure(TokenServerException e) { + // This is tricky since the token server fairly + // consistently rejects a token the first time it sees it + // before accepting it for the rest of its lifetime. + long MAX_TOKEN_FAILURES_PER_TOKEN = 2; + long tokenFailures = 1 + sharedPrefs.getLong("tokenFailures", 0); + if (tokenFailures > MAX_TOKEN_FAILURES_PER_TOKEN) { + fxAccount.setCertificate(null); + tokenFailures = 0; + Logger.warn(LOG_TAG, "Seen too many failures with this token; resetting: " + tokenFailures); + Logger.warn(LOG_TAG, "To aid debugging, synchronously sending assertion to remote verifier for second look."); + debugAssertion(tokenServerEndpoint, assertion); + } else { + Logger.info(LOG_TAG, "Seen " + tokenFailures + " failures with this token so far."); + } + sharedPrefs.edit().putLong("tokenFailures", tokenFailures).commit(); handleError(e); } @@ -190,4 +205,34 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter { latch.countDown(); } } + + protected void debugAssertion(String audience, String assertion) { + final CountDownLatch verifierLatch = new CountDownLatch(1); + BrowserIDRemoteVerifierClient client = new BrowserIDRemoteVerifierClient(URI.create(BrowserIDRemoteVerifierClient.DEFAULT_VERIFIER_URL)); + client.verify(audience, assertion, new BrowserIDVerifierDelegate() { + @Override + public void handleSuccess(ExtendedJSONObject response) { + Logger.info(LOG_TAG, "Remote verifier returned success: " + response.toJSONString()); + verifierLatch.countDown(); + } + + @Override + public void handleFailure(ExtendedJSONObject response) { + Logger.warn(LOG_TAG, "Remote verifier returned failure: " + response.toJSONString()); + verifierLatch.countDown(); + } + + @Override + public void handleError(Exception e) { + Logger.error(LOG_TAG, "Remote verifier returned error.", e); + verifierLatch.countDown(); + } + }); + + try { + verifierLatch.await(); + } catch (InterruptedException e) { + Logger.error(LOG_TAG, "Got error.", e); + } + } } diff --git a/mobile/android/base/resources/values/fxaccount_styles.xml b/mobile/android/base/resources/values/fxaccount_styles.xml deleted file mode 100644 index c73f4bee04e..00000000000 --- a/mobile/android/base/resources/values/fxaccount_styles.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - -