package org.bouncycastle.crypto.modes;

import java.util.Vector;

import org.bouncycastle.crypto.BlockCipher;
import org.bouncycastle.crypto.CipherParameters;
import org.bouncycastle.crypto.DataLengthException;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.OutputLengthException;
import org.bouncycastle.crypto.params.AEADParameters;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithIV;
import org.bouncycastle.util.Arrays;
import org.bouncycastle.util.Bytes;
import org.bouncycastle.util.Longs;

/**
 * An implementation of <a href="https://tools.ietf.org/html/rfc7253">RFC 7253 on The OCB
 * Authenticated-Encryption Algorithm</a>. For those still concerned about the original patents
 * around this, please see:
 * <p>
 * https://mailarchive.ietf.org/arch/msg/cfrg/qLTveWOdTJcLn4HP3ev-vrj05Vg/
 * </p>
 * Text reproduced below.
 * <blockquote>
 * Phillip Rogaway &gt;rogaway@cs.ucdavis.edu&lt; Sat, 27 February 2021 02:46 UTCShow header
 *
 * I can confirm that I have abandoned all OCB patents
 * and placed into the public domain all OCB-related IP of mine.
 * While I have been telling people this for quite some time, I don't
 * think I ever made a proper announcement to the CFRG or on the
 * OCB webpage. Consider that done.
 * </blockquote>
 */
