All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.bouncycastle.openpgp.operator.jcajce.JceAEADUtil Maven / Gradle / Ivy

Go to download

The Bouncy Castle Java APIs for the OpenPGP Protocol. The APIs are designed primarily to be used in conjunction with the BC FIPS provider. The APIs may also be used with other providers although if being used in a FIPS context it is the responsibility of the user to ensure that any other providers used are FIPS certified and used appropriately.

There is a newer version: 2.0.9
Show newest version
package org.bouncycastle.openpgp.operator.jcajce;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.GeneralSecurityException;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

import org.bouncycastle.bcpg.AEADAlgorithmTags;
import org.bouncycastle.bcpg.AEADEncDataPacket;
import org.bouncycastle.bcpg.AEADUtils;
import org.bouncycastle.bcpg.SymmetricEncIntegrityPacket;
import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags;
import org.bouncycastle.bcpg.SymmetricKeyUtils;
import org.bouncycastle.crypto.KDFCalculator;
import org.bouncycastle.crypto.fips.FipsKDF;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPSessionKey;
import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.openpgp.operator.PGPDataDecryptor;
import org.bouncycastle.openpgp.operator.PGPDigestCalculator;
import org.bouncycastle.util.Arrays;
import org.bouncycastle.util.Pack;
import org.bouncycastle.util.io.Streams;

class JceAEADUtil
{
    private final OperatorHelper helper;

    public JceAEADUtil(OperatorHelper helper)
    {
        this.helper = helper;
    }

    /**
     * Generate a nonce by xor-ing the given iv with the chunk index.
     *
     * @param iv         initialization vector
     * @param chunkIndex chunk index
     * @return nonce
     */
    protected static byte[] getNonce(byte[] iv, long chunkIndex)
    {
        byte[] nonce = Arrays.clone(iv);

        xorChunkId(nonce, chunkIndex);

        return nonce;
    }

    /**
     * XOR the byte array with the chunk index in-place.
     *
     * @param nonce      byte array
     * @param chunkIndex chunk index
     */
    protected static void xorChunkId(byte[] nonce, long chunkIndex)
    {
        int index = nonce.length - 8;

        nonce[index++] ^= (byte)(chunkIndex >> 56);
        nonce[index++] ^= (byte)(chunkIndex >> 48);
        nonce[index++] ^= (byte)(chunkIndex >> 40);
        nonce[index++] ^= (byte)(chunkIndex >> 32);
        nonce[index++] ^= (byte)(chunkIndex >> 24);
        nonce[index++] ^= (byte)(chunkIndex >> 16);
        nonce[index++] ^= (byte)(chunkIndex >> 8);
        nonce[index] ^= (byte)(chunkIndex);
    }

    /**
     * Calculate an actual chunk length from the encoded chunk size.
     *
     * @param chunkSize encoded chunk size
     * @return decoded length
     */
    protected static long getChunkLength(int chunkSize)
    {
        return 1L << (chunkSize + 6);
    }

    /**
     * Derive a message key and IV from the given session key.
     * The result is two byte arrays containing the key bytes and the IV.
     *
     * @param aeadAlgo   AEAD algorithm
     * @param cipherAlgo symmetric cipher algorithm
     * @param sessionKey session key
     * @param salt       salt
     * @param hkdfInfo   HKDF info
     * @return message key and separate IV
     * @throws PGPException
     */
    static byte[][] deriveMessageKeyAndIv(int aeadAlgo, int cipherAlgo, byte[] sessionKey, byte[] salt, byte[] hkdfInfo)
        throws PGPException
    {
        // TODO: needs to be JCA based. KeyGenerator
        int keyLen = SymmetricKeyUtils.getKeyLengthInOctets(cipherAlgo);
        int ivLen = AEADUtils.getIVLength(aeadAlgo);
        byte[] messageKeyAndIv = generateHKDFBytes(sessionKey, salt, hkdfInfo, keyLen + ivLen - 8);

        return new byte[][]{Arrays.copyOfRange(messageKeyAndIv, 0, keyLen), Arrays.copyOfRange(messageKeyAndIv, keyLen, keyLen + ivLen)};
    }

