Wed, 31 Dec 2014 07:22:50 +0100
Correct previous dual key logic pending first delivery installment.
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 }