1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/sync/jpake/JPakeClient.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,437 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +package org.mozilla.gecko.sync.jpake; 1.9 + 1.10 +import java.io.UnsupportedEncodingException; 1.11 +import java.math.BigInteger; 1.12 +import java.util.LinkedList; 1.13 +import java.util.Queue; 1.14 + 1.15 +import org.json.simple.JSONObject; 1.16 +import org.mozilla.apache.commons.codec.binary.Base64; 1.17 +import org.mozilla.gecko.background.common.log.Logger; 1.18 +import org.mozilla.gecko.sync.ExtendedJSONObject; 1.19 +import org.mozilla.gecko.sync.ThreadPool; 1.20 +import org.mozilla.gecko.sync.Utils; 1.21 +import org.mozilla.gecko.sync.crypto.CryptoException; 1.22 +import org.mozilla.gecko.sync.crypto.CryptoInfo; 1.23 +import org.mozilla.gecko.sync.crypto.KeyBundle; 1.24 +import org.mozilla.gecko.sync.crypto.NoKeyBundleException; 1.25 +import org.mozilla.gecko.sync.jpake.stage.CompleteStage; 1.26 +import org.mozilla.gecko.sync.jpake.stage.ComputeFinalStage; 1.27 +import org.mozilla.gecko.sync.jpake.stage.ComputeKeyVerificationStage; 1.28 +import org.mozilla.gecko.sync.jpake.stage.ComputeStepOneStage; 1.29 +import org.mozilla.gecko.sync.jpake.stage.ComputeStepTwoStage; 1.30 +import org.mozilla.gecko.sync.jpake.stage.DecryptDataStage; 1.31 +import org.mozilla.gecko.sync.jpake.stage.DeleteChannel; 1.32 +import org.mozilla.gecko.sync.jpake.stage.GetChannelStage; 1.33 +import org.mozilla.gecko.sync.jpake.stage.GetRequestStage; 1.34 +import org.mozilla.gecko.sync.jpake.stage.JPakeStage; 1.35 +import org.mozilla.gecko.sync.jpake.stage.PutRequestStage; 1.36 +import org.mozilla.gecko.sync.jpake.stage.VerifyPairingStage; 1.37 +import org.mozilla.gecko.sync.setup.Constants; 1.38 +import org.mozilla.gecko.sync.setup.activities.SetupSyncActivity; 1.39 + 1.40 +import ch.boye.httpclientandroidlib.entity.StringEntity; 1.41 + 1.42 +public class JPakeClient { 1.43 + 1.44 + private static String LOG_TAG = "JPakeClient"; 1.45 + 1.46 + // J-PAKE constants. 1.47 + public final static int REQUEST_TIMEOUT = 60 * 1000; // 1 min 1.48 + public final static int KEYEXCHANGE_VERSION = 3; 1.49 + public final static String JPAKE_VERIFY_VALUE = "0123456789ABCDEF"; 1.50 + 1.51 + private final static String JPAKE_SIGNERID_SENDER = "sender"; 1.52 + private final static String JPAKE_SIGNERID_RECEIVER = "receiver"; 1.53 + private final static int JPAKE_LENGTH_SECRET = 8; 1.54 + private final static int JPAKE_LENGTH_CLIENTID = 256; 1.55 + 1.56 + private final static int MAX_TRIES = 10; 1.57 + private final static int MAX_TRIES_FIRST_MSG = 300; 1.58 + private final static int MAX_TRIES_LAST_MSG = 300; 1.59 + 1.60 + // J-PAKE session values. 1.61 + public String clientId; 1.62 + public String secret; 1.63 + 1.64 + public String myEtag; 1.65 + public String mySignerId; 1.66 + public String theirEtag; 1.67 + public String theirSignerId; 1.68 + public String jpakeServer; 1.69 + 1.70 + // J-PAKE state. 1.71 + public boolean paired = false; 1.72 + public boolean finished = false; 1.73 + 1.74 + // J-PAKE values. 1.75 + public int jpakePollInterval; 1.76 + public int jpakeMaxTries; 1.77 + public String channel; 1.78 + public volatile String channelUrl; 1.79 + 1.80 + // J-PAKE session data. 1.81 + public KeyBundle myKeyBundle; 1.82 + public JSONObject jCreds; 1.83 + 1.84 + public ExtendedJSONObject jOutgoing; 1.85 + public ExtendedJSONObject jIncoming; 1.86 + 1.87 + public JPakeParty jParty; 1.88 + public JPakeNumGenerator numGen; 1.89 + 1.90 + public int pollTries = 0; 1.91 + 1.92 + // UI controller. 1.93 + private SetupSyncActivity controllerActivity; 1.94 + private Queue<JPakeStage> stages; 1.95 + 1.96 + public JPakeClient(SetupSyncActivity activity) { 1.97 + controllerActivity = activity; 1.98 + jpakeServer = "https://setup.services.mozilla.com/"; 1.99 + jpakePollInterval = 1 * 1000; // 1 second 1.100 + jpakeMaxTries = MAX_TRIES; 1.101 + 1.102 + if (!jpakeServer.endsWith("/")) { 1.103 + jpakeServer += "/"; 1.104 + } 1.105 + 1.106 + setClientId(); 1.107 + numGen = new JPakeNumGeneratorRandom(); 1.108 + } 1.109 + 1.110 + /** 1.111 + * Set up Sender sequence of stages for J-PAKE. (sender of credentials) 1.112 + * 1.113 + */ 1.114 + 1.115 + private void prepareSenderStages() { 1.116 + Queue<JPakeStage> jStages = new LinkedList<JPakeStage>(); 1.117 + jStages.add(new ComputeStepOneStage()); 1.118 + jStages.add(new GetRequestStage()); 1.119 + jStages.add(new PutRequestStage()); 1.120 + jStages.add(new ComputeStepTwoStage()); 1.121 + jStages.add(new GetRequestStage()); 1.122 + jStages.add(new PutRequestStage()); 1.123 + jStages.add(new ComputeFinalStage()); 1.124 + jStages.add(new GetRequestStage()); 1.125 + jStages.add(new VerifyPairingStage()); // Calls onPaired if verified. 1.126 + 1.127 + stages = jStages; 1.128 + } 1.129 + 1.130 + /** 1.131 + * Set up Receiver sequence of stages for J-PAKE. (receiver of credentials) 1.132 + * 1.133 + */ 1.134 + private void prepareReceiverStages() { 1.135 + Queue<JPakeStage> jStages = new LinkedList<JPakeStage>(); 1.136 + jStages.add(new GetChannelStage()); 1.137 + jStages.add(new ComputeStepOneStage()); 1.138 + jStages.add(new PutRequestStage()); 1.139 + jStages.add(new GetRequestStage()); 1.140 + jStages.add(new JPakeStage() { 1.141 + @Override 1.142 + public void execute(JPakeClient jpakeClient) { 1.143 + 1.144 + // Notify controller that pairing has started. 1.145 + jpakeClient.onPairingStart(); 1.146 + 1.147 + // Switch back to smaller time-out. 1.148 + jpakeClient.jpakeMaxTries = JPakeClient.MAX_TRIES; 1.149 + jpakeClient.runNextStage(); 1.150 + } 1.151 + }); 1.152 + jStages.add(new ComputeStepTwoStage()); 1.153 + jStages.add(new PutRequestStage()); 1.154 + jStages.add(new GetRequestStage()); 1.155 + jStages.add(new ComputeFinalStage()); 1.156 + jStages.add(new ComputeKeyVerificationStage()); 1.157 + jStages.add(new PutRequestStage()); 1.158 + jStages.add(new JPakeStage() { 1.159 + 1.160 + @Override 1.161 + public void execute(JPakeClient jpakeClient) { 1.162 + jpakeMaxTries = MAX_TRIES_LAST_MSG; 1.163 + jpakeClient.runNextStage(); 1.164 + } 1.165 + 1.166 + }); 1.167 + jStages.add(new GetRequestStage()); 1.168 + jStages.add(new DecryptDataStage()); 1.169 + jStages.add(new CompleteStage()); 1.170 + 1.171 + stages = jStages; 1.172 + } 1.173 + 1.174 + /** 1.175 + * 1.176 + * Pairing using PIN provided on other device. Functionality available only 1.177 + * when a Sync account has already been set up. 1.178 + * 1.179 + * @param pin 1.180 + * 12-character string containing PIN entered by the user. 1.181 + */ 1.182 + public void pairWithPin(String pin) { 1.183 + mySignerId = JPAKE_SIGNERID_SENDER; 1.184 + theirSignerId = JPAKE_SIGNERID_RECEIVER; 1.185 + jParty = new JPakeParty(mySignerId); 1.186 + 1.187 + // Extract secret and server channel. 1.188 + secret = pin.substring(0, JPAKE_LENGTH_SECRET); 1.189 + channel = pin.substring(JPAKE_LENGTH_SECRET); 1.190 + channelUrl = jpakeServer + channel; 1.191 + 1.192 + prepareSenderStages(); 1.193 + runNextStage(); 1.194 + } 1.195 + 1.196 + /** 1.197 + * 1.198 + * Initiate pairing and receive data, without having received a PIN. The PIN 1.199 + * will be generated and passed on to the controller to be displayed to the 1.200 + * user. 1.201 + * 1.202 + * Starts J-PAKE protocol. 1.203 + */ 1.204 + public void receiveNoPin() { 1.205 + mySignerId = JPAKE_SIGNERID_RECEIVER; 1.206 + theirSignerId = JPAKE_SIGNERID_SENDER; 1.207 + jParty = new JPakeParty(mySignerId); 1.208 + 1.209 + // TODO: fetch from prefs 1.210 + jpakeMaxTries = MAX_TRIES_FIRST_MSG; 1.211 + 1.212 + createSecret(); 1.213 + prepareReceiverStages(); 1.214 + runNextStage(); 1.215 + } 1.216 + 1.217 + /** 1.218 + * Run next stage of J-PAKE. 1.219 + */ 1.220 + public void runNextStage() { 1.221 + if (finished || stages.size() == 0) { 1.222 + Logger.debug(LOG_TAG, "All stages complete."); 1.223 + return; 1.224 + } 1.225 + JPakeStage currentStage = null; 1.226 + try{ 1.227 + currentStage = stages.remove(); 1.228 + Logger.debug(LOG_TAG, "starting stage " + currentStage.toString()); 1.229 + currentStage.execute(this); 1.230 + } catch (Exception e) { 1.231 + Logger.error(LOG_TAG, "Exception in stage " + currentStage, e); 1.232 + abort("Stage exception."); 1.233 + } 1.234 + } 1.235 + 1.236 + /** 1.237 + * Abort J-PAKE. This can propagate an error from the stages, or result from 1.238 + * UI abort (onPause, user abort) 1.239 + * 1.240 + * @param reason 1.241 + * Reason for abort. 1.242 + */ 1.243 + public void abort(String reason) { 1.244 + finished = true; 1.245 + // We do not need to clean up the channel in the following cases: 1.246 + if (Constants.JPAKE_ERROR_CHANNEL.equals(reason) || 1.247 + Constants.JPAKE_ERROR_NETWORK.equals(reason) || 1.248 + Constants.JPAKE_ERROR_NODATA.equals(reason) || 1.249 + channelUrl == null) { 1.250 + // We may leak a channel if the activity aborts sync while requesting the channel. 1.251 + // The server, however, will delete the channel anyways after a certain time has passed. 1.252 + displayAbort(reason); 1.253 + } else { 1.254 + // Delete channel, then call controller's displayAbort in callback. 1.255 + new DeleteChannel().execute(this, reason); 1.256 + } 1.257 + } 1.258 + 1.259 + public void displayAbort(String reason) { 1.260 + controllerActivity.displayAbort(reason); 1.261 + } 1.262 + 1.263 + /* Static helper methods used by stages. */ 1.264 + 1.265 + /** 1.266 + * Run on a different thread from the thread pool. 1.267 + * 1.268 + * @param run 1.269 + * Runnable to run on separate thread. 1.270 + */ 1.271 + public static void runOnThread(Runnable run) { 1.272 + ThreadPool.run(run); 1.273 + } 1.274 + 1.275 + /** 1.276 + * 1.277 + * @param secretString 1.278 + * String to convert to BigInteger 1.279 + * @return BigInteger representation of secretString 1.280 + * 1.281 + * @throws UnsupportedEncodingException 1.282 + */ 1.283 + public static BigInteger secretAsBigInteger(String secretString) throws UnsupportedEncodingException { 1.284 + return new BigInteger(secretString.getBytes("UTF-8")); 1.285 + } 1.286 + 1.287 + /** 1.288 + * Helper method for doing actual encryption. 1.289 + * 1.290 + * Input: String of JSONObject KeyBundle with keys for encryption 1.291 + * 1.292 + * Output: ExtendedJSONObject with IV, ciphertext, hmac (if sender) 1.293 + * 1.294 + * @throws CryptoException 1.295 + * @throws UnsupportedEncodingException 1.296 + */ 1.297 + public static ExtendedJSONObject encryptPayload(String data, KeyBundle keyBundle, boolean makeHmac) 1.298 + throws UnsupportedEncodingException, CryptoException { 1.299 + if (keyBundle == null) { 1.300 + throw new NoKeyBundleException(); 1.301 + } 1.302 + 1.303 + byte[] cleartextBytes = data.getBytes("UTF-8"); 1.304 + CryptoInfo encrypted = CryptoInfo.encrypt(cleartextBytes, keyBundle); 1.305 + 1.306 + ExtendedJSONObject payload = new ExtendedJSONObject(); 1.307 + 1.308 + String message64 = new String(Base64.encodeBase64(encrypted.getMessage())); 1.309 + String iv64 = new String(Base64.encodeBase64(encrypted.getIV())); 1.310 + 1.311 + payload.put(Constants.JSON_KEY_CIPHERTEXT, message64); 1.312 + payload.put(Constants.JSON_KEY_IV, iv64); 1.313 + if (makeHmac) { 1.314 + String hmacHex = Utils.byte2Hex(encrypted.getHMAC()); 1.315 + payload.put(Constants.JSON_KEY_HMAC, hmacHex); 1.316 + } 1.317 + return payload; 1.318 + } 1.319 + 1.320 + /* 1.321 + * Helper for turning a JSON object into a payload. 1.322 + * 1.323 + * @param body JSONObject body to be converted to StringEntity. 1.324 + * @return StringEntity representation of JSONObject. 1.325 + * 1.326 + * @throws UnsupportedEncodingException 1.327 + */ 1.328 + public static StringEntity jsonEntity(JSONObject body) 1.329 + throws UnsupportedEncodingException { 1.330 + StringEntity entity = new StringEntity(body.toJSONString(), "UTF-8"); 1.331 + entity.setContentType("application/json"); 1.332 + return entity; 1.333 + } 1.334 + 1.335 + /* 1.336 + * Controller methods. 1.337 + */ 1.338 + public void makeAndDisplayPin(String channel) { 1.339 + controllerActivity.displayPin(secret + channel); 1.340 + } 1.341 + 1.342 + public void onPairingStart() { 1.343 + Logger.debug(LOG_TAG, "Pairing started."); 1.344 + controllerActivity.onPairingStart(); 1.345 + } 1.346 + 1.347 + public void onPaired() { 1.348 + Logger.debug(LOG_TAG, "Pairing completed. Starting credential exchange."); 1.349 + controllerActivity.onPaired(); 1.350 + } 1.351 + 1.352 + public void complete(JSONObject credentials) { 1.353 + controllerActivity.onComplete(credentials); 1.354 + } 1.355 + 1.356 + /* 1.357 + * Called from controller, with Sync credentials to be encrypted and sent. 1.358 + */ 1.359 + public void sendAndComplete(JSONObject jObj) 1.360 + throws JPakeNoActivePairingException { 1.361 + if (!paired || finished) { 1.362 + Logger.error(LOG_TAG, "Can't send data, no active pairing!"); 1.363 + throw new JPakeNoActivePairingException(); 1.364 + } 1.365 + stages.clear(); 1.366 + stages.add(new PutRequestStage()); 1.367 + stages.add(new CompleteStage()); 1.368 + 1.369 + // Encrypt data to send and set as jOutgoing. 1.370 + String outData = jObj.toJSONString(); 1.371 + encryptData(myKeyBundle, outData); 1.372 + 1.373 + // Start stages for sending credentials. 1.374 + runNextStage(); 1.375 + } 1.376 + 1.377 + /* Setup helper functions */ 1.378 + 1.379 + /* 1.380 + * Generates and sets a clientId for communications with JPAKE setup server. 1.381 + */ 1.382 + private void setClientId() { 1.383 + byte[] rBytes = Utils.generateRandomBytes(JPAKE_LENGTH_CLIENTID / 2); 1.384 + StringBuilder id = new StringBuilder(); 1.385 + 1.386 + for (byte b : rBytes) { 1.387 + String hexString = Integer.toHexString(b); 1.388 + if (hexString.length() == 1) { 1.389 + hexString = "0" + hexString; 1.390 + } 1.391 + int len = hexString.length(); 1.392 + id.append(hexString.substring(len - 2, len)); 1.393 + } 1.394 + clientId = id.toString(); 1.395 + } 1.396 + 1.397 + /* 1.398 + * Generates and sets a JPAKE PIN to be displayed to user. 1.399 + */ 1.400 + private void createSecret() { 1.401 + // 0-9a-z without 1,l,o,0 1.402 + String key = "23456789abcdefghijkmnpqrstuvwxyz"; 1.403 + int keylen = key.length(); 1.404 + 1.405 + byte[] rBytes = Utils.generateRandomBytes(JPAKE_LENGTH_SECRET); 1.406 + StringBuilder secret = new StringBuilder(); 1.407 + for (byte b : rBytes) { 1.408 + secret.append(key.charAt(Math.abs(b) * keylen / 256)); 1.409 + } 1.410 + this.secret = secret.toString(); 1.411 + } 1.412 + 1.413 + /* 1.414 + * 1.415 + * Encrypt payload and package into jOutgoing for sending with a PUT request. 1.416 + * 1.417 + * @param keyBundle Encryption keys derived during J-PAKE. 1.418 + * 1.419 + * @param payload Credentials data to be encrypted. 1.420 + */ 1.421 + private void encryptData(KeyBundle keyBundle, String payload) { 1.422 + Logger.debug(LOG_TAG, "Encrypting data."); 1.423 + ExtendedJSONObject jPayload = null; 1.424 + try { 1.425 + jPayload = encryptPayload(payload, keyBundle, true); 1.426 + } catch (UnsupportedEncodingException e) { 1.427 + Logger.error(LOG_TAG, "Failed to encrypt data.", e); 1.428 + abort(Constants.JPAKE_ERROR_INTERNAL); 1.429 + return; 1.430 + } catch (CryptoException e) { 1.431 + Logger.error(LOG_TAG, "Failed to encrypt data.", e); 1.432 + abort(Constants.JPAKE_ERROR_INTERNAL); 1.433 + return; 1.434 + } 1.435 + jOutgoing = new ExtendedJSONObject(); 1.436 + jOutgoing.put(Constants.JSON_KEY_TYPE, mySignerId + "3"); 1.437 + jOutgoing.put(Constants.JSON_KEY_VERSION, KEYEXCHANGE_VERSION); 1.438 + jOutgoing.put(Constants.JSON_KEY_PAYLOAD, jPayload.object); 1.439 + } 1.440 +}