-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
5a9522b
commit 49d267f
Showing
5 changed files
with
216 additions
and
37 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,61 @@ | ||
Hash and verify passwords hashed with PHP's built-in password_* functions | ||
# node-php-password | ||
|
||
Designed to be future proof for new hashing algorithms. | ||
*Verify password hashed generated in PHP, and hash passwords in the same format.* | ||
|
||
Usage: | ||
Designed to be **future proof** for new hashing algorithms. | ||
|
||
node-php-password is a solution for every kind of webapp which has a user database with passwords hashed with PHP's password_hash. Instead of starting from scratch, just use this package to get compatibility with PHP. | ||
|
||
### Usage: | ||
```javascript | ||
var Password = require("node-php-password"); | ||
var hash = Password.hash("password123"); | ||
// Password.hash(password, [algorithm], [options]); | ||
``` | ||
|
||
**Output:** | ||
```javascript | ||
"$2y$10$8mNOnsos8qo4qHLcd32zrOg7gmyvfZ6/o9.2nsP/u6TRbrANdLREy" | ||
``` | ||
|
||
### To verify a password against an existing hash in a database o.l: | ||
```javascript | ||
var Password = require("node-php-password"); | ||
var hash = "$2y$10$8mNOnsos8qo4qHLcd32zrOg7gmyvfZ6/o9.2nsP/u6TRbrANdLREy"; | ||
|
||
var hash = Password.password_hash("password123", "PASSWORD_DEFAULT", { cost: 10 }); | ||
//hash: "$2y$10$8mNOnsos8qo4qHLcd32zrOg7gmyvfZ6/o9.2nsP/u6TRbrANdLREy" | ||
if(Password.verify("password123", hash)){ | ||
//Authentication OK | ||
}else{ | ||
//Authentication FAILED | ||
} | ||
``` | ||
|
||
console.log(Password.password_verify("password123", hash); | ||
//true | ||
### Options | ||
```javascript | ||
var Password = require("node-php-password"); | ||
var options = { | ||
cost: 10, | ||
salt: "qwertyuiopasdfghjklzxc" | ||
} | ||
// Valid algorithms are "PASSWORD_DEFAULT", and "PASSWORD_BCRYPT" | ||
// "PASSWORD_DEFAULT" is just an alias to "PASSWORD_BCRYPT", to be more | ||
// compatible with PHP | ||
var hash = Password.hash("password123", "PASSWORD_DEFAULT", options); | ||
``` | ||
|
||
var hash = $1$7576f3a00f6de47b0c72c5baf2d505b0 | ||
console.log(Password.password_needs_rehash(hash, "PASSWORD_DEFAULT"); | ||
//true | ||
**Output:** | ||
```javascript | ||
"$2y$10$qwertyuiopasdfghjklzxO3U1f6PD/l04UrnxUgya51pjyLtkGNQi" | ||
``` | ||
|
||
WARNING password_needs_rehash is currently not working | ||
### Check if password needs rehash | ||
If you have a mix of passwords hashed with different algorithms (md5, sha256, etc...), or with a different cost value, you can check if they comply with your password policy by checking if they need a rehash. If they do, you can prompt your user to update their password. | ||
```javascript | ||
var Password = require("node-php-password"); | ||
var hash = Password.hash("password123", "PASSWORD_DEFAULT", {cost: 10}); | ||
if(Password.needsRehash(hash, "PASSWORD_DEFAULT", {cost: 11}){ | ||
//Password needs rehash, prompt the user to renew their password | ||
}else{ | ||
//Password is OK. | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,18 @@ | ||
/** | ||
* bcrypt | ||
* | ||
* @package node-php-password | ||
* @copyright (c) 2016, Thomas Alrek | ||
* @author Thomas Alrek <[email protected]> | ||
*/ | ||
|
||
exports.name = "PASSWORD_BCRYPT"; | ||
|
||
var BCrypt = require("bcryptjs"); | ||
var expression = /\$(2[a|x|y])\$(\d+)\$(.{53})/g; | ||
var defaultOptions = { | ||
cost: 10 | ||
} | ||
|
||
function verify(password, hash){ | ||
expression.lastIndex = 0; | ||
|
@@ -12,16 +23,25 @@ function verify(password, hash){ | |
|
||
function hash(password, options){ | ||
expression.lastIndex = 0; | ||
var salt; | ||
if(typeof options == 'undefined'){ | ||
options = {}; | ||
options = defaultOptions; | ||
} | ||
if(typeof options.cost == 'undefined'){ | ||
options.cost = 10; | ||
options.cost = defaultOptions.cost; | ||
} | ||
if(options.cost < defaultOptions.cost){ | ||
options.cost = defaultOptions.cost; | ||
} | ||
if(options.cost < 10){ | ||
options.cost = 10; | ||
if(typeof options.salt !== 'undefined'){ | ||
console.log("Warning: Password.hash(): Use of the 'salt' option to Password.hash is deprecated"); | ||
if(options.salt.length < 16){ | ||
throw("Provided salt is too short: " + options.salt.length + " expecting 16"); | ||
} | ||
salt = "$2y$" + options.cost + "$" + options.salt; | ||
}else{ | ||
salt = BCrypt.genSaltSync(options.cost); | ||
} | ||
var salt = BCrypt.genSaltSync(options.cost); | ||
var hash = BCrypt.hashSync(password, salt); | ||
var output = expression.exec(hash); | ||
return "$2y$" + options.cost + "$" + output[3];; | ||
|
@@ -37,6 +57,7 @@ function cost(hash){ | |
} | ||
|
||
exports.expression = expression; | ||
exports.defaultOptions = defaultOptions; | ||
exports.verify = verify; | ||
exports.cost = cost; | ||
exports.hash = hash; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,41 +1,60 @@ | ||
/** | ||
* node-php-password | ||
* | ||
* @package node-php-password | ||
* @copyright (c) 2016, Thomas Alrek | ||
* @author Thomas Alrek <[email protected]> | ||
*/ | ||
|
||
var glob = require("glob"); | ||
var path = require('path'); | ||
var algorithms = {}; | ||
var aliases = require("./package.json").aliases; | ||
|
||
/* hold all algorithm modules */ | ||
var algorithms = {}; | ||
|
||
/* load algorithm modules */ | ||
glob.sync('./algorithms/*.js').forEach(function (file) { | ||
try{ | ||
var algorithm = require(path.resolve(file)); | ||
|
||
/* verify loaded module */ | ||
if(typeof(algorithm.name) !== 'string'){ | ||
throw("Module has invalid name"); | ||
throw("Invalid module: Module has an invalid name"); | ||
} | ||
if(typeof algorithms[algorithm.name] !== 'undefined'){ | ||
throw("Multiple module instances with name '" + algorithm.name + "'"); | ||
throw("Invalid module: Multiple module instances with name '" + algorithm.name + "'"); | ||
} | ||
algorithms[algorithm.name] = algorithm; | ||
if(!(algorithms[algorithm.name].expression instanceof RegExp)){ | ||
throw("Module has invalid expression"); | ||
throw("Invalid module: Module has an invalid expression"); | ||
} | ||
if(typeof algorithms[algorithm.name].verify !== 'function'){ | ||
throw("Module verify() is not a valid function"); | ||
throw("Invalid module: Module verify() is not a valid function"); | ||
} | ||
if(typeof algorithms[algorithm.name].cost !== 'function'){ | ||
throw("Module cost() is not a valid function"); | ||
throw("Invalid module: Module cost() is not a valid function"); | ||
} | ||
if(typeof algorithms[algorithm.name].hash !== 'function'){ | ||
throw("Module hash() is not a valid function"); | ||
throw("Invalid module: Module hash() is not a valid function"); | ||
} | ||
}catch(e){ | ||
throw("Invalid algorithm module"); | ||
throw("Invalid module"); | ||
} | ||
}); | ||
|
||
/* check if any modules where loaded, otherwise throw error */ | ||
if(algorithms.length == 0){ | ||
throw("exception no algorithms loaded"); | ||
} | ||
|
||
function password_get_info(hash){ | ||
/** | ||
* Get information from password hash | ||
* @param {string} hash A password hash to check | ||
* @return {object} Info object | ||
* @throws {Exception} Will throw an exception if unable to parse hash | ||
*/ | ||
function getInfo(hash){ | ||
var found = false; | ||
var info = { | ||
algoName: "", | ||
|
@@ -58,7 +77,15 @@ function password_get_info(hash){ | |
return info; | ||
} | ||
|
||
function password_hash(password, algorithm, options){ | ||
/** | ||
* Hash a password string | ||
* @param {string} password The plaintext password to hash | ||
* @param {string} algorithm Algorithm name, e.g "PASSWORD_DEFAULT" | ||
* @param {object} options Options to pass to the hashing algorithm | ||
* @return {string} Password hash | ||
* @throws {Exception} Will throw an exception if an invalid algorithm is passed | ||
*/ | ||
function hash(password, algorithm, options){ | ||
var algo; | ||
if(typeof algorithm == 'undefined'){ | ||
algorithm = "PASSWORD_DEFAULT"; | ||
|
@@ -76,21 +103,33 @@ function password_hash(password, algorithm, options){ | |
return algo.hash(password, options); | ||
} | ||
|
||
function password_needs_rehash(hash, algorithm, options){ | ||
/** | ||
* Check if a given password needs to be rehashed | ||
* @param {string} hash A password hash to check | ||
* @param {string} algorithm Algorithm name, e.g "PASSWORD_DEFAULT" | ||
* @param {object} options Options to pass to the hashing algorithm | ||
* @return {bool} true if password needs rehash, otherwise false | ||
* @throws {Exception} Will throw an exception if an invalid algorithm is passed | ||
*/ | ||
function needsRehash(hash, algorithm, options){ | ||
var info = {}; | ||
try{ | ||
var info = password_get_info(hash); | ||
info = getInfo(hash); | ||
}catch(e){ | ||
/* unable to parse hash, so we assume it's an old or unknown format */ | ||
return true; | ||
} | ||
/* check if the supplied algorithm name is an alias */ | ||
if(typeof aliases[algorithm] !== 'undefined'){ | ||
algorithm = aliases[algorithm]; | ||
} | ||
/* unable to compare, because an invalid algorithm was supplied */ | ||
if(typeof algorithms[algorithm] == 'undefined'){ | ||
throw("exception unknown algorithm"); | ||
} | ||
if(algorithms[algorithm].name == info.algoName){ | ||
if(typeof options !== 'undefined' && typeof options.cost !== 'undefined'){ | ||
if(info.options.cost != options.cost){ | ||
if(info.options.cost < options.cost){ | ||
return true; | ||
} | ||
} | ||
|
@@ -99,12 +138,19 @@ function password_needs_rehash(hash, algorithm, options){ | |
return true; | ||
} | ||
|
||
function password_verify(password, hash){ | ||
var info = password_get_info(hash); | ||
/** | ||
* Verify a plaintext password against hash | ||
* @param {string} password The plaintext password to check | ||
* @param {string} hash A password hash to check | ||
* @return {bool} true if password is verified against given hash, otherwise false | ||
* @throws {Exception} Will throw an exception if unable to parse hash | ||
*/ | ||
function verify(password, hash){ | ||
var info = getInfo(hash); | ||
return algorithms[info.algoName].verify(password, hash); | ||
} | ||
|
||
exports.password_get_info = password_get_info; | ||
exports.password_hash = password_hash; | ||
exports.password_needs_rehash = password_needs_rehash; | ||
exports.password_verify = password_verify; | ||
exports.getInfo = getInfo; | ||
exports.hash = hash; | ||
exports.needsRehash = needsRehash; | ||
exports.verify = verify; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,21 +1,36 @@ | ||
{ | ||
"name": "node-php-password", | ||
"version": "0.0.1", | ||
"version": "0.1.0", | ||
"description": "A node compatibility layer to verify and hash php compatible password hashes", | ||
"main": "index.js", | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/alrek-consulting/node-php-password.git" | ||
}, | ||
"keywords": [ | ||
"php", | ||
"password", | ||
"security", | ||
"bcrypt", | ||
"hash" | ||
], | ||
"scripts": { | ||
"test": "echo \"Error: no test specified\" && exit 1" | ||
"test": "mocha --reporter spec test" | ||
}, | ||
"author": "Thomas Alrek <[email protected]>", | ||
"license": "GPL-2.0", | ||
"bugs": { | ||
"url": "https://github.com/alrek-consulting/node-php-password/issues" | ||
}, | ||
"homepage": "https://github.com/alrek-consulting/node-php-password#readme", | ||
"dependencies": { | ||
"bcryptjs": "^2.3.0", | ||
"glob": "^7.0.5" | ||
}, | ||
"devDependencies": { | ||
"mocha": "^2.4.5", | ||
"chai": "^3.5.0" | ||
}, | ||
"aliases": { | ||
"PASSWORD_DEFAULT": "PASSWORD_BCRYPT" | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
/** | ||
* test | ||
* | ||
* @package node-php-password | ||
* @copyright (c) 2016, Thomas Alrek | ||
* @author Thomas Alrek <[email protected]> | ||
*/ | ||
|
||
var assert = require('assert'); | ||
var expect = require('chai').expect; | ||
var Password = require("../index.js"); | ||
var package = require("../package.json"); | ||
var test_password = "password123"; | ||
|
||
describe('PHP-Password', function() { | ||
var test_hash = Password.hash(test_password, "PASSWORD_DEFAULT", {cost: 10}); | ||
describe('constants', function () { | ||
it('"PASSWORD_DEFAULT" == "PASSWORD_BCRYPT"', function () { | ||
assert(package.aliases["PASSWORD_DEFAULT"] == "PASSWORD_BCRYPT", "'PASSWORD_DEFAULT' is not 'PASSWORD_BCRYPT'"); | ||
}); | ||
}); | ||
describe('hash', function () { | ||
it('Hash is not equal to Password', function () { | ||
assert(test_hash !== test_password, 'Hash is equal to Password'); | ||
}); | ||
it("Hash starts with '$'", function () { | ||
assert(test_hash.charAt(0) == "$", "Hash doesn't starts with '$'"); | ||
}); | ||
it('Hash is 60 characters long', function () { | ||
assert(test_hash.length == 60, "Hash is not 60 characters long"); | ||
}); | ||
}); | ||
describe('verify', function () { | ||
it('Verify hash against original password', function () { | ||
assert(Password.verify(test_password, test_hash), "Couldn't verify hash against original password"); | ||
}); | ||
}); | ||
describe('getInfo', function () { | ||
var info = Password.getInfo(test_hash); | ||
it("Verify that hash has 'bcrypt' as algorithm", function () { | ||
assert(info.algoName == "PASSWORD_BCRYPT", "Hash doesn't have 'bcrypt' as algorithm"); | ||
}); | ||
it('Verify that hash has cost value of 10', function () { | ||
assert(info.options.cost == 10, "Couldn't verify that hash has cost value of 10"); | ||
}); | ||
}); | ||
describe('needsRehash', function () { | ||
it("Verify that password doesn't needs rehash {cost: 10}", function () { | ||
assert(!Password.needsRehash(test_hash, "PASSWORD_DEFAULT", {cost: 10}), "Password needs rehash"); | ||
}); | ||
it("Verify that password needs rehash {cost: 11}", function () { | ||
assert(Password.needsRehash(test_hash, "PASSWORD_DEFAULT", {cost: 11}), "Password doesn't need rehash"); | ||
}); | ||
}); | ||
}); |