package org.bouncycastle.pqc.crypto.sphincs;

import org.bouncycastle.crypto.CipherParameters;
import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.params.ParametersWithRandom;
import org.bouncycastle.pqc.crypto.MessageSigner;
import org.bouncycastle.util.Pack;

/**
 * SPHINCS-256 signer.
 * <p>
 * This implementation is heavily based on the reference implementation in SUPERCOP, the main difference being the digests used
 * for message hashing and tree construction are now configurable (within limits...) and that the implementation produces
 * detached signatures.
 * </p>
 * <p>
 * The SPHINCS reference implementation is public domain, as per the statement in the second last paragraph of
 * section 1 in https://eprint.iacr.org/2014/795.pdf
 * </p>
 */
public class SPHINCS256Signer
    implements MessageSigner
{
    private final HashFunctions hashFunctions;

    private byte[] keyData;

    /**
     * Base constructor.
     *
     * @param nDigest    the "n-digest" must produce 32 bytes of output - used for tree construction.
     * @param twoNDigest the "2n-digest" must produce 64 bytes of output - used for initial message/key/seed hashing.
     */
    public SPHINCS256Signer(Digest nDigest, Digest twoNDigest)
    {
        if (nDigest.getDigestSize() != 32)
        {
            throw new IllegalArgumentException("n-digest needs to produce 32 bytes of output");
        }
        if (twoNDigest.getDigestSize() != 64)
        {
            throw new IllegalArgumentException("2n-digest needs to produce 64 bytes of output");
        }

        this.hashFunctions = new HashFunctions(nDigest, twoNDigest);
    }

    public void init(boolean forSigning, CipherParameters param)
    {
        if (forSigning)
        {
            if (param instanceof ParametersWithRandom)
            {
                // SPHINCS-256 signatures are deterministic, RNG is not required.
                keyData = ((SPHINCSPrivateKeyParameters)((ParametersWithRandom)param).getParameters()).getKeyData();
            }
            else
            {
                keyData = ((SPHINCSPrivateKeyParameters)param).getKeyData();
            }
        }
        else
        {
            keyData = ((SPHINCSPublicKeyParameters)param).getKeyData();
        }
    }

    public byte[] generateSignature(byte[] message)
    {
        return crypto_sign(hashFunctions, message, keyData);
    }

    public boolean verifySignature(byte[] message, byte[] signature)
    {
        return verify(hashFunctions, message, signature, keyData);
    }

    static void validate_authpath(HashFunctions hs, byte[] root, byte[] leaf, int leafidx, byte[] authpath, int auOff, byte[] masks, int height)
    {
        int i, j;
        byte[] buffer = new byte[2 * SPHINCS256Config.HASH_BYTES];

        if ((leafidx & 1) != 0)
        {
            for (j = 0; j < SPHINCS256Config.HASH_BYTES; j++)
            {
                buffer[SPHINCS256Config.HASH_BYTES + j] = leaf[j];
            }
            for (j = 0; j < SPHINCS256Config.HASH_BYTES; j++)
            {
                buffer[j] = authpath[auOff + j];
            }
        }
        else
        {
            for (j = 0; j < SPHINCS256Config.HASH_BYTES; j++)
            {
                buffer[j] = leaf[j];
            }
            for (j = 0; j < SPHINCS256Config.HASH_BYTES; j++)
            {
                buffer[SPHINCS256Config.HASH_BYTES + j] = authpath[auOff + j];
            }
        }
        int authOff = auOff + SPHINCS256Config.HASH_BYTES;

        for (i = 0; i < height - 1; i++)
        {
            leafidx >>>= 1;
            if ((leafidx & 1) != 0)
            {
                hs.hash_2n_n_mask(buffer, SPHINCS256Config.HASH_BYTES, buffer, 0, masks, 2 * (Wots.WOTS_LOG_L + i) * SPHINCS256Config.HASH_BYTES);
                for (j = 0; j < SPHINCS256Config.HASH_BYTES; j++)
                {
                    buffer[j] = authpath[authOff + j];
                }
            }
            else
            {
                hs.hash_2n_n_mask(buffer, 0, buffer, 0, masks, 2 * (Wots.WOTS_LOG_L + i) * SPHINCS256Config.HASH_BYTES);
                for (j = 0; j < SPHINCS256Config.HASH_BYTES; j++)
                {
                    buffer[j + SPHINCS256Config.HASH_BYTES] = authpath[authOff + j];
                }
            }
            authOff += SPHINCS256Config.HASH_BYTES;
        }
        hs.hash_2n_n_mask(root, 0, buffer, 0, masks, 2 * (Wots.WOTS_LOG_L + height - 1) * SPHINCS256Config.HASH_BYTES);
    }


    static void compute_authpath_wots(HashFunctions hs, byte[] root, byte[] authpath, int authOff, Tree.leafaddr a, byte[] sk, byte[] masks, int height)
    {
        int i, idx, j;
        Tree.leafaddr ta = new Tree.leafaddr(a);

        byte[] tree = new byte[2 * (1 << SPHINCS256Config.SUBTREE_HEIGHT) * SPHINCS256Config.HASH_BYTES];
        byte[] seed = new byte[(1 << SPHINCS256Config.SUBTREE_HEIGHT) * SPHINCS256Config.SEED_BYTES];
        byte[] pk = new byte[(1 << SPHINCS256Config.SUBTREE_HEIGHT) * Wots.WOTS_L * SPHINCS256Config.HASH_BYTES];

        // level 0
        for (ta.subleaf = 0; ta.subleaf < (1 << SPHINCS256Config.SUBTREE_HEIGHT); ta.subleaf++)
        {
            Seed.get_seed(hs, seed, (int)(ta.subleaf * SPHINCS256Config.SEED_BYTES), sk, ta);
        }

        Wots w = new Wots();

        for (ta.subleaf = 0; ta.subleaf < (1 << SPHINCS256Config.SUBTREE_HEIGHT); ta.subleaf++)
        {
            w.wots_pkgen(hs, pk, (int)(ta.subleaf * Wots.WOTS_L * SPHINCS256Config.HASH_BYTES), seed, (int)(ta.subleaf * SPHINCS256Config.SEED_BYTES), masks, 0);
        }

        for (ta.subleaf = 0; ta.subleaf < (1 << SPHINCS256Config.SUBTREE_HEIGHT); ta.subleaf++)
        {
            Tree.l_tree(hs, tree, (int)((1 << SPHINCS256Config.SUBTREE_HEIGHT) * SPHINCS256Config.HASH_BYTES + ta.subleaf * SPHINCS256Config.HASH_BYTES),
                pk, (int)(ta.subleaf * Wots.WOTS_L * SPHINCS256Config.HASH_BYTES), masks, 0);
        }

        int level = 0;

        // tree
        for (i = (1 << SPHINCS256Config.SUBTREE_HEIGHT); i > 0; i >>>= 1)
        {
            for (j = 0; j < i; j += 2)
            {
                hs.hash_2n_n_mask(tree, (i >>> 1) * SPHINCS256Config.HASH_BYTES + (j >>> 1) * SPHINCS256Config.HASH_BYTES,
                    tree, i * SPHINCS256Config.HASH_BYTES + j * SPHINCS256Config.HASH_BYTES,
                    masks, 2 * (Wots.WOTS_LOG_L + level) * SPHINCS256Config.HASH_BYTES);
            }

            level++;
        }


        idx = (int)a.subleaf;

        // copy authpath
        for (i = 0; i < height; i++)
        {
            System.arraycopy(tree, ((1 << SPHINCS256Config.SUBTREE_HEIGHT) >>> i) * SPHINCS256Config.HASH_BYTES + ((idx >>> i) ^ 1) * SPHINCS256Config.HASH_BYTES, authpath, authOff + i * SPHINCS256Config.HASH_BYTES, SPHINCS256Config.HASH_BYTES);
        }

        // copy root
        System.arraycopy(tree, SPHINCS256Config.HASH_BYTES, root, 0, SPHINCS256Config.HASH_BYTES);
    }

    byte[] crypto_sign(HashFunctions hs, byte[] m, byte[] sk)
    {
        byte[] sm = new byte[SPHINCS256Config.CRYPTO_BYTES];

        int i;
        long leafidx;
        byte[] R = new byte[SPHINCS256Config.MESSAGE_HASH_SEED_BYTES];
        byte[] m_h = new byte[SPHINCS256Config.MSGHASH_BYTES];
        long[] rnd = new long[8];

        byte[] root = new byte[SPHINCS256Config.HASH_BYTES];
        byte[] seed = new byte[SPHINCS256Config.SEED_BYTES];
        byte[] masks = new byte[Horst.N_MASKS * SPHINCS256Config.HASH_BYTES];
        int pk;
        byte[] tsk = new byte[SPHINCS256Config.CRYPTO_SECRETKEYBYTES];

        for (i = 0; i < SPHINCS256Config.CRYPTO_SECRETKEYBYTES; i++)
        {
            tsk[i] = sk[i];
        }

        // create leafidx deterministically
        {
            // shift scratch upwards so we can reuse msg later
            int scratch = SPHINCS256Config.CRYPTO_BYTES - SPHINCS256Config.SK_RAND_SEED_BYTES;

            // Copy secret random seed to scratch
            System.arraycopy(tsk, SPHINCS256Config.CRYPTO_SECRETKEYBYTES - SPHINCS256Config.SK_RAND_SEED_BYTES, sm, scratch, SPHINCS256Config.SK_RAND_SEED_BYTES);

            Digest d = hs.getMessageHash();
            byte[] bRnd = new byte[d.getDigestSize()];

            d.update(sm, scratch, SPHINCS256Config.SK_RAND_SEED_BYTES);

            d.update(m, 0, m.length);

            d.doFinal(bRnd, 0);

            // wipe sk
            zerobytes(sm, scratch, SPHINCS256Config.SK_RAND_SEED_BYTES);

            for (int j = 0; j != rnd.length; j++)
            {
                rnd[j] = Pack.littleEndianToLong(bRnd, j * 8);
            }
            leafidx = rnd[0] & 0xfffffffffffffffL;

            System.arraycopy(bRnd, 16, R, 0, SPHINCS256Config.MESSAGE_HASH_SEED_BYTES);

            // prepare msg_hash
            scratch = SPHINCS256Config.CRYPTO_BYTES - SPHINCS256Config.MESSAGE_HASH_SEED_BYTES - SPHINCS256Config.CRYPTO_PUBLICKEYBYTES;

            // cpy R
            System.arraycopy(R, 0, sm, scratch, SPHINCS256Config.MESSAGE_HASH_SEED_BYTES);

            // construct and cpy pk
            Tree.leafaddr b = new Tree.leafaddr();
            b.level = SPHINCS256Config.N_LEVELS - 1;
            b.subtree = 0;
            b.subleaf = 0;

            pk = scratch + SPHINCS256Config.MESSAGE_HASH_SEED_BYTES;

            System.arraycopy(tsk, SPHINCS256Config.SEED_BYTES, sm, pk, Horst.N_MASKS * SPHINCS256Config.HASH_BYTES);

            Tree.treehash(hs, sm, pk + (Horst.N_MASKS * SPHINCS256Config.HASH_BYTES), SPHINCS256Config.SUBTREE_HEIGHT, tsk, b, sm, pk);

            d = hs.getMessageHash();

            d.update(sm, scratch, SPHINCS256Config.MESSAGE_HASH_SEED_BYTES + SPHINCS256Config.CRYPTO_PUBLICKEYBYTES);
            d.update(m, 0, m.length);
            d.doFinal(m_h, 0);
        }

        Tree.leafaddr a = new Tree.leafaddr();

        a.level = SPHINCS256Config.N_LEVELS; // Use unique value $d$ for HORST address.
        a.subleaf = (int)(leafidx & ((1 << SPHINCS256Config.SUBTREE_HEIGHT) - 1));
        a.subtree = leafidx >>> SPHINCS256Config.SUBTREE_HEIGHT;

        for (i = 0; i < SPHINCS256Config.MESSAGE_HASH_SEED_BYTES; i++)
        {
            sm[i] = R[i];
        }

        int smOff = SPHINCS256Config.MESSAGE_HASH_SEED_BYTES;

        System.arraycopy(tsk, SPHINCS256Config.SEED_BYTES, masks, 0, Horst.N_MASKS * SPHINCS256Config.HASH_BYTES);
        for (i = 0; i < (SPHINCS256Config.TOTALTREE_HEIGHT + 7) / 8; i++)
        {
            sm[smOff + i] = (byte)((leafidx >>> 8 * i) & 0xff);
        }

        smOff += (SPHINCS256Config.TOTALTREE_HEIGHT + 7) / 8;

        Seed.get_seed(hs, seed, 0, tsk, a);
        Horst ht = new Horst();

        int horst_sigbytes = ht.horst_sign(hs, sm, smOff, root, seed, masks, m_h);

        smOff += horst_sigbytes;

        Wots w = new Wots();

        for (i = 0; i < SPHINCS256Config.N_LEVELS; i++)
        {
            a.level = i;

            Seed.get_seed(hs, seed, 0, tsk, a); //XXX: Don't use the same address as for horst_sign here!

            w.wots_sign(hs, sm, smOff, root, seed, masks);

            smOff += Wots.WOTS_SIGBYTES;

            compute_authpath_wots(hs, root, sm, smOff, a, tsk, masks, SPHINCS256Config.SUBTREE_HEIGHT);
            smOff += SPHINCS256Config.SUBTREE_HEIGHT * SPHINCS256Config.HASH_BYTES;

            a.subleaf = (int)(a.subtree & ((1 << SPHINCS256Config.SUBTREE_HEIGHT) - 1));
            a.subtree >>>= SPHINCS256Config.SUBTREE_HEIGHT;
        }

        zerobytes(tsk, 0, SPHINCS256Config.CRYPTO_SECRETKEYBYTES);

        return sm;
    }

    private void zerobytes(byte[] tsk, int off, int cryptoSecretkeybytes)
    {
        for (int i = 0; i != cryptoSecretkeybytes; i++)
        {
            tsk[off + i] = 0;
        }
    }

    boolean verify(HashFunctions hs, byte[] m, byte[] sm, byte[] pk)
    {
        int i;
        int smlen = sm.length;
        long leafidx = 0;
        byte[] wots_pk = new byte[Wots.WOTS_L * SPHINCS256Config.HASH_BYTES];
        byte[] pkhash = new byte[SPHINCS256Config.HASH_BYTES];
        byte[] root = new byte[SPHINCS256Config.HASH_BYTES];
        byte[] sig = new byte[SPHINCS256Config.CRYPTO_BYTES];
        int sigp;
        byte[] tpk = new byte[SPHINCS256Config.CRYPTO_PUBLICKEYBYTES];

        if (smlen != SPHINCS256Config.CRYPTO_BYTES)
        {
            throw new IllegalArgumentException("signature wrong size");
        }

        byte[] m_h = new byte[SPHINCS256Config.MSGHASH_BYTES];

        for (i = 0; i < SPHINCS256Config.CRYPTO_PUBLICKEYBYTES; i++)
        {
            tpk[i] = pk[i];
        }

        // construct message hash
        {
            byte[] R = new byte[SPHINCS256Config.MESSAGE_HASH_SEED_BYTES];

            for (i = 0; i < SPHINCS256Config.MESSAGE_HASH_SEED_BYTES; i++)
            {
                R[i] = sm[i];
            }

            System.arraycopy(sm, 0, sig, 0, SPHINCS256Config.CRYPTO_BYTES);

            Digest mHash = hs.getMessageHash();

            // input R
            mHash.update(R, 0, SPHINCS256Config.MESSAGE_HASH_SEED_BYTES);

            // input pub key
            mHash.update(tpk, 0, SPHINCS256Config.CRYPTO_PUBLICKEYBYTES);

            // input message
            mHash.update(m, 0, m.length);

            mHash.doFinal(m_h, 0);
        }

        sigp = 0;

        sigp += SPHINCS256Config.MESSAGE_HASH_SEED_BYTES;
        smlen -= SPHINCS256Config.MESSAGE_HASH_SEED_BYTES;


        for (i = 0; i < (SPHINCS256Config.TOTALTREE_HEIGHT + 7) / 8; i++)
        {
            leafidx ^= ((long)(sig[sigp + i] & 0xff) << (8 * i));
        }


        new Horst().horst_verify(hs, root, sig, sigp + (SPHINCS256Config.TOTALTREE_HEIGHT + 7) / 8,
            tpk, m_h);

        sigp += (SPHINCS256Config.TOTALTREE_HEIGHT + 7) / 8;
        smlen -= (SPHINCS256Config.TOTALTREE_HEIGHT + 7) / 8;

        sigp += Horst.HORST_SIGBYTES;
        smlen -= Horst.HORST_SIGBYTES;

        Wots w = new Wots();

        for (i = 0; i < SPHINCS256Config.N_LEVELS; i++)
        {
            w.wots_verify(hs, wots_pk, sig, sigp, root, tpk);

            sigp += Wots.WOTS_SIGBYTES;
            smlen -= Wots.WOTS_SIGBYTES;

            Tree.l_tree(hs, pkhash, 0, wots_pk, 0, tpk, 0);
            validate_authpath(hs, root, pkhash, (int)(leafidx & 0x1f), sig, sigp, tpk, SPHINCS256Config.SUBTREE_HEIGHT);
            leafidx >>= 5;

            sigp += SPHINCS256Config.SUBTREE_HEIGHT * SPHINCS256Config.HASH_BYTES;
            smlen -= SPHINCS256Config.SUBTREE_HEIGHT * SPHINCS256Config.HASH_BYTES;
        }

        // Because we use custom offsets on tpk, rather than incurring an
        // expensive copy, we use a manual constant time comparison.
        boolean verified = true;
        for (i = 0; i < SPHINCS256Config.HASH_BYTES; i++)
        {
            verified &= root[i] == tpk[i + Horst.N_MASKS * SPHINCS256Config.HASH_BYTES];
        }

        return verified;
    }
}

