CVE-2025-54885
Description
Thinbus Javascript Secure Remote Password is a browser SRP6a implementation for zero-knowledge password authentication. In versions 2.0.0 and below, a protocol compliance bug causes the client to generate a fixed 252 bits of entropy instead of the intended bit length of the safe prime (defaulted to 2048 bits). The client public value is being generated from a private value that is 4 bits below the specification. This reduces the protocol's designed security margin it is now practically exploitable. The servers full sized 2048 bit random number is used to create the shared session key and password proof. This is fixed in version 2.0.1.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
thinbus-srpnpm | < 2.0.1 | 2.0.1 |
Patches
2a7dcbbd989c4aa7064c1db72Merge pull request #30 from simbo1905/fix-client-hexlength-entropy
6 files changed · +118 −51
browser.js+0 −12 modified@@ -161,20 +161,16 @@ function srpClientFactory (N_base10, g_base10, k_base16) { }; // public helper - /* jshint ignore:start */ SRP6JavascriptClientSession.prototype.fromHex = function(s) { "use strict"; return new BigInteger(""+s, 16); // jdk1.7 rhino requires string concat }; - /* jshint ignore:end */ // public helper to hide BigInteger from the linter - /* jshint ignore:start */ SRP6JavascriptClientSession.prototype.BigInteger = function(string, radix) { "use strict"; return new BigInteger(""+string, radix); // jdk1.7 rhino requires string concat }; - /* jshint ignore:end */ // public getter of the current workflow state. @@ -224,9 +220,7 @@ function srpClientFactory (N_base10, g_base10, k_base16) { "use strict"; var s = null; - /* jshint ignore:start */ s = randomStrings.hex(32); // 16 bytes - /* jshint ignore:end */ // if you invoke without passing the string parameter the '+' operator uses 'undefined' so no nullpointer risk here var ss = this.H((new Date())+':'+opionalServerSalt+':'+s); @@ -299,7 +293,6 @@ function srpClientFactory (N_base10, g_base10, k_base16) { //console.log("SRP6JavascriptClientSession.prototype.computeU"); this.check(Astr, "Astr"); this.check(Bstr, "Bstr"); - /* jshint ignore:start */ var output = this.H(Astr+Bstr); //console.log("js raw u:"+output); var u = new BigInteger(""+output,16); @@ -308,16 +301,13 @@ function srpClientFactory (N_base10, g_base10, k_base16) { throw new Error("SRP6Exception bad shared public value 'u' as u==0"); } return u; - /* jshint ignore:end */ }; SRP6JavascriptClientSession.prototype.random16byteHex = function() { "use strict"; var r1 = null; - /* jshint ignore:start */ r1 = random16byteHex.random(); - /* jshint ignore:end */ return r1; }; @@ -413,9 +403,7 @@ function srpClientFactory (N_base10, g_base10, k_base16) { var ZERO = null; - /* jshint ignore:start */ ZERO = BigInteger.ZERO; - /* jshint ignore:end */ if (this.B.mod(this.N()).equals(ZERO)) { throw new Error("SRP6Exception bad server public value 'B' as B == 0 (mod N)");
client-exports.js+20 −29 modified@@ -119,7 +119,7 @@ function srpClientFactory (N_base10, g_base10, k_base16) { //console.log("js hash:"+hash) //console.log("js x before modN "+this.fromHex(hash)); - this.x = this.fromHex(hash).mod(this.N()); + this.x = this.fromHex(hash).mod(this.N); return this.x; }; @@ -149,32 +149,34 @@ function srpClientFactory (N_base10, g_base10, k_base16) { this.check(B, "B"); var exp = u.multiply(x).add(a); - var tmp = this.g().modPow(x, this.N()).multiply(k); - return B.subtract(tmp).modPow(exp, this.N()); + var tmp = this.g.modPow(x, this.N).multiply(k); + return B.subtract(tmp).modPow(exp, this.N); }; } // public helper SRP6JavascriptClientSession.prototype.toHex = function(n) { "use strict"; + if (n === null || n === undefined || typeof n.toString !== 'function') { + throw new Error("Invalid parameter for hex conversion: " + typeof n); + } return n.toString(16); }; // public helper - /* jshint ignore:start */ SRP6JavascriptClientSession.prototype.fromHex = function(s) { "use strict"; + if (s === null || s === undefined || typeof s !== 'string') { + throw new Error("Invalid hex string for BigInteger conversion: " + typeof s); + } return new BigInteger(""+s, 16); // jdk1.7 rhino requires string concat }; - /* jshint ignore:end */ // public helper to hide BigInteger from the linter - /* jshint ignore:start */ SRP6JavascriptClientSession.prototype.BigInteger = function(string, radix) { "use strict"; return new BigInteger(""+string, radix); // jdk1.7 rhino requires string concat }; - /* jshint ignore:end */ // public getter of the current workflow state. @@ -224,9 +226,7 @@ function srpClientFactory (N_base10, g_base10, k_base16) { "use strict"; var s = null; - /* jshint ignore:start */ s = randomStrings.hex(32); // 16 bytes - /* jshint ignore:end */ // if you invoke without passing the string parameter the '+' operator uses 'undefined' so no nullpointer risk here var ss = this.H((new Date())+':'+opionalServerSalt+':'+s); @@ -249,7 +249,7 @@ function srpClientFactory (N_base10, g_base10, k_base16) { // no need to check the parameters as generateX will do this var x = this.generateX(salt, identity, password); //console.log("js x: "+this.toHex(x)); - this.v = this.g().modPow(x, this.N()); + this.v = this.g.modPow(x, this.N); //console.log("js v: "+this.toHex(this.v)); return this.toHex(this.v); }; @@ -299,7 +299,6 @@ function srpClientFactory (N_base10, g_base10, k_base16) { //console.log("SRP6JavascriptClientSession.prototype.computeU"); this.check(Astr, "Astr"); this.check(Bstr, "Bstr"); - /* jshint ignore:start */ var output = this.H(Astr+Bstr); //console.log("js raw u:"+output); var u = new BigInteger(""+output,16); @@ -308,16 +307,13 @@ function srpClientFactory (N_base10, g_base10, k_base16) { throw new Error("SRP6Exception bad shared public value 'u' as u==0"); } return u; - /* jshint ignore:end */ }; SRP6JavascriptClientSession.prototype.random16byteHex = function() { "use strict"; var r1 = null; - /* jshint ignore:start */ r1 = random16byteHex.random(); - /* jshint ignore:end */ return r1; }; @@ -330,13 +326,14 @@ function srpClientFactory (N_base10, g_base10, k_base16) { * to the generated random number. * @param N The safe prime. */ - SRP6JavascriptClientSession.prototype.randomA = function(N) { + SRP6JavascriptClientSession.prototype.randomA = function() { "use strict"; - //console.log("N:"+N); + //console.log("N:"+this.N); + // our ideal number of random bits to use for `a` as long as its bigger than 256 bits - var hexLength = this.toHex(N).length; + var hexLength = this.toHex(this.N).length; var ZERO = this.BigInteger("0", 10); var ONE = this.BigInteger("1", 10); @@ -366,7 +363,7 @@ function srpClientFactory (N_base10, g_base10, k_base16) { // this protected against a buggy browser random number generated generating a constant value // we mod(N) to wrap to the range [0,N) then loop if we get 0 to give [1,N) // mod(N) is broken due to buggy library code so we workaround with modPow(1,N) - r = (oneTimeBi.add(rBi)).modPow(ONE, N); + r = (oneTimeBi.add(rBi)).modPow(ONE, this.N); } //console.log("r:"+r); @@ -413,11 +410,9 @@ function srpClientFactory (N_base10, g_base10, k_base16) { var ZERO = null; - /* jshint ignore:start */ ZERO = BigInteger.ZERO; - /* jshint ignore:end */ - if (this.B.mod(this.N()).equals(ZERO)) { + if (this.B.mod(this.N).equals(ZERO)) { throw new Error("SRP6Exception bad server public value 'B' as B == 0 (mod N)"); } @@ -432,11 +427,11 @@ function srpClientFactory (N_base10, g_base10, k_base16) { //console.log("N:"+this.toHex(this.N).toString(16)); - this.a = this.randomA(this.N); + this.a = this.randomA(); //console.log("a:" + this.toHex(this.a)); - this.A = this.g().modPow(this.a, this.N()); + this.A = this.g.modPow(this.a, this.N); //console.log("A:" + this.toHex(this.A)); this.check(this.A, "A"); @@ -531,13 +526,9 @@ function srpClientFactory (N_base10, g_base10, k_base16) { SRP6JavascriptClientSessionSHA256.prototype = new SRP6JavascriptClientSession(); - SRP6JavascriptClientSessionSHA256.prototype.N = function() { - return new BigInteger(N_base10, 10); - } + SRP6JavascriptClientSessionSHA256.prototype.N = new BigInteger(N_base10, 10); - SRP6JavascriptClientSessionSHA256.prototype.g = function() { - return new BigInteger(g_base10, 10); - } + SRP6JavascriptClientSessionSHA256.prototype.g = new BigInteger(g_base10, 10); SRP6JavascriptClientSessionSHA256.prototype.H = function (x) { return SHA256(x).toString().toLowerCase();
.jshintrc+41 −0 added@@ -0,0 +1,41 @@ +{ + "esversion": 11, + "browser": true, + "node": true, + "strict": false, + "undef": false, + "unused": false, + "predef": [ + "BigInteger", + "randomStrings", + "SHA256", + "globalThis", + "module", + "exports", + "require", + "console", + "CryptoJS", + "random16byteHex", + "define", + "self" + ], + "maxerr": 200, + "evil": true, + "expr": true, + "funcscope": false, + "iterator": false, + "lastsemic": true, + "laxbreak": false, + "laxcomma": false, + "loopfunc": false, + "multistr": false, + "onecase": false, + "proto": false, + "regexdash": false, + "scripturl": false, + "smarttabs": false, + "shadow": true, + "sub": false, + "supernew": false, + "validthis": false +} \ No newline at end of file
package.json+3 −1 modified@@ -15,7 +15,8 @@ "test:e2e:headed": "HEADED=true npm run test:e2e", "test:e2e:debug": "DEBUG=true HEADED=true npm run test:e2e", "test:e2e:slow": "SLOW=true npm run test:e2e", - "build": "npm run build-es && npm run build-server", + "build": "npm run build-es && npm run build-server && npm run lint", + "lint": "npx jshint client.mjs server.mjs browser.js", "build-legacy": "mkdir -p dist && npm run build-es && rollup -c rollup.config.js", "test:umd": "npm run build-legacy && npm run build-server && mocha e2e/tests/umd.e2e.test.js --timeout 10000", "test:umd:headed": "HEADED=true npm run test:umd", @@ -54,6 +55,7 @@ "express": "^5.1.0", "jasmine-node": "^1.14.5", "jsonfn": "^0.31.0", + "jshint": "^2.13.6", "mocha": "^11.7.1", "puppeteer": "^24.15.0", "rollup": "^4.46.2",
server-exports.js+8 −9 modified@@ -82,7 +82,7 @@ function srpServerFactory (N_base10, g_base10, k_base16) { //return {I: this.I, v: this.toHex(this.v), s: this.toHex(this.salt), b: this.toHex(this.b)}; this.I = obj.I; this.v = this.fromHex(obj.v); - this.salt = this.fromHex(obj.salt); + this.salt = this.fromHex(obj.s); // Note: stored as 's', not 'salt' this.b = this.fromHex(obj.b); this.B = this.g.modPow(this.b, this.N).add(this.v.multiply(this.k)).mod(this.N); this.state = this.STEP_1; @@ -92,24 +92,26 @@ function srpServerFactory (N_base10, g_base10, k_base16) { // public helper SRP6JavascriptServerSession.prototype.toHex = function(n) { "use strict"; + if (n === null || n === undefined || typeof n.toString !== 'function') { + throw new Error("Invalid parameter for hex conversion: " + typeof n); + } return n.toString(16); }; // public helper - /* jshint ignore:start */ SRP6JavascriptServerSession.prototype.fromHex = function(s) { "use strict"; + if (s === null || s === undefined || typeof s !== 'string') { + throw new Error("Invalid hex string for BigInteger conversion: " + typeof s); + } return new BigInteger(""+s, 16); // jdk1.7 rhino requires string concat }; - /* jshint ignore:end */ // public helper to hide BigInteger from the linter - /* jshint ignore:start */ SRP6JavascriptServerSession.prototype.BigInteger = function(string, radix) { "use strict"; return new BigInteger(""+string, radix); // jdk1.7 rhino requires string concat }; - /* jshint ignore:end */ // public getter of the current workflow state. @@ -209,7 +211,6 @@ function srpServerFactory (N_base10, g_base10, k_base16) { //console.log("SRP6JavascriptServerSession.prototype.computeU"); this.check(Astr, "Astr"); this.check(Bstr, "Bstr"); - /* jshint ignore:start */ var output = this.H(Astr+Bstr); //console.log("js raw u:"+output); var u = new BigInteger(""+output,16); @@ -218,16 +219,13 @@ function srpServerFactory (N_base10, g_base10, k_base16) { throw new Error("SRP6Exception bad shared public value 'u' as u==0"); } return u; - /* jshint ignore:end */ }; SRP6JavascriptServerSession.prototype.random16byteHex = function() { "use strict"; var r1 = null; - /* jshint ignore:start */ r1 = random16byteHex.random(); - /* jshint ignore:end */ return r1; }; @@ -242,6 +240,7 @@ function srpServerFactory (N_base10, g_base10, k_base16) { SRP6JavascriptServerSession.prototype.randomB = function() { "use strict"; + // our ideal number of random bits to use for `a` as long as its bigger than 256 bits var hexLength = this.toHex(this.N).length;
test/testrunner-esm.js+46 −0 modified@@ -127,6 +127,52 @@ try { console.log("✅ Session keys generated and verified"); console.log("✅ Full SRP round-trip successful"); + console.log("\n🧪 RFC 5054 ENTROPY TEST (SHOULD FAIL)"); + console.log("======================================"); + + // RFC 5054 specifies N should be 1024-bit = 257 hex chars (including leading zero padding) + // Both client and server should generate same length randoms based on N + const testClient = new SRP6JavascriptClientSession(); + const testServer = new SRP6JavascriptServerSession(); + + // Test client randomA - currently broken, passes function instead of BigInteger + const clientA = testClient.randomA(testClient.N); // BUG: passes function + const clientAHex = testClient.toHex(clientA); + console.log(`❌ CLIENT randomA hex length: ${clientAHex.length} chars`); + console.log(`❌ CLIENT randomA value: ${clientAHex.substring(0, 32)}...`); + + // Test server randomB - should be correct + const serverB = testServer.randomB(); + const serverBHex = testServer.toHex(serverB); + console.log(`🔍 SERVER randomB hex length: ${serverBHex.length} chars`); + console.log(`🔍 SERVER randomB value: ${serverBHex.substring(0, 32)}...`); + + // Test what N should actually be + const clientNActual = testClient.N; // Access the property to get BigInteger + const clientNHex = testClient.toHex(clientNActual); + console.log(`✅ CORRECT N hex length: ${clientNHex.length} chars`); + console.log(`✅ CORRECT N value: ${clientNHex.substring(0, 32)}...`); + + // RFC 5054 compliance check - this codebase uses 2048-bit not 1024-bit + const expectedNLength = 512; // 2048 bits / 4 bits per hex char = 512 + console.log(`📋 This codebase uses 2048-bit N, expected length: ${expectedNLength} chars`); + + if (clientAHex.length !== expectedNLength) { + console.error(`❌ ENTROPY BUG: Client A is ${clientAHex.length} chars, should be ${expectedNLength}`); + } else { + console.log(`✅ ENTROPY FIX CONFIRMED: Client A is ${clientAHex.length} chars, matches expected ${expectedNLength}`); + } + if (serverBHex.length !== expectedNLength) { + console.error(`❌ ENTROPY BUG: Server B is ${serverBHex.length} chars, should be ${expectedNLength}`); + } else { + console.log(`✅ Server B entropy correct: ${serverBHex.length} chars matches expected ${expectedNLength}`); + } + if (clientNHex.length !== expectedNLength) { + console.error(`❌ PARAMETER BUG: N is ${clientNHex.length} chars, should be ${expectedNLength}`); + } else { + console.log(`✅ N parameter correct: ${clientNHex.length} chars matches expected ${expectedNLength}`); + } + console.log("\n📊 COMPATIBILITY TEST"); console.log("=====================");
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-8q6v-474h-whggghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-54885ghsaADVISORY
- github.com/simbo1905/thinbus-srp-npm/commit/aa7064c1db7294ce867e9bc92f26fa6c71a5a2cbghsaWEB
- github.com/simbo1905/thinbus-srp-npm/issues/28nvdWEB
- github.com/simbo1905/thinbus-srp-npm/pull/30/commits/4aeaea2366e090765a8204059c7bcf3616438d31nvdWEB
- github.com/simbo1905/thinbus-srp-npm/security/advisories/GHSA-8q6v-474h-whggnvdWEB
News mentions
0No linked articles in our index yet.