Skip to content

Commit

Permalink
Add helper to check for password input length
Browse files Browse the repository at this point in the history
  • Loading branch information
dcodeIO committed Feb 13, 2025
1 parent e09eb9a commit d5656b3
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 80 deletions.
32 changes: 18 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ increasing computation power. ([see](http://en.wikipedia.org/wiki/Bcrypt))
While bcrypt.js is compatible to the C++ bcrypt binding, it is written in pure JavaScript and thus slower ([about 30%](https://github.com/dcodeIO/bcrypt.js/wiki/Benchmark)), effectively reducing the number of iterations that can be
processed in an equal time span.

The maximum input length is 72 bytes (note that UTF8 encoded characters use up to 4 bytes) and the length of generated
hashes is 60 characters.
The maximum input length is 72 bytes (note that UTF-8 encoded characters use up to 4 bytes) and the length of generated
hashes is 60 characters. Note that maximum input length is not implicitly checked by the library for compatibility with
the C++ binding on Node.js, but should be checked with `bcrypt.truncates(password)` where necessary.

## Usage

Expand Down Expand Up @@ -150,23 +151,26 @@ Usage: bcrypt <input> [rounds|salt]
- bcrypt.**genSalt**([rounds: `number`, ]callback: `Callback<string>`): `void`<br />
Asynchronously generates a salt. Number of rounds defaults to 10 when omitted.

- bcrypt.**hashSync**(s: `string`, salt?: `number | string`): `string`
Synchronously generates a hash for the given string. Number of rounds defaults to 10 when omitted.
- bcrypt.**truncates**(password: `string`): `boolean`<br />
Tests if a password will be truncated when hashed, that is its length is greater than 72 bytes when converted to UTF-8.

- bcrypt.**hash**(s: `string`, salt: `number | string`): `Promise<string>`<br />
Asynchronously generates a hash for the given string.
- bcrypt.**hashSync**(password: `string`, salt?: `number | string`): `string`
Synchronously generates a hash for the given password. Number of rounds defaults to 10 when omitted.

- bcrypt.**hash**(s: `string`, salt: `number | string`, callback: `Callback<string>`, progressCallback?: `ProgressCallback`): `void`<br />
Asynchronously generates a hash for the given string.
- bcrypt.**hash**(password: `string`, salt: `number | string`): `Promise<string>`<br />
Asynchronously generates a hash for the given password.

- bcrypt.**compareSync**(s: `string`, hash: `string`): `boolean`<br />
Synchronously tests a string against a hash.
- bcrypt.**hash**(password: `string`, salt: `number | string`, callback: `Callback<string>`, progressCallback?: `ProgressCallback`): `void`<br />
Asynchronously generates a hash for the given password.

- bcrypt.**compare**(s: `string`, hash: `string`): `Promise<boolean>`<br />
Asynchronously compares a string against a hash.
- bcrypt.**compareSync**(password: `string`, hash: `string`): `boolean`<br />
Synchronously tests a password against a hash.

- bcrypt.**compare**(s: `string`, hash: `string`, callback: `Callback<boolean>`, progressCallback?: `ProgressCallback`)<br />
Asynchronously compares a string against a hash.
- bcrypt.**compare**(password: `string`, hash: `string`): `Promise<boolean>`<br />
Asynchronously compares a password against a hash.

- bcrypt.**compare**(password: `string`, hash: `string`, callback: `Callback<boolean>`, progressCallback?: `ProgressCallback`)<br />
Asynchronously compares a password against a hash.

- bcrypt.**getRounds**(hash: `string`): `number`<br />
Gets the number of rounds used to encrypt the specified hash.
Expand Down
99 changes: 57 additions & 42 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,42 +148,42 @@ export function genSalt(rounds, seed_length, callback) {
}

/**
* Synchronously generates a hash for the given string.
* @param {string} s String to hash
* Synchronously generates a hash for the given password.
* @param {string} password Password to hash
* @param {(number|string)=} salt Salt length to generate or salt to use, default to 10
* @returns {string} Resulting hash
*/
export function hashSync(s, salt) {
export function hashSync(password, salt) {
if (typeof salt === "undefined") salt = GENSALT_DEFAULT_LOG2_ROUNDS;
if (typeof salt === "number") salt = genSaltSync(salt);
if (typeof s !== "string" || typeof salt !== "string")
throw Error("Illegal arguments: " + typeof s + ", " + typeof salt);
return _hash(s, salt);
if (typeof password !== "string" || typeof salt !== "string")
throw Error("Illegal arguments: " + typeof password + ", " + typeof salt);
return _hash(password, salt);
}

/**
* Asynchronously generates a hash for the given string.
* @param {string} s String to hash
* Asynchronously generates a hash for the given password.
* @param {string} password Password to hash
* @param {number|string} salt Salt length to generate or salt to use
* @param {function(Error, string=)=} callback Callback receiving the error, if any, and the resulting hash
* @param {function(number)=} progressCallback Callback successively called with the percentage of rounds completed
* (0.0 - 1.0), maximally once per `MAX_EXECUTION_TIME = 100` ms.
* @returns {!Promise} If `callback` has been omitted
* @throws {Error} If `callback` is present but not a function
*/
export function hash(s, salt, callback, progressCallback) {
export function hash(password, salt, callback, progressCallback) {
function _async(callback) {
if (typeof s === "string" && typeof salt === "number")
if (typeof password === "string" && typeof salt === "number")
genSalt(salt, function (err, salt) {
_hash(s, salt, callback, progressCallback);
_hash(password, salt, callback, progressCallback);
});
else if (typeof s === "string" && typeof salt === "string")
_hash(s, salt, callback, progressCallback);
else if (typeof password === "string" && typeof salt === "string")
_hash(password, salt, callback, progressCallback);
else
nextTick(
callback.bind(
this,
Error("Illegal arguments: " + typeof s + ", " + typeof salt),
Error("Illegal arguments: " + typeof password + ", " + typeof salt),
),
);
}
Expand Down Expand Up @@ -220,39 +220,41 @@ function safeStringCompare(known, unknown) {
}

/**
* Synchronously tests a string against a hash.
* @param {string} s String to compare
* Synchronously tests a password against a hash.
* @param {string} password Password to compare
* @param {string} hash Hash to test against
* @returns {boolean} true if matching, otherwise false
* @throws {Error} If an argument is illegal
*/
export function compareSync(s, hash) {
if (typeof s !== "string" || typeof hash !== "string")
throw Error("Illegal arguments: " + typeof s + ", " + typeof hash);
export function compareSync(password, hash) {
if (typeof password !== "string" || typeof hash !== "string")
throw Error("Illegal arguments: " + typeof password + ", " + typeof hash);
if (hash.length !== 60) return false;
return safeStringCompare(
hashSync(s, hash.substring(0, hash.length - 31)),
hashSync(password, hash.substring(0, hash.length - 31)),
hash,
);
}

/**
* Asynchronously compares the given data against the given hash.
* @param {string} s Data to compare
* @param {string} hashValue Data to be compared to
* Asynchronously tests a password against a hash.
* @param {string} password Password to compare
* @param {string} hashValue Hash to test against
* @param {function(Error, boolean)=} callback Callback receiving the error, if any, otherwise the result
* @param {function(number)=} progressCallback Callback successively called with the percentage of rounds completed
* (0.0 - 1.0), maximally once per `MAX_EXECUTION_TIME = 100` ms.
* @returns {!Promise} If `callback` has been omitted
* @throws {Error} If `callback` is present but not a function
*/
export function compare(s, hashValue, callback, progressCallback) {
export function compare(password, hashValue, callback, progressCallback) {
function _async(callback) {
if (typeof s !== "string" || typeof hashValue !== "string") {
if (typeof password !== "string" || typeof hashValue !== "string") {
nextTick(
callback.bind(
this,
Error("Illegal arguments: " + typeof s + ", " + typeof hashValue),
Error(
"Illegal arguments: " + typeof password + ", " + typeof hashValue,
),
),
);
return;
Expand All @@ -262,7 +264,7 @@ export function compare(s, hashValue, callback, progressCallback) {
return;
}
hash(
s,
password,
hashValue.substring(0, 29),
function (err, comp) {
if (err) callback(err);
Expand Down Expand Up @@ -314,6 +316,18 @@ export function getSalt(hash) {
return hash.substring(0, 29);
}

/**
* Tests if a password will be truncated when hashed, that is its length is
* greater than 72 bytes when converted to UTF-8.
* @param {string} password The password to test
* @returns {boolean} `true` if truncated, otherwise `false`
*/
export function truncates(password) {
if (typeof password !== "string")
throw Error("Illegal arguments: " + typeof password);
return utf8Length(password) > 72;
}

/**
* Continues with the callback on the next tick.
* @function
Expand Down Expand Up @@ -960,7 +974,7 @@ function _crypt(b, salt, rounds, callback, progressCallback) {
j;

//Use typed arrays when available - huge speedup!
if (Int32Array) {
if (typeof Int32Array === "function") {
P = new Int32Array(P_ORIG);
S = new Int32Array(S_ORIG);
} else {
Expand Down Expand Up @@ -1014,18 +1028,18 @@ function _crypt(b, salt, rounds, callback, progressCallback) {
}

/**
* Internally hashes a string.
* @param {string} s String to hash
* Internally hashes a password.
* @param {string} password Password to hash
* @param {?string} salt Salt to use, actually never null
* @param {function(Error, string=)=} callback Callback receiving the error, if any, and the resulting hash. If omitted,
* hashing is performed synchronously.
* @param {function(number)=} progressCallback Callback called with the current progress
* @returns {string|undefined} Resulting hash if callback has been omitted, otherwise `undefined`
* @inner
*/
function _hash(s, salt, callback, progressCallback) {
function _hash(password, salt, callback, progressCallback) {
var err;
if (typeof s !== "string" || typeof salt !== "string") {
if (typeof password !== "string" || typeof salt !== "string") {
err = Error("Invalid string / salt: Not a string");
if (callback) {
nextTick(callback.bind(this, err));
Expand Down Expand Up @@ -1070,9 +1084,9 @@ function _hash(s, salt, callback, progressCallback) {
r2 = parseInt(salt.substring(offset + 1, offset + 2), 10),
rounds = r1 + r2,
real_salt = salt.substring(offset + 3, offset + 25);
s += minor >= "a" ? "\x00" : "";
password += minor >= "a" ? "\x00" : "";

var passwordb = utf8Array(s),
var passwordb = utf8Array(password),
saltb = base64_decode(real_salt, BCRYPT_SALT_LEN);

/**
Expand Down Expand Up @@ -1115,23 +1129,23 @@ function _hash(s, salt, callback, progressCallback) {
/**
* Encodes a byte array to base64 with up to len bytes of input, using the custom bcrypt alphabet.
* @function
* @param {!Array.<number>} b Byte array
* @param {number} len Maximum input length
* @param {!Array.<number>} bytes Byte array
* @param {number} length Maximum input length
* @returns {string}
*/
export function encodeBase64(b, len) {
return base64_encode(b, len);
export function encodeBase64(bytes, length) {
return base64_encode(bytes, length);
}

/**
* Decodes a base64 encoded string to up to len bytes of output, using the custom bcrypt alphabet.
* @function
* @param {string} s String to decode
* @param {number} len Maximum output length
* @param {string} string String to decode
* @param {number} length Maximum output length
* @returns {!Array.<number>}
*/
export function decodeBase64(s, len) {
return base64_decode(s, len);
export function decodeBase64(string, length) {
return base64_decode(string, length);
}

export default {
Expand All @@ -1144,6 +1158,7 @@ export default {
compare,
getRounds,
getSalt,
truncates,
encodeBase64,
decodeBase64,
};
15 changes: 13 additions & 2 deletions tests/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,13 @@ const tests = [
assert.equal(bcrypt.getRounds(hash1), 10);
done();
},
function truncates(done) {
assert(!bcrypt.truncates(""));
assert(!bcrypt.truncates("a".repeat(72)));
assert(bcrypt.truncates("a".repeat(73)));
assert(bcrypt.truncates("๏ เป็นมนุษย์สุดประเสริฐเลิศคุณค่า"));
done();
},
function progress(done) {
bcrypt.genSalt(12, function (err, salt) {
assert(!err);
Expand Down Expand Up @@ -173,7 +180,11 @@ const tests = [
},
function compat_hash(done) {
var pass = [
" space ",
"",
" ",
" a ",
"a".repeat(72),
"a".repeat(73),
"Heizölrückstoßabdämpfung",
"Ξεσκεπάζω τὴν ψυχοφθόρα βδελυγμία",
"El pingüino Wenceslao hizo kilómetros bajo exhaustiva lluvia y ",
Expand All @@ -197,7 +208,7 @@ const tests = [
}
done();
},
function compat_roundsOOB(done) {
function compat_rounds(done) {
var salt1 = bcrypt.genSaltSync(0), // $10$ like not set
salt2 = bcryptcpp.genSaltSync(0);
assert.strictEqual(salt1.substring(0, 7), "$2b$10$");
Expand Down
Loading

0 comments on commit d5656b3

Please sign in to comment.