/*
 * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package org.openjsse.sun.security.ssl;

import java.nio.ByteBuffer;
import java.security.AccessController;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.PrivilegedAction;
import java.security.SecureRandom;
import java.security.Security;
import java.security.spec.AlgorithmParameterSpec;
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.SecretKey;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import java.security.NoSuchAlgorithmException;
import org.openjsse.sun.security.ssl.Authenticator.MAC;
import static org.openjsse.sun.security.ssl.CipherType.*;
import static org.openjsse.sun.security.ssl.JsseJce.*;

enum SSLCipher {
    // exportable ciphers
    @SuppressWarnings({"unchecked", "rawtypes"})
    B_NULL("NULL", NULL_CIPHER, 0, 0, 0, 0, true, true,
        (Map.Entry<ReadCipherGenerator,
                ProtocolVersion[]>[])(new Map.Entry[] {
            new SimpleImmutableEntry<ReadCipherGenerator, ProtocolVersion[]>(
                new NullReadCipherGenerator(),
                ProtocolVersion.PROTOCOLS_OF_NONE
            ),
            new SimpleImmutableEntry<ReadCipherGenerator, ProtocolVersion[]>(
                new NullReadCipherGenerator(),
                ProtocolVersion.PROTOCOLS_TO_13
            )
        }),
        (Map.Entry<WriteCipherGenerator,
                ProtocolVersion[]>[])(new Map.Entry[] {
            new SimpleImmutableEntry<WriteCipherGenerator, ProtocolVersion[]>(
                new NullWriteCipherGenerator(),
                ProtocolVersion.PROTOCOLS_OF_NONE
            ),
            new SimpleImmutableEntry<WriteCipherGenerator, ProtocolVersion[]>(
                new NullWriteCipherGenerator(),
                ProtocolVersion.PROTOCOLS_TO_13
            )
        })),

    @SuppressWarnings({"unchecked", "rawtypes"})
    B_RC4_40(CIPHER_RC4, STREAM_CIPHER, 5, 16, 0, 0, true, true,
        (Map.Entry<ReadCipherGenerator,
                ProtocolVersion[]>[])(new Map.Entry[] {
            new SimpleImmutableEntry<ReadCipherGenerator, ProtocolVersion[]>(
                new StreamReadCipherGenerator(),
                ProtocolVersion.PROTOCOLS_TO_10
            )
        }),
        (Map.Entry<WriteCipherGenerator,
                ProtocolVersion[]>[])(new Map.Entry[] {
            new SimpleImmutableEntry<WriteCipherGenerator, ProtocolVersion[]>(
                new StreamWriteCipherGenerator(),
                ProtocolVersion.PROTOCOLS_TO_10
            )
        })),

    @SuppressWarnings({"unchecked", "rawtypes"})
    B_RC2_40("RC2", BLOCK_CIPHER, 5, 16, 8, 0, false, true,
        (Map.Entry<ReadCipherGenerator,
                ProtocolVersion[]>[])(new Map.Entry[] {
            new SimpleImmutableEntry<ReadCipherGenerator, ProtocolVersion[]>(
                new StreamReadCipherGenerator(),
                ProtocolVersion.PROTOCOLS_TO_10
            )
        }),
        (Map.Entry<WriteCipherGenerator,
                ProtocolVersion[]>[])(new Map.Entry[] {
            new SimpleImmutableEntry<WriteCipherGenerator, ProtocolVersion[]>(
                new StreamWriteCipherGenerator(),
                ProtocolVersion.PROTOCOLS_TO_10
            )
        })),

    @SuppressWarnings({"unchecked", "rawtypes"})
    B_DES_40(CIPHER_DES,  BLOCK_CIPHER, 5, 8, 8, 0, true, true,
        (Map.Entry<ReadCipherGenerator,
                ProtocolVersion[]>[])(new Map.Entry[] {
            new SimpleImmutableEntry<ReadCipherGenerator, ProtocolVersion[]>(
                new T10BlockReadCipherGenerator(),
                ProtocolVersion.PROTOCOLS_TO_10
            )
        }),
        (Map.Entry<WriteCipherGenerator,
                ProtocolVersion[]>[])(new Map.Entry[] {
            new SimpleImmutableEntry<WriteCipherGenerator, ProtocolVersion[]>(
                new T10BlockWriteCipherGenerator(),
                ProtocolVersion.PROTOCOLS_TO_10
            )
        })),

    // domestic strength ciphers
    @SuppressWarnings({"unchecked", "rawtypes"})
    B_RC4_128(CIPHER_RC4, STREAM_CIPHER, 16, 16, 0, 0, true, false,
        (Map.Entry<ReadCipherGenerator,
                ProtocolVersion[]>[])(new Map.Entry[] {
            new SimpleImmutableEntry<ReadCipherGenerator, ProtocolVersion[]>(
                new StreamReadCipherGenerator(),
                ProtocolVersion.PROTOCOLS_TO_12
            )
        }),
        (Map.Entry<WriteCipherGenerator,
                ProtocolVersion[]>[])(new Map.Entry[] {
            new SimpleImmutableEntry<WriteCipherGenerator, ProtocolVersion[]>(
                new StreamWriteCipherGenerator(),
                ProtocolVersion.PROTOCOLS_TO_12
            )
        })),

    @SuppressWarnings({"unchecked", "rawtypes"})
    B_DES(CIPHER_DES, BLOCK_CIPHER, 8, 8, 8, 0, true, false,
        (Map.Entry<ReadCipherGenerator,
                ProtocolVersion[]>[])(new Map.Entry[] {
            new SimpleImmutableEntry<ReadCipherGenerator, ProtocolVersion[]>(
                new T10BlockReadCipherGenerator(),
                ProtocolVersion.PROTOCOLS_TO_10
            ),
            new SimpleImmutableEntry<ReadCipherGenerator, ProtocolVersion[]>(
                new T11BlockReadCipherGenerator(),
                ProtocolVersion.PROTOCOLS_OF_11
            )
        }),
        (Map.Entry<WriteCipherGenerator,
                ProtocolVersion[]>[])(new Map.Entry[] {
            new SimpleImmutableEntry<WriteCipherGenerator, ProtocolVersion[]>(
                new T10BlockWriteCipherGenerator(),
                ProtocolVersion.PROTOCOLS_TO_10
            ),
            new SimpleImmutableEntry<WriteCipherGenerator, ProtocolVersion[]>(
                new T11BlockWriteCipherGenerator(),
                ProtocolVersion.PROTOCOLS_OF_11
            )
        })),

    @SuppressWarnings({"unchecked", "rawtypes"})
    B_3DES(CIPHER_3DES, BLOCK_CIPHER, 24, 24, 8, 0, true, false,
        (Map.Entry<ReadCipherGenerator,
                ProtocolVersion[]>[])(new Map.Entry[] {
            new SimpleImmutableEntry<ReadCipherGenerator, ProtocolVersion[]>(
                new T10BlockReadCipherGenerator(),
                ProtocolVersion.PROTOCOLS_TO_10
            ),
            new SimpleImmutableEntry<ReadCipherGenerator, ProtocolVersion[]>(
                new T11BlockReadCipherGenerator(),
                ProtocolVersion.PROTOCOLS_11_12
            )
        }),
        (Map.Entry<WriteCipherGenerator,
                ProtocolVersion[]>[])(new Map.Entry[] {
            new SimpleImmutableEntry<WriteCipherGenerator, ProtocolVersion[]>(
                new T10BlockWriteCipherGenerator(),
                ProtocolVersion.PROTOCOLS_TO_10
            ),
            new SimpleImmutableEntry<WriteCipherGenerator, ProtocolVersion[]>(
                new T11BlockWriteCipherGenerator(),
                ProtocolVersion.PROTOCOLS_11_12
            )
        })),

    @SuppressWarnings({"unchecked", "rawtypes"})
    B_IDEA("IDEA", BLOCK_CIPHER, 16, 16, 8, 0, false, false,
        (Map.Entry<ReadCipherGenerator,
                ProtocolVersion[]>[])(new Map.Entry[] {
            new SimpleImmutableEntry<ReadCipherGenerator, ProtocolVersion[]>(
                null,
                ProtocolVersion.PROTOCOLS_TO_12
            )
        }),
        (Map.Entry<WriteCipherGenerator,
                ProtocolVersion[]>[])(new Map.Entry[] {
            new SimpleImmutableEntry<WriteCipherGenerator, ProtocolVersion[]>(
                null,
                ProtocolVersion.PROTOCOLS_TO_12
            )
        })),

    @SuppressWarnings({"unchecked", "rawtypes"})
    B_AES_128(CIPHER_AES, BLOCK_CIPHER, 16, 16, 16, 0, true, false,
        (Map.Entry<ReadCipherGenerator,
                ProtocolVersion[]>[])(new Map.Entry[] {
            new SimpleImmutableEntry<ReadCipherGenerator, ProtocolVersion[]>(
                new T10BlockReadCipherGenerator(),
                ProtocolVersion.PROTOCOLS_TO_10
            ),
            new SimpleImmutableEntry<ReadCipherGenerator, ProtocolVersion[]>(
                new T11BlockReadCipherGenerator(),
                ProtocolVersion.PROTOCOLS_11_12
            )
        }),
        (Map.Entry<WriteCipherGenerator,
                ProtocolVersion[]>[])(new Map.Entry[] {
            new SimpleImmutableEntry<WriteCipherGenerator, ProtocolVersion[]>(
                new T10BlockWriteCipherGenerator(),
                ProtocolVersion.PROTOCOLS_TO_10
            ),
            new SimpleImmutableEntry<WriteCipherGenerator, ProtocolVersion[]>(
                new T11BlockWriteCipherGenerator(),
                ProtocolVersion.PROTOCOLS_11_12
            )
        })),

    @SuppressWarnings({"unchecked", "rawtypes"})
    B_AES_256(CIPHER_AES, BLOCK_CIPHER, 32, 32, 16, 0, true, false,
        (Map.Entry<ReadCipherGenerator,
                ProtocolVersion[]>[])(new Map.Entry[] {
            new SimpleImmutableEntry<ReadCipherGenerator, ProtocolVersion[]>(
                new T10BlockReadCipherGenerator(),
                ProtocolVersion.PROTOCOLS_TO_10
            ),
            new SimpleImmutableEntry<ReadCipherGenerator, ProtocolVersion[]>(
                new T11BlockReadCipherGenerator(),
                ProtocolVersion.PROTOCOLS_11_12
            )
        }),
        (Map.Entry<WriteCipherGenerator,
                ProtocolVersion[]>[])(new Map.Entry[] {
            new SimpleImmutableEntry<WriteCipherGenerator, ProtocolVersion[]>(
                new T10BlockWriteCipherGenerator(),
                ProtocolVersion.PROTOCOLS_TO_10
            ),
            new SimpleImmutableEntry<WriteCipherGenerator, ProtocolVersion[]>(
                new T11BlockWriteCipherGenerator(),
                ProtocolVersion.PROTOCOLS_11_12
            )
        })),

    @SuppressWarnings({"unchecked", "rawtypes"})
    B_AES_128_GCM(CIPHER_AES_GCM, AEAD_CIPHER, 16, 16, 12, 4, true, false,
        (Map.Entry<ReadCipherGenerator,
                ProtocolVersion[]>[])(new Map.Entry[] {
            new SimpleImmutableEntry<ReadCipherGenerator, ProtocolVersion[]>(
                new T12GcmReadCipherGenerator(),
                ProtocolVersion.PROTOCOLS_OF_12
            )
        }),
        (Map.Entry<WriteCipherGenerator,
                ProtocolVersion[]>[])(new Map.Entry[] {
            new SimpleImmutableEntry<WriteCipherGenerator, ProtocolVersion[]>(
                new T12GcmWriteCipherGenerator(),
                ProtocolVersion.PROTOCOLS_OF_12
            )
        })),

    @SuppressWarnings({"unchecked", "rawtypes"})
    B_AES_256_GCM(CIPHER_AES_GCM, AEAD_CIPHER, 32, 32, 12, 4, true, false,
        (Map.Entry<ReadCipherGenerator,
                ProtocolVersion[]>[])(new Map.Entry[] {
            new SimpleImmutableEntry<ReadCipherGenerator, ProtocolVersion[]>(
                new T12GcmReadCipherGenerator(),
                ProtocolVersion.PROTOCOLS_OF_12
            )
        }),
        (Map.Entry<WriteCipherGenerator,
                ProtocolVersion[]>[])(new Map.Entry[] {
            new SimpleImmutableEntry<WriteCipherGenerator, ProtocolVersion[]>(
                new T12GcmWriteCipherGenerator(),
                ProtocolVersion.PROTOCOLS_OF_12
            )
        })),

    @SuppressWarnings({"unchecked", "rawtypes"})
    B_AES_128_GCM_IV(CIPHER_AES_GCM, AEAD_CIPHER, 16, 16, 12, 0, true, false,
        (Map.Entry<ReadCipherGenerator,
                ProtocolVersion[]>[])(new Map.Entry[] {
            new SimpleImmutableEntry<ReadCipherGenerator, ProtocolVersion[]>(
                new T13GcmReadCipherGenerator(),
                ProtocolVersion.PROTOCOLS_OF_13
            )
        }),
        (Map.Entry<WriteCipherGenerator,
                ProtocolVersion[]>[])(new Map.Entry[] {
            new SimpleImmutableEntry<WriteCipherGenerator, ProtocolVersion[]>(
                new T13GcmWriteCipherGenerator(),
                ProtocolVersion.PROTOCOLS_OF_13
            )
        })),

    @SuppressWarnings({"unchecked", "rawtypes"})
    B_AES_256_GCM_IV(CIPHER_AES_GCM, AEAD_CIPHER, 32, 32, 12, 0, true, false,
        (Map.Entry<ReadCipherGenerator,
                ProtocolVersion[]>[])(new Map.Entry[] {
            new SimpleImmutableEntry<ReadCipherGenerator, ProtocolVersion[]>(
                new T13GcmReadCipherGenerator(),
                ProtocolVersion.PROTOCOLS_OF_13
            )
        }),
        (Map.Entry<WriteCipherGenerator,
                ProtocolVersion[]>[])(new Map.Entry[] {
            new SimpleImmutableEntry<WriteCipherGenerator, ProtocolVersion[]>(
                new T13GcmWriteCipherGenerator(),
                ProtocolVersion.PROTOCOLS_OF_13
            )
        })),

    @SuppressWarnings({"unchecked", "rawtypes"})
    B_CC20_P1305(CIPHER_CHACHA20_POLY1305, AEAD_CIPHER, 32, 32, 12,
            12, true, false,
        (Map.Entry<ReadCipherGenerator,
                ProtocolVersion[]>[])(new Map.Entry[] {
            new SimpleImmutableEntry<ReadCipherGenerator, ProtocolVersion[]>(
                new T12CC20P1305ReadCipherGenerator(),
                ProtocolVersion.PROTOCOLS_OF_12
            ),
            new SimpleImmutableEntry<ReadCipherGenerator, ProtocolVersion[]>(
                new T13CC20P1305ReadCipherGenerator(),
                ProtocolVersion.PROTOCOLS_OF_13
            )
        }),
        (Map.Entry<WriteCipherGenerator,
                ProtocolVersion[]>[])(new Map.Entry[] {
            new SimpleImmutableEntry<WriteCipherGenerator, ProtocolVersion[]>(
                new T12CC20P1305WriteCipherGenerator(),
                ProtocolVersion.PROTOCOLS_OF_12
            ),
            new SimpleImmutableEntry<WriteCipherGenerator, ProtocolVersion[]>(
                new T13CC20P1305WriteCipherGenerator(),
                ProtocolVersion.PROTOCOLS_OF_13
            )
        }));

    // descriptive name including key size, e.g. AES/128
    final String description;

    // JCE cipher transformation string, e.g. AES/CBC/NoPadding
    final String transformation;

    // algorithm name, e.g. AES
    final String algorithm;

    // supported and compile time enabled. Also see isAvailable()
    final boolean allowed;

    // number of bytes of entropy in the key
    final int keySize;

    // length of the actual cipher key in bytes.
    // for non-exportable ciphers, this is the same as keySize
    final int expandedKeySize;

    // size of the IV
    final int ivSize;

    // size of fixed IV
    //
    // record_iv_length = ivSize - fixedIvSize
    final int fixedIvSize;

    // exportable under 512/40 bit rules
    final boolean exportable;

    // Is the cipher algorithm of Cipher Block Chaining (CBC) mode?
    final CipherType cipherType;

    // size of the authentication tag, only applicable to cipher suites in
    // Galois Counter Mode (GCM)
    //
    // As far as we know, all supported GCM cipher suites use 128-bits
    // authentication tags.
    final int tagSize = 16;

    // runtime availability
    private final boolean isAvailable;

    private final Map.Entry<ReadCipherGenerator,
            ProtocolVersion[]>[] readCipherGenerators;
    private final Map.Entry<WriteCipherGenerator,
            ProtocolVersion[]>[] writeCipherGenerators;

    // Map of Ciphers listed in jdk.tls.keyLimits
    private static final HashMap<String, Long> cipherLimits = new HashMap<>();

    // Keywords found on the jdk.tls.keyLimits security property.
    final static String tag[] = {"KEYUPDATE"};

    static  {
        final long max = 4611686018427387904L; // 2^62
        String prop = AccessController.doPrivileged(
                new PrivilegedAction<String>() {
            @Override
            public String run() {
                return Security.getProperty("jdk.tls.keyLimits");
            }
        });
        if (prop != null) {
            String propvalue[] = prop.split(",");

            for (String entry : propvalue) {
                int index;
                // If this is not a UsageLimit, goto to next entry.
                String values[] = entry.trim().toUpperCase().split(" ");

                if (values[1].contains(tag[0])) {
                    index = 0;
                } else {
                    if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
                        SSLLogger.fine("jdk.tls.keyLimits:  Unknown action:  " +
                                entry);
                    }
                    continue;
                }

                long size;
                int i = values[2].indexOf("^");
                try {
                    if (i >= 0) {
                        size = (long) Math.pow(2,
                                Integer.parseInt(values[2].substring(i + 1)));
                    } else {
                        size = Long.parseLong(values[2]);
                    }
                    if (size < 1 || size > max) {
                        throw new NumberFormatException(
                            "Length exceeded limits");
                    }
                } catch (NumberFormatException e) {
                    if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
                        SSLLogger.fine("jdk.tls.keyLimits:  " + e.getMessage() +
                                ":  " +  entry);
                    }
                    continue;
                }
                if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
                    SSLLogger.fine("jdk.tls.keyLimits:  entry = " + entry +
                            ". " + values[0] + ":" + tag[index] + " = " + size);
                }
                cipherLimits.put(values[0] + ":" + tag[index], size);
            }
        }
    }

    private SSLCipher(String transformation,
            CipherType cipherType, int keySize,
            int expandedKeySize, int ivSize,
            int fixedIvSize, boolean allowed, boolean exportable,
            Map.Entry<ReadCipherGenerator,
                    ProtocolVersion[]>[] readCipherGenerators,
            Map.Entry<WriteCipherGenerator,
                    ProtocolVersion[]>[] writeCipherGenerators) {
        this.transformation = transformation;
        String[] splits = transformation.split("/");
        this.algorithm = splits[0];
        this.cipherType = cipherType;
        this.description = this.algorithm + "/" + (keySize << 3);
        this.keySize = keySize;
        this.ivSize = ivSize;
        this.fixedIvSize = fixedIvSize;
        this.allowed = allowed;

        this.expandedKeySize = expandedKeySize;
        this.exportable = exportable;

        // availability of this bulk cipher
        //
        // AES/256 is unavailable when the default JCE policy jurisdiction files
        // are installed because of key length restrictions.
        this.isAvailable = allowed && isUnlimited(keySize, transformation) &&
                isTransformationAvailable(transformation);

        this.readCipherGenerators = readCipherGenerators;
        this.writeCipherGenerators = writeCipherGenerators;
    }

    private static boolean isTransformationAvailable(String transformation) {
        if (transformation.equals("NULL")) {
            return true;
        }
        try {
            JsseJce.getCipher(transformation);
            return true;
        } catch (NoSuchAlgorithmException e) {
            if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
                SSLLogger.fine("Transformation " + transformation + " is" +
                        " not available.");
            }
        }
        return false;

    }

    SSLReadCipher createReadCipher(Authenticator authenticator,
            ProtocolVersion protocolVersion,
            SecretKey key, IvParameterSpec iv,
            SecureRandom random) throws GeneralSecurityException {
        if (writeCipherGenerators.length == 0) {
            return null;
        }

        ReadCipherGenerator wcg = null;
        for (Map.Entry<ReadCipherGenerator,
                ProtocolVersion[]> me : readCipherGenerators) {
            for (ProtocolVersion pv : me.getValue()) {
                if (protocolVersion == pv) {
                    wcg = me.getKey();
                }
            }
        }

        if (wcg != null) {
            return wcg.createCipher(this, authenticator,
                    protocolVersion, transformation, key, iv, random);
        }
        return null;
    }

    SSLWriteCipher createWriteCipher(Authenticator authenticator,
            ProtocolVersion protocolVersion,
            SecretKey key, IvParameterSpec iv,
            SecureRandom random) throws GeneralSecurityException {
        if (readCipherGenerators.length == 0) {
            return null;
        }

        WriteCipherGenerator rcg = null;
        for (Map.Entry<WriteCipherGenerator,
                ProtocolVersion[]> me : writeCipherGenerators) {
            for (ProtocolVersion pv : me.getValue()) {
                if (protocolVersion == pv) {
                    rcg = me.getKey();
                }
            }
        }

        if (rcg != null) {
            return rcg.createCipher(this, authenticator,
                    protocolVersion, transformation, key, iv, random);
        }
        return null;
    }

    /**
     * Test if this bulk cipher is available. For use by CipherSuite.
     */
    boolean isAvailable() {
        return this.isAvailable;
    }

    private static boolean isUnlimited(int keySize, String transformation) {
        int keySizeInBits = keySize * 8;
        if (keySizeInBits > 128) {    // need the JCE unlimited
                                      // strength jurisdiction policy
            try {
                if (Cipher.getMaxAllowedKeyLength(
                        transformation) < keySizeInBits) {
                    return false;
                }
            } catch (Exception e) {
                return false;
            }
        }

        return true;
    }

    @Override
    public String toString() {
        return description;
    }

    interface ReadCipherGenerator {
        SSLReadCipher createCipher(SSLCipher sslCipher,
                Authenticator authenticator,
                ProtocolVersion protocolVersion, String algorithm,
                Key key, AlgorithmParameterSpec params,
                SecureRandom random) throws GeneralSecurityException;
    }

    abstract static class SSLReadCipher {
        final Authenticator authenticator;
        final ProtocolVersion protocolVersion;
        boolean keyLimitEnabled = false;
        long keyLimitCountdown = 0;
        SecretKey baseSecret;

        SSLReadCipher(Authenticator authenticator,
                ProtocolVersion protocolVersion) {
            this.authenticator = authenticator;
            this.protocolVersion = protocolVersion;
        }

        static final SSLReadCipher nullTlsReadCipher() {
            try {
                return B_NULL.createReadCipher(
                        Authenticator.nullTlsMac(),
                        ProtocolVersion.NONE, null, null, null);
            } catch (GeneralSecurityException gse) {
                // unlikely
                throw new RuntimeException("Cannot create NULL SSLCipher", gse);
            }
        }

        static final SSLReadCipher nullDTlsReadCipher() {
            try {
                return B_NULL.createReadCipher(
                        Authenticator.nullDtlsMac(),
                        ProtocolVersion.NONE, null, null, null);
            } catch (GeneralSecurityException gse) {
                // unlikely
                throw new RuntimeException("Cannot create NULL SSLCipher", gse);
            }
        }

        abstract Plaintext decrypt(byte contentType, ByteBuffer bb,
                    byte[] sequence) throws GeneralSecurityException;

        void dispose() {
            // blank
        }

        abstract int estimateFragmentSize(int packetSize, int headerSize);

        boolean isNullCipher() {
            return false;
        }

        /**
         * Check if processed bytes have reached the key usage limit.
         * If key usage limit is not be monitored, return false.
         */
        public boolean atKeyLimit() {
            if (keyLimitCountdown >= 0) {
                return false;
            }

            // Turn off limit checking as KeyUpdate will be occurring
            keyLimitEnabled = false;
            return true;
        }
    }

    interface WriteCipherGenerator {
        SSLWriteCipher createCipher(SSLCipher sslCipher,
                Authenticator authenticator,
                ProtocolVersion protocolVersion, String algorithm,
                Key key, AlgorithmParameterSpec params,
                SecureRandom random) throws GeneralSecurityException;
    }

    abstract static class SSLWriteCipher {
        final Authenticator authenticator;
        final ProtocolVersion protocolVersion;
        boolean keyLimitEnabled = false;
        long keyLimitCountdown = 0;
        SecretKey baseSecret;

        SSLWriteCipher(Authenticator authenticator,
                ProtocolVersion protocolVersion) {
            this.authenticator = authenticator;
            this.protocolVersion = protocolVersion;
        }

        abstract int encrypt(byte contentType, ByteBuffer bb);

        static final SSLWriteCipher nullTlsWriteCipher() {
            try {
                return B_NULL.createWriteCipher(
                        Authenticator.nullTlsMac(),
                        ProtocolVersion.NONE, null, null, null);
            } catch (GeneralSecurityException gse) {
                // unlikely
                throw new RuntimeException(
                        "Cannot create NULL SSL write Cipher", gse);
            }
        }

        static final SSLWriteCipher nullDTlsWriteCipher() {
            try {
                return B_NULL.createWriteCipher(
                        Authenticator.nullDtlsMac(),
                        ProtocolVersion.NONE, null, null, null);
            } catch (GeneralSecurityException gse) {
                // unlikely
                throw new RuntimeException(
                        "Cannot create NULL SSL write Cipher", gse);
            }
        }

        void dispose() {
            // blank
        }

        abstract int getExplicitNonceSize();
        abstract int calculateFragmentSize(int packetLimit, int headerSize);
        abstract int calculatePacketSize(int fragmentSize, int headerSize);

        boolean isCBCMode() {
            return false;
        }

        boolean isNullCipher() {
            return false;
        }

        /**
         * Check if processed bytes have reached the key usage limit.
         * If key usage limit is not be monitored, return false.
         */
        public boolean atKeyLimit() {
            if (keyLimitCountdown >= 0) {
                return false;
            }

            // Turn off limit checking as KeyUpdate will be occurring
            keyLimitEnabled = false;
            return true;
        }
    }

    private static final
            class NullReadCipherGenerator implements ReadCipherGenerator {
        @Override
        public SSLReadCipher createCipher(SSLCipher sslCipher,
                Authenticator authenticator,
                ProtocolVersion protocolVersion, String algorithm,
                Key key, AlgorithmParameterSpec params,
                SecureRandom random) throws GeneralSecurityException {
            return new NullReadCipher(authenticator, protocolVersion);
        }

        static final class NullReadCipher extends SSLReadCipher {
            NullReadCipher(Authenticator authenticator,
                    ProtocolVersion protocolVersion) {
                super(authenticator, protocolVersion);
            }

            @Override
            public Plaintext decrypt(byte contentType, ByteBuffer bb,
                    byte[] sequence) throws GeneralSecurityException {
                MAC signer = (MAC)authenticator;
                if (signer.macAlg().size != 0) {
                    checkStreamMac(signer, bb, contentType, sequence);
                } else {
                    authenticator.increaseSequenceNumber();
                }

                return new Plaintext(contentType,
                        ProtocolVersion.NONE.major, ProtocolVersion.NONE.minor,
                        -1, -1L, bb.slice());
            }

            @Override
            int estimateFragmentSize(int packetSize, int headerSize) {
                int macLen = ((MAC)authenticator).macAlg().size;
                return packetSize - headerSize - macLen;
            }

            @Override
            boolean isNullCipher() {
                return true;
            }
        }
    }

    private static final
            class NullWriteCipherGenerator implements WriteCipherGenerator {
        @Override
        public SSLWriteCipher createCipher(SSLCipher sslCipher,
                Authenticator authenticator,
                ProtocolVersion protocolVersion, String algorithm,
                Key key, AlgorithmParameterSpec params,
                SecureRandom random) throws GeneralSecurityException {
            return new NullWriteCipher(authenticator, protocolVersion);
        }

        static final class NullWriteCipher extends SSLWriteCipher {
            NullWriteCipher(Authenticator authenticator,
                    ProtocolVersion protocolVersion) {
                super(authenticator, protocolVersion);
            }

            @Override
            public int encrypt(byte contentType, ByteBuffer bb) {
                // add message authentication code
                MAC signer = (MAC)authenticator;
                if (signer.macAlg().size != 0) {
                    addMac(signer, bb, contentType);
                } else {
                    authenticator.increaseSequenceNumber();
                }

                int len = bb.remaining();
                bb.position(bb.limit());
                return len;
            }


            @Override
            int getExplicitNonceSize() {
                return 0;
            }

            @Override
            int calculateFragmentSize(int packetLimit, int headerSize) {
                int macLen = ((MAC)authenticator).macAlg().size;
                return packetLimit - headerSize - macLen;
            }

            @Override
            int calculatePacketSize(int fragmentSize, int headerSize) {
                int macLen = ((MAC)authenticator).macAlg().size;
                return fragmentSize + headerSize + macLen;
            }

            @Override
            boolean isNullCipher() {
                return true;
            }
        }
    }

    private static final
            class StreamReadCipherGenerator implements ReadCipherGenerator {
        @Override
        public SSLReadCipher createCipher(SSLCipher sslCipher,
                Authenticator authenticator,
                ProtocolVersion protocolVersion, String algorithm,
                Key key, AlgorithmParameterSpec params,
                SecureRandom random) throws GeneralSecurityException {
            return new StreamReadCipher(authenticator, protocolVersion,
                    algorithm, key, params, random);
        }

        static final class StreamReadCipher extends SSLReadCipher {
            private final Cipher cipher;

            StreamReadCipher(Authenticator authenticator,
                    ProtocolVersion protocolVersion, String algorithm,
                    Key key, AlgorithmParameterSpec params,
                    SecureRandom random) throws GeneralSecurityException {
                super(authenticator, protocolVersion);
                this.cipher = JsseJce.getCipher(algorithm);
                cipher.init(Cipher.DECRYPT_MODE, key, params, random);
            }

            @Override
            public Plaintext decrypt(byte contentType, ByteBuffer bb,
                    byte[] sequence) throws GeneralSecurityException {
                int len = bb.remaining();
                int pos = bb.position();
                ByteBuffer dup = bb.duplicate();
                try {
                    if (len != cipher.update(dup, bb)) {
                        // catch BouncyCastle buffering error
                        throw new RuntimeException(
                                "Unexpected number of plaintext bytes");
                    }
                    if (bb.position() != dup.position()) {
                        throw new RuntimeException(
                                "Unexpected ByteBuffer position");
                    }
                } catch (ShortBufferException sbe) {
                    // catch BouncyCastle buffering error
                    throw new RuntimeException("Cipher buffering error in " +
                        "JCE provider " + cipher.getProvider().getName(), sbe);
                }
                bb.position(pos);
                if (SSLLogger.isOn && SSLLogger.isOn("plaintext")) {
                    SSLLogger.fine(
                            "Plaintext after DECRYPTION", bb.duplicate());
                }

                MAC signer = (MAC)authenticator;
                if (signer.macAlg().size != 0) {
                    checkStreamMac(signer, bb, contentType, sequence);
                } else {
                    authenticator.increaseSequenceNumber();
                }

                return new Plaintext(contentType,
                        ProtocolVersion.NONE.major, ProtocolVersion.NONE.minor,
                        -1, -1L, bb.slice());
            }

            @Override
            void dispose() {
                if (cipher != null) {
                    try {
                        cipher.doFinal();
                    } catch (Exception e) {
                        // swallow all types of exceptions.
                    }
                }
            }

            @Override
            int estimateFragmentSize(int packetSize, int headerSize) {
                int macLen = ((MAC)authenticator).macAlg().size;
                return packetSize - headerSize - macLen;
            }
        }
    }

    private static final
            class StreamWriteCipherGenerator implements WriteCipherGenerator {
        @Override
        public SSLWriteCipher createCipher(SSLCipher sslCipher,
                Authenticator authenticator,
                ProtocolVersion protocolVersion, String algorithm,
                Key key, AlgorithmParameterSpec params,
                SecureRandom random) throws GeneralSecurityException {
            return new StreamWriteCipher(authenticator,
                    protocolVersion, algorithm, key, params, random);
        }

        static final class StreamWriteCipher extends SSLWriteCipher {
            private final Cipher cipher;

            StreamWriteCipher(Authenticator authenticator,
                    ProtocolVersion protocolVersion, String algorithm,
                    Key key, AlgorithmParameterSpec params,
                    SecureRandom random) throws GeneralSecurityException {
                super(authenticator, protocolVersion);
                this.cipher = JsseJce.getCipher(algorithm);
                cipher.init(Cipher.ENCRYPT_MODE, key, params, random);
            }

            @Override
            public int encrypt(byte contentType, ByteBuffer bb) {
                // add message authentication code
                MAC signer = (MAC)authenticator;
                if (signer.macAlg().size != 0) {
                    addMac(signer, bb, contentType);
                } else {
                    authenticator.increaseSequenceNumber();
                }

                if (SSLLogger.isOn && SSLLogger.isOn("plaintext")) {
                    SSLLogger.finest(
                        "Padded plaintext before ENCRYPTION", bb.duplicate());
                }

                int len = bb.remaining();
                ByteBuffer dup = bb.duplicate();
                try {
                    if (len != cipher.update(dup, bb)) {
                        // catch BouncyCastle buffering error
                        throw new RuntimeException(
                                "Unexpected number of plaintext bytes");
                    }
                    if (bb.position() != dup.position()) {
                        throw new RuntimeException(
                                "Unexpected ByteBuffer position");
                    }
                } catch (ShortBufferException sbe) {
                    // catch BouncyCastle buffering error
                    throw new RuntimeException("Cipher buffering error in " +
                        "JCE provider " + cipher.getProvider().getName(), sbe);
                }

                return len;
            }

            @Override
            void dispose() {
                if (cipher != null) {
                    try {
                        cipher.doFinal();
                    } catch (Exception e) {
                        // swallow all types of exceptions.
                    }
                }
            }

            @Override
            int getExplicitNonceSize() {
                return 0;
            }

            @Override
            int calculateFragmentSize(int packetLimit, int headerSize) {
                int macLen = ((MAC)authenticator).macAlg().size;
                return packetLimit - headerSize - macLen;
            }

            @Override
            int calculatePacketSize(int fragmentSize, int headerSize) {
                int macLen = ((MAC)authenticator).macAlg().size;
                return fragmentSize + headerSize + macLen;
            }
        }
    }

    private static final
            class T10BlockReadCipherGenerator implements ReadCipherGenerator {
        @Override
        public SSLReadCipher createCipher(SSLCipher sslCipher,
                Authenticator authenticator,
                ProtocolVersion protocolVersion, String algorithm,
                Key key, AlgorithmParameterSpec params,
                SecureRandom random) throws GeneralSecurityException {
            return new BlockReadCipher(authenticator,
                    protocolVersion, algorithm, key, params, random);
        }

        static final class BlockReadCipher extends SSLReadCipher {
            private final Cipher cipher;

            BlockReadCipher(Authenticator authenticator,
                    ProtocolVersion protocolVersion, String algorithm,
                    Key key, AlgorithmParameterSpec params,
                    SecureRandom random) throws GeneralSecurityException {
                super(authenticator, protocolVersion);
                this.cipher = JsseJce.getCipher(algorithm);
                cipher.init(Cipher.DECRYPT_MODE, key, params, random);
            }

            @Override
            public Plaintext decrypt(byte contentType, ByteBuffer bb,
                    byte[] sequence) throws GeneralSecurityException {
                BadPaddingException reservedBPE = null;

                // sanity check length of the ciphertext
                MAC signer = (MAC)authenticator;
                int cipheredLength = bb.remaining();
                int tagLen = signer.macAlg().size;
                if (tagLen != 0) {
                    if (!sanityCheck(tagLen, bb.remaining())) {
                        reservedBPE = new BadPaddingException(
                                "ciphertext sanity check failed");
                    }
                }
                // decryption
                int len = bb.remaining();
                int pos = bb.position();
                ByteBuffer dup = bb.duplicate();
                try {
                    if (len != cipher.update(dup, bb)) {
                        // catch BouncyCastle buffering error
                        throw new RuntimeException(
                                "Unexpected number of plaintext bytes");
                    }

                    if (bb.position() != dup.position()) {
                        throw new RuntimeException(
                                "Unexpected ByteBuffer position");
                    }
                } catch (ShortBufferException sbe) {
                    // catch BouncyCastle buffering error
                    throw new RuntimeException("Cipher buffering error in " +
                        "JCE provider " + cipher.getProvider().getName(), sbe);
                }

                if (SSLLogger.isOn && SSLLogger.isOn("plaintext")) {
                    SSLLogger.fine(
                            "Padded plaintext after DECRYPTION",
                            bb.duplicate().position(pos));
                }

                // remove the block padding
                int blockSize = cipher.getBlockSize();
                bb.position(pos);
                try {
                    removePadding(bb, tagLen, blockSize, protocolVersion);
                } catch (BadPaddingException bpe) {
                    if (reservedBPE == null) {
                        reservedBPE = bpe;
                    }
                }

                // Requires message authentication code for null, stream and
                // block cipher suites.
                try {
                    if (tagLen != 0) {
                        checkCBCMac(signer, bb,
                                contentType, cipheredLength, sequence);
                    } else {
                        authenticator.increaseSequenceNumber();
                    }
                } catch (BadPaddingException bpe) {
                    if (reservedBPE == null) {
                        reservedBPE = bpe;
                    }
                }

                // Is it a failover?
                if (reservedBPE != null) {
                    throw reservedBPE;
                }

                return new Plaintext(contentType,
                        ProtocolVersion.NONE.major, ProtocolVersion.NONE.minor,
                        -1, -1L, bb.slice());
            }

            @Override
            void dispose() {
                if (cipher != null) {
                    try {
                        cipher.doFinal();
                    } catch (Exception e) {
                        // swallow all types of exceptions.
                    }
                }
            }

            @Override
            int estimateFragmentSize(int packetSize, int headerSize) {
                int macLen = ((MAC)authenticator).macAlg().size;

                // No padding for a maximum fragment.
                //
                // 1 byte padding length field: 0x00
                return packetSize - headerSize - macLen - 1;
            }

            /**
             * Sanity check the length of a fragment before decryption.
             *
             * In CBC mode, check that the fragment length is one or multiple
             * times of the block size of the cipher suite, and is at least
             * one (one is the smallest size of padding in CBC mode) bigger
             * than the tag size of the MAC algorithm except the explicit IV
             * size for TLS 1.1 or later.
             *
             * In non-CBC mode, check that the fragment length is not less than
             * the tag size of the MAC algorithm.
             *
             * @return true if the length of a fragment matches above
             *         requirements
             */
            private boolean sanityCheck(int tagLen, int fragmentLen) {
                int blockSize = cipher.getBlockSize();
                if ((fragmentLen % blockSize) == 0) {
                    int minimal = tagLen + 1;
                    minimal = (minimal >= blockSize) ? minimal : blockSize;

                    return (fragmentLen >= minimal);
                }

                return false;
            }
        }
    }

    private static final
            class T10BlockWriteCipherGenerator implements WriteCipherGenerator {
        @Override
        public SSLWriteCipher createCipher(SSLCipher sslCipher,
                Authenticator authenticator,
                ProtocolVersion protocolVersion, String algorithm,
                Key key, AlgorithmParameterSpec params,
                SecureRandom random) throws GeneralSecurityException {
            return new BlockWriteCipher(authenticator,
                    protocolVersion, algorithm, key, params, random);
        }

        static final class BlockWriteCipher extends SSLWriteCipher {
            private final Cipher cipher;

            BlockWriteCipher(Authenticator authenticator,
                    ProtocolVersion protocolVersion, String algorithm,
                    Key key, AlgorithmParameterSpec params,
                    SecureRandom random) throws GeneralSecurityException {
                super(authenticator, protocolVersion);
                this.cipher = JsseJce.getCipher(algorithm);
                cipher.init(Cipher.ENCRYPT_MODE, key, params, random);
            }

            @Override
            public int encrypt(byte contentType, ByteBuffer bb) {
                int pos = bb.position();

                // add message authentication code
                MAC signer = (MAC)authenticator;
                if (signer.macAlg().size != 0) {
                    addMac(signer, bb, contentType);
                } else {
                    authenticator.increaseSequenceNumber();
                }

                int blockSize = cipher.getBlockSize();
                int len = addPadding(bb, blockSize);
                bb.position(pos);

                if (SSLLogger.isOn && SSLLogger.isOn("plaintext")) {
                    SSLLogger.fine(
                            "Padded plaintext before ENCRYPTION",
                            bb.duplicate());
                }

                ByteBuffer dup = bb.duplicate();
                try {
                    if (len != cipher.update(dup, bb)) {
                        // catch BouncyCastle buffering error
                        throw new RuntimeException(
                                "Unexpected number of plaintext bytes");
                    }

                    if (bb.position() != dup.position()) {
                        throw new RuntimeException(
                                "Unexpected ByteBuffer position");
                    }
                } catch (ShortBufferException sbe) {
                    // catch BouncyCastle buffering error
                    throw new RuntimeException("Cipher buffering error in " +
                        "JCE provider " + cipher.getProvider().getName(), sbe);
                }

                return len;
            }

            @Override
            void dispose() {
                if (cipher != null) {
                    try {
                        cipher.doFinal();
                    } catch (Exception e) {
                        // swallow all types of exceptions.
                    }
                }
            }

            @Override
            int getExplicitNonceSize() {
                return 0;
            }

            @Override
            int calculateFragmentSize(int packetLimit, int headerSize) {
                int macLen = ((MAC)authenticator).macAlg().size;
                int blockSize = cipher.getBlockSize();
                int fragLen = packetLimit - headerSize;
                fragLen -= (fragLen % blockSize);   // cannot hold a block
                // No padding for a maximum fragment.
                fragLen -= 1;       // 1 byte padding length field: 0x00
                fragLen -= macLen;
                return fragLen;
            }

            @Override
            int calculatePacketSize(int fragmentSize, int headerSize) {
                int macLen = ((MAC)authenticator).macAlg().size;
                int blockSize = cipher.getBlockSize();
                int paddedLen = fragmentSize + macLen + 1;
                if ((paddedLen % blockSize)  != 0) {
                    paddedLen += blockSize - 1;
                    paddedLen -= paddedLen % blockSize;
                }

                return headerSize + paddedLen;
            }

            @Override
            boolean isCBCMode() {
                return true;
            }
        }
    }

    // For TLS 1.1 and 1.2
    private static final
            class T11BlockReadCipherGenerator implements ReadCipherGenerator {
        @Override
        public SSLReadCipher createCipher(SSLCipher sslCipher,
                Authenticator authenticator, ProtocolVersion protocolVersion,
                String algorithm, Key key, AlgorithmParameterSpec params,
                SecureRandom random) throws GeneralSecurityException {
            return new BlockReadCipher(authenticator, protocolVersion,
                    sslCipher, algorithm, key, params, random);
        }

        static final class BlockReadCipher extends SSLReadCipher {
            private final Cipher cipher;

            BlockReadCipher(Authenticator authenticator,
                    ProtocolVersion protocolVersion,
                    SSLCipher sslCipher, String algorithm,
                    Key key, AlgorithmParameterSpec params,
                    SecureRandom random) throws GeneralSecurityException {
                super(authenticator, protocolVersion);
                this.cipher = JsseJce.getCipher(algorithm);
                if (params == null) {
                    params = new IvParameterSpec(new byte[sslCipher.ivSize]);
                }
                cipher.init(Cipher.DECRYPT_MODE, key, params, random);
            }

            @Override
            public Plaintext decrypt(byte contentType, ByteBuffer bb,
                    byte[] sequence) throws GeneralSecurityException {
                BadPaddingException reservedBPE = null;

                // sanity check length of the ciphertext
                MAC signer = (MAC)authenticator;
                int cipheredLength = bb.remaining();
                int tagLen = signer.macAlg().size;
                if (tagLen != 0) {
                    if (!sanityCheck(tagLen, bb.remaining())) {
                        reservedBPE = new BadPaddingException(
                                "ciphertext sanity check failed");
                    }
                }

                // decryption
                int len = bb.remaining();
                int pos = bb.position();
                ByteBuffer dup = bb.duplicate();
                try {
                    if (len != cipher.update(dup, bb)) {
                        // catch BouncyCastle buffering error
                        throw new RuntimeException(
                                "Unexpected number of plaintext bytes");
                    }

                    if (bb.position() != dup.position()) {
                        throw new RuntimeException(
                                "Unexpected ByteBuffer position");
                    }
                } catch (ShortBufferException sbe) {
                    // catch BouncyCastle buffering error
                    throw new RuntimeException("Cipher buffering error in " +
                        "JCE provider " + cipher.getProvider().getName(), sbe);
                }

                if (SSLLogger.isOn && SSLLogger.isOn("plaintext")) {
                    SSLLogger.fine(
                            "Padded plaintext after DECRYPTION",
                            bb.duplicate().position(pos));
                }

                // Ignore the explicit nonce.
                bb.position(pos + cipher.getBlockSize());
                pos = bb.position();

                // remove the block padding
                int blockSize = cipher.getBlockSize();
                bb.position(pos);
                try {
                    removePadding(bb, tagLen, blockSize, protocolVersion);
                } catch (BadPaddingException bpe) {
                    if (reservedBPE == null) {
                        reservedBPE = bpe;
                    }
                }

                // Requires message authentication code for null, stream and
                // block cipher suites.
                try {
                    if (tagLen != 0) {
                        checkCBCMac(signer, bb,
                                contentType, cipheredLength, sequence);
                    } else {
                        authenticator.increaseSequenceNumber();
                    }
                } catch (BadPaddingException bpe) {
                    if (reservedBPE == null) {
                        reservedBPE = bpe;
                    }
                }

                // Is it a failover?
                if (reservedBPE != null) {
                    throw reservedBPE;
                }

                return new Plaintext(contentType,
                        ProtocolVersion.NONE.major, ProtocolVersion.NONE.minor,
                        -1, -1L, bb.slice());
            }

            @Override
            void dispose() {
                if (cipher != null) {
                    try {
                        cipher.doFinal();
                    } catch (Exception e) {
                        // swallow all types of exceptions.
                    }
                }
            }

            @Override
            int estimateFragmentSize(int packetSize, int headerSize) {
                int macLen = ((MAC)authenticator).macAlg().size;

                // No padding for a maximum fragment.
                //
                // 1 byte padding length field: 0x00
                int nonceSize = cipher.getBlockSize();
                return packetSize - headerSize - nonceSize - macLen - 1;
            }

            /**
             * Sanity check the length of a fragment before decryption.
             *
             * In CBC mode, check that the fragment length is one or multiple
             * times of the block size of the cipher suite, and is at least
             * one (one is the smallest size of padding in CBC mode) bigger
             * than the tag size of the MAC algorithm except the explicit IV
             * size for TLS 1.1 or later.
             *
             * In non-CBC mode, check that the fragment length is not less than
             * the tag size of the MAC algorithm.
             *
             * @return true if the length of a fragment matches above
             *         requirements
             */
            private boolean sanityCheck(int tagLen, int fragmentLen) {
                int blockSize = cipher.getBlockSize();
                if ((fragmentLen % blockSize) == 0) {
                    int minimal = tagLen + 1;
                    minimal = (minimal >= blockSize) ? minimal : blockSize;
                    minimal += blockSize;

                    return (fragmentLen >= minimal);
                }

                return false;
            }
        }
    }

    // For TLS 1.1 and 1.2
    private static final
            class T11BlockWriteCipherGenerator implements WriteCipherGenerator {
        @Override
        public SSLWriteCipher createCipher(SSLCipher sslCipher,
                Authenticator authenticator, ProtocolVersion protocolVersion,
                String algorithm, Key key, AlgorithmParameterSpec params,
                SecureRandom random) throws GeneralSecurityException {
            return new BlockWriteCipher(authenticator, protocolVersion,
                    sslCipher, algorithm, key, params, random);
        }

        static final class BlockWriteCipher extends SSLWriteCipher {
            private final Cipher cipher;
            private final SecureRandom random;

            BlockWriteCipher(Authenticator authenticator,
                    ProtocolVersion protocolVersion,
                    SSLCipher sslCipher, String algorithm,
                    Key key, AlgorithmParameterSpec params,
                    SecureRandom random) throws GeneralSecurityException {
                super(authenticator, protocolVersion);
                this.cipher = JsseJce.getCipher(algorithm);
                this.random = random;
                if (params == null) {
                    params = new IvParameterSpec(new byte[sslCipher.ivSize]);
                }
                cipher.init(Cipher.ENCRYPT_MODE, key, params, random);
            }

            @Override
            public int encrypt(byte contentType, ByteBuffer bb) {
                // To be unique and aware of overflow-wrap, sequence number
                // is used as the nonce_explicit of block cipher suites.
                int pos = bb.position();

                // add message authentication code
                MAC signer = (MAC)authenticator;
                if (signer.macAlg().size != 0) {
                    addMac(signer, bb, contentType);
                } else {
                    authenticator.increaseSequenceNumber();
                }

                // DON'T WORRY, the nonce spaces are considered already.
                byte[] nonce = new byte[cipher.getBlockSize()];
                random.nextBytes(nonce);
                pos = pos - nonce.length;
                bb.position(pos);
                bb.put(nonce);
                bb.position(pos);

                int blockSize = cipher.getBlockSize();
                int len = addPadding(bb, blockSize);
                bb.position(pos);

                if (SSLLogger.isOn && SSLLogger.isOn("plaintext")) {
                    SSLLogger.fine(
                            "Padded plaintext before ENCRYPTION",
                            bb.duplicate());
                }

                ByteBuffer dup = bb.duplicate();
                try {
                    if (len != cipher.update(dup, bb)) {
                        // catch BouncyCastle buffering error
                        throw new RuntimeException(
                                "Unexpected number of plaintext bytes");
                    }

                    if (bb.position() != dup.position()) {
                        throw new RuntimeException(
                                "Unexpected ByteBuffer position");
                    }
                } catch (ShortBufferException sbe) {
                    // catch BouncyCastle buffering error
                    throw new RuntimeException("Cipher buffering error in " +
                        "JCE provider " + cipher.getProvider().getName(), sbe);
                }

                return len;
            }

            @Override
            void dispose() {
                if (cipher != null) {
                    try {
                        cipher.doFinal();
                    } catch (Exception e) {
                        // swallow all types of exceptions.
                    }
                }
            }

            @Override
            int getExplicitNonceSize() {
                return cipher.getBlockSize();
            }

            @Override
            int calculateFragmentSize(int packetLimit, int headerSize) {
                int macLen = ((MAC)authenticator).macAlg().size;
                int blockSize = cipher.getBlockSize();
                int fragLen = packetLimit - headerSize - blockSize;
                fragLen -= (fragLen % blockSize);   // cannot hold a block
                // No padding for a maximum fragment.
                fragLen -= 1;       // 1 byte padding length field: 0x00
                fragLen -= macLen;
                return fragLen;
            }

            @Override
            int calculatePacketSize(int fragmentSize, int headerSize) {
                int macLen = ((MAC)authenticator).macAlg().size;
                int blockSize = cipher.getBlockSize();
                int paddedLen = fragmentSize + macLen + 1;
                if ((paddedLen % blockSize)  != 0) {
                    paddedLen += blockSize - 1;
                    paddedLen -= paddedLen % blockSize;
                }

                return headerSize + blockSize + paddedLen;
            }

            @Override
            boolean isCBCMode() {
                return true;
            }
        }
    }

    private static final
            class T12GcmReadCipherGenerator implements ReadCipherGenerator {
        @Override
        public SSLReadCipher createCipher(SSLCipher sslCipher,
                Authenticator authenticator,
                ProtocolVersion protocolVersion, String algorithm,
                Key key, AlgorithmParameterSpec params,
                SecureRandom random) throws GeneralSecurityException {
            return new GcmReadCipher(authenticator, protocolVersion, sslCipher,
                    algorithm, key, params, random);
        }

        static final class GcmReadCipher extends SSLReadCipher {
            private final Cipher cipher;
            private final int tagSize;
            private final Key key;
            private final byte[] fixedIv;
            private final int recordIvSize;
            private final SecureRandom random;

            GcmReadCipher(Authenticator authenticator,
                    ProtocolVersion protocolVersion,
                    SSLCipher sslCipher, String algorithm,
                    Key key, AlgorithmParameterSpec params,
                    SecureRandom random) throws GeneralSecurityException {
                super(authenticator, protocolVersion);
                this.cipher = JsseJce.getCipher(algorithm);
                this.tagSize = sslCipher.tagSize;
                this.key = key;
                this.fixedIv = ((IvParameterSpec)params).getIV();
                this.recordIvSize = sslCipher.ivSize - sslCipher.fixedIvSize;
                this.random = random;

                // DON'T initialize the cipher for AEAD!
            }

            @Override
            public Plaintext decrypt(byte contentType, ByteBuffer bb,
                    byte[] sequence) throws GeneralSecurityException {
                if (bb.remaining() < (recordIvSize + tagSize)) {
                    throw new BadPaddingException(
                        "Insufficient buffer remaining for AEAD cipher " +
                        "fragment (" + bb.remaining() + "). Needs to be " +
                        "more than or equal to IV size (" + recordIvSize +
                         ") + tag size (" + tagSize + ")");
                }

                // initialize the AEAD cipher for the unique IV
                byte[] iv = Arrays.copyOf(fixedIv,
                                    fixedIv.length + recordIvSize);
                bb.get(iv, fixedIv.length, recordIvSize);
                GCMParameterSpec spec = new GCMParameterSpec(tagSize * 8, iv);
                try {
                    cipher.init(Cipher.DECRYPT_MODE, key, spec, random);
                } catch (InvalidKeyException |
                            InvalidAlgorithmParameterException ikae) {
                    // unlikely to happen
                    throw new RuntimeException(
                                "invalid key or spec in GCM mode", ikae);
                }

                // update the additional authentication data
                byte[] aad = authenticator.acquireAuthenticationBytes(
                        contentType, bb.remaining() - tagSize,
                        sequence);
                cipher.updateAAD(aad);

                // DON'T decrypt the nonce_explicit for AEAD mode. The buffer
                // position has moved out of the nonce_explicit range.
                int len, pos = bb.position();
                ByteBuffer dup = bb.duplicate();
                try {
                    len = cipher.doFinal(dup, bb);
                } catch (IllegalBlockSizeException ibse) {
                    // unlikely to happen
                    throw new RuntimeException(
                        "Cipher error in AEAD mode \"" + ibse.getMessage() +
                        " \"in JCE provider " + cipher.getProvider().getName());
                } catch (ShortBufferException sbe) {
                    // catch BouncyCastle buffering error
                    throw new RuntimeException("Cipher buffering error in " +
                        "JCE provider " + cipher.getProvider().getName(), sbe);
                }
                // reset the limit to the end of the decrypted data
                bb.position(pos);
                bb.limit(pos + len);

                if (SSLLogger.isOn && SSLLogger.isOn("plaintext")) {
                    SSLLogger.fine(
                            "Plaintext after DECRYPTION", bb.duplicate());
                }

                return new Plaintext(contentType,
                        ProtocolVersion.NONE.major, ProtocolVersion.NONE.minor,
                        -1, -1L, bb.slice());
            }

            @Override
            void dispose() {
                if (cipher != null) {
                    try {
                        cipher.doFinal();
                    } catch (Exception e) {
                        // swallow all types of exceptions.
                    }
                }
            }

            @Override
            int estimateFragmentSize(int packetSize, int headerSize) {
                return packetSize - headerSize - recordIvSize - tagSize;
            }
        }
    }

    private static final
            class T12GcmWriteCipherGenerator implements WriteCipherGenerator {
        @Override
        public SSLWriteCipher createCipher(SSLCipher sslCipher,
                Authenticator authenticator,
                ProtocolVersion protocolVersion, String algorithm,
                Key key, AlgorithmParameterSpec params,
                SecureRandom random) throws GeneralSecurityException {
            return new GcmWriteCipher(authenticator, protocolVersion, sslCipher,
                    algorithm, key, params, random);
        }

        private static final class GcmWriteCipher extends SSLWriteCipher {
            private final Cipher cipher;
            private final int tagSize;
            private final Key key;
            private final byte[] fixedIv;
            private final int recordIvSize;
            private final SecureRandom random;

            GcmWriteCipher(Authenticator authenticator,
                    ProtocolVersion protocolVersion,
                    SSLCipher sslCipher, String algorithm,
                    Key key, AlgorithmParameterSpec params,
                    SecureRandom random) throws GeneralSecurityException {
                super(authenticator, protocolVersion);
                this.cipher = JsseJce.getCipher(algorithm);
                this.tagSize = sslCipher.tagSize;
                this.key = key;
                this.fixedIv = ((IvParameterSpec)params).getIV();
                this.recordIvSize = sslCipher.ivSize - sslCipher.fixedIvSize;
                this.random = random;

                // DON'T initialize the cipher for AEAD!
            }

            @Override
            public int encrypt(byte contentType,
                    ByteBuffer bb) {
                // To be unique and aware of overflow-wrap, sequence number
                // is used as the nonce_explicit of AEAD cipher suites.
                byte[] nonce = authenticator.sequenceNumber();

                // initialize the AEAD cipher for the unique IV
                byte[] iv = Arrays.copyOf(fixedIv,
                                            fixedIv.length + nonce.length);
                System.arraycopy(nonce, 0, iv, fixedIv.length, nonce.length);

                GCMParameterSpec spec = new GCMParameterSpec(tagSize * 8, iv);
                try {
                    cipher.init(Cipher.ENCRYPT_MODE, key, spec, random);
                } catch (InvalidKeyException |
                            InvalidAlgorithmParameterException ikae) {
                    // unlikely to happen
                    throw new RuntimeException(
                                "invalid key or spec in GCM mode", ikae);
                }

                // Update the additional authentication data, using the
                // implicit sequence number of the authenticator.
                byte[] aad = authenticator.acquireAuthenticationBytes(
                                        contentType, bb.remaining(), null);
                cipher.updateAAD(aad);

                // DON'T WORRY, the nonce spaces are considered already.
                bb.position(bb.position() - nonce.length);
                bb.put(nonce);

                // DON'T encrypt the nonce for AEAD mode.
                int len, pos = bb.position();
                if (SSLLogger.isOn && SSLLogger.isOn("plaintext")) {
                    SSLLogger.fine(
                            "Plaintext before ENCRYPTION",
                            bb.duplicate());
                }

                ByteBuffer dup = bb.duplicate();
                int outputSize = cipher.getOutputSize(dup.remaining());
                if (outputSize > bb.remaining()) {
                    // Need to expand the limit of the output buffer for
                    // the authentication tag.
                    //
                    // DON'T worry about the buffer's capacity, we have
                    // reserved space for the authentication tag.
                    bb.limit(pos + outputSize);
                }

                try {
                    len = cipher.doFinal(dup, bb);
                } catch (IllegalBlockSizeException |
                            BadPaddingException | ShortBufferException ibse) {
                    // unlikely to happen
                    throw new RuntimeException(
                            "Cipher error in AEAD mode in JCE provider " +
                            cipher.getProvider().getName(), ibse);
                }

                if (len != outputSize) {
                    throw new RuntimeException(
                            "Cipher buffering error in JCE provider " +
                            cipher.getProvider().getName());
                }

                return len + nonce.length;
            }

            @Override
            void dispose() {
                if (cipher != null) {
                    try {
                        cipher.doFinal();
                    } catch (Exception e) {
                        // swallow all types of exceptions.
                    }
                }
            }

            @Override
            int getExplicitNonceSize() {
                return recordIvSize;
            }

            @Override
            int calculateFragmentSize(int packetLimit, int headerSize) {
                return packetLimit - headerSize - recordIvSize - tagSize;
            }

            @Override
            int calculatePacketSize(int fragmentSize, int headerSize) {
                return fragmentSize + headerSize + recordIvSize + tagSize;
            }
        }
    }

    private static final
            class T13GcmReadCipherGenerator implements ReadCipherGenerator {

        @Override
        public SSLReadCipher createCipher(SSLCipher sslCipher,
                Authenticator authenticator, ProtocolVersion protocolVersion,
                String algorithm, Key key, AlgorithmParameterSpec params,
                SecureRandom random) throws GeneralSecurityException {
            return new GcmReadCipher(authenticator, protocolVersion, sslCipher,
                    algorithm, key, params, random);
        }

        static final class GcmReadCipher extends SSLReadCipher {
            private final Cipher cipher;
            private final int tagSize;
            private final Key key;
            private final byte[] iv;
            private final SecureRandom random;

            GcmReadCipher(Authenticator authenticator,
                    ProtocolVersion protocolVersion,
                    SSLCipher sslCipher, String algorithm,
                    Key key, AlgorithmParameterSpec params,
                    SecureRandom random) throws GeneralSecurityException {
                super(authenticator, protocolVersion);
                this.cipher = JsseJce.getCipher(algorithm);
                this.tagSize = sslCipher.tagSize;
                this.key = key;
                this.iv = ((IvParameterSpec)params).getIV();
                this.random = random;

                keyLimitCountdown = cipherLimits.getOrDefault(
                        algorithm.toUpperCase() + ":" + tag[0], 0L);
                if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
                    SSLLogger.fine("KeyLimit read side: algorithm = " +
                            algorithm.toUpperCase() + ":" + tag[0] +
                            "\ncountdown value = " + keyLimitCountdown);
                }
                if (keyLimitCountdown > 0) {
                    keyLimitEnabled = true;
                }
                // DON'T initialize the cipher for AEAD!
            }

            @Override
            public Plaintext decrypt(byte contentType, ByteBuffer bb,
                    byte[] sequence) throws GeneralSecurityException {
                // An implementation may receive an unencrypted record of type
                // change_cipher_spec consisting of the single byte value 0x01
                // at any time after the first ClientHello message has been
                // sent or received and before the peer's Finished message has
                // been received and MUST simply drop it without further
                // processing.
                if (contentType == ContentType.CHANGE_CIPHER_SPEC.id) {
                    return new Plaintext(contentType,
                        ProtocolVersion.NONE.major, ProtocolVersion.NONE.minor,
                        -1, -1L, bb.slice());
                }

                if (bb.remaining() <= tagSize) {
                    throw new BadPaddingException(
                        "Insufficient buffer remaining for AEAD cipher " +
                        "fragment (" + bb.remaining() + "). Needs to be " +
                        "more than tag size (" + tagSize + ")");
                }

                byte[] sn = sequence;
                if (sn == null) {
                    sn = authenticator.sequenceNumber();
                }
                byte[] nonce = iv.clone();
                int offset = nonce.length - sn.length;
                for (int i = 0; i < sn.length; i++) {
                    nonce[offset + i] ^= sn[i];
                }

                // initialize the AEAD cipher for the unique IV
                GCMParameterSpec spec =
                        new GCMParameterSpec(tagSize * 8, nonce);
                try {
                    cipher.init(Cipher.DECRYPT_MODE, key, spec, random);
                } catch (InvalidKeyException |
                            InvalidAlgorithmParameterException ikae) {
                    // unlikely to happen
                    throw new RuntimeException(
                                "invalid key or spec in GCM mode", ikae);
                }

                // Update the additional authentication data, using the
                // implicit sequence number of the authenticator.
                byte[] aad = authenticator.acquireAuthenticationBytes(
                                        contentType, bb.remaining(), sn);
                cipher.updateAAD(aad);

                int len, pos = bb.position();
                ByteBuffer dup = bb.duplicate();
                try {
                    len = cipher.doFinal(dup, bb);
                } catch (IllegalBlockSizeException ibse) {
                    // unlikely to happen
                    throw new RuntimeException(
                        "Cipher error in AEAD mode \"" + ibse.getMessage() +
                        " \"in JCE provider " + cipher.getProvider().getName());
                } catch (ShortBufferException sbe) {
                    // catch BouncyCastle buffering error
                    throw new RuntimeException("Cipher buffering error in " +
                        "JCE provider " + cipher.getProvider().getName(), sbe);
                }
                // reset the limit to the end of the decrypted data
                bb.position(pos);
                bb.limit(pos + len);

                // remove inner plaintext padding
                int i = bb.limit() - 1;
                for (; i > 0 && bb.get(i) == 0; i--) {
                    // blank
                }
                if (i < (pos + 1)) {
                    throw new BadPaddingException(
                            "Incorrect inner plaintext: no content type");
                }
                contentType = bb.get(i);
                bb.limit(i);

                if (SSLLogger.isOn && SSLLogger.isOn("plaintext")) {
                    SSLLogger.fine(
                            "Plaintext after DECRYPTION", bb.duplicate());
                }
                if (keyLimitEnabled) {
                    keyLimitCountdown -= len;
                }

                return new Plaintext(contentType,
                        ProtocolVersion.NONE.major, ProtocolVersion.NONE.minor,
                        -1, -1L, bb.slice());
            }

            @Override
            void dispose() {
                if (cipher != null) {
                    try {
                        cipher.doFinal();
                    } catch (Exception e) {
                        // swallow all types of exceptions.
                    }
                }
            }

            @Override
            int estimateFragmentSize(int packetSize, int headerSize) {
                return packetSize - headerSize - tagSize;
            }
        }
    }

    private static final
            class T13GcmWriteCipherGenerator implements WriteCipherGenerator {
        @Override
        public SSLWriteCipher createCipher(SSLCipher sslCipher,
                Authenticator authenticator, ProtocolVersion protocolVersion,
                String algorithm, Key key, AlgorithmParameterSpec params,
                SecureRandom random) throws GeneralSecurityException {
            return new GcmWriteCipher(authenticator, protocolVersion, sslCipher,
                    algorithm, key, params, random);
        }

        private static final class GcmWriteCipher extends SSLWriteCipher {
            private final Cipher cipher;
            private final int tagSize;
            private final Key key;
            private final byte[] iv;
            private final SecureRandom random;

            GcmWriteCipher(Authenticator authenticator,
                    ProtocolVersion protocolVersion,
                    SSLCipher sslCipher, String algorithm,
                    Key key, AlgorithmParameterSpec params,
                    SecureRandom random) throws GeneralSecurityException {
                super(authenticator, protocolVersion);
                this.cipher = JsseJce.getCipher(algorithm);
                this.tagSize = sslCipher.tagSize;
                this.key = key;
                this.iv = ((IvParameterSpec)params).getIV();
                this.random = random;

                keyLimitCountdown = cipherLimits.getOrDefault(
                        algorithm.toUpperCase() + ":" + tag[0], 0L);
                if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
                    SSLLogger.fine("KeyLimit write side: algorithm = "
                            + algorithm.toUpperCase() + ":" + tag[0] +
                            "\ncountdown value = " + keyLimitCountdown);
                }
                if (keyLimitCountdown > 0) {
                    keyLimitEnabled = true;
                }

                // DON'T initialize the cipher for AEAD!
            }

            @Override
            public int encrypt(byte contentType,
                    ByteBuffer bb) {
                byte[] sn = authenticator.sequenceNumber();
                byte[] nonce = iv.clone();
                int offset = nonce.length - sn.length;
                for (int i = 0; i < sn.length; i++) {
                    nonce[offset + i] ^= sn[i];
                }

                // initialize the AEAD cipher for the unique IV
                GCMParameterSpec spec =
                        new GCMParameterSpec(tagSize * 8, nonce);
                try {
                    cipher.init(Cipher.ENCRYPT_MODE, key, spec, random);
                } catch (InvalidKeyException |
                            InvalidAlgorithmParameterException ikae) {
                    // unlikely to happen
                    throw new RuntimeException(
                                "invalid key or spec in GCM mode", ikae);
                }

                // Update the additional authentication data, using the
                // implicit sequence number of the authenticator.
                int outputSize = cipher.getOutputSize(bb.remaining());
                byte[] aad = authenticator.acquireAuthenticationBytes(
                                        contentType, outputSize, sn);
                cipher.updateAAD(aad);

                int len, pos = bb.position();
                if (SSLLogger.isOn && SSLLogger.isOn("plaintext")) {
                    SSLLogger.fine(
                            "Plaintext before ENCRYPTION",
                            bb.duplicate());
                }

                ByteBuffer dup = bb.duplicate();
                if (outputSize > bb.remaining()) {
                    // Need to expand the limit of the output buffer for
                    // the authentication tag.
                    //
                    // DON'T worry about the buffer's capacity, we have
                    // reserved space for the authentication tag.
                    bb.limit(pos + outputSize);
                }

                try {
                    len = cipher.doFinal(dup, bb);
                } catch (IllegalBlockSizeException |
                            BadPaddingException | ShortBufferException ibse) {
                    // unlikely to happen
                    throw new RuntimeException(
                            "Cipher error in AEAD mode in JCE provider " +
                            cipher.getProvider().getName(), ibse);
                }

                if (len != outputSize) {
                    throw new RuntimeException(
                            "Cipher buffering error in JCE provider " +
                            cipher.getProvider().getName());
                }

                if (keyLimitEnabled) {
                    keyLimitCountdown -= len;
                }
                return len;
            }

            @Override
            void dispose() {
                if (cipher != null) {
                    try {
                        cipher.doFinal();
                    } catch (Exception e) {
                        // swallow all types of exceptions.
                    }
                }
            }

            @Override
            int getExplicitNonceSize() {
                return 0;
            }

            @Override
            int calculateFragmentSize(int packetLimit, int headerSize) {
                return packetLimit - headerSize - tagSize;
            }

            @Override
            int calculatePacketSize(int fragmentSize, int headerSize) {
                return fragmentSize + headerSize + tagSize;
            }
        }
    }

    private static final class T12CC20P1305ReadCipherGenerator
            implements ReadCipherGenerator {

        @Override
        public SSLReadCipher createCipher(SSLCipher sslCipher,
                Authenticator authenticator, ProtocolVersion protocolVersion,
                String algorithm, Key key, AlgorithmParameterSpec params,
                SecureRandom random) throws GeneralSecurityException {
            return new CC20P1305ReadCipher(authenticator, protocolVersion,
                    sslCipher, algorithm, key, params, random);
        }

        static final class CC20P1305ReadCipher extends SSLReadCipher {
            private final Cipher cipher;
            private final int tagSize;
            private final Key key;
            private final byte[] iv;
            private final SecureRandom random;

            CC20P1305ReadCipher(Authenticator authenticator,
                    ProtocolVersion protocolVersion,
                    SSLCipher sslCipher, String algorithm,
                    Key key, AlgorithmParameterSpec params,
                    SecureRandom random) throws GeneralSecurityException {
                super(authenticator, protocolVersion);
                this.cipher = JsseJce.getCipher(algorithm);
                this.tagSize = sslCipher.tagSize;
                this.key = key;
                this.iv = ((IvParameterSpec)params).getIV();
                this.random = random;

                // DON'T initialize the cipher for AEAD!
            }

            @Override
            public Plaintext decrypt(byte contentType, ByteBuffer bb,
                    byte[] sequence) throws GeneralSecurityException {
                if (bb.remaining() <= tagSize) {
                    throw new BadPaddingException(
                        "Insufficient buffer remaining for AEAD cipher " +
                        "fragment (" + bb.remaining() + "). Needs to be " +
                        "more than tag size (" + tagSize + ")");
                }

                byte[] sn = sequence;
                if (sn == null) {
                    sn = authenticator.sequenceNumber();
                }
                byte[] nonce = new byte[iv.length];
                System.arraycopy(sn, 0, nonce, nonce.length - sn.length,
                        sn.length);
                for (int i = 0; i < nonce.length; i++) {
                    nonce[i] ^= iv[i];
                }

                // initialize the AEAD cipher with the unique IV
                AlgorithmParameterSpec spec = new IvParameterSpec(nonce);
                try {
                    cipher.init(Cipher.DECRYPT_MODE, key, spec, random);
                } catch (InvalidKeyException |
                            InvalidAlgorithmParameterException ikae) {
                    // unlikely to happen
                    throw new RuntimeException(
                                "invalid key or spec in AEAD mode", ikae);
                }

                // update the additional authentication data
                byte[] aad = authenticator.acquireAuthenticationBytes(
                        contentType, bb.remaining() - tagSize, sequence);
                cipher.updateAAD(aad);

                // DON'T decrypt the nonce_explicit for AEAD mode. The buffer
                // position has moved out of the nonce_explicit range.
                int len = bb.remaining();
                int pos = bb.position();
                ByteBuffer dup = bb.duplicate();
                try {
                    len = cipher.doFinal(dup, bb);
                } catch (IllegalBlockSizeException ibse) {
                    // unlikely to happen
                    throw new RuntimeException(
                        "Cipher error in AEAD mode \"" + ibse.getMessage() +
                        " \"in JCE provider " + cipher.getProvider().getName());
                } catch (ShortBufferException sbe) {
                    // catch BouncyCastle buffering error
                    throw new RuntimeException("Cipher buffering error in " +
                        "JCE provider " + cipher.getProvider().getName(), sbe);
                }
                // reset the limit to the end of the decrypted data
                bb.position(pos);
                bb.limit(pos + len);

                if (SSLLogger.isOn && SSLLogger.isOn("plaintext")) {
                    SSLLogger.fine(
                            "Plaintext after DECRYPTION", bb.duplicate());
                }

                return new Plaintext(contentType,
                        ProtocolVersion.NONE.major, ProtocolVersion.NONE.minor,
                        -1, -1L, bb.slice());
            }

            @Override
            void dispose() {
                if (cipher != null) {
                    try {
                        cipher.doFinal();
                    } catch (Exception e) {
                        // swallow all types of exceptions.
                    }
                }
            }

            @Override
            int estimateFragmentSize(int packetSize, int headerSize) {
                return packetSize - headerSize - tagSize;
            }
        }
    }

    private static final class T12CC20P1305WriteCipherGenerator
            implements WriteCipherGenerator {
        @Override
        public SSLWriteCipher createCipher(SSLCipher sslCipher,
                Authenticator authenticator, ProtocolVersion protocolVersion,
                String algorithm, Key key, AlgorithmParameterSpec params,
                SecureRandom random) throws GeneralSecurityException {
            return new CC20P1305WriteCipher(authenticator, protocolVersion,
                    sslCipher, algorithm, key, params, random);
        }

        private static final class CC20P1305WriteCipher extends SSLWriteCipher {
            private final Cipher cipher;
            private final int tagSize;
            private final Key key;
            private final byte[] iv;
            private final SecureRandom random;

            CC20P1305WriteCipher(Authenticator authenticator,
                    ProtocolVersion protocolVersion,
                    SSLCipher sslCipher, String algorithm,
                    Key key, AlgorithmParameterSpec params,
                    SecureRandom random) throws GeneralSecurityException {
                super(authenticator, protocolVersion);
                this.cipher = JsseJce.getCipher(algorithm);
                this.tagSize = sslCipher.tagSize;
                this.key = key;
                this.iv = ((IvParameterSpec)params).getIV();
                this.random = random;

                keyLimitCountdown = cipherLimits.getOrDefault(
                        algorithm.toUpperCase() + ":" + tag[0], 0L);
                if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
                    SSLLogger.fine("algorithm = " + algorithm.toUpperCase() +
                            ":" + tag[0] + "\ncountdown value = " +
                            keyLimitCountdown);
                }
                if (keyLimitCountdown > 0) {
                    keyLimitEnabled = true;
                }

                // DON'T initialize the cipher for AEAD!
            }

            @Override
            public int encrypt(byte contentType,
                    ByteBuffer bb) {
                byte[] sn = authenticator.sequenceNumber();
                byte[] nonce = new byte[iv.length];
                System.arraycopy(sn, 0, nonce, nonce.length - sn.length,
                        sn.length);
                for (int i = 0; i < nonce.length; i++) {
                    nonce[i] ^= iv[i];
                }

                // initialize the AEAD cipher for the unique IV
                AlgorithmParameterSpec spec = new IvParameterSpec(nonce);
                try {
                    cipher.init(Cipher.ENCRYPT_MODE, key, spec, random);
                } catch (InvalidKeyException |
                            InvalidAlgorithmParameterException ikae) {
                    // unlikely to happen
                    throw new RuntimeException(
                                "invalid key or spec in AEAD mode", ikae);
                }

                // Update the additional authentication data, using the
                // implicit sequence number of the authenticator.
                byte[] aad = authenticator.acquireAuthenticationBytes(
                                        contentType, bb.remaining(), null);
                cipher.updateAAD(aad);

                // DON'T encrypt the nonce for AEAD mode.
                int len = bb.remaining();
                int pos = bb.position();
                if (SSLLogger.isOn && SSLLogger.isOn("plaintext")) {
                    SSLLogger.fine(
                            "Plaintext before ENCRYPTION",
                            bb.duplicate());
                }

                ByteBuffer dup = bb.duplicate();
                int outputSize = cipher.getOutputSize(dup.remaining());
                if (outputSize > bb.remaining()) {
                    // Need to expand the limit of the output buffer for
                    // the authentication tag.
                    //
                    // DON'T worry about the buffer's capacity, we have
                    // reserved space for the authentication tag.
                    bb.limit(pos + outputSize);
                }

                try {
                    len = cipher.doFinal(dup, bb);
                } catch (IllegalBlockSizeException |
                            BadPaddingException | ShortBufferException ibse) {
                    // unlikely to happen
                    throw new RuntimeException(
                            "Cipher error in AEAD mode in JCE provider " +
                            cipher.getProvider().getName(), ibse);
                }

                if (len != outputSize) {
                    throw new RuntimeException(
                            "Cipher buffering error in JCE provider " +
                            cipher.getProvider().getName());
                }

                return len;
            }

            @Override
            void dispose() {
                if (cipher != null) {
                    try {
                        cipher.doFinal();
                    } catch (Exception e) {
                        // swallow all types of exceptions.
                    }
                }
            }

            @Override
            int getExplicitNonceSize() {
                return 0;
            }

            @Override
            int calculateFragmentSize(int packetLimit, int headerSize) {
                return packetLimit - headerSize - tagSize;
            }

            @Override
            int calculatePacketSize(int fragmentSize, int headerSize) {
                return fragmentSize + headerSize + tagSize;
            }
        }
    }

    private static final class T13CC20P1305ReadCipherGenerator
            implements ReadCipherGenerator {

        @Override
        public SSLReadCipher createCipher(SSLCipher sslCipher,
                Authenticator authenticator, ProtocolVersion protocolVersion,
                String algorithm, Key key, AlgorithmParameterSpec params,
                SecureRandom random) throws GeneralSecurityException {
            return new CC20P1305ReadCipher(authenticator, protocolVersion,
                    sslCipher, algorithm, key, params, random);
        }

        static final class CC20P1305ReadCipher extends SSLReadCipher {
            private final Cipher cipher;
            private final int tagSize;
            private final Key key;
            private final byte[] iv;
            private final SecureRandom random;

            CC20P1305ReadCipher(Authenticator authenticator,
                    ProtocolVersion protocolVersion,
                    SSLCipher sslCipher, String algorithm,
                    Key key, AlgorithmParameterSpec params,
                    SecureRandom random) throws GeneralSecurityException {
                super(authenticator, protocolVersion);
                this.cipher = JsseJce.getCipher(algorithm);
                this.tagSize = sslCipher.tagSize;
                this.key = key;
                this.iv = ((IvParameterSpec)params).getIV();
                this.random = random;

                // DON'T initialize the cipher for AEAD!
            }

            @Override
            public Plaintext decrypt(byte contentType, ByteBuffer bb,
                    byte[] sequence) throws GeneralSecurityException {
                // An implementation may receive an unencrypted record of type
                // change_cipher_spec consisting of the single byte value 0x01
                // at any time after the first ClientHello message has been
                // sent or received and before the peer's Finished message has
                // been received and MUST simply drop it without further
                // processing.
                if (contentType == ContentType.CHANGE_CIPHER_SPEC.id) {
                    return new Plaintext(contentType,
                        ProtocolVersion.NONE.major, ProtocolVersion.NONE.minor,
                        -1, -1L, bb.slice());
                }

                if (bb.remaining() <= tagSize) {
                    throw new BadPaddingException(
                        "Insufficient buffer remaining for AEAD cipher " +
                        "fragment (" + bb.remaining() + "). Needs to be " +
                        "more than tag size (" + tagSize + ")");
                }

                byte[] sn = sequence;
                if (sn == null) {
                    sn = authenticator.sequenceNumber();
                }
                byte[] nonce = new byte[iv.length];
                System.arraycopy(sn, 0, nonce, nonce.length - sn.length,
                        sn.length);
                for (int i = 0; i < nonce.length; i++) {
                    nonce[i] ^= iv[i];
                }

                // initialize the AEAD cipher with the unique IV
                AlgorithmParameterSpec spec = new IvParameterSpec(nonce);
                try {
                    cipher.init(Cipher.DECRYPT_MODE, key, spec, random);
                } catch (InvalidKeyException |
                            InvalidAlgorithmParameterException ikae) {
                    // unlikely to happen
                    throw new RuntimeException(
                                "invalid key or spec in AEAD mode", ikae);
                }

                // Update the additional authentication data, using the
                // implicit sequence number of the authenticator.
                byte[] aad = authenticator.acquireAuthenticationBytes(
                                        contentType, bb.remaining(), sn);
                cipher.updateAAD(aad);

                int len = bb.remaining();
                int pos = bb.position();
                ByteBuffer dup = bb.duplicate();
                try {
                    len = cipher.doFinal(dup, bb);
                } catch (IllegalBlockSizeException ibse) {
                    // unlikely to happen
                    throw new RuntimeException(
                        "Cipher error in AEAD mode \"" + ibse.getMessage() +
                        " \"in JCE provider " + cipher.getProvider().getName());
                } catch (ShortBufferException sbe) {
                    // catch BouncyCastle buffering error
                    throw new RuntimeException("Cipher buffering error in " +
                        "JCE provider " + cipher.getProvider().getName(), sbe);
                }
                // reset the limit to the end of the decrypted data
                bb.position(pos);
                bb.limit(pos + len);

                // remove inner plaintext padding
                int i = bb.limit() - 1;
                for (; i > 0 && bb.get(i) == 0; i--) {
                    // blank
                }
                if (i < (pos + 1)) {
                    throw new BadPaddingException(
                            "Incorrect inner plaintext: no content type");
                }
                contentType = bb.get(i);
                bb.limit(i);

                if (SSLLogger.isOn && SSLLogger.isOn("plaintext")) {
                    SSLLogger.fine(
                            "Plaintext after DECRYPTION", bb.duplicate());
                }

                return new Plaintext(contentType,
                        ProtocolVersion.NONE.major, ProtocolVersion.NONE.minor,
                        -1, -1L, bb.slice());
            }

            @Override
            void dispose() {
                if (cipher != null) {
                    try {
                        cipher.doFinal();
                    } catch (Exception e) {
                        // swallow all types of exceptions.
                    }
                }
            }

            @Override
            int estimateFragmentSize(int packetSize, int headerSize) {
                return packetSize - headerSize - tagSize;
            }
        }
    }

    private static final class T13CC20P1305WriteCipherGenerator
            implements WriteCipherGenerator {
        @Override
        public SSLWriteCipher createCipher(SSLCipher sslCipher,
                Authenticator authenticator, ProtocolVersion protocolVersion,
                String algorithm, Key key, AlgorithmParameterSpec params,
                SecureRandom random) throws GeneralSecurityException {
            return new CC20P1305WriteCipher(authenticator, protocolVersion,
                    sslCipher, algorithm, key, params, random);
        }

        private static final class CC20P1305WriteCipher extends SSLWriteCipher {
            private final Cipher cipher;
            private final int tagSize;
            private final Key key;
            private final byte[] iv;
            private final SecureRandom random;

            CC20P1305WriteCipher(Authenticator authenticator,
                    ProtocolVersion protocolVersion,
                    SSLCipher sslCipher, String algorithm,
                    Key key, AlgorithmParameterSpec params,
                    SecureRandom random) throws GeneralSecurityException {
                super(authenticator, protocolVersion);
                this.cipher = JsseJce.getCipher(algorithm);
                this.tagSize = sslCipher.tagSize;
                this.key = key;
                this.iv = ((IvParameterSpec)params).getIV();
                this.random = random;

                keyLimitCountdown = cipherLimits.getOrDefault(
                        algorithm.toUpperCase() + ":" + tag[0], 0L);
                if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
                    SSLLogger.fine("algorithm = " + algorithm.toUpperCase() +
                            ":" + tag[0] + "\ncountdown value = " +
                            keyLimitCountdown);
                }
                if (keyLimitCountdown > 0) {
                    keyLimitEnabled = true;
                }

                // DON'T initialize the cipher for AEAD!
            }

            @Override
            public int encrypt(byte contentType,
                    ByteBuffer bb) {
                byte[] sn = authenticator.sequenceNumber();
                byte[] nonce = new byte[iv.length];
                System.arraycopy(sn, 0, nonce, nonce.length - sn.length,
                        sn.length);
                for (int i = 0; i < nonce.length; i++) {
                    nonce[i] ^= iv[i];
                }

                // initialize the AEAD cipher for the unique IV
                AlgorithmParameterSpec spec = new IvParameterSpec(nonce);
                try {
                    cipher.init(Cipher.ENCRYPT_MODE, key, spec, random);
                } catch (InvalidKeyException |
                            InvalidAlgorithmParameterException ikae) {
                    // unlikely to happen
                    throw new RuntimeException(
                                "invalid key or spec in AEAD mode", ikae);
                }

                // Update the additional authentication data, using the
                // implicit sequence number of the authenticator.
                int outputSize = cipher.getOutputSize(bb.remaining());
                byte[] aad = authenticator.acquireAuthenticationBytes(
                                        contentType, outputSize, sn);
                cipher.updateAAD(aad);

                int len = bb.remaining();
                int pos = bb.position();
                if (SSLLogger.isOn && SSLLogger.isOn("plaintext")) {
                    SSLLogger.fine(
                            "Plaintext before ENCRYPTION",
                            bb.duplicate());
                }

                ByteBuffer dup = bb.duplicate();
                if (outputSize > bb.remaining()) {
                    // Need to expand the limit of the output buffer for
                    // the authentication tag.
                    //
                    // DON'T worry about the buffer's capacity, we have
                    // reserved space for the authentication tag.
                    bb.limit(pos + outputSize);
                }

                try {
                    len = cipher.doFinal(dup, bb);
                } catch (IllegalBlockSizeException |
                            BadPaddingException | ShortBufferException ibse) {
                    // unlikely to happen
                    throw new RuntimeException(
                            "Cipher error in AEAD mode in JCE provider " +
                            cipher.getProvider().getName(), ibse);
                }

                if (len != outputSize) {
                    throw new RuntimeException(
                            "Cipher buffering error in JCE provider " +
                            cipher.getProvider().getName());
                }

                if (keyLimitEnabled) {
                    keyLimitCountdown -= len;
                }
                return len;
            }

            @Override
            void dispose() {
                if (cipher != null) {
                    try {
                        cipher.doFinal();
                    } catch (Exception e) {
                        // swallow all types of exceptions.
                    }
                }
            }

            @Override
            int getExplicitNonceSize() {
                return 0;
            }

            @Override
            int calculateFragmentSize(int packetLimit, int headerSize) {
                return packetLimit - headerSize - tagSize;
            }

            @Override
            int calculatePacketSize(int fragmentSize, int headerSize) {
                return fragmentSize + headerSize + tagSize;
            }
        }
    }

    private static void addMac(MAC signer,
            ByteBuffer destination, byte contentType) {
        if (signer.macAlg().size != 0) {
            int dstContent = destination.position();
            byte[] hash = signer.compute(contentType, destination, false);

            /*
             * position was advanced to limit in MAC compute above.
             *
             * Mark next area as writable (above layers should have
             * established that we have plenty of room), then write
             * out the hash.
             */
            destination.limit(destination.limit() + hash.length);
            destination.put(hash);

            // reset the position and limit
            destination.position(dstContent);
        }
    }

    // for null and stream cipher
    private static void checkStreamMac(MAC signer, ByteBuffer bb,
            byte contentType,  byte[] sequence) throws BadPaddingException {
        int tagLen = signer.macAlg().size;

        // Requires message authentication code for null, stream and
        // block cipher suites.
        if (tagLen != 0) {
            int contentLen = bb.remaining() - tagLen;
            if (contentLen < 0) {
                throw new BadPaddingException("bad record");
            }

            // Run MAC computation and comparison on the payload.
            //
            // MAC data would be stripped off during the check.
            if (checkMacTags(contentType, bb, signer, sequence, false)) {
                throw new BadPaddingException("bad record MAC");
            }
        }
    }

    // for CBC cipher
    private static void checkCBCMac(MAC signer, ByteBuffer bb,
            byte contentType, int cipheredLength,
            byte[] sequence) throws BadPaddingException {
        BadPaddingException reservedBPE = null;
        int tagLen = signer.macAlg().size;
        int pos = bb.position();

        if (tagLen != 0) {
            int contentLen = bb.remaining() - tagLen;
            if (contentLen < 0) {
                reservedBPE = new BadPaddingException("bad record");

                // set offset of the dummy MAC
                contentLen = cipheredLength - tagLen;
                bb.limit(pos + cipheredLength);
            }

            // Run MAC computation and comparison on the payload.
            //
            // MAC data would be stripped off during the check.
            if (checkMacTags(contentType, bb, signer, sequence, false)) {
                if (reservedBPE == null) {
                    reservedBPE =
                            new BadPaddingException("bad record MAC");
                }
            }

            // Run MAC computation and comparison on the remainder.
            int remainingLen = calculateRemainingLen(
                    signer, cipheredLength, contentLen);

            // NOTE: remainingLen may be bigger (less than 1 block of the
            // hash algorithm of the MAC) than the cipheredLength.
            //
            // Is it possible to use a static buffer, rather than allocate
            // it dynamically?
            remainingLen += signer.macAlg().size;
            ByteBuffer temporary = ByteBuffer.allocate(remainingLen);

            // Won't need to worry about the result on the remainder. And
            // then we won't need to worry about what's actual data to
            // check MAC tag on.  We start the check from the header of the
            // buffer so that we don't need to construct a new byte buffer.
            checkMacTags(contentType, temporary, signer, sequence, true);
        }

        // Is it a failover?
        if (reservedBPE != null) {
            throw reservedBPE;
        }
    }

    /*
     * Run MAC computation and comparison
     */
    private static boolean checkMacTags(byte contentType, ByteBuffer bb,
            MAC signer, byte[] sequence, boolean isSimulated) {
        int tagLen = signer.macAlg().size;
        int position = bb.position();
        int lim = bb.limit();
        int macOffset = lim - tagLen;

        bb.limit(macOffset);
        byte[] hash = signer.compute(contentType, bb, sequence, isSimulated);
        if (hash == null || tagLen != hash.length) {
            // Something is wrong with MAC implementation.
            throw new RuntimeException("Internal MAC error");
        }

        bb.position(macOffset);
        bb.limit(lim);
        try {
            int[] results = compareMacTags(bb, hash);
            return (results[0] != 0);
        } finally {
            // reset to the data
            bb.position(position);
            bb.limit(macOffset);
        }
    }

    /*
     * A constant-time comparison of the MAC tags.
     *
     * Please DON'T change the content of the ByteBuffer parameter!
     */
    private static int[] compareMacTags(ByteBuffer bb, byte[] tag) {
        // An array of hits is used to prevent Hotspot optimization for
        // the purpose of a constant-time check.
        int[] results = {0, 0};     // {missed #, matched #}

        // The caller ensures there are enough bytes available in the buffer.
        // So we won't need to check the remaining of the buffer.
        for (byte t : tag) {
            if (bb.get() != t) {
                results[0]++;       // mismatched bytes
            } else {
                results[1]++;       // matched bytes
            }
        }

        return results;
    }

    /*
     * Calculate the length of a dummy buffer to run MAC computation
     * and comparison on the remainder.
     *
     * The caller MUST ensure that the fullLen is not less than usedLen.
     */
    private static int calculateRemainingLen(
            MAC signer, int fullLen, int usedLen) {

        int blockLen = signer.macAlg().hashBlockSize;
        int minimalPaddingLen = signer.macAlg().minimalPaddingSize;

        // (blockLen - minimalPaddingLen) is the maximum message size of
        // the last block of hash function operation. See FIPS 180-4, or
        // MD5 specification.
        fullLen += 13 - (blockLen - minimalPaddingLen);
        usedLen += 13 - (blockLen - minimalPaddingLen);

        // Note: fullLen is always not less than usedLen, and blockLen
        // is always bigger than minimalPaddingLen, so we don't worry
        // about negative values. 0x01 is added to the result to ensure
        // that the return value is positive.  The extra one byte does
        // not impact the overall MAC compression function evaluations.
        return 0x01 + (int)(Math.ceil(fullLen/(1.0d * blockLen)) -
                Math.ceil(usedLen/(1.0d * blockLen))) * blockLen;
    }

    private static int addPadding(ByteBuffer bb, int blockSize) {

        int     len = bb.remaining();
        int     offset = bb.position();

        int     newlen = len + 1;
        byte    pad;
        int     i;

        if ((newlen % blockSize) != 0) {
            newlen += blockSize - 1;
            newlen -= newlen % blockSize;
        }
        pad = (byte) (newlen - len);

        /*
         * Update the limit to what will be padded.
         */
        bb.limit(newlen + offset);

        /*
         * TLS version of the padding works for both SSLv3 and TLSv1
         */
        for (i = 0, offset += len; i < pad; i++) {
            bb.put(offset++, (byte) (pad - 1));
        }

        bb.position(offset);
        bb.limit(offset);

        return newlen;
    }

    @SuppressWarnings("cast")
    private static int removePadding(ByteBuffer bb,
            int tagLen, int blockSize,
            ProtocolVersion protocolVersion) throws BadPaddingException {
        int len = bb.remaining();
        int offset = bb.position();

        // last byte is length byte (i.e. actual padding length - 1)
        int padOffset = offset + len - 1;
        int padLen = bb.get(padOffset) & 0xFF;

        int newLen = len - (padLen + 1);
        if ((newLen - tagLen) < 0) {
            // If the buffer is not long enough to contain the padding plus
            // a MAC tag, do a dummy constant-time padding check.
            //
            // Note that it is a dummy check, so we won't care about what is
            // the actual padding data.
            checkPadding(bb.duplicate(), (byte)(padLen & 0xFF));

            throw new BadPaddingException("Invalid Padding length: " + padLen);
        }

        // The padding data should be filled with the padding length value.
        int[] results = checkPadding(
                (ByteBuffer)(bb.duplicate()).position(offset + newLen),
                (byte)(padLen & 0xFF));
        if (protocolVersion.useTLS10PlusSpec()) {
            if (results[0] != 0) {          // padding data has invalid bytes
                throw new BadPaddingException("Invalid TLS padding data");
            }
        } else { // SSLv3
            // SSLv3 requires 0 <= length byte < block size
            // some implementations do 1 <= length byte <= block size,
            // so accept that as well
            // v3 does not require any particular value for the other bytes
            if (padLen > blockSize) {
                throw new BadPaddingException("Padding length (" +
                padLen + ") of SSLv3 message should not be bigger " +
                "than the block size (" + blockSize + ")");
            }
        }

        // Reset buffer limit to remove padding.
        bb.limit(offset + newLen);

        return newLen;
    }

    /*
     * A constant-time check of the padding.
     *
     * NOTE that we are checking both the padding and the padLen bytes here.
     *
     * The caller MUST ensure that the bb parameter has remaining.
     */
    private static int[] checkPadding(ByteBuffer bb, byte pad) {
        if (!bb.hasRemaining()) {
            throw new RuntimeException("hasRemaining() must be positive");
        }

        // An array of hits is used to prevent Hotspot optimization for
        // the purpose of a constant-time check.
        int[] results = {0, 0};    // {missed #, matched #}
        bb.mark();
        for (int i = 0; i <= 256; bb.reset()) {
            for (; bb.hasRemaining() && i <= 256; i++) {
                if (bb.get() != pad) {
                    results[0]++;       // mismatched padding data
                } else {
                    results[1]++;       // matched padding data
                }
            }
        }

        return results;
    }
}