Wed, 31 Dec 2014 07:22:50 +0100
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 | } |