    static byte[] generateHKDFBytes(byte[] sessionKey, byte[] salt, byte[] hkdfInfo, int len)
    {
        KDFCalculator hkdfGen = new FipsKDF.AgreementOperatorFactory().createKDFCalculator(
                  FipsKDF.HKDF.withPRF(FipsKDF.AgreementKDFPRF.SHA256).using(sessionKey)
                      .withSalt(salt)
                      .withIV(hkdfInfo));

        byte[] messageKeyAndIv = new byte[len];
        hkdfGen.generateBytes(messageKeyAndIv, 0, messageKeyAndIv.length);
        return messageKeyAndIv;
    }

    /**
     * Create a {@link PGPDataDecryptor} for decrypting AEAD encrypted OpenPGP v5 data packets.
     *
     * @param aeadEncDataPacket AEAD encrypted data packet
     * @param sessionKey        session key to decrypt the data
     * @return decryptor
     * @throws PGPException
     */
    PGPDataDecryptor createOpenPgpV5DataDecryptor(AEADEncDataPacket aeadEncDataPacket,
                                                  PGPSessionKey sessionKey)
        throws PGPException
    {
        final int aeadAlgorithm = aeadEncDataPacket.getAEADAlgorithm();
        final byte[] iv = aeadEncDataPacket.getIV();
        final int chunkSize = aeadEncDataPacket.getChunkSize();
        final int encAlgorithm = sessionKey.getAlgorithm();
        final byte[] key = sessionKey.getKey();
        final byte[] aaData = aeadEncDataPacket.getAAData();
        try
        {
            final SecretKey secretKey = new SecretKeySpec(key, PGPUtil.getSymmetricCipherName(encAlgorithm));

            final Cipher c = createAEADCipher(encAlgorithm, aeadAlgorithm);

            return new PGPDataDecryptor()
            {
                public InputStream getInputStream(InputStream in)
                {
                    try
                    {
                        return new JceAEADUtil.PGPAeadInputStream(true, in, c, secretKey, iv, encAlgorithm, aeadAlgorithm, chunkSize, aaData);
                    }
                    catch (IOException e)
                    {
                        throw Exceptions.illegalStateException("unable to open stream: " + e.getMessage(), e);
                    }
                }

                public int getBlockSize()
                {
                    return c.getBlockSize();
                }

                public PGPDigestCalculator getIntegrityCalculator()
                {
                    return new SHA1PGPDigestCalculator();
                }
            };
        }
        catch (PGPException e)
        {
            throw e;
        }
        catch (Exception e)
        {
            throw new PGPException("Exception creating cipher", e);
        }
    }

    /**
     * Create a {@link PGPDataDecryptor} for decrypting AEAD encrypted OpenPGP v6 data.
     *
     * @param seipd      version 2 SEIPD packet
     * @param sessionKey session key to decrypt the data
     * @return decryptor
     * @throws PGPException
     */
    PGPDataDecryptor createOpenPgpV6DataDecryptor(SymmetricEncIntegrityPacket seipd,
                                                  PGPSessionKey sessionKey)
        throws PGPException
    {
        final int cipherAlgo = seipd.getCipherAlgorithm();
        final int aeadAlgo = seipd.getAeadAlgorithm();
        final int chunkSize = seipd.getChunkSize();
        final byte[] salt = seipd.getSalt();
        final byte[] aaData = seipd.getAAData();


        final byte[][] messageKeyAndIv = deriveMessageKeyAndIv(aeadAlgo, cipherAlgo, sessionKey.getKey(), salt, aaData);
        final byte[] messageKey = messageKeyAndIv[0];
        final byte[] iv = messageKeyAndIv[1];

        try
        {
            final SecretKey secretKey = new SecretKeySpec(messageKey, PGPUtil.getSymmetricCipherName(cipherAlgo));
            final Cipher c = createAEADCipher(cipherAlgo, aeadAlgo);

            return new PGPDataDecryptor()
            {
                public InputStream getInputStream(InputStream in)
                {
                    try
                    {
                        return new JceAEADUtil.PGPAeadInputStream(false, in, c, secretKey, iv, cipherAlgo, aeadAlgo, chunkSize, aaData);
                    }
                    catch (IOException e)
                    {
                        throw Exceptions.illegalStateException("unable to open stream: " + e.getMessage(), e);
                    }
                }

                public int getBlockSize()
                {
                    return c.getBlockSize();
                }

                public PGPDigestCalculator getIntegrityCalculator()
                {
                    return new SHA1PGPDigestCalculator();
                }
            };
        }
        catch (PGPException e)
        {
            throw e;
        }
        catch (Exception e)
        {
            throw new PGPException("Exception creating cipher", e);
        }
    }

