@@ -3,8 +3,11 @@ import { hexToBigInt } from "../utils/hex";
33import { minExponentSize , SrpGroup } from "./srpGroup" ;
44import { createHash , randomBytes } from "node:crypto" ;
55import { BigInteger } from "jsbn" ;
6+ import { safeXORBytes } from "../utils/ops" ;
7+ import { constantTimeEqual } from "../utils/compare" ;
68
79const zero = new BigInteger ( "0" ) ;
10+ const SHA256_SIZE = 32 ;
811
912export class SrpClient {
1013 private ephemeralPrivate : BigInteger = zero ;
@@ -14,7 +17,7 @@ export class SrpClient {
1417 private v : BigInteger = zero ;
1518 private u : BigInteger | null = zero ;
1619 private k : BigInteger = zero ;
17- private premasterKey : BigInteger ;
20+ private premasterKey : BigInteger | null = null ;
1821 private key : Uint8Array | null = null ;
1922 private m : Uint8Array | null = null ;
2023 private cProof : Uint8Array | null = null ;
@@ -115,4 +118,188 @@ export class SrpClient {
115118 }
116119 return this . u ;
117120 }
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+ }
118305}
0 commit comments