Skip to content

Commit 8836e19

Browse files
committed
feat: public methods
1 parent 9831c96 commit 8836e19

File tree

4 files changed

+248
-1
lines changed

4 files changed

+248
-1
lines changed

src/srp/srpClient.ts

Lines changed: 188 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ import { hexToBigInt } from "../utils/hex";
33
import { minExponentSize, SrpGroup } from "./srpGroup";
44
import { createHash, randomBytes } from "node:crypto";
55
import { BigInteger } from "jsbn";
6+
import { safeXORBytes } from "../utils/ops";
7+
import { constantTimeEqual } from "../utils/compare";
68

79
const zero = new BigInteger("0");
10+
const SHA256_SIZE = 32;
811

912
export 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
}

src/utils/compare.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import assert from "assert";
2+
import { constantTimeEqual } from "./compare";
3+
function runTests() {
4+
// Test equal arrays
5+
assert.strictEqual(
6+
constantTimeEqual(new Uint8Array([1, 2, 3]), new Uint8Array([1, 2, 3])),
7+
true,
8+
"Equal arrays should return true"
9+
);
10+
11+
// Test different arrays
12+
assert.strictEqual(
13+
constantTimeEqual(new Uint8Array([1, 2, 3]), new Uint8Array([1, 2, 4])),
14+
false,
15+
"Different arrays should return false"
16+
);
17+
18+
// Test different lengths
19+
assert.strictEqual(
20+
constantTimeEqual(new Uint8Array([1, 2]), new Uint8Array([1, 2, 3])),
21+
false,
22+
"Different length arrays should return false"
23+
);
24+
25+
// Test empty arrays
26+
assert.strictEqual(
27+
constantTimeEqual(new Uint8Array([]), new Uint8Array([])),
28+
true,
29+
"Empty arrays should return true"
30+
);
31+
32+
console.log("All tests passed!");
33+
}
34+
35+
runTests();

src/utils/compare.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean {
2+
if (a.length !== b.length) return false;
3+
4+
let result = 0;
5+
for (let i = 0; i < a.length; i++) {
6+
result |= a[i] ^ b[i];
7+
}
8+
return result === 0;
9+
}

src/utils/ops.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export function safeXORBytes(
2+
dst: Uint8Array,
3+
a: Uint8Array,
4+
b: Uint8Array
5+
): number {
6+
let n: number = a.length;
7+
if (b.length < n) {
8+
n = b.length;
9+
}
10+
11+
for (let i = 0; i < n; i++) {
12+
dst[i] = a[i] ^ b[i];
13+
}
14+
15+
return n;
16+
}

0 commit comments

Comments
 (0)