    Cipher createAEADCipher(int encAlgorithm, int aeadAlgorithm)
        throws PGPException
    {
        if (encAlgorithm != SymmetricKeyAlgorithmTags.AES_128
            && encAlgorithm != SymmetricKeyAlgorithmTags.AES_192
            && encAlgorithm != SymmetricKeyAlgorithmTags.AES_256
            && encAlgorithm != SymmetricKeyAlgorithmTags.CAMELLIA_128
            && encAlgorithm != SymmetricKeyAlgorithmTags.CAMELLIA_192
            && encAlgorithm != SymmetricKeyAlgorithmTags.CAMELLIA_256)
        {
            // Block Cipher must work on 16 byte blocks
            throw new PGPException("AEAD only supported for AES and Camellia" + " based algorithms");
        }

        String mode;
        switch (aeadAlgorithm)
        {
        case AEADAlgorithmTags.EAX:
            mode = "EAX";
            break;
        case AEADAlgorithmTags.OCB:
            mode = "OCB";
            break;
        case AEADAlgorithmTags.GCM:
            mode = "GCM";
            break;
        default:
            throw new PGPException("encountered unknown AEAD algorithm: " + aeadAlgorithm);
        }

        String cName = PGPUtil.getSymmetricCipherName(encAlgorithm)
            + "/" + mode + "/NoPadding";

        return helper.createCipher(cName);
    }

