Wed, 31 Dec 2014 06:09:35 +0100
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 }