'use strict';

export {};

// SCRAM-SHA-512 SASL mechanism (RFC 5802 with SHA-512)
// Compatible with the saslmechanisms / @xmpp/sasl plugin interface.

const cryptoLib : any = require('crypto');

const CLIENT_KEY = 'Client Key';
const SERVER_KEY = 'Server Key';

function parse(chal) {
    const dtives = {};
    const tokens = chal.split(/,(?=(?:[^"]|"[^"]*")*$)/);
    for (let i = 0; i < tokens.length; i++) {
        const dtiv = /(\w+)=["]?([^"]+)["]?$/.exec(tokens[i]);
        if (dtiv) {
            dtives[dtiv[1]] = dtiv[2];
        }
    }
    return dtives;
}

function saslname(name) {
    const escaped = [];
    for (let i = 0; i < name.length; i++) {
        if (name[i] === ',') {
            escaped.push('=2C');
        } else if (name[i] === '=') {
            escaped.push('=3D');
        } else {
            escaped.push(name[i]);
        }
    }
    return escaped.join('');
}

function genNonce() {
    return cryptoLib.randomBytes(16).toString('hex');
}

function H(data) {
    return cryptoLib.createHash('sha512').update(data).digest();
}

function HMAC(key, msg) {
    return cryptoLib.createHmac('sha512', key).update(msg).digest();
}

function Hi(password, salt, iterations) {
    const saltBuf = Buffer.from(salt);
    const concat = Buffer.alloc(saltBuf.length + 4);
    saltBuf.copy(concat);
    concat.writeUInt32BE(1, saltBuf.length);

    let ui1 = HMAC(Buffer.from(password, 'utf8'), concat);
    let ui = Buffer.from(ui1);

    for (let i = 1; i < iterations; i++) {
        ui1 = HMAC(Buffer.from(password, 'utf8'), ui1);
        for (let j = 0; j < ui.length; j++) {
            ui[j] ^= ui1[j];
        }
    }
    return ui;
}

function XOR(a, b) {
    const len = Math.min(a.length, b.length);
    const result = Buffer.allocUnsafe(len);
    for (let i = 0; i < len; i++) {
        result[i] = a[i] ^ b[i];
    }
    return result;
}

const RESP : any = {};

function Mechanism(options) {
    options = options || {};
    this._genNonce = options.genNonce || genNonce;
    this._stage = 'initial';
}

Mechanism.Mechanism = Mechanism;
Mechanism.prototype.name = 'SCRAM-SHA-512';
Mechanism.prototype.clientFirst = true;

Mechanism.prototype.response = function (cred) {
    return RESP[this._stage](this, cred);
};

Mechanism.prototype.challenge = function (chal) {
    const values :any = parse(chal);
    this._salt = Buffer.from(values.s || '', 'base64');
    this._iterationCount = parseInt(values.i, 10);
    this._nonce = values.r;
    this._verifier = values.v;
    this._error = values.e;
    this._challenge = chal;
    return this;
};

RESP.initial = function (mech, cred) {
    mech._cnonce = mech._genNonce();

    let authzid = '';
    if (cred.authzid) {
        authzid = 'a=' + saslname(cred.authzid);
    }

    mech._gs2Header = 'n,' + authzid + ',';
    const username = 'n=' + saslname(cred.username || '');
    const nonce = 'r=' + mech._cnonce;

    mech._clientFirstMessageBare = username + ',' + nonce;
    mech._stage = 'challenge';

    return mech._gs2Header + mech._clientFirstMessageBare;
};

RESP.challenge = function (mech, cred) {
    const gs2Header = Buffer.from(mech._gs2Header).toString('base64');
    mech._clientFinalMessageWithoutProof = 'c=' + gs2Header + ',r=' + mech._nonce;

    let saltedPassword, clientKey, serverKey;

    const saltSame = cred.salt &&
        Buffer.isBuffer(cred.salt) &&
        cred.salt.length === mech._salt.length &&
        cred.salt.equals(mech._salt);

    if (saltSame && cred.clientKey && cred.serverKey) {
        clientKey = cred.clientKey;
        serverKey = cred.serverKey;
    } else if (saltSame && cred.saltedPassword) {
        saltedPassword = cred.saltedPassword;
        clientKey = HMAC(saltedPassword, CLIENT_KEY);
        serverKey = HMAC(saltedPassword, SERVER_KEY);
    } else {
        saltedPassword = Hi(cred.password || '', mech._salt, mech._iterationCount);
        clientKey = HMAC(saltedPassword, CLIENT_KEY);
        serverKey = HMAC(saltedPassword, SERVER_KEY);
    }

    const storedKey = H(clientKey);
    const authMessage = mech._clientFirstMessageBare + ',' +
                        mech._challenge + ',' +
                        mech._clientFinalMessageWithoutProof;

    const clientSignature = HMAC(storedKey, authMessage);
    const clientProof = XOR(clientKey, clientSignature).toString('base64');

    mech._serverSignature = HMAC(serverKey, authMessage);

    mech._stage = 'final';
    mech.cache = {
        salt: mech._salt,
        saltedPassword,
        clientKey,
        serverKey,
    };

    return mech._clientFinalMessageWithoutProof + ',p=' + clientProof;
};

RESP.final = function () {
    return '';
};

function saslScramSha512(sasl) {
    sasl.use(Mechanism);
}

// Plugin entry-point: called as saslFactory => saslFactory.use(Mechanism)
module.exports = saslScramSha512;

export {saslScramSha512};
export default saslScramSha512;