    static class PGPAeadInputStream
        extends InputStream
    {
        private final InputStream in;
        private final byte[] buf;
        private final Cipher c;
        private final SecretKey secretKey;
        private final byte[] aaData;
        private final byte[] iv;
        private final int chunkLength;
        private final int aeadTagLength;

        private byte[] data;
        private int dataOff;
        private long chunkIndex = 0;
        private long totalBytes = 0;
        private boolean v5StyleAEAD;

        /**
         * InputStream for decrypting AEAD encrypted data.
         *
         * @param isV5AEAD      AEAD flavour (OpenPGP v5 or v6)
         * @param in            underlying input stream
         * @param c             AEAD cipher
         * @param secretKey     secret key
         * @param iv            initialization vector
         * @param encAlgorithm  encryption algorithm
         * @param aeadAlgorithm AEAD algorithm
         * @param chunkSize     chunk size of the AEAD encryption
         * @param aaData        associated data
         * @throws IOException
         */
        public PGPAeadInputStream(boolean isV5AEAD, InputStream in,
                                  Cipher c,
                                  SecretKey secretKey,
                                  byte[] iv,
                                  int encAlgorithm,
                                  int aeadAlgorithm,
                                  int chunkSize,
                                  byte[] aaData)
            throws IOException
        {
            this.v5StyleAEAD = isV5AEAD;
            this.in = in;
            this.iv = iv;
            this.chunkLength = (int)getChunkLength(chunkSize);
            this.aeadTagLength = AEADUtils.getAuthTagLength(aeadAlgorithm);
            this.buf = new byte[chunkLength + aeadTagLength + aeadTagLength]; // allow room for chunk tag and message tag
            this.c = c;
            this.secretKey = secretKey;
            this.aaData = aaData;

            // prime with 2 * tag len bytes.
            Streams.readFully(in, buf, 0, aeadTagLength + aeadTagLength);

            // load the first block
            this.data = readBlock();
            this.dataOff = 0;
        }

        public int read()
            throws IOException
        {
            if (data != null && dataOff == data.length)
            {
                this.data = readBlock();
                this.dataOff = 0;
            }

            if (this.data == null)
            {
                return -1;
            }

            return data[dataOff++] & 0xff;
        }

        public int read(byte[] b, int off, int len)
            throws IOException
        {
            if (data != null && dataOff == data.length)
            {
                this.data = readBlock();
                this.dataOff = 0;
            }

            if (this.data == null)
            {
                return -1;
            }

            int supplyLen = Math.min(len, available());
            System.arraycopy(data, dataOff, b, off, supplyLen);
            dataOff += supplyLen;

            return supplyLen;
        }

        public long skip(long n)
            throws IOException
        {
            if (n <= 0)
            {
                return 0;
            }

            int skip = (int)Math.min(n, available());
            dataOff += skip;
            return skip;
        }

        public int available()
            throws IOException
        {
            if (data != null && dataOff == data.length)
            {
                this.data = readBlock();
                this.dataOff = 0;
            }

            if (this.data == null)
            {
                return -1;
            }

            return data.length - dataOff;
        }

        private byte[] readBlock()
            throws IOException
        {
            // we initialise with the first 16 bytes as there is an additional 16 bytes following
            // the last chunk (which may not be the exact chunklength).
            int dataLen = Streams.readFully(in, buf, aeadTagLength + aeadTagLength, chunkLength);
            if (dataLen == 0)
            {
                return null;
            }

            byte[] adata = new byte[v5StyleAEAD ? 13 : aaData.length];
            System.arraycopy(aaData, 0, adata, 0, aaData.length);
            if (v5StyleAEAD)
            {
                xorChunkId(adata, chunkIndex);
            }

            byte[] decData;
            try
            {
                JceAEADCipherUtil.setUpAeadCipher(c, secretKey, Cipher.DECRYPT_MODE, getNonce(iv, chunkIndex), 128, adata);

                decData = c.doFinal(buf, 0, dataLen + aeadTagLength);
            }
            catch (GeneralSecurityException e)
            {
                throw new IOException("exception processing chunk " + chunkIndex + ": " + e.getMessage());
            }

            totalBytes += decData.length;
            chunkIndex++;

            System.arraycopy(buf, dataLen + aeadTagLength, buf, 0, aeadTagLength); // copy back the "tag"

            if (dataLen != chunkLength)     // it's our last block
            {
                adata = PGPAeadOutputStream.getAdata(v5StyleAEAD, aaData, chunkIndex, totalBytes);
                try
                {
                    if (v5StyleAEAD)
                    {
                        JceAEADCipherUtil.setUpAeadCipher(c, secretKey, Cipher.DECRYPT_MODE, getNonce(iv, chunkIndex), 128, Arrays.concatenate(adata, Pack.longToBigEndian(totalBytes)));
                    }
                    else
                    {
                        JceAEADCipherUtil.setUpAeadCipher(c, secretKey, Cipher.DECRYPT_MODE, getNonce(iv, chunkIndex), 128, adata);
                    }

                    c.doFinal(buf, 0, aeadTagLength); // check final tag
                }
                catch (GeneralSecurityException e)
                {
                    throw new IOException("exception processing final tag: " + e.getMessage());
                }
            }
            else
            {
                Streams.readFully(in, buf, aeadTagLength, aeadTagLength);   // read the next tag bytes
            }

            return decData;
        }
    }

