// 
// Decompiled by Procyon v0.6.0
// 

package org.bouncycastle.crypto.modes;

import java.io.ByteArrayOutputStream;
import org.bouncycastle.util.Pack;
import org.bouncycastle.crypto.OutputLengthException;
import org.bouncycastle.util.Arrays;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.DataLengthException;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithIV;
import org.bouncycastle.crypto.params.AEADParameters;
import org.bouncycastle.crypto.CipherParameters;
import org.bouncycastle.crypto.modes.gcm.Tables4kGCMMultiplier;
import org.bouncycastle.crypto.engines.AESEngine;
import org.bouncycastle.crypto.modes.gcm.GCMMultiplier;
import org.bouncycastle.crypto.BlockCipher;

public class GCMSIVBlockCipher implements AEADBlockCipher
{
    private static final int BUFLEN = 16;
    private static final int HALFBUFLEN = 8;
    private static final int NONCELEN = 12;
    private static final int MAX_DATALEN = 2147483623;
    private static final byte MASK = Byte.MIN_VALUE;
    private static final byte ADD = -31;
    private static final int INIT = 1;
    private static final int AEAD_COMPLETE = 2;
    private final BlockCipher theCipher;
    private final GCMMultiplier theMultiplier;
    private final byte[] theGHash;
    private final byte[] theReverse;
    private final GCMSIVHasher theAEADHasher;
    private final GCMSIVHasher theDataHasher;
    private GCMSIVCache thePlain;
    private GCMSIVCache theEncData;
    private boolean forEncryption;
    private byte[] theInitialAEAD;
    private byte[] theNonce;
    private int theFlags;
    private byte[] macBlock;
    
    public GCMSIVBlockCipher() {
        this(AESEngine.newInstance());
    }
    
    public GCMSIVBlockCipher(final BlockCipher blockCipher) {
        this(blockCipher, new Tables4kGCMMultiplier());
    }
    
    public GCMSIVBlockCipher(final BlockCipher theCipher, final GCMMultiplier theMultiplier) {
        this.theGHash = new byte[16];
        this.theReverse = new byte[16];
        this.macBlock = new byte[16];
        if (theCipher.getBlockSize() != 16) {
            throw new IllegalArgumentException("Cipher required with a block size of 16.");
        }
        this.theCipher = theCipher;
        this.theMultiplier = theMultiplier;
        this.theAEADHasher = new GCMSIVHasher();
        this.theDataHasher = new GCMSIVHasher();
    }
    
    @Override
    public BlockCipher getUnderlyingCipher() {
        return this.theCipher;
    }
    
    @Override
    public void init(final boolean forEncryption, final CipherParameters cipherParameters) throws IllegalArgumentException {
        byte[] associatedText = null;
        byte[] theNonce;
        KeyParameter key;
        if (cipherParameters instanceof AEADParameters) {
            final AEADParameters aeadParameters = (AEADParameters)cipherParameters;
            associatedText = aeadParameters.getAssociatedText();
            theNonce = aeadParameters.getNonce();
            key = aeadParameters.getKey();
        }
        else {
            if (!(cipherParameters instanceof ParametersWithIV)) {
                throw new IllegalArgumentException("invalid parameters passed to GCM-SIV");
            }
            final ParametersWithIV parametersWithIV = (ParametersWithIV)cipherParameters;
            theNonce = parametersWithIV.getIV();
            key = (KeyParameter)parametersWithIV.getParameters();
        }
        if (theNonce == null || theNonce.length != 12) {
            throw new IllegalArgumentException("Invalid nonce");
        }
        if (key == null || (key.getKeyLength() != 16 && key.getKeyLength() != 32)) {
            throw new IllegalArgumentException("Invalid key");
        }
        this.forEncryption = forEncryption;
        this.theInitialAEAD = associatedText;
        this.theNonce = theNonce;
        this.deriveKeys(key);
        this.resetStreams();
    }
    
    @Override
    public String getAlgorithmName() {
        return this.theCipher.getAlgorithmName() + "-GCM-SIV";
    }
    
    private void checkAEADStatus(final int n) {
        if ((this.theFlags & 0x1) == 0x0) {
            throw new IllegalStateException("Cipher is not initialised");
        }
        if ((this.theFlags & 0x2) != 0x0) {
            throw new IllegalStateException("AEAD data cannot be processed after ordinary data");
        }
        if (this.theAEADHasher.getBytesProcessed() + Long.MIN_VALUE > 2147483623 - n + Long.MIN_VALUE) {
            throw new IllegalStateException("AEAD byte count exceeded");
        }
    }
    
