diff --git a/index.js b/index.js index 4a19b3d..5071637 100644 --- a/index.js +++ b/index.js @@ -82,6 +82,7 @@ export function setRandomFallback(random) { * @param {number=} rounds Number of rounds to use, defaults to 10 if omitted * @param {number=} seed_length Not supported. * @returns {string} Resulting salt + * @throws {Error} If rounds is not an integer * @throws {Error} If a random fallback is required but not set */ export function genSaltSync(rounds, seed_length) { @@ -90,6 +91,8 @@ export function genSaltSync(rounds, seed_length) { throw Error( "Illegal arguments: " + typeof rounds + ", " + typeof seed_length, ); + if (!Number.isInteger(rounds)) + throw Error("Illegal arguments: rounds must be an integer"); if (rounds < 4) rounds = 4; else if (rounds > 31) rounds = 31; var salt = []; @@ -108,6 +111,7 @@ export function genSaltSync(rounds, seed_length) { * @param {function(Error, string=)=} callback Callback receiving the error, if any, and the resulting salt * @returns {!Promise} If `callback` has been omitted * @throws {Error} If `callback` is present but not a function + * @rejects {Error} If rounds is not an integer */ export function genSalt(rounds, seed_length, callback) { if (typeof seed_length === "function") @@ -172,6 +176,10 @@ export function hash(password, salt, callback, progressCallback) { function _async(callback) { if (typeof password === "string" && typeof salt === "number") genSalt(salt, function (err, salt) { + if (err) { + callback(err); + return; + } _hash(password, salt, callback, progressCallback); }); else if (typeof password === "string" && typeof salt === "string") diff --git a/tests/index.js b/tests/index.js index 525a44d..1436a2a 100644 --- a/tests/index.js +++ b/tests/index.js @@ -33,29 +33,111 @@ const tests = [ assert(salt); assert(typeof salt === "string"); assert(salt.length > 0); + + // Negative test cases: non-integer rounds must throw + assert.throws(function () { + bcrypt.genSaltSync(10.5); + }, /Illegal arguments: rounds must be an integer/); + assert.throws(function () { + bcrypt.genSaltSync(Infinity); + }, /Illegal arguments: rounds must be an integer/); + + // Positive test cases: valid integers and boundaries (4 to 31) + // Bcrypt salts follow the Modular Crypt Format (MCF): "$$$" + // Splitting by "$" yields ["", "", "", ""], so parts[2] is always the rounds. + [ + { rounds: 4, expectedCost: "04" }, + { rounds: 10, expectedCost: "10" }, + { rounds: 10.0, expectedCost: "10" }, // 10.0 is represented identically to 10 in JS + { rounds: 31, expectedCost: "31" }, + ].forEach(function (testCase) { + var s = bcrypt.genSaltSync(testCase.rounds); + var parts = s.split("$"); + assert.strictEqual(parts[2], testCase.expectedCost); + }); done(); }, function genSalt(done) { - bcrypt.genSalt(10, function (err, salt) { - assert(!err); - assert(salt); - assert(typeof salt === "string"); - assert(salt.length > 0); - done(); - }); + // Non-integer rounds without a callback: returns a rejected promise + Promise.all([ + assert.rejects( + bcrypt.genSalt(10.5), + /Illegal arguments: rounds must be an integer/, + ), + assert.rejects( + bcrypt.genSalt(Infinity), + /Illegal arguments: rounds must be an integer/, + ), + ]) + .then(function () { + // Non-integer rounds with a callback: error is passed to callback + bcrypt.genSalt(10.5, function (err) { + assert(err); + assert( + /Illegal arguments: rounds must be an integer/.test(err.message), + ); + bcrypt.genSalt(10, function (err, salt) { + assert(!err); + assert(salt); + assert(typeof salt === "string"); + assert(salt.length > 0); + done(); + }); + }); + }) + .catch(function (err) { + assert.fail("genSalt rejection test failed: " + err.message); + }); }, function hashSync(done) { assert.doesNotThrow(function () { bcrypt.hashSync("hello", 10); }); assert.notEqual(bcrypt.hashSync("hello", 10), bcrypt.hashSync("hello", 10)); + assert.throws(function () { + bcrypt.hashSync("x", 10.5); + }, /Illegal arguments: rounds must be an integer/); + assert.throws(function () { + bcrypt.hashSync("x", Infinity); + }, /Illegal arguments: rounds must be an integer/); done(); }, function hash(done) { bcrypt.hash("hello", 10, function (err, hash) { assert(!err); assert(hash); - done(); + Promise.all([ + assert.rejects( + bcrypt.hash("x", 10.5), + /Illegal arguments: rounds must be an integer/, + ), + assert.rejects( + bcrypt.hash("x", Infinity), + /Illegal arguments: rounds must be an integer/, + ), + ]) + .then(function () { + bcrypt.hash("x", 10.5, function (err2, hash2) { + assert(err2); + assert(!hash2); + assert( + /Illegal arguments: rounds must be an integer/.test(err2.message), + ); + bcrypt.hash("x", Infinity, function (err3, hash3) { + assert(err3); + assert(!hash3); + assert( + /Illegal arguments: rounds must be an integer/.test( + err3.message, + ), + ); + done(); + }); + }); + }) + .catch(function (err) { + assert.fail("hash rejection test failed: " + err.message); + }); }); }, function compareSync(done) { diff --git a/types.d.ts b/types.d.ts index 3cbe5b1..9bf616e 100644 --- a/types.d.ts +++ b/types.d.ts @@ -19,6 +19,7 @@ export declare function setRandomFallback(random: RandomFallback): void; * Synchronously generates a salt. * @param rounds Number of rounds to use, defaults to 10 if omitted * @return Resulting salt + * @throws If rounds is not an integer * @throws If a random fallback is required but not set */ export declare function genSaltSync(rounds?: number): string; @@ -27,6 +28,7 @@ export declare function genSaltSync(rounds?: number): string; * Asynchronously generates a salt. * @param rounds Number of rounds to use, defaults to 10 if omitted * @return Promise with resulting salt, if callback has been omitted + * @rejects If rounds is not an integer */ export declare function genSalt(rounds?: number): Promise;