/**
 * PSS signature scheme
 */

var BigInteger = require('../libs/jsbn');
var crypt = require('crypto');

module.exports = {
    isEncryption: false,
    isSignature: true
};

var DEFAULT_HASH_FUNCTION = 'sha1';
var DEFAULT_SALT_LENGTH = 20;

module.exports.makeScheme = function (key, options) {
    var OAEP = require('./schemes').pkcs1_oaep;

    /**
     * @param key
     * @param options
     * options    [Object]    An object that contains the following keys that specify certain options for encoding.
     *  └>signingSchemeOptions
     *     ├>hash    [String]    Hash function to use when encoding and generating masks. Must be a string accepted by node's crypto.createHash function. (default = "sha1")
     *     ├>mgf    [function]    The mask generation function to use when encoding. (default = mgf1SHA1)
     *     └>sLen    [uint]        The length of the salt to generate. (default = 20)
     * @constructor
     */
    function Scheme(key, options) {
        this.key = key;
        this.options = options;
    }

    Scheme.prototype.sign = function (buffer) {
        var mHash = crypt.createHash(this.options.signingSchemeOptions.hash || DEFAULT_HASH_FUNCTION);
        mHash.update(buffer);

        var encoded = this.emsa_pss_encode(mHash.digest(), this.key.keySize - 1);
        return this.key.$doPrivate(new BigInteger(encoded)).toBuffer(this.key.encryptedDataLength);
    };

    Scheme.prototype.verify = function (buffer, signature, signature_encoding) {
        if (signature_encoding) {
            signature = Buffer.from(signature, signature_encoding);
        }
        signature = new BigInteger(signature);

        var emLen = Math.ceil((this.key.keySize - 1) / 8);
        var m = this.key.$doPublic(signature).toBuffer(emLen);

        var mHash = crypt.createHash(this.options.signingSchemeOptions.hash || DEFAULT_HASH_FUNCTION);
        mHash.update(buffer);

        return this.emsa_pss_verify(mHash.digest(), m, this.key.keySize - 1);
    };

    /*
     * https://tools.ietf.org/html/rfc3447#section-9.1.1
     *
     * mHash	[Buffer]	Hashed message to encode
     * emBits	[uint]		Maximum length of output in bits. Must be at least 8hLen + 8sLen + 9 (hLen = Hash digest length in bytes | sLen = length of salt in bytes)
     * @returns {Buffer} The encoded message
     */
    Scheme.prototype.emsa_pss_encode = function (mHash, emBits) {
        var hash = this.options.signingSchemeOptions.hash || DEFAULT_HASH_FUNCTION;
        var mgf = this.options.signingSchemeOptions.mgf || OAEP.eme_oaep_mgf1;
        var sLen = this.options.signingSchemeOptions.saltLength || DEFAULT_SALT_LENGTH;

        var hLen = OAEP.digestLength[hash];
        var emLen = Math.ceil(emBits / 8);

        if (emLen < hLen + sLen + 2) {
            throw new Error("Output length passed to emBits(" + emBits + ") is too small for the options " +
                "specified(" + hash + ", " + sLen + "). To fix this issue increase the value of emBits. (minimum size: " +
                (8 * hLen + 8 * sLen + 9) + ")"
            );
        }

        var salt = crypt.randomBytes(sLen);

        var Mapostrophe = Buffer.alloc(8 + hLen + sLen);
        Mapostrophe.fill(0, 0, 8);
        mHash.copy(Mapostrophe, 8);
        salt.copy(Mapostrophe, 8 + mHash.length);

        var H = crypt.createHash(hash);
        H.update(Mapostrophe);
        H = H.digest();

        var PS = Buffer.alloc(emLen - salt.length - hLen - 2);
        PS.fill(0);

        var DB = Buffer.alloc(PS.length + 1 + salt.length);
        PS.copy(DB);
        DB[PS.length] = 0x01;
        salt.copy(DB, PS.length + 1);

        var dbMask = mgf(H, DB.length, hash);

        // XOR DB and dbMask together
        var maskedDB = Buffer.alloc(DB.length);
        for (var i = 0; i < dbMask.length; i++) {
            maskedDB[i] = DB[i] ^ dbMask[i];
        }

        var bits = 8 * emLen - emBits;
        var mask = 255 ^ (255 >> 8 - bits << 8 - bits);
        maskedDB[0] = maskedDB[0] & mask;

        var EM = Buffer.alloc(maskedDB.length + H.length + 1);
        maskedDB.copy(EM, 0);
        H.copy(EM, maskedDB.length);
        EM[EM.length - 1] = 0xbc;

        return EM;
    };

    /*
     * https://tools.ietf.org/html/rfc3447#section-9.1.2
     *
     * mHash	[Buffer]	Hashed message
     * EM		[Buffer]	Signature
     * emBits	[uint]		Length of EM in bits. Must be at least 8hLen + 8sLen + 9 to be a valid signature. (hLen = Hash digest length in bytes | sLen = length of salt in bytes)
     * @returns {Boolean} True if signature(EM) matches message(M)
     */
    Scheme.prototype.emsa_pss_verify = function (mHash, EM, emBits) {
        var hash = this.options.signingSchemeOptions.hash || DEFAULT_HASH_FUNCTION;
        var mgf = this.options.signingSchemeOptions.mgf || OAEP.eme_oaep_mgf1;
        var sLen = this.options.signingSchemeOptions.saltLength || DEFAULT_SALT_LENGTH;

        var hLen = OAEP.digestLength[hash];
        var emLen = Math.ceil(emBits / 8);

        if (emLen < hLen + sLen + 2 || EM[EM.length - 1] != 0xbc) {
            return false;
        }

        var DB = Buffer.alloc(emLen - hLen - 1);
        EM.copy(DB, 0, 0, emLen - hLen - 1);

        var mask = 0;
        for (var i = 0, bits = 8 * emLen - emBits; i < bits; i++) {
            mask |= 1 << (7 - i);
        }

        if ((DB[0] & mask) !== 0) {
            return false;
        }

        var H = EM.slice(emLen - hLen - 1, emLen - 1);
        var dbMask = mgf(H, DB.length, hash);

        // Unmask DB
        for (i = 0; i < DB.length; i++) {
            DB[i] ^= dbMask[i];
        }

        bits = 8 * emLen - emBits;
        mask = 255 ^ (255 >> 8 - bits << 8 - bits);
        DB[0] = DB[0] & mask;

        // Filter out padding
        for (i = 0; DB[i] === 0 && i < DB.length; i++);
        if (DB[i] != 1) {
            return false;
        }

        var salt = DB.slice(DB.length - sLen);

        var Mapostrophe = Buffer.alloc(8 + hLen + sLen);
        Mapostrophe.fill(0, 0, 8);
        mHash.copy(Mapostrophe, 8);
        salt.copy(Mapostrophe, 8 + mHash.length);

        var Hapostrophe = crypt.createHash(hash);
        Hapostrophe.update(Mapostrophe);
        Hapostrophe = Hapostrophe.digest();

        return H.toString("hex") === Hapostrophe.toString("hex");
    };

    return new Scheme(key, options);
};