    private void checkStatus(final int n) {
        if ((this.theFlags & 0x1) == 0x0) {
            throw new IllegalStateException("Cipher is not initialised");
        }
        if ((this.theFlags & 0x2) == 0x0) {
            this.theAEADHasher.completeHash();
            this.theFlags |= 0x2;
        }
        long n2 = 2147483623L;
        long n3 = this.thePlain.size();
        if (!this.forEncryption) {
            n2 += 16L;
            n3 = this.theEncData.size();
        }
        if (n3 + Long.MIN_VALUE > n2 - n + Long.MIN_VALUE) {
            throw new IllegalStateException("byte count exceeded");
        }
    }
    
    @Override
    public void processAADByte(final byte b) {
        this.checkAEADStatus(1);
        this.theAEADHasher.updateHash(b);
    }
    
    @Override
    public void processAADBytes(final byte[] array, final int n, final int n2) {
        this.checkAEADStatus(n2);
        checkBuffer(array, n, n2, false);
        this.theAEADHasher.updateHash(array, n, n2);
    }
    
    @Override
    public int processByte(final byte b, final byte[] array, final int n) throws DataLengthException {
        this.checkStatus(1);
        if (this.forEncryption) {
            this.thePlain.write(b);
            this.theDataHasher.updateHash(b);
        }
        else {
            this.theEncData.write(b);
        }
        return 0;
    }
    
    @Override
    public int processBytes(final byte[] array, final int n, final int n2, final byte[] array2, final int n3) throws DataLengthException {
        this.checkStatus(n2);
        checkBuffer(array, n, n2, false);
        if (this.forEncryption) {
            this.thePlain.write(array, n, n2);
            this.theDataHasher.updateHash(array, n, n2);
        }
        else {
            this.theEncData.write(array, n, n2);
        }
        return 0;
    }
    
    @Override
    public int doFinal(final byte[] array, final int n) throws IllegalStateException, InvalidCipherTextException {
        this.checkStatus(0);
        checkBuffer(array, n, this.getOutputSize(0), true);
        if (this.forEncryption) {
            final byte[] calculateTag = this.calculateTag();
            final int n2 = 16 + this.encryptPlain(calculateTag, array, n);
            System.arraycopy(calculateTag, 0, array, n + this.thePlain.size(), 16);
            System.arraycopy(calculateTag, 0, this.macBlock, 0, this.macBlock.length);
            this.resetStreams();
            return n2;
        }
        this.decryptPlain();
        final int size = this.thePlain.size();
        System.arraycopy(this.thePlain.getBuffer(), 0, array, n, size);
        this.resetStreams();
        return size;
    }
    
    @Override
    public byte[] getMac() {
        return Arrays.clone(this.macBlock);
    }
    
    @Override
    public int getUpdateOutputSize(final int n) {
        return 0;
    }
    
    @Override
    public int getOutputSize(final int n) {
        if (this.forEncryption) {
            return n + this.thePlain.size() + 16;
        }
        final int n2 = n + this.theEncData.size();
        return (n2 > 16) ? (n2 - 16) : 0;
    }
    
    @Override
    public void reset() {
        this.resetStreams();
    }
    
    private void resetStreams() {
        if (this.thePlain != null) {
            this.thePlain.clearBuffer();
        }
        this.theAEADHasher.reset();
        this.theDataHasher.reset();
        this.thePlain = new GCMSIVCache();
        this.theEncData = (this.forEncryption ? null : new GCMSIVCache());
        this.theFlags &= 0xFFFFFFFD;
        Arrays.fill(this.theGHash, (byte)0);
        if (this.theInitialAEAD != null) {
            this.theAEADHasher.updateHash(this.theInitialAEAD, 0, this.theInitialAEAD.length);
        }
    }
    
    private static int bufLength(final byte[] array) {
        return (array == null) ? 0 : array.length;
    }
    
    private static void checkBuffer(final byte[] array, final int n, final int n2, final boolean b) {
        final int bufLength = bufLength(array);
        final int n3 = n + n2;
        if (n2 < 0 || n < 0 || n3 < 0 || n3 > bufLength) {
            throw b ? new OutputLengthException("Output buffer too short.") : new DataLengthException("Input buffer too short.");
        }
    }
    
