@@ -3,8 +3,11 @@ import { hexToBigInt } from "../utils/hex";
3
3
import { minExponentSize , SrpGroup } from "./srpGroup" ;
4
4
import { createHash , randomBytes } from "node:crypto" ;
5
5
import { BigInteger } from "jsbn" ;
6
+ import { safeXORBytes } from "../utils/ops" ;
7
+ import { constantTimeEqual } from "../utils/compare" ;
6
8
7
9
const zero = new BigInteger ( "0" ) ;
10
+ const SHA256_SIZE = 32 ;
8
11
9
12
export class SrpClient {
10
13
private ephemeralPrivate : BigInteger = zero ;
@@ -14,7 +17,7 @@ export class SrpClient {
14
17
private v : BigInteger = zero ;
15
18
private u : BigInteger | null = zero ;
16
19
private k : BigInteger = zero ;
17
- private premasterKey : BigInteger ;
20
+ private premasterKey : BigInteger | null = null ;
18
21
private key : Uint8Array | null = null ;
19
22
private m : Uint8Array | null = null ;
20
23
private cProof : Uint8Array | null = null ;
@@ -115,4 +118,188 @@ export class SrpClient {
115
118
}
116
119
return this . u ;
117
120
}
121
+
122
+ public ephemeralPublic ( ) : BigInteger {
123
+ if ( this . ephemeralPublicA . compareTo ( zero ) === 0 ) {
124
+ this . makeA ( ) ;
125
+ }
126
+ return this . ephemeralPublicA ;
127
+ }
128
+
129
+ public verifier ( ) : BigInteger {
130
+ return this . makeVerifier ( ) ;
131
+ }
132
+
133
+ public setOthersPublic ( AorB : BigInteger ) {
134
+ if ( ! this . isPublicValid ( AorB ) ) {
135
+ throw new Error ( "invalid public exponent" ) ;
136
+ }
137
+ this . ephemeralPublicB = AorB ;
138
+ }
139
+
140
+ /*
141
+ Key creates and returns the session Key.
142
+
143
+ Caller MUST check error status.
144
+
145
+ Once the ephemeral public key is received from the other party and properly
146
+ set, SRP should have enough information to compute the session key.
147
+
148
+ If and only if, each party knowns their respective long term secret
149
+ (x for client, v for server) will both parties compute the same Key.
150
+ Be sure to confirm that client and server have the same key before
151
+ using it.
152
+
153
+ Note that although the resulting key is 256 bits, its effective strength
154
+ is (typically) far less and depends on the group used.
155
+ 8 * (SRP.Group.ExponentSize / 2) should provide a reasonable estimate if you
156
+ need that.
157
+ */
158
+ public getKey ( ) : Uint8Array {
159
+ if ( this . key !== null ) {
160
+ return this . key ;
161
+ }
162
+ if ( this . badState ) {
163
+ throw new Error ( "we have bad data" ) ;
164
+ }
165
+ if ( this . u === null || ! this . isUValid ( ) ) {
166
+ this . u = this . calculateU ( ) ;
167
+ }
168
+ if ( ! this . isUValid ( ) ) {
169
+ this . badState = true ;
170
+ throw new Error ( "invalid u" ) ;
171
+ }
172
+ if ( this . ephemeralPrivate . compareTo ( zero ) === 0 ) {
173
+ throw new Error ( "cannot make Key with my ephemeral secret" ) ;
174
+ }
175
+
176
+ let b = new BigInteger ( "0" ) ;
177
+ let e = new BigInteger ( "0" ) ;
178
+
179
+ if (
180
+ this . ephemeralPublicB . compareTo ( zero ) === 0 ||
181
+ this . k . compareTo ( zero ) === 0 ||
182
+ this . x . compareTo ( zero ) === 0
183
+ ) {
184
+ throw new Error ( "not enough is known to create Key" ) ;
185
+ }
186
+ e = this . u . multiply ( this . x ) ;
187
+ e = e . add ( this . ephemeralPrivate ) ;
188
+
189
+ b = this . group . getGenerator ( ) . modPow ( this . x , this . group . getN ( ) ) ;
190
+ b = b . multiply ( this . k ) ;
191
+ b = this . ephemeralPublicB . subtract ( b ) ;
192
+ b = b . mod ( this . group . getN ( ) ) ;
193
+
194
+ this . premasterKey = b . modPow ( e , this . group . getN ( ) ) ;
195
+
196
+ const hash = createHash ( "sha256" ) ;
197
+ hash . update ( new TextEncoder ( ) . encode ( this . premasterKey . toString ( 16 ) ) ) ;
198
+ this . key = new Uint8Array ( hash . digest ( ) ) ;
199
+ return this . key ;
200
+ }
201
+
202
+ /*
203
+ From http://srp.stanford.edu/design.html
204
+
205
+ Client -> Server: M = H(H(N) xor H(g), H(I), s, A, B, Key)
206
+ Server >- Client: H(A, M, K)
207
+
208
+ The client must show its proof first
209
+
210
+ To make that useful, we are going to need to define the hash of big ints.
211
+ We will use math/big Bytes() to get the absolute value as a big-endian byte
212
+ slice (without padding to size of N)
213
+ */
214
+ public computeM ( salt : Uint8Array , uname : string ) : Uint8Array {
215
+ const nLen = bigIntToBytes ( this . group . getN ( ) ) . length ;
216
+ console . log ( `Server padding length: ${ nLen } ` ) ;
217
+
218
+ if ( this . m !== null ) {
219
+ return this . m ;
220
+ }
221
+
222
+ if ( this . key === null ) {
223
+ throw new Error ( "don't try to prove anything before you have the key" ) ;
224
+ }
225
+
226
+ // First lets work on the H(H(A) ⊕ H(g)) part.
227
+ const nHash = new Uint8Array (
228
+ createHash ( "sha256" ) . update ( bigIntToBytes ( this . group . getN ( ) ) ) . digest ( )
229
+ ) ;
230
+ const gHash = new Uint8Array (
231
+ createHash ( "sha256" )
232
+ . update ( bigIntToBytes ( this . group . getGenerator ( ) ) )
233
+ . digest ( )
234
+ ) ;
235
+ let groupXOR = new Uint8Array ( SHA256_SIZE ) ;
236
+ const length = safeXORBytes ( groupXOR , nHash , gHash ) ;
237
+ if ( length !== SHA256_SIZE ) {
238
+ throw new Error (
239
+ `XOR had length ${ length } bytes instead of ${ SHA256_SIZE } `
240
+ ) ;
241
+ }
242
+ const groupHash = new Uint8Array (
243
+ createHash ( "sha256" ) . update ( groupXOR ) . digest ( )
244
+ ) ;
245
+
246
+ const uHash = new Uint8Array (
247
+ createHash ( "sha256" ) . update ( new TextEncoder ( ) . encode ( uname ) ) . digest ( )
248
+ ) ;
249
+
250
+ const m = createHash ( "sha256" ) ;
251
+
252
+ m . update ( groupHash ) ;
253
+ m . update ( uHash ) ;
254
+ m . update ( salt ) ;
255
+ m . update ( bigIntToBytes ( this . ephemeralPublicA ) ) ;
256
+ m . update ( bigIntToBytes ( this . ephemeralPublicB ) ) ;
257
+ m . update ( this . key ) ;
258
+
259
+ this . m = new Uint8Array ( m . digest ( ) ) ;
260
+ return this . m ;
261
+ }
262
+
263
+ public goodServerProof (
264
+ salt : Uint8Array ,
265
+ uname : string ,
266
+ proof : Uint8Array
267
+ ) : boolean {
268
+ let myM : Uint8Array | null = null ;
269
+ try {
270
+ myM = this . computeM ( salt , uname ) ;
271
+ } catch ( e ) {
272
+ console . error ( e ) ;
273
+ // well that's odd. Better return false if something is wrong here
274
+ this . isServerProved = false ;
275
+ return false ;
276
+ }
277
+ this . isServerProved = constantTimeEqual ( myM , proof ) ;
278
+ return this . isServerProved ;
279
+ }
280
+
281
+ public clientProof ( ) : Uint8Array {
282
+ if ( ! this . isServerProved ) {
283
+ throw new Error ( "don't construct client proof until server is proved" ) ;
284
+ }
285
+ if ( this . cProof !== null ) {
286
+ return this . cProof ;
287
+ }
288
+
289
+ if (
290
+ this . ephemeralPublicA . compareTo ( zero ) === 0 ||
291
+ this . m === null ||
292
+ this . key === null
293
+ ) {
294
+ throw new Error ( "not enough pieces in place to construct client proof" ) ;
295
+ }
296
+
297
+ const hash = createHash ( "sha256" ) ;
298
+ hash . update ( bigIntToBytes ( this . ephemeralPublicA ) ) ;
299
+ hash . update ( this . m ) ;
300
+ hash . update ( this . key ) ;
301
+
302
+ this . cProof = new Uint8Array ( hash . digest ( ) ) ;
303
+ return this . cProof ;
304
+ }
118
305
}
0 commit comments