Bug 959915 - Adapt FxA token server client to include X-Client-State header. r=nalexander
authorRichard Newman <rnewman@mozilla.com>
Wed, 29 Jan 2014 17:08:39 -0800
changeset 182089 ced212be523f883abd9261b88a7e2eb5e28a1a98
parent 182088 5c97f95f439a61cba24f6fe585c8cad386873bc2
child 182090 3f021e06a878823693d2ede45804bca1bab92e1a
push id3343
push userffxbld
push dateMon, 17 Mar 2014 21:55:32 +0000
treeherdermozilla-beta@2f7d3415f79f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnalexander
bugs959915
milestone29.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 959915 - Adapt FxA token server client to include X-Client-State header. r=nalexander
mobile/android/base/background/fxa/FxAccountUtils.java
mobile/android/base/fxa/login/Married.java
mobile/android/base/fxa/sync/FxAccountSyncAdapter.java
mobile/android/base/tokenserver/TokenServerClient.java
--- a/mobile/android/base/background/fxa/FxAccountUtils.java
+++ b/mobile/android/base/background/fxa/FxAccountUtils.java
@@ -134,9 +134,28 @@ public class FxAccountUtils {
       throw new IllegalArgumentException("unwrapkB and wrapkB must be " + CRYPTO_KEY_LENGTH_BYTES + " bytes long");
     }
     byte[] kB = new byte[CRYPTO_KEY_LENGTH_BYTES];
     for (int i = 0; i < wrapkB.length; i++) {
       kB[i] = (byte) (wrapkB[i] ^ unwrapkB[i]);
     }
     return kB;
   }
+
+  /**
+   * The token server accepts an X-Client-State header, which is the
+   * lowercase-hex-encoded first 16 bytes of the SHA-256 hash of the
+   * bytes of kB.
+   * @param kB a byte array, expected to be 32 bytes long.
+   * @return a 32-character string.
+   * @throws NoSuchAlgorithmException
+   */
+  public static String computeClientState(byte[] kB) throws NoSuchAlgorithmException {
+    if (kB == null ||
+        kB.length != 32) {
+      throw new IllegalArgumentException("Unexpected kB.");
+    }
+    byte[] sha256 = Utils.sha256(kB);
+    byte[] truncated = new byte[16];
+    System.arraycopy(sha256, 0, truncated, 0, 16);
+    return Utils.byte2Hex(truncated);    // This is automatically lowercase.
+  }
 }
--- a/mobile/android/base/fxa/login/Married.java
+++ b/mobile/android/base/fxa/login/Married.java
@@ -24,21 +24,28 @@ import org.mozilla.gecko.sync.ExtendedJS
 import org.mozilla.gecko.sync.NonObjectJSONException;
 import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
 
 public class Married extends TokensAndKeysState {
   private static final String LOG_TAG = Married.class.getSimpleName();
 
   protected final String certificate;
+  protected final String clientState;
 
   public Married(String email, String uid, byte[] sessionToken, byte[] kA, byte[] kB, BrowserIDKeyPair keyPair, String certificate) {
     super(StateLabel.Married, email, uid, sessionToken, kA, kB, keyPair);
     Utils.throwIfNull(certificate);
     this.certificate = certificate;
+    try {
+      this.clientState = FxAccountUtils.computeClientState(kB);
+    } catch (NoSuchAlgorithmException e) {
+      // This should never occur.
+      throw new IllegalStateException("Unable to compute client state from kB.");
+    }
   }
 
   @Override
   public ExtendedJSONObject toJSONObject() {
     ExtendedJSONObject o = super.toJSONObject();
     // Fields are non-null by constructor.
     o.put("certificate", certificate);
     return o;
@@ -91,12 +98,19 @@ public class Married extends TokensAndKe
     return assertion;
   }
 
   public KeyBundle getSyncKeyBundle() throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException {
     // TODO Document this choice for deriving from kB.
     return FxAccountUtils.generateSyncKeyBundle(kB);
   }
 
+  public String getClientState() {
+    if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
+      FxAccountConstants.pii(LOG_TAG, "Client state: " + this.clientState);
+    }
+    return this.clientState;
+  }
+
   public State makeCohabitingState() {
     return new Cohabiting(email, uid, sessionToken, kA, kB, keyPair);
   }
 }
--- a/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java
+++ b/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java
@@ -211,19 +211,26 @@ public class FxAccountSyncAdapter extend
 
     @Override
     public void handleAborted(GlobalSession globalSession, String reason) {
       Logger.warn(LOG_TAG, "Global session aborted: " + reason);
       syncDelegate.handleError(null);
     }
   };
 