public class OCBBlockCipher
    implements AEADBlockCipher
{
    private static final int BLOCK_SIZE = 16;

    private BlockCipher hashCipher;
    private BlockCipher mainCipher;

    /*
     * CONFIGURATION
     */
    private boolean forEncryption;
    private int macSize;
    private byte[] initialAssociatedText;

    /*
     * KEY-DEPENDENT
     */
    // NOTE: elements are lazily calculated
    private Vector L;
    private byte[] L_Asterisk, L_Dollar;

    /*
     * NONCE-DEPENDENT
     */
    private byte[] KtopInput = null;
    private byte[] Stretch = new byte[24];
    private byte[] OffsetMAIN_0 = new byte[16];

    /*
     * PER-ENCRYPTION/DECRYPTION
     */
    private byte[] hashBlock, mainBlock;
    private int hashBlockPos, mainBlockPos;
    private long hashBlockCount, mainBlockCount;
    private byte[] OffsetHASH;
    private byte[] Sum;
    private byte[] OffsetMAIN = new byte[16];
    private byte[] Checksum;

    // NOTE: The MAC value is preserved after doFinal
    private byte[] macBlock;

    public OCBBlockCipher(BlockCipher hashCipher, BlockCipher mainCipher)
    {
        if (hashCipher == null)
        {
            throw new IllegalArgumentException("'hashCipher' cannot be null");
        }
        if (hashCipher.getBlockSize() != BLOCK_SIZE)
        {
            throw new IllegalArgumentException("'hashCipher' must have a block size of "
                + BLOCK_SIZE);
        }
        if (mainCipher == null)
        {
            throw new IllegalArgumentException("'mainCipher' cannot be null");
        }
        if (mainCipher.getBlockSize() != BLOCK_SIZE)
        {
            throw new IllegalArgumentException("'mainCipher' must have a block size of "
                + BLOCK_SIZE);
        }

        if (!hashCipher.getAlgorithmName().equals(mainCipher.getAlgorithmName()))
        {
            throw new IllegalArgumentException(
                "'hashCipher' and 'mainCipher' must be the same algorithm");
        }

        this.hashCipher = hashCipher;
        this.mainCipher = mainCipher;
    }

    public BlockCipher getUnderlyingCipher()
    {
        return mainCipher;
    }

    public String getAlgorithmName()
    {
        return mainCipher.getAlgorithmName() + "/OCB";
    }

    public void init(boolean forEncryption, CipherParameters parameters)
        throws IllegalArgumentException
    {
        boolean oldForEncryption = this.forEncryption;
        this.forEncryption = forEncryption;
        this.macBlock = null;

        KeyParameter keyParameter;

        byte[] N;
        if (parameters instanceof AEADParameters)
        {
            AEADParameters aeadParameters = (AEADParameters)parameters;

            N = aeadParameters.getNonce();
            initialAssociatedText = aeadParameters.getAssociatedText();

            int macSizeBits = aeadParameters.getMacSize();
            if (macSizeBits < 64 || macSizeBits > 128 || macSizeBits % 8 != 0)
            {
                throw new IllegalArgumentException("Invalid value for MAC size: " + macSizeBits);
            }

            macSize = macSizeBits / 8;
            keyParameter = aeadParameters.getKey();
        }
        else if (parameters instanceof ParametersWithIV)
        {
            ParametersWithIV parametersWithIV = (ParametersWithIV)parameters;

            N = parametersWithIV.getIV();
            initialAssociatedText = null;
            macSize = 16;
            keyParameter = (KeyParameter)parametersWithIV.getParameters();
        }
        else
        {
            throw new IllegalArgumentException("invalid parameters passed to OCB");
        }

        this.hashBlock = new byte[16];
        this.mainBlock = new byte[forEncryption ? BLOCK_SIZE : (BLOCK_SIZE + macSize)];

        if (N == null)
        {
            N = new byte[0];
        }

        if (N.length > 15)
        {
            throw new IllegalArgumentException("IV must be no more than 15 bytes");
        }

        /*
         * KEY-DEPENDENT INITIALISATION
         */

        if (keyParameter != null)
        {
            // hashCipher always used in forward mode
            hashCipher.init(true, keyParameter);
            mainCipher.init(forEncryption, keyParameter);
            KtopInput = null;
        }
        else if (oldForEncryption != forEncryption)
        {
            throw new IllegalArgumentException("cannot change encrypting state without providing key.");
        }

        this.L_Asterisk = new byte[16];
        hashCipher.processBlock(L_Asterisk, 0, L_Asterisk, 0);

        this.L_Dollar = OCB_double(L_Asterisk);

        this.L = new Vector();
        this.L.addElement(OCB_double(L_Dollar));

        /*
         * NONCE-DEPENDENT AND PER-ENCRYPTION/DECRYPTION INITIALISATION
         */

        int bottom = processNonce(N);

        int bits = bottom % 8, bytes = bottom / 8;
        if (bits == 0)
        {
            System.arraycopy(Stretch, bytes, OffsetMAIN_0, 0, 16);
        }
        else
        {
            for (int i = 0; i < 16; ++i)
            {
                int b1 = Stretch[bytes] & 0xff;
                int b2 = Stretch[++bytes] & 0xff;
                this.OffsetMAIN_0[i] = (byte)((b1 << bits) | (b2 >>> (8 - bits)));
            }
        }

        this.hashBlockPos = 0;
        this.mainBlockPos = 0;

        this.hashBlockCount = 0;
        this.mainBlockCount = 0;

        this.OffsetHASH = new byte[16];
        this.Sum = new byte[16];
        System.arraycopy(this.OffsetMAIN_0, 0, this.OffsetMAIN, 0, 16);
        this.Checksum = new byte[16];

        if (initialAssociatedText != null)
        {
            processAADBytes(initialAssociatedText, 0, initialAssociatedText.length);
        }
    }

    protected int processNonce(byte[] N)
    {
        byte[] nonce = new byte[16];
        System.arraycopy(N, 0, nonce, nonce.length - N.length, N.length);
        nonce[0] = (byte)(macSize << 4);
        nonce[15 - N.length] |= 1;

        int bottom = nonce[15] & 0x3F;
        nonce[15] &= 0xC0;

        /*
         * When used with incrementing nonces, the cipher is only applied once every 64 inits.
         */
        if (KtopInput == null || !Arrays.areEqual(nonce, KtopInput))
        {
            byte[] Ktop = new byte[16];
            KtopInput = nonce;
            hashCipher.processBlock(KtopInput, 0, Ktop, 0);
            System.arraycopy(Ktop, 0, Stretch, 0, 16);
            for (int i = 0; i < 8; ++i)
            {
                Stretch[16 + i] = (byte)(Ktop[i] ^ Ktop[i + 1]);
            }
        }

        return bottom;
    }

    public byte[] getMac()
    {
        if (macBlock == null)
        {
            return new byte[macSize];
        }
        return Arrays.clone(macBlock);
    }

    public int getOutputSize(int len)
    {
        int totalData = len + mainBlockPos;
        if (forEncryption)
        {
            return totalData + macSize;
        }
        return totalData < macSize ? 0 : totalData - macSize;
    }

    public int getUpdateOutputSize(int len)
    {
        int totalData = len + mainBlockPos;
        if (!forEncryption)
        {
            if (totalData < macSize)
            {
                return 0;
            }
            totalData -= macSize;
        }
        return totalData - totalData % BLOCK_SIZE;
    }

    public void processAADByte(byte input)
    {
        hashBlock[hashBlockPos] = input;
        if (++hashBlockPos == hashBlock.length)
        {
            processHashBlock();
        }
    }

    public void processAADBytes(byte[] input, int off, int len)
    {
        for (int i = 0; i < len; ++i)
        {
            hashBlock[hashBlockPos] = input[off + i];
            if (++hashBlockPos == hashBlock.length)
            {
                processHashBlock();
            }
        }
    }

    public int processByte(byte input, byte[] output, int outOff)
        throws DataLengthException
    {
        mainBlock[mainBlockPos] = input;
        if (++mainBlockPos == mainBlock.length)
        {
            processMainBlock(output, outOff);
            return BLOCK_SIZE;
        }
        return 0;
    }

    public int processBytes(byte[] input, int inOff, int len, byte[] output, int outOff)
        throws DataLengthException
    {
        if (input.length < (inOff + len))
        {
            throw new DataLengthException("Input buffer too short");
        }
        int resultLen = 0;

        for (int i = 0; i < len; ++i)
        {
            mainBlock[mainBlockPos] = input[inOff + i];
            if (++mainBlockPos == mainBlock.length)
            {
                processMainBlock(output, outOff + resultLen);
                resultLen += BLOCK_SIZE;
            }
        }

        return resultLen;
    }

    public int doFinal(byte[] output, int outOff)
        throws IllegalStateException,
        InvalidCipherTextException
    {
        /*
         * For decryption, get the tag from the end of the message
         */
        byte[] tag = null;
        if (!forEncryption)
        {
            if (mainBlockPos < macSize)
            {
                throw new InvalidCipherTextException("data too short");
            }
            mainBlockPos -= macSize;
            tag = new byte[macSize];
            System.arraycopy(mainBlock, mainBlockPos, tag, 0, macSize);
        }

        /*
         * HASH: Process any final partial block; compute final hash value
         */
        if (hashBlockPos > 0)
        {
            OCB_extend(hashBlock, hashBlockPos);
            updateHASH(L_Asterisk);
        }

        /*
         * OCB-ENCRYPT/OCB-DECRYPT: Process any final partial block
         */
        if (mainBlockPos > 0)
        {
            if (forEncryption)
            {
                OCB_extend(mainBlock, mainBlockPos);
                xor(Checksum, mainBlock);
            }

            xor(OffsetMAIN, L_Asterisk);

            byte[] Pad = new byte[16];
            hashCipher.processBlock(OffsetMAIN, 0, Pad, 0);

            xor(mainBlock, Pad);

            if (output.length < (outOff + mainBlockPos))
            {
                throw new OutputLengthException("Output buffer too short");
            }
            System.arraycopy(mainBlock, 0, output, outOff, mainBlockPos);

            if (!forEncryption)
            {
                OCB_extend(mainBlock, mainBlockPos);
                xor(Checksum, mainBlock);
            }
        }

        /*
         * OCB-ENCRYPT/OCB-DECRYPT: Compute raw tag
         */
        xor(Checksum, OffsetMAIN);
        xor(Checksum, L_Dollar);
        hashCipher.processBlock(Checksum, 0, Checksum, 0);
        xor(Checksum, Sum);

        this.macBlock = new byte[macSize];
        System.arraycopy(Checksum, 0, macBlock, 0, macSize);

        /*
         * Validate or append tag and reset this cipher for the next run
         */
        int resultLen = mainBlockPos;

        if (forEncryption)
        {
            if (output.length < (outOff + resultLen + macSize))
            {
                throw new OutputLengthException("Output buffer too short");
            }
            // Append tag to the message
            System.arraycopy(macBlock, 0, output, outOff + resultLen, macSize);
            resultLen += macSize;
        }
        else
        {
            // Compare the tag from the message with the calculated one
            if (!Arrays.constantTimeAreEqual(macBlock, tag))
            {
                throw new InvalidCipherTextException("mac check in OCB failed");
            }
        }

        reset(false);

        return resultLen;
    }

    public void reset()
    {
        reset(true);
    }

    protected void clear(byte[] bs)
    {
        if (bs != null)
        {
            Arrays.fill(bs, (byte)0);
        }
    }

    protected byte[] getLSub(int n)
    {
        while (n >= L.size())
        {
            L.addElement(OCB_double((byte[])L.lastElement()));
        }
        return (byte[])L.elementAt(n);
    }

    protected void processHashBlock()
    {
        /*
         * HASH: Process any whole blocks
         */
        updateHASH(getLSub(OCB_ntz(++hashBlockCount)));
        hashBlockPos = 0;
    }

    protected void processMainBlock(byte[] output, int outOff)
    {
        if (output.length < (outOff + BLOCK_SIZE))
        {
            throw new OutputLengthException("Output buffer too short");
        }

        /*
         * OCB-ENCRYPT/OCB-DECRYPT: Process any whole blocks
         */

        if (forEncryption)
        {
            xor(Checksum, mainBlock);
            mainBlockPos = 0;
        }

        xor(OffsetMAIN, getLSub(OCB_ntz(++mainBlockCount)));

        xor(mainBlock, OffsetMAIN);
        mainCipher.processBlock(mainBlock, 0, mainBlock, 0);
        xor(mainBlock, OffsetMAIN);

        System.arraycopy(mainBlock, 0, output, outOff, 16);

        if (!forEncryption)
        {
            xor(Checksum, mainBlock);
            System.arraycopy(mainBlock, BLOCK_SIZE, mainBlock, 0, macSize);
            mainBlockPos = macSize;
        }
    }

    protected void reset(boolean clearMac)
    {
        hashCipher.reset();
        mainCipher.reset();

        clear(hashBlock);
        clear(mainBlock);

        hashBlockPos = 0;
        mainBlockPos = 0;

        hashBlockCount = 0;
        mainBlockCount = 0;

        clear(OffsetHASH);
        clear(Sum);
        System.arraycopy(OffsetMAIN_0, 0, OffsetMAIN, 0, 16);
        clear(Checksum);

        if (clearMac)
        {
            macBlock = null;
        }

        if (initialAssociatedText != null)
        {
            processAADBytes(initialAssociatedText, 0, initialAssociatedText.length);
        }
    }

    protected void updateHASH(byte[] LSub)
    {
        xor(OffsetHASH, LSub);
        xor(hashBlock, OffsetHASH);
        hashCipher.processBlock(hashBlock, 0, hashBlock, 0);
        xor(Sum, hashBlock);
    }

    protected static byte[] OCB_double(byte[] block)
    {
        byte[] result = new byte[16];
        int carry = shiftLeft(block, result);

        /*
         * NOTE: This construction is an attempt at a constant-time implementation.
         */
        result[15] ^= (0x87 >>> ((1 - carry) << 3));

        return result;
    }

    protected static void OCB_extend(byte[] block, int pos)
    {
        block[pos] = (byte)0x80;
        while (++pos < 16)
        {
            block[pos] = 0;
        }
    }

    protected static int OCB_ntz(long x)
    {
        return Longs.numberOfTrailingZeros(x);
    }

    protected static int shiftLeft(byte[] block, byte[] output)
    {
        int i = 16;
        int bit = 0;
        while (--i >= 0)
        {
            int b = block[i] & 0xff;
            output[i] = (byte)((b << 1) | bit);
            bit = (b >>> 7) & 1;
        }
        return bit;
    }

    protected static void xor(byte[] block, byte[] val)
    {
        Bytes.xorTo(16, val, block);
    }
}
