Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Hash by max time allotted #787 #887

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,15 @@ bcrypt.hash(myPlaintextPassword, saltRounds, function(err, hash) {

Note that both techniques achieve the same end-result.

Technique 3 (same as technique 2 but defining work in terms of time (not rounds)):

exptime = maximum time allotted for hashing [#787](https://github.com/kelektiv/node.bcrypt.js/issues/787)
```javascript
bcrypt.hashByTime(myPlaintextPassword, expTime, function(err, hash) {
// Store hash in your password DB.
});
```

#### To check a password:

```javascript
Expand Down
110 changes: 110 additions & 0 deletions bcrypt.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,65 @@ module.exports.genSalt = function genSalt(rounds, minor, cb) {
});
};

module.exports.genSaltByTime = function genSaltByTime(exptime, minor, cb) {
var error;
var rounds = 10;
// if callback is first argument, then use defaults for others
if (typeof arguments[0] === 'function') {
// have to set callback first otherwise arguments are overriden
cb = arguments[0];
exptime = 100;
minor = 'b';
// callback is second argument
} else if (typeof arguments[1] === 'function') {
// have to set callback first otherwise arguments are overriden
cb = arguments[1];
minor = 'b';
}

if (!cb) {
return promises.promise(genSaltByTime, this, [exptime, minor]);
}

// default 100 milliseconds and minimum 4 miliseconds
if (!exptime) {
exptime = 100;
} else if (exptime < 4) {
exptime = 4;
} else if (typeof exptime !== 'number') {
// callback error asynchronously
error = new Error('Expected time must be a number');
return process.nextTick(function() {
cb(error);
});
}

if (!minor) {
minor = 'b'
} else if (minor !== 'b' && minor !== 'a') {
error = new Error('minor must be either "a" or "b"');
return process.nextTick(function() {
cb(error);
});
}

crypto.randomBytes(16, function(error, randomBytes) {
if (error) {
cb(error);
return;
}

//since the relation b/w expected time and rounds roughly follows exptime = 2^(rounds-3)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Source of this?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While working with the code, I observed that the time taken for each round was going exponential. When I analyzed the actual time taken with the help of following snippet.

var bcrypt=require('./bcrypt.js');
async function pass(){
    for (let rounds = 1; rounds <= 20; rounds++){
        var ini = new Date();
        var hashpass = await bcrypt.hashSync("Blueisthecolorofsky", rounds);
        var fin = new Date();
        console.log(rounds,fin-ini);
    }
}
pass();

and plotted the following graph with the help of Excel,

Graph1

and compared it with the graph of 2^(rounds) with different proportionality constants k:

Graph2

where it was observed to be very close with constant k=1/8, leading us to believe that time taken for each round closely follows the formula:

expected_time = (1/8) * 2^(number_of_rounds)

Thus it was concluded that:

number_of_rounds = log2(expected_time) + 3

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As processors go faster and architectures change, the k value is going to change, the best tool is to benchmark on your server

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't know much about this, but couldn't a max time be set then it will do more rounds until time capped--with a minimum value? or is bcrypt not built like this

//rounds is equal to log2(exptime)+3
rounds = Math.log(exptime)/Math.log(2);
rounds = Math.round(rounds)+3;
// for a secure hash, taking 4 as minimum rounds
rounds = Math.max(rounds, 4);

bindings.gen_salt(minor, rounds, randomBytes, cb);
});
};

/// hash data using a salt
/// @param {String|Buffer} data the data to encrypt
/// @param {String} salt the salt to use when hashing
Expand Down Expand Up @@ -157,6 +216,57 @@ module.exports.hash = function hash(data, salt, cb) {
return bindings.encrypt(data, salt, cb);
};

module.exports.hashByTime = function hashByTime(data, salt, cb) {
var error;

if (typeof data === 'function') {
error = new Error('data must be a string or Buffer and salt must either be a salt string or a number of rounds');
return process.nextTick(function() {
data(error);
});
}

if (typeof salt === 'function') {
error = new Error('data must be a string or Buffer and salt must either be a salt string or a number of rounds');
return process.nextTick(function() {
salt(error);
});
}

// cb exists but is not a function
// return a rejecting promise
if (cb && typeof cb !== 'function') {
return promises.reject(new Error('cb must be a function or null to return a Promise'));
}

if (!cb) {
return promises.promise(hashByTime, this, [data, salt]);
}

if (data == null || salt == null) {
error = new Error('data and salt arguments required');
return process.nextTick(function() {
cb(error);
});
}

if (!(typeof data === 'string' || data instanceof Buffer) || (typeof salt !== 'string' && typeof salt !== 'number')) {
error = new Error('data must be a string or Buffer and salt must either be a salt string or a number of rounds');
return process.nextTick(function() {
cb(error);
});
}


if (typeof salt === 'number') {
return module.exports.genSaltByTime(salt, function(err, salt) {
return bindings.encrypt(data, salt, cb);
});
}

return bindings.encrypt(data, salt, cb);
};

/// compare raw data to hash
/// @param {String|Buffer} data the data to hash and compare
/// @param {String} hash expected hash
Expand Down