|
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/. */ |
|
4 |
|
5 package org.mozilla.gecko.sync.jpake; |
|
6 |
|
7 import java.io.UnsupportedEncodingException; |
|
8 import java.math.BigInteger; |
|
9 import java.util.LinkedList; |
|
10 import java.util.Queue; |
|
11 |
|
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; |
|
36 |
|
37 import ch.boye.httpclientandroidlib.entity.StringEntity; |
|
38 |
|
39 public class JPakeClient { |
|
40 |
|
41 private static String LOG_TAG = "JPakeClient"; |
|
42 |
|
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"; |
|
47 |
|
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; |
|
52 |
|
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; |
|
56 |
|
57 // J-PAKE session values. |
|
58 public String clientId; |
|
59 public String secret; |
|
60 |
|
61 public String myEtag; |
|
62 public String mySignerId; |
|
63 public String theirEtag; |
|
64 public String theirSignerId; |
|
65 public String jpakeServer; |
|
66 |
|
67 // J-PAKE state. |
|
68 public boolean paired = false; |
|
69 public boolean finished = false; |
|
70 |
|
71 // J-PAKE values. |
|
72 public int jpakePollInterval; |
|
73 public int jpakeMaxTries; |
|
74 public String channel; |
|
75 public volatile String channelUrl; |
|
76 |
|
77 // J-PAKE session data. |
|
78 public KeyBundle myKeyBundle; |
|
79 public JSONObject jCreds; |
|
80 |
|
81 public ExtendedJSONObject jOutgoing; |
|
82 public ExtendedJSONObject jIncoming; |
|
83 |
|
84 public JPakeParty jParty; |
|
85 public JPakeNumGenerator numGen; |
|
86 |
|
87 public int pollTries = 0; |
|
88 |
|
89 // UI controller. |
|
90 private SetupSyncActivity controllerActivity; |
|
91 private Queue<JPakeStage> stages; |
|
92 |
|
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; |
|
98 |
|
99 if (!jpakeServer.endsWith("/")) { |
|
100 jpakeServer += "/"; |
|
101 } |
|
102 |
|
103 setClientId(); |
|
104 numGen = new JPakeNumGeneratorRandom(); |
|
105 } |
|
106 |
|
107 /** |
|
108 * Set up Sender sequence of stages for J-PAKE. (sender of credentials) |
|
109 * |
|
110 */ |
|
111 |
|
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. |
|
123 |
|
124 stages = jStages; |
|
125 } |
|
126 |
|
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) { |
|
140 |
|
141 // Notify controller that pairing has started. |
|
142 jpakeClient.onPairingStart(); |
|
143 |
|
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() { |
|
156 |
|
157 @Override |
|
158 public void execute(JPakeClient jpakeClient) { |
|
159 jpakeMaxTries = MAX_TRIES_LAST_MSG; |
|
160 jpakeClient.runNextStage(); |
|
161 } |
|
162 |
|
163 }); |
|
164 jStages.add(new GetRequestStage()); |
|
165 jStages.add(new DecryptDataStage()); |
|
166 jStages.add(new CompleteStage()); |
|
167 |
|
168 stages = jStages; |
|
169 } |
|
170 |
|
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); |
|
183 |
|
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; |
|
188 |
|
189 prepareSenderStages(); |
|
190 runNextStage(); |
|
191 } |
|
192 |
|
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); |
|
205 |
|
206 // TODO: fetch from prefs |
|
207 jpakeMaxTries = MAX_TRIES_FIRST_MSG; |
|
208 |
|
209 createSecret(); |
|
210 prepareReceiverStages(); |
|
211 runNextStage(); |
|
212 } |
|
213 |
|
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 } |
|
232 |
|
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 } |
|
255 |
|
256 public void displayAbort(String reason) { |
|
257 controllerActivity.displayAbort(reason); |
|
258 } |
|
259 |
|
260 /* Static helper methods used by stages. */ |
|
261 |
|
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 } |
|
271 |
|
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 } |
|
283 |
|
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 } |
|
299 |
|
300 byte[] cleartextBytes = data.getBytes("UTF-8"); |
|
301 CryptoInfo encrypted = CryptoInfo.encrypt(cleartextBytes, keyBundle); |
|
302 |
|
303 ExtendedJSONObject payload = new ExtendedJSONObject(); |
|
304 |
|
305 String message64 = new String(Base64.encodeBase64(encrypted.getMessage())); |
|
306 String iv64 = new String(Base64.encodeBase64(encrypted.getIV())); |
|
307 |
|
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 } |
|
316 |
|
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 } |
|
331 |
|
332 /* |
|
333 * Controller methods. |
|
334 */ |
|
335 public void makeAndDisplayPin(String channel) { |
|
336 controllerActivity.displayPin(secret + channel); |
|
337 } |
|
338 |
|
339 public void onPairingStart() { |
|
340 Logger.debug(LOG_TAG, "Pairing started."); |
|
341 controllerActivity.onPairingStart(); |
|
342 } |
|
343 |
|
344 public void onPaired() { |
|
345 Logger.debug(LOG_TAG, "Pairing completed. Starting credential exchange."); |
|
346 controllerActivity.onPaired(); |
|
347 } |
|
348 |
|
349 public void complete(JSONObject credentials) { |
|
350 controllerActivity.onComplete(credentials); |
|
351 } |
|
352 |
|
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()); |
|
365 |
|
366 // Encrypt data to send and set as jOutgoing. |
|
367 String outData = jObj.toJSONString(); |
|
368 encryptData(myKeyBundle, outData); |
|
369 |
|
370 // Start stages for sending credentials. |
|
371 runNextStage(); |
|
372 } |
|
373 |
|
374 /* Setup helper functions */ |
|
375 |
|
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(); |
|
382 |
|
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 } |
|
393 |
|
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(); |
|
401 |
|
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 } |
|
409 |
|
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 } |