forked from bsiddiqui/bookshelf-bcrypt
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.js
136 lines (119 loc) · 4.23 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
'use strict'
let merge = require('lodash.merge')
let get = require('lodash.get')
let bcrypt = require('bcryptjs')
// https://paragonie.com/blog/2016/02/how-safely-store-password-in-2016
const RECOMMENDED_ROUNDS = 12
module.exports = (bookshelf, settings) => {
let BookshelfModel = bookshelf.Model
// Add default settings
settings = merge({
allowEmptyPassword: false,
rounds: RECOMMENDED_ROUNDS,
detectBcrypt: () => false,
onRehash: function () {
throw new this.constructor.BcryptRehashDetected()
}
}, settings)
/**
* Hashes a string and stores it inside the provided model
* @param {String} string A string to be hashed
* @param {String} field The field that will receive the hashed string
* @param {Object} model An instantiated bookshelf model
* @throws {BcryptRehashDetected} If it detects a rehash
* @return {Promise} A promise that resolves with the model correctly updated
*/
function hashAndStore (string, field, model) {
return new Promise(function (resolve, reject) {
// Avoid rehashing a string by mistake but allow users to implement
// non throwing logic
if (settings.detectBcrypt(string) && typeof settings.onRehash === 'function') {
try {
settings.onRehash.call(model)
} catch (err) {
return reject(err)
}
}
bcrypt.hash(string, settings.rounds, (err, hash) => {
if (err) return reject(err)
// Set the field and resolves the promise
model.set(field, hash)
resolve(model)
})
})
}
/**
* Compares a string against a bcrypt hash
* @param {String} str The raw string to be compared
* @param {String} hash A bcrypt hash to match against the string
* @return {Promise} A promise that resolves to a boolean indicating if the
* hash was generated from the provided string
*/
function compare (str, hash) {
return new Promise(function (resolve, reject) {
bcrypt.compare(str, hash, (err, res) => {
if (err) {
reject(err)
} else {
resolve(res)
}
})
})
}
/**
* Custom error class for throwing when this plugin detects a rehash
* @type {Error}
*/
bookshelf.BcryptRehashDetected = BookshelfModel.BcryptRehashDetected = class extends Error {
constructor () {
super('Bcrypt tried to hash another bcrypt hash')
this.name = 'BcryptRehashDetected'
}
}
/**
* Custom error class for throwing when this plugin detects a null or undefined password
* @type {Error}
*/
bookshelf.EmptyPasswordDetected = BookshelfModel.EmptyPasswordDetected = class extends Error {
constructor () {
super('Bcrypt cannot hash a null or undefined password')
this.name = 'EmptyPasswordDetected'
}
}
// Extends the default model class
bookshelf.Model = bookshelf.Model.extend({}, {
extended (child) {
// Check if the extended model has the bcrypt option
let field = get(child.prototype, 'bcrypt.field')
// Configure bcrypt only for enabled models
if (field) {
let initialize = child.prototype.initialize
child.prototype.initialize = function () {
// Do not override child's initialization
if (initialize) initialize.call(this)
// Hash the password when saving
this.on('saving', (model, attrs, options) => {
let field = get(this, 'bcrypt.field')
if (model.hasChanged(field) && options.bcrypt !== false) {
let password = model.get(field)
if (password !== null && typeof password !== 'undefined') {
return hashAndStore(password, field, model)
} else if (this.bcrypt.allowEmptyPassword !== true) {
throw new this.constructor.EmptyPasswordDetected()
}
}
})
}
/**
* Compares a string against a bcrypt hash stored in the current model
* @param {String} str The string to compare against the hash
* @return {Promise} A promise that resolves to a boolean indicating if
* the provided string is valid or not
*/
child.prototype.compare = function (str) {
return compare(str, this.get(field))
}
}
}
})
}