diff options
Diffstat (limited to 'libraries/spongycastle/core/src/main/java/org/spongycastle/crypto/agreement/jpake/JPAKEUtil.java')
-rw-r--r-- | libraries/spongycastle/core/src/main/java/org/spongycastle/crypto/agreement/jpake/JPAKEUtil.java | 508 |
1 files changed, 508 insertions, 0 deletions
diff --git a/libraries/spongycastle/core/src/main/java/org/spongycastle/crypto/agreement/jpake/JPAKEUtil.java b/libraries/spongycastle/core/src/main/java/org/spongycastle/crypto/agreement/jpake/JPAKEUtil.java new file mode 100644 index 000000000..aa65ef8ec --- /dev/null +++ b/libraries/spongycastle/core/src/main/java/org/spongycastle/crypto/agreement/jpake/JPAKEUtil.java @@ -0,0 +1,508 @@ +package org.spongycastle.crypto.agreement.jpake; + +import java.math.BigInteger; +import java.security.SecureRandom; + +import org.spongycastle.crypto.CryptoException; +import org.spongycastle.crypto.Digest; +import org.spongycastle.crypto.Mac; +import org.spongycastle.crypto.macs.HMac; +import org.spongycastle.crypto.params.KeyParameter; +import org.spongycastle.util.Arrays; +import org.spongycastle.util.BigIntegers; +import org.spongycastle.util.Strings; + +/** + * Primitives needed for a J-PAKE exchange. + * <p/> + * <p/> + * The recommended way to perform a J-PAKE exchange is by using + * two {@link JPAKEParticipant}s. Internally, those participants + * call these primitive operations in {@link JPAKEUtil}. + * <p/> + * <p/> + * The primitives, however, can be used without a {@link JPAKEParticipant} + * if needed. + */ +public class JPAKEUtil +{ + static final BigInteger ZERO = BigInteger.valueOf(0); + static final BigInteger ONE = BigInteger.valueOf(1); + + /** + * Return a value that can be used as x1 or x3 during round 1. + * <p/> + * <p/> + * The returned value is a random value in the range <tt>[0, q-1]</tt>. + */ + public static BigInteger generateX1( + BigInteger q, + SecureRandom random) + { + BigInteger min = ZERO; + BigInteger max = q.subtract(ONE); + return BigIntegers.createRandomInRange(min, max, random); + } + + /** + * Return a value that can be used as x2 or x4 during round 1. + * <p/> + * <p/> + * The returned value is a random value in the range <tt>[1, q-1]</tt>. + */ + public static BigInteger generateX2( + BigInteger q, + SecureRandom random) + { + BigInteger min = ONE; + BigInteger max = q.subtract(ONE); + return BigIntegers.createRandomInRange(min, max, random); + } + + /** + * Converts the given password to a {@link BigInteger} + * for use in arithmetic calculations. + */ + public static BigInteger calculateS(char[] password) + { + return new BigInteger(Strings.toUTF8ByteArray(password)); + } + + /** + * Calculate g^x mod p as done in round 1. + */ + public static BigInteger calculateGx( + BigInteger p, + BigInteger g, + BigInteger x) + { + return g.modPow(x, p); + } + + + /** + * Calculate ga as done in round 2. + */ + public static BigInteger calculateGA( + BigInteger p, + BigInteger gx1, + BigInteger gx3, + BigInteger gx4) + { + // ga = g^(x1+x3+x4) = g^x1 * g^x3 * g^x4 + return gx1.multiply(gx3).multiply(gx4).mod(p); + } + + + /** + * Calculate x2 * s as done in round 2. + */ + public static BigInteger calculateX2s( + BigInteger q, + BigInteger x2, + BigInteger s) + { + return x2.multiply(s).mod(q); + } + + + /** + * Calculate A as done in round 2. + */ + public static BigInteger calculateA( + BigInteger p, + BigInteger q, + BigInteger gA, + BigInteger x2s) + { + // A = ga^(x*s) + return gA.modPow(x2s, p); + } + + /** + * Calculate a zero knowledge proof of x using Schnorr's signature. + * The returned array has two elements {g^v, r = v-x*h} for x. + */ + public static BigInteger[] calculateZeroKnowledgeProof( + BigInteger p, + BigInteger q, + BigInteger g, + BigInteger gx, + BigInteger x, + String participantId, + Digest digest, + SecureRandom random) + { + BigInteger[] zeroKnowledgeProof = new BigInteger[2]; + + /* Generate a random v, and compute g^v */ + BigInteger vMin = ZERO; + BigInteger vMax = q.subtract(ONE); + BigInteger v = BigIntegers.createRandomInRange(vMin, vMax, random); + + BigInteger gv = g.modPow(v, p); + BigInteger h = calculateHashForZeroKnowledgeProof(g, gv, gx, participantId, digest); // h + + zeroKnowledgeProof[0] = gv; + zeroKnowledgeProof[1] = v.subtract(x.multiply(h)).mod(q); // r = v-x*h + + return zeroKnowledgeProof; + } + + private static BigInteger calculateHashForZeroKnowledgeProof( + BigInteger g, + BigInteger gr, + BigInteger gx, + String participantId, + Digest digest) + { + digest.reset(); + + updateDigestIncludingSize(digest, g); + + updateDigestIncludingSize(digest, gr); + + updateDigestIncludingSize(digest, gx); + + updateDigestIncludingSize(digest, participantId); + + byte[] output = new byte[digest.getDigestSize()]; + digest.doFinal(output, 0); + + return new BigInteger(output); + } + + /** + * Validates that g^x4 is not 1. + * + * @throws CryptoException if g^x4 is 1 + */ + public static void validateGx4(BigInteger gx4) + throws CryptoException + { + if (gx4.equals(ONE)) + { + throw new CryptoException("g^x validation failed. g^x should not be 1."); + } + } + + /** + * Validates that ga is not 1. + * <p/> + * <p/> + * As described by Feng Hao... + * <p/> + * <blockquote> + * Alice could simply check ga != 1 to ensure it is a generator. + * In fact, as we will explain in Section 3, (x1 + x3 + x4 ) is random over Zq even in the face of active attacks. + * Hence, the probability for ga = 1 is extremely small - on the order of 2^160 for 160-bit q. + * </blockquote> + * + * @throws CryptoException if ga is 1 + */ + public static void validateGa(BigInteger ga) + throws CryptoException + { + if (ga.equals(ONE)) + { + throw new CryptoException("ga is equal to 1. It should not be. The chances of this happening are on the order of 2^160 for a 160-bit q. Try again."); + } + } + + /** + * Validates the zero knowledge proof (generated by + * {@link #calculateZeroKnowledgeProof(BigInteger, BigInteger, BigInteger, BigInteger, BigInteger, String, Digest, SecureRandom)}) + * is correct. + * + * @throws CryptoException if the zero knowledge proof is not correct + */ + public static void validateZeroKnowledgeProof( + BigInteger p, + BigInteger q, + BigInteger g, + BigInteger gx, + BigInteger[] zeroKnowledgeProof, + String participantId, + Digest digest) + throws CryptoException + { + + /* sig={g^v,r} */ + BigInteger gv = zeroKnowledgeProof[0]; + BigInteger r = zeroKnowledgeProof[1]; + + BigInteger h = calculateHashForZeroKnowledgeProof(g, gv, gx, participantId, digest); + if (!(gx.compareTo(ZERO) == 1 && // g^x > 0 + gx.compareTo(p) == -1 && // g^x < p + gx.modPow(q, p).compareTo(ONE) == 0 && // g^x^q mod q = 1 + /* + * Below, I took an straightforward way to compute g^r * g^x^h, + * which needs 2 exp. Using a simultaneous computation technique + * would only need 1 exp. + */ + g.modPow(r, p).multiply(gx.modPow(h, p)).mod(p).compareTo(gv) == 0)) // g^v=g^r * g^x^h + { + throw new CryptoException("Zero-knowledge proof validation failed"); + } + } + + /** + * Calculates the keying material, which can be done after round 2 has completed. + * A session key must be derived from this key material using a secure key derivation function (KDF). + * The KDF used to derive the key is handled externally (i.e. not by {@link JPAKEParticipant}). + * <p/> + * <p/> + * <pre> + * KeyingMaterial = (B/g^{x2*x4*s})^x2 + * </pre> + */ + public static BigInteger calculateKeyingMaterial( + BigInteger p, + BigInteger q, + BigInteger gx4, + BigInteger x2, + BigInteger s, + BigInteger B) + { + return gx4.modPow(x2.multiply(s).negate().mod(q), p).multiply(B).modPow(x2, p); + } + + /** + * Validates that the given participant ids are not equal. + * (For the J-PAKE exchange, each participant must use a unique id.) + * + * @throws CryptoException if the participantId strings are equal. + */ + public static void validateParticipantIdsDiffer(String participantId1, String participantId2) + throws CryptoException + { + if (participantId1.equals(participantId2)) + { + throw new CryptoException( + "Both participants are using the same participantId (" + + participantId1 + + "). This is not allowed. " + + "Each participant must use a unique participantId."); + } + } + + /** + * Validates that the given participant ids are equal. + * This is used to ensure that the payloads received from + * each round all come from the same participant. + * + * @throws CryptoException if the participantId strings are equal. + */ + public static void validateParticipantIdsEqual(String expectedParticipantId, String actualParticipantId) + throws CryptoException + { + if (!expectedParticipantId.equals(actualParticipantId)) + { + throw new CryptoException( + "Received payload from incorrect partner (" + + actualParticipantId + + "). Expected to receive payload from " + + expectedParticipantId + + "."); + } + } + + /** + * Validates that the given object is not null. + * + * @param object object in question + * @param description name of the object (to be used in exception message) + * @throws NullPointerException if the object is null. + */ + public static void validateNotNull(Object object, String description) + { + if (object == null) + { + throw new NullPointerException(description + " must not be null"); + } + } + + /** + * Calculates the MacTag (to be used for key confirmation), as defined by + * <a href="http://csrc.nist.gov/publications/nistpubs/800-56A/SP800-56A_Revision1_Mar08-2007.pdf">NIST SP 800-56A Revision 1</a>, + * Section 8.2 Unilateral Key Confirmation for Key Agreement Schemes. + * <p/> + * <p/> + * <pre> + * MacTag = HMAC(MacKey, MacLen, MacData) + * + * MacKey = H(K || "JPAKE_KC") + * + * MacData = "KC_1_U" || participantId || partnerParticipantId || gx1 || gx2 || gx3 || gx4 + * + * Note that both participants use "KC_1_U" because the sender of the round 3 message + * is always the initiator for key confirmation. + * + * HMAC = {@link HMac} used with the given {@link Digest} + * H = The given {@link Digest}</li> + * MacLen = length of MacTag + * </pre> + * <p/> + */ + public static BigInteger calculateMacTag( + String participantId, + String partnerParticipantId, + BigInteger gx1, + BigInteger gx2, + BigInteger gx3, + BigInteger gx4, + BigInteger keyingMaterial, + Digest digest) + { + byte[] macKey = calculateMacKey( + keyingMaterial, + digest); + + HMac mac = new HMac(digest); + byte[] macOutput = new byte[mac.getMacSize()]; + mac.init(new KeyParameter(macKey)); + + /* + * MacData = "KC_1_U" || participantId_Alice || participantId_Bob || gx1 || gx2 || gx3 || gx4. + */ + updateMac(mac, "KC_1_U"); + updateMac(mac, participantId); + updateMac(mac, partnerParticipantId); + updateMac(mac, gx1); + updateMac(mac, gx2); + updateMac(mac, gx3); + updateMac(mac, gx4); + + mac.doFinal(macOutput, 0); + + Arrays.fill(macKey, (byte)0); + + return new BigInteger(macOutput); + + } + + /** + * Calculates the MacKey (i.e. the key to use when calculating the MagTag for key confirmation). + * <p/> + * <p/> + * <pre> + * MacKey = H(K || "JPAKE_KC") + * </pre> + */ + private static byte[] calculateMacKey(BigInteger keyingMaterial, Digest digest) + { + digest.reset(); + + updateDigest(digest, keyingMaterial); + /* + * This constant is used to ensure that the macKey is NOT the same as the derived key. + */ + updateDigest(digest, "JPAKE_KC"); + + byte[] output = new byte[digest.getDigestSize()]; + digest.doFinal(output, 0); + + return output; + } + + /** + * Validates the MacTag received from the partner participant. + * <p/> + * + * @param partnerMacTag the MacTag received from the partner. + * @throws CryptoException if the participantId strings are equal. + */ + public static void validateMacTag( + String participantId, + String partnerParticipantId, + BigInteger gx1, + BigInteger gx2, + BigInteger gx3, + BigInteger gx4, + BigInteger keyingMaterial, + Digest digest, + BigInteger partnerMacTag) + throws CryptoException + { + /* + * Calculate the expected MacTag using the parameters as the partner + * would have used when the partner called calculateMacTag. + * + * i.e. basically all the parameters are reversed. + * participantId <-> partnerParticipantId + * x1 <-> x3 + * x2 <-> x4 + */ + BigInteger expectedMacTag = calculateMacTag( + partnerParticipantId, + participantId, + gx3, + gx4, + gx1, + gx2, + keyingMaterial, + digest); + + if (!expectedMacTag.equals(partnerMacTag)) + { + throw new CryptoException( + "Partner MacTag validation failed. " + + "Therefore, the password, MAC, or digest algorithm of each participant does not match."); + } + } + + private static void updateDigest(Digest digest, BigInteger bigInteger) + { + byte[] byteArray = BigIntegers.asUnsignedByteArray(bigInteger); + digest.update(byteArray, 0, byteArray.length); + Arrays.fill(byteArray, (byte)0); + } + + private static void updateDigestIncludingSize(Digest digest, BigInteger bigInteger) + { + byte[] byteArray = BigIntegers.asUnsignedByteArray(bigInteger); + digest.update(intToByteArray(byteArray.length), 0, 4); + digest.update(byteArray, 0, byteArray.length); + Arrays.fill(byteArray, (byte)0); + } + + private static void updateDigest(Digest digest, String string) + { + byte[] byteArray = Strings.toUTF8ByteArray(string); + digest.update(byteArray, 0, byteArray.length); + Arrays.fill(byteArray, (byte)0); + } + + private static void updateDigestIncludingSize(Digest digest, String string) + { + byte[] byteArray = Strings.toUTF8ByteArray(string); + digest.update(intToByteArray(byteArray.length), 0, 4); + digest.update(byteArray, 0, byteArray.length); + Arrays.fill(byteArray, (byte)0); + } + + private static void updateMac(Mac mac, BigInteger bigInteger) + { + byte[] byteArray = BigIntegers.asUnsignedByteArray(bigInteger); + mac.update(byteArray, 0, byteArray.length); + Arrays.fill(byteArray, (byte)0); + } + + private static void updateMac(Mac mac, String string) + { + byte[] byteArray = Strings.toUTF8ByteArray(string); + mac.update(byteArray, 0, byteArray.length); + Arrays.fill(byteArray, (byte)0); + } + + private static byte[] intToByteArray(int value) + { + return new byte[]{ + (byte)(value >>> 24), + (byte)(value >>> 16), + (byte)(value >>> 8), + (byte)value + }; + } + +} |