Bug 956844 - Part 1: update Android FxSyncAdapter to use new onepw protocol login flow. r=rnewman

This commit is contained in:
Nick Alexander 2014-01-16 18:35:08 -08:00
parent 43b73d2e64
commit 8d080742f8
21 changed files with 1425 additions and 75 deletions

View File

@ -503,7 +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',
'background/fxa/FxAccountUtils.java',
@ -541,8 +541,13 @@ sync_java_files = [
'browserid/verifier/BrowserIDVerifierException.java',
'browserid/VerifyingPublicKey.java',
'fxa/activities/FxAccountSetupActivity.java',
'fxa/authenticator/AbstractFxAccount.java',
'fxa/authenticator/AndroidFxAccount.java',
'fxa/authenticator/FxAccountAuthenticator.java',
'fxa/authenticator/FxAccountAuthenticatorService.java',
'fxa/authenticator/FxAccountLoginDelegate.java',
'fxa/authenticator/FxAccountLoginException.java',
'fxa/authenticator/FxAccountLoginPolicy.java',
'fxa/sync/FxAccount.java',
'fxa/sync/FxAccountGlobalSession.java',
'fxa/sync/FxAccountSyncAdapter.java',

View File

@ -40,6 +40,7 @@ public abstract class BackgroundService extends IntentService {
* Returns true if the OS will allow us to perform background
* data operations. This logic varies by OS version.
*/
@SuppressWarnings("deprecation")
protected boolean backgroundDataIsEnabled() {
ConnectivityManager connectivity = (ConnectivityManager) this.getSystemService(Context.CONNECTIVITY_SERVICE);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {

View File

@ -34,6 +34,7 @@ public class AnnouncementPresenter {
* @param uri
* The URL to open when the notification is tapped.
*/
@SuppressWarnings("deprecation")
public static void displayAnnouncement(final Context context,
final int notificationID,
final String title,

View File

@ -13,7 +13,7 @@ import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.net.SRPConstants;
public class FxAccount10AuthDelegate implements FxAccountClient.AuthDelegate {
public class FxAccount10AuthDelegate implements FxAccountClient10.AuthDelegate {
// Fixed by protocol.
protected final BigInteger N;
protected final BigInteger g;

View File

@ -9,7 +9,7 @@ import java.math.BigInteger;
import java.security.NoSuchAlgorithmException;
import org.json.simple.JSONObject;
import org.mozilla.gecko.background.fxa.FxAccountClient.CreateDelegate;
import org.mozilla.gecko.background.fxa.FxAccountClient10.CreateDelegate;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.net.SRPConstants;

View File

@ -8,8 +8,12 @@ import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import org.json.simple.JSONObject;
import org.mozilla.gecko.background.fxa.FxAccountClient10.CreateDelegate;
import org.mozilla.gecko.sync.Utils;
public class FxAccount20CreateDelegate extends FxAccount20LoginDelegate {
public class FxAccount20CreateDelegate implements CreateDelegate {
protected final byte[] emailUTF8;
protected final byte[] authPW;
protected final boolean preVerified;
/**
@ -17,24 +21,31 @@ public class FxAccount20CreateDelegate extends FxAccount20LoginDelegate {
*
* @param emailUTF8
* email as UTF-8 bytes.
* @param passwordUTF8
* password 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[] passwordUTF8, boolean preVerified) throws UnsupportedEncodingException, GeneralSecurityException {
super(emailUTF8, passwordUTF8);
public FxAccount20CreateDelegate(byte[] emailUTF8, byte[] quickStretchedPW, boolean preVerified) throws UnsupportedEncodingException, GeneralSecurityException {
this.emailUTF8 = emailUTF8;
this.authPW = FxAccountUtils.generateAuthPW(quickStretchedPW);
this.preVerified = preVerified;
}
@SuppressWarnings("unchecked")
@Override
public JSONObject getCreateBody() throws FxAccountClientException {
final JSONObject body = super.getCreateBody();
body.put("preVerified", preVerified);
return body;
final JSONObject body = new JSONObject();
try {
body.put("email", new String(emailUTF8, "UTF-8"));
body.put("authPW", Utils.byte2Hex(authPW));
body.put("preVerified", preVerified);
return body;
} catch (UnsupportedEncodingException e) {
throw new FxAccountClientException(e);
}
}
}

View File

@ -8,7 +8,7 @@ import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import org.json.simple.JSONObject;
import org.mozilla.gecko.background.fxa.FxAccountClient.CreateDelegate;
import org.mozilla.gecko.background.fxa.FxAccountClient10.CreateDelegate;
import org.mozilla.gecko.sync.Utils;
/**
@ -17,13 +17,11 @@ import org.mozilla.gecko.sync.Utils;
*/
public class FxAccount20LoginDelegate implements CreateDelegate {
protected final byte[] emailUTF8;
protected final byte[] passwordUTF8;
protected final byte[] authPW;
public FxAccount20LoginDelegate(byte[] emailUTF8, byte[] passwordUTF8) throws UnsupportedEncodingException, GeneralSecurityException {
public FxAccount20LoginDelegate(byte[] emailUTF8, byte[] quickStretchedPW) throws UnsupportedEncodingException, GeneralSecurityException {
this.emailUTF8 = emailUTF8;
this.passwordUTF8 = passwordUTF8;
this.authPW = FxAccountUtils.generateAuthPW(FxAccountUtils.generateQuickStretchedPW(emailUTF8, passwordUTF8));
this.authPW = FxAccountUtils.generateAuthPW(quickStretchedPW);
}
@SuppressWarnings("unchecked")

View File

@ -0,0 +1,642 @@
/* 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.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.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.
* <p>
* The reference server is developed at
* <a href="https://github.com/mozilla/picl-idp">https://github.com/mozilla/picl-idp</a>.
* This implementation was developed against
* <a href="https://github.com/mozilla/picl-idp/commit/c7a02a0cbbb43f332058dc060bd84a21e56ec208">https://github.com/mozilla/picl-idp/commit/c7a02a0cbbb43f332058dc060bd84a21e56ec208</a>.
* <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.
* <p>
* Further, we add internal <code>CreateDelegate</code> and
* <code>AuthDelegate</code> delegates to make it easier to modify the request
* bodies sent to the /create and /auth endpoints.
*/
public class FxAccountClient10 {
protected static final String LOG_TAG = FxAccountClient10.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 FxAccountClient10(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<T> {
public void handleError(Exception e);
public void handleFailure(int status, HttpResponse response);
public void handleSuccess(T result);
}
/**
* A <code>CreateDelegate</code> produces the body of a /create request.
*/
public interface CreateDelegate {
public JSONObject getCreateBody() throws FxAccountClientException;
}
/**
* A <code>AuthDelegate</code> 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 <T> void invokeHandleError(final RequestDelegate<T> 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.
* <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;
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<T> delegate) {
this(resource, delegate, null, null, false);
}
/**
* Create a delegate for a Hawk-authenticated resource.
*/
public ResourceDelegate(final Resource resource, final RequestDelegate<T> 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 <T> void post(BaseResource resource, final JSONObject requestBody, final RequestDelegate<T> 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<String> 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<String> 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<String>(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<AuthDelegate> 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<AuthDelegate>(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<byte[]> 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<byte[]>(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<byte[]> delegate) {
login(new FxAccount10AuthDelegate(email, stretchedPWBytes), delegate);
}
protected void login(final AuthDelegate authDelegate, final RequestDelegate<byte[]> delegate) {
authStart(authDelegate, new RequestDelegate<AuthDelegate>() {
@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<TwoTokens> 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<TwoTokens>(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<Void> 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<Void>(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 <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 = new BaseResource(new URI(serverURI + "account/keys"));
} catch (URISyntaxException e) {
invokeHandleError(delegate, e);
return;
}
resource.delegate = new ResourceDelegate<TwoKeys>(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.
* <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 status(byte[] sessionToken, final RequestDelegate<StatusResponse> 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<StatusResponse>(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 durationInMilliseconds, final RequestDelegate<String> delegate) {
final JSONObject body = new JSONObject();
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 = new BaseResource(new URI(serverURI + "certificate/sign"));
} catch (URISyntaxException e) {
invokeHandleError(delegate, e);
return;
}
resource.delegate = new ResourceDelegate<String>(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);
}
}

View File

@ -14,7 +14,7 @@ import org.mozilla.gecko.sync.net.BaseResource;
import ch.boye.httpclientandroidlib.HttpResponse;
public class FxAccountClient20 extends FxAccountClient {
public class FxAccountClient20 extends FxAccountClient10 {
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 };
@ -23,10 +23,10 @@ public class FxAccountClient20 extends FxAccountClient {
super(serverURI, executor);
}
public void createAccount(final byte[] emailUTF8, final byte[] passwordUTF8, final boolean preVerified,
public void createAccount(final byte[] emailUTF8, final byte[] quickStretchedPW, final boolean preVerified,
final RequestDelegate<String> delegate) {
try {
createAccount(new FxAccount20CreateDelegate(emailUTF8, passwordUTF8, preVerified), delegate);
createAccount(new FxAccount20CreateDelegate(emailUTF8, quickStretchedPW, preVerified), delegate);
} catch (final Exception e) {
invokeHandleError(delegate, e);
return;
@ -55,25 +55,25 @@ public class FxAccountClient20 extends FxAccountClient {
}
}
public void login(final byte[] emailUTF8, final byte[] passwordUTF8,
public void login(final byte[] emailUTF8, final byte[] quickStretchedPW,
final RequestDelegate<LoginResponse> delegate) {
login(emailUTF8, passwordUTF8, false, delegate);
login(emailUTF8, quickStretchedPW, false, delegate);
}
public void loginAndGetKeys(final byte[] emailUTF8, final byte[] passwordUTF8,
public void loginAndGetKeys(final byte[] emailUTF8, final byte[] quickStretchedPW,
final RequestDelegate<LoginResponse> delegate) {
login(emailUTF8, passwordUTF8, true, delegate);
login(emailUTF8, quickStretchedPW, true, delegate);
}
// Public for testing only; prefer login and loginAndGetKeys (without boolean parameter).
public void login(final byte[] emailUTF8, final byte[] passwordUTF8, final boolean getKeys,
public void login(final byte[] emailUTF8, final byte[] quickStretchedPW, final boolean getKeys,
final RequestDelegate<LoginResponse> delegate) {
BaseResource resource;
JSONObject body;
final String path = getKeys ? "account/login?keys=true" : "account/login";
try {
resource = new BaseResource(new URI(serverURI + path));
body = new FxAccount20LoginDelegate(emailUTF8, passwordUTF8).getCreateBody();
body = new FxAccount20LoginDelegate(emailUTF8, quickStretchedPW).getCreateBody();
} catch (Exception e) {
invokeHandleError(delegate, e);
return;

View File

@ -0,0 +1,80 @@
/* 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.GeneralSecurityException;
import org.mozilla.gecko.browserid.BrowserIDKeyPair;
/**
* A representation of a Firefox Account.
* <p>
* Keeps track of:
* <ul>
* <li>tokens;</li>
* <li>verification state;</li>
* <li>auth server managed keys;</li>
* <li>locally managed key pairs</li>
* </ul>
* <p>
* <code>kA</code> is a <i>recoverable</i> auth server managed key.
* <code>kB</code> is an <i>unrecoverable</i> auth server managed key. Changing
* the user's password maintains <code>kA</code> and <code>kB</code>, but
* resetting the user's password retains only <code>kA</code> (and losees
* <code>kB</code>).
* <p>
* The entropy of <code>kB</code> is partially derived from the server and
* partially from the user's password. The auth server stores <code>kB</code>
* remotely, wrapped in a key derived from the user's password. The unwrapping
* process is implementation specific, but it is expected that the appropriate
* derivative of the user's password will be stored until
* <code>setWrappedKb</code> is called, at which point <code>kB</code> will be
* computed and cached, ready to be returned by <code>getKb</code>.
*/
public interface AbstractFxAccount {
/**
* Get the Firefox Account auth server URI that this account login flow should
* talk to.
*/
public String getServerURI();
public byte[] getSessionToken();
public byte[] getKeyFetchToken();
public void invalidateSessionToken();
public void invalidateKeyFetchToken();
/**
* Return true if and only if this account is guaranteed to be verified. This
* is intended to be a local cache of the verified state. Do not query the
* auth server!
*/
public boolean isVerified();
/**
* Update the account's local cache to reflect that this account is known to
* be verified.
*/
public void setVerified();
public byte[] getKa();
public void setKa(byte[] kA);
public byte[] getKb();
/**
* The auth server returns <code>kA</code> and <code>wrap(kB)</code> in
* response to <code>/account/keys</code>. This method accepts that wrapped
* value and uses whatever (per concrete type) method it can to derive the
* unwrapped value and cache it for retrieval by <code>getKb</code>.
* <p>
* See also {@link AbstractFxAccount}.
*
* @param wrappedKb <code>wrap(kB)</code> from auth server response.
*/
public void setWrappedKb(byte[] wrappedKb);
BrowserIDKeyPair getAssertionKeyPair() throws GeneralSecurityException;
}

View File

@ -0,0 +1,225 @@
/* 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.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.browserid.BrowserIDKeyPair;
import org.mozilla.gecko.browserid.RSACryptoImplementation;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.Utils;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.Context;
import android.os.Bundle;
/**
* 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 implements AbstractFxAccount {
protected static final String LOG_TAG = AndroidFxAccount.class.getSimpleName();
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";
public static final String ACCOUNT_KEY_VERIFIED = "verified";
public static final String ACCOUNT_KEY_KA = "kA";
public static final String ACCOUNT_KEY_KB = "kB";
public static final String ACCOUNT_KEY_UNWRAPKB = "unwrapkB";
public static final String ACCOUNT_KEY_ASSERTION_KEY_PAIR = "assertionKeyPair";
protected final Context context;
protected final AccountManager accountManager;
protected final Account account;
/**
* 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);
}
@Override
public String getServerURI() {
return accountManager.getUserData(account, ACCOUNT_KEY_SERVERURI);
}
protected byte[] getUserDataBytes(String key) {
String data = accountManager.getUserData(account, key);
if (data == null) {
return null;
}
return Utils.hex2Byte(data);
}
@Override
public byte[] getSessionToken() {
return getUserDataBytes(ACCOUNT_KEY_SESSION_TOKEN);
}
@Override
public byte[] getKeyFetchToken() {
return getUserDataBytes(ACCOUNT_KEY_KEY_FETCH_TOKEN);
}
@Override
public void invalidateSessionToken() {
accountManager.setUserData(account, ACCOUNT_KEY_SESSION_TOKEN, null);
}
@Override
public void invalidateKeyFetchToken() {
accountManager.setUserData(account, ACCOUNT_KEY_KEY_FETCH_TOKEN, null);
}
@Override
public boolean isVerified() {
String data = accountManager.getUserData(account, ACCOUNT_KEY_VERIFIED);
return Boolean.valueOf(data);
}
@Override
public void setVerified() {
accountManager.setUserData(account, ACCOUNT_KEY_VERIFIED, Boolean.valueOf(true).toString());
}
@Override
public byte[] getKa() {
return getUserDataBytes(ACCOUNT_KEY_KA);
}
@Override
public void setKa(byte[] kA) {
accountManager.setUserData(account, ACCOUNT_KEY_KA, Utils.byte2Hex(kA));
}
@Override
public void setWrappedKb(byte[] wrappedKb) {
byte[] unwrapKb = getUserDataBytes(ACCOUNT_KEY_UNWRAPKB);
byte[] kB = new byte[wrappedKb.length]; // We could hard-code this to be 32.
for (int i = 0; i < wrappedKb.length; i++) {
kB[i] = (byte) (wrappedKb[i] ^ unwrapKb[i]);
}
accountManager.setUserData(account, ACCOUNT_KEY_KB, Utils.byte2Hex(kB));
}
@Override
public byte[] getKb() {
return getUserDataBytes(ACCOUNT_KEY_KB);
}
protected BrowserIDKeyPair generateNewAssertionKeyPair() throws GeneralSecurityException {
Logger.info(LOG_TAG, "Generating new assertion key pair.");
// TODO Have the key size be a non-constant in FxAccountUtils, or read from SharedPreferences, or...
return RSACryptoImplementation.generateKeyPair(1024);
}
@Override
public BrowserIDKeyPair getAssertionKeyPair() throws GeneralSecurityException {
try {
String data = accountManager.getUserData(account, ACCOUNT_KEY_ASSERTION_KEY_PAIR);
return RSACryptoImplementation.fromJSONObject(new ExtendedJSONObject(data));
} catch (Exception e) {
// Fall through to generating a new key pair.
}
BrowserIDKeyPair keyPair = generateNewAssertionKeyPair();
accountManager.setUserData(account, ACCOUNT_KEY_ASSERTION_KEY_PAIR, keyPair.toJSONObject().toJSONString());
return keyPair;
}
/**
* 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 = new ExtendedJSONObject();
for (String key : new String[] {
ACCOUNT_KEY_SERVERURI,
ACCOUNT_KEY_SESSION_TOKEN,
ACCOUNT_KEY_KEY_FETCH_TOKEN,
ACCOUNT_KEY_VERIFIED,
ACCOUNT_KEY_KA,
ACCOUNT_KEY_KB,
ACCOUNT_KEY_UNWRAPKB,
ACCOUNT_KEY_ASSERTION_KEY_PAIR }) {
o.put(key, accountManager.getUserData(account, key));
}
return o;
}
public static Account addAndroidAccount(Context context, String email, String password,
String serverURI, byte[] sessionToken, byte[] keyFetchToken, boolean verified)
throws UnsupportedEncodingException, GeneralSecurityException {
if (email == null) {
throw new IllegalArgumentException("email must not be null");
}
if (password == null) {
throw new IllegalArgumentException("password must not be null");
}
if (serverURI == null) {
throw new IllegalArgumentException("serverURI must not be null");
}
// sessionToken and keyFetchToken are allowed to be null; they can be
// fetched via /account/login from the password. These tokens are generated
// by the server and we have no length or formatting guarantees. However, if
// one is given, both should be given: they come from the server together.
if ((sessionToken == null && keyFetchToken != null) ||
(sessionToken != null && keyFetchToken == null)) {
throw new IllegalArgumentException("none or both of sessionToken and keyFetchToken may be null");
}
byte[] emailUTF8 = email.getBytes("UTF-8");
byte[] passwordUTF8 = password.getBytes("UTF-8");
byte[] quickStretchedPW = FxAccountUtils.generateQuickStretchedPW(emailUTF8, passwordUTF8);
byte[] unwrapBkey = FxAccountUtils.generateUnwrapBKey(quickStretchedPW);
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_VERIFIED, Boolean.valueOf(verified).toString());
userdata.putString(AndroidFxAccount.ACCOUNT_KEY_UNWRAPKB, Utils.byte2Hex(unwrapBkey));
Account account = new Account(email, FxAccountConstants.ACCOUNT_TYPE);
AccountManager accountManager = AccountManager.get(context);
boolean added = accountManager.addAccountExplicitly(account, Utils.byte2Hex(quickStretchedPW), userdata);
if (!added) {
return null;
}
FxAccountAuthenticator.enableSyncing(context, account);
return account;
}
}

View File

@ -8,8 +8,6 @@ import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.activities.FxAccountSetupActivity;
import org.mozilla.gecko.fxa.sync.FxAccount;
import org.mozilla.gecko.sync.Utils;
import android.accounts.AbstractAccountAuthenticator;
import android.accounts.Account;
@ -137,24 +135,4 @@ public class FxAccountAuthenticator extends AbstractAccountAuthenticator {
return null;
}
/**
* Extract an FxAccount from an Android Account object.
*
* @param context to use for AccountManager.
* @param account to extract FxAccount from.
* @return FxAccount instance.
*/
public static FxAccount fromAndroidAccount(Context context, Account account) {
AccountManager accountManager = AccountManager.get(context);
final byte[] sessionTokenBytes = Utils.hex2Byte(accountManager.getUserData(account, JSON_KEY_SESSION_TOKEN));
final byte[] kA = Utils.hex2Byte(accountManager.getUserData(account, JSON_KEY_KA), 16);
final byte[] kB = Utils.hex2Byte(accountManager.getUserData(account, JSON_KEY_KB), 16);
final String idpEndpoint = accountManager.getUserData(account, JSON_KEY_IDP_ENDPOINT);
final String authEndpoint = accountManager.getUserData(account, JSON_KEY_AUTH_ENDPOINT);
return new FxAccount(account.name, sessionTokenBytes, kA, kB, idpEndpoint, authEndpoint);
}
}

View File

@ -0,0 +1,26 @@
/* 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

@ -0,0 +1,33 @@
/* 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

@ -0,0 +1,312 @@
/* 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.GeneralSecurityException;
import java.util.LinkedList;
import java.util.concurrent.Executor;
import org.mozilla.gecko.background.common.log.Logger;
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.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.authenticator.FxAccountLoginException.FxAccountLoginAccountNotVerifiedException;
import org.mozilla.gecko.fxa.authenticator.FxAccountLoginException.FxAccountLoginBadPasswordException;
import org.mozilla.gecko.sync.HTTPFailureException;
import org.mozilla.gecko.sync.net.SyncStorageResponse;
import android.content.Context;
import ch.boye.httpclientandroidlib.HttpResponse;
public class FxAccountLoginPolicy {
public static final String LOG_TAG = FxAccountLoginPolicy.class.getSimpleName();
public final Context context;
public final AbstractFxAccount fxAccount;
public final Executor executor;
public FxAccountLoginPolicy(Context context, AbstractFxAccount fxAccount, Executor executor) {
this.context = context;
this.fxAccount = fxAccount;
this.executor = executor;
}
protected void invokeHandleHardFailure(final FxAccountLoginDelegate delegate, final FxAccountLoginException e) {
executor.execute(new Runnable() {
@Override
public void run() {
delegate.handleError(e);
}
});
}
/**
* Do as much of a Firefox Account login dance as possible.
* <p>
* To avoid deeply nested callbacks, we maintain a simple queue of stages to
* execute in sequence.
*
* @param audience to generate assertion for.
* @param delegate providing callbacks to invoke.
*/
public void login(final String audience, final FxAccountLoginDelegate delegate) {
final LinkedList<LoginStage> stages = new LinkedList<LoginStage>();
stages.add(new CheckPreconditionsLoginStage());
stages.add(new CheckVerifiedLoginStage());
stages.add(new EnsureDerivedKeysLoginStage());
stages.add(new FetchCertificateLoginStage());
advance(audience, stages, delegate);
}
protected interface LoginStageDelegate {
public String getAssertionAudience();
public void handleError(FxAccountLoginException e);
public void handleStageSuccess();
public void handleLoginSuccess(String assertion);
}
protected interface LoginStage {
public void execute(LoginStageDelegate delegate);
}
/**
* Pop the next stage off <code>stages</code> and execute it.
* <p>
* This trades stack efficiency for implementation simplicity.
*
* @param delegate
* @param stages
*/
protected void advance(final String audience, final LinkedList<LoginStage> 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?"));
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();
}
}
/**
* 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 {
@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;
}
String serverURI = fxAccount.getServerURI();
byte[] sessionToken = fxAccount.getSessionToken();
final FxAccountClient20 client = new FxAccountClient20(serverURI, executor);
client.status(sessionToken, new RequestDelegate<StatusResponse>() {
@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;
}
delegate.handleError(new FxAccountLoginBadPasswordException("Auth server rejected session token while fetching status."));
}
@Override
public void handleSuccess(StatusResponse result) {
// We're not yet verified. We can't go forward yet.
if (!result.verified) {
delegate.handleError(new FxAccountLoginAccountNotVerifiedException("Account is not yet verified."));
return;
}
// We've transitioned to verified state. Make a note of it, and continue past go.
fxAccount.setVerified();
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 EnsureDerivedKeysLoginStage 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.");
delegate.handleStageSuccess();
return;
}
byte[] keyFetchToken = fxAccount.getKeyFetchToken();
if (keyFetchToken == null) {
// XXX this might mean something else?
delegate.handleError(new FxAccountLoginBadPasswordException("Account has no key fetch token."));
return;
}
String serverURI = fxAccount.getServerURI();
final FxAccountClient20 client = new FxAccountClient20(serverURI, executor);
client.keys(keyFetchToken, new RequestDelegate<FxAccountClient10.TwoKeys>() {
@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;
}
delegate.handleError(new FxAccountLoginBadPasswordException("Auth server rejected key token while fetching keys."));
}
@Override
public void handleSuccess(TwoKeys result) {
fxAccount.setKa(result.kA);
fxAccount.setWrappedKb(result.wrapkB);
delegate.handleStageSuccess();
}
});
}
}
public class FetchCertificateLoginStage 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;
}
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<String>() {
@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;
}
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;
}
}
});
}
}
}

View File

@ -10,7 +10,7 @@ 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.FxAccountClient10;
import org.mozilla.gecko.browserid.BrowserIDKeyPair;
import org.mozilla.gecko.browserid.JSONWebTokenUtils;
import org.mozilla.gecko.fxa.authenticator.FxAccountAuthenticator;
@ -59,7 +59,7 @@ public class FxAccount {
this.executor = Executors.newSingleThreadExecutor();
}
protected static class InnerFxAccountClientRequestDelegate implements FxAccountClient.RequestDelegate<String> {
protected static class InnerFxAccountClientRequestDelegate implements FxAccountClient10.RequestDelegate<String> {
protected final Executor executor;
protected final String audience;
protected final String tokenServerEndpoint;
@ -167,7 +167,7 @@ public class FxAccount {
// the delegates, we can guarantee that there is no dead-lock between the
// inner FxAccountClient delegate, the outer TokenServerClient delegate, and
// the user supplied delegate.
FxAccountClient fxAccountClient = new FxAccountClient(idpEndpoint, executor);
FxAccountClient10 fxAccountClient = new FxAccountClient10(idpEndpoint, executor);
fxAccountClient.sign(sessionTokenBytes, publicKeyObject,
JSONWebTokenUtils.DEFAULT_CERTIFICATE_DURATION_IN_MILLISECONDS,
new InnerFxAccountClientRequestDelegate(executor, authEndpoint, tokenServerEndpoint, keyPair, delegate));

View File

@ -5,23 +5,33 @@
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.BrowserIDKeyPair;
import org.mozilla.gecko.browserid.RSACryptoImplementation;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.authenticator.FxAccountAuthenticator;
import org.mozilla.gecko.fxa.authenticator.FxAccountLoginDelegate;
import org.mozilla.gecko.fxa.authenticator.FxAccountLoginException;
import org.mozilla.gecko.fxa.authenticator.FxAccountLoginPolicy;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.GlobalSession;
import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate;
import org.mozilla.gecko.sync.crypto.KeyBundle;
import org.mozilla.gecko.sync.delegates.BaseGlobalSessionCallback;
import org.mozilla.gecko.sync.delegates.ClientsDataDelegate;
import org.mozilla.gecko.sync.net.AuthHeaderProvider;
import org.mozilla.gecko.sync.net.HawkAuthHeaderProvider;
import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
import org.mozilla.gecko.tokenserver.TokenServerClient;
import org.mozilla.gecko.tokenserver.TokenServerClientDelegate;
import org.mozilla.gecko.tokenserver.TokenServerException;
import org.mozilla.gecko.tokenserver.TokenServerToken;
import android.accounts.Account;
import android.content.AbstractThreadedSyncAdapter;
@ -113,47 +123,71 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter {
final CountDownLatch latch = new CountDownLatch(1);
try {
final BrowserIDKeyPair keyPair = RSACryptoImplementation.generateKeyPair(1024);
Logger.info(LOG_TAG, "Generated keypair. ");
final String authEndpoint = FxAccountConstants.DEFAULT_AUTH_ENDPOINT;
final String tokenServerEndpoint = authEndpoint + (authEndpoint.endsWith("/") ? "" : "/") + "1.0/sync/1.1";
final URI tokenServerEndpointURI = new URI(tokenServerEndpoint);
final FxAccount fxAccount = FxAccountAuthenticator.fromAndroidAccount(getContext(), account);
final String tokenServerEndpoint = fxAccount.authEndpoint + (fxAccount.authEndpoint.endsWith("/") ? "" : "/") + "1.0/sync/1.1";
final AndroidFxAccount fxAccount = new AndroidFxAccount(getContext(), account);
fxAccount.login(getContext(), tokenServerEndpoint, keyPair, new FxAccount.Delegate() {
if (Logger.LOG_PERSONAL_INFORMATION) {
ExtendedJSONObject o = new AndroidFxAccount(getContext(), account).toJSONObject();
ArrayList<String> list = new ArrayList<String>(o.keySet());
Collections.sort(list);
for (String key : list) {
Logger.pii(LOG_TAG, key + ": " + o.getString(key));
}
}
final FxAccountLoginPolicy loginPolicy = new FxAccountLoginPolicy(getContext(), fxAccount, executor);
loginPolicy.login(authEndpoint, new FxAccountLoginDelegate() {
@Override
public void handleSuccess(final String uid, final String endpoint, final AuthHeaderProvider authHeaderProvider) {
Logger.pii(LOG_TAG, "Got token! uid is " + uid + " and endpoint is " + endpoint + ".");
final BaseGlobalSessionCallback callback = new SessionCallback(latch);
Executors.newSingleThreadExecutor().execute(new Runnable() {
public void handleSuccess(final String assertion) {
TokenServerClient tokenServerclient = new TokenServerClient(tokenServerEndpointURI, executor);
tokenServerclient.getTokenFromBrowserIDAssertion(assertion, true, new TokenServerClientDelegate() {
@Override
public void run() {
public void handleSuccess(final TokenServerToken token) {
Logger.pii(LOG_TAG, "Got token! uid is " + token.uid + " and endpoint is " + token.endpoint + ".");
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.kB);
globalSession = new FxAccountGlobalSession(endpoint, uid, authHeaderProvider, FxAccountConstants.PREFS_PATH, syncKeyBundle, callback, getContext(), extras, clientsDataDelegate);
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);
globalSession = new FxAccountGlobalSession(token.endpoint, token.uid, authHeaderProvider, FxAccountConstants.PREFS_PATH, syncKeyBundle, callback, getContext(), extras, clientsDataDelegate);
globalSession.start();
} catch (Exception e) {
callback.handleError(globalSession, e);
return;
}
}
@Override
public void handleFailure(TokenServerException e) {
handleError(e);
}
@Override
public void handleError(Exception e) {
Logger.error(LOG_TAG, "Failed to get token.", e);
latch.countDown();
}
});
}
@Override
public void handleError(Exception e) {
Logger.info(LOG_TAG, "Failed to get token.", e);
public void handleError(FxAccountLoginException e) {
Logger.error(LOG_TAG, "Got error logging in.", e);
latch.countDown();
}
});
latch.await();
} catch (Exception e) {
Logger.error(LOG_TAG, "Got error logging in.", e);
Logger.error(LOG_TAG, "Got error syncing.", e);
latch.countDown();
}
}
}

View File

@ -239,6 +239,7 @@ public class CommandProcessor {
}
}
@SuppressWarnings("deprecation")
public static void displayURI(final List<String> args, final Context context) {
// We trust the client sender that these exist.
final String uri = args.get(0);

View File

@ -30,6 +30,7 @@ import org.mozilla.apache.commons.codec.binary.Base64;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.sync.setup.Constants;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
@ -139,6 +140,7 @@ public class Utils {
return Base64.decodeBase64(base64.getBytes("UTF-8"));
}
@SuppressLint("DefaultLocale")
public static byte[] decodeFriendlyBase32(String base32) {
Base32 converter = new Base32();
final String translated = base32.replace('8', 'l').replace('9', 'o');
@ -316,6 +318,7 @@ public class Utils {
* Takes a URI, extracting URI components.
* @param scheme the URI scheme on which to match.
*/
@SuppressWarnings("deprecation")
public static Map<String, String> extractURIComponents(String scheme, String uri) {
if (uri.indexOf(scheme) != 0) {
throw new IllegalArgumentException("URI scheme does not match: " + scheme);
@ -347,7 +350,7 @@ public class Utils {
}
// Because TextUtils.join is not stubbed.
public static String toDelimitedString(String delimiter, Collection<String> items) {
public static String toDelimitedString(String delimiter, Collection<? extends Object> items) {
if (items == null || items.size() == 0) {
return "";
}
@ -355,8 +358,8 @@ public class Utils {
StringBuilder sb = new StringBuilder();
int i = 0;
int c = items.size();
for (String string : items) {
sb.append(string);
for (Object object : items) {
sb.append(object.toString());
if (++i < c) {
sb.append(delimiter);
}
@ -364,7 +367,7 @@ public class Utils {
return sb.toString();
}
public static String toCommaSeparatedString(Collection<String> items) {
public static String toCommaSeparatedString(Collection<? extends Object> items) {
return toDelimitedString(", ", items);
}

View File

@ -7,7 +7,6 @@ import org.mozilla.gecko.background.sync.helpers.HistoryHelpers;
import org.mozilla.gecko.sync.repositories.NullCursorException;
import org.mozilla.gecko.sync.repositories.android.AndroidBrowserHistoryDataExtender;
import org.mozilla.gecko.sync.repositories.android.ClientsDatabase;
import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor;
import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
import org.mozilla.gecko.sync.repositories.domain.HistoryRecord;
import org.mozilla.gecko.sync.setup.Constants;

View File

@ -9,7 +9,6 @@ import org.json.simple.JSONArray;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.repositories.NullCursorException;
import org.mozilla.gecko.sync.repositories.android.ClientsDatabase;
import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor;
import org.mozilla.gecko.sync.repositories.android.RepoUtils;
import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
import org.mozilla.gecko.sync.setup.Constants;
@ -122,6 +121,7 @@ public class TestClientsDatabase extends AndroidTestCase {
}
}
@SuppressWarnings("resource")
public void testDelete() {
ClientRecord record1 = new ClientRecord();
ClientRecord record2 = new ClientRecord();
@ -161,6 +161,7 @@ public class TestClientsDatabase extends AndroidTestCase {
}
}
@SuppressWarnings("resource")
public void testWipe() {
ClientRecord record1 = new ClientRecord();
ClientRecord record2 = new ClientRecord();