From 1f6f5ba4e3d57690214b45fa607e9d54e6e4cc4e Mon Sep 17 00:00:00 2001
From: zhanghua1831
Date: Fri, 26 Feb 2021 12:54:01 +0800
Subject: [PATCH] fix CVE-2020-7226 and fix build error by using Java8
---
backport-CVE-2020-7226-1.patch | 1261 ++++++++++++++++++++++++++++++++
backport-CVE-2020-7226-2.patch | 81 ++
backport-CVE-2020-7226-3.patch | 22 +
change-version-to-Java8.patch | 37 +
cryptacular.spec | 11 +-
5 files changed, 1410 insertions(+), 2 deletions(-)
create mode 100644 backport-CVE-2020-7226-1.patch
create mode 100644 backport-CVE-2020-7226-2.patch
create mode 100644 backport-CVE-2020-7226-3.patch
create mode 100644 change-version-to-Java8.patch
diff --git a/backport-CVE-2020-7226-1.patch b/backport-CVE-2020-7226-1.patch
new file mode 100644
index 0000000..679fffd
--- /dev/null
+++ b/backport-CVE-2020-7226-1.patch
@@ -0,0 +1,1261 @@
+From 8c6c7528f1e24c6b71f3e36db0cb8a697256ce25 Mon Sep 17 00:00:00 2001
+From: "Marvin S. Addison"
+Date: Tue, 21 Jan 2020 16:59:39 -0500
+Subject: [PATCH] Define new ciphertext header format.
+
+New format does not allocate any memory until HMAC check passes, which
+guards against untrusted input. All encryption components have been
+updated to use the new header, while preserving backward compatibility
+to decrypt messages encrypted with the old format. The decoding process
+for the old header has been hardened to impose reasonable limits on header
+fields: nonce sizes up to 255 bytes, key names up to 500 bytes.
+
+Fixes #52.
+---
+ .../org/cryptacular/CiphertextHeader.java | 65 +++-
+ .../org/cryptacular/CiphertextHeaderV2.java | 307 ++++++++++++++++++
+ .../bean/AbstractBlockCipherBean.java | 10 +-
+ .../cryptacular/bean/AbstractCipherBean.java | 31 +-
+ .../java/org/cryptacular/util/ByteUtil.java | 45 ++-
+ .../java/org/cryptacular/util/CipherUtil.java | 90 +++--
+ .../org/cryptacular/CiphertextHeaderTest.java | 55 ++++
+ .../cryptacular/CiphertextHeaderV2Test.java | 67 ++++
+ .../bean/AEADBlockCipherBeanTest.java | 65 ++--
+ .../org/cryptacular/util/CipherUtilTest.java | 47 +++
+ 10 files changed, 707 insertions(+), 75 deletions(-)
+ create mode 100644 src/main/java/org/cryptacular/CiphertextHeaderV2.java
+ create mode 100644 src/test/java/org/cryptacular/CiphertextHeaderTest.java
+ create mode 100644 src/test/java/org/cryptacular/CiphertextHeaderV2Test.java
+
+diff --git a/src/main/java/org/cryptacular/CiphertextHeader.java b/src/main/java/org/cryptacular/CiphertextHeader.java
+index 93623c9..c17e735 100644
+--- a/src/main/java/org/cryptacular/CiphertextHeader.java
++++ b/src/main/java/org/cryptacular/CiphertextHeader.java
+@@ -34,18 +34,26 @@
+ * decrypt outstanding data which will be subsequently re-encrypted with a new key.
+ *
+ * @author Middleware Services
++ *
++ * @deprecated Superseded by {@link CiphertextHeaderV2}
+ */
++@Deprecated
+ public class CiphertextHeader
+ {
++ /** Maximum nonce length in bytes. */
++ protected static final int MAX_NONCE_LEN = 255;
++
++ /** Maximum key name length in bytes. */
++ protected static final int MAX_KEYNAME_LEN = 500;
+
+ /** Header nonce field value. */
+- private final byte[] nonce;
++ protected final byte[] nonce;
+
+ /** Header key name field value. */
+- private String keyName;
++ protected final String keyName;
+
+ /** Header length in bytes. */
+- private int length;
++ protected final int length;
+
+
+ /**
+@@ -67,12 +75,17 @@ public CiphertextHeader(final byte[] nonce)
+ */
+ public CiphertextHeader(final byte[] nonce, final String keyName)
+ {
+- this.nonce = nonce;
+- this.length = 8 + nonce.length;
++ if (nonce.length > 255) {
++ throw new IllegalArgumentException("Nonce exceeds size limit in bytes (255)");
++ }
+ if (keyName != null) {
+- this.length += 4 + keyName.getBytes().length;
+- this.keyName = keyName;
++ if (ByteUtil.toBytes(keyName).length > MAX_KEYNAME_LEN) {
++ throw new IllegalArgumentException("Key name exceeds size limit in bytes (500)");
++ }
+ }
++ this.nonce = nonce;
++ this.keyName = keyName;
++ length = computeLength();
+ }
+
+ /**
+@@ -127,6 +140,19 @@ public String getKeyName()
+ }
+
+
++ /**
++ * @return Length of this header encoded as bytes.
++ */
++ protected int computeLength()
++ {
++ int len = 8 + nonce.length;
++ if (keyName != null) {
++ len += 4 + keyName.getBytes().length;
++ }
++ return len;
++ }
++
++
+ /**
+ * Creates a header from encrypted data containing a cleartext header prepended to the start.
+ *
+@@ -143,17 +169,20 @@ public static CiphertextHeader decode(final byte[] data) throws EncodingExceptio
+
+ final int length = bb.getInt();
+ if (length < 0) {
+- throw new EncodingException("Invalid ciphertext header length: " + length);
++ throw new EncodingException("Bad ciphertext header");
+ }
+
+ final byte[] nonce;
+ int nonceLen = 0;
+ try {
+ nonceLen = bb.getInt();
++ if (nonceLen > MAX_NONCE_LEN) {
++ throw new EncodingException("Bad ciphertext header: maximum nonce length exceeded");
++ }
+ nonce = new byte[nonceLen];
+ bb.get(nonce);
+ } catch (IndexOutOfBoundsException | BufferUnderflowException e) {
+- throw new EncodingException("Invalid nonce length: " + nonceLen);
++ throw new EncodingException("Bad ciphertext header");
+ }
+
+ String keyName = null;
+@@ -162,11 +191,14 @@ public static CiphertextHeader decode(final byte[] data) throws EncodingExceptio
+ int keyLen = 0;
+ try {
+ keyLen = bb.getInt();
++ if (keyLen > MAX_KEYNAME_LEN) {
++ throw new EncodingException("Bad ciphertext header: maximum key length exceeded");
++ }
+ b = new byte[keyLen];
+ bb.get(b);
+ keyName = new String(b);
+ } catch (IndexOutOfBoundsException | BufferUnderflowException e) {
+- throw new EncodingException("Invalid key length: " + keyLen);
++ throw new EncodingException("Bad ciphertext header");
+ }
+ }
+
+@@ -188,17 +220,20 @@ public static CiphertextHeader decode(final InputStream input) throws EncodingEx
+ {
+ final int length = ByteUtil.readInt(input);
+ if (length < 0) {
+- throw new EncodingException("Invalid ciphertext header length: " + length);
++ throw new EncodingException("Bad ciphertext header");
+ }
+
+ final byte[] nonce;
+ int nonceLen = 0;
+ try {
+ nonceLen = ByteUtil.readInt(input);
++ if (nonceLen > MAX_NONCE_LEN) {
++ throw new EncodingException("Bad ciphertext header: maximum nonce size exceeded");
++ }
+ nonce = new byte[nonceLen];
+ input.read(nonce);
+ } catch (ArrayIndexOutOfBoundsException e) {
+- throw new EncodingException("Invalid nonce length: " + nonceLen);
++ throw new EncodingException("Bad ciphertext header");
+ } catch (IOException e) {
+ throw new StreamException(e);
+ }
+@@ -209,10 +244,13 @@ public static CiphertextHeader decode(final InputStream input) throws EncodingEx
+ int keyLen = 0;
+ try {
+ keyLen = ByteUtil.readInt(input);
++ if (keyLen > MAX_KEYNAME_LEN) {
++ throw new EncodingException("Bad ciphertext header: maximum key length exceeded");
++ }
+ b = new byte[keyLen];
+ input.read(b);
+ } catch (ArrayIndexOutOfBoundsException e) {
+- throw new EncodingException("Invalid key length: " + keyLen);
++ throw new EncodingException("Bad ciphertext header");
+ } catch (IOException e) {
+ throw new StreamException(e);
+ }
+@@ -221,4 +259,5 @@ public static CiphertextHeader decode(final InputStream input) throws EncodingEx
+
+ return new CiphertextHeader(nonce, keyName);
+ }
++
+ }
+diff --git a/src/main/java/org/cryptacular/CiphertextHeaderV2.java b/src/main/java/org/cryptacular/CiphertextHeaderV2.java
+new file mode 100644
+index 0000000..8119f4e
+--- /dev/null
++++ b/src/main/java/org/cryptacular/CiphertextHeaderV2.java
+@@ -0,0 +1,307 @@
++/* See LICENSE for licensing and NOTICE for copyright. */
++package org.cryptacular;
++
++import java.io.ByteArrayOutputStream;
++import java.io.IOException;
++import java.io.InputStream;
++import java.nio.BufferUnderflowException;
++import java.nio.ByteBuffer;
++import java.nio.ByteOrder;
++import java.util.function.BiConsumer;
++import java.util.function.Function;
++import javax.crypto.SecretKey;
++import org.bouncycastle.crypto.digests.SHA256Digest;
++import org.bouncycastle.crypto.macs.HMac;
++import org.cryptacular.util.ByteUtil;
++
++/**
++ * Cleartext header prepended to ciphertext providing data required for decryption.
++ *
++ * Data format:
++ *
++ *
++ +---------+---------+---+----------+-------+------+
++ | Version | KeyName | 0 | NonceLen | Nonce | HMAC |
++ +---------+---------+---+----------+-------+------+
++ | |
++ +--- 4 ---+--- x ---+ 1 +--- 1 ----+-- y --+- 32 -+
++ *
++ *
++ * Where fields are defined as follows:
++ *
++ *
++ * Version - Header version format as a negative number (4-byte integer). Current version is -2.
++ * KeyName - Symbolic key name encoded as UTF-8 bytes (variable length)
++ * 0 - Null byte signifying the end of the symbolic key name
++ * NonceLen - Nonce length in bytes (1-byte unsigned integer)
++ * Nonce - Nonce bytes (variable length)
++ * HMAC - HMAC-256 over preceding fields (32 bytes)
++ *
++ *
++ * The last two fields provide support for multiple keys at the encryption provider. A common case for multiple
++ * keys is key rotation; by tagging encrypted data with a key name, an old key may be retrieved by name to decrypt
++ * outstanding data which will be subsequently re-encrypted with a new key.
++ *
++ * @author Middleware Services
++ */
++public class CiphertextHeaderV2 extends CiphertextHeader
++{
++ /** Header version format. */
++ private static final int VERSION = -2;
++
++ /** Size of HMAC algorithm output in bytes. */
++ private static final int HMAC_SIZE = 32;
++
++ /** Function to resolve a key from a symbolic key name. */
++ private Function keyLookup;
++
++
++ /**
++ * Creates a new instance with a nonce and named key.
++ *
++ * @param nonce Nonce bytes.
++ * @param keyName Key name.
++ */
++ public CiphertextHeaderV2(final byte[] nonce, final String keyName)
++ {
++ super(nonce, keyName);
++ if (keyName == null || keyName.isEmpty()) {
++ throw new IllegalArgumentException("Key name is required");
++ }
++ }
++
++
++ /**
++ * Sets the function to resolve keys from {@link #keyName}.
++ *
++ * @param keyLookup Key lookup function.
++ */
++ public void setKeyLookup(final Function keyLookup)
++ {
++ this.keyLookup = keyLookup;
++ }
++
++
++ @Override
++ public byte[] encode()
++ {
++ final SecretKey key = keyLookup != null ? keyLookup.apply(keyName) : null;
++ if (key == null) {
++ throw new IllegalStateException("Could not resolve secret key to generate header HMAC");
++ }
++ return encode(key);
++ }
++
++
++ /**
++ * Encodes the header into bytes.
++ *
++ * @param hmacKey Key used to generate header HMAC.
++ *
++ * @return Byte representation of header.
++ */
++ public byte[] encode(final SecretKey hmacKey)
++ {
++ final ByteBuffer bb = ByteBuffer.allocate(length);
++ bb.order(ByteOrder.BIG_ENDIAN);
++ bb.putInt(VERSION);
++ bb.put(ByteUtil.toBytes(keyName));
++ bb.put((byte) 0);
++ bb.put(ByteUtil.toUnsignedByte(nonce.length));
++ bb.put(nonce);
++ if (hmacKey != null) {
++ final byte[] hmac = hmac(bb.array(), 0, bb.limit() - HMAC_SIZE);
++ bb.put(hmac);
++ }
++ return bb.array();
++ }
++
++
++ /**
++ * @return Length of this header encoded as bytes.
++ */
++ protected int computeLength()
++ {
++ return 4 + ByteUtil.toBytes(keyName).length + 2 + nonce.length + HMAC_SIZE;
++ }
++
++
++ /**
++ * Creates a header from encrypted data containing a cleartext header prepended to the start.
++ *
++ * @param data Encrypted data with prepended header data.
++ * @param keyLookup Function used to look up the secret key from the symbolic key name in the header.
++ *
++ * @return Decoded header.
++ *
++ * @throws EncodingException when ciphertext header cannot be decoded.
++ */
++ public static CiphertextHeaderV2 decode(final byte[] data, final Function keyLookup)
++ throws EncodingException
++ {
++ final ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN);
++ return decodeInternal(
++ ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN),
++ keyLookup,
++ ByteBuffer -> bb.getInt(),
++ ByteBuffer -> bb.get(),
++ (ByteBuffer, output) -> bb.get(output));
++ }
++
++
++ /**
++ * Creates a header from encrypted data containing a cleartext header prepended to the start.
++ *
++ * @param input Input stream that is positioned at the start of ciphertext header data.
++ * @param keyLookup Function used to look up the secret key from the symbolic key name in the header.
++ *
++ * @return Decoded header.
++ *
++ * @throws EncodingException when ciphertext header cannot be decoded.
++ * @throws StreamException on stream IO errors.
++ */
++ public static CiphertextHeaderV2 decode(final InputStream input, final Function keyLookup)
++ throws EncodingException, StreamException
++ {
++ return decodeInternal(
++ input, keyLookup, ByteUtil::readInt, CiphertextHeaderV2::readByte, CiphertextHeaderV2::readInto);
++ }
++
++
++ /**
++ * Internal header decoding routine.
++ *
++ * @param Type of input source.
++ * @param source Source of header data (input stream or byte buffer).
++ * @param keyLookup Function to look up key from symbolic key name in header.
++ * @param readIntFn Function that produces a 4-byte integer from the input source.
++ * @param readByteFn Function that produces a byte from the input source.
++ * @param readBytesConsumer Function that fills a byte array from the input source.
++ *
++ * @return Decoded header.
++ */
++ private static CiphertextHeaderV2 decodeInternal(
++ final T source,
++ final Function keyLookup,
++ final Function readIntFn,
++ final Function readByteFn,
++ final BiConsumer readBytesConsumer)
++ {
++ final SecretKey key;
++ final String keyName;
++ final byte[] nonce;
++ final byte[] hmac;
++ try {
++ final int version = readIntFn.apply(source);
++ if (version != VERSION) {
++ throw new EncodingException("Unsupported ciphertext header version");
++ }
++ final ByteArrayOutputStream out = new ByteArrayOutputStream(100);
++ byte b = 0;
++ int count = 0;
++ while ((b = readByteFn.apply(source)) != 0) {
++ out.write(b);
++ if (out.size() > MAX_KEYNAME_LEN) {
++ throw new EncodingException("Bad ciphertext header: maximum nonce length exceeded");
++ }
++ count++;
++ }
++ keyName = ByteUtil.toString(out.toByteArray(), 0, count);
++ key = keyLookup.apply(keyName);
++ if (key == null) {
++ throw new IllegalStateException("Symbolic key name mentioned in header was not found");
++ }
++ final int nonceLen = ByteUtil.toInt(readByteFn.apply(source));
++ nonce = new byte[nonceLen];
++ readBytesConsumer.accept(source, nonce);
++ hmac = new byte[HMAC_SIZE];
++ readBytesConsumer.accept(source, hmac);
++ } catch (IndexOutOfBoundsException | BufferUnderflowException e) {
++ throw new EncodingException("Bad ciphertext header");
++ }
++ final CiphertextHeaderV2 header = new CiphertextHeaderV2(nonce, keyName);
++ final byte[] encoded = header.encode(key);
++ if (!arraysEqual(hmac, 0, encoded, encoded.length - HMAC_SIZE, HMAC_SIZE)) {
++ throw new EncodingException("Ciphertext header HMAC verification failed");
++ }
++ header.setKeyLookup(keyLookup);
++ return header;
++ }
++
++
++ /**
++ * Generates an HMAC-256 over the given input byte array.
++ *
++ * @param input Input bytes.
++ * @param offset Starting position in input byte array.
++ * @param length Number of bytes in input to consume.
++ *
++ * @return HMAC as byte array.
++ */
++ private static byte[] hmac(final byte[] input, final int offset, final int length)
++ {
++ final HMac hmac = new HMac(new SHA256Digest());
++ final byte[] output = new byte[HMAC_SIZE];
++ hmac.update(input, offset, length);
++ hmac.doFinal(output, 0);
++ return output;
++ }
++
++
++ /**
++ * Read output.length bytes from the input stream into the output buffer.
++ *
++ * @param input Input stream.
++ * @param output Output buffer.
++ */
++ private static void readInto(final InputStream input, final byte[] output)
++ {
++ try {
++ input.read(output);
++ } catch (IOException e) {
++ throw new StreamException(e);
++ }
++ }
++
++
++ /**
++ * Read a single byte from the input stream.
++ *
++ * @param input Input stream.
++ *
++ * @return Byte read from input stream.
++ */
++ private static byte readByte(final InputStream input)
++ {
++ try {
++ return (byte) input.read();
++ } catch (IOException e) {
++ throw new StreamException(e);
++ }
++ }
++
++
++ /**
++ * Determines if two byte array ranges are equal bytewise.
++ *
++ * @param a First array to compare.
++ * @param aOff Offset into first array.
++ * @param b Second array to compare.
++ * @param bOff Offset into second array.
++ * @param length Number of bytes to compare.
++ *
++ * @return True if every byte in the given range is equal, false otherwise.
++ */
++ private static boolean arraysEqual(final byte[] a, final int aOff, final byte[] b, final int bOff, final int length)
++ {
++ if (length + aOff > a.length || length + bOff > b.length) {
++ return false;
++ }
++ for (int i = 0; i < length; i++) {
++ if (a[i + aOff] != b[i + bOff]) {
++ return false;
++ }
++ }
++ return true;
++ }
++}
+diff --git a/src/main/java/org/cryptacular/bean/AbstractBlockCipherBean.java b/src/main/java/org/cryptacular/bean/AbstractBlockCipherBean.java
+index 0cd6542..0d06b32 100644
+--- a/src/main/java/org/cryptacular/bean/AbstractBlockCipherBean.java
++++ b/src/main/java/org/cryptacular/bean/AbstractBlockCipherBean.java
+@@ -45,12 +45,12 @@ public AbstractBlockCipherBean(
+ protected byte[] process(final CiphertextHeader header, final boolean mode, final byte[] input)
+ {
+ final BlockCipherAdapter cipher = newCipher(header, mode);
+- final byte[] headerBytes = header.encode();
+ int outOff;
+ final int inOff;
+ final int length;
+ final byte[] output;
+ if (mode) {
++ final byte[] headerBytes = header.encode();
+ final int outSize = headerBytes.length + cipher.getOutputSize(input.length);
+ output = new byte[outSize];
+ System.arraycopy(headerBytes, 0, output, 0, headerBytes.length);
+@@ -58,12 +58,12 @@ public AbstractBlockCipherBean(
+ outOff = headerBytes.length;
+ length = input.length;
+ } else {
+- length = input.length - headerBytes.length;
++ outOff = 0;
++ inOff = header.getLength();
++ length = input.length - inOff;
+
+ final int outSize = cipher.getOutputSize(length);
+ output = new byte[outSize];
+- inOff = headerBytes.length;
+- outOff = 0;
+ }
+ outOff += cipher.processBytes(input, inOff, length, output, outOff);
+ outOff += cipher.doFinal(output, outOff);
+@@ -85,7 +85,7 @@ protected void process(
+ {
+ final BlockCipherAdapter cipher = newCipher(header, mode);
+ final int outSize = cipher.getOutputSize(StreamUtil.CHUNK_SIZE);
+- final byte[] outBuf = new byte[outSize > StreamUtil.CHUNK_SIZE ? outSize : StreamUtil.CHUNK_SIZE];
++ final byte[] outBuf = new byte[Math.max(outSize, StreamUtil.CHUNK_SIZE)];
+ StreamUtil.pipeAll(
+ input,
+ output,
+diff --git a/src/main/java/org/cryptacular/bean/AbstractCipherBean.java b/src/main/java/org/cryptacular/bean/AbstractCipherBean.java
+index f82a259..fd73763 100644
+--- a/src/main/java/org/cryptacular/bean/AbstractCipherBean.java
++++ b/src/main/java/org/cryptacular/bean/AbstractCipherBean.java
+@@ -8,14 +8,16 @@
+ import java.security.KeyStore;
+ import javax.crypto.SecretKey;
+ import org.cryptacular.CiphertextHeader;
++import org.cryptacular.CiphertextHeaderV2;
+ import org.cryptacular.CryptoException;
+ import org.cryptacular.EncodingException;
+ import org.cryptacular.StreamException;
+ import org.cryptacular.generator.Nonce;
++import org.cryptacular.util.CipherUtil;
+
+ /**
+ * Base class for all cipher beans. The base class assumes all ciphertext output will contain a prepended {@link
+- * CiphertextHeader} containing metadata that facilitates decryption.
++ * CiphertextHeaderV2} containing metadata that facilitates decryption.
+ *
+ * @author Middleware Services
+ */
+@@ -128,14 +130,14 @@ public void setNonce(final Nonce nonce)
+ @Override
+ public byte[] encrypt(final byte[] input) throws CryptoException
+ {
+- return process(new CiphertextHeader(nonce.generate(), keyAlias), true, input);
++ return process(header(), true, input);
+ }
+
+
+ @Override
+ public void encrypt(final InputStream input, final OutputStream output) throws CryptoException, StreamException
+ {
+- final CiphertextHeader header = new CiphertextHeader(nonce.generate(), keyAlias);
++ final CiphertextHeaderV2 header = header();
+ try {
+ output.write(header.encode());
+ } catch (IOException e) {
+@@ -148,11 +150,7 @@ public void encrypt(final InputStream input, final OutputStream output) throws C
+ @Override
+ public byte[] decrypt(final byte[] input) throws CryptoException, EncodingException
+ {
+- final CiphertextHeader header = CiphertextHeader.decode(input);
+- if (header.getKeyName() == null) {
+- throw new CryptoException("Ciphertext header does not contain required key");
+- }
+- return process(header, false, input);
++ return process(CipherUtil.decodeHeader(input, this::lookupKey), false, input);
+ }
+
+
+@@ -160,11 +158,7 @@ public void encrypt(final InputStream input, final OutputStream output) throws C
+ public void decrypt(final InputStream input, final OutputStream output)
+ throws CryptoException, EncodingException, StreamException
+ {
+- final CiphertextHeader header = CiphertextHeader.decode(input);
+- if (header.getKeyName() == null) {
+- throw new CryptoException("Ciphertext header does not contain required key");
+- }
+- process(header, false, input, output);
++ process(CipherUtil.decodeHeader(input, this::lookupKey), false, input, output);
+ }
+
+
+@@ -211,4 +205,15 @@ protected SecretKey lookupKey(final String alias)
+ * @param output Stream that receives output of cipher.
+ */
+ protected abstract void process(CiphertextHeader header, boolean mode, InputStream input, OutputStream output);
++
++
++ /**
++ * @return New ciphertext header for a pending encryption or decryption operation performed by this instance.
++ */
++ private CiphertextHeaderV2 header()
++ {
++ final CiphertextHeaderV2 header = new CiphertextHeaderV2(nonce.generate(), keyAlias);
++ header.setKeyLookup(this::lookupKey);
++ return header;
++ }
+ }
+diff --git a/src/main/java/org/cryptacular/util/ByteUtil.java b/src/main/java/org/cryptacular/util/ByteUtil.java
+index 541fbf7..2163639 100644
+--- a/src/main/java/org/cryptacular/util/ByteUtil.java
++++ b/src/main/java/org/cryptacular/util/ByteUtil.java
+@@ -31,7 +31,7 @@ private ByteUtil() {}
+ *
+ * @param data 4-byte array in big-endian format.
+ *
+- * @return Long integer value.
++ * @return Integer value.
+ */
+ public static int toInt(final byte[] data)
+ {
+@@ -39,6 +39,19 @@ public static int toInt(final byte[] data)
+ }
+
+
++ /**
++ * Converts an unsigned byte into an integer.
++ *
++ * @param unsigned Unsigned byte.
++ *
++ * @return Integer value.
++ */
++ public static int toInt(final byte unsigned)
++ {
++ return 0x000000FF & unsigned;
++ }
++
++
+ /**
+ * Reads 4-bytes from the input stream and converts to a 32-bit integer.
+ *
+@@ -175,6 +188,21 @@ public static String toString(final byte[] bytes)
+ }
+
+
++ /**
++ * Converts a portion of a byte array into a string in the UTF-8 character set.
++ *
++ * @param bytes Byte array to convert.
++ * @param offset Offset into byte array where string content begins.
++ * @param length Total number of bytes to convert.
++ *
++ * @return UTF-8 string representation of bytes.
++ */
++ public static String toString(final byte[] bytes, final int offset, final int length)
++ {
++ return new String(bytes, offset, length, DEFAULT_CHARSET);
++ }
++
++
+ /**
+ * Converts a byte buffer into a string in the UTF-8 character set.
+ *
+@@ -226,6 +254,19 @@ public static ByteBuffer toByteBuffer(final String s)
+ }
+
+
++ /**
++ * Converts an integer into an unsigned byte. All bits above 1 byte are truncated.
++ *
++ * @param b Integer value.
++ *
++ * @return Unsigned byte as a byte.
++ */
++ public static byte toUnsignedByte(final int b)
++ {
++ return (byte) (0x000000FF & b);
++ }
++
++
+ /**
+ * Converts a byte buffer into a byte array.
+ *
+@@ -244,4 +285,6 @@ public static ByteBuffer toByteBuffer(final String s)
+ buffer.get(array);
+ return array;
+ }
++
++
+ }
+diff --git a/src/main/java/org/cryptacular/util/CipherUtil.java b/src/main/java/org/cryptacular/util/CipherUtil.java
+index d460039..cdbac0d 100644
+--- a/src/main/java/org/cryptacular/util/CipherUtil.java
++++ b/src/main/java/org/cryptacular/util/CipherUtil.java
+@@ -4,6 +4,7 @@
+ import java.io.IOException;
+ import java.io.InputStream;
+ import java.io.OutputStream;
++import java.util.function.Function;
+ import javax.crypto.SecretKey;
+ import org.bouncycastle.crypto.BlockCipher;
+ import org.bouncycastle.crypto.modes.AEADBlockCipher;
+@@ -13,6 +14,7 @@
+ import org.bouncycastle.crypto.params.KeyParameter;
+ import org.bouncycastle.crypto.params.ParametersWithIV;
+ import org.cryptacular.CiphertextHeader;
++import org.cryptacular.CiphertextHeaderV2;
+ import org.cryptacular.CryptoException;
+ import org.cryptacular.EncodingException;
+ import org.cryptacular.StreamException;
+@@ -37,15 +39,15 @@ private CipherUtil() {}
+
+
+ /**
+- * Encrypts data using an AEAD cipher. A {@link CiphertextHeader} is prepended to the resulting ciphertext and used as
+- * AAD (Additional Authenticated Data) passed to the AEAD cipher.
++ * Encrypts data using an AEAD cipher. A {@link CiphertextHeaderV2} is prepended to the resulting ciphertext and
++ * used as AAD (Additional Authenticated Data) passed to the AEAD cipher.
+ *
+ * @param cipher AEAD cipher.
+ * @param key Encryption key.
+ * @param nonce Nonce generator.
+ * @param data Plaintext data to be encrypted.
+ *
+- * @return Concatenation of encoded {@link CiphertextHeader} and encrypted data that completely fills the returned
++ * @return Concatenation of encoded {@link CiphertextHeaderV2} and encrypted data that completely fills the returned
+ * byte array.
+ *
+ * @throws CryptoException on encryption errors.
+@@ -54,22 +56,22 @@ private CipherUtil() {}
+ throws CryptoException
+ {
+ final byte[] iv = nonce.generate();
+- final byte[] header = new CiphertextHeader(iv).encode();
++ final byte[] header = new CiphertextHeaderV2(iv, "1").encode(key);
+ cipher.init(true, new AEADParameters(new KeyParameter(key.getEncoded()), MAC_SIZE_BITS, iv, header));
+ return encrypt(new AEADBlockCipherAdapter(cipher), header, data);
+ }
+
+
+ /**
+- * Encrypts data using an AEAD cipher. A {@link CiphertextHeader} is prepended to the resulting ciphertext and used as
+- * AAD (Additional Authenticated Data) passed to the AEAD cipher.
++ * Encrypts data using an AEAD cipher. A {@link CiphertextHeaderV2} is prepended to the resulting ciphertext and used
++ * as AAD (Additional Authenticated Data) passed to the AEAD cipher.
+ *
+ * @param cipher AEAD cipher.
+ * @param key Encryption key.
+ * @param nonce Nonce generator.
+ * @param input Input stream containing plaintext data.
+- * @param output Output stream that receives a {@link CiphertextHeader} followed by ciphertext data produced by the
+- * AEAD cipher in encryption mode.
++ * @param output Output stream that receives a {@link CiphertextHeaderV2} followed by ciphertext data produced by
++ * the AEAD cipher in encryption mode.
+ *
+ * @throws CryptoException on encryption errors.
+ * @throws StreamException on IO errors.
+@@ -83,7 +85,7 @@ public static void encrypt(
+ throws CryptoException, StreamException
+ {
+ final byte[] iv = nonce.generate();
+- final byte[] header = new CiphertextHeader(iv).encode();
++ final byte[] header = new CiphertextHeaderV2(iv, "1").encode(key);
+ cipher.init(true, new AEADParameters(new KeyParameter(key.getEncoded()), MAC_SIZE_BITS, iv, header));
+ writeHeader(header, output);
+ process(new AEADBlockCipherAdapter(cipher), input, output);
+@@ -95,7 +97,7 @@ public static void encrypt(
+ *
+ * @param cipher AEAD cipher.
+ * @param key Encryption key.
+- * @param data Ciphertext data containing a prepended {@link CiphertextHeader}. The header is treated as AAD input
++ * @param data Ciphertext data containing a prepended {@link CiphertextHeaderV2}. The header is treated as AAD input
+ * to the cipher that is verified during decryption.
+ *
+ * @return Decrypted data that completely fills the returned byte array.
+@@ -106,7 +108,7 @@ public static void encrypt(
+ public static byte[] decrypt(final AEADBlockCipher cipher, final SecretKey key, final byte[] data)
+ throws CryptoException, EncodingException
+ {
+- final CiphertextHeader header = CiphertextHeader.decode(data);
++ final CiphertextHeader header = decodeHeader(data, String -> key);
+ final byte[] nonce = header.getNonce();
+ final byte[] hbytes = header.encode();
+ cipher.init(false, new AEADParameters(new KeyParameter(key.getEncoded()), MAC_SIZE_BITS, nonce, hbytes));
+@@ -119,7 +121,7 @@ public static void encrypt(
+ *
+ * @param cipher AEAD cipher.
+ * @param key Encryption key.
+- * @param input Input stream containing a {@link CiphertextHeader} followed by ciphertext data. The header is
++ * @param input Input stream containing a {@link CiphertextHeaderV2} followed by ciphertext data. The header is
+ * treated as AAD input to the cipher that is verified during decryption.
+ * @param output Output stream that receives plaintext produced by block cipher in decryption mode.
+ *
+@@ -134,7 +136,7 @@ public static void decrypt(
+ final OutputStream output)
+ throws CryptoException, EncodingException, StreamException
+ {
+- final CiphertextHeader header = CiphertextHeader.decode(input);
++ final CiphertextHeader header = decodeHeader(input, String -> key);
+ final byte[] nonce = header.getNonce();
+ final byte[] hbytes = header.encode();
+ cipher.init(false, new AEADParameters(new KeyParameter(key.getEncoded()), MAC_SIZE_BITS, nonce, hbytes));
+@@ -143,7 +145,7 @@ public static void decrypt(
+
+
+ /**
+- * Encrypts data using the given block cipher with PKCS5 padding. A {@link CiphertextHeader} is prepended to the
++ * Encrypts data using the given block cipher with PKCS5 padding. A {@link CiphertextHeaderV2} is prepended to the
+ * resulting ciphertext.
+ *
+ * @param cipher Block cipher.
+@@ -152,7 +154,7 @@ public static void decrypt(
+ * cipher block size.
+ * @param data Plaintext data to be encrypted.
+ *
+- * @return Concatenation of encoded {@link CiphertextHeader} and encrypted data that completely fills the returned
++ * @return Concatenation of encoded {@link CiphertextHeaderV2} and encrypted data that completely fills the returned
+ * byte array.
+ *
+ * @throws CryptoException on encryption errors.
+@@ -161,7 +163,7 @@ public static void decrypt(
+ throws CryptoException
+ {
+ final byte[] iv = nonce.generate();
+- final byte[] header = new CiphertextHeader(iv).encode();
++ final byte[] header = new CiphertextHeaderV2(iv, "1").encode(key);
+ final PaddedBufferedBlockCipher padded = new PaddedBufferedBlockCipher(cipher, new PKCS7Padding());
+ padded.init(true, new ParametersWithIV(new KeyParameter(key.getEncoded()), iv));
+ return encrypt(new BufferedBlockCipherAdapter(padded), header, data);
+@@ -191,7 +193,7 @@ public static void encrypt(
+ throws CryptoException, StreamException
+ {
+ final byte[] iv = nonce.generate();
+- final byte[] header = new CiphertextHeader(iv).encode();
++ final byte[] header = new CiphertextHeaderV2(iv, "1").encode(key);
+ final PaddedBufferedBlockCipher padded = new PaddedBufferedBlockCipher(cipher, new PKCS7Padding());
+ padded.init(true, new ParametersWithIV(new KeyParameter(key.getEncoded()), iv));
+ writeHeader(header, output);
+@@ -214,7 +216,7 @@ public static void encrypt(
+ public static byte[] decrypt(final BlockCipher cipher, final SecretKey key, final byte[] data)
+ throws CryptoException, EncodingException
+ {
+- final CiphertextHeader header = CiphertextHeader.decode(data);
++ final CiphertextHeader header = decodeHeader(data, String -> key);
+ final PaddedBufferedBlockCipher padded = new PaddedBufferedBlockCipher(cipher, new PKCS7Padding());
+ padded.init(false, new ParametersWithIV(new KeyParameter(key.getEncoded()), header.getNonce()));
+ return decrypt(new BufferedBlockCipherAdapter(padded), data, header.getLength());
+@@ -240,13 +242,62 @@ public static void decrypt(
+ final OutputStream output)
+ throws CryptoException, EncodingException, StreamException
+ {
+- final CiphertextHeader header = CiphertextHeader.decode(input);
++ final CiphertextHeader header = decodeHeader(input, String -> key);
+ final PaddedBufferedBlockCipher padded = new PaddedBufferedBlockCipher(cipher, new PKCS7Padding());
+ padded.init(false, new ParametersWithIV(new KeyParameter(key.getEncoded()), header.getNonce()));
+ process(new BufferedBlockCipherAdapter(padded), input, output);
+ }
+
+
++ /**
++ * Decodes the ciphertext header at the start of the given byte array.
++ * Supports both original (deprecated) and v2 formats.
++ *
++ * @param data Ciphertext data with prepended header.
++ * @param keyLookup Decryption key lookup function.
++ *
++ * @return Ciphertext header instance.
++ */
++ public static CiphertextHeader decodeHeader(final byte[] data, final Function keyLookup)
++ {
++ try {
++ return CiphertextHeaderV2.decode(data, keyLookup);
++ } catch (EncodingException e) {
++ return CiphertextHeader.decode(data);
++ }
++ }
++
++
++ /**
++ * Decodes the ciphertext header at the start of the given input stream.
++ * Supports both original (deprecated) and v2 formats.
++ *
++ * @param in Ciphertext stream that is positioned at the start of the ciphertext header.
++ * @param keyLookup Decryption key lookup function.
++ *
++ * @return Ciphertext header instance.
++ */
++ public static CiphertextHeader decodeHeader(final InputStream in, final Function keyLookup)
++ {
++ CiphertextHeader header;
++ try {
++ // Mark the stream start position so we can try again with old format header
++ if (in.markSupported()) {
++ in.mark(4);
++ }
++ header = CiphertextHeaderV2.decode(in, keyLookup);
++ } catch (EncodingException e) {
++ try {
++ in.reset();
++ } catch (IOException ioe) {
++ throw new StreamException("Stream error trying to process old header format: " + ioe.getMessage());
++ }
++ header = CiphertextHeader.decode(in);
++ }
++ return header;
++ }
++
++
+ /**
+ * Encrypts the given data.
+ *
+@@ -325,6 +376,7 @@ private static void process(final BlockCipherAdapter cipher, final InputStream i
+ }
+
+
++
+ /**
+ * Writes a ciphertext header to the output stream.
+ *
+diff --git a/src/test/java/org/cryptacular/CiphertextHeaderTest.java b/src/test/java/org/cryptacular/CiphertextHeaderTest.java
+new file mode 100644
+index 0000000..51abfae
+--- /dev/null
++++ b/src/test/java/org/cryptacular/CiphertextHeaderTest.java
+@@ -0,0 +1,55 @@
++/* See LICENSE for licensing and NOTICE for copyright. */
++package org.cryptacular;
++
++import java.util.Arrays;
++import org.cryptacular.util.CodecUtil;
++import org.testng.annotations.Test;
++import static org.testng.Assert.assertEquals;
++
++/**
++ * Unit test for {@link CiphertextHeader}.
++ *
++ * @author Middleware Services
++ */
++public class CiphertextHeaderTest
++{
++
++ @Test(
++ expectedExceptions = IllegalArgumentException.class,
++ expectedExceptionsMessageRegExp = "Nonce exceeds size limit in bytes.*")
++ public void testNonceLimitConstructor()
++ {
++ new CiphertextHeader(new byte[256], "key2");
++ }
++
++ @Test
++ public void testEncodeDecodeSuccess()
++ {
++ final byte[] nonce = new byte[255];
++ Arrays.fill(nonce, (byte) 7);
++ final CiphertextHeader expected = new CiphertextHeader(nonce, "aleph");
++ final byte[] encoded = expected.encode();
++ assertEquals(expected.getLength(), encoded.length);
++ final CiphertextHeader actual = CiphertextHeader.decode(encoded);
++ assertEquals(expected.getNonce(), actual.getNonce());
++ assertEquals(expected.getKeyName(), actual.getKeyName());
++ assertEquals(expected.getLength(), actual.getLength());
++ }
++
++ @Test(
++ expectedExceptions = EncodingException.class,
++ expectedExceptionsMessageRegExp = "Bad ciphertext header: maximum nonce length exceeded")
++ public void testDecodeFailNonceLengthExceeded()
++ {
++ // https://github.com/vt-middleware/cryptacular/issues/52
++ CiphertextHeader.decode(CodecUtil.hex("000000347ffffffd"));
++ }
++
++ @Test(
++ expectedExceptions = EncodingException.class,
++ expectedExceptionsMessageRegExp = "Bad ciphertext header: maximum key length exceeded")
++ public void testDecodeFailKeyLengthExceeded()
++ {
++ CiphertextHeader.decode(CodecUtil.hex("000000F300000004DEADBEEF00FFFFFF"));
++ }
++}
+diff --git a/src/test/java/org/cryptacular/CiphertextHeaderV2Test.java b/src/test/java/org/cryptacular/CiphertextHeaderV2Test.java
+new file mode 100644
+index 0000000..7313d35
+--- /dev/null
++++ b/src/test/java/org/cryptacular/CiphertextHeaderV2Test.java
+@@ -0,0 +1,67 @@
++/* See LICENSE for licensing and NOTICE for copyright. */
++package org.cryptacular;
++
++import java.util.Arrays;
++import javax.crypto.SecretKey;
++import javax.crypto.spec.SecretKeySpec;
++import org.cryptacular.generator.sp80038a.RBGNonce;
++import org.testng.annotations.Test;
++import static org.testng.Assert.assertEquals;
++
++/**
++ * Unit test for {@link CiphertextHeaderV2}.
++ *
++ * @author Middleware Services
++ */
++public class CiphertextHeaderV2Test
++{
++ /** Test HMAC key. */
++ private final SecretKey key = new SecretKeySpec(new RBGNonce().generate(), "AES");
++
++ @Test(
++ expectedExceptions = IllegalArgumentException.class,
++ expectedExceptionsMessageRegExp = "Nonce exceeds size limit in bytes.*")
++ public void testNonceLimitConstructor()
++ {
++ new CiphertextHeaderV2(new byte[256], "key2");
++ }
++
++ @Test
++ public void testEncodeDecodeSuccess()
++ {
++ final byte[] nonce = new byte[255];
++ Arrays.fill(nonce, (byte) 7);
++ final CiphertextHeaderV2 expected = new CiphertextHeaderV2(nonce, "aleph");
++ expected.setKeyLookup(this::getKey);
++ final byte[] encoded = expected.encode();
++ assertEquals(expected.getLength(), encoded.length);
++ final CiphertextHeaderV2 actual = CiphertextHeaderV2.decode(encoded, this::getKey);
++ assertEquals(expected.getNonce(), actual.getNonce());
++ assertEquals(expected.getKeyName(), actual.getKeyName());
++ assertEquals(expected.getLength(), actual.getLength());
++ }
++
++ @Test(
++ expectedExceptions = EncodingException.class,
++ expectedExceptionsMessageRegExp = "Ciphertext header HMAC verification failed")
++ public void testEncodeDecodeFailBadHMAC()
++ {
++ final byte[] nonce = new byte[16];
++ Arrays.fill(nonce, (byte) 3);
++ final CiphertextHeaderV2 expected = new CiphertextHeaderV2(nonce, "aleph");
++ // Tamper with computed HMAC
++ final byte[] encoded = expected.encode(key);
++ final int index = encoded.length - 3;
++ final byte b = encoded[index];
++ encoded[index] = (byte) (b + 1);
++ CiphertextHeaderV2.decode(encoded, this::getKey);
++ }
++
++ private SecretKey getKey(final String alias)
++ {
++ if ("aleph".equals(alias)) {
++ return key;
++ }
++ return null;
++ }
++}
+diff --git a/src/test/java/org/cryptacular/bean/AEADBlockCipherBeanTest.java b/src/test/java/org/cryptacular/bean/AEADBlockCipherBeanTest.java
+index fde2c97..a26f341 100644
+--- a/src/test/java/org/cryptacular/bean/AEADBlockCipherBeanTest.java
++++ b/src/test/java/org/cryptacular/bean/AEADBlockCipherBeanTest.java
+@@ -5,13 +5,12 @@
+ import java.io.ByteArrayOutputStream;
+ import java.io.File;
+ import java.security.KeyStore;
+-import javax.crypto.SecretKey;
+-
+ import org.cryptacular.FailListener;
+ import org.cryptacular.generator.sp80038d.CounterNonce;
+ import org.cryptacular.io.FileResource;
+ import org.cryptacular.spec.AEADBlockCipherSpec;
+ import org.cryptacular.util.ByteUtil;
++import org.cryptacular.util.CodecUtil;
+ import org.cryptacular.util.StreamUtil;
+ import org.testng.annotations.DataProvider;
+ import org.testng.annotations.Listeners;
+@@ -25,6 +25,7 @@
+ @Listeners(FailListener.class)
+ public class AEADBlockCipherBeanTest
+ {
++
+ @DataProvider(name = "test-arrays")
+ public Object[][] getTestArrays()
+ {
+@@ -78,14 +79,7 @@
+ public void testEncryptDecryptArray(final String input, final String cipherSpecString)
+ throws Exception
+ {
+- final AEADBlockCipherBean cipherBean = new AEADBlockCipherBean();
+- final AEADBlockCipherSpec cipherSpec = AEADBlockCipherSpec.parse(cipherSpecString);
+- cipherBean.setNonce(new CounterNonce("vtmw", System.nanoTime()));
+- cipherBean.setKeyAlias("vtcrypt");
+- cipherBean.setKeyPassword("vtcrypt");
+- cipherBean.setKeyStore(getTestKeyStore());
+- cipherBean.setBlockCipherSpec(cipherSpec);
+-
++ final AEADBlockCipherBean cipherBean = newCipherBean(AEADBlockCipherSpec.parse(cipherSpecString));
+ final byte[] ciphertext = cipherBean.encrypt(ByteUtil.toBytes(input));
+ assertEquals(ByteUtil.toString(cipherBean.decrypt(ciphertext)), input);
+ }
+@@ -95,14 +89,7 @@ public void testEncryptDecryptArray(final String input, final String cipherSpecS
+ public void testEncryptDecryptStream(final String path, final String cipherSpecString)
+ throws Exception
+ {
+- final AEADBlockCipherBean cipherBean = new AEADBlockCipherBean();
+- final AEADBlockCipherSpec cipherSpec = AEADBlockCipherSpec.parse(cipherSpecString);
+- cipherBean.setNonce(new CounterNonce("vtmw", System.nanoTime()));
+- cipherBean.setKeyAlias("vtcrypt");
+- cipherBean.setKeyPassword("vtcrypt");
+- cipherBean.setKeyStore(getTestKeyStore());
+- cipherBean.setBlockCipherSpec(cipherSpec);
+-
++ final AEADBlockCipherBean cipherBean = newCipherBean(AEADBlockCipherSpec.parse(cipherSpecString));
+ final ByteArrayOutputStream tempOut = new ByteArrayOutputStream(8192);
+ cipherBean.encrypt(StreamUtil.makeStream(new File(path)), tempOut);
+
+@@ -113,6 +100,34 @@ public void testEncryptDecryptStream(final String path, final String cipherSpecS
+ }
+
+
++ @Test
++ public void testDecryptArrayBackwardCompatibleHeader()
++ {
++ final AEADBlockCipherBean cipherBean = newCipherBean(new AEADBlockCipherSpec("Twofish", "OCB"));
++ final String expected = "Have you passed through this night?";
++ final String v1CiphertextHex =
++ "0000001f0000000c76746d770002ba17043672d900000007767463727970745a38dee735266e3f5f7aafec8d1c9ed8a0830a2ff9" +
++ "c3a46c25f89e69b6eb39dbb82fd13da50e32b2544a73f1a4476677b377e6";
++ final byte[] plaintext = cipherBean.decrypt(CodecUtil.hex(v1CiphertextHex));
++ assertEquals(expected, ByteUtil.toString(plaintext));
++ }
++
++
++ @Test
++ public void testDecryptStreamBackwardCompatibleHeader()
++ {
++ final AEADBlockCipherBean cipherBean = newCipherBean(new AEADBlockCipherSpec("Twofish", "OCB"));
++ final String expected = "Have you passed through this night?";
++ final String v1CiphertextHex =
++ "0000001f0000000c76746d770002ba17043672d900000007767463727970745a38dee735266e3f5f7aafec8d1c9ed8a0830a2ff9" +
++ "c3a46c25f89e69b6eb39dbb82fd13da50e32b2544a73f1a4476677b377e6";
++ final ByteArrayInputStream in = new ByteArrayInputStream(CodecUtil.hex(v1CiphertextHex));
++ final ByteArrayOutputStream out = new ByteArrayOutputStream();
++ cipherBean.decrypt(in, out);
++ assertEquals(expected, ByteUtil.toString(out.toByteArray()));
++ }
++
++
+ private static KeyStore getTestKeyStore()
+ {
+ final KeyStoreFactoryBean bean = new KeyStoreFactoryBean();
+@@ -122,12 +137,15 @@ private static KeyStore getTestKeyStore()
+ return bean.newInstance();
+ }
+
+- private static SecretKey getTestKey()
++
++ private static AEADBlockCipherBean newCipherBean(final AEADBlockCipherSpec cipherSpec)
+ {
+- final KeyStoreBasedKeyFactoryBean secretKeyFactoryBean = new KeyStoreBasedKeyFactoryBean<>();
+- secretKeyFactoryBean.setKeyStore(getTestKeyStore());
+- secretKeyFactoryBean.setPassword("vtcrypt");
+- secretKeyFactoryBean.setAlias("vtcrypt");
+- return secretKeyFactoryBean.newInstance();
++ final AEADBlockCipherBean cipherBean = new AEADBlockCipherBean();
++ cipherBean.setNonce(new CounterNonce("vtmw", System.nanoTime()));
++ cipherBean.setKeyAlias("vtcrypt");
++ cipherBean.setKeyPassword("vtcrypt");
++ cipherBean.setKeyStore(getTestKeyStore());
++ cipherBean.setBlockCipherSpec(cipherSpec);
++ return cipherBean;
+ }
+ }
+diff --git a/src/test/java/org/cryptacular/util/CipherUtilTest.java b/src/test/java/org/cryptacular/util/CipherUtilTest.java
+index f2cd7de..886ce54 100644
+--- a/src/test/java/org/cryptacular/util/CipherUtilTest.java
++++ b/src/test/java/org/cryptacular/util/CipherUtilTest.java
+@@ -17,11 +17,14 @@
+ import org.bouncycastle.crypto.modes.OCBBlockCipher;
+ import org.bouncycastle.crypto.modes.OFBBlockCipher;
+ import org.cryptacular.FailListener;
++import org.cryptacular.bean.KeyStoreBasedKeyFactoryBean;
++import org.cryptacular.bean.KeyStoreFactoryBean;
+ import org.cryptacular.generator.Nonce;
+ import org.cryptacular.generator.SecretKeyGenerator;
+ import org.cryptacular.generator.sp80038a.LongCounterNonce;
+ import org.cryptacular.generator.sp80038a.RBGNonce;
+ import org.cryptacular.generator.sp80038d.CounterNonce;
++import org.cryptacular.io.FileResource;
+ import org.testng.annotations.DataProvider;
+ import org.testng.annotations.Listeners;
+ import org.testng.annotations.Test;
+@@ -35,6 +38,22 @@
+ @Listeners(FailListener.class)
+ public class CipherUtilTest
+ {
++ /** Static key derived from keystore on resource classpath. */
++ private static final SecretKey STATIC_KEY;
++
++ static
++ {
++ final KeyStoreFactoryBean keyStoreFactory = new KeyStoreFactoryBean();
++ keyStoreFactory.setPassword("vtcrypt");
++ keyStoreFactory.setResource(new FileResource(new File("src/test/resources/keystores/cipher-bean.jceks")));
++ keyStoreFactory.setType("JCEKS");
++ final KeyStoreBasedKeyFactoryBean keyFactory = new KeyStoreBasedKeyFactoryBean<>();
++ keyFactory.setKeyStore(keyStoreFactory.newInstance());
++ keyFactory.setAlias("vtcrypt");
++ keyFactory.setPassword("vtcrypt");
++ STATIC_KEY = keyFactory.newInstance();
++ }
++
+ @DataProvider(name = "block-cipher")
+ public Object[][] getBlockCipherData()
+ {
+@@ -165,4 +184,32 @@ public void testAeadBlockCipherEncryptDecryptStream(final String path)
+ CipherUtil.decrypt(cipher, key, tempIn, actual);
+ assertEquals(new String(actual.toByteArray()), expected);
+ }
++
++
++ @Test
++ public void testDecryptArrayBackwardCompatibleHeader()
++ {
++ final AEADBlockCipher cipher = new OCBBlockCipher(new TwofishEngine(), new TwofishEngine());
++ final String expected = "Have you passed through this night?";
++ final String v1CiphertextHex =
++ "0000001f0000000c76746d770002ba17043672d900000007767463727970745a38dee735266e3f5f7aafec8d1c9ed8a0830a2ff9" +
++ "c3a46c25f89e69b6eb39dbb82fd13da50e32b2544a73f1a4476677b377e6";
++ final byte[] plaintext = CipherUtil.decrypt(cipher, STATIC_KEY, CodecUtil.hex(v1CiphertextHex));
++ assertEquals(expected, ByteUtil.toString(plaintext));
++ }
++
++
++ @Test
++ public void testDecryptStreamBackwardCompatibleHeader()
++ {
++ final AEADBlockCipher cipher = new OCBBlockCipher(new TwofishEngine(), new TwofishEngine());
++ final String expected = "Have you passed through this night?";
++ final String v1CiphertextHex =
++ "0000001f0000000c76746d770002ba17043672d900000007767463727970745a38dee735266e3f5f7aafec8d1c9ed8a0830a2ff9" +
++ "c3a46c25f89e69b6eb39dbb82fd13da50e32b2544a73f1a4476677b377e6";
++ final ByteArrayInputStream in = new ByteArrayInputStream(CodecUtil.hex(v1CiphertextHex));
++ final ByteArrayOutputStream out = new ByteArrayOutputStream();
++ CipherUtil.decrypt(cipher, STATIC_KEY, in, out);
++ assertEquals(expected, ByteUtil.toString(out.toByteArray()));
++ }
+ }
diff --git a/backport-CVE-2020-7226-2.patch b/backport-CVE-2020-7226-2.patch
new file mode 100644
index 0000000..7c602fb
--- /dev/null
+++ b/backport-CVE-2020-7226-2.patch
@@ -0,0 +1,81 @@
+From 132f15ead532d78d4c19d2bcb39ec8f319ad6945 Mon Sep 17 00:00:00 2001
+From: "Marvin S. Addison"
+Date: Mon, 27 Jan 2020 14:39:35 -0500
+Subject: [PATCH] Address code review feedback points.
+
+---
+ src/main/java/org/cryptacular/CiphertextHeader.java | 6 +++---
+ .../java/org/cryptacular/CiphertextHeaderV2.java | 12 +++++++-----
+ src/main/java/org/cryptacular/util/CipherUtil.java | 1 -
+ 3 files changed, 10 insertions(+), 9 deletions(-)
+
+diff --git a/src/main/java/org/cryptacular/CiphertextHeader.java b/src/main/java/org/cryptacular/CiphertextHeader.java
+index c17e735..d43bf9a 100644
+--- a/src/main/java/org/cryptacular/CiphertextHeader.java
++++ b/src/main/java/org/cryptacular/CiphertextHeader.java
+@@ -75,12 +75,12 @@ public CiphertextHeader(final byte[] nonce)
+ */
+ public CiphertextHeader(final byte[] nonce, final String keyName)
+ {
+- if (nonce.length > 255) {
+- throw new IllegalArgumentException("Nonce exceeds size limit in bytes (255)");
++ if (nonce.length > MAX_NONCE_LEN) {
++ throw new IllegalArgumentException("Nonce exceeds size limit in bytes (" + MAX_NONCE_LEN + ")");
+ }
+ if (keyName != null) {
+ if (ByteUtil.toBytes(keyName).length > MAX_KEYNAME_LEN) {
+- throw new IllegalArgumentException("Key name exceeds size limit in bytes (500)");
++ throw new IllegalArgumentException("Key name exceeds size limit in bytes (" + MAX_KEYNAME_LEN + ")");
+ }
+ }
+ this.nonce = nonce;
+diff --git a/src/main/java/org/cryptacular/CiphertextHeaderV2.java b/src/main/java/org/cryptacular/CiphertextHeaderV2.java
+index 8119f4e..1fe095b 100644
+--- a/src/main/java/org/cryptacular/CiphertextHeaderV2.java
++++ b/src/main/java/org/cryptacular/CiphertextHeaderV2.java
+@@ -102,6 +102,9 @@ public void setKeyLookup(final Function keyLookup)
+ */
+ public byte[] encode(final SecretKey hmacKey)
+ {
++ if (hmacKey == null) {
++ throw new IllegalArgumentException("Secret key cannot be null");
++ }
+ final ByteBuffer bb = ByteBuffer.allocate(length);
+ bb.order(ByteOrder.BIG_ENDIAN);
+ bb.putInt(VERSION);
+@@ -109,10 +112,7 @@ public void setKeyLookup(final Function keyLookup)
+ bb.put((byte) 0);
+ bb.put(ByteUtil.toUnsignedByte(nonce.length));
+ bb.put(nonce);
+- if (hmacKey != null) {
+- final byte[] hmac = hmac(bb.array(), 0, bb.limit() - HMAC_SIZE);
+- bb.put(hmac);
+- }
++ bb.put(hmac(bb.array(), 0, bb.limit() - HMAC_SIZE));
+ return bb.array();
+ }
+
+@@ -253,8 +253,10 @@ public static CiphertextHeaderV2 decode(final InputStream input, final Function<
+ *
+ * @param input Input stream.
+ * @param output Output buffer.
++ *
++ * @throws StreamException on stream IO errors.
+ */
+- private static void readInto(final InputStream input, final byte[] output)
++ private static void readInto(final InputStream input, final byte[] output) throws StreamException
+ {
+ try {
+ input.read(output);
+diff --git a/src/main/java/org/cryptacular/util/CipherUtil.java b/src/main/java/org/cryptacular/util/CipherUtil.java
+index cdbac0d..40ef4d1 100644
+--- a/src/main/java/org/cryptacular/util/CipherUtil.java
++++ b/src/main/java/org/cryptacular/util/CipherUtil.java
+@@ -376,7 +376,6 @@ private static void process(final BlockCipherAdapter cipher, final InputStream i
+ }
+
+
+-
+ /**
+ * Writes a ciphertext header to the output stream.
+ *
diff --git a/backport-CVE-2020-7226-3.patch b/backport-CVE-2020-7226-3.patch
new file mode 100644
index 0000000..f4758e4
--- /dev/null
+++ b/backport-CVE-2020-7226-3.patch
@@ -0,0 +1,22 @@
+From 00395c232cdc62d4292ce27999c026fc1f076b1d Mon Sep 17 00:00:00 2001
+From: "Marvin S. Addison"
+Date: Wed, 29 Jan 2020 16:51:35 -0500
+Subject: [PATCH] Remove runtime exception from method sig.
+
+---
+ src/main/java/org/cryptacular/CiphertextHeaderV2.java | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/src/main/java/org/cryptacular/CiphertextHeaderV2.java b/src/main/java/org/cryptacular/CiphertextHeaderV2.java
+index 1fe095b..23d039e 100644
+--- a/src/main/java/org/cryptacular/CiphertextHeaderV2.java
++++ b/src/main/java/org/cryptacular/CiphertextHeaderV2.java
+@@ -256,7 +256,7 @@ public static CiphertextHeaderV2 decode(final InputStream input, final Function<
+ *
+ * @throws StreamException on stream IO errors.
+ */
+- private static void readInto(final InputStream input, final byte[] output) throws StreamException
++ private static void readInto(final InputStream input, final byte[] output)
+ {
+ try {
+ input.read(output);
diff --git a/change-version-to-Java8.patch b/change-version-to-Java8.patch
new file mode 100644
index 0000000..dc87b44
--- /dev/null
+++ b/change-version-to-Java8.patch
@@ -0,0 +1,37 @@
+From 1972c658289468599bbb832bad03fe0a5a34713d Mon Sep 17 00:00:00 2001
+From: zhanghua1831
+Date: Fri, 26 Feb 2021 12:33:02 +0800
+Subject: [PATCH] fix build error by using Java8
+
+changes of CVE-2020-7226's patches require Java8
+---
+ pom.xml | 6 +++---
+ 1 file changed, 3 insertions(+), 3 deletions(-)
+
+diff --git a/pom.xml b/pom.xml
+index 1f83d44..9506e54 100644
+--- a/pom.xml
++++ b/pom.xml
+@@ -140,8 +140,8 @@
+ true
+ true
+ -Xlint:unchecked
+- 1.7
+- 1.7
++ 1.8
++ 1.8
+
+
+
+@@ -182,7 +182,7 @@
+ 2.10.3
+
+
+- http://download.oracle.com/javase/7/docs/api
++ http://download.oracle.com/javase/8/docs/api
+
+ Copyright © 2003-2015 Virginia Tech. All Rights Reserved.]]>
+
+--
+2.23.0
+
diff --git a/cryptacular.spec b/cryptacular.spec
index b99cf18..83739fc 100644
--- a/cryptacular.spec
+++ b/cryptacular.spec
@@ -1,10 +1,14 @@
Name: cryptacular
Version: 1.1.0
-Release: 1
+Release: 2
Summary: Java Library that complement to the Bouncy Castle crypto API
License: ASL 2.0 or LGPLv3
URL: http://www.cryptacular.org/
Source0: https://github.com/vt-middleware/cryptacular/archive/v%{version}.tar.gz
+Patch0000: backport-CVE-2020-7226-1.patch
+Patch0001: backport-CVE-2020-7226-2.patch
+Patch0002: backport-CVE-2020-7226-3.patch
+Patch0003: change-version-to-Java8.patch
BuildRequires: maven-local mvn(org.apache.felix:maven-bundle-plugin)
BuildRequires: mvn(org.apache.maven.plugins:maven-assembly-plugin)
BuildRequires: mvn(org.apache.maven.plugins:maven-release-plugin)
@@ -27,7 +31,7 @@ Obsoletes: %{name}-javadoc < %{version}-%{release}
This package contains man pages and other related documents for %{name}.
%prep
-%setup -q -n %{name}-%{version}
+%autosetup -n %{name}-%{version} -p1
%pom_remove_plugin :maven-source-plugin
%pom_xpath_remove "pom:plugin[pom:artifactId = 'maven-javadoc-plugin']/pom:executions"
%pom_remove_plugin :maven-checkstyle-plugin
@@ -47,5 +51,8 @@ This package contains man pages and other related documents for %{name}.
%license LICENSE LICENSE-apache2 LICENSE-lgpl NOTICE
%changelog
+* Thu Feb 25 2021 zhanghua - 1.1.0-2
+- fix CVE-2020-7226 and fix build error by using Java8
+
* Fri Aug 14 2020 leiju - 1.1.0-1
- Package init