mobile/android/base/sync/jpake/JPakeClient.java

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

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

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

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.sync.jpake;
michael@0 6
michael@0 7 import java.io.UnsupportedEncodingException;
michael@0 8 import java.math.BigInteger;
michael@0 9 import java.util.LinkedList;
michael@0 10 import java.util.Queue;
michael@0 11
michael@0 12 import org.json.simple.JSONObject;
michael@0 13 import org.mozilla.apache.commons.codec.binary.Base64;
michael@0 14 import org.mozilla.gecko.background.common.log.Logger;
michael@0 15 import org.mozilla.gecko.sync.ExtendedJSONObject;
michael@0 16 import org.mozilla.gecko.sync.ThreadPool;
michael@0 17 import org.mozilla.gecko.sync.Utils;
michael@0 18 import org.mozilla.gecko.sync.crypto.CryptoException;
michael@0 19 import org.mozilla.gecko.sync.crypto.CryptoInfo;
michael@0 20 import org.mozilla.gecko.sync.crypto.KeyBundle;
michael@0 21 import org.mozilla.gecko.sync.crypto.NoKeyBundleException;
michael@0 22 import org.mozilla.gecko.sync.jpake.stage.CompleteStage;
michael@0 23 import org.mozilla.gecko.sync.jpake.stage.ComputeFinalStage;
michael@0 24 import org.mozilla.gecko.sync.jpake.stage.ComputeKeyVerificationStage;
michael@0 25 import org.mozilla.gecko.sync.jpake.stage.ComputeStepOneStage;
michael@0 26 import org.mozilla.gecko.sync.jpake.stage.ComputeStepTwoStage;
michael@0 27 import org.mozilla.gecko.sync.jpake.stage.DecryptDataStage;
michael@0 28 import org.mozilla.gecko.sync.jpake.stage.DeleteChannel;
michael@0 29 import org.mozilla.gecko.sync.jpake.stage.GetChannelStage;
michael@0 30 import org.mozilla.gecko.sync.jpake.stage.GetRequestStage;
michael@0 31 import org.mozilla.gecko.sync.jpake.stage.JPakeStage;
michael@0 32 import org.mozilla.gecko.sync.jpake.stage.PutRequestStage;
michael@0 33 import org.mozilla.gecko.sync.jpake.stage.VerifyPairingStage;
michael@0 34 import org.mozilla.gecko.sync.setup.Constants;
michael@0 35 import org.mozilla.gecko.sync.setup.activities.SetupSyncActivity;
michael@0 36
michael@0 37 import ch.boye.httpclientandroidlib.entity.StringEntity;
michael@0 38
michael@0 39 public class JPakeClient {
michael@0 40
michael@0 41 private static String LOG_TAG = "JPakeClient";
michael@0 42
michael@0 43 // J-PAKE constants.
michael@0 44 public final static int REQUEST_TIMEOUT = 60 * 1000; // 1 min
michael@0 45 public final static int KEYEXCHANGE_VERSION = 3;
michael@0 46 public final static String JPAKE_VERIFY_VALUE = "0123456789ABCDEF";
michael@0 47
michael@0 48 private final static String JPAKE_SIGNERID_SENDER = "sender";
michael@0 49 private final static String JPAKE_SIGNERID_RECEIVER = "receiver";
michael@0 50 private final static int JPAKE_LENGTH_SECRET = 8;
michael@0 51 private final static int JPAKE_LENGTH_CLIENTID = 256;
michael@0 52
michael@0 53 private final static int MAX_TRIES = 10;
michael@0 54 private final static int MAX_TRIES_FIRST_MSG = 300;
michael@0 55 private final static int MAX_TRIES_LAST_MSG = 300;
michael@0 56
michael@0 57 // J-PAKE session values.
michael@0 58 public String clientId;
michael@0 59 public String secret;
michael@0 60
michael@0 61 public String myEtag;
michael@0 62 public String mySignerId;
michael@0 63 public String theirEtag;
michael@0 64 public String theirSignerId;
michael@0 65 public String jpakeServer;
michael@0 66
michael@0 67 // J-PAKE state.
michael@0 68 public boolean paired = false;
michael@0 69 public boolean finished = false;
michael@0 70
michael@0 71 // J-PAKE values.
michael@0 72 public int jpakePollInterval;
michael@0 73 public int jpakeMaxTries;
michael@0 74 public String channel;
michael@0 75 public volatile String channelUrl;
michael@0 76
michael@0 77 // J-PAKE session data.
michael@0 78 public KeyBundle myKeyBundle;
michael@0 79 public JSONObject jCreds;
michael@0 80
michael@0 81 public ExtendedJSONObject jOutgoing;
michael@0 82 public ExtendedJSONObject jIncoming;
michael@0 83
michael@0 84 public JPakeParty jParty;
michael@0 85 public JPakeNumGenerator numGen;
michael@0 86
michael@0 87 public int pollTries = 0;
michael@0 88
michael@0 89 // UI controller.
michael@0 90 private SetupSyncActivity controllerActivity;
michael@0 91 private Queue<JPakeStage> stages;
michael@0 92
michael@0 93 public JPakeClient(SetupSyncActivity activity) {
michael@0 94 controllerActivity = activity;
michael@0 95 jpakeServer = "https://setup.services.mozilla.com/";
michael@0 96 jpakePollInterval = 1 * 1000; // 1 second
michael@0 97 jpakeMaxTries = MAX_TRIES;
michael@0 98
michael@0 99 if (!jpakeServer.endsWith("/")) {
michael@0 100 jpakeServer += "/";
michael@0 101 }
michael@0 102
michael@0 103 setClientId();
michael@0 104 numGen = new JPakeNumGeneratorRandom();
michael@0 105 }
michael@0 106
michael@0 107 /**
michael@0 108 * Set up Sender sequence of stages for J-PAKE. (sender of credentials)
michael@0 109 *
michael@0 110 */
michael@0 111
michael@0 112 private void prepareSenderStages() {
michael@0 113 Queue<JPakeStage> jStages = new LinkedList<JPakeStage>();
michael@0 114 jStages.add(new ComputeStepOneStage());
michael@0 115 jStages.add(new GetRequestStage());
michael@0 116 jStages.add(new PutRequestStage());
michael@0 117 jStages.add(new ComputeStepTwoStage());
michael@0 118 jStages.add(new GetRequestStage());
michael@0 119 jStages.add(new PutRequestStage());
michael@0 120 jStages.add(new ComputeFinalStage());
michael@0 121 jStages.add(new GetRequestStage());
michael@0 122 jStages.add(new VerifyPairingStage()); // Calls onPaired if verified.
michael@0 123
michael@0 124 stages = jStages;
michael@0 125 }
michael@0 126
michael@0 127 /**
michael@0 128 * Set up Receiver sequence of stages for J-PAKE. (receiver of credentials)
michael@0 129 *
michael@0 130 */
michael@0 131 private void prepareReceiverStages() {
michael@0 132 Queue<JPakeStage> jStages = new LinkedList<JPakeStage>();
michael@0 133 jStages.add(new GetChannelStage());
michael@0 134 jStages.add(new ComputeStepOneStage());
michael@0 135 jStages.add(new PutRequestStage());
michael@0 136 jStages.add(new GetRequestStage());
michael@0 137 jStages.add(new JPakeStage() {
michael@0 138 @Override
michael@0 139 public void execute(JPakeClient jpakeClient) {
michael@0 140
michael@0 141 // Notify controller that pairing has started.
michael@0 142 jpakeClient.onPairingStart();
michael@0 143
michael@0 144 // Switch back to smaller time-out.
michael@0 145 jpakeClient.jpakeMaxTries = JPakeClient.MAX_TRIES;
michael@0 146 jpakeClient.runNextStage();
michael@0 147 }
michael@0 148 });
michael@0 149 jStages.add(new ComputeStepTwoStage());
michael@0 150 jStages.add(new PutRequestStage());
michael@0 151 jStages.add(new GetRequestStage());
michael@0 152 jStages.add(new ComputeFinalStage());
michael@0 153 jStages.add(new ComputeKeyVerificationStage());
michael@0 154 jStages.add(new PutRequestStage());
michael@0 155 jStages.add(new JPakeStage() {
michael@0 156
michael@0 157 @Override
michael@0 158 public void execute(JPakeClient jpakeClient) {
michael@0 159 jpakeMaxTries = MAX_TRIES_LAST_MSG;
michael@0 160 jpakeClient.runNextStage();
michael@0 161 }
michael@0 162
michael@0 163 });
michael@0 164 jStages.add(new GetRequestStage());
michael@0 165 jStages.add(new DecryptDataStage());
michael@0 166 jStages.add(new CompleteStage());
michael@0 167
michael@0 168 stages = jStages;
michael@0 169 }
michael@0 170
michael@0 171 /**
michael@0 172 *
michael@0 173 * Pairing using PIN provided on other device. Functionality available only
michael@0 174 * when a Sync account has already been set up.
michael@0 175 *
michael@0 176 * @param pin
michael@0 177 * 12-character string containing PIN entered by the user.
michael@0 178 */
michael@0 179 public void pairWithPin(String pin) {
michael@0 180 mySignerId = JPAKE_SIGNERID_SENDER;
michael@0 181 theirSignerId = JPAKE_SIGNERID_RECEIVER;
michael@0 182 jParty = new JPakeParty(mySignerId);
michael@0 183
michael@0 184 // Extract secret and server channel.
michael@0 185 secret = pin.substring(0, JPAKE_LENGTH_SECRET);
michael@0 186 channel = pin.substring(JPAKE_LENGTH_SECRET);
michael@0 187 channelUrl = jpakeServer + channel;
michael@0 188
michael@0 189 prepareSenderStages();
michael@0 190 runNextStage();
michael@0 191 }
michael@0 192
michael@0 193 /**
michael@0 194 *
michael@0 195 * Initiate pairing and receive data, without having received a PIN. The PIN
michael@0 196 * will be generated and passed on to the controller to be displayed to the
michael@0 197 * user.
michael@0 198 *
michael@0 199 * Starts J-PAKE protocol.
michael@0 200 */
michael@0 201 public void receiveNoPin() {
michael@0 202 mySignerId = JPAKE_SIGNERID_RECEIVER;
michael@0 203 theirSignerId = JPAKE_SIGNERID_SENDER;
michael@0 204 jParty = new JPakeParty(mySignerId);
michael@0 205
michael@0 206 // TODO: fetch from prefs
michael@0 207 jpakeMaxTries = MAX_TRIES_FIRST_MSG;
michael@0 208
michael@0 209 createSecret();
michael@0 210 prepareReceiverStages();
michael@0 211 runNextStage();
michael@0 212 }
michael@0 213
michael@0 214 /**
michael@0 215 * Run next stage of J-PAKE.
michael@0 216 */
michael@0 217 public void runNextStage() {
michael@0 218 if (finished || stages.size() == 0) {
michael@0 219 Logger.debug(LOG_TAG, "All stages complete.");
michael@0 220 return;
michael@0 221 }
michael@0 222 JPakeStage currentStage = null;
michael@0 223 try{
michael@0 224 currentStage = stages.remove();
michael@0 225 Logger.debug(LOG_TAG, "starting stage " + currentStage.toString());
michael@0 226 currentStage.execute(this);
michael@0 227 } catch (Exception e) {
michael@0 228 Logger.error(LOG_TAG, "Exception in stage " + currentStage, e);
michael@0 229 abort("Stage exception.");
michael@0 230 }
michael@0 231 }
michael@0 232
michael@0 233 /**
michael@0 234 * Abort J-PAKE. This can propagate an error from the stages, or result from
michael@0 235 * UI abort (onPause, user abort)
michael@0 236 *
michael@0 237 * @param reason
michael@0 238 * Reason for abort.
michael@0 239 */
michael@0 240 public void abort(String reason) {
michael@0 241 finished = true;
michael@0 242 // We do not need to clean up the channel in the following cases:
michael@0 243 if (Constants.JPAKE_ERROR_CHANNEL.equals(reason) ||
michael@0 244 Constants.JPAKE_ERROR_NETWORK.equals(reason) ||
michael@0 245 Constants.JPAKE_ERROR_NODATA.equals(reason) ||
michael@0 246 channelUrl == null) {
michael@0 247 // We may leak a channel if the activity aborts sync while requesting the channel.
michael@0 248 // The server, however, will delete the channel anyways after a certain time has passed.
michael@0 249 displayAbort(reason);
michael@0 250 } else {
michael@0 251 // Delete channel, then call controller's displayAbort in callback.
michael@0 252 new DeleteChannel().execute(this, reason);
michael@0 253 }
michael@0 254 }
michael@0 255
michael@0 256 public void displayAbort(String reason) {
michael@0 257 controllerActivity.displayAbort(reason);
michael@0 258 }
michael@0 259
michael@0 260 /* Static helper methods used by stages. */
michael@0 261
michael@0 262 /**
michael@0 263 * Run on a different thread from the thread pool.
michael@0 264 *
michael@0 265 * @param run
michael@0 266 * Runnable to run on separate thread.
michael@0 267 */
michael@0 268 public static void runOnThread(Runnable run) {
michael@0 269 ThreadPool.run(run);
michael@0 270 }
michael@0 271
michael@0 272 /**
michael@0 273 *
michael@0 274 * @param secretString
michael@0 275 * String to convert to BigInteger
michael@0 276 * @return BigInteger representation of secretString
michael@0 277 *
michael@0 278 * @throws UnsupportedEncodingException
michael@0 279 */
michael@0 280 public static BigInteger secretAsBigInteger(String secretString) throws UnsupportedEncodingException {
michael@0 281 return new BigInteger(secretString.getBytes("UTF-8"));
michael@0 282 }
michael@0 283
michael@0 284 /**
michael@0 285 * Helper method for doing actual encryption.
michael@0 286 *
michael@0 287 * Input: String of JSONObject KeyBundle with keys for encryption
michael@0 288 *
michael@0 289 * Output: ExtendedJSONObject with IV, ciphertext, hmac (if sender)
michael@0 290 *
michael@0 291 * @throws CryptoException
michael@0 292 * @throws UnsupportedEncodingException
michael@0 293 */
michael@0 294 public static ExtendedJSONObject encryptPayload(String data, KeyBundle keyBundle, boolean makeHmac)
michael@0 295 throws UnsupportedEncodingException, CryptoException {
michael@0 296 if (keyBundle == null) {
michael@0 297 throw new NoKeyBundleException();
michael@0 298 }
michael@0 299
michael@0 300 byte[] cleartextBytes = data.getBytes("UTF-8");
michael@0 301 CryptoInfo encrypted = CryptoInfo.encrypt(cleartextBytes, keyBundle);
michael@0 302
michael@0 303 ExtendedJSONObject payload = new ExtendedJSONObject();
michael@0 304
michael@0 305 String message64 = new String(Base64.encodeBase64(encrypted.getMessage()));
michael@0 306 String iv64 = new String(Base64.encodeBase64(encrypted.getIV()));
michael@0 307
michael@0 308 payload.put(Constants.JSON_KEY_CIPHERTEXT, message64);
michael@0 309 payload.put(Constants.JSON_KEY_IV, iv64);
michael@0 310 if (makeHmac) {
michael@0 311 String hmacHex = Utils.byte2Hex(encrypted.getHMAC());
michael@0 312 payload.put(Constants.JSON_KEY_HMAC, hmacHex);
michael@0 313 }
michael@0 314 return payload;
michael@0 315 }
michael@0 316
michael@0 317 /*
michael@0 318 * Helper for turning a JSON object into a payload.
michael@0 319 *
michael@0 320 * @param body JSONObject body to be converted to StringEntity.
michael@0 321 * @return StringEntity representation of JSONObject.
michael@0 322 *
michael@0 323 * @throws UnsupportedEncodingException
michael@0 324 */
michael@0 325 public static StringEntity jsonEntity(JSONObject body)
michael@0 326 throws UnsupportedEncodingException {
michael@0 327 StringEntity entity = new StringEntity(body.toJSONString(), "UTF-8");
michael@0 328 entity.setContentType("application/json");
michael@0 329 return entity;
michael@0 330 }
michael@0 331
michael@0 332 /*
michael@0 333 * Controller methods.
michael@0 334 */
michael@0 335 public void makeAndDisplayPin(String channel) {
michael@0 336 controllerActivity.displayPin(secret + channel);
michael@0 337 }
michael@0 338
michael@0 339 public void onPairingStart() {
michael@0 340 Logger.debug(LOG_TAG, "Pairing started.");
michael@0 341 controllerActivity.onPairingStart();
michael@0 342 }
michael@0 343
michael@0 344 public void onPaired() {
michael@0 345 Logger.debug(LOG_TAG, "Pairing completed. Starting credential exchange.");
michael@0 346 controllerActivity.onPaired();
michael@0 347 }
michael@0 348
michael@0 349 public void complete(JSONObject credentials) {
michael@0 350 controllerActivity.onComplete(credentials);
michael@0 351 }
michael@0 352
michael@0 353 /*
michael@0 354 * Called from controller, with Sync credentials to be encrypted and sent.
michael@0 355 */
michael@0 356 public void sendAndComplete(JSONObject jObj)
michael@0 357 throws JPakeNoActivePairingException {
michael@0 358 if (!paired || finished) {
michael@0 359 Logger.error(LOG_TAG, "Can't send data, no active pairing!");
michael@0 360 throw new JPakeNoActivePairingException();
michael@0 361 }
michael@0 362 stages.clear();
michael@0 363 stages.add(new PutRequestStage());
michael@0 364 stages.add(new CompleteStage());
michael@0 365
michael@0 366 // Encrypt data to send and set as jOutgoing.
michael@0 367 String outData = jObj.toJSONString();
michael@0 368 encryptData(myKeyBundle, outData);
michael@0 369
michael@0 370 // Start stages for sending credentials.
michael@0 371 runNextStage();
michael@0 372 }
michael@0 373
michael@0 374 /* Setup helper functions */
michael@0 375
michael@0 376 /*
michael@0 377 * Generates and sets a clientId for communications with JPAKE setup server.
michael@0 378 */
michael@0 379 private void setClientId() {
michael@0 380 byte[] rBytes = Utils.generateRandomBytes(JPAKE_LENGTH_CLIENTID / 2);
michael@0 381 StringBuilder id = new StringBuilder();
michael@0 382
michael@0 383 for (byte b : rBytes) {
michael@0 384 String hexString = Integer.toHexString(b);
michael@0 385 if (hexString.length() == 1) {
michael@0 386 hexString = "0" + hexString;
michael@0 387 }
michael@0 388 int len = hexString.length();
michael@0 389 id.append(hexString.substring(len - 2, len));
michael@0 390 }
michael@0 391 clientId = id.toString();
michael@0 392 }
michael@0 393
michael@0 394 /*
michael@0 395 * Generates and sets a JPAKE PIN to be displayed to user.
michael@0 396 */
michael@0 397 private void createSecret() {
michael@0 398 // 0-9a-z without 1,l,o,0
michael@0 399 String key = "23456789abcdefghijkmnpqrstuvwxyz";
michael@0 400 int keylen = key.length();
michael@0 401
michael@0 402 byte[] rBytes = Utils.generateRandomBytes(JPAKE_LENGTH_SECRET);
michael@0 403 StringBuilder secret = new StringBuilder();
michael@0 404 for (byte b : rBytes) {
michael@0 405 secret.append(key.charAt(Math.abs(b) * keylen / 256));
michael@0 406 }
michael@0 407 this.secret = secret.toString();
michael@0 408 }
michael@0 409
michael@0 410 /*
michael@0 411 *
michael@0 412 * Encrypt payload and package into jOutgoing for sending with a PUT request.
michael@0 413 *
michael@0 414 * @param keyBundle Encryption keys derived during J-PAKE.
michael@0 415 *
michael@0 416 * @param payload Credentials data to be encrypted.
michael@0 417 */
michael@0 418 private void encryptData(KeyBundle keyBundle, String payload) {
michael@0 419 Logger.debug(LOG_TAG, "Encrypting data.");
michael@0 420 ExtendedJSONObject jPayload = null;
michael@0 421 try {
michael@0 422 jPayload = encryptPayload(payload, keyBundle, true);
michael@0 423 } catch (UnsupportedEncodingException e) {
michael@0 424 Logger.error(LOG_TAG, "Failed to encrypt data.", e);
michael@0 425 abort(Constants.JPAKE_ERROR_INTERNAL);
michael@0 426 return;
michael@0 427 } catch (CryptoException e) {
michael@0 428 Logger.error(LOG_TAG, "Failed to encrypt data.", e);
michael@0 429 abort(Constants.JPAKE_ERROR_INTERNAL);
michael@0 430 return;
michael@0 431 }
michael@0 432 jOutgoing = new ExtendedJSONObject();
michael@0 433 jOutgoing.put(Constants.JSON_KEY_TYPE, mySignerId + "3");
michael@0 434 jOutgoing.put(Constants.JSON_KEY_VERSION, KEYEXCHANGE_VERSION);
michael@0 435 jOutgoing.put(Constants.JSON_KEY_PAYLOAD, jPayload.object);
michael@0 436 }
michael@0 437 }

mercurial