    private int encryptPlain(final byte[] array, final byte[] array2, final int n) {
        final byte[] buffer = this.thePlain.getBuffer();
        final byte[] clone;
        final byte[] array3 = clone = Arrays.clone(array);
        final int n2 = 15;
        clone[n2] |= 0xFFFFFF80;
        final byte[] array4 = new byte[16];
        int i = this.thePlain.size();
        int n3 = 0;
        while (i > 0) {
            this.theCipher.processBlock(array3, 0, array4, 0);
            final int min = Math.min(16, i);
            xorBlock(array4, buffer, n3, min);
            System.arraycopy(array4, 0, array2, n + n3, min);
            i -= min;
            n3 += min;
            incrementCounter(array3);
        }
        return this.thePlain.size();
    }
    
    private void decryptPlain() throws InvalidCipherTextException {
        final byte[] buffer = this.theEncData.getBuffer();
        int i = this.theEncData.size() - 16;
        if (i < 0) {
            throw new InvalidCipherTextException("Data too short");
        }
        final byte[] copyOfRange = Arrays.copyOfRange(buffer, i, i + 16);
        final byte[] clone;
        final byte[] array = clone = Arrays.clone(copyOfRange);
        final int n = 15;
        clone[n] |= 0xFFFFFF80;
        final byte[] b = new byte[16];
        int n2 = 0;
        while (i > 0) {
            this.theCipher.processBlock(array, 0, b, 0);
            final int min = Math.min(16, i);
            xorBlock(b, buffer, n2, min);
            this.thePlain.write(b, 0, min);
            this.theDataHasher.updateHash(b, 0, min);
            i -= min;
            n2 += min;
            incrementCounter(array);
        }
        final byte[] calculateTag = this.calculateTag();
        if (!Arrays.constantTimeAreEqual(calculateTag, copyOfRange)) {
            this.reset();
            throw new InvalidCipherTextException("mac check failed");
        }
        System.arraycopy(calculateTag, 0, this.macBlock, 0, this.macBlock.length);
    }
    
    private byte[] calculateTag() {
        this.theDataHasher.completeHash();
        final byte[] completePolyVal = this.completePolyVal();
        final byte[] array = new byte[16];
        for (int i = 0; i < 12; ++i) {
            final byte[] array2 = completePolyVal;
            final int n = i;
            array2[n] ^= this.theNonce[i];
        }
        final byte[] array3 = completePolyVal;
        final int n2 = 15;
        array3[n2] &= (byte)(-129);
        this.theCipher.processBlock(completePolyVal, 0, array, 0);
        return array;
    }
    
    private byte[] completePolyVal() {
        final byte[] array = new byte[16];
        this.gHashLengths();
        fillReverse(this.theGHash, 0, 16, array);
        return array;
    }
    
    private void gHashLengths() {
        final byte[] array = new byte[16];
        Pack.longToBigEndian(8L * this.theDataHasher.getBytesProcessed(), array, 0);
        Pack.longToBigEndian(8L * this.theAEADHasher.getBytesProcessed(), array, 8);
        this.gHASH(array);
    }
    
    private void gHASH(final byte[] array) {
        xorBlock(this.theGHash, array);
        this.theMultiplier.multiplyH(this.theGHash);
    }
    
    private static void fillReverse(final byte[] array, final int n, final int n2, final byte[] array2) {
        for (int i = 0, n3 = 15; i < n2; ++i, --n3) {
            array2[n3] = array[n + i];
        }
    }
    
    private static void xorBlock(final byte[] array, final byte[] array2) {
        for (int i = 0; i < 16; ++i) {
            final int n = i;
            array[n] ^= array2[i];
        }
    }
    
    private static void xorBlock(final byte[] array, final byte[] array2, final int n, final int n2) {
        for (int i = 0; i < n2; ++i) {
            final int n3 = i;
            array[n3] ^= array2[i + n];
        }
    }
    
    private static void incrementCounter(final byte[] array) {
        for (int i = 0; i < 4; ++i) {
            final int n = i;
            if (++array[n] != 0) {
                break;
            }
        }
    }
    
    private static void mulX(final byte[] array) {
        int n = 0;
        for (int i = 0; i < 16; ++i) {
            final byte b = array[i];
            array[i] = (byte)((b >> 1 & 0x7F) | n);
            n = (((b & 0x1) == 0x0) ? 0 : -128);
        }
        if (n != 0) {
            final int n2 = 0;
            array[n2] ^= 0xFFFFFFE1;
        }
    }
    
