mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 956844 - Part 1: update Android FxSyncAdapter to use new onepw protocol login flow. r=rnewman
This commit is contained in:
parent
43b73d2e64
commit
8d080742f8
@ -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',
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
|
642
mobile/android/base/background/fxa/FxAccountClient10.java
Normal file
642
mobile/android/base/background/fxa/FxAccountClient10.java
Normal 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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
80
mobile/android/base/fxa/authenticator/AbstractFxAccount.java
Normal file
80
mobile/android/base/fxa/authenticator/AbstractFxAccount.java
Normal 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;
|
||||
}
|
225
mobile/android/base/fxa/authenticator/AndroidFxAccount.java
Normal file
225
mobile/android/base/fxa/authenticator/AndroidFxAccount.java
Normal 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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
312
mobile/android/base/fxa/authenticator/FxAccountLoginPolicy.java
Normal file
312
mobile/android/base/fxa/authenticator/FxAccountLoginPolicy.java
Normal 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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
|
@ -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 + ".");
|
||||
public void handleSuccess(final String assertion) {
|
||||
TokenServerClient tokenServerclient = new TokenServerClient(tokenServerEndpointURI, executor);
|
||||
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 + ".");
|
||||
|
||||
final BaseGlobalSessionCallback callback = new SessionCallback(latch);
|
||||
|
||||
Executors.newSingleThreadExecutor().execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
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.info(LOG_TAG, "Failed to get token.", e);
|
||||
Logger.error(LOG_TAG, "Failed to get token.", e);
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
Loading…
Reference in New Issue
Block a user