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:

-+ * -+ * -+ * -+ *

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