package org.bouncycastle.pqc.legacy.crypto.mceliece;

import java.security.SecureRandom;

import org.bouncycastle.crypto.CipherParameters;
import org.bouncycastle.crypto.CryptoServicesRegistrar;
import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.digests.SHA1Digest;
import org.bouncycastle.crypto.params.ParametersWithRandom;
import org.bouncycastle.crypto.prng.DigestRandomGenerator;
import org.bouncycastle.pqc.crypto.MessageEncryptor;
import org.bouncycastle.pqc.legacy.math.linearalgebra.ByteUtils;
import org.bouncycastle.pqc.legacy.math.linearalgebra.GF2Vector;
import org.bouncycastle.pqc.legacy.math.linearalgebra.IntegerFunctions;

/**
 * This class implements the Kobara/Imai conversion of the McEliecePKCS. This is
 * a conversion of the McEliecePKCS which is CCA2-secure. For details, see D.
 * Engelbert, R. Overbeck, A. Schmidt, "A Summary of McEliece-Type Cryptosystems and their Security", technical report.
 * https://www.degruyter.com/document/doi/10.1515/JMC.2007.009/html
 */
public class McElieceKobaraImaiCipher
    implements MessageEncryptor
{

    /**
     * The OID of the algorithm.
     */
    public static final String OID = "1.3.6.1.4.1.8301.3.1.3.4.2.3";

    private static final String DEFAULT_PRNG_NAME = "SHA1PRNG";

    /**
     * A predetermined public constant.
     */
    public static final byte[] PUBLIC_CONSTANT = "a predetermined public constant"
        .getBytes();


    private Digest messDigest;

    private SecureRandom sr;

    McElieceCCA2KeyParameters key;

    /**
     * The McEliece main parameters
     */
    private int n, k, t;
    private boolean forEncryption;


    public void init(boolean forEncryption,
                     CipherParameters param)
    {
        this.forEncryption = forEncryption;
        if (forEncryption)
        {
            if (param instanceof ParametersWithRandom)
            {
                ParametersWithRandom rParam = (ParametersWithRandom)param;

                this.sr = rParam.getRandom();
                this.key = (McElieceCCA2PublicKeyParameters)rParam.getParameters();
                this.initCipherEncrypt((McElieceCCA2PublicKeyParameters)key);

            }
            else
            {
                this.sr = CryptoServicesRegistrar.getSecureRandom();
                this.key = (McElieceCCA2PublicKeyParameters)param;
                this.initCipherEncrypt((McElieceCCA2PublicKeyParameters)key);
            }
        }
        else
        {
            this.key = (McElieceCCA2PrivateKeyParameters)param;
            this.initCipherDecrypt((McElieceCCA2PrivateKeyParameters)key);
        }

    }

    /**
     * Return the key size of the given key object.
     *
     * @param key the McElieceCCA2KeyParameters object
     * @return the key size of the given key object
     */
    public int getKeySize(McElieceCCA2KeyParameters key)
    {
        if (key instanceof McElieceCCA2PublicKeyParameters)
        {
            return ((McElieceCCA2PublicKeyParameters)key).getN();

        }
        if (key instanceof McElieceCCA2PrivateKeyParameters)
        {
            return ((McElieceCCA2PrivateKeyParameters)key).getN();
        }
        throw new IllegalArgumentException("unsupported type");
    }

    private void initCipherEncrypt(McElieceCCA2PublicKeyParameters pubKey)
    {
        this.messDigest = Utils.getDigest(pubKey.getDigest());
        n = pubKey.getN();
        k = pubKey.getK();
        t = pubKey.getT();

    }

    private void initCipherDecrypt(McElieceCCA2PrivateKeyParameters privKey)
    {
        this.messDigest = Utils.getDigest(privKey.getDigest());
        n = privKey.getN();
        k = privKey.getK();
        t = privKey.getT();
    }

    public byte[] messageEncrypt(byte[] input)
    {
        if (!forEncryption)
        {
            throw new IllegalStateException("cipher initialised for decryption");
        }

        int c2Len = messDigest.getDigestSize();
        int c4Len = k >> 3;
        int c5Len = (IntegerFunctions.binomial(n, t).bitLength() - 1) >> 3;


        int mLen = c4Len + c5Len - c2Len - PUBLIC_CONSTANT.length;
        if (input.length > mLen)
        {
            mLen = input.length;
        }

        int c1Len = mLen + PUBLIC_CONSTANT.length;
        int c6Len = c1Len + c2Len - c4Len - c5Len;

        // compute (m||const)
        byte[] mConst = new byte[c1Len];
        System.arraycopy(input, 0, mConst, 0, input.length);
        System.arraycopy(PUBLIC_CONSTANT, 0, mConst, mLen,
            PUBLIC_CONSTANT.length);

        // generate random r of length c2Len bytes
        byte[] r = new byte[c2Len];
        sr.nextBytes(r);

        // get PRNG object
                // get PRNG object
        DigestRandomGenerator sr0 = new DigestRandomGenerator(new SHA1Digest());

        // seed PRNG with r'
        sr0.addSeedMaterial(r);

        // generate random sequence ...
        byte[] c1 = new byte[c1Len];
        sr0.nextBytes(c1);

        // ... and XOR with (m||const) to obtain c1
        for (int i = c1Len - 1; i >= 0; i--)
        {
            c1[i] ^= mConst[i];
        }

        // compute H(c1) ...
        byte[] c2 = new byte[messDigest.getDigestSize()];
        messDigest.update(c1, 0, c1.length);
        messDigest.doFinal(c2, 0);

        // ... and XOR with r
        for (int i = c2Len - 1; i >= 0; i--)
        {
            c2[i] ^= r[i];
        }

        // compute (c2||c1)
        byte[] c2c1 = ByteUtils.concatenate(c2, c1);

        // split (c2||c1) into (c6||c5||c4), where c4Len is k/8 bytes, c5Len is
        // floor[log(n|t)]/8 bytes, and c6Len is c1Len+c2Len-c4Len-c5Len (may be
        // 0).
        byte[] c6 = new byte[0];
        if (c6Len > 0)
        {
            c6 = new byte[c6Len];
            System.arraycopy(c2c1, 0, c6, 0, c6Len);
        }

        byte[] c5 = new byte[c5Len];
        System.arraycopy(c2c1, c6Len, c5, 0, c5Len);

        byte[] c4 = new byte[c4Len];
        System.arraycopy(c2c1, c6Len + c5Len, c4, 0, c4Len);

        // convert c4 to vector over GF(2)
        GF2Vector c4Vec = GF2Vector.OS2VP(k, c4);

        // convert c5 to error vector z
        GF2Vector z = Conversions.encode(n, t, c5);

        // compute encC4 = E(c4, z)
        byte[] encC4 = McElieceCCA2Primitives.encryptionPrimitive((McElieceCCA2PublicKeyParameters)key,
            c4Vec, z).getEncoded();

        // if c6Len > 0
        if (c6Len > 0)
        {
            // return (c6||encC4)
            return ByteUtils.concatenate(c6, encC4);
        }
        // else, return encC4
        return encC4;
    }


    public byte[] messageDecrypt(byte[] input)
        throws InvalidCipherTextException
    {
        if (forEncryption)
        {
            throw new IllegalStateException("cipher initialised for decryption");
        }

        int nDiv8 = n >> 3;

        if (input.length < nDiv8)
        {
            throw new InvalidCipherTextException("Bad Padding: Ciphertext too short.");
        }

        int c2Len = messDigest.getDigestSize();
        int c4Len = k >> 3;
        int c5Len = (IntegerFunctions.binomial(n, t).bitLength() - 1) >> 3;
        int c6Len = input.length - nDiv8;

        // split cipher text (c6||encC4), where c6 may be empty
        byte[] c6, encC4;
        if (c6Len > 0)
        {
            byte[][] c6EncC4 = ByteUtils.split(input, c6Len);
            c6 = c6EncC4[0];
            encC4 = c6EncC4[1];
        }
        else
        {
            c6 = new byte[0];
            encC4 = input;
        }

        // convert encC4 into vector over GF(2)
        GF2Vector encC4Vec = GF2Vector.OS2VP(n, encC4);

        // decrypt encC4Vec to obtain c4 and error vector z
        GF2Vector[] c4z = McElieceCCA2Primitives.decryptionPrimitive((McElieceCCA2PrivateKeyParameters)key,
            encC4Vec);
        byte[] c4 = c4z[0].getEncoded();
        GF2Vector z = c4z[1];

        // if length of c4 is greater than c4Len (because of padding) ...
        if (c4.length > c4Len)
        {
            // ... truncate the padding bytes
            c4 = ByteUtils.subArray(c4, 0, c4Len);
        }

        // compute c5 = Conv^-1(z)
        byte[] c5 = Conversions.decode(n, t, z);

        // if c5 is shorter than expected, pad with leading zeroes
        if (c5.length < c5Len)
        {
            byte[] paddedC5 = new byte[c5Len];
            System.arraycopy(c5, 0, paddedC5, c5Len - c5.length, c5.length);
            c5 = paddedC5;
        }

        // compute (c6||c5||c4)
        byte[] c6c5c4 = ByteUtils.concatenate(c6, c5);
        c6c5c4 = ByteUtils.concatenate(c6c5c4, c4);

        // split (c6||c5||c4) into (c2||c1), where c2Len = mdLen and c1Len =
        // input.length-c2Len bytes.
        int c1Len = c6c5c4.length - c2Len;
        byte[][] c2c1 = ByteUtils.split(c6c5c4, c2Len);
        byte[] c2 = c2c1[0];
        byte[] c1 = c2c1[1];

        // compute H(c1) ...
        byte[] rPrime = new byte[messDigest.getDigestSize()];
        messDigest.update(c1, 0, c1.length);
        messDigest.doFinal(rPrime, 0);

        // ... and XOR with c2 to obtain r'
        for (int i = c2Len - 1; i >= 0; i--)
        {
            rPrime[i] ^= c2[i];
        }

        // get PRNG object
        DigestRandomGenerator sr0 = new DigestRandomGenerator(new SHA1Digest());

        // seed PRNG with r'
        sr0.addSeedMaterial(rPrime);

        // generate random sequence R(r') ...
        byte[] mConstPrime = new byte[c1Len];
        sr0.nextBytes(mConstPrime);

        // ... and XOR with c1 to obtain (m||const')
        for (int i = c1Len - 1; i >= 0; i--)
        {
            mConstPrime[i] ^= c1[i];
        }

        if (mConstPrime.length < c1Len)
        {
            throw new InvalidCipherTextException("Bad Padding: invalid ciphertext");
        }

        byte[][] temp = ByteUtils.split(mConstPrime, c1Len
            - PUBLIC_CONSTANT.length);
        byte[] mr = temp[0];
        byte[] constPrime = temp[1];

        if (!ByteUtils.equals(constPrime, PUBLIC_CONSTANT))
        {
            throw new InvalidCipherTextException("Bad Padding: invalid ciphertext");
        }

        return mr;
    }
}