    static class PGPAeadOutputStream
        extends OutputStream
    {
        private final boolean isV5AEAD;
        private final OutputStream out;
        private final byte[] data;
        private final Cipher c;
        private final SecretKey secretKey;
        private final byte[] aaData;
        private final byte[] iv;
        private final int chunkLength;

        private int dataOff;
        private long chunkIndex = 0;
        private long totalBytes = 0;

        /**
         * OutputStream for AEAD encryption.
         *
         * @param isV5AEAD      isV5AEAD of AEAD (OpenPGP v5 or v6)
         * @param out           underlying OutputStream
         * @param c             AEAD cipher
         * @param secretKey     secret key
         * @param iv            initialization vector
         * @param encAlgorithm  encryption algorithm
         * @param aeadAlgorithm AEAD algorithm
         * @param chunkSize     chunk size of the AEAD algorithm
         */
        public PGPAeadOutputStream(boolean isV5AEAD,
                                   OutputStream out,
                                   Cipher c,
                                   SecretKey secretKey,
                                   byte[] iv,
                                   int encAlgorithm,
                                   int aeadAlgorithm,
                                   int chunkSize)
        {
            this.isV5AEAD = isV5AEAD;
            this.out = out;
            this.iv = iv;
            this.chunkLength = (int)getChunkLength(chunkSize);
            this.data = new byte[chunkLength];
            this.c = c;
            this.secretKey = secretKey;

            this.aaData = createAAD(isV5AEAD, encAlgorithm, aeadAlgorithm, chunkSize);
        }

        /**
         * Create the associated data for the AEAD encryption.
         * Since the associated data array differs between OpenPGP v5 and v6, we need to know the flavour.
         *
         * @param isV5AEAD      true if flavour of AEAD OpenPGP v5
         * @param encAlgorithm  symmetric encryption algorithm
         * @param aeadAlgorithm AEAD algorithm
         * @param chunkSize     chunk size
         * @return associated data
         */
        private byte[] createAAD(boolean isV5AEAD, int encAlgorithm, int aeadAlgorithm, int chunkSize)
        {
            if (isV5AEAD)
            {
                return AEADEncDataPacket.createAAData(AEADEncDataPacket.VERSION_1,
                    encAlgorithm, aeadAlgorithm, chunkSize);
            }
            else
            {
                return SymmetricEncIntegrityPacket.createAAData(SymmetricEncIntegrityPacket.VERSION_2,
                    encAlgorithm, aeadAlgorithm, chunkSize);
            }
        }

        public void write(int b)
            throws IOException
        {
            if (dataOff == data.length)
            {
                writeBlock();
            }
            data[dataOff++] = (byte)b;
        }

        public void write(byte[] b, int off, int len)
            throws IOException
        {
            if (dataOff == data.length)
            {
                writeBlock();
            }

            if (len < data.length - dataOff)
            {
                System.arraycopy(b, off, data, dataOff, len);
                dataOff += len;
            }
            else
            {
                int gap = data.length - dataOff;
                System.arraycopy(b, off, data, dataOff, gap);
                dataOff += gap;
                writeBlock();

                len -= gap;
                off += gap;

                while (len >= data.length)
                {
                    System.arraycopy(b, off, data, 0, data.length);
                    dataOff = data.length;
                    writeBlock();
                    len -= data.length;
                    off += data.length;
                }

                if (len > 0)
                {
                    System.arraycopy(b, off, data, 0, len);
                    dataOff = len;
                }
            }
        }

        public void close()
            throws IOException
        {
            finish();
        }

        private void writeBlock()
            throws IOException
        {
            byte[] adata = isV5AEAD ? new byte[13] : new byte[aaData.length];
            System.arraycopy(aaData, 0, adata, 0, aaData.length);
            if (isV5AEAD)
            {
                xorChunkId(adata, chunkIndex);
            }

            try
            {
                JceAEADCipherUtil.setUpAeadCipher(c, secretKey, Cipher.ENCRYPT_MODE, getNonce(iv, chunkIndex), 128, adata);

                out.write(c.doFinal(data, 0, dataOff));
            }
            catch (GeneralSecurityException e)
            {
                throw new IOException("exception processing chunk " + chunkIndex + ": " + e.getMessage());
            }

            totalBytes += dataOff;
            chunkIndex++;
            dataOff = 0;
        }

        private void finish()
            throws IOException
        {
            if (dataOff > 0)
            {
                writeBlock();
            }

            byte[] adata = getAdata(isV5AEAD, aaData, chunkIndex, totalBytes);

            try
            {
                if (isV5AEAD)
                {
                    JceAEADCipherUtil.setUpAeadCipher(c, secretKey, Cipher.ENCRYPT_MODE, getNonce(iv, chunkIndex), 128, Arrays.concatenate(adata, Pack.longToBigEndian(totalBytes)));
                }
                else
                {
                    JceAEADCipherUtil.setUpAeadCipher(c, secretKey, Cipher.ENCRYPT_MODE, getNonce(iv, chunkIndex), 128, adata);
                }

                out.write(c.doFinal(aaData, 0, 0)); // output final tag
            }
            catch (GeneralSecurityException e)
            {
                throw new IOException("exception processing final tag: " + e.getMessage());
            }
            out.close();
        }

        private static byte[] getAdata(boolean isV5AEAD, byte[] aaData, long chunkIndex, long totalBytes)
        {
            byte[] adata;
            if (isV5AEAD)
            {
                adata = new byte[13];
                System.arraycopy(aaData, 0, adata, 0, aaData.length);
                xorChunkId(adata, chunkIndex);
            }
            else
            {
                adata = new byte[aaData.length + 8];
                System.arraycopy(aaData, 0, adata, 0, aaData.length);
                System.arraycopy(Pack.longToBigEndian(totalBytes), 0, adata, aaData.length, 8);
            }
            return adata;
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy