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

package org.bouncycastle.crypto.modes;

import org.bouncycastle.util.Pack;
import org.bouncycastle.crypto.OutputLengthException;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.DataLengthException;
import org.bouncycastle.util.Arrays;
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.macs.Poly1305;
import org.bouncycastle.crypto.Mac;
import org.bouncycastle.crypto.engines.ChaCha7539Engine;

public class ChaCha20Poly1305 implements AEADCipher
{
    private static final int BUF_SIZE = 64;
    private static final int KEY_SIZE = 32;
    private static final int NONCE_SIZE = 12;
    private static final int MAC_SIZE = 16;
    private static final byte[] ZEROES;
    private static final long AAD_LIMIT = -1L;
    private static final long DATA_LIMIT = 274877906880L;
    private final ChaCha7539Engine chacha20;
    private final Mac poly1305;
    private final byte[] key;
    private final byte[] nonce;
    private final byte[] buf;
    private final byte[] mac;
    private byte[] initialAAD;
    private long aadCount;
    private long dataCount;
    private int state;
    private int bufPos;
    
    public ChaCha20Poly1305() {
        this(new Poly1305());
    }
    
    public ChaCha20Poly1305(final Mac poly1305) {
        this.key = new byte[32];
        this.nonce = new byte[12];
        this.buf = new byte[80];
        this.mac = new byte[16];
        this.state = 0;
        if (null == poly1305) {
            throw new NullPointerException("'poly1305' cannot be null");
        }
        if (16 != poly1305.getMacSize()) {
            throw new IllegalArgumentException("'poly1305' must be a 128-bit MAC");
        }
        this.chacha20 = new ChaCha7539Engine();
        this.poly1305 = poly1305;
    }
    
    @Override
    public String getAlgorithmName() {
        return "ChaCha20Poly1305";
    }
    
    @Override
    public void init(final boolean b, final CipherParameters cipherParameters) throws IllegalArgumentException {
        KeyParameter key;
        byte[] array;
        ParametersWithIV parametersWithIV;
        if (cipherParameters instanceof AEADParameters) {
            final AEADParameters aeadParameters = (AEADParameters)cipherParameters;
            final int macSize = aeadParameters.getMacSize();
            if (128 != macSize) {
                throw new IllegalArgumentException("Invalid value for MAC size: " + macSize);
            }
            key = aeadParameters.getKey();
            array = aeadParameters.getNonce();
            parametersWithIV = new ParametersWithIV(key, array);
            this.initialAAD = aeadParameters.getAssociatedText();
        }
        else {
            if (!(cipherParameters instanceof ParametersWithIV)) {
                throw new IllegalArgumentException("invalid parameters passed to ChaCha20Poly1305");
            }
            final ParametersWithIV parametersWithIV2 = (ParametersWithIV)cipherParameters;
            key = (KeyParameter)parametersWithIV2.getParameters();
            array = parametersWithIV2.getIV();
            parametersWithIV = parametersWithIV2;
            this.initialAAD = null;
        }
        if (null == key) {
            if (0 == this.state) {
                throw new IllegalArgumentException("Key must be specified in initial init");
            }
        }
        else if (32 != key.getKeyLength()) {
            throw new IllegalArgumentException("Key must be 256 bits");
        }
        if (null == array || 12 != array.length) {
            throw new IllegalArgumentException("Nonce must be 96 bits");
        }
        if (0 != this.state && b && Arrays.areEqual(this.nonce, array) && (null == key || Arrays.areEqual(this.key, key.getKey()))) {
            throw new IllegalArgumentException("cannot reuse nonce for ChaCha20Poly1305 encryption");
        }
        if (null != key) {
            key.copyTo(this.key, 0, 32);
        }
        System.arraycopy(array, 0, this.nonce, 0, 12);
        this.chacha20.init(true, parametersWithIV);
        this.state = (b ? 1 : 5);
        this.reset(true, false);
    }
    
    @Override
    public int getOutputSize(final int b) {
        final int n = Math.max(0, b) + this.bufPos;
        switch (this.state) {
            case 5:
            case 6:
            case 7: {
                return Math.max(0, n - 16);
            }
            case 1:
            case 2:
            case 3: {
                return n + 16;
            }
            default: {
                throw new IllegalStateException();
            }
        }
    }
    
    @Override
    public int getUpdateOutputSize(final int b) {
        int max = Math.max(0, b) + this.bufPos;
        switch (this.state) {
            case 5:
            case 6:
            case 7: {
                max = Math.max(0, max - 16);
                break;
            }
            case 1:
            case 2:
            case 3: {
                break;
            }
            default: {
                throw new IllegalStateException();
            }
        }
        return max - max % 64;
    }
    
    @Override
    public void processAADByte(final byte b) {
        this.checkAAD();
        this.aadCount = this.incrementCount(this.aadCount, 1, -1L);
        this.poly1305.update(b);
    }
    
    @Override
    public void processAADBytes(final byte[] array, final int n, final int n2) {
        if (null == array) {
            throw new NullPointerException("'in' cannot be null");
        }
        if (n < 0) {
            throw new IllegalArgumentException("'inOff' cannot be negative");
        }
        if (n2 < 0) {
            throw new IllegalArgumentException("'len' cannot be negative");
        }
        if (n > array.length - n2) {
            throw new DataLengthException("Input buffer too short");
        }
        this.checkAAD();
        if (n2 > 0) {
            this.aadCount = this.incrementCount(this.aadCount, n2, -1L);
            this.poly1305.update(array, n, n2);
        }
    }
    
    @Override
    public int processByte(final byte b, final byte[] array, final int n) throws DataLengthException {
        this.checkData();
        switch (this.state) {
            case 7: {
                this.buf[this.bufPos] = b;
                if (++this.bufPos == this.buf.length) {
                    this.poly1305.update(this.buf, 0, 64);
                    this.processData(this.buf, 0, 64, array, n);
                    System.arraycopy(this.buf, 64, this.buf, 0, 16);
                    this.bufPos = 16;
                    return 64;
                }
                return 0;
            }
            case 3: {
                this.buf[this.bufPos] = b;
                if (++this.bufPos == 64) {
                    this.processData(this.buf, 0, 64, array, n);
                    this.poly1305.update(array, n, 64);
                    this.bufPos = 0;
                    return 64;
                }
                return 0;
            }
            default: {
                throw new IllegalStateException();
            }
        }
    }
    
    @Override
    public int processBytes(byte[] array, int n, int i, final byte[] array2, final int n2) throws DataLengthException {
        if (null == array) {
            throw new NullPointerException("'in' cannot be null");
        }
        if (null == array2) {}
        if (n < 0) {
            throw new IllegalArgumentException("'inOff' cannot be negative");
        }
        if (i < 0) {
            throw new IllegalArgumentException("'len' cannot be negative");
        }
        if (n > array.length - i) {
            throw new DataLengthException("Input buffer too short");
        }
        if (n2 < 0) {
            throw new IllegalArgumentException("'outOff' cannot be negative");
        }
        if (array == array2 && Arrays.segmentsOverlap(n, i, n2, this.getUpdateOutputSize(i))) {
            array = new byte[i];
            System.arraycopy(array2, n, array, 0, i);
            n = 0;
        }
        this.checkData();
        int n3 = 0;
        switch (this.state) {
            case 7: {
                for (int j = 0; j < i; ++j) {
                    this.buf[this.bufPos] = array[n + j];
                    if (++this.bufPos == this.buf.length) {
                        this.poly1305.update(this.buf, 0, 64);
                        this.processData(this.buf, 0, 64, array2, n2 + n3);
                        System.arraycopy(this.buf, 64, this.buf, 0, 16);
                        this.bufPos = 16;
                        n3 += 64;
                    }
                }
                break;
            }
            case 3: {
                if (this.bufPos != 0) {
                    while (i > 0) {
                        --i;
                        this.buf[this.bufPos] = array[n++];
                        if (++this.bufPos == 64) {
                            this.processData(this.buf, 0, 64, array2, n2);
                            this.poly1305.update(array2, n2, 64);
                            this.bufPos = 0;
                            n3 = 64;
                            break;
                        }
                    }
                }
                while (i >= 64) {
                    this.processData(array, n, 64, array2, n2 + n3);
                    this.poly1305.update(array2, n2 + n3, 64);
                    n += 64;
                    i -= 64;
                    n3 += 64;
                }
                if (i > 0) {
                    System.arraycopy(array, n, this.buf, 0, i);
                    this.bufPos = i;
                    break;
                }
                break;
            }
            default: {
                throw new IllegalStateException();
            }
        }
        return n3;
    }
    
    @Override
    public int doFinal(final byte[] array, final int n) throws IllegalStateException, InvalidCipherTextException {
        if (null == array) {
            throw new NullPointerException("'out' cannot be null");
        }
        if (n < 0) {
            throw new IllegalArgumentException("'outOff' cannot be negative");
        }
        this.checkData();
        Arrays.clear(this.mac);
        int n2 = 0;
        switch (this.state) {
            case 7: {
                if (this.bufPos < 16) {
                    throw new InvalidCipherTextException("data too short");
                }
                n2 = this.bufPos - 16;
                if (n > array.length - n2) {
                    throw new OutputLengthException("Output buffer too short");
                }
                if (n2 > 0) {
                    this.poly1305.update(this.buf, 0, n2);
                    this.processData(this.buf, 0, n2, array, n);
                }
                this.finishData(8);
                if (!Arrays.constantTimeAreEqual(16, this.mac, 0, this.buf, n2)) {
                    throw new InvalidCipherTextException("mac check in ChaCha20Poly1305 failed");
                }
                break;
            }
            case 3: {
                n2 = this.bufPos + 16;
                if (n > array.length - n2) {
                    throw new OutputLengthException("Output buffer too short");
                }
                if (this.bufPos > 0) {
                    this.processData(this.buf, 0, this.bufPos, array, n);
                    this.poly1305.update(array, n, this.bufPos);
                }
                this.finishData(4);
                System.arraycopy(this.mac, 0, array, n + this.bufPos, 16);
                break;
            }
            default: {
                throw new IllegalStateException();
            }
        }
        this.reset(false, true);
        return n2;
    }
    
    @Override
    public byte[] getMac() {
        return Arrays.clone(this.mac);
    }
    
    @Override
    public void reset() {
        this.reset(true, true);
    }
    
    private void checkAAD() {
        switch (this.state) {
            case 5: {
                this.state = 6;
                break;
            }
            case 1: {
                this.state = 2;
                break;
            }
            case 2:
            case 6: {
                break;
            }
            case 4: {
                throw new IllegalStateException("ChaCha20Poly1305 cannot be reused for encryption");
            }
            default: {
                throw new IllegalStateException();
            }
        }
    }
    
    private void checkData() {
        switch (this.state) {
            case 5:
            case 6: {
                this.finishAAD(7);
                break;
            }
            case 1:
            case 2: {
                this.finishAAD(3);
                break;
            }
            case 3:
            case 7: {
                break;
            }
            case 4: {
                throw new IllegalStateException("ChaCha20Poly1305 cannot be reused for encryption");
            }
            default: {
                throw new IllegalStateException();
            }
        }
    }
    
    private void finishAAD(final int state) {
        this.padMAC(this.aadCount);
        this.state = state;
    }
    
    private void finishData(final int state) {
        this.padMAC(this.dataCount);
        final byte[] array = new byte[16];
        Pack.longToLittleEndian(this.aadCount, array, 0);
        Pack.longToLittleEndian(this.dataCount, array, 8);
        this.poly1305.update(array, 0, 16);
        this.poly1305.doFinal(this.mac, 0);
        this.state = state;
    }
    
    private long incrementCount(final long n, final int n2, final long n3) {
        if (n + Long.MIN_VALUE > n3 - n2 + Long.MIN_VALUE) {
            throw new IllegalStateException("Limit exceeded");
        }
        return n + n2;
    }
    
    private void initMAC() {
        final byte[] array = new byte[64];
        try {
            this.chacha20.processBytes(array, 0, 64, array, 0);
            this.poly1305.init(new KeyParameter(array, 0, 32));
        }
        finally {
            Arrays.clear(array);
        }
    }
    
    private void padMAC(final long n) {
        final int n2 = (int)n & 0xF;
        if (n2 != 0) {
            this.poly1305.update(ChaCha20Poly1305.ZEROES, 0, 16 - n2);
        }
    }
    
    private void processData(final byte[] array, final int n, final int n2, final byte[] array2, final int n3) {
        if (n3 > array2.length - n2) {
            throw new OutputLengthException("Output buffer too short");
        }
        this.chacha20.processBytes(array, n, n2, array2, n3);
        this.dataCount = this.incrementCount(this.dataCount, n2, 274877906880L);
    }
    
    private void reset(final boolean b, final boolean b2) {
        Arrays.clear(this.buf);
        if (b) {
            Arrays.clear(this.mac);
        }
        this.aadCount = 0L;
        this.dataCount = 0L;
        this.bufPos = 0;
        switch (this.state) {
            case 1:
            case 5: {
                break;
            }
            case 6:
            case 7:
            case 8: {
                this.state = 5;
                break;
            }
            case 2:
            case 3:
            case 4: {
                this.state = 4;
                return;
            }
            default: {
                throw new IllegalStateException();
            }
        }
        if (b2) {
            this.chacha20.reset();
        }
        this.initMAC();
        if (null != this.initialAAD) {
            this.processAADBytes(this.initialAAD, 0, this.initialAAD.length);
        }
    }
    
    static {
        ZEROES = new byte[15];
    }
    
    private static final class State
    {
        static final int UNINITIALIZED = 0;
        static final int ENC_INIT = 1;
        static final int ENC_AAD = 2;
        static final int ENC_DATA = 3;
        static final int ENC_FINAL = 4;
        static final int DEC_INIT = 5;
        static final int DEC_AAD = 6;
        static final int DEC_DATA = 7;
        static final int DEC_FINAL = 8;
    }
}
