mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-02-24 11:27:49 +00:00
Bug 729170 - Restructuring of J-PAKE into stages. r=rnewman
This commit is contained in:
parent
24475cc4ba
commit
00ca37181a
File diff suppressed because it is too large
Load Diff
23
mobile/android/base/sync/jpake/JPakeJson.java
Normal file
23
mobile/android/base/sync/jpake/JPakeJson.java
Normal file
@ -0,0 +1,23 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.sync.jpake;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
import org.mozilla.gecko.sync.ExtendedJSONObject;
|
||||
import org.mozilla.gecko.sync.setup.Constants;
|
||||
|
||||
public class JPakeJson {
|
||||
/*
|
||||
* Helper function to generate a JSON-encoded ZKP.
|
||||
*/
|
||||
public static ExtendedJSONObject makeJZkp(BigInteger gr, BigInteger b, String id) {
|
||||
ExtendedJSONObject result = new ExtendedJSONObject();
|
||||
result.put(Constants.ZKP_KEY_GR, BigIntegerHelper.toEvenLengthHex(gr));
|
||||
result.put(Constants.ZKP_KEY_B, BigIntegerHelper.toEvenLengthHex(b));
|
||||
result.put(Constants.ZKP_KEY_ID, id);
|
||||
return result;
|
||||
}
|
||||
}
|
15
mobile/android/base/sync/jpake/stage/CompleteStage.java
Normal file
15
mobile/android/base/sync/jpake/stage/CompleteStage.java
Normal file
@ -0,0 +1,15 @@
|
||||
package org.mozilla.gecko.sync.jpake.stage;
|
||||
|
||||
import org.mozilla.gecko.sync.Logger;
|
||||
import org.mozilla.gecko.sync.jpake.JPakeClient;
|
||||
|
||||
public class CompleteStage extends JPakeStage {
|
||||
|
||||
@Override
|
||||
public void execute(JPakeClient jClient) {
|
||||
Logger.debug(LOG_TAG, "Exchange complete.");
|
||||
jClient.finished = true;
|
||||
jClient.complete(jClient.jCreds);
|
||||
jClient.runNextStage();
|
||||
}
|
||||
}
|
94
mobile/android/base/sync/jpake/stage/ComputeFinalStage.java
Normal file
94
mobile/android/base/sync/jpake/stage/ComputeFinalStage.java
Normal file
@ -0,0 +1,94 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.sync.jpake.stage;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.math.BigInteger;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import org.mozilla.gecko.sync.ExtendedJSONObject;
|
||||
import org.mozilla.gecko.sync.Logger;
|
||||
import org.mozilla.gecko.sync.NonObjectJSONException;
|
||||
import org.mozilla.gecko.sync.jpake.IncorrectZkpException;
|
||||
import org.mozilla.gecko.sync.jpake.JPakeClient;
|
||||
import org.mozilla.gecko.sync.jpake.JPakeCrypto;
|
||||
import org.mozilla.gecko.sync.jpake.Zkp;
|
||||
import org.mozilla.gecko.sync.setup.Constants;
|
||||
|
||||
public class ComputeFinalStage extends JPakeStage {
|
||||
|
||||
@Override
|
||||
public void execute(JPakeClient jClient) {
|
||||
Logger.debug(LOG_TAG, "Computing final round.");
|
||||
|
||||
// Check incoming message type.
|
||||
if (!jClient.jIncoming.get(Constants.JSON_KEY_TYPE).equals(jClient.theirSignerId + "2")) {
|
||||
Logger.error(LOG_TAG, "Invalid round 2 message: " + jClient.jIncoming.toJSONString());
|
||||
jClient.abort(Constants.JPAKE_ERROR_WRONGMESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check incoming message fields.
|
||||
ExtendedJSONObject iPayload;
|
||||
ExtendedJSONObject zkpPayload;
|
||||
try {
|
||||
iPayload = jClient.jIncoming.getObject(Constants.JSON_KEY_PAYLOAD);
|
||||
if (iPayload == null
|
||||
|| iPayload.getObject(Constants.ZKP_KEY_ZKP_A) == null) {
|
||||
Logger.error(LOG_TAG,
|
||||
"Invalid round 2 message: " + jClient.jIncoming.toJSONString());
|
||||
jClient.abort(Constants.JPAKE_ERROR_WRONGMESSAGE);
|
||||
return;
|
||||
}
|
||||
zkpPayload = iPayload.getObject(Constants.ZKP_KEY_ZKP_A);
|
||||
} catch (NonObjectJSONException e) {
|
||||
Logger.error(LOG_TAG, "JSON object Exception.", e);
|
||||
jClient.abort(Constants.JPAKE_ERROR_INVALID);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!jClient.theirSignerId.equals(zkpPayload.get(Constants.ZKP_KEY_ID))) {
|
||||
Logger.error(LOG_TAG, "Invalid round 2 message: " + jClient.jIncoming.toJSONString());
|
||||
jClient.abort(Constants.JPAKE_ERROR_WRONGMESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract fields.
|
||||
jClient.jParty.otherA = new BigInteger((String) iPayload.get(Constants.ZKP_KEY_A), 16);
|
||||
|
||||
// Extract ZKP.
|
||||
String gr = (String) zkpPayload.get(Constants.ZKP_KEY_GR);
|
||||
String b = (String) zkpPayload.get(Constants.ZKP_KEY_B);
|
||||
String id = (String) zkpPayload.get(Constants.ZKP_KEY_ID);
|
||||
|
||||
jClient.jParty.otherZkpA = new Zkp(new BigInteger(gr, 16), new BigInteger(b, 16), id);
|
||||
|
||||
jClient.myKeyBundle = null;
|
||||
try {
|
||||
jClient.myKeyBundle = JPakeCrypto.finalRound(JPakeClient.secretAsBigInteger(jClient.secret), jClient.jParty);
|
||||
} catch (IncorrectZkpException e) {
|
||||
Logger.error(LOG_TAG, "ZKP mismatch");
|
||||
jClient.abort(Constants.JPAKE_ERROR_WRONGMESSAGE);
|
||||
return;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
Logger.error(LOG_TAG, "NoSuchAlgorithmException", e);
|
||||
jClient.abort(Constants.JPAKE_ERROR_INTERNAL);
|
||||
return;
|
||||
} catch (InvalidKeyException e) {
|
||||
Logger.error(LOG_TAG, "InvalidKeyException", e);
|
||||
jClient.abort(Constants.JPAKE_ERROR_INTERNAL);
|
||||
return;
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
Logger.error(LOG_TAG, "UnsupportedEncodingException", e);
|
||||
jClient.abort(Constants.JPAKE_ERROR_INTERNAL);
|
||||
return;
|
||||
}
|
||||
|
||||
// Run next stage.
|
||||
jClient.runNextStage();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.sync.jpake.stage;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
||||
import org.mozilla.gecko.sync.ExtendedJSONObject;
|
||||
import org.mozilla.gecko.sync.Logger;
|
||||
import org.mozilla.gecko.sync.crypto.CryptoException;
|
||||
import org.mozilla.gecko.sync.crypto.KeyBundle;
|
||||
import org.mozilla.gecko.sync.jpake.JPakeClient;
|
||||
import org.mozilla.gecko.sync.setup.Constants;
|
||||
|
||||
public class ComputeKeyVerificationStage extends JPakeStage {
|
||||
|
||||
@Override
|
||||
public void execute(JPakeClient jClient) {
|
||||
Logger.debug(LOG_TAG, "Computing verification to send.");
|
||||
if (jClient.myKeyBundle == null) {
|
||||
Logger.error(LOG_TAG, "KeyBundle has not been set; aborting.");
|
||||
jClient.abort(Constants.JPAKE_ERROR_INTERNAL);
|
||||
}
|
||||
try {
|
||||
jClient.jOutgoing = computeKeyVerification(jClient.myKeyBundle, jClient.mySignerId);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
Logger.error(LOG_TAG, "Failure in key verification.", e);
|
||||
jClient.abort(Constants.JPAKE_ERROR_INVALID);
|
||||
return;
|
||||
} catch (CryptoException e) {
|
||||
Logger.error(LOG_TAG, "Encryption failure in key verification.", e);
|
||||
jClient.abort(Constants.JPAKE_ERROR_INTERNAL);
|
||||
return;
|
||||
}
|
||||
|
||||
jClient.runNextStage();
|
||||
}
|
||||
|
||||
/*
|
||||
* Helper function to compute a ciphertext, IV, and HMAC from derived
|
||||
* keyBundle for verifying other party.
|
||||
*
|
||||
* (Made 'public' for testing and is a stateless function.)
|
||||
*/
|
||||
public ExtendedJSONObject computeKeyVerification(KeyBundle keyBundle, String signerId)
|
||||
throws UnsupportedEncodingException, CryptoException
|
||||
{
|
||||
Logger.debug(LOG_TAG, "Encrypting key verification value.");
|
||||
ExtendedJSONObject jPayload = JPakeClient.encryptPayload(JPakeClient.JPAKE_VERIFY_VALUE, keyBundle, true);
|
||||
ExtendedJSONObject result = new ExtendedJSONObject();
|
||||
result.put(Constants.JSON_KEY_TYPE, signerId + "3");
|
||||
result.put(Constants.JSON_KEY_VERSION, JPakeClient.KEYEXCHANGE_VERSION);
|
||||
result.put(Constants.JSON_KEY_PAYLOAD, jPayload.object);
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.sync.jpake.stage;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import org.mozilla.gecko.sync.ExtendedJSONObject;
|
||||
import org.mozilla.gecko.sync.Logger;
|
||||
import org.mozilla.gecko.sync.jpake.BigIntegerHelper;
|
||||
import org.mozilla.gecko.sync.jpake.JPakeClient;
|
||||
import org.mozilla.gecko.sync.jpake.JPakeCrypto;
|
||||
import org.mozilla.gecko.sync.jpake.JPakeJson;
|
||||
import org.mozilla.gecko.sync.jpake.JPakeParty;
|
||||
import org.mozilla.gecko.sync.jpake.Zkp;
|
||||
import org.mozilla.gecko.sync.setup.Constants;
|
||||
|
||||
public class ComputeStepOneStage extends JPakeStage {
|
||||
|
||||
@Override
|
||||
public void execute(JPakeClient jClient) {
|
||||
Logger.debug(LOG_TAG, "Computing round 1.");
|
||||
|
||||
JPakeParty jClientParty = jClient.jParty;
|
||||
try {
|
||||
JPakeCrypto.round1(jClientParty, jClient.numGen);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
Logger.error(LOG_TAG, "No such algorithm.", e);
|
||||
jClient.abort(Constants.JPAKE_ERROR_INTERNAL);
|
||||
return;
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
Logger.error(LOG_TAG, "Unsupported encoding.", e);
|
||||
jClient.abort(Constants.JPAKE_ERROR_INVALID);
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
Logger.error(LOG_TAG, "Unexpected exception.", e);
|
||||
jClient.abort(Constants.JPAKE_ERROR_INTERNAL);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set outgoing message.
|
||||
ExtendedJSONObject jOne = new ExtendedJSONObject();
|
||||
jOne.put(Constants.ZKP_KEY_GX1,
|
||||
BigIntegerHelper.toEvenLengthHex(jClientParty.gx1));
|
||||
jOne.put(Constants.ZKP_KEY_GX2,
|
||||
BigIntegerHelper.toEvenLengthHex(jClientParty.gx2));
|
||||
|
||||
Zkp zkp1 = jClientParty.zkp1;
|
||||
Zkp zkp2 = jClientParty.zkp2;
|
||||
ExtendedJSONObject jZkp1 = JPakeJson.makeJZkp(zkp1.gr, zkp1.b, jClient.mySignerId);
|
||||
ExtendedJSONObject jZkp2 = JPakeJson.makeJZkp(zkp2.gr, zkp2.b, jClient.mySignerId);
|
||||
|
||||
jOne.put(Constants.ZKP_KEY_ZKP_X1, jZkp1);
|
||||
jOne.put(Constants.ZKP_KEY_ZKP_X2, jZkp2);
|
||||
|
||||
jClient.jOutgoing = new ExtendedJSONObject();
|
||||
jClient.jOutgoing.put(Constants.JSON_KEY_TYPE, jClient.mySignerId + "1");
|
||||
jClient.jOutgoing.put(Constants.JSON_KEY_PAYLOAD, jOne);
|
||||
jClient.jOutgoing.put(Constants.JSON_KEY_VERSION, JPakeClient.KEYEXCHANGE_VERSION);
|
||||
Logger.debug(LOG_TAG, "Sending: " + jClient.jOutgoing.toJSONString());
|
||||
|
||||
jClient.runNextStage();
|
||||
}
|
||||
}
|
125
mobile/android/base/sync/jpake/stage/ComputeStepTwoStage.java
Normal file
125
mobile/android/base/sync/jpake/stage/ComputeStepTwoStage.java
Normal file
@ -0,0 +1,125 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.sync.jpake.stage;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.math.BigInteger;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import org.mozilla.gecko.sync.ExtendedJSONObject;
|
||||
import org.mozilla.gecko.sync.Logger;
|
||||
import org.mozilla.gecko.sync.NonObjectJSONException;
|
||||
import org.mozilla.gecko.sync.jpake.BigIntegerHelper;
|
||||
import org.mozilla.gecko.sync.jpake.Gx3OrGx4IsZeroOrOneException;
|
||||
import org.mozilla.gecko.sync.jpake.IncorrectZkpException;
|
||||
import org.mozilla.gecko.sync.jpake.JPakeClient;
|
||||
import org.mozilla.gecko.sync.jpake.JPakeCrypto;
|
||||
import org.mozilla.gecko.sync.jpake.JPakeJson;
|
||||
import org.mozilla.gecko.sync.jpake.Zkp;
|
||||
import org.mozilla.gecko.sync.setup.Constants;
|
||||
|
||||
public class ComputeStepTwoStage extends JPakeStage {
|
||||
|
||||
@Override
|
||||
public void execute(JPakeClient jClient) {
|
||||
Logger.debug(LOG_TAG, "Computing round 2.");
|
||||
|
||||
// Check incoming message sender.
|
||||
if (!jClient.jIncoming.get(Constants.JSON_KEY_TYPE).equals(jClient.theirSignerId + "1")) {
|
||||
Logger.error(LOG_TAG, "Invalid round 1 message: " + jClient.jIncoming.toJSONString());
|
||||
jClient.abort(Constants.JPAKE_ERROR_WRONGMESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check incoming message fields.
|
||||
ExtendedJSONObject iPayload;
|
||||
try {
|
||||
iPayload = jClient.jIncoming.getObject(Constants.JSON_KEY_PAYLOAD);
|
||||
} catch (NonObjectJSONException e) {
|
||||
Logger.error(LOG_TAG, "JSON object exception.", e);
|
||||
jClient.abort(Constants.JPAKE_ERROR_INVALID);
|
||||
return;
|
||||
}
|
||||
if (iPayload == null) {
|
||||
Logger.error(LOG_TAG, "Invalid round 1 message: " + jClient.jIncoming.toJSONString());
|
||||
jClient.abort(Constants.JPAKE_ERROR_WRONGMESSAGE);
|
||||
return;
|
||||
}
|
||||
ExtendedJSONObject zkpPayload3;
|
||||
ExtendedJSONObject zkpPayload4;
|
||||
try {
|
||||
zkpPayload3 = iPayload.getObject(Constants.ZKP_KEY_ZKP_X1);
|
||||
zkpPayload4 = iPayload.getObject(Constants.ZKP_KEY_ZKP_X2);
|
||||
} catch (NonObjectJSONException e1) {
|
||||
jClient.abort(Constants.JPAKE_ERROR_INVALID);
|
||||
return;
|
||||
}
|
||||
|
||||
if (zkpPayload3 == null || zkpPayload4 == null) {
|
||||
Logger.error(LOG_TAG, "Invalid round 1 zkpPayload message");
|
||||
jClient.abort(Constants.JPAKE_ERROR_WRONGMESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!jClient.theirSignerId.equals(zkpPayload3.get(Constants.ZKP_KEY_ID)) ||
|
||||
!jClient.theirSignerId.equals(zkpPayload4.get(Constants.ZKP_KEY_ID))) {
|
||||
Logger.error(LOG_TAG, "Invalid round 1 zkpPayload message");
|
||||
jClient.abort(Constants.JPAKE_ERROR_WRONGMESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract message fields.
|
||||
jClient.jParty.gx3 = new BigInteger((String) iPayload.get(Constants.ZKP_KEY_GX1), 16);
|
||||
jClient.jParty.gx4 = new BigInteger((String) iPayload.get(Constants.ZKP_KEY_GX2), 16);
|
||||
|
||||
// Extract ZKPs.
|
||||
String zkp3_gr = (String) zkpPayload3.get(Constants.ZKP_KEY_GR);
|
||||
String zkp3_b = (String) zkpPayload3.get(Constants.ZKP_KEY_B);
|
||||
String zkp3_id = (String) zkpPayload3.get(Constants.ZKP_KEY_ID);
|
||||
|
||||
String zkp4_gr = (String) zkpPayload4.get(Constants.ZKP_KEY_GR);
|
||||
String zkp4_b = (String) zkpPayload4.get(Constants.ZKP_KEY_B);
|
||||
String zkp4_id = (String) zkpPayload4.get(Constants.ZKP_KEY_ID);
|
||||
|
||||
jClient.jParty.zkp3 = new Zkp(new BigInteger(zkp3_gr, 16), new BigInteger(zkp3_b, 16), zkp3_id);
|
||||
jClient.jParty.zkp4 = new Zkp(new BigInteger(zkp4_gr, 16), new BigInteger(zkp4_b, 16), zkp4_id);
|
||||
|
||||
// J-PAKE round 2.
|
||||
try {
|
||||
JPakeCrypto.round2(JPakeClient.secretAsBigInteger(jClient.secret), jClient.jParty, jClient.numGen);
|
||||
} catch (Gx3OrGx4IsZeroOrOneException e) {
|
||||
Logger.error(LOG_TAG, "gx3 and gx4 cannot equal 0 or 1.");
|
||||
jClient.abort(Constants.JPAKE_ERROR_INTERNAL);
|
||||
return;
|
||||
} catch (IncorrectZkpException e) {
|
||||
Logger.error(LOG_TAG, "ZKP mismatch");
|
||||
jClient.abort(Constants.JPAKE_ERROR_WRONGMESSAGE);
|
||||
return;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
Logger.error(LOG_TAG, "NoSuchAlgorithmException", e);
|
||||
jClient.abort(Constants.JPAKE_ERROR_INTERNAL);
|
||||
return;
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
Logger.error(LOG_TAG, "UnsupportedEncodingException", e);
|
||||
jClient.abort(Constants.JPAKE_ERROR_INTERNAL);
|
||||
return;
|
||||
}
|
||||
|
||||
// Make outgoing payload.
|
||||
Zkp zkpA = jClient.jParty.thisZkpA;
|
||||
ExtendedJSONObject oPayload = new ExtendedJSONObject();
|
||||
ExtendedJSONObject jZkpA = JPakeJson.makeJZkp(zkpA.gr, zkpA.b, zkpA.id);
|
||||
oPayload.put(Constants.ZKP_KEY_A, BigIntegerHelper.toEvenLengthHex(jClient.jParty.thisA));
|
||||
oPayload.put(Constants.ZKP_KEY_ZKP_A, jZkpA);
|
||||
|
||||
// Make outgoing message.
|
||||
jClient.jOutgoing = new ExtendedJSONObject();
|
||||
jClient.jOutgoing.put(Constants.JSON_KEY_TYPE, jClient.mySignerId + "2");
|
||||
jClient.jOutgoing.put(Constants.JSON_KEY_VERSION, JPakeClient.KEYEXCHANGE_VERSION);
|
||||
jClient.jOutgoing.put(Constants.JSON_KEY_PAYLOAD, oPayload);
|
||||
|
||||
jClient.runNextStage();
|
||||
}
|
||||
}
|
112
mobile/android/base/sync/jpake/stage/DecryptDataStage.java
Normal file
112
mobile/android/base/sync/jpake/stage/DecryptDataStage.java
Normal file
@ -0,0 +1,112 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.sync.jpake.stage;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.io.StringReader;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
||||
import org.json.simple.JSONObject;
|
||||
import org.json.simple.parser.JSONParser;
|
||||
import org.json.simple.parser.ParseException;
|
||||
import org.mozilla.gecko.sync.ExtendedJSONObject;
|
||||
import org.mozilla.gecko.sync.Logger;
|
||||
import org.mozilla.gecko.sync.NonObjectJSONException;
|
||||
import org.mozilla.gecko.sync.Utils;
|
||||
import org.mozilla.gecko.sync.crypto.CryptoException;
|
||||
import org.mozilla.gecko.sync.crypto.CryptoInfo;
|
||||
import org.mozilla.gecko.sync.crypto.KeyBundle;
|
||||
import org.mozilla.gecko.sync.jpake.JPakeClient;
|
||||
import org.mozilla.gecko.sync.setup.Constants;
|
||||
|
||||
public class DecryptDataStage extends JPakeStage {
|
||||
|
||||
@Override
|
||||
public void execute(JPakeClient jClient) {
|
||||
Logger.debug(LOG_TAG, "Decrypting their payload.");
|
||||
if (!(jClient.theirSignerId + "3").equals((String) jClient.jIncoming
|
||||
.get(Constants.JSON_KEY_TYPE))) {
|
||||
Logger.error(LOG_TAG, "Invalid round 3 data: " + jClient.jIncoming.toJSONString());
|
||||
jClient.abort(Constants.JPAKE_ERROR_WRONGMESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
// Decrypt payload and verify HMAC.
|
||||
Logger.debug(LOG_TAG, "Decrypting payload.");
|
||||
ExtendedJSONObject iPayload = null;
|
||||
try {
|
||||
iPayload = jClient.jIncoming.getObject(Constants.JSON_KEY_PAYLOAD);
|
||||
} catch (NonObjectJSONException e1) {
|
||||
Logger.error(LOG_TAG, "Invalid round 3 data.", e1);
|
||||
jClient.abort(Constants.JPAKE_ERROR_WRONGMESSAGE);
|
||||
return;
|
||||
}
|
||||
Logger.debug(LOG_TAG, "Decrypting data.");
|
||||
String cleartext = null;
|
||||
try {
|
||||
cleartext = new String(decryptPayload(iPayload, jClient.myKeyBundle), "UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
Logger.error(LOG_TAG, "Failed to decrypt data.", e);
|
||||
jClient.abort(Constants.JPAKE_ERROR_INTERNAL);
|
||||
return;
|
||||
} catch (CryptoException e) {
|
||||
Logger.error(LOG_TAG, "Failed to decrypt data.", e);
|
||||
jClient.abort(Constants.JPAKE_ERROR_KEYMISMATCH);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
jClient.jCreds = getJSONObject(cleartext);
|
||||
} catch (IOException e) {
|
||||
Logger.error(LOG_TAG, "I/O exception while creating JSON object.", e);
|
||||
jClient.abort(Constants.JPAKE_ERROR_INVALID);
|
||||
return;
|
||||
} catch (ParseException e) {
|
||||
Logger.error(LOG_TAG, "JSON parse error.", e);
|
||||
jClient.abort(Constants.JPAKE_ERROR_INVALID);
|
||||
return;
|
||||
}
|
||||
|
||||
jClient.runNextStage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for doing actual decryption.
|
||||
*
|
||||
* Input: JSONObject containing a valid payload (cipherText, IV, HMAC),
|
||||
* KeyBundle with keys for decryption. Output: byte[] clearText
|
||||
*
|
||||
* @throws CryptoException
|
||||
* @throws UnsupportedEncodingException
|
||||
*/
|
||||
private byte[] decryptPayload(ExtendedJSONObject payload, KeyBundle keybundle)
|
||||
throws CryptoException, UnsupportedEncodingException {
|
||||
|
||||
String sCiphertext = (String) payload.get(Constants.JSON_KEY_CIPHERTEXT);
|
||||
String sIv = (String) payload.get(Constants.JSON_KEY_IV);
|
||||
String sHmac = (String) payload.get(Constants.JSON_KEY_HMAC);
|
||||
|
||||
byte[] ciphertext = Utils.decodeBase64(sCiphertext);
|
||||
byte[] iv = Utils.decodeBase64(sIv);
|
||||
byte[] hmac = Utils.hex2Byte(sHmac);
|
||||
|
||||
CryptoInfo decrypted = CryptoInfo.decrypt(ciphertext, iv, hmac, keybundle);
|
||||
return decrypted.getMessage();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param jsonString
|
||||
* String to be packaged as JSON object.
|
||||
* @return JSONObject
|
||||
* @throws ParseException
|
||||
* @throws IOException
|
||||
* @throws Exception
|
||||
*/
|
||||
private JSONObject getJSONObject(String jsonString) throws IOException, ParseException{
|
||||
Reader in = new StringReader(jsonString);
|
||||
return (JSONObject) new JSONParser().parse(in);
|
||||
}
|
||||
}
|
137
mobile/android/base/sync/jpake/stage/GetChannelStage.java
Normal file
137
mobile/android/base/sync/jpake/stage/GetChannelStage.java
Normal file
@ -0,0 +1,137 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.sync.jpake.stage;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.security.GeneralSecurityException;
|
||||
|
||||
import org.mozilla.gecko.sync.Logger;
|
||||
import org.mozilla.gecko.sync.jpake.JPakeClient;
|
||||
import org.mozilla.gecko.sync.jpake.JPakeResponse;
|
||||
import org.mozilla.gecko.sync.net.BaseResource;
|
||||
import org.mozilla.gecko.sync.net.SyncResourceDelegate;
|
||||
import org.mozilla.gecko.sync.setup.Constants;
|
||||
|
||||
import ch.boye.httpclientandroidlib.HttpResponse;
|
||||
import ch.boye.httpclientandroidlib.client.ClientProtocolException;
|
||||
import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
|
||||
import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
|
||||
import ch.boye.httpclientandroidlib.message.BasicHeader;
|
||||
|
||||
public class GetChannelStage extends JPakeStage {
|
||||
|
||||
private interface GetChannelStageDelegate {
|
||||
public void handleSuccess(String channel);
|
||||
public void handleFailure(String error);
|
||||
public void handleError(Exception e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(final JPakeClient jClient) {
|
||||
Logger.debug(LOG_TAG, "Getting channel.");
|
||||
|
||||
// Make delegate to handle responses and propagate them to JPakeClient.
|
||||
GetChannelStageDelegate callbackDelegate = new GetChannelStageDelegate() {
|
||||
|
||||
@Override
|
||||
public void handleSuccess(String channel) {
|
||||
if (jClient.finished) {
|
||||
Logger.debug(LOG_TAG, "Finished; returning.");
|
||||
return;
|
||||
}
|
||||
|
||||
jClient.channelUrl = jClient.jpakeServer + channel;
|
||||
Logger.debug(LOG_TAG, "Using channel " + channel);
|
||||
jClient.makeAndDisplayPin(channel);
|
||||
|
||||
jClient.runNextStage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleFailure(String error) {
|
||||
jClient.abort(error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleError(Exception e) {
|
||||
Logger.error(LOG_TAG, "Threw HTTP exception.", e);
|
||||
jClient.abort(Constants.JPAKE_ERROR_CHANNEL);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
makeChannelRequest(callbackDelegate, jClient.jpakeServer + "new_channel", jClient.clientId);
|
||||
} catch (URISyntaxException e) {
|
||||
Logger.error(LOG_TAG, "Incorrect URI syntax.", e);
|
||||
jClient.abort(Constants.JPAKE_ERROR_INVALID);
|
||||
} catch (Exception e) {
|
||||
Logger.error(LOG_TAG, "Unexpected exception.", e);
|
||||
jClient.abort(Constants.JPAKE_ERROR_INTERNAL);
|
||||
}
|
||||
}
|
||||
|
||||
private void makeChannelRequest(final GetChannelStageDelegate callbackDelegate, String getChannelUrl, final String clientId) throws URISyntaxException {
|
||||
final BaseResource httpResource = new BaseResource(getChannelUrl);
|
||||
httpResource.delegate = new SyncResourceDelegate(httpResource) {
|
||||
|
||||
@Override
|
||||
public void addHeaders(HttpRequestBase request, DefaultHttpClient client) {
|
||||
request.setHeader(new BasicHeader("X-KeyExchange-Id", clientId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleHttpResponse(HttpResponse response) {
|
||||
|
||||
JPakeResponse res = new JPakeResponse(response);
|
||||
Object body = null;
|
||||
try {
|
||||
body = res.jsonBody();
|
||||
} catch (Exception e) {
|
||||
callbackDelegate.handleError(e);
|
||||
SyncResourceDelegate.consumeEntity(response.getEntity());
|
||||
return;
|
||||
}
|
||||
String channel = body instanceof String ? (String) body : null;
|
||||
if (channel == null) {
|
||||
callbackDelegate.handleFailure(Constants.JPAKE_ERROR_CHANNEL);
|
||||
SyncResourceDelegate.consumeEntity(response.getEntity());
|
||||
return;
|
||||
}
|
||||
callbackDelegate.handleSuccess(channel);
|
||||
// Clean up.
|
||||
SyncResourceDelegate.consumeEntity(response.getEntity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleHttpProtocolException(ClientProtocolException e) {
|
||||
callbackDelegate.handleError(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleHttpIOException(IOException e) {
|
||||
callbackDelegate.handleError(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleTransportException(GeneralSecurityException e) {
|
||||
callbackDelegate.handleError(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int connectionTimeout() {
|
||||
return JPakeClient.REQUEST_TIMEOUT;
|
||||
}
|
||||
};
|
||||
|
||||
// Make GET request.
|
||||
JPakeClient.runOnThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
httpResource.get();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
208
mobile/android/base/sync/jpake/stage/GetRequestStage.java
Normal file
208
mobile/android/base/sync/jpake/stage/GetRequestStage.java
Normal file
@ -0,0 +1,208 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.sync.jpake.stage;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
import org.mozilla.gecko.sync.Logger;
|
||||
import org.mozilla.gecko.sync.jpake.JPakeClient;
|
||||
import org.mozilla.gecko.sync.jpake.JPakeResponse;
|
||||
import org.mozilla.gecko.sync.net.BaseResource;
|
||||
import org.mozilla.gecko.sync.net.Resource;
|
||||
import org.mozilla.gecko.sync.net.SyncResourceDelegate;
|
||||
import org.mozilla.gecko.sync.setup.Constants;
|
||||
|
||||
import ch.boye.httpclientandroidlib.Header;
|
||||
import ch.boye.httpclientandroidlib.HttpResponse;
|
||||
import ch.boye.httpclientandroidlib.client.ClientProtocolException;
|
||||
import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
|
||||
import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
|
||||
import ch.boye.httpclientandroidlib.message.BasicHeader;
|
||||
|
||||
public class GetRequestStage extends JPakeStage {
|
||||
|
||||
private Timer timerScheduler = new Timer();
|
||||
private int pollTries;
|
||||
private GetStepTimerTask getStepTimerTask;
|
||||
|
||||
private interface GetRequestStageDelegate {
|
||||
public void handleSuccess(HttpResponse response);
|
||||
public void handleFailure(String error);
|
||||
public void handleError(Exception e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(final JPakeClient jClient) {
|
||||
Logger.debug(LOG_TAG, "Retrieving next message.");
|
||||
|
||||
final GetRequestStageDelegate callbackDelegate = new GetRequestStageDelegate() {
|
||||
|
||||
@Override
|
||||
public void handleSuccess(HttpResponse response) {
|
||||
if (jClient.finished) {
|
||||
Logger.debug(LOG_TAG, "Finished; returning.");
|
||||
return;
|
||||
}
|
||||
JPakeResponse res = new JPakeResponse(response);
|
||||
|
||||
Header etagHeader = response.getFirstHeader("etag");
|
||||
if (etagHeader == null) {
|
||||
Logger.error(LOG_TAG, "Server did not supply ETag.");
|
||||
jClient.abort(Constants.JPAKE_ERROR_SERVER);
|
||||
return;
|
||||
}
|
||||
|
||||
jClient.theirEtag = etagHeader.getValue();
|
||||
try {
|
||||
jClient.jIncoming = res.jsonObjectBody();
|
||||
} catch (Exception e) {
|
||||
Logger.error(LOG_TAG, "Illegal state.", e);
|
||||
jClient.abort(Constants.JPAKE_ERROR_INVALID);
|
||||
return;
|
||||
}
|
||||
Logger.debug(LOG_TAG, "incoming message: " + jClient.jIncoming.toJSONString());
|
||||
|
||||
jClient.runNextStage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleFailure(String error) {
|
||||
jClient.abort(error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleError(Exception e) {
|
||||
Logger.error(LOG_TAG, "Threw HTTP exception.", e);
|
||||
jClient.abort(Constants.JPAKE_ERROR_NETWORK);
|
||||
}
|
||||
};
|
||||
|
||||
Resource httpRequest;
|
||||
try {
|
||||
httpRequest = createGetRequest(callbackDelegate, jClient);
|
||||
} catch (URISyntaxException e) {
|
||||
Logger.error(LOG_TAG, "Incorrect URI syntax.", e);
|
||||
jClient.abort(Constants.JPAKE_ERROR_INVALID);
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.debug(LOG_TAG, "Scheduling GET request.");
|
||||
getStepTimerTask = new GetStepTimerTask(httpRequest);
|
||||
timerScheduler.schedule(getStepTimerTask, jClient.jpakePollInterval);
|
||||
}
|
||||
|
||||
private Resource createGetRequest(final GetRequestStageDelegate callbackDelegate, final JPakeClient jpakeClient) throws URISyntaxException {
|
||||
BaseResource httpResource = new BaseResource(jpakeClient.channelUrl);
|
||||
httpResource.delegate = new SyncResourceDelegate(httpResource) {
|
||||
|
||||
@Override
|
||||
public void addHeaders(HttpRequestBase request, DefaultHttpClient client) {
|
||||
request.setHeader(new BasicHeader("X-KeyExchange-Id", jpakeClient.clientId));
|
||||
if (jpakeClient.myEtag != null) {
|
||||
request.setHeader(new BasicHeader("If-None-Match", jpakeClient.myEtag));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleHttpResponse(HttpResponse response) {
|
||||
int statusCode = response.getStatusLine().getStatusCode();
|
||||
switch (statusCode) {
|
||||
case 200:
|
||||
jpakeClient.pollTries = 0; // Reset pollTries for next GET.
|
||||
callbackDelegate.handleSuccess(response);
|
||||
break;
|
||||
case 304:
|
||||
Logger.debug(LOG_TAG, "Channel hasn't been updated yet. Will try again later");
|
||||
if (pollTries >= jpakeClient.jpakeMaxTries) {
|
||||
Logger.error(LOG_TAG, "Tried for " + pollTries + " times, maxTries " + jpakeClient.jpakeMaxTries + ", aborting");
|
||||
callbackDelegate.handleFailure(Constants.JPAKE_ERROR_TIMEOUT);
|
||||
break;
|
||||
}
|
||||
jpakeClient.pollTries += 1;
|
||||
if (!jpakeClient.finished) {
|
||||
Logger.debug(LOG_TAG, "Scheduling next GET request.");
|
||||
scheduleGetRequest(jpakeClient.jpakePollInterval, jpakeClient);
|
||||
} else {
|
||||
Logger.debug(LOG_TAG, "Resetting pollTries");
|
||||
jpakeClient.pollTries = 0;
|
||||
}
|
||||
break;
|
||||
case 404:
|
||||
Logger.error(LOG_TAG, "No data found in channel.");
|
||||
callbackDelegate.handleFailure(Constants.JPAKE_ERROR_NODATA);
|
||||
break;
|
||||
case 412: // "Precondition failed"
|
||||
Logger.debug(LOG_TAG, "Message already replaced on server by other party.");
|
||||
callbackDelegate.handleSuccess(response);
|
||||
break;
|
||||
default:
|
||||
Logger.error(LOG_TAG, "Could not retrieve data. Server responded with HTTP " + statusCode);
|
||||
callbackDelegate.handleFailure(Constants.JPAKE_ERROR_SERVER);
|
||||
break;
|
||||
}
|
||||
// Clean up.
|
||||
SyncResourceDelegate.consumeEntity(response.getEntity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleHttpProtocolException(ClientProtocolException e) {
|
||||
callbackDelegate.handleError(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleHttpIOException(IOException e) {
|
||||
callbackDelegate.handleError(e);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleTransportException(GeneralSecurityException e) {
|
||||
callbackDelegate.handleError(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int connectionTimeout() {
|
||||
return JPakeClient.REQUEST_TIMEOUT;
|
||||
}
|
||||
};
|
||||
return httpResource;
|
||||
}
|
||||
|
||||
/**
|
||||
* TimerTask for use with delayed GET requests.
|
||||
*
|
||||
*/
|
||||
public class GetStepTimerTask extends TimerTask {
|
||||
private Resource request;
|
||||
|
||||
public GetStepTimerTask(Resource request) {
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
request.get();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Helper method to schedule a GET request with some delay.
|
||||
* Basically, run another GetRequestStage.
|
||||
*/
|
||||
private void scheduleGetRequest(int delay, final JPakeClient jClient) {
|
||||
timerScheduler.schedule(new TimerTask() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
new GetRequestStage().execute(jClient);
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
|
||||
}
|
12
mobile/android/base/sync/jpake/stage/JPakeStage.java
Normal file
12
mobile/android/base/sync/jpake/stage/JPakeStage.java
Normal file
@ -0,0 +1,12 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.sync.jpake.stage;
|
||||
|
||||
import org.mozilla.gecko.sync.jpake.JPakeClient;
|
||||
|
||||
public abstract class JPakeStage {
|
||||
protected final String LOG_TAG = "SyncJPakeStage";
|
||||
public abstract void execute(JPakeClient jClient);
|
||||
}
|
148
mobile/android/base/sync/jpake/stage/PutRequestStage.java
Normal file
148
mobile/android/base/sync/jpake/stage/PutRequestStage.java
Normal file
@ -0,0 +1,148 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.sync.jpake.stage;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
import org.mozilla.gecko.sync.Logger;
|
||||
import org.mozilla.gecko.sync.jpake.JPakeClient;
|
||||
import org.mozilla.gecko.sync.net.BaseResource;
|
||||
import org.mozilla.gecko.sync.net.Resource;
|
||||
import org.mozilla.gecko.sync.net.SyncResourceDelegate;
|
||||
import org.mozilla.gecko.sync.setup.Constants;
|
||||
|
||||
import ch.boye.httpclientandroidlib.Header;
|
||||
import ch.boye.httpclientandroidlib.HttpResponse;
|
||||
import ch.boye.httpclientandroidlib.client.ClientProtocolException;
|
||||
import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
|
||||
import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
|
||||
import ch.boye.httpclientandroidlib.message.BasicHeader;
|
||||
|
||||
public class PutRequestStage extends JPakeStage {
|
||||
|
||||
private interface PutRequestStageDelegate {
|
||||
public void handleSuccess(HttpResponse response);
|
||||
public void handleFailure(String error);
|
||||
public void handleError(Exception e);
|
||||
};
|
||||
|
||||
@Override
|
||||
public void execute(final JPakeClient jClient) {
|
||||
Logger.debug(LOG_TAG, "Upload message.");
|
||||
|
||||
// Create delegate.
|
||||
final PutRequestStageDelegate callbackDelegate = new PutRequestStageDelegate() {
|
||||
|
||||
@Override
|
||||
public void handleSuccess(HttpResponse response) {
|
||||
TimerTask runNextStage = new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
jClient.runNextStage();
|
||||
}
|
||||
};
|
||||
Timer timer = new Timer();
|
||||
|
||||
Logger.debug(LOG_TAG, "Pause for 2 * pollInterval before continuing.");
|
||||
// There's no point in returning early here since the next step will
|
||||
// always be a GET, so let's pause for twice the poll interval.
|
||||
timer.schedule(runNextStage, 2 * jClient.jpakePollInterval);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleFailure(String error) {
|
||||
jClient.abort(error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleError(Exception e) {
|
||||
Logger.error(LOG_TAG, "HTTP exception.", e);
|
||||
jClient.abort(Constants.JPAKE_ERROR_NETWORK);
|
||||
}
|
||||
};
|
||||
|
||||
// Create PUT request.
|
||||
Resource putRequest;
|
||||
try {
|
||||
putRequest = createPutRequest(callbackDelegate, jClient);
|
||||
} catch (URISyntaxException e) {
|
||||
Logger.error(LOG_TAG, "URISyntaxException", e);
|
||||
jClient.abort(Constants.JPAKE_ERROR_CHANNEL);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
putRequest.put(JPakeClient.jsonEntity(jClient.jOutgoing.object));
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
Logger.debug(LOG_TAG, "Outgoing message: " + jClient.jOutgoing.toJSONString());
|
||||
}
|
||||
|
||||
private Resource createPutRequest(final PutRequestStageDelegate callbackDelegate, final JPakeClient jpakeClient) throws URISyntaxException {
|
||||
BaseResource httpResource = new BaseResource(jpakeClient.channelUrl);
|
||||
httpResource.delegate = new SyncResourceDelegate(httpResource) {
|
||||
|
||||
@Override
|
||||
public void addHeaders(HttpRequestBase request, DefaultHttpClient client) {
|
||||
request.setHeader(new BasicHeader("X-KeyExchange-Id", jpakeClient.clientId));
|
||||
if (jpakeClient.theirEtag != null) {
|
||||
request.setHeader(new BasicHeader("If-Match", jpakeClient.theirEtag));
|
||||
} else {
|
||||
request.setHeader(new BasicHeader("If-None-Match", "*"));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleHttpResponse(HttpResponse response) {
|
||||
int statusCode = response.getStatusLine().getStatusCode();
|
||||
switch (statusCode) {
|
||||
case 200:
|
||||
Header etagHeader = response.getFirstHeader("etag");
|
||||
if (etagHeader == null) {
|
||||
Logger.error(LOG_TAG, "Server did not supply ETag.");
|
||||
callbackDelegate.handleFailure(Constants.JPAKE_ERROR_SERVER);
|
||||
SyncResourceDelegate.consumeEntity(response.getEntity());
|
||||
return;
|
||||
}
|
||||
jpakeClient.myEtag = etagHeader.getValue();
|
||||
callbackDelegate.handleSuccess(response);
|
||||
break;
|
||||
default:
|
||||
Logger.error(LOG_TAG, "Could not upload data. Server responded with HTTP " + statusCode);
|
||||
callbackDelegate.handleFailure(Constants.JPAKE_ERROR_SERVER);
|
||||
}
|
||||
SyncResourceDelegate.consumeEntity(response.getEntity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleHttpProtocolException(ClientProtocolException e) {
|
||||
callbackDelegate.handleError(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleHttpIOException(IOException e) {
|
||||
callbackDelegate.handleError(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleTransportException(GeneralSecurityException e) {
|
||||
callbackDelegate.handleError(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int connectionTimeout() {
|
||||
return JPakeClient.REQUEST_TIMEOUT;
|
||||
}
|
||||
|
||||
};
|
||||
return httpResource;
|
||||
}
|
||||
}
|
74
mobile/android/base/sync/jpake/stage/VerifyPairingStage.java
Normal file
74
mobile/android/base/sync/jpake/stage/VerifyPairingStage.java
Normal file
@ -0,0 +1,74 @@
|
||||
package org.mozilla.gecko.sync.jpake.stage;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
||||
import org.mozilla.apache.commons.codec.binary.Base64;
|
||||
import org.mozilla.gecko.sync.ExtendedJSONObject;
|
||||
import org.mozilla.gecko.sync.Logger;
|
||||
import org.mozilla.gecko.sync.NonObjectJSONException;
|
||||
import org.mozilla.gecko.sync.crypto.CryptoException;
|
||||
import org.mozilla.gecko.sync.crypto.CryptoInfo;
|
||||
import org.mozilla.gecko.sync.crypto.KeyBundle;
|
||||
import org.mozilla.gecko.sync.jpake.JPakeClient;
|
||||
import org.mozilla.gecko.sync.setup.Constants;
|
||||
|
||||
public class VerifyPairingStage extends JPakeStage {
|
||||
|
||||
@Override
|
||||
public void execute(JPakeClient jClient) {
|
||||
Logger.debug(LOG_TAG, "Verifying their key.");
|
||||
|
||||
ExtendedJSONObject verificationObj = jClient.jIncoming;
|
||||
String signerId = (String) verificationObj.get(Constants.JSON_KEY_TYPE);
|
||||
if (!signerId.equals(jClient.theirSignerId + "3")) {
|
||||
Logger.error(LOG_TAG, "Invalid round 3 message: " + verificationObj.toJSONString());
|
||||
jClient.abort(Constants.JPAKE_ERROR_WRONGMESSAGE);
|
||||
return;
|
||||
}
|
||||
ExtendedJSONObject payload;
|
||||
try {
|
||||
payload = verificationObj.getObject(Constants.JSON_KEY_PAYLOAD);
|
||||
} catch (NonObjectJSONException e) {
|
||||
Logger.error(LOG_TAG, "JSON exception.", e);
|
||||
jClient.abort(Constants.JPAKE_ERROR_INVALID);
|
||||
return;
|
||||
}
|
||||
String theirCiphertext = (String) payload.get(Constants.JSON_KEY_CIPHERTEXT);
|
||||
String iv = (String) payload.get(Constants.JSON_KEY_IV);
|
||||
boolean correctPairing;
|
||||
try {
|
||||
correctPairing = verifyCiphertext(theirCiphertext, iv, jClient.myKeyBundle);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
Logger.error(LOG_TAG, "Unsupported encoding.", e);
|
||||
jClient.abort(Constants.JPAKE_ERROR_INTERNAL);
|
||||
return;
|
||||
} catch (CryptoException e) {
|
||||
Logger.error(LOG_TAG, "Crypto exception.", e);
|
||||
jClient.abort(Constants.JPAKE_ERROR_INTERNAL);
|
||||
return;
|
||||
}
|
||||
if (correctPairing) {
|
||||
Logger.debug(LOG_TAG, "Keys verified successfully.");
|
||||
jClient.paired = true;
|
||||
jClient.onPaired();
|
||||
} else {
|
||||
Logger.error(LOG_TAG, "Keys don't match.");
|
||||
jClient.abort(Constants.JPAKE_ERROR_KEYMISMATCH);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Helper function to verify an incoming ciphertext and IV against derived
|
||||
* keyBundle.
|
||||
*
|
||||
* (Made 'public' for testing and is a stateless function.)
|
||||
*/
|
||||
|
||||
public boolean verifyCiphertext(String theirCiphertext, String iv,
|
||||
KeyBundle keyBundle) throws UnsupportedEncodingException, CryptoException {
|
||||
byte[] cleartextBytes = JPakeClient.JPAKE_VERIFY_VALUE.getBytes("UTF-8");
|
||||
CryptoInfo encrypted = CryptoInfo.encrypt(cleartextBytes, Base64.decodeBase64(iv), keyBundle);
|
||||
String myCiphertext = new String(Base64.encodeBase64(encrypted.getMessage()), "UTF-8");
|
||||
return myCiphertext.equals(theirCiphertext);
|
||||
}
|
||||
}
|
@ -1,40 +1,6 @@
|
||||
/* ***** BEGIN LICENSE BLOCK *****
|
||||
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
||||
*
|
||||
* The contents of this file are subject to the Mozilla Public License Version
|
||||
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
* http://www.mozilla.org/MPL/
|
||||
*
|
||||
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing rights and limitations under the
|
||||
* License.
|
||||
*
|
||||
* The Original Code is Android Sync Client.
|
||||
*
|
||||
* The Initial Developer of the Original Code is
|
||||
* the Mozilla Foundation.
|
||||
* Portions created by the Initial Developer are Copyright (C) 2011
|
||||
* the Initial Developer. All Rights Reserved.
|
||||
*
|
||||
* Contributor(s):
|
||||
* Chenxia Liu <liuche@mozilla.com>
|
||||
* Richard Newman <rnewman@mozilla.com>
|
||||
*
|
||||
* Alternatively, the contents of this file may be used under the terms of
|
||||
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
||||
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
||||
* in which case the provisions of the GPL or the LGPL are applicable instead
|
||||
* of those above. If you wish to allow use of your version of this file only
|
||||
* under the terms of either the GPL or the LGPL, and not to allow others to
|
||||
* use your version of this file under the terms of the MPL, indicate your
|
||||
* decision by deleting the provisions above and replace them with the notice
|
||||
* and other provisions required by the GPL or the LGPL. If you do not delete
|
||||
* the provisions above, a recipient may use your version of this file under
|
||||
* the terms of any one of the MPL, the GPL or the LGPL.
|
||||
*
|
||||
* ***** END LICENSE BLOCK ***** */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.sync.setup.activities;
|
||||
|
||||
@ -42,6 +8,7 @@ import java.util.HashMap;
|
||||
|
||||
import org.json.simple.JSONObject;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.sync.Logger;
|
||||
import org.mozilla.gecko.sync.jpake.JPakeClient;
|
||||
import org.mozilla.gecko.sync.jpake.JPakeNoActivePairingException;
|
||||
import org.mozilla.gecko.sync.setup.Constants;
|
||||
@ -58,7 +25,6 @@ import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
@ -67,9 +33,9 @@ import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
public class SetupSyncActivity extends AccountAuthenticatorActivity {
|
||||
private final static String LOG_TAG = "SetupSync";
|
||||
private final static String LOG_TAG = "SetupSync";
|
||||
|
||||
private boolean pairWithPin = false;
|
||||
private boolean pairWithPin = false;
|
||||
|
||||
// UI elements for pairing through PIN entry.
|
||||
private EditText row1;
|
||||
@ -93,25 +59,25 @@ public class SetupSyncActivity extends AccountAuthenticatorActivity {
|
||||
|
||||
public SetupSyncActivity() {
|
||||
super();
|
||||
Log.i(LOG_TAG, "SetupSyncActivity constructor called.");
|
||||
Logger.info(LOG_TAG, "SetupSyncActivity constructor called.");
|
||||
}
|
||||
|
||||
/** Called when the activity is first created. */
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
setTheme(R.style.SyncTheme);
|
||||
Log.i(LOG_TAG, "Called SetupSyncActivity.onCreate.");
|
||||
Logger.info(LOG_TAG, "Called SetupSyncActivity.onCreate.");
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Set Activity variables.
|
||||
mContext = getApplicationContext();
|
||||
Log.d(LOG_TAG, "AccountManager.get(" + mContext + ")");
|
||||
Logger.debug(LOG_TAG, "AccountManager.get(" + mContext + ")");
|
||||
mAccountManager = AccountManager.get(mContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
Log.i(LOG_TAG, "Called SetupSyncActivity.onResume.");
|
||||
Logger.info(LOG_TAG, "Called SetupSyncActivity.onResume.");
|
||||
super.onResume();
|
||||
|
||||
if (!hasInternet()) {
|
||||
@ -123,7 +89,12 @@ public class SetupSyncActivity extends AccountAuthenticatorActivity {
|
||||
Account[] accts = mAccountManager.getAccountsByType(Constants.ACCOUNTTYPE_SYNC);
|
||||
|
||||
if (accts.length == 0) { // Start J-PAKE for pairing if no accounts present.
|
||||
Logger.debug(LOG_TAG, "No accounts; starting J-PAKE receiver.");
|
||||
displayReceiveNoPin();
|
||||
if (jClient != null) {
|
||||
// Mark previous J-PAKE as finished. Don't bother propagating back up to this Activity.
|
||||
jClient.finished = true;
|
||||
}
|
||||
jClient = new JPakeClient(this);
|
||||
jClient.receiveNoPin();
|
||||
return;
|
||||
@ -132,13 +103,17 @@ public class SetupSyncActivity extends AccountAuthenticatorActivity {
|
||||
// Set layout based on starting Intent.
|
||||
Bundle extras = this.getIntent().getExtras();
|
||||
if (extras != null) {
|
||||
Logger.debug(LOG_TAG, "SetupSync with extras.");
|
||||
boolean isSetup = extras.getBoolean(Constants.INTENT_EXTRA_IS_SETUP);
|
||||
if (!isSetup) {
|
||||
Logger.debug(LOG_TAG, "Account exists; Pair a Device started.");
|
||||
pairWithPin = true;
|
||||
displayPairWithPin();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Logger.debug(LOG_TAG, "Only one account supported. Redirecting.");
|
||||
// Display toast for "Only one account supported." and redirect to account management.
|
||||
Toast toast = Toast.makeText(mContext, R.string.sync_notification_oneaccount, Toast.LENGTH_LONG);
|
||||
toast.show();
|
||||
@ -177,7 +152,7 @@ public class SetupSyncActivity extends AccountAuthenticatorActivity {
|
||||
}
|
||||
|
||||
public void connectClickHandler(View target) {
|
||||
Log.d(LOG_TAG, "Connect clicked.");
|
||||
Logger.debug(LOG_TAG, "Connect clicked.");
|
||||
// Set UI feedback.
|
||||
pinError.setVisibility(View.INVISIBLE);
|
||||
enablePinEntry(false);
|
||||
@ -189,11 +164,19 @@ public class SetupSyncActivity extends AccountAuthenticatorActivity {
|
||||
pin += row2.getText().toString() + row3.getText().toString();
|
||||
|
||||
// Start J-PAKE.
|
||||
Log.d(LOG_TAG, "Starting J-PAKE...");
|
||||
if (jClient != null) {
|
||||
// Cancel previous J-PAKE exchange.
|
||||
jClient.finished = true;
|
||||
}
|
||||
jClient = new JPakeClient(this);
|
||||
jClient.pairWithPin(pin, false);
|
||||
jClient.pairWithPin(pin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler when "Show me how" link is clicked.
|
||||
* @param target
|
||||
* View that received the click.
|
||||
*/
|
||||
public void showClickHandler(View target) {
|
||||
Uri uri = null;
|
||||
// TODO: fetch these from fennec
|
||||
@ -206,9 +189,15 @@ public class SetupSyncActivity extends AccountAuthenticatorActivity {
|
||||
}
|
||||
|
||||
/* Controller methods */
|
||||
|
||||
/**
|
||||
* Display generated PIN to user.
|
||||
* @param pin
|
||||
* 12-character string generated for J-PAKE.
|
||||
*/
|
||||
public void displayPin(String pin) {
|
||||
if (pin == null) {
|
||||
Log.w(LOG_TAG, "Asked to display null pin.");
|
||||
Logger.warn(LOG_TAG, "Asked to display null pin.");
|
||||
return;
|
||||
}
|
||||
// Format PIN for display.
|
||||
@ -224,7 +213,7 @@ public class SetupSyncActivity extends AccountAuthenticatorActivity {
|
||||
TextView view2 = pinTextView2;
|
||||
TextView view3 = pinTextView3;
|
||||
if (view1 == null || view2 == null || view3 == null) {
|
||||
Log.w(LOG_TAG, "Couldn't find view to display PIN.");
|
||||
Logger.warn(LOG_TAG, "Couldn't find view to display PIN.");
|
||||
return;
|
||||
}
|
||||
view1.setText(pin1);
|
||||
@ -234,12 +223,17 @@ public class SetupSyncActivity extends AccountAuthenticatorActivity {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Abort current J-PAKE pairing. Clear forms/restart pairing.
|
||||
* @param error
|
||||
*/
|
||||
public void displayAbort(String error) {
|
||||
if (!Constants.JPAKE_ERROR_USERABORT.equals(error) && !hasInternet()) {
|
||||
setContentView(R.layout.sync_setup_nointernet);
|
||||
return;
|
||||
}
|
||||
if (pairWithPin) {
|
||||
// Clear PIN entries and display error.
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
@ -257,7 +251,7 @@ public class SetupSyncActivity extends AccountAuthenticatorActivity {
|
||||
}
|
||||
|
||||
// Start new JPakeClient for restarting J-PAKE.
|
||||
Log.d(LOG_TAG, "abort reason: " + error);
|
||||
Logger.debug(LOG_TAG, "abort reason: " + error);
|
||||
if (!Constants.JPAKE_ERROR_USERABORT.equals(error)) {
|
||||
jClient = new JPakeClient(this);
|
||||
runOnUiThread(new Runnable() {
|
||||
@ -285,7 +279,7 @@ public class SetupSyncActivity extends AccountAuthenticatorActivity {
|
||||
fields.put(Constants.JSON_KEY_PASSWORD, password);
|
||||
fields.put(Constants.JSON_KEY_SERVER, serverURL);
|
||||
|
||||
Log.d(LOG_TAG, "Extracted account data: " + jAccount.toJSONString());
|
||||
Logger.debug(LOG_TAG, "Extracted account data: " + jAccount.toJSONString());
|
||||
return jAccount;
|
||||
}
|
||||
|
||||
@ -294,21 +288,11 @@ public class SetupSyncActivity extends AccountAuthenticatorActivity {
|
||||
* link to a Sync account. Display "waiting for other device" dialog.
|
||||
*/
|
||||
public void onPaired() {
|
||||
if (!pairWithPin) {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
setContentView(R.layout.sync_setup_jpake_waiting);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract Sync account data.
|
||||
Account[] accts = mAccountManager.getAccountsByType(Constants.ACCOUNTTYPE_SYNC);
|
||||
if (accts.length == 0) {
|
||||
// Error, no account present.
|
||||
Log.e(LOG_TAG, "No accounts present.");
|
||||
Logger.error(LOG_TAG, "No accounts present.");
|
||||
displayAbort(Constants.JPAKE_ERROR_INVALID);
|
||||
return;
|
||||
}
|
||||
@ -324,8 +308,8 @@ public class SetupSyncActivity extends AccountAuthenticatorActivity {
|
||||
try {
|
||||
jClient.sendAndComplete(jAccount);
|
||||
} catch (JPakeNoActivePairingException e) {
|
||||
Log.e(LOG_TAG, "No active J-PAKE pairing.", e);
|
||||
// TODO: some user-visible action!
|
||||
Logger.error(LOG_TAG, "No active J-PAKE pairing.", e);
|
||||
displayAbort(Constants.JPAKE_ERROR_INVALID);
|
||||
}
|
||||
}
|
||||
|
||||
@ -334,8 +318,14 @@ public class SetupSyncActivity extends AccountAuthenticatorActivity {
|
||||
* pairing, does not require UI feedback to user.
|
||||
*/
|
||||
public void onPairingStart() {
|
||||
if (pairWithPin) {
|
||||
// TODO: add in functionality if/when adding pairWithPIN.
|
||||
if (!pairWithPin) {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
setContentView(R.layout.sync_setup_jpake_waiting);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@ -352,7 +342,7 @@ public class SetupSyncActivity extends AccountAuthenticatorActivity {
|
||||
String syncKey = (String) jCreds.get(Constants.JSON_KEY_SYNCKEY);
|
||||
String serverURL = (String) jCreds.get(Constants.JSON_KEY_SERVER);
|
||||
|
||||
Log.d(LOG_TAG, "Using account manager " + mAccountManager);
|
||||
Logger.debug(LOG_TAG, "Using account manager " + mAccountManager);
|
||||
final Intent intent = AccountActivity.createAccount(mContext, mAccountManager,
|
||||
accountName,
|
||||
syncKey, password, serverURL);
|
||||
@ -416,20 +406,20 @@ public class SetupSyncActivity extends AccountAuthenticatorActivity {
|
||||
}
|
||||
|
||||
private boolean hasInternet() {
|
||||
Log.d(LOG_TAG, "Checking internet connectivity.");
|
||||
Logger.debug(LOG_TAG, "Checking internet connectivity.");
|
||||
ConnectivityManager connManager = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
NetworkInfo wifi = connManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
|
||||
NetworkInfo mobile = connManager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE);
|
||||
|
||||
if (wifi.isConnected() || mobile.isConnected()) {
|
||||
Log.d(LOG_TAG, "Internet connected.");
|
||||
Logger.debug(LOG_TAG, "Internet connected.");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void displayPairWithPin() {
|
||||
Log.d(LOG_TAG, "PairWithPin initiated.");
|
||||
Logger.debug(LOG_TAG, "PairWithPin initiated.");
|
||||
setContentView(R.layout.sync_setup_pair);
|
||||
connectButton = (Button) findViewById(R.id.pair_button_connect);
|
||||
pinError = (LinearLayout) findViewById(R.id.pair_error);
|
||||
@ -455,8 +445,8 @@ public class SetupSyncActivity extends AccountAuthenticatorActivity {
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
row2.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
@ -474,7 +464,6 @@ public class SetupSyncActivity extends AccountAuthenticatorActivity {
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
row3.addTextChangedListener(new TextWatcher() {
|
||||
@ -491,12 +480,11 @@ public class SetupSyncActivity extends AccountAuthenticatorActivity {
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
private void displayReceiveNoPin() {
|
||||
Log.d(LOG_TAG, "ReceiveNoPin initiated");
|
||||
Logger.debug(LOG_TAG, "ReceiveNoPin initiated");
|
||||
setContentView(R.layout.sync_setup);
|
||||
|
||||
// Set up UI.
|
||||
@ -509,13 +497,13 @@ public class SetupSyncActivity extends AccountAuthenticatorActivity {
|
||||
|
||||
// UI checks.
|
||||
if (setupTitleView == null) {
|
||||
Log.e(LOG_TAG, "No title view.");
|
||||
Logger.error(LOG_TAG, "No title view.");
|
||||
}
|
||||
if (setupSubtitleView == null) {
|
||||
Log.e(LOG_TAG, "No subtitle view.");
|
||||
Logger.error(LOG_TAG, "No subtitle view.");
|
||||
}
|
||||
if (setupNoDeviceLinkTitleView == null) {
|
||||
Log.e(LOG_TAG, "No 'no device' link view.");
|
||||
Logger.error(LOG_TAG, "No 'no device' link view.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user