TryGhost/Ghost

View on GitHub
ghost/security/lib/tokens.js

Summary

Maintainability
A
1 hr
Test Coverage
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;
    }
};