-  protected void syncWithAssertion(final String audience, final String assertion, URI tokenServerEndpointURI, final String prefsPath, final SharedPreferences sharedPrefs, final KeyBundle syncKeyBundle, final BaseGlobalSessionCallback callback) {
+  protected void syncWithAssertion(final String audience,
+                                   final String assertion,
+                                   URI tokenServerEndpointURI,
+                                   final String prefsPath,
+                                   final SharedPreferences sharedPrefs,
+                                   final KeyBundle syncKeyBundle,
+                                   final String clientState,
+                                   final BaseGlobalSessionCallback callback) {
     TokenServerClient tokenServerclient = new TokenServerClient(tokenServerEndpointURI, executor);
-    tokenServerclient.getTokenFromBrowserIDAssertion(assertion, true, new TokenServerClientDelegate() {
+    tokenServerclient.getTokenFromBrowserIDAssertion(assertion, true, clientState, new TokenServerClientDelegate() {
       @Override
       public void handleSuccess(final TokenServerToken token) {
         FxAccountConstants.pii(LOG_TAG, "Got token! uid is " + token.uid + " and endpoint is " + token.endpoint + ".");
 
         FxAccountGlobalSession globalSession = null;
         try {
           ClientsDataDelegate clientsDataDelegate = new SharedPreferencesClientsDataDelegate(sharedPrefs);
 
@@ -377,17 +384,17 @@ public class FxAccountSyncAdapter extend
 
             Married married = (Married) state;
             final long now = System.currentTimeMillis();
             SkewHandler skewHandler = SkewHandler.getSkewHandlerFromEndpointString(tokenServerEndpoint);
             String assertion = married.generateAssertion(audience, JSONWebTokenUtils.DEFAULT_ASSERTION_ISSUER,
                 now + skewHandler.getSkewInMillis(),
                 this.getAssertionDurationInMilliseconds());
             final BaseGlobalSessionCallback sessionCallback = new SessionCallback(syncDelegate);
-            syncWithAssertion(audience, assertion, tokenServerEndpointURI, prefsPath, sharedPrefs, married.getSyncKeyBundle(), sessionCallback);
+            syncWithAssertion(audience, assertion, tokenServerEndpointURI, prefsPath, sharedPrefs, married.getSyncKeyBundle(), married.getClientState(), sessionCallback);
           } catch (Exception e) {
             syncDelegate.handleError(e);
             return;
           }
         }
       });
 
       latch.await();
--- a/mobile/android/base/tokenserver/TokenServerClient.java
+++ b/mobile/android/base/tokenserver/TokenServerClient.java
@@ -24,16 +24,17 @@ import org.mozilla.gecko.sync.net.BaseRe
 import org.mozilla.gecko.sync.net.BrowserIDAuthHeaderProvider;
 import org.mozilla.gecko.sync.net.SyncResponse;
 import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerConditionsRequiredException;
 import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerInvalidCredentialsException;
 import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerMalformedRequestException;
 import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerMalformedResponseException;
 import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerUnknownServiceException;
 
+import ch.boye.httpclientandroidlib.HttpHeaders;
 import ch.boye.httpclientandroidlib.HttpResponse;
 import ch.boye.httpclientandroidlib.client.ClientProtocolException;
 import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
 import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
 import ch.boye.httpclientandroidlib.message.BasicHeader;
 
 /**
  * HTTP client for interacting with the Mozilla Services Token Server API v1.0,
@@ -52,16 +53,19 @@ public class TokenServerClient {
   public static final String JSON_KEY_API_ENDPOINT = "api_endpoint";
   public static final String JSON_KEY_CONDITION_URLS = "condition_urls";
   public static final String JSON_KEY_DURATION = "duration";
   public static final String JSON_KEY_ERRORS = "errors";
   public static final String JSON_KEY_ID = "id";
   public static final String JSON_KEY_KEY = "key";
   public static final String JSON_KEY_UID = "uid";
 
+  public static final String HEADER_CONDITIONS_ACCEPTED = "X-Conditions-Accepted";
+  public static final String HEADER_CLIENT_STATE = "X-Client-State";
+
   protected final Executor executor;
   protected final URI uri;
 
   public TokenServerClient(URI uri, Executor executor) {
     if (uri == null) {
       throw new IllegalArgumentException("uri must not be null");
     }
     if (executor == null) {
@@ -197,59 +201,86 @@ public class TokenServerClient {
     Logger.debug(LOG_TAG, "Successful token response: " + result.getString(JSON_KEY_ID));
 
     return new TokenServerToken(result.getString(JSON_KEY_ID),
         result.getString(JSON_KEY_KEY),
         result.get(JSON_KEY_UID).toString(),
         result.getString(JSON_KEY_API_ENDPOINT));
   }
 
-  public void getTokenFromBrowserIDAssertion(final String assertion, final boolean conditionsAccepted,
-      final TokenServerClientDelegate delegate) {
-    final BaseResource r = new BaseResource(uri);
+  public static class TokenFetchResourceDelegate extends BaseResourceDelegate {
+    private final TokenServerClient         client;
+    private final TokenServerClientDelegate delegate;
+    private final String                    assertion;
+    private final String                    clientState;
+    private final BaseResource              resource;
+    private final boolean                   conditionsAccepted;
 
-    r.delegate = new BaseResourceDelegate(r) {
-      @Override
-      public void handleHttpResponse(HttpResponse response) {
-        SkewHandler skewHandler = SkewHandler.getSkewHandlerForResource(r);
-        skewHandler.updateSkew(response, System.currentTimeMillis());
-        try {
-          TokenServerToken token = processResponse(response);
-          invokeHandleSuccess(delegate, token);
-        } catch (TokenServerException e) {
-          invokeHandleFailure(delegate, e);
-        }
+    public TokenFetchResourceDelegate(TokenServerClient client,
+                                      BaseResource resource,
+                                      TokenServerClientDelegate delegate,
+                                      String assertion, String clientState,
+                                      boolean conditionsAccepted) {
+      super(resource);
+      this.client = client;
+      this.delegate = delegate;
+      this.assertion = assertion;
+      this.clientState = clientState;
+      this.resource = resource;
+      this.conditionsAccepted = conditionsAccepted;
+    }
+
+    @Override
+    public void handleHttpResponse(HttpResponse response) {
+      SkewHandler skewHandler = SkewHandler.getSkewHandlerForResource(resource);
+      skewHandler.updateSkew(response, System.currentTimeMillis());
+      try {
+        TokenServerToken token = client.processResponse(response);
+        client.invokeHandleSuccess(delegate, token);
+      } catch (TokenServerException e) {
+        client.invokeHandleFailure(delegate, e);
       }
-
-      @Override
-      public void handleTransportException(GeneralSecurityException e) {
-        invokeHandleError(delegate, e);
-      }
+    }
 
-      @Override
-      public void handleHttpProtocolException(ClientProtocolException e) {
-        invokeHandleError(delegate, e);
-      }
+    @Override
+    public void handleTransportException(GeneralSecurityException e) {
+      client.invokeHandleError(delegate, e);
+    }
+
+    @Override
+    public void handleHttpProtocolException(ClientProtocolException e) {
+      client.invokeHandleError(delegate, e);
+    }
 
-      @Override
-      public void handleHttpIOException(IOException e) {
-        invokeHandleError(delegate, e);
-      }
+    @Override
+    public void handleHttpIOException(IOException e) {
+      client.invokeHandleError(delegate, e);
+    }
 
-      @Override
-      public AuthHeaderProvider getAuthHeaderProvider() {
-        return new BrowserIDAuthHeaderProvider(assertion);
+    @Override
+    public AuthHeaderProvider getAuthHeaderProvider() {
+      return new BrowserIDAuthHeaderProvider(assertion);
+    }
+
+    @Override
+    public void addHeaders(HttpRequestBase request, DefaultHttpClient client) {
+      String host = request.getURI().getHost();
+      request.setHeader(new BasicHeader(HttpHeaders.HOST, host));
+      if (clientState != null) {
+        request.setHeader(new BasicHeader(HEADER_CLIENT_STATE, clientState));
       }
-
-      @Override
-      public void addHeaders(HttpRequestBase request, DefaultHttpClient client) {
-        String host = request.getURI().getHost();
-        request.setHeader(new BasicHeader("Host", host));
+      if (conditionsAccepted) {
+        request.addHeader(HEADER_CONDITIONS_ACCEPTED, "1");
+      }
+    }
+  }
 
-        if (conditionsAccepted) {
-          request.addHeader("X-Conditions-Accepted", "1");
-        }
-      }
-    };
-
-    r.get();
+  public void getTokenFromBrowserIDAssertion(final String assertion,
+                                             final boolean conditionsAccepted,
+                                             final String clientState,
+                                             final TokenServerClientDelegate delegate) {
+    final BaseResource resource = new BaseResource(this.uri);
+    resource.delegate = new TokenFetchResourceDelegate(this, resource, delegate,
+                                                       assertion, clientState,
+                                                       conditionsAccepted);
+    resource.get();
   }
 }