mobile/android/base/background/fxa/FxAccountClient10.java

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 package org.mozilla.gecko.background.fxa;
     7 import java.io.IOException;
     8 import java.io.UnsupportedEncodingException;
     9 import java.net.URI;
    10 import java.net.URISyntaxException;
    11 import java.security.GeneralSecurityException;
    12 import java.security.InvalidKeyException;
    13 import java.security.NoSuchAlgorithmException;
    14 import java.util.Arrays;
    15 import java.util.Locale;
    16 import java.util.concurrent.Executor;
    18 import javax.crypto.Mac;
    20 import org.json.simple.JSONObject;
    21 import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientMalformedResponseException;
    22 import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
    23 import org.mozilla.gecko.fxa.FxAccountConstants;
    24 import org.mozilla.gecko.sync.ExtendedJSONObject;
    25 import org.mozilla.gecko.sync.Utils;
    26 import org.mozilla.gecko.sync.crypto.HKDF;
    27 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
    28 import org.mozilla.gecko.sync.net.BaseResource;
    29 import org.mozilla.gecko.sync.net.BaseResourceDelegate;
    30 import org.mozilla.gecko.sync.net.HawkAuthHeaderProvider;
    31 import org.mozilla.gecko.sync.net.Resource;
    32 import org.mozilla.gecko.sync.net.SyncResponse;
    33 import org.mozilla.gecko.sync.net.SyncStorageResponse;
    35 import ch.boye.httpclientandroidlib.HttpEntity;
    36 import ch.boye.httpclientandroidlib.HttpHeaders;
    37 import ch.boye.httpclientandroidlib.HttpResponse;
    38 import ch.boye.httpclientandroidlib.client.ClientProtocolException;
    39 import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
    40 import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
    42 /**
    43  * An HTTP client for talking to an FxAccount server.
    44  * <p>
    45  * The reference server is developed at
    46  * <a href="https://github.com/mozilla/picl-idp">https://github.com/mozilla/picl-idp</a>.
    47  * This implementation was developed against
    48  * <a href="https://github.com/mozilla/picl-idp/commit/c7a02a0cbbb43f332058dc060bd84a21e56ec208">https://github.com/mozilla/picl-idp/commit/c7a02a0cbbb43f332058dc060bd84a21e56ec208</a>.
    49  * <p>
    50  * The delegate structure used is a little different from the rest of the code
    51  * base. We add a <code>RequestDelegate</code> layer that processes a typed
    52  * value extracted from the body of a successful response.
    53  * <p>
    54  * Further, we add internal <code>CreateDelegate</code> and
    55  * <code>AuthDelegate</code> delegates to make it easier to modify the request
    56  * bodies sent to the /create and /auth endpoints.
    57  */
    58 public class FxAccountClient10 {
    59   protected static final String LOG_TAG = FxAccountClient10.class.getSimpleName();
    61   protected static final String ACCEPT_HEADER = "application/json;charset=utf-8";
    63   public static final String JSON_KEY_EMAIL = "email";
    64   public static final String JSON_KEY_KEYFETCHTOKEN = "keyFetchToken";
    65   public static final String JSON_KEY_SESSIONTOKEN = "sessionToken";
    66   public static final String JSON_KEY_UID = "uid";
    67   public static final String JSON_KEY_VERIFIED = "verified";
    68   public static final String JSON_KEY_ERROR = "error";
    69   public static final String JSON_KEY_MESSAGE = "message";
    70   public static final String JSON_KEY_INFO = "info";
    71   public static final String JSON_KEY_CODE = "code";
    72   public static final String JSON_KEY_ERRNO = "errno";
    75   protected static final String[] requiredErrorStringFields = { JSON_KEY_ERROR, JSON_KEY_MESSAGE, JSON_KEY_INFO };
    76   protected static final String[] requiredErrorLongFields = { JSON_KEY_CODE, JSON_KEY_ERRNO };
    78   /**
    79    * The server's URI.
    80    * <p>
    81    * We assume throughout that this ends with a trailing slash (and guarantee as
    82    * much in the constructor).
    83    */
    84   protected final String serverURI;
    86   protected final Executor executor;
    88   public FxAccountClient10(String serverURI, Executor executor) {
    89     if (serverURI == null) {
    90       throw new IllegalArgumentException("Must provide a server URI.");
    91     }
    92     if (executor == null) {
    93       throw new IllegalArgumentException("Must provide a non-null executor.");
    94     }
    95     this.serverURI = serverURI.endsWith("/") ? serverURI : serverURI + "/";
    96     if (!this.serverURI.endsWith("/")) {
    97       throw new IllegalArgumentException("Constructed serverURI must end with a trailing slash: " + this.serverURI);
    98     }
    99     this.executor = executor;
   100   }
   102   /**
   103    * Process a typed value extracted from a successful response (in an
   104    * endpoint-dependent way).
   105    */
   106   public interface RequestDelegate<T> {
   107     public void handleError(Exception e);
   108     public void handleFailure(FxAccountClientRemoteException e);
   109     public void handleSuccess(T result);
   110   }
   112   /**
   113    * A <code>CreateDelegate</code> produces the body of a /create request.
   114    */
   115   public interface CreateDelegate {
   116     public JSONObject getCreateBody() throws FxAccountClientException;
   117   }
   119   /**
   120    * A <code>AuthDelegate</code> produces the bodies of an /auth/{start,finish}
   121    * request pair and exposes state generated by a successful response.
   122    */
   123   public interface AuthDelegate {
   124     public JSONObject getAuthStartBody() throws FxAccountClientException;
   125     public void onAuthStartResponse(ExtendedJSONObject body) throws FxAccountClientException;
   126     public JSONObject getAuthFinishBody() throws FxAccountClientException;
   128     public byte[] getSharedBytes() throws FxAccountClientException;
   129   }
   131   /**
   132    * Thin container for two access tokens.
   133    */
   134   public static class TwoTokens {
   135     public final byte[] keyFetchToken;
   136     public final byte[] sessionToken;
   137     public TwoTokens(byte[] keyFetchToken, byte[] sessionToken) {
   138       this.keyFetchToken = keyFetchToken;
   139       this.sessionToken = sessionToken;
   140     }
   141   }
   143   /**
   144    * Thin container for two cryptographic keys.
   145    */
   146   public static class TwoKeys {
   147     public final byte[] kA;
   148     public final byte[] wrapkB;
   149     public TwoKeys(byte[] kA, byte[] wrapkB) {
   150       this.kA = kA;
   151       this.wrapkB = wrapkB;
   152     }
   153   }
   155   protected <T> void invokeHandleError(final RequestDelegate<T> delegate, final Exception e) {
   156     executor.execute(new Runnable() {
   157       @Override
   158       public void run() {
   159         delegate.handleError(e);
   160       }
   161     });
   162   }
   164   /**
   165    * Translate resource callbacks into request callbacks invoked on the provided
   166    * executor.
   167    * <p>
   168    * Override <code>handleSuccess</code> to parse the body of the resource
   169    * request and call the request callback. <code>handleSuccess</code> is
   170    * invoked via the executor, so you don't need to delegate further.
   171    */
   172   protected abstract class ResourceDelegate<T> extends BaseResourceDelegate {
   173     protected abstract void handleSuccess(final int status, HttpResponse response, final ExtendedJSONObject body);
   175     protected final RequestDelegate<T> delegate;
   177     protected final byte[] tokenId;
   178     protected final byte[] reqHMACKey;
   179     protected final SkewHandler skewHandler;
   181     /**
   182      * Create a delegate for an un-authenticated resource.
   183      */
   184     public ResourceDelegate(final Resource resource, final RequestDelegate<T> delegate) {
   185       this(resource, delegate, null, null);
   186     }
   188     /**
   189      * Create a delegate for a Hawk-authenticated resource.
   190      * <p>
   191      * Every Hawk request that encloses an entity (PATCH, POST, and PUT) will
   192      * include the payload verification hash.
   193      */
   194     public ResourceDelegate(final Resource resource, final RequestDelegate<T> delegate, final byte[] tokenId, final byte[] reqHMACKey) {
   195       super(resource);
   196       this.delegate = delegate;
   197       this.reqHMACKey = reqHMACKey;
   198       this.tokenId = tokenId;
   199       this.skewHandler = SkewHandler.getSkewHandlerForResource(resource);
   200     }
   202     @Override
   203     public AuthHeaderProvider getAuthHeaderProvider() {
   204       if (tokenId != null && reqHMACKey != null) {
   205         // We always include the payload verification hash for FxA Hawk-authenticated requests.
   206         final boolean includePayloadVerificationHash = true;
   207         return new HawkAuthHeaderProvider(Utils.byte2Hex(tokenId), reqHMACKey, includePayloadVerificationHash, skewHandler.getSkewInSeconds());
   208       }
   209       return super.getAuthHeaderProvider();
   210     }
   212     @Override
   213     public String getUserAgent() {
   214       return FxAccountConstants.USER_AGENT;
   215     }
   217     @Override
   218     public void handleHttpResponse(HttpResponse response) {
   219       try {
   220         final int status = validateResponse(response);
   221         skewHandler.updateSkew(response, now());
   222         invokeHandleSuccess(status, response);
   223       } catch (FxAccountClientRemoteException e) {
   224         if (!skewHandler.updateSkew(response, now())) {
   225           // If we couldn't update skew, but we got a failure, let's try clearing the skew.
   226           skewHandler.resetSkew();
   227         }
   228         invokeHandleFailure(e);
   229       }
   230     }
   232     protected void invokeHandleFailure(final FxAccountClientRemoteException e) {
   233       executor.execute(new Runnable() {
   234         @Override
   235         public void run() {
   236           delegate.handleFailure(e);
   237         }
   238       });
   239     }
   241     protected void invokeHandleSuccess(final int status, final HttpResponse response) {
   242       executor.execute(new Runnable() {
   243         @Override
   244         public void run() {
   245           try {
   246             ExtendedJSONObject body = new SyncResponse(response).jsonObjectBody();
   247             ResourceDelegate.this.handleSuccess(status, response, body);
   248           } catch (Exception e) {
   249             delegate.handleError(e);
   250           }
   251         }
   252       });
   253     }
   255     @Override
   256     public void handleHttpProtocolException(final ClientProtocolException e) {
   257       invokeHandleError(delegate, e);
   258     }
   260     @Override
   261     public void handleHttpIOException(IOException e) {
   262       invokeHandleError(delegate, e);
   263     }
   265     @Override
   266     public void handleTransportException(GeneralSecurityException e) {
   267       invokeHandleError(delegate, e);
   268     }
   270     @Override
   271     public void addHeaders(HttpRequestBase request, DefaultHttpClient client) {
   272       super.addHeaders(request, client);
   274       // The basics.
   275       final Locale locale = Locale.getDefault();
   276       request.addHeader(HttpHeaders.ACCEPT_LANGUAGE, Utils.getLanguageTag(locale));
   277       request.addHeader(HttpHeaders.ACCEPT, ACCEPT_HEADER);
   278     }
   279   }
   281   protected <T> void post(BaseResource resource, final JSONObject requestBody, final RequestDelegate<T> delegate) {
   282     try {
   283       if (requestBody == null) {
   284         resource.post((HttpEntity) null);
   285       } else {
   286         resource.post(requestBody);
   287       }
   288     } catch (UnsupportedEncodingException e) {
   289       invokeHandleError(delegate, e);
   290       return;
   291     }
   292   }
   294   @SuppressWarnings("static-method")
   295   public long now() {
   296     return System.currentTimeMillis();
   297   }
   299   /**
   300    * Intepret a response from the auth server.
   301    * <p>
   302    * Throw an appropriate exception on errors; otherwise, return the response's
   303    * status code.
   304    *
   305    * @return response's HTTP status code.
   306    * @throws FxAccountClientException
   307    */
   308   public static int validateResponse(HttpResponse response) throws FxAccountClientRemoteException {
   309     final int status = response.getStatusLine().getStatusCode();
   310     if (status == 200) {
   311       return status;
   312     }
   313     int code;
   314     int errno;
   315     String error;
   316     String message;
   317     String info;
   318     ExtendedJSONObject body;
   319     try {
   320       body = new SyncStorageResponse(response).jsonObjectBody();
   321       body.throwIfFieldsMissingOrMisTyped(requiredErrorStringFields, String.class);
   322       body.throwIfFieldsMissingOrMisTyped(requiredErrorLongFields, Long.class);
   323       code = body.getLong(JSON_KEY_CODE).intValue();
   324       errno = body.getLong(JSON_KEY_ERRNO).intValue();
   325       error = body.getString(JSON_KEY_ERROR);
   326       message = body.getString(JSON_KEY_MESSAGE);
   327       info = body.getString(JSON_KEY_INFO);
   328     } catch (Exception e) {
   329       throw new FxAccountClientMalformedResponseException(response);
   330     }
   331     throw new FxAccountClientRemoteException(response, code, errno, error, message, info, body);
   332   }
   334   public void createAccount(final String email, final byte[] stretchedPWBytes,
   335       final String srpSalt, final String mainSalt,
   336       final RequestDelegate<String> delegate) {
   337     try {
   338       createAccount(new FxAccount10CreateDelegate(email, stretchedPWBytes, srpSalt, mainSalt), delegate);
   339     } catch (final Exception e) {
   340       invokeHandleError(delegate, e);
   341       return;
   342     }
   343   }
   345   protected void createAccount(final CreateDelegate createDelegate, final RequestDelegate<String> delegate) {
   346     JSONObject body = null;
   347     try {
   348       body = createDelegate.getCreateBody();
   349     } catch (FxAccountClientException e) {
   350       invokeHandleError(delegate, e);
   351       return;
   352     }
   354     BaseResource resource;
   355     try {
   356       resource = new BaseResource(new URI(serverURI + "account/create"));
   357     } catch (URISyntaxException e) {
   358       invokeHandleError(delegate, e);
   359       return;
   360     }
   362     resource.delegate = new ResourceDelegate<String>(resource, delegate) {
   363       @Override
   364       public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
   365         String uid = body.getString("uid");
   366         if (uid == null) {
   367           delegate.handleError(new FxAccountClientException("uid must be a non-null string"));
   368           return;
   369         }
   370         delegate.handleSuccess(uid);
   371       }
   372     };
   373     post(resource, body, delegate);
   374   }
   376   protected void authStart(final AuthDelegate authDelegate, final RequestDelegate<AuthDelegate> delegate) {
   377     JSONObject body;
   378     try {
   379       body = authDelegate.getAuthStartBody();
   380     } catch (FxAccountClientException e) {
   381       invokeHandleError(delegate, e);
   382       return;
   383     }
   385     BaseResource resource;
   386     try {
   387       resource = new BaseResource(new URI(serverURI + "auth/start"));
   388     } catch (URISyntaxException e) {
   389       invokeHandleError(delegate, e);
   390       return;
   391     }
   393     resource.delegate = new ResourceDelegate<AuthDelegate>(resource, delegate) {
   394       @Override
   395       public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
   396         try {
   397           authDelegate.onAuthStartResponse(body);
   398           delegate.handleSuccess(authDelegate);
   399         } catch (Exception e) {
   400           delegate.handleError(e);
   401           return;
   402         }
   403       }
   404     };
   405     post(resource, body, delegate);
   406   }
   408   protected void authFinish(final AuthDelegate authDelegate, RequestDelegate<byte[]> delegate) {
   409     JSONObject body;
   410     try {
   411       body = authDelegate.getAuthFinishBody();
   412     } catch (FxAccountClientException e) {
   413       invokeHandleError(delegate, e);
   414       return;
   415     }
   417     BaseResource resource;
   418     try {
   419       resource = new BaseResource(new URI(serverURI + "auth/finish"));
   420     } catch (URISyntaxException e) {
   421       invokeHandleError(delegate, e);
   422       return;
   423     }
   425     resource.delegate = new ResourceDelegate<byte[]>(resource, delegate) {
   426       @Override
   427       public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
   428         try {
   429           byte[] authToken = new byte[32];
   430           unbundleBody(body, authDelegate.getSharedBytes(), FxAccountUtils.KW("auth/finish"), authToken);
   431           delegate.handleSuccess(authToken);
   432         } catch (Exception e) {
   433           delegate.handleError(e);
   434           return;
   435         }
   436       }
   437     };
   438     post(resource, body, delegate);
   439   }
   441   public void login(final String email, final byte[] stretchedPWBytes, final RequestDelegate<byte[]> delegate) {
   442     login(new FxAccount10AuthDelegate(email, stretchedPWBytes), delegate);
   443   }
   445   protected void login(final AuthDelegate authDelegate, final RequestDelegate<byte[]> delegate) {
   446     authStart(authDelegate, new RequestDelegate<AuthDelegate>() {
   447       @Override
   448       public void handleSuccess(AuthDelegate srpSession) {
   449         authFinish(srpSession, delegate);
   450       }
   452       @Override
   453       public void handleError(final Exception e) {
   454         invokeHandleError(delegate, e);
   455         return;
   456       }
   458       @Override
   459       public void handleFailure(final FxAccountClientRemoteException e) {
   460         executor.execute(new Runnable() {
   461           @Override
   462           public void run() {
   463             delegate.handleFailure(e);
   464           }
   465         });
   466       }
   467     });
   468   }
   470   public void sessionCreate(byte[] authToken, final RequestDelegate<TwoTokens> delegate) {
   471     final byte[] tokenId = new byte[32];
   472     final byte[] reqHMACKey = new byte[32];
   473     final byte[] requestKey = new byte[32];
   474     try {
   475       HKDF.deriveMany(authToken, new byte[0], FxAccountUtils.KW("authToken"), tokenId, reqHMACKey, requestKey);
   476     } catch (Exception e) {
   477       invokeHandleError(delegate, e);
   478       return;
   479     }
   481     BaseResource resource;
   482     try {
   483       resource = new BaseResource(new URI(serverURI + "session/create"));
   484     } catch (URISyntaxException e) {
   485       invokeHandleError(delegate, e);
   486       return;
   487     }
   489     resource.delegate = new ResourceDelegate<TwoTokens>(resource, delegate, tokenId, reqHMACKey) {
   490       @Override
   491       public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
   492         try {
   493           byte[] keyFetchToken = new byte[32];
   494           byte[] sessionToken = new byte[32];
   495           unbundleBody(body, requestKey, FxAccountUtils.KW("session/create"), keyFetchToken, sessionToken);
   496           delegate.handleSuccess(new TwoTokens(keyFetchToken, sessionToken));
   497           return;
   498         } catch (Exception e) {
   499           delegate.handleError(e);
   500           return;
   501         }
   502       }
   503     };
   504     post(resource, null, delegate);
   505   }
   507   public void sessionDestroy(byte[] sessionToken, final RequestDelegate<Void> delegate) {
   508     final byte[] tokenId = new byte[32];
   509     final byte[] reqHMACKey = new byte[32];
   510     try {
   511       HKDF.deriveMany(sessionToken, new byte[0], FxAccountUtils.KW("sessionToken"), tokenId, reqHMACKey);
   512     } catch (Exception e) {
   513       invokeHandleError(delegate, e);
   514       return;
   515     }
   517     BaseResource resource;
   518     try {
   519       resource = new BaseResource(new URI(serverURI + "session/destroy"));
   520     } catch (URISyntaxException e) {
   521       invokeHandleError(delegate, e);
   522       return;
   523     }
   525     resource.delegate = new ResourceDelegate<Void>(resource, delegate, tokenId, reqHMACKey) {
   526       @Override
   527       public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
   528         delegate.handleSuccess(null);
   529       }
   530     };
   531     post(resource, null, delegate);
   532   }
   534   /**
   535    * Don't call this directly. Use <code>unbundleBody</code> instead.
   536    */
   537   protected void unbundleBytes(byte[] bundleBytes, byte[] respHMACKey, byte[] respXORKey, byte[]... rest)
   538       throws InvalidKeyException, NoSuchAlgorithmException, FxAccountClientException {
   539     if (bundleBytes.length < 32) {
   540       throw new IllegalArgumentException("input bundle must include HMAC");
   541     }
   542     int len = respXORKey.length;
   543     if (bundleBytes.length != len + 32) {
   544       throw new IllegalArgumentException("input bundle and XOR key with HMAC have different lengths");
   545     }
   546     int left = len;
   547     for (byte[] array : rest) {
   548       left -= array.length;
   549     }
   550     if (left != 0) {
   551       throw new IllegalArgumentException("XOR key and total output arrays have different lengths");
   552     }
   554     byte[] ciphertext = new byte[len];
   555     byte[] HMAC = new byte[32];
   556     System.arraycopy(bundleBytes, 0, ciphertext, 0, len);
   557     System.arraycopy(bundleBytes, len, HMAC, 0, 32);
   559     Mac hmacHasher = HKDF.makeHMACHasher(respHMACKey);
   560     byte[] computedHMAC = hmacHasher.doFinal(ciphertext);
   561     if (!Arrays.equals(computedHMAC, HMAC)) {
   562       throw new FxAccountClientException("Bad message HMAC");
   563     }
   565     int offset = 0;
   566     for (byte[] array : rest) {
   567       for (int i = 0; i < array.length; i++) {
   568         array[i] = (byte) (respXORKey[offset + i] ^ ciphertext[offset + i]);
   569       }
   570       offset += array.length;
   571     }
   572   }
   574   protected void unbundleBody(ExtendedJSONObject body, byte[] requestKey, byte[] ctxInfo, byte[]... rest) throws Exception {
   575     int length = 0;
   576     for (byte[] array : rest) {
   577       length += array.length;
   578     }
   580     if (body == null) {
   581       throw new FxAccountClientException("body must be non-null");
   582     }
   583     String bundle = body.getString("bundle");
   584     if (bundle == null) {
   585       throw new FxAccountClientException("bundle must be a non-null string");
   586     }
   587     byte[] bundleBytes = Utils.hex2Byte(bundle);
   589     final byte[] respHMACKey = new byte[32];
   590     final byte[] respXORKey = new byte[length];
   591     HKDF.deriveMany(requestKey, new byte[0], ctxInfo, respHMACKey, respXORKey);
   592     unbundleBytes(bundleBytes, respHMACKey, respXORKey, rest);
   593   }
   595   public void keys(byte[] keyFetchToken, final RequestDelegate<TwoKeys> delegate) {
   596     final byte[] tokenId = new byte[32];
   597     final byte[] reqHMACKey = new byte[32];
   598     final byte[] requestKey = new byte[32];
   599     try {
   600       HKDF.deriveMany(keyFetchToken, new byte[0], FxAccountUtils.KW("keyFetchToken"), tokenId, reqHMACKey, requestKey);
   601     } catch (Exception e) {
   602       invokeHandleError(delegate, e);
   603       return;
   604     }
   606     BaseResource resource;
   607     try {
   608       resource = new BaseResource(new URI(serverURI + "account/keys"));
   609     } catch (URISyntaxException e) {
   610       invokeHandleError(delegate, e);
   611       return;
   612     }
   614     resource.delegate = new ResourceDelegate<TwoKeys>(resource, delegate, tokenId, reqHMACKey) {
   615       @Override
   616       public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
   617         try {
   618           byte[] kA = new byte[FxAccountUtils.CRYPTO_KEY_LENGTH_BYTES];
   619           byte[] wrapkB = new byte[FxAccountUtils.CRYPTO_KEY_LENGTH_BYTES];
   620           unbundleBody(body, requestKey, FxAccountUtils.KW("account/keys"), kA, wrapkB);
   621           delegate.handleSuccess(new TwoKeys(kA, wrapkB));
   622           return;
   623         } catch (Exception e) {
   624           delegate.handleError(e);
   625           return;
   626         }
   627       }
   628     };
   629     resource.get();
   630   }
   632   /**
   633    * Thin container for status response.
   634    */
   635   public static class StatusResponse {
   636     public final String email;
   637     public final boolean verified;
   638     public StatusResponse(String email, boolean verified) {
   639       this.email = email;
   640       this.verified = verified;
   641     }
   642   }
   644   /**
   645    * Query the status of an account given a valid session token.
   646    * <p>
   647    * This API is a little odd: the auth server returns the email and
   648    * verification state of the account that corresponds to the (opaque) session
   649    * token. It might fail if the session token is unknown (or invalid, or
   650    * revoked).
   651    *
   652    * @param sessionToken
   653    *          to query.
   654    * @param delegate
   655    *          to invoke callbacks.
   656    */
   657   public void status(byte[] sessionToken, final RequestDelegate<StatusResponse> delegate) {
   658     final byte[] tokenId = new byte[32];
   659     final byte[] reqHMACKey = new byte[32];
   660     final byte[] requestKey = new byte[32];
   661     try {
   662       HKDF.deriveMany(sessionToken, new byte[0], FxAccountUtils.KW("sessionToken"), tokenId, reqHMACKey, requestKey);
   663     } catch (Exception e) {
   664       invokeHandleError(delegate, e);
   665       return;
   666     }
   668     BaseResource resource;
   669     try {
   670       resource = new BaseResource(new URI(serverURI + "recovery_email/status"));
   671     } catch (URISyntaxException e) {
   672       invokeHandleError(delegate, e);
   673       return;
   674     }
   676     resource.delegate = new ResourceDelegate<StatusResponse>(resource, delegate, tokenId, reqHMACKey) {
   677       @Override
   678       public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
   679         try {
   680           String[] requiredStringFields = new String[] { JSON_KEY_EMAIL };
   681           body.throwIfFieldsMissingOrMisTyped(requiredStringFields, String.class);
   682           String email = body.getString(JSON_KEY_EMAIL);
   683           Boolean verified = body.getBoolean(JSON_KEY_VERIFIED);
   684           delegate.handleSuccess(new StatusResponse(email, verified));
   685           return;
   686         } catch (Exception e) {
   687           delegate.handleError(e);
   688           return;
   689         }
   690       }
   691     };
   692     resource.get();
   693   }
   695   @SuppressWarnings("unchecked")
   696   public void sign(final byte[] sessionToken, final ExtendedJSONObject publicKey, long durationInMilliseconds, final RequestDelegate<String> delegate) {
   697     final JSONObject body = new JSONObject();
   698     body.put("publicKey", publicKey);
   699     body.put("duration", durationInMilliseconds);
   701     final byte[] tokenId = new byte[32];
   702     final byte[] reqHMACKey = new byte[32];
   703     try {
   704       HKDF.deriveMany(sessionToken, new byte[0], FxAccountUtils.KW("sessionToken"), tokenId, reqHMACKey);
   705     } catch (Exception e) {
   706       invokeHandleError(delegate, e);
   707       return;
   708     }
   710     BaseResource resource;
   711     try {
   712       resource = new BaseResource(new URI(serverURI + "certificate/sign"));
   713     } catch (URISyntaxException e) {
   714       invokeHandleError(delegate, e);
   715       return;
   716     }
   718     resource.delegate = new ResourceDelegate<String>(resource, delegate, tokenId, reqHMACKey) {
   719       @Override
   720       public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
   721         String cert = body.getString("cert");
   722         if (cert == null) {
   723           delegate.handleError(new FxAccountClientException("cert must be a non-null string"));
   724           return;
   725         }
   726         delegate.handleSuccess(cert);
   727       }
   728     };
   729     post(resource, body, delegate);
   730   }
   732   /**
   733    * Request a verification link be sent to the account email, given a valid session token.
   734    *
   735    * @param sessionToken
   736    *          to authenticate with.
   737    * @param delegate
   738    *          to invoke callbacks.
   739    */
   740   public void resendCode(byte[] sessionToken, final RequestDelegate<Void> delegate) {
   741     final byte[] tokenId = new byte[32];
   742     final byte[] reqHMACKey = new byte[32];
   743     final byte[] requestKey = new byte[32];
   744     try {
   745       HKDF.deriveMany(sessionToken, new byte[0], FxAccountUtils.KW("sessionToken"), tokenId, reqHMACKey, requestKey);
   746     } catch (Exception e) {
   747       invokeHandleError(delegate, e);
   748       return;
   749     }
   751     BaseResource resource;
   752     try {
   753       resource = new BaseResource(new URI(serverURI + "recovery_email/resend_code"));
   754     } catch (URISyntaxException e) {
   755       invokeHandleError(delegate, e);
   756       return;
   757     }
   759     resource.delegate = new ResourceDelegate<Void>(resource, delegate, tokenId, reqHMACKey) {
   760       @Override
   761       public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
   762         try {
   763           delegate.handleSuccess(null);
   764           return;
   765         } catch (Exception e) {
   766           delegate.handleError(e);
   767           return;
   768         }
   769       }
   770     };
   771     post(resource, new JSONObject(), delegate);
   772   }
   773 }

mercurial