package org.mozilla.android.sync.test; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.math.BigInteger; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import org.json.simple.parser.ParseException; import org.junit.Test; import org.mozilla.apache.commons.codec.binary.Base64; import org.mozilla.gecko.sync.ExtendedJSONObject; import org.mozilla.gecko.sync.NonObjectJSONException; import org.mozilla.gecko.sync.crypto.CryptoException; import org.mozilla.gecko.sync.crypto.KeyBundle; 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.JPakeNumGenerator; import org.mozilla.gecko.sync.jpake.JPakeNumGeneratorRandom; import org.mozilla.gecko.sync.jpake.JPakeParty; import org.mozilla.gecko.sync.jpake.stage.ComputeKeyVerificationStage; import org.mozilla.gecko.sync.jpake.stage.VerifyPairingStage; import org.mozilla.gecko.sync.setup.Constants; public class TestJPakeSetup { // Note: will throw NullPointerException if aborts. Only use stateless public // methods. @Test public void testGx3OrGx4ZeroOrOneThrowsException() throws UnsupportedEncodingException { JPakeNumGeneratorRandom gen = new JPakeNumGeneratorRandom(); JPakeParty p = new JPakeParty("foobar"); BigInteger secret = JPakeClient.secretAsBigInteger("secret"); p.gx4 = new BigInteger("2"); p.gx3 = new BigInteger("0"); try { JPakeCrypto.round2(secret, p, gen); fail("round2 should fail if gx3 == 0"); } catch (Gx3OrGx4IsZeroOrOneException e) { // Hurrah. } catch (Exception e) { fail("Unexpected exception " + e); } p.gx3 = new BigInteger("1"); try { JPakeCrypto.round2(secret, p, gen); fail("round2 should fail if gx3 == 1"); } catch (Gx3OrGx4IsZeroOrOneException e) { // Hurrah. } catch (Exception e) { fail("Unexpected exception " + e); } p.gx3 = new BigInteger("3"); try { JPakeCrypto.round2(secret, p, gen); } catch (Gx3OrGx4IsZeroOrOneException e) { fail("Unexpected exception " + e); } catch (Exception e) { // There are plenty of other reasons this should fail. } p.gx3 = new BigInteger("2"); p.gx4 = new BigInteger("0"); try { JPakeCrypto.round2(secret, p, gen); fail("round2 should fail if gx4 == 0"); } catch (Gx3OrGx4IsZeroOrOneException e) { // Hurrah. } catch (Exception e) { fail("Unexpected exception " + e); } p.gx4 = new BigInteger("1"); try { JPakeCrypto.round2(secret, p, gen); fail("round2 should fail if gx4 == 1"); } catch (Gx3OrGx4IsZeroOrOneException e) { // Hurrah. } catch (Exception e) { fail("Unexpected exception " + e); } p.gx4 = new BigInteger("3"); try { JPakeCrypto.round2(secret, p, gen); } catch (Gx3OrGx4IsZeroOrOneException e) { fail("Unexpected exception " + e); } catch (Exception e) { // There are plenty of other reasons this should fail. } } /* * Tests encryption key and hmac generation from a derived key, using values * taken from a successful run of J-PAKE. */ @Test public void testKeyDerivation() throws UnsupportedEncodingException { String keyChars16 = "811565455b22c857a3e303d1f48ff72ae9ef42d9c3fe3740ce7772cb5bfe23491dd5b7ee5af4828ab9b7d5844866f378b4cf0156810aff0504ef2947402e8e40be1179cf7f37b231bc0db9e4e1bb239c849aa5c12ed2b0b4413017599270aae71ee993dd755ee8c045c5fe03d713894692bf72158d9835ad905442edfd8235e1d0c915053debfc49d8248e4dae16608743aef5dab061f49fd6edd0b93ecdf9feafcbe47eb7e6c3678356d96e9bcd87814b13b9eb1a791fd446d69cb040ec7d7194031267e26f266ee3decbc1a85c5203427361997adf9823fbffe16af9946f1347c5354956356732e436ef5f8307e96554cf69a54e4e8a78552e3f506e9310a1c4438d3ddce44a37482270533e47fc40dc84abfe39c1f95328d0d2540074f6301d4f121c2f0eac49c47a2c430614234ca26dede2a429e2fdb6d282a85174886c3a68c3cf5edc87ccb82af4ae4a9a26fffadc7f4d8ded4ff47b3d2d171f374b230e52e6b45963d3a0a6b20cbe6a440fd4a932279d52a6fd7694b4cbc0cb67ff3c"; String expectedEncKey64 = "3TXwVlWf6YbuIPcg8m/2U4UXYV4a8RNu6pE2GOVkJJo="; String expectedHmac64 = "L49fnEPAD31G5uEKy5e4bGZ6IF3G/62qW6Ua/1NvBeQ="; byte[] encKeyBytes = new byte[32]; byte[] hmacBytes = new byte[32]; try { JPakeCrypto.generateKeyAndHmac(new BigInteger(keyChars16, 16), encKeyBytes, hmacBytes); } catch (Exception e) { fail("Unexpected exception " + e); } String encKey64 = new String(Base64.encodeBase64(encKeyBytes)); String hmac64 = new String(Base64.encodeBase64(hmacBytes)); assertTrue(expectedEncKey64.equals(encKey64)); assertTrue(expectedHmac64.equals(hmac64)); } /* * Test correct key derivation when both parties share a secret. */ @Test public void testJPakeCorrectSecret() throws Gx3OrGx4IsZeroOrOneException, IncorrectZkpException, IOException, ParseException, NonObjectJSONException, CryptoException, NoSuchAlgorithmException, InvalidKeyException { BigInteger secret = JPakeClient.secretAsBigInteger("byubd7u75qmq"); JPakeNumGenerator gen = new JPakeNumGeneratorRandom(); // Keys derived should be the same. assertTrue(jPakeDeriveSameKey(gen, gen, secret, secret)); } /* * Test incorrect key derivation when parties do not share the same secret. */ @Test public void testJPakeIncorrectSecret() throws Gx3OrGx4IsZeroOrOneException, IncorrectZkpException, IOException, ParseException, NonObjectJSONException, CryptoException, NoSuchAlgorithmException, InvalidKeyException { BigInteger secret1 = JPakeClient.secretAsBigInteger("shareSecret1"); BigInteger secret2 = JPakeClient.secretAsBigInteger("shareSecret2"); JPakeNumGenerator gen = new JPakeNumGeneratorRandom(); // Unsuccessful key derivation. assertFalse(jPakeDeriveSameKey(gen, gen, secret1, secret2)); } /* * Helper simulation of a J-PAKE key derivation between two parties, with * secret1 and secret2. Both parties are assumed to be communicating on the * same channel; otherwise, J-PAKE would have failed immediately. */ public boolean jPakeDeriveSameKey(JPakeNumGenerator gen1, JPakeNumGenerator gen2, BigInteger secret1, BigInteger secret2) throws IncorrectZkpException, Gx3OrGx4IsZeroOrOneException, IOException, ParseException, NonObjectJSONException, CryptoException, NoSuchAlgorithmException, InvalidKeyException { // Communicating parties. JPakeParty party1 = new JPakeParty("party1"); JPakeParty party2 = new JPakeParty("party2"); JPakeCrypto.round1(party1, gen1); // After party1 round 1, these values should no longer be null. assertNotNull(party1.signerId); assertNotNull(party1.x2); assertNotNull(party1.gx1); assertNotNull(party1.gx2); assertNotNull(party1.zkp1); assertNotNull(party1.zkp2); assertNotNull(party1.zkp1.b); assertNotNull(party1.zkp1.gr); assertNotNull(party1.zkp1.id); assertNotNull(party1.zkp2.b); assertNotNull(party1.zkp2.gr); assertNotNull(party1.zkp2.id); // party2 receives the following values from party1. party2.gx3 = party1.gx1; party2.gx4 = party1.gx2; party2.zkp3 = party1.zkp1; party2.zkp4 = party1.zkp2; // TODO Run JPakeClient checks. JPakeCrypto.round1(party2, gen2); // After party2 round 1, these values should no longer be null. assertNotNull(party2.signerId); assertNotNull(party2.x2); assertNotNull(party2.gx1); assertNotNull(party2.gx2); assertNotNull(party2.zkp1); assertNotNull(party2.zkp2); assertNotNull(party2.zkp1.b); assertNotNull(party2.zkp1.gr); assertNotNull(party2.zkp1.id); assertNotNull(party2.zkp2.b); assertNotNull(party2.zkp2.gr); assertNotNull(party2.zkp2.id); // Pass relevant values to party1. party1.gx3 = party2.gx1; party1.gx4 = party2.gx2; party1.zkp3 = party2.zkp1; party1.zkp4 = party2.zkp2; // TODO Run JPakeClient checks. JPakeCrypto.round2(secret1, party1, gen1); // After party1 round 2, these values should no longer be null. assertNotNull(party1.thisA); assertNotNull(party1.thisZkpA); assertNotNull(party1.thisZkpA.b); assertNotNull(party1.thisZkpA.gr); assertNotNull(party1.thisZkpA.id); // Pass relevant values to party2. party2.otherA = party1.thisA; party2.otherZkpA = party1.thisZkpA; JPakeCrypto.round2(secret2, party2, gen2); // Check for nulls. assertNotNull(party2.thisA); assertNotNull(party2.thisZkpA); assertNotNull(party2.thisZkpA.b); assertNotNull(party2.thisZkpA.gr); assertNotNull(party2.thisZkpA.id); // Pass values to party1. party1.otherA = party2.thisA; party1.otherZkpA = party2.thisZkpA; KeyBundle keyBundle1 = JPakeCrypto.finalRound(secret1, party1); assertNotNull(keyBundle1); // party1 computes the shared key, generates an encrypted message to party2. ExtendedJSONObject verificationMsg = new ComputeKeyVerificationStage() .computeKeyVerification(keyBundle1, party1.signerId); ExtendedJSONObject payload = verificationMsg .getObject(Constants.JSON_KEY_PAYLOAD); String ciphertext1 = (String) payload.get(Constants.JSON_KEY_CIPHERTEXT); String iv1 = (String) payload.get(Constants.JSON_KEY_IV); // party2 computes the key as well, using its copy of the secret. KeyBundle keyBundle2 = JPakeCrypto.finalRound(secret2, party2); // party2 fetches the encrypted message and verifies the pairing against its // own derived key. boolean isSuccess = new VerifyPairingStage().verifyCiphertext(ciphertext1, iv1, keyBundle2); return isSuccess; } }