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

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

Correct previous dual key logic pending first delivery installment.

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

mercurial