summary refs log tree commit diff
path: root/crypto/src/crypto/agreement/jpake/JPakeParticipant.cs
blob: 794284866811cf761ff18719fa642f6dcd98171e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
using System;

using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Digests;
using Org.BouncyCastle.Math;
using Org.BouncyCastle.Security;

namespace Org.BouncyCastle.Crypto.Agreement.JPake
{
    /// <summary>
    /// 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
    /// <a href="http://grouper.ieee.org/groups/1363/Research/contributions/hao-ryan-2008.pdf">
    /// "Password Authenticated Key Exchange by Juggling, 2008."</a>
    ///
    /// 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 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
    /// <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.
    ///
    /// 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).
    /// </summary>
    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 <tt>STATE_*</tt> constants for possible values.
        private int state;

        /// <summary>
        /// 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.
        /// </summary>
        /// <param name="participantId">Unique identifier of this participant.
        ///      The two participants in the exchange must NOT share the same id.</param>
        /// <param name="password">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.</param>
        public JPakeParticipant(string participantId, char[] password)
            : this(participantId, password, JPakePrimeOrderGroups.NIST_3072) { }

        /// <summary>
        /// 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.
        /// </summary>
        /// <param name="participantId">Unique identifier of this participant.
        ///      The two participants in the exchange must NOT share the same id.</param>
        /// <param name="password">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.</param>
        /// <param name="group">Prime order group. See JPakePrimeOrderGroups for standard groups.</param>
        public JPakeParticipant(string participantId, char[] password, JPakePrimeOrderGroup group)
            : this(participantId, password, group, new Sha256Digest(), new SecureRandom()) { }


        /// <summary>
        /// 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.
        /// </summary>
        /// <param name="participantId">Unique identifier of this participant.
        ///      The two participants in the exchange must NOT share the same id.</param>
        /// <param name="password">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.</param>
        /// <param name="group">Prime order group. See JPakePrimeOrderGroups for standard groups.</param>
        /// <param name="digest">Digest to use during zero knowledge proofs and key confirmation
        ///     (SHA-256 or stronger preferred).</param>
        /// <param name="random">Source of secure random data for x1 and x2, and for the zero knowledge proofs.</param>
        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;
        }

        /// <summary>
        /// Gets the current state of this participant.
        /// See the <tt>STATE_*</tt> constants for possible values.
        /// </summary>
        public virtual int State
        {
            get { return state; }
        }


        /// <summary>
        /// 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}.
        /// </summary>
        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);
        }

        /// <summary>
        /// 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.
        /// </summary>
        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;
        }

        /// <summary>
        /// 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
        /// </summary>
        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);
        }

        /// <summary>
        /// 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
        /// </summary>
        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;
        }

        /// <summary>
        /// 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 <tt>[0, p-1]</tt>.
        ///
        /// 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.
        /// </summary>
        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;
        }

        /// <summary>
        /// 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.
        /// </summary>
        /// <param name="keyingMaterial">The keying material as returned from CalculateKeyingMaterial().</param> 
        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);
        }

        /// <summary>
        /// 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
        /// </summary>
        /// <param name="round3PayloadReceived">The round 3 payload received from the other participant.</param> 
        /// <param name="keyingMaterial">The keying material as returned from CalculateKeyingMaterial().</param> 
        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;
        }
    }
}