#if !BESTHTTP_DISABLE_ALTERNATE_SSL && (!UNITY_WEBGL || UNITY_EDITOR)
#pragma warning disable
using System;
using BestHTTP.SecureProtocol.Org.BouncyCastle.Crypto;
using BestHTTP.SecureProtocol.Org.BouncyCastle.Crypto.Digests;
using BestHTTP.SecureProtocol.Org.BouncyCastle.Math;
using BestHTTP.SecureProtocol.Org.BouncyCastle.Security;
namespace BestHTTP.SecureProtocol.Org.BouncyCastle.Crypto.Agreement.JPake
{
///
/// A participant in a Password Authenticated Key Exchange by Juggling (J-PAKE) exchange.
///
/// The J-PAKE exchange is defined by Feng Hao and Peter Ryan in the paper
///
/// "Password Authenticated Key Exchange by Juggling, 2008."
///
/// The J-PAKE protocol is symmetric.
/// There is no notion of a client or server, but rather just two participants.
/// An instance of JPakeParticipant represents one participant, and
/// is the primary interface for executing the exchange.
///
/// To execute an exchange, construct a 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):
///
/// CreateRound1PayloadToSend() - and send the payload to the other participant
/// ValidateRound1PayloadReceived(JPakeRound1Payload) - use the payload received from the other participant
/// CreateRound2PayloadToSend() - and send the payload to the other participant
/// ValidateRound2PayloadReceived(JPakeRound2Payload) - use the payload received from the other participant
/// CalculateKeyingMaterial()
/// CreateRound3PayloadToSend(BigInteger) - and send the payload to the other participant
/// ValidateRound3PayloadReceived(JPakeRound3Payload, BigInteger) - use the payload received from the other participant
///
/// Each side should derive a session key from the keying material returned by CalculateKeyingMaterial().
/// The caller is responsible for deriving the session key using a secure key derivation function (KDF).
///
/// 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.)
///
/// If the round 3 validation succeeds, then the keys are guaranteed to be the same on both sides.
///
/// 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.
///
/// The key confirmation process is implemented as specified in
/// NIST SP 800-56A Revision 1,
/// Section 8.2 Unilateral Key Confirmation for Key Agreement Schemes.
///
/// This class is stateful and NOT threadsafe.
/// Each instance should only be used for ONE complete J-PAKE exchange
/// (i.e. a new JPakeParticipant should be constructed for each new J-PAKE exchange).
///
public class JPakeParticipant
{
// Possible internal states. Used for state checking.
public static readonly int STATE_INITIALIZED = 0;
public static readonly int STATE_ROUND_1_CREATED = 10;
public static readonly int STATE_ROUND_1_VALIDATED = 20;
public static readonly int STATE_ROUND_2_CREATED = 30;
public static readonly int STATE_ROUND_2_VALIDATED = 40;
public static readonly int STATE_KEY_CALCULATED = 50;
public static readonly int STATE_ROUND_3_CREATED = 60;
public static readonly int STATE_ROUND_3_VALIDATED = 70;
// Unique identifier of this participant.
// The two participants in the exchange must NOT share the same id.
private string participantId;
// Shared secret. This only contains the secret between construction
// and the call to CalculateKeyingMaterial().
//
// i.e. When 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 IDigest digest;
// Source of secure random data.
private readonly SecureRandom random;
private readonly BigInteger p;
private readonly BigInteger q;
private readonly 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 STATE_* constants for possible values.
private int state;
///
/// Convenience constructor for a new JPakeParticipant that uses
/// the JPakePrimeOrderGroups#NIST_3072 prime order group,
/// a SHA-256 digest, and a default SecureRandom implementation.
///
/// After construction, the State state will be STATE_INITIALIZED.
///
/// Throws NullReferenceException if any argument is null. Throws
/// ArgumentException if password is empty.
///
/// Unique identifier of this participant.
/// The two participants in the exchange must NOT share the same id.
/// Shared secret.
/// A defensive copy of this array is made (and cleared once CalculateKeyingMaterial() is called).
/// Caller should clear the input password as soon as possible.
public JPakeParticipant(string participantId, char[] password)
: this(participantId, password, JPakePrimeOrderGroups.NIST_3072) { }
///
/// Convenience constructor for a new JPakeParticipant that uses
/// a SHA-256 digest, and a default SecureRandom implementation.
///
/// After construction, the State state will be STATE_INITIALIZED.
///
/// Throws NullReferenceException if any argument is null. Throws
/// ArgumentException if password is empty.
///
/// Unique identifier of this participant.
/// The two participants in the exchange must NOT share the same id.
/// Shared secret.
/// A defensive copy of this array is made (and cleared once CalculateKeyingMaterial() is called).
/// Caller should clear the input password as soon as possible.
/// Prime order group. See JPakePrimeOrderGroups for standard groups.
public JPakeParticipant(string participantId, char[] password, JPakePrimeOrderGroup group)
: this(participantId, password, group, new Sha256Digest(), new SecureRandom()) { }
///
/// Constructor for a new JPakeParticipant.
///
/// After construction, the State state will be STATE_INITIALIZED.
///
/// Throws NullReferenceException if any argument is null. Throws
/// ArgumentException if password is empty.
///
/// Unique identifier of this participant.
/// The two participants in the exchange must NOT share the same id.
/// Shared secret.
/// A defensive copy of this array is made (and cleared once CalculateKeyingMaterial() is called).
/// Caller should clear the input password as soon as possible.
/// Prime order group. See JPakePrimeOrderGroups for standard groups.
/// Digest to use during zero knowledge proofs and key confirmation
/// (SHA-256 or stronger preferred).
/// Source of secure random data for x1 and x2, and for the zero knowledge proofs.
public JPakeParticipant(string participantId, char[] password, JPakePrimeOrderGroup group, IDigest digest, SecureRandom random)
{
JPakeUtilities.ValidateNotNull(participantId, "participantId");
JPakeUtilities.ValidateNotNull(password, "password");
JPakeUtilities.ValidateNotNull(group, "p");
JPakeUtilities.ValidateNotNull(digest, "digest");
JPakeUtilities.ValidateNotNull(random, "random");
if (password.Length == 0)
{
throw new ArgumentException("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 CalculateKeyingMaterial() is called.
//
// i.e. When 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 = new char[password.Length];
Array.Copy(password, this.password, password.Length);
this.p = group.P;
this.q = group.Q;
this.g = group.G;
this.digest = digest;
this.random = random;
this.state = STATE_INITIALIZED;
}
///
/// Gets the current state of this participant.
/// See the STATE_* constants for possible values.
///
public virtual int State
{
get { return state; }
}
///
/// Creates and returns the payload to send to the other participant during round 1.
///
/// After execution, the State state} will be STATE_ROUND_1_CREATED}.
///
public virtual JPakeRound1Payload CreateRound1PayloadToSend()
{
if (this.state >= STATE_ROUND_1_CREATED)
throw new InvalidOperationException("Round 1 payload already created for " + this.participantId);
this.x1 = JPakeUtilities.GenerateX1(q, random);
this.x2 = JPakeUtilities.GenerateX2(q, random);
this.gx1 = JPakeUtilities.CalculateGx(p, g, x1);
this.gx2 = JPakeUtilities.CalculateGx(p, g, x2);
BigInteger[] knowledgeProofForX1 = JPakeUtilities.CalculateZeroKnowledgeProof(p, q, g, gx1, x1, participantId, digest, random);
BigInteger[] knowledgeProofForX2 = JPakeUtilities.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.
///
/// Must be called prior to CreateRound2PayloadToSend().
///
/// After execution, the State state will be STATE_ROUND_1_VALIDATED.
///
/// Throws CryptoException if validation fails. Throws InvalidOperationException
/// if called multiple times.
///
public virtual void ValidateRound1PayloadReceived(JPakeRound1Payload round1PayloadReceived)
{
if (this.state >= STATE_ROUND_1_VALIDATED)
throw new InvalidOperationException("Validation already attempted for round 1 payload for " + this.participantId);
this.partnerParticipantId = round1PayloadReceived.ParticipantId;
this.gx3 = round1PayloadReceived.Gx1;
this.gx4 = round1PayloadReceived.Gx2;
BigInteger[] knowledgeProofForX3 = round1PayloadReceived.KnowledgeProofForX1;
BigInteger[] knowledgeProofForX4 = round1PayloadReceived.KnowledgeProofForX2;
JPakeUtilities.ValidateParticipantIdsDiffer(participantId, round1PayloadReceived.ParticipantId);
JPakeUtilities.ValidateGx4(gx4);
JPakeUtilities.ValidateZeroKnowledgeProof(p, q, g, gx3, knowledgeProofForX3, round1PayloadReceived.ParticipantId, digest);
JPakeUtilities.ValidateZeroKnowledgeProof(p, q, g, gx4, knowledgeProofForX4, round1PayloadReceived.ParticipantId, digest);
this.state = STATE_ROUND_1_VALIDATED;
}
///
/// Creates and returns the payload to send to the other participant during round 2.
///
/// ValidateRound1PayloadReceived(JPakeRound1Payload) must be called prior to this method.
///
/// After execution, the State state will be STATE_ROUND_2_CREATED.
///
/// Throws InvalidOperationException if called prior to ValidateRound1PayloadReceived(JPakeRound1Payload), or multiple times
///
public virtual JPakeRound2Payload CreateRound2PayloadToSend()
{
if (this.state >= STATE_ROUND_2_CREATED)
throw new InvalidOperationException("Round 2 payload already created for " + this.participantId);
if (this.state < STATE_ROUND_1_VALIDATED)
throw new InvalidOperationException("Round 1 payload must be validated prior to creating round 2 payload for " + this.participantId);
BigInteger gA = JPakeUtilities.CalculateGA(p, gx1, gx3, gx4);
BigInteger s = JPakeUtilities.CalculateS(password);
BigInteger x2s = JPakeUtilities.CalculateX2s(q, x2, s);
BigInteger A = JPakeUtilities.CalculateA(p, q, gA, x2s);
BigInteger[] knowledgeProofForX2s = JPakeUtilities.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.
/// 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)
///
/// Must be called prior to CalculateKeyingMaterial().
///
/// After execution, the State state will be STATE_ROUND_2_VALIDATED.
///
/// Throws CryptoException if validation fails. Throws
/// InvalidOperationException if called prior to ValidateRound1PayloadReceived(JPakeRound1Payload), or multiple times
///
public virtual void ValidateRound2PayloadReceived(JPakeRound2Payload round2PayloadReceived)
{
if (this.state >= STATE_ROUND_2_VALIDATED)
throw new InvalidOperationException("Validation already attempted for round 2 payload for " + this.participantId);
if (this.state < STATE_ROUND_1_VALIDATED)
throw new InvalidOperationException("Round 1 payload must be validated prior to validation round 2 payload for " + this.participantId);
BigInteger gB = JPakeUtilities.CalculateGA(p, gx3, gx1, gx2);
this.b = round2PayloadReceived.A;
BigInteger[] knowledgeProofForX4s = round2PayloadReceived.KnowledgeProofForX2s;
JPakeUtilities.ValidateParticipantIdsDiffer(participantId, round2PayloadReceived.ParticipantId);
JPakeUtilities.ValidateParticipantIdsEqual(this.partnerParticipantId, round2PayloadReceived.ParticipantId);
JPakeUtilities.ValidateGa(gB);
JPakeUtilities.ValidateZeroKnowledgeProof(p, q, gB, b, knowledgeProofForX4s, round2PayloadReceived.ParticipantId, 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 JPakeParticipant).
///
/// 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 JPakeParticipant for details on how to execute
/// rounds 3 and 4.
///
/// The keying material will be in the range [0, p-1].
///
/// ValidateRound2PayloadReceived(JPakeRound2Payload) must be called prior to this method.
///
/// As a side effect, the internal password array is cleared, since it is no longer needed.
///
/// After execution, the State state will be STATE_KEY_CALCULATED.
///
/// Throws InvalidOperationException if called prior to ValidateRound2PayloadReceived(JPakeRound2Payload),
/// or if called multiple times.
///
public virtual BigInteger CalculateKeyingMaterial()
{
if (this.state >= STATE_KEY_CALCULATED)
throw new InvalidOperationException("Key already calculated for " + participantId);
if (this.state < STATE_ROUND_2_VALIDATED)
throw new InvalidOperationException("Round 2 payload must be validated prior to creating key for " + participantId);
BigInteger s = JPakeUtilities.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.
Array.Clear(password, 0, password.Length);
this.password = null;
BigInteger keyingMaterial = JPakeUtilities.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.
///
/// See JPakeParticipant for more details on round 3.
///
/// After execution, the State state} will be STATE_ROUND_3_CREATED.
/// Throws InvalidOperationException if called prior to CalculateKeyingMaterial, or multiple
/// times.
///
/// The keying material as returned from CalculateKeyingMaterial().
public virtual JPakeRound3Payload CreateRound3PayloadToSend(BigInteger keyingMaterial)
{
if (this.state >= STATE_ROUND_3_CREATED)
throw new InvalidOperationException("Round 3 payload already created for " + this.participantId);
if (this.state < STATE_KEY_CALCULATED)
throw new InvalidOperationException("Keying material must be calculated prior to creating round 3 payload for " + this.participantId);
BigInteger macTag = JPakeUtilities.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.
///
/// See JPakeParticipant for more details on round 3.
///
/// After execution, the State state will be STATE_ROUND_3_VALIDATED.
///
/// Throws CryptoException if validation fails. Throws InvalidOperationException if called prior to
/// CalculateKeyingMaterial or multiple times
///
/// The round 3 payload received from the other participant.
/// The keying material as returned from CalculateKeyingMaterial().
public virtual void ValidateRound3PayloadReceived(JPakeRound3Payload round3PayloadReceived, BigInteger keyingMaterial)
{
if (this.state >= STATE_ROUND_3_VALIDATED)
throw new InvalidOperationException("Validation already attempted for round 3 payload for " + this.participantId);
if (this.state < STATE_KEY_CALCULATED)
throw new InvalidOperationException("Keying material must be calculated prior to validating round 3 payload for " + this.participantId);
JPakeUtilities.ValidateParticipantIdsDiffer(participantId, round3PayloadReceived.ParticipantId);
JPakeUtilities.ValidateParticipantIdsEqual(this.partnerParticipantId, round3PayloadReceived.ParticipantId);
JPakeUtilities.ValidateMacTag(
this.participantId,
this.partnerParticipantId,
this.gx1,
this.gx2,
this.gx3,
this.gx4,
keyingMaterial,
this.digest,
round3PayloadReceived.MacTag);
// Clear the rest of the fields.
this.gx1 = null;
this.gx2 = null;
this.gx3 = null;
this.gx4 = null;
this.state = STATE_ROUND_3_VALIDATED;
}
}
}
#pragma warning restore
#endif