diff --git a/backport-CVE-2020-7226-1.patch b/backport-CVE-2020-7226-1.patch
deleted file mode 100644
index 679fffd..0000000
--- a/backport-CVE-2020-7226-1.patch
+++ /dev/null
@@ -1,1261 +0,0 @@
-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
deleted file mode 100644
index 7c602fb..0000000
--- a/backport-CVE-2020-7226-2.patch
+++ /dev/null
@@ -1,81 +0,0 @@
-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
deleted file mode 100644
index f4758e4..0000000
--- a/backport-CVE-2020-7226-3.patch
+++ /dev/null
@@ -1,22 +0,0 @@
-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
deleted file mode 100644
index dc87b44..0000000
--- a/change-version-to-Java8.patch
+++ /dev/null
@@ -1,37 +0,0 @@
-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 83739fc..292b6de 100644
--- a/cryptacular.spec
+++ b/cryptacular.spec
@@ -1,14 +1,10 @@
Name: cryptacular
-Version: 1.1.0
-Release: 2
+Version: 1.2.4
+Release: 1
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)
@@ -51,6 +47,9 @@ This package contains man pages and other related documents for %{name}.
%license LICENSE LICENSE-apache2 LICENSE-lgpl NOTICE
%changelog
+* Wed Dec 29 2021 wangkai - 1.2.4-1
+* Update to 1.2.4
+
* Thu Feb 25 2021 zhanghua - 1.1.0-2
- fix CVE-2020-7226 and fix build error by using Java8
diff --git a/v1.1.0.tar.gz b/v1.1.0.tar.gz
deleted file mode 100644
index 1cb3839..0000000
Binary files a/v1.1.0.tar.gz and /dev/null differ
diff --git a/v1.2.4.tar.gz b/v1.2.4.tar.gz
new file mode 100644
index 0000000..e211dcc
Binary files /dev/null and b/v1.2.4.tar.gz differ