diff options
Diffstat (limited to 'core/src/main/java/org/spongycastle/crypto/agreement/jpake')
7 files changed, 1480 insertions, 0 deletions
diff --git a/core/src/main/java/org/spongycastle/crypto/agreement/jpake/JPAKEParticipant.java b/core/src/main/java/org/spongycastle/crypto/agreement/jpake/JPAKEParticipant.java new file mode 100644 index 00000000..e7f1853e --- /dev/null +++ b/core/src/main/java/org/spongycastle/crypto/agreement/jpake/JPAKEParticipant.java @@ -0,0 +1,546 @@ +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.digests.SHA256Digest; +import org.spongycastle.util.Arrays; + +/** + * A participant in a Password Authenticated Key Exchange by Juggling (J-PAKE) exchange. + * <p> + * The J-PAKE exchange is defined by Feng Hao and Peter Ryan in the paper + * <a href="http://grouper.ieee.org/groups/1363/Research/contributions/hao-ryan-2008.pdf"> + * "Password Authenticated Key Exchange by Juggling, 2008."</a> + * <p> + * The J-PAKE protocol is symmetric. + * There is no notion of a <i>client</i> or <i>server</i>, but rather just two <i>participants</i>. + * An instance of {@link JPAKEParticipant} represents one participant, and + * is the primary interface for executing the exchange. + * <p> + * To execute an exchange, construct a {@link JPAKEParticipant} on each end, + * and call the following 7 methods + * (once and only once, in the given order, for each participant, sending messages between them as described): + * <ol> + * <li>{@link #createRound1PayloadToSend()} - and send the payload to the other participant</li> + * <li>{@link #validateRound1PayloadReceived(JPAKERound1Payload)} - use the payload received from the other participant</li> + * <li>{@link #createRound2PayloadToSend()} - and send the payload to the other participant</li> + * <li>{@link #validateRound2PayloadReceived(JPAKERound2Payload)} - use the payload received from the other participant</li> + * <li>{@link #calculateKeyingMaterial()}</li> + * <li>{@link #createRound3PayloadToSend(BigInteger)} - and send the payload to the other participant</li> + * <li>{@link #validateRound3PayloadReceived(JPAKERound3Payload, BigInteger)} - use the payload received from the other participant</li> + * </ol> + * <p> + * Each side should derive a session key from the keying material returned by {@link #calculateKeyingMaterial()}. + * The caller is responsible for deriving the session key using a secure key derivation function (KDF). + * <p> + * Round 3 is an optional key confirmation process. + * If you do not execute round 3, then there is no assurance that both participants are using the same key. + * (i.e. if the participants used different passwords, then their session keys will differ.) + * <p> + * If the round 3 validation succeeds, then the keys are guaranteed to be the same on both sides. + * <p> + * The symmetric design can easily support the asymmetric cases when one party initiates the communication. + * e.g. Sometimes the round1 payload and round2 payload may be sent in one pass. + * Also, in some cases, the key confirmation payload can be sent together with the round2 payload. + * These are the trivial techniques to optimize the communication. + * <p> + * The key confirmation process is implemented as specified in + * <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> + * This class is stateful and NOT threadsafe. + * Each instance should only be used for ONE complete J-PAKE exchange + * (i.e. a new {@link JPAKEParticipant} should be constructed for each new J-PAKE exchange). + * <p> + * See {@link JPAKEExample} for example usage. + */ +public class JPAKEParticipant +{ + /* + * Possible internal states. Used for state checking. + */ + + public static final int STATE_INITIALIZED = 0; + public static final int STATE_ROUND_1_CREATED = 10; + public static final int STATE_ROUND_1_VALIDATED = 20; + public static final int STATE_ROUND_2_CREATED = 30; + public static final int STATE_ROUND_2_VALIDATED = 40; + public static final int STATE_KEY_CALCULATED = 50; + public static final int STATE_ROUND_3_CREATED = 60; + public static final int STATE_ROUND_3_VALIDATED = 70; + + /** + * Unique identifier of this participant. + * The two participants in the exchange must NOT share the same id. + */ + private final String participantId; + + /** + * Shared secret. This only contains the secret between construction + * and the call to {@link #calculateKeyingMaterial()}. + * <p/> + * i.e. When {@link #calculateKeyingMaterial()} is called, this buffer overwritten with 0's, + * and the field is set to null. + */ + private char[] password; + + /** + * Digest to use during calculations. + */ + private final Digest digest; + + /** + * Source of secure random data. + */ + private final SecureRandom random; + + private final BigInteger p; + private final BigInteger q; + private final BigInteger g; + + /** + * The participantId of the other participant in this exchange. + */ + private String partnerParticipantId; + + /** + * Alice's x1 or Bob's x3. + */ + private BigInteger x1; + /** + * Alice's x2 or Bob's x4. + */ + private BigInteger x2; + /** + * Alice's g^x1 or Bob's g^x3. + */ + private BigInteger gx1; + /** + * Alice's g^x2 or Bob's g^x4. + */ + private BigInteger gx2; + /** + * Alice's g^x3 or Bob's g^x1. + */ + private BigInteger gx3; + /** + * Alice's g^x4 or Bob's g^x2. + */ + private BigInteger gx4; + /** + * Alice's B or Bob's A. + */ + private BigInteger b; + + /** + * The current state. + * See the <tt>STATE_*</tt> constants for possible values. + */ + private int state; + + /** + * Convenience constructor for a new {@link JPAKEParticipant} that uses + * the {@link JPAKEPrimeOrderGroups#NIST_3072} prime order group, + * a SHA-256 digest, and a default {@link SecureRandom} implementation. + * <p> + * After construction, the {@link #getState() state} will be {@link #STATE_INITIALIZED}. + * + * @param participantId unique identifier of this participant. + * The two participants in the exchange must NOT share the same id. + * @param password shared secret. + * A defensive copy of this array is made (and cleared once {@link #calculateKeyingMaterial()} is called). + * Caller should clear the input password as soon as possible. + * @throws NullPointerException if any argument is null + * @throws IllegalArgumentException if password is empty + */ + public JPAKEParticipant( + String participantId, + char[] password) + { + this( + participantId, + password, + JPAKEPrimeOrderGroups.NIST_3072); + } + + + /** + * Convenience constructor for a new {@link JPAKEParticipant} that uses + * a SHA-256 digest and a default {@link SecureRandom} implementation. + * <p> + * After construction, the {@link #getState() state} will be {@link #STATE_INITIALIZED}. + * + * @param participantId unique identifier of this participant. + * The two participants in the exchange must NOT share the same id. + * @param password shared secret. + * A defensive copy of this array is made (and cleared once {@link #calculateKeyingMaterial()} is called). + * Caller should clear the input password as soon as possible. + * @param group prime order group. + * See {@link JPAKEPrimeOrderGroups} for standard groups + * @throws NullPointerException if any argument is null + * @throws IllegalArgumentException if password is empty + */ + public JPAKEParticipant( + String participantId, + char[] password, + JPAKEPrimeOrderGroup group) + { + this( + participantId, + password, + group, + new SHA256Digest(), + new SecureRandom()); + } + + + /** + * Construct a new {@link JPAKEParticipant}. + * <p> + * After construction, the {@link #getState() state} will be {@link #STATE_INITIALIZED}. + * + * @param participantId unique identifier of this participant. + * The two participants in the exchange must NOT share the same id. + * @param password shared secret. + * A defensive copy of this array is made (and cleared once {@link #calculateKeyingMaterial()} is called). + * Caller should clear the input password as soon as possible. + * @param group prime order group. + * See {@link JPAKEPrimeOrderGroups} for standard groups + * @param digest digest to use during zero knowledge proofs and key confirmation (SHA-256 or stronger preferred) + * @param random source of secure random data for x1 and x2, and for the zero knowledge proofs + * @throws NullPointerException if any argument is null + * @throws IllegalArgumentException if password is empty + */ + public JPAKEParticipant( + String participantId, + char[] password, + JPAKEPrimeOrderGroup group, + Digest digest, + SecureRandom random) + { + JPAKEUtil.validateNotNull(participantId, "participantId"); + JPAKEUtil.validateNotNull(password, "password"); + JPAKEUtil.validateNotNull(group, "p"); + JPAKEUtil.validateNotNull(digest, "digest"); + JPAKEUtil.validateNotNull(random, "random"); + if (password.length == 0) + { + throw new IllegalArgumentException("Password must not be empty."); + } + + this.participantId = participantId; + + /* + * Create a defensive copy so as to fully encapsulate the password. + * + * This array will contain the password for the lifetime of this + * participant BEFORE {@link #calculateKeyingMaterial()} is called. + * + * i.e. When {@link #calculateKeyingMaterial()} is called, the array will be cleared + * in order to remove the password from memory. + * + * The caller is responsible for clearing the original password array + * given as input to this constructor. + */ + this.password = Arrays.copyOf(password, password.length); + + this.p = group.getP(); + this.q = group.getQ(); + this.g = group.getG(); + + this.digest = digest; + this.random = random; + + this.state = STATE_INITIALIZED; + } + + /** + * Gets the current state of this participant. + * See the <tt>STATE_*</tt> constants for possible values. + */ + public int getState() + { + return this.state; + } + + /** + * Creates and returns the payload to send to the other participant during round 1. + * <p> + * After execution, the {@link #getState() state} will be {@link #STATE_ROUND_1_CREATED}. + */ + public JPAKERound1Payload createRound1PayloadToSend() + { + if (this.state >= STATE_ROUND_1_CREATED) + { + throw new IllegalStateException("Round1 payload already created for " + participantId); + } + + this.x1 = JPAKEUtil.generateX1(q, random); + this.x2 = JPAKEUtil.generateX2(q, random); + + this.gx1 = JPAKEUtil.calculateGx(p, g, x1); + this.gx2 = JPAKEUtil.calculateGx(p, g, x2); + BigInteger[] knowledgeProofForX1 = JPAKEUtil.calculateZeroKnowledgeProof(p, q, g, gx1, x1, participantId, digest, random); + BigInteger[] knowledgeProofForX2 = JPAKEUtil.calculateZeroKnowledgeProof(p, q, g, gx2, x2, participantId, digest, random); + + this.state = STATE_ROUND_1_CREATED; + + return new JPAKERound1Payload(participantId, gx1, gx2, knowledgeProofForX1, knowledgeProofForX2); + } + + /** + * Validates the payload received from the other participant during round 1. + * <p> + * Must be called prior to {@link #createRound2PayloadToSend()}. + * <p> + * After execution, the {@link #getState() state} will be {@link #STATE_ROUND_1_VALIDATED}. + * + * @throws CryptoException if validation fails. + * @throws IllegalStateException if called multiple times. + */ + public void validateRound1PayloadReceived(JPAKERound1Payload round1PayloadReceived) + throws CryptoException + { + if (this.state >= STATE_ROUND_1_VALIDATED) + { + throw new IllegalStateException("Validation already attempted for round1 payload for" + participantId); + } + this.partnerParticipantId = round1PayloadReceived.getParticipantId(); + this.gx3 = round1PayloadReceived.getGx1(); + this.gx4 = round1PayloadReceived.getGx2(); + + BigInteger[] knowledgeProofForX3 = round1PayloadReceived.getKnowledgeProofForX1(); + BigInteger[] knowledgeProofForX4 = round1PayloadReceived.getKnowledgeProofForX2(); + + JPAKEUtil.validateParticipantIdsDiffer(participantId, round1PayloadReceived.getParticipantId()); + JPAKEUtil.validateGx4(gx4); + JPAKEUtil.validateZeroKnowledgeProof(p, q, g, gx3, knowledgeProofForX3, round1PayloadReceived.getParticipantId(), digest); + JPAKEUtil.validateZeroKnowledgeProof(p, q, g, gx4, knowledgeProofForX4, round1PayloadReceived.getParticipantId(), digest); + + this.state = STATE_ROUND_1_VALIDATED; + } + + /** + * Creates and returns the payload to send to the other participant during round 2. + * <p> + * {@link #validateRound1PayloadReceived(JPAKERound1Payload)} must be called prior to this method. + * <p> + * After execution, the {@link #getState() state} will be {@link #STATE_ROUND_2_CREATED}. + * + * @throws IllegalStateException if called prior to {@link #validateRound1PayloadReceived(JPAKERound1Payload)}, or multiple times + */ + public JPAKERound2Payload createRound2PayloadToSend() + { + if (this.state >= STATE_ROUND_2_CREATED) + { + throw new IllegalStateException("Round2 payload already created for " + this.participantId); + } + if (this.state < STATE_ROUND_1_VALIDATED) + { + throw new IllegalStateException("Round1 payload must be validated prior to creating Round2 payload for " + this.participantId); + } + BigInteger gA = JPAKEUtil.calculateGA(p, gx1, gx3, gx4); + BigInteger s = JPAKEUtil.calculateS(password); + BigInteger x2s = JPAKEUtil.calculateX2s(q, x2, s); + BigInteger A = JPAKEUtil.calculateA(p, q, gA, x2s); + BigInteger[] knowledgeProofForX2s = JPAKEUtil.calculateZeroKnowledgeProof(p, q, gA, A, x2s, participantId, digest, random); + + this.state = STATE_ROUND_2_CREATED; + + return new JPAKERound2Payload(participantId, A, knowledgeProofForX2s); + } + + /** + * Validates the payload received from the other participant during round 2. + * <p> + * Note that this DOES NOT detect a non-common password. + * The only indication of a non-common password is through derivation + * of different keys (which can be detected explicitly by executing round 3 and round 4) + * <p> + * Must be called prior to {@link #calculateKeyingMaterial()}. + * <p> + * After execution, the {@link #getState() state} will be {@link #STATE_ROUND_2_VALIDATED}. + * + * @throws CryptoException if validation fails. + * @throws IllegalStateException if called prior to {@link #validateRound1PayloadReceived(JPAKERound1Payload)}, or multiple times + */ + public void validateRound2PayloadReceived(JPAKERound2Payload round2PayloadReceived) + throws CryptoException + { + if (this.state >= STATE_ROUND_2_VALIDATED) + { + throw new IllegalStateException("Validation already attempted for round2 payload for" + participantId); + } + if (this.state < STATE_ROUND_1_VALIDATED) + { + throw new IllegalStateException("Round1 payload must be validated prior to validating Round2 payload for " + this.participantId); + } + BigInteger gB = JPAKEUtil.calculateGA(p, gx3, gx1, gx2); + this.b = round2PayloadReceived.getA(); + BigInteger[] knowledgeProofForX4s = round2PayloadReceived.getKnowledgeProofForX2s(); + + JPAKEUtil.validateParticipantIdsDiffer(participantId, round2PayloadReceived.getParticipantId()); + JPAKEUtil.validateParticipantIdsEqual(this.partnerParticipantId, round2PayloadReceived.getParticipantId()); + JPAKEUtil.validateGa(gB); + JPAKEUtil.validateZeroKnowledgeProof(p, q, gB, b, knowledgeProofForX4s, round2PayloadReceived.getParticipantId(), digest); + + this.state = STATE_ROUND_2_VALIDATED; + } + + /** + * Calculates and returns the key material. + * 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> + * The keying material will be identical for each participant if and only if + * each participant's password is the same. i.e. If the participants do not + * share the same password, then each participant will derive a different key. + * Therefore, if you immediately start using a key derived from + * the keying material, then you must handle detection of incorrect keys. + * If you want to handle this detection explicitly, you can optionally perform + * rounds 3 and 4. See {@link JPAKEParticipant} for details on how to execute + * rounds 3 and 4. + * <p> + * The keying material will be in the range <tt>[0, p-1]</tt>. + * <p> + * {@link #validateRound2PayloadReceived(JPAKERound2Payload)} must be called prior to this method. + * <p> + * As a side effect, the internal {@link #password} array is cleared, since it is no longer needed. + * <p> + * After execution, the {@link #getState() state} will be {@link #STATE_KEY_CALCULATED}. + * + * @throws IllegalStateException if called prior to {@link #validateRound2PayloadReceived(JPAKERound2Payload)}, + * or if called multiple times. + */ + public BigInteger calculateKeyingMaterial() + { + if (this.state >= STATE_KEY_CALCULATED) + { + throw new IllegalStateException("Key already calculated for " + participantId); + } + if (this.state < STATE_ROUND_2_VALIDATED) + { + throw new IllegalStateException("Round2 payload must be validated prior to creating key for " + participantId); + } + BigInteger s = JPAKEUtil.calculateS(password); + + /* + * Clear the password array from memory, since we don't need it anymore. + * + * Also set the field to null as a flag to indicate that the key has already been calculated. + */ + Arrays.fill(password, (char)0); + this.password = null; + + BigInteger keyingMaterial = JPAKEUtil.calculateKeyingMaterial(p, q, gx4, x2, s, b); + + /* + * Clear the ephemeral private key fields as well. + * Note that we're relying on the garbage collector to do its job to clean these up. + * The old objects will hang around in memory until the garbage collector destroys them. + * + * If the ephemeral private keys x1 and x2 are leaked, + * the attacker might be able to brute-force the password. + */ + this.x1 = null; + this.x2 = null; + this.b = null; + + /* + * Do not clear gx* yet, since those are needed by round 3. + */ + + this.state = STATE_KEY_CALCULATED; + + return keyingMaterial; + } + + + /** + * Creates and returns the payload to send to the other participant during round 3. + * <p> + * See {@link JPAKEParticipant} for more details on round 3. + * <p> + * After execution, the {@link #getState() state} will be {@link #STATE_ROUND_3_CREATED}. + * + * @param keyingMaterial The keying material as returned from {@link #calculateKeyingMaterial()}. + * @throws IllegalStateException if called prior to {@link #calculateKeyingMaterial()}, or multiple times + */ + public JPAKERound3Payload createRound3PayloadToSend(BigInteger keyingMaterial) + { + if (this.state >= STATE_ROUND_3_CREATED) + { + throw new IllegalStateException("Round3 payload already created for " + this.participantId); + } + if (this.state < STATE_KEY_CALCULATED) + { + throw new IllegalStateException("Keying material must be calculated prior to creating Round3 payload for " + this.participantId); + } + + BigInteger macTag = JPAKEUtil.calculateMacTag( + this.participantId, + this.partnerParticipantId, + this.gx1, + this.gx2, + this.gx3, + this.gx4, + keyingMaterial, + this.digest); + + this.state = STATE_ROUND_3_CREATED; + + return new JPAKERound3Payload(participantId, macTag); + } + + /** + * Validates the payload received from the other participant during round 3. + * <p> + * See {@link JPAKEParticipant} for more details on round 3. + * <p> + * After execution, the {@link #getState() state} will be {@link #STATE_ROUND_3_VALIDATED}. + * + * @param keyingMaterial The keying material as returned from {@link #calculateKeyingMaterial()}. + * @throws CryptoException if validation fails. + * @throws IllegalStateException if called prior to {@link #calculateKeyingMaterial()}, or multiple times + */ + public void validateRound3PayloadReceived(JPAKERound3Payload round3PayloadReceived, BigInteger keyingMaterial) + throws CryptoException + { + if (this.state >= STATE_ROUND_3_VALIDATED) + { + throw new IllegalStateException("Validation already attempted for round3 payload for" + participantId); + } + if (this.state < STATE_KEY_CALCULATED) + { + throw new IllegalStateException("Keying material must be calculated validated prior to validating Round3 payload for " + this.participantId); + } + JPAKEUtil.validateParticipantIdsDiffer(participantId, round3PayloadReceived.getParticipantId()); + JPAKEUtil.validateParticipantIdsEqual(this.partnerParticipantId, round3PayloadReceived.getParticipantId()); + + JPAKEUtil.validateMacTag( + this.participantId, + this.partnerParticipantId, + this.gx1, + this.gx2, + this.gx3, + this.gx4, + keyingMaterial, + this.digest, + round3PayloadReceived.getMacTag()); + + + /* + * Clear the rest of the fields. + */ + this.gx1 = null; + this.gx2 = null; + this.gx3 = null; + this.gx4 = null; + + this.state = STATE_ROUND_3_VALIDATED; + } + +} diff --git a/core/src/main/java/org/spongycastle/crypto/agreement/jpake/JPAKEPrimeOrderGroup.java b/core/src/main/java/org/spongycastle/crypto/agreement/jpake/JPAKEPrimeOrderGroup.java new file mode 100644 index 00000000..3571e81b --- /dev/null +++ b/core/src/main/java/org/spongycastle/crypto/agreement/jpake/JPAKEPrimeOrderGroup.java @@ -0,0 +1,115 @@ +package org.spongycastle.crypto.agreement.jpake; + +import java.math.BigInteger; + +/** + * A pre-computed prime order group for use during a J-PAKE exchange. + * <p> + * Typically a Schnorr group is used. In general, J-PAKE can use any prime order group + * that is suitable for public key cryptography, including elliptic curve cryptography. + * <p> + * See {@link JPAKEPrimeOrderGroups} for convenient standard groups. + * <p> + * NIST <a href="http://csrc.nist.gov/groups/ST/toolkit/documents/Examples/DSA2_All.pdf">publishes</a> + * many groups that can be used for the desired level of security. + */ +public class JPAKEPrimeOrderGroup +{ + private final BigInteger p; + private final BigInteger q; + private final BigInteger g; + + /** + * Constructs a new {@link JPAKEPrimeOrderGroup}. + * <p> + * In general, you should use one of the pre-approved groups from + * {@link JPAKEPrimeOrderGroups}, rather than manually constructing one. + * <p> + * The following basic checks are performed: + * <ul> + * <li>p-1 must be evenly divisible by q</li> + * <li>g must be in [2, p-1]</li> + * <li>g^q mod p must equal 1</li> + * <li>p must be prime (within reasonably certainty)</li> + * <li>q must be prime (within reasonably certainty)</li> + * </ul> + * <p> + * The prime checks are performed using {@link BigInteger#isProbablePrime(int)}, + * and are therefore subject to the same probability guarantees. + * <p> + * These checks prevent trivial mistakes. + * However, due to the small uncertainties if p and q are not prime, + * advanced attacks are not prevented. + * Use it at your own risk. + * + * @throws NullPointerException if any argument is null + * @throws IllegalArgumentException if any of the above validations fail + */ + public JPAKEPrimeOrderGroup(BigInteger p, BigInteger q, BigInteger g) + { + /* + * Don't skip the checks on user-specified groups. + */ + this(p, q, g, false); + } + + /** + * Internal package-private constructor used by the pre-approved + * groups in {@link JPAKEPrimeOrderGroups}. + * These pre-approved groups can avoid the expensive checks. + */ + JPAKEPrimeOrderGroup(BigInteger p, BigInteger q, BigInteger g, boolean skipChecks) + { + JPAKEUtil.validateNotNull(p, "p"); + JPAKEUtil.validateNotNull(q, "q"); + JPAKEUtil.validateNotNull(g, "g"); + + if (!skipChecks) + { + if (!p.subtract(JPAKEUtil.ONE).mod(q).equals(JPAKEUtil.ZERO)) + { + throw new IllegalArgumentException("p-1 must be evenly divisible by q"); + } + if (g.compareTo(BigInteger.valueOf(2)) == -1 || g.compareTo(p.subtract(JPAKEUtil.ONE)) == 1) + { + throw new IllegalArgumentException("g must be in [2, p-1]"); + } + if (!g.modPow(q, p).equals(JPAKEUtil.ONE)) + { + throw new IllegalArgumentException("g^q mod p must equal 1"); + } + /* + * Note that these checks do not guarantee that p and q are prime. + * We just have reasonable certainty that they are prime. + */ + if (!p.isProbablePrime(20)) + { + throw new IllegalArgumentException("p must be prime"); + } + if (!q.isProbablePrime(20)) + { + throw new IllegalArgumentException("q must be prime"); + } + } + + this.p = p; + this.q = q; + this.g = g; + } + + public BigInteger getP() + { + return p; + } + + public BigInteger getQ() + { + return q; + } + + public BigInteger getG() + { + return g; + } + +} diff --git a/core/src/main/java/org/spongycastle/crypto/agreement/jpake/JPAKEPrimeOrderGroups.java b/core/src/main/java/org/spongycastle/crypto/agreement/jpake/JPAKEPrimeOrderGroups.java new file mode 100644 index 00000000..17f576a0 --- /dev/null +++ b/core/src/main/java/org/spongycastle/crypto/agreement/jpake/JPAKEPrimeOrderGroups.java @@ -0,0 +1,111 @@ +package org.spongycastle.crypto.agreement.jpake; + +import java.math.BigInteger; + +/** + * Standard pre-computed prime order groups for use by J-PAKE. + * (J-PAKE can use pre-computed prime order groups, same as DSA and Diffie-Hellman.) + * <p> + * This class contains some convenient constants for use as input for + * constructing {@link JPAKEParticipant}s. + * <p> + * The prime order groups below are taken from Sun's JDK JavaDoc (docs/guide/security/CryptoSpec.html#AppB), + * and from the prime order groups + * <a href="http://csrc.nist.gov/groups/ST/toolkit/documents/Examples/DSA2_All.pdf">published by NIST</a>. + */ +public class JPAKEPrimeOrderGroups +{ + /** + * From Sun's JDK JavaDoc (docs/guide/security/CryptoSpec.html#AppB) + * 1024-bit p, 160-bit q and 1024-bit g for 80-bit security. + */ + public static final JPAKEPrimeOrderGroup SUN_JCE_1024 = new JPAKEPrimeOrderGroup( + // p + new BigInteger( + "fd7f53811d75122952df4a9c2eece4e7f611b7523cef4400c31e3f80b6512669" + + "455d402251fb593d8d58fabfc5f5ba30f6cb9b556cd7813b801d346ff26660b7" + + "6b9950a5a49f9fe8047b1022c24fbba9d7feb7c61bf83b57e7c6a8a6150f04fb" + + "83f6d3c51ec3023554135a169132f675f3ae2b61d72aeff22203199dd14801c7", 16), + // q + new BigInteger( + "9760508f15230bccb292b982a2eb840bf0581cf5", 16), + // g + new BigInteger( + "f7e1a085d69b3ddecbbcab5c36b857b97994afbbfa3aea82f9574c0b3d078267" + + "5159578ebad4594fe67107108180b449167123e84c281613b7cf09328cc8a6e1" + + "3c167a8b547c8d28e0a3ae1e2bb3a675916ea37f0bfa213562f1fb627a01243b" + + "cca4f1bea8519089a883dfe15ae59f06928b665e807b552564014c3bfecf492a", 16), + true + ); + + /** + * From NIST. + * 2048-bit p, 224-bit q and 2048-bit g for 112-bit security. + */ + public static final JPAKEPrimeOrderGroup NIST_2048 = new JPAKEPrimeOrderGroup( + // p + new BigInteger( + "C196BA05AC29E1F9C3C72D56DFFC6154A033F1477AC88EC37F09BE6C5BB95F51" + + "C296DD20D1A28A067CCC4D4316A4BD1DCA55ED1066D438C35AEBAABF57E7DAE4" + + "28782A95ECA1C143DB701FD48533A3C18F0FE23557EA7AE619ECACC7E0B51652" + + "A8776D02A425567DED36EABD90CA33A1E8D988F0BBB92D02D1D20290113BB562" + + "CE1FC856EEB7CDD92D33EEA6F410859B179E7E789A8F75F645FAE2E136D252BF" + + "FAFF89528945C1ABE705A38DBC2D364AADE99BE0D0AAD82E5320121496DC65B3" + + "930E38047294FF877831A16D5228418DE8AB275D7D75651CEFED65F78AFC3EA7" + + "FE4D79B35F62A0402A1117599ADAC7B269A59F353CF450E6982D3B1702D9CA83", 16), + // q + new BigInteger( + "90EAF4D1AF0708B1B612FF35E0A2997EB9E9D263C9CE659528945C0D", 16), + // g + new BigInteger( + "A59A749A11242C58C894E9E5A91804E8FA0AC64B56288F8D47D51B1EDC4D6544" + + "4FECA0111D78F35FC9FDD4CB1F1B79A3BA9CBEE83A3F811012503C8117F98E50" + + "48B089E387AF6949BF8784EBD9EF45876F2E6A5A495BE64B6E770409494B7FEE" + + "1DBB1E4B2BC2A53D4F893D418B7159592E4FFFDF6969E91D770DAEBD0B5CB14C" + + "00AD68EC7DC1E5745EA55C706C4A1C5C88964E34D09DEB753AD418C1AD0F4FDF" + + "D049A955E5D78491C0B7A2F1575A008CCD727AB376DB6E695515B05BD412F5B8" + + "C2F4C77EE10DA48ABD53F5DD498927EE7B692BBBCDA2FB23A516C5B4533D7398" + + "0B2A3B60E384ED200AE21B40D273651AD6060C13D97FD69AA13C5611A51B9085", 16), + true + ); + + /** + * From NIST. + * 3072-bit p, 256-bit q and 3072-bit g for 128-bit security. + */ + public static final JPAKEPrimeOrderGroup NIST_3072 = new JPAKEPrimeOrderGroup( + // p + new BigInteger( + "90066455B5CFC38F9CAA4A48B4281F292C260FEEF01FD61037E56258A7795A1C" + + "7AD46076982CE6BB956936C6AB4DCFE05E6784586940CA544B9B2140E1EB523F" + + "009D20A7E7880E4E5BFA690F1B9004A27811CD9904AF70420EEFD6EA11EF7DA1" + + "29F58835FF56B89FAA637BC9AC2EFAAB903402229F491D8D3485261CD068699B" + + "6BA58A1DDBBEF6DB51E8FE34E8A78E542D7BA351C21EA8D8F1D29F5D5D159394" + + "87E27F4416B0CA632C59EFD1B1EB66511A5A0FBF615B766C5862D0BD8A3FE7A0" + + "E0DA0FB2FE1FCB19E8F9996A8EA0FCCDE538175238FC8B0EE6F29AF7F642773E" + + "BE8CD5402415A01451A840476B2FCEB0E388D30D4B376C37FE401C2A2C2F941D" + + "AD179C540C1C8CE030D460C4D983BE9AB0B20F69144C1AE13F9383EA1C08504F" + + "B0BF321503EFE43488310DD8DC77EC5B8349B8BFE97C2C560EA878DE87C11E3D" + + "597F1FEA742D73EEC7F37BE43949EF1A0D15C3F3E3FC0A8335617055AC91328E" + + "C22B50FC15B941D3D1624CD88BC25F3E941FDDC6200689581BFEC416B4B2CB73", 16), + // q + new BigInteger( + "CFA0478A54717B08CE64805B76E5B14249A77A4838469DF7F7DC987EFCCFB11D", 16), + // g + new BigInteger( + "5E5CBA992E0A680D885EB903AEA78E4A45A469103D448EDE3B7ACCC54D521E37" + + "F84A4BDD5B06B0970CC2D2BBB715F7B82846F9A0C393914C792E6A923E2117AB" + + "805276A975AADB5261D91673EA9AAFFEECBFA6183DFCB5D3B7332AA19275AFA1" + + "F8EC0B60FB6F66CC23AE4870791D5982AAD1AA9485FD8F4A60126FEB2CF05DB8" + + "A7F0F09B3397F3937F2E90B9E5B9C9B6EFEF642BC48351C46FB171B9BFA9EF17" + + "A961CE96C7E7A7CC3D3D03DFAD1078BA21DA425198F07D2481622BCE45969D9C" + + "4D6063D72AB7A0F08B2F49A7CC6AF335E08C4720E31476B67299E231F8BD90B3" + + "9AC3AE3BE0C6B6CACEF8289A2E2873D58E51E029CAFBD55E6841489AB66B5B4B" + + "9BA6E2F784660896AFF387D92844CCB8B69475496DE19DA2E58259B090489AC8" + + "E62363CDF82CFD8EF2A427ABCD65750B506F56DDE3B988567A88126B914D7828" + + "E2B63A6D7ED0747EC59E0E0A23CE7D8A74C1D2C2A7AFB6A29799620F00E11C33" + + "787F7DED3B30E1A22D09F1FBDA1ABBBFBF25CAE05A13F812E34563F99410E73B", 16), + true + ); + +} diff --git a/core/src/main/java/org/spongycastle/crypto/agreement/jpake/JPAKERound1Payload.java b/core/src/main/java/org/spongycastle/crypto/agreement/jpake/JPAKERound1Payload.java new file mode 100644 index 00000000..0f6d3838 --- /dev/null +++ b/core/src/main/java/org/spongycastle/crypto/agreement/jpake/JPAKERound1Payload.java @@ -0,0 +1,96 @@ +package org.spongycastle.crypto.agreement.jpake; + +import java.math.BigInteger; + +import org.spongycastle.util.Arrays; + +/** + * The payload sent/received during the first round of a J-PAKE exchange. + * <p> + * Each {@link JPAKEParticipant} creates and sends an instance + * of this payload to the other {@link JPAKEParticipant}. + * The payload to send should be created via + * {@link JPAKEParticipant#createRound1PayloadToSend()}. + * <p> + * Each {@link JPAKEParticipant} must also validate the payload + * received from the other {@link JPAKEParticipant}. + * The received payload should be validated via + * {@link JPAKEParticipant#validateRound1PayloadReceived(JPAKERound1Payload)}. + */ +public class JPAKERound1Payload +{ + /** + * The id of the {@link JPAKEParticipant} who created/sent this payload. + */ + private final String participantId; + + /** + * The value of g^x1 + */ + private final BigInteger gx1; + + /** + * The value of g^x2 + */ + private final BigInteger gx2; + + /** + * The zero knowledge proof for x1. + * <p/> + * This is a two element array, containing {g^v, r} for x1. + */ + private final BigInteger[] knowledgeProofForX1; + + /** + * The zero knowledge proof for x2. + * <p/> + * This is a two element array, containing {g^v, r} for x2. + */ + private final BigInteger[] knowledgeProofForX2; + + public JPAKERound1Payload( + String participantId, + BigInteger gx1, + BigInteger gx2, + BigInteger[] knowledgeProofForX1, + BigInteger[] knowledgeProofForX2) + { + JPAKEUtil.validateNotNull(participantId, "participantId"); + JPAKEUtil.validateNotNull(gx1, "gx1"); + JPAKEUtil.validateNotNull(gx2, "gx2"); + JPAKEUtil.validateNotNull(knowledgeProofForX1, "knowledgeProofForX1"); + JPAKEUtil.validateNotNull(knowledgeProofForX2, "knowledgeProofForX2"); + + this.participantId = participantId; + this.gx1 = gx1; + this.gx2 = gx2; + this.knowledgeProofForX1 = Arrays.copyOf(knowledgeProofForX1, knowledgeProofForX1.length); + this.knowledgeProofForX2 = Arrays.copyOf(knowledgeProofForX2, knowledgeProofForX2.length); + } + + public String getParticipantId() + { + return participantId; + } + + public BigInteger getGx1() + { + return gx1; + } + + public BigInteger getGx2() + { + return gx2; + } + + public BigInteger[] getKnowledgeProofForX1() + { + return Arrays.copyOf(knowledgeProofForX1, knowledgeProofForX1.length); + } + + public BigInteger[] getKnowledgeProofForX2() + { + return Arrays.copyOf(knowledgeProofForX2, knowledgeProofForX2.length); + } + +} diff --git a/core/src/main/java/org/spongycastle/crypto/agreement/jpake/JPAKERound2Payload.java b/core/src/main/java/org/spongycastle/crypto/agreement/jpake/JPAKERound2Payload.java new file mode 100644 index 00000000..3df5c7b9 --- /dev/null +++ b/core/src/main/java/org/spongycastle/crypto/agreement/jpake/JPAKERound2Payload.java @@ -0,0 +1,68 @@ +package org.spongycastle.crypto.agreement.jpake; + +import java.math.BigInteger; + +import org.spongycastle.util.Arrays; + +/** + * The payload sent/received during the second round of a J-PAKE exchange. + * <p> + * Each {@link JPAKEParticipant} creates and sends an instance + * of this payload to the other {@link JPAKEParticipant}. + * The payload to send should be created via + * {@link JPAKEParticipant#createRound2PayloadToSend()} + * <p> + * Each {@link JPAKEParticipant} must also validate the payload + * received from the other {@link JPAKEParticipant}. + * The received payload should be validated via + * {@link JPAKEParticipant#validateRound2PayloadReceived(JPAKERound2Payload)} + */ +public class JPAKERound2Payload +{ + /** + * The id of the {@link JPAKEParticipant} who created/sent this payload. + */ + private final String participantId; + + /** + * The value of A, as computed during round 2. + */ + private final BigInteger a; + + /** + * The zero knowledge proof for x2 * s. + * <p/> + * This is a two element array, containing {g^v, r} for x2 * s. + */ + private final BigInteger[] knowledgeProofForX2s; + + public JPAKERound2Payload( + String participantId, + BigInteger a, + BigInteger[] knowledgeProofForX2s) + { + JPAKEUtil.validateNotNull(participantId, "participantId"); + JPAKEUtil.validateNotNull(a, "a"); + JPAKEUtil.validateNotNull(knowledgeProofForX2s, "knowledgeProofForX2s"); + + this.participantId = participantId; + this.a = a; + this.knowledgeProofForX2s = Arrays.copyOf(knowledgeProofForX2s, knowledgeProofForX2s.length); + } + + public String getParticipantId() + { + return participantId; + } + + public BigInteger getA() + { + return a; + } + + public BigInteger[] getKnowledgeProofForX2s() + { + return Arrays.copyOf(knowledgeProofForX2s, knowledgeProofForX2s.length); + } + +} diff --git a/core/src/main/java/org/spongycastle/crypto/agreement/jpake/JPAKERound3Payload.java b/core/src/main/java/org/spongycastle/crypto/agreement/jpake/JPAKERound3Payload.java new file mode 100644 index 00000000..bb4f6e14 --- /dev/null +++ b/core/src/main/java/org/spongycastle/crypto/agreement/jpake/JPAKERound3Payload.java @@ -0,0 +1,49 @@ +package org.spongycastle.crypto.agreement.jpake; + +import java.math.BigInteger; + +/** + * The payload sent/received during the optional third round of a J-PAKE exchange, + * which is for explicit key confirmation. + * <p> + * Each {@link JPAKEParticipant} creates and sends an instance + * of this payload to the other {@link JPAKEParticipant}. + * The payload to send should be created via + * {@link JPAKEParticipant#createRound3PayloadToSend(BigInteger)} + * <p> + * Each {@link JPAKEParticipant} must also validate the payload + * received from the other {@link JPAKEParticipant}. + * The received payload should be validated via + * {@link JPAKEParticipant#validateRound3PayloadReceived(JPAKERound3Payload, BigInteger)} + */ +public class JPAKERound3Payload +{ + /** + * The id of the {@link JPAKEParticipant} who created/sent this payload. + */ + private final String participantId; + + /** + * The value of MacTag, as computed by round 3. + * + * @see JPAKEUtil#calculateMacTag(String, String, BigInteger, BigInteger, BigInteger, BigInteger, BigInteger, org.spongycastle.crypto.Digest) + */ + private final BigInteger macTag; + + public JPAKERound3Payload(String participantId, BigInteger magTag) + { + this.participantId = participantId; + this.macTag = magTag; + } + + public String getParticipantId() + { + return participantId; + } + + public BigInteger getMacTag() + { + return macTag; + } + +} diff --git a/core/src/main/java/org/spongycastle/crypto/agreement/jpake/JPAKEUtil.java b/core/src/main/java/org/spongycastle/crypto/agreement/jpake/JPAKEUtil.java new file mode 100644 index 00000000..39931f4a --- /dev/null +++ b/core/src/main/java/org/spongycastle/crypto/agreement/jpake/JPAKEUtil.java @@ -0,0 +1,495 @@ +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> + * 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> + * 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> + * 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> + * 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> + * 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}). + * <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. + * <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} + * MacLen = length of MacTag + * </pre> + */ + 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). + * <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. + * + * @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 + }; + } + +} |