07972312ed
refs https://github.com/TryGhost/Ghost/issues/11878 - To be able to identify the reason behind comparison failure on more granular level (like token expiration) had to provide additional information in return result for falsy token comparisons
130 lines
3.4 KiB
JavaScript
130 lines
3.4 KiB
JavaScript
const crypto = require('crypto');
|
|
|
|
module.exports.generateFromContent = function generateFromContent(options) {
|
|
options = options || {};
|
|
|
|
const hash = crypto.createHash('sha256');
|
|
const content = options.content;
|
|
|
|
let text = '';
|
|
|
|
hash.update(content);
|
|
|
|
text += [content, hash.digest('base64')].join('|');
|
|
return Buffer.from(text).toString('base64');
|
|
};
|
|
|
|
module.exports.generateFromEmail = function generateFromEmail(options) {
|
|
options = options || {};
|
|
|
|
const hash = crypto.createHash('sha256');
|
|
const expires = options.expires;
|
|
const email = options.email;
|
|
const secret = options.secret;
|
|
|
|
let text = '';
|
|
|
|
hash.update(String(expires));
|
|
hash.update(email.toLocaleLowerCase());
|
|
hash.update(String(secret));
|
|
|
|
text += [expires, email, hash.digest('base64')].join('|');
|
|
return Buffer.from(text).toString('base64');
|
|
};
|
|
|
|
module.exports.resetToken = {
|
|
generateHash: function generateHash(options) {
|
|
options = options || {};
|
|
|
|
const hash = crypto.createHash('sha256');
|
|
const expires = options.expires;
|
|
const email = options.email;
|
|
const dbHash = options.dbHash;
|
|
const password = options.password;
|
|
let text = '';
|
|
|
|
hash.update(String(expires));
|
|
hash.update(email.toLocaleLowerCase());
|
|
hash.update(password);
|
|
hash.update(String(dbHash));
|
|
|
|
text += [expires, email, hash.digest('base64')].join('|');
|
|
return Buffer.from(text).toString('base64');
|
|
},
|
|
extract: function extract(options) {
|
|
options = options || {};
|
|
|
|
const token = options.token;
|
|
const tokenText = Buffer.from(token, 'base64').toString('ascii');
|
|
let parts;
|
|
let expires;
|
|
let email;
|
|
|
|
parts = tokenText.split('|');
|
|
|
|
// Check if invalid structure
|
|
if (!parts || parts.length !== 3) {
|
|
return false;
|
|
}
|
|
|
|
expires = parseInt(parts[0], 10);
|
|
email = parts[1];
|
|
|
|
return {
|
|
expires: expires,
|
|
email: email
|
|
};
|
|
},
|
|
compare: function compare(options) {
|
|
options = options || {};
|
|
|
|
const tokenToCompare = options.token;
|
|
const parts = exports.resetToken.extract({token: tokenToCompare});
|
|
const dbHash = options.dbHash;
|
|
const password = options.password;
|
|
let generatedToken;
|
|
let diff = 0;
|
|
let i;
|
|
|
|
if (isNaN(parts.expires)) {
|
|
return {
|
|
correct: false,
|
|
reason: 'invalid_expiry'
|
|
};
|
|
}
|
|
|
|
// Check if token is expired to prevent replay attacks
|
|
if (parts.expires < Date.now()) {
|
|
return {
|
|
correct: false,
|
|
reason: 'expired'
|
|
};
|
|
}
|
|
|
|
generatedToken = exports.resetToken.generateHash({
|
|
email: parts.email,
|
|
expires: parts.expires,
|
|
dbHash: dbHash,
|
|
password: password
|
|
});
|
|
|
|
if (tokenToCompare.length !== generatedToken.length) {
|
|
diff = 1;
|
|
}
|
|
|
|
for (i = tokenToCompare.length - 1; i >= 0; i = i - 1) {
|
|
diff |= tokenToCompare.charCodeAt(i) ^ generatedToken.charCodeAt(i);
|
|
}
|
|
|
|
const result = {
|
|
correct: (diff === 0)
|
|
};
|
|
|
|
if (!result.correct) {
|
|
result.reason = 'invalid';
|
|
}
|
|
|
|
return result;
|
|
}
|
|
};
|