Skip to content

Commit

Permalink
Added test and documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
thomas-alrek committed Jul 4, 2016
1 parent 5a9522b commit 49d267f
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 37 deletions.
64 changes: 53 additions & 11 deletions README.md
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.
}
```
31 changes: 26 additions & 5 deletions algorithms/bcrypt.js
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;
Expand All @@ -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];;
Expand All @@ -37,6 +57,7 @@ function cost(hash){
}

exports.expression = expression;
exports.defaultOptions = defaultOptions;
exports.verify = verify;
exports.cost = cost;
exports.hash = hash;
84 changes: 65 additions & 19 deletions index.js
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: "",
Expand All @@ -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";
Expand All @@ -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;
}
}
Expand All @@ -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;
19 changes: 17 additions & 2 deletions package.json
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"
}
Expand Down
55 changes: 55 additions & 0 deletions test/test.js
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");
});
});
});

0 comments on commit 49d267f

Please sign in to comment.