    private void deriveKeys(final KeyParameter keyParameter) {
        final byte[] array = new byte[16];
        final byte[] array2 = new byte[16];
        final byte[] array3 = new byte[16];
        final byte[] array4 = new byte[keyParameter.getKeyLength()];
        System.arraycopy(this.theNonce, 0, array, 4, 12);
        this.theCipher.init(true, keyParameter);
        int n = 0;
        this.theCipher.processBlock(array, 0, array2, 0);
        System.arraycopy(array2, 0, array3, n, 8);
        final byte[] array5 = array;
        final int n2 = 0;
        ++array5[n2];
        n += 8;
        this.theCipher.processBlock(array, 0, array2, 0);
        System.arraycopy(array2, 0, array3, n, 8);
        final byte[] array6 = array;
        final int n3 = 0;
        ++array6[n3];
        int n4 = 0;
        this.theCipher.processBlock(array, 0, array2, 0);
        System.arraycopy(array2, 0, array4, n4, 8);
        final byte[] array7 = array;
        final int n5 = 0;
        ++array7[n5];
        n4 += 8;
        this.theCipher.processBlock(array, 0, array2, 0);
        System.arraycopy(array2, 0, array4, n4, 8);
        if (array4.length == 32) {
            final byte[] array8 = array;
            final int n6 = 0;
            ++array8[n6];
            n4 += 8;
            this.theCipher.processBlock(array, 0, array2, 0);
            System.arraycopy(array2, 0, array4, n4, 8);
            final byte[] array9 = array;
            final int n7 = 0;
            ++array9[n7];
            n4 += 8;
            this.theCipher.processBlock(array, 0, array2, 0);
            System.arraycopy(array2, 0, array4, n4, 8);
        }
        this.theCipher.init(true, new KeyParameter(array4));
        fillReverse(array3, 0, 16, array2);
        mulX(array2);
        this.theMultiplier.init(array2);
        this.theFlags |= 0x1;
    }
    
    private static class GCMSIVCache extends ByteArrayOutputStream
    {
        GCMSIVCache() {
        }
        
        byte[] getBuffer() {
            return this.buf;
        }
        
        void clearBuffer() {
            Arrays.fill(this.getBuffer(), (byte)0);
        }
    }
    
    private class GCMSIVHasher
    {
        private final byte[] theBuffer;
        private final byte[] theByte;
        private int numActive;
        private long numHashed;
        
        private GCMSIVHasher() {
            this.theBuffer = new byte[16];
            this.theByte = new byte[1];
        }
        
        long getBytesProcessed() {
            return this.numHashed;
        }
        
        void reset() {
            this.numActive = 0;
            this.numHashed = 0L;
        }
        
        void updateHash(final byte b) {
            this.theByte[0] = b;
            this.updateHash(this.theByte, 0, 1);
        }
        
        void updateHash(final byte[] array, final int n, final int n2) {
            final int n3 = 16 - this.numActive;
            int n4 = 0;
            int i = n2;
            if (this.numActive > 0 && n2 >= n3) {
                System.arraycopy(array, n, this.theBuffer, this.numActive, n3);
                fillReverse(this.theBuffer, 0, 16, GCMSIVBlockCipher.this.theReverse);
                GCMSIVBlockCipher.this.gHASH(GCMSIVBlockCipher.this.theReverse);
                n4 += n3;
                i -= n3;
                this.numActive = 0;
            }
            while (i >= 16) {
                fillReverse(array, n + n4, 16, GCMSIVBlockCipher.this.theReverse);
                GCMSIVBlockCipher.this.gHASH(GCMSIVBlockCipher.this.theReverse);
                n4 += 16;
                i -= 16;
            }
            if (i > 0) {
                System.arraycopy(array, n + n4, this.theBuffer, this.numActive, i);
                this.numActive += i;
            }
            this.numHashed += n2;
        }
        
        void completeHash() {
            if (this.numActive > 0) {
                Arrays.fill(GCMSIVBlockCipher.this.theReverse, (byte)0);
                fillReverse(this.theBuffer, 0, this.numActive, GCMSIVBlockCipher.this.theReverse);
                GCMSIVBlockCipher.this.gHASH(GCMSIVBlockCipher.this.theReverse);
            }
        }
    }
}
