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.

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

mercurial