You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cayenne.apache.org by nt...@apache.org on 2017/03/31 13:03:46 UTC
cayenne git commit: CAY-2109 cayenne-crypto: add value authentication
(HMAC)
Repository: cayenne
Updated Branches:
refs/heads/master 2b6d3507f -> 4911ad11d
CAY-2109 cayenne-crypto: add value authentication (HMAC)
Project: http://git-wip-us.apache.org/repos/asf/cayenne/repo
Commit: http://git-wip-us.apache.org/repos/asf/cayenne/commit/4911ad11
Tree: http://git-wip-us.apache.org/repos/asf/cayenne/tree/4911ad11
Diff: http://git-wip-us.apache.org/repos/asf/cayenne/diff/4911ad11
Branch: refs/heads/master
Commit: 4911ad11de89ea4753d61ddba54375e7eb1530d3
Parents: 2b6d350
Author: Nikita Timofeev <st...@gmail.com>
Authored: Fri Mar 31 16:03:25 2017 +0300
Committer: Nikita Timofeev <st...@gmail.com>
Committed: Fri Mar 31 16:03:25 2017 +0300
----------------------------------------------------------------------
.../apache/cayenne/crypto/CryptoConstants.java | 24 ++++---
.../cayenne/crypto/CryptoModuleBuilder.java | 13 ++++
.../bytes/CbcBytesTransformerFactory.java | 5 +-
.../bytes/DefaultBytesTransformerFactory.java | 3 +-
.../crypto/transformer/bytes/Header.java | 20 +++++-
.../transformer/bytes/HeaderDecryptor.java | 7 +-
.../crypto/transformer/bytes/HmacCreator.java | 64 ++++++++++++++++++
.../crypto/transformer/bytes/HmacDecryptor.java | 59 +++++++++++++++++
.../crypto/transformer/bytes/HmacEncryptor.java | 47 +++++++++++++
.../value/DefaultValueDecryptor.java | 1 +
.../cayenne/crypto/Runtime_AES128_Base.java | 9 ++-
.../crypto/Runtime_AES128_GZIP_HMAC_IT.java | 34 ++++++++++
.../cayenne/crypto/Runtime_AES128_GZIP_IT.java | 2 +-
.../cayenne/crypto/Runtime_AES128_HMAC_IT.java | 34 ++++++++++
.../cayenne/crypto/Runtime_AES128_IT.java | 2 +-
.../cayenne/crypto/Runtime_LazyInit_IT.java | 2 +-
.../transformer/bytes/HeaderEncryptorTest.java | 2 +-
.../crypto/transformer/bytes/HeaderTest.java | 32 +++++++--
.../transformer/bytes/HmacCreatorTest.java | 70 ++++++++++++++++++++
.../transformer/bytes/HmacDecryptorTest.java | 56 ++++++++++++++++
.../transformer/bytes/HmacEncryptorTest.java | 53 +++++++++++++++
.../cayenne/crypto/unit/CryptoUnitUtils.java | 10 ++-
.../crypto/unit/SwapBytesTransformer.java | 4 +-
docs/doc/src/main/resources/RELEASE-NOTES.txt | 1 +
24 files changed, 524 insertions(+), 30 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/cayenne/blob/4911ad11/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/CryptoConstants.java
----------------------------------------------------------------------
diff --git a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/CryptoConstants.java b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/CryptoConstants.java
index f24934e..601c72a 100644
--- a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/CryptoConstants.java
+++ b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/CryptoConstants.java
@@ -28,40 +28,46 @@ public interface CryptoConstants {
/**
* An injection key for the Map<String, String> of the crypto properties.
*/
- public static final String PROPERTIES_MAP = "cayenne.crypto.properties";
+ String PROPERTIES_MAP = "cayenne.crypto.properties";
/**
* An injection key for the Map<String, char[]> of credentials.
*/
- public static final String CREDENTIALS_MAP = "cayenne.crypto.properties";
+ String CREDENTIALS_MAP = "cayenne.crypto.properties";
- public static final String CIPHER_ALGORITHM = "cayenne.crypto.cipher.algorithm";
+ String CIPHER_ALGORITHM = "cayenne.crypto.cipher.algorithm";
- public static final String CIPHER_MODE = "cayenne.crypto.cipher.mode";
+ String CIPHER_MODE = "cayenne.crypto.cipher.mode";
- public static final String CIPHER_PADDING = "cayenne.crypto.cipher.padding";
+ String CIPHER_PADDING = "cayenne.crypto.cipher.padding";
/**
* Defines a URL of a KeyStore. The actual format depends on the
* {@link KeySource} implementation that will be reading it. E.g. it can be
* a "jceks" Java key store.
*/
- public static final String KEYSTORE_URL = "cayenne.crypto.keystore.url";
+ String KEYSTORE_URL = "cayenne.crypto.keystore.url";
/**
* A password to access all secret keys within the keystore.
*/
- public static final String KEY_PASSWORD = "cayenne.crypto.key.password";
+ String KEY_PASSWORD = "cayenne.crypto.key.password";
/**
* A symbolic name of the default encryption key in the keystore.
*/
- public static final String ENCRYPTION_KEY_ALIAS = "cayenne.crypto.key.enc.alias";
+ String ENCRYPTION_KEY_ALIAS = "cayenne.crypto.key.enc.alias";
/**
* A property that defines whether compression is enabled. Should be "true"
* or "false". "False" is the default.
*/
- public static final String COMPRESSION = "cayenne.crypto.compression";
+ String COMPRESSION = "cayenne.crypto.compression";
+
+ /**
+ * A property that defines whether HMAC is enabled.
+ * Should be "true" or "false". "False" is the default.
+ */
+ String USE_HMAC = "cayenne.crypto.use_hmac";
}
http://git-wip-us.apache.org/repos/asf/cayenne/blob/4911ad11/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/CryptoModuleBuilder.java
----------------------------------------------------------------------
diff --git a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/CryptoModuleBuilder.java b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/CryptoModuleBuilder.java
index 62a8af2..d05ecba 100644
--- a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/CryptoModuleBuilder.java
+++ b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/CryptoModuleBuilder.java
@@ -67,6 +67,7 @@ public class CryptoModuleBuilder {
private char[] keyPassword;
private boolean compress;
+ private boolean useHMAC;
// use CryptoModule.builder() to create the builder...
protected CryptoModuleBuilder() {
@@ -214,6 +215,14 @@ public class CryptoModuleBuilder {
}
/**
+ * Enable authentication codes
+ */
+ public CryptoModuleBuilder useHMAC() {
+ this.useHMAC = true;
+ return this;
+ }
+
+ /**
* Produces a module that can be used to start Cayenne runtime.
*/
public Module build() {
@@ -246,6 +255,10 @@ public class CryptoModuleBuilder {
props.put(CryptoConstants.COMPRESSION, "true");
}
+ if (useHMAC) {
+ props.put(CryptoConstants.USE_HMAC, "true");
+ }
+
if (keyPassword != null) {
CryptoModule.contributeCredentials(binder).put(CryptoConstants.KEY_PASSWORD, keyPassword);
}
http://git-wip-us.apache.org/repos/asf/cayenne/blob/4911ad11/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/bytes/CbcBytesTransformerFactory.java
----------------------------------------------------------------------
diff --git a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/bytes/CbcBytesTransformerFactory.java b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/bytes/CbcBytesTransformerFactory.java
index bc38a51..c3059a2 100644
--- a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/bytes/CbcBytesTransformerFactory.java
+++ b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/bytes/CbcBytesTransformerFactory.java
@@ -42,7 +42,7 @@ class CbcBytesTransformerFactory implements BytesTransformerFactory {
CbcBytesTransformerFactory(CipherFactory cipherFactory, KeySource keySource, Header encryptionHeader) {
- this.randoms = new ConcurrentLinkedQueue<SecureRandom>();
+ this.randoms = new ConcurrentLinkedQueue<>();
this.keySource = keySource;
this.cipherFactory = cipherFactory;
@@ -93,6 +93,9 @@ class CbcBytesTransformerFactory implements BytesTransformerFactory {
if (encryptionHeader.isCompressed()) {
delegate = new GzipEncryptor(delegate);
}
+ if (encryptionHeader.haveHMAC()) {
+ delegate = new HmacEncryptor(delegate, encryptionHeader, key);
+ }
return new HeaderEncryptor(delegate, encryptionHeader);
}
http://git-wip-us.apache.org/repos/asf/cayenne/blob/4911ad11/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/bytes/DefaultBytesTransformerFactory.java
----------------------------------------------------------------------
diff --git a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/bytes/DefaultBytesTransformerFactory.java b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/bytes/DefaultBytesTransformerFactory.java
index 7a26a2a..ba8a0f0 100644
--- a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/bytes/DefaultBytesTransformerFactory.java
+++ b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/bytes/DefaultBytesTransformerFactory.java
@@ -38,7 +38,8 @@ public class DefaultBytesTransformerFactory implements BytesTransformerFactory {
static Header createEncryptionHeader(Map<String, String> properties, KeySource keySource) {
boolean compressed = "true".equals(properties.get(CryptoConstants.COMPRESSION));
- return Header.create(keySource.getDefaultKeyAlias(), compressed);
+ boolean useHMAC = "true".equals(properties.get(CryptoConstants.USE_HMAC));
+ return Header.create(keySource.getDefaultKeyAlias(), compressed, useHMAC);
}
public DefaultBytesTransformerFactory(@Inject(CryptoConstants.PROPERTIES_MAP) Map<String, String> properties,
http://git-wip-us.apache.org/repos/asf/cayenne/blob/4911ad11/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/bytes/Header.java
----------------------------------------------------------------------
diff --git a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/bytes/Header.java b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/bytes/Header.java
index a2baa76..605b304 100644
--- a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/bytes/Header.java
+++ b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/bytes/Header.java
@@ -74,10 +74,15 @@ public class Header {
*/
private static final int COMPRESS_BIT = 0;
+ /**
+ * A position if the HMAC bit
+ */
+ private static final int HMAC_BIT = 1;
+
private byte[] data;
private int offset;
- public static Header create(String keyName, boolean compessed) {
+ public static Header create(String keyName, boolean compressed, boolean withHMAC) {
byte[] keyNameBytes;
try {
keyNameBytes = keyName.getBytes(KEY_NAME_CHARSET);
@@ -99,9 +104,12 @@ public class Header {
data[SIZE_POSITION] = (byte) n;
// flags
- if (compessed) {
+ if (compressed) {
data[FLAGS_POSITION] = bitOn(data[FLAGS_POSITION], COMPRESS_BIT);
}
+ if (withHMAC) {
+ data[FLAGS_POSITION] = bitOn(data[FLAGS_POSITION], HMAC_BIT);
+ }
// key name
System.arraycopy(keyNameBytes, 0, data, KEY_NAME_OFFSET, keyNameBytes.length);
@@ -117,6 +125,10 @@ public class Header {
return compressed ? bitOn(bits, COMPRESS_BIT) : bitOff(bits, COMPRESS_BIT);
}
+ public static byte setHaveHMAC(byte bits, boolean haveHMAC) {
+ return haveHMAC ? bitOn(bits, HMAC_BIT) : bitOff(bits, HMAC_BIT);
+ }
+
private static byte bitOn(byte bits, int position) {
return (byte) (bits | (1 << position));
}
@@ -143,6 +155,10 @@ public class Header {
return isBitOn(getFlags(), COMPRESS_BIT);
}
+ public boolean haveHMAC() {
+ return isBitOn(getFlags(), HMAC_BIT);
+ }
+
public byte getFlags() {
return data[offset + FLAGS_POSITION];
}
http://git-wip-us.apache.org/repos/asf/cayenne/blob/4911ad11/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/bytes/HeaderDecryptor.java
----------------------------------------------------------------------
diff --git a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/bytes/HeaderDecryptor.java b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/bytes/HeaderDecryptor.java
index 072fd82..eed1e1d 100644
--- a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/bytes/HeaderDecryptor.java
+++ b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/bytes/HeaderDecryptor.java
@@ -45,9 +45,12 @@ class HeaderDecryptor implements BytesDecryptor {
// ignoring the parameter key... using the key from the first block
Key inRecordKey = keySource.getKey(header.getKeyName());
- // if compression was used to create a record, filter through
- // GzipDecryptor...
+ // if compression was used to create a record, filter through GzipDecryptor...
BytesDecryptor worker = header.isCompressed() ? decompressDelegate : delegate;
+ // if record has HMAC, create appropriate decryptor
+ if(header.haveHMAC()) {
+ worker = new HmacDecryptor(worker, header, inRecordKey);
+ }
return worker.decrypt(input, inputOffset + header.size(), inRecordKey);
}
http://git-wip-us.apache.org/repos/asf/cayenne/blob/4911ad11/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/bytes/HmacCreator.java
----------------------------------------------------------------------
diff --git a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/bytes/HmacCreator.java b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/bytes/HmacCreator.java
new file mode 100644
index 0000000..12565c0
--- /dev/null
+++ b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/bytes/HmacCreator.java
@@ -0,0 +1,64 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.crypto.transformer.bytes;
+
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.NoSuchAlgorithmException;
+import javax.crypto.Mac;
+
+import org.apache.cayenne.CayenneRuntimeException;
+
+/**
+ * Actual authentication code generation logic that is used
+ * by both {@link HmacEncryptor} and {@link HmacDecryptor}.
+ *
+ * @since 4.0
+ */
+abstract class HmacCreator {
+
+ /**
+ * Default algorithm for authentication code creation.
+ */
+ public static final String DEFAULT_HMAC_ALGORITHM = "HmacSHA256";
+
+ private Header header;
+ private Mac mac;
+
+ HmacCreator(Header header, Key key) {
+ this.header = header;
+ try {
+ // Currently algorithm is hardcoded, but can be easily transformed into configurable parameter
+ mac = Mac.getInstance(DEFAULT_HMAC_ALGORITHM);
+ mac.init(key);
+ } catch (NoSuchAlgorithmException nsae) {
+ throw new CayenneRuntimeException("Algorithm %s not supported for HMAC generation", nsae, DEFAULT_HMAC_ALGORITHM);
+ } catch (InvalidKeyException ike) {
+ throw new CayenneRuntimeException("Invalid key for HMAC generation", ike);
+ }
+ }
+
+ byte[] createHmac(byte[] input) {
+ byte[] rawHeader = new byte[header.size()];
+ header.store(rawHeader, 0, header.getFlags());
+ mac.update(rawHeader);
+ return mac.doFinal(input);
+ }
+}
http://git-wip-us.apache.org/repos/asf/cayenne/blob/4911ad11/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/bytes/HmacDecryptor.java
----------------------------------------------------------------------
diff --git a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/bytes/HmacDecryptor.java b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/bytes/HmacDecryptor.java
new file mode 100644
index 0000000..a6eeee8
--- /dev/null
+++ b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/bytes/HmacDecryptor.java
@@ -0,0 +1,59 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.crypto.transformer.bytes;
+
+import java.security.Key;
+import java.util.Arrays;
+
+import org.apache.cayenne.crypto.CayenneCryptoException;
+
+/**
+ * This class not only parse HMAC but also verifies it
+ * and throws {@link org.apache.cayenne.crypto.CayenneCryptoException} in case it is invalid.
+ *
+ * @since 4.0
+ */
+class HmacDecryptor extends HmacCreator implements BytesDecryptor {
+
+ BytesDecryptor delegate;
+
+ HmacDecryptor(BytesDecryptor delegate, Header header, Key key) {
+ super(header, key);
+ this.delegate = delegate;
+ }
+
+ @Override
+ public byte[] decrypt(byte[] input, int inputOffset, Key key) {
+ byte hmacLength = input[inputOffset++];
+ if(hmacLength <= 0) {
+ throw new CayenneCryptoException("Input is corrupted: invalid HMAC length.");
+ }
+
+ byte[] receivedHmac = new byte[hmacLength];
+ byte[] decrypted = delegate.decrypt(input, inputOffset + hmacLength, key);
+ byte[] realHmac = createHmac(decrypted);
+
+ System.arraycopy(input, inputOffset, receivedHmac, 0, hmacLength);
+ if(!Arrays.equals(receivedHmac, realHmac)) {
+ throw new CayenneCryptoException("Input is corrupted: wrong HMAC.");
+ }
+ return decrypted;
+ }
+}
http://git-wip-us.apache.org/repos/asf/cayenne/blob/4911ad11/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/bytes/HmacEncryptor.java
----------------------------------------------------------------------
diff --git a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/bytes/HmacEncryptor.java b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/bytes/HmacEncryptor.java
new file mode 100644
index 0000000..133e725
--- /dev/null
+++ b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/bytes/HmacEncryptor.java
@@ -0,0 +1,47 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.crypto.transformer.bytes;
+
+import java.security.Key;
+
+/**
+ * Encryptor that stores authentication code into output.
+ * HMAC is formed from full header concatenated with unencrypted input.
+ *
+ * @since 4.0
+ */
+class HmacEncryptor extends HmacCreator implements BytesEncryptor {
+
+ BytesEncryptor delegate;
+
+ HmacEncryptor(BytesEncryptor delegate, Header header, Key key) {
+ super(header, key);
+ this.delegate = delegate;
+ }
+
+ @Override
+ public byte[] encrypt(byte[] input, int outputOffset, byte[] flags) {
+ byte[] hmac = createHmac(input);
+ byte[] encrypted = delegate.encrypt(input, outputOffset + hmac.length + 1, flags);
+ encrypted[outputOffset++] = (byte)hmac.length; // store HMAC length
+ System.arraycopy(hmac, 0, encrypted, outputOffset, hmac.length);
+ return encrypted;
+ }
+}
http://git-wip-us.apache.org/repos/asf/cayenne/blob/4911ad11/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/value/DefaultValueDecryptor.java
----------------------------------------------------------------------
diff --git a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/value/DefaultValueDecryptor.java b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/value/DefaultValueDecryptor.java
index 09f0849..4a7e087 100644
--- a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/value/DefaultValueDecryptor.java
+++ b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/value/DefaultValueDecryptor.java
@@ -34,6 +34,7 @@ class DefaultValueDecryptor implements ValueDecryptor {
public DefaultValueDecryptor(BytesConverter preConverter, BytesConverter postConverter, Key defaultKey) {
this.preConverter = preConverter;
this.postConverter = postConverter;
+ this.defaultKey = defaultKey;
}
BytesConverter getPreConverter() {
http://git-wip-us.apache.org/repos/asf/cayenne/blob/4911ad11/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/Runtime_AES128_Base.java
----------------------------------------------------------------------
diff --git a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/Runtime_AES128_Base.java b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/Runtime_AES128_Base.java
index aea8775..8c5bfce 100644
--- a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/Runtime_AES128_Base.java
+++ b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/Runtime_AES128_Base.java
@@ -34,9 +34,9 @@ public class Runtime_AES128_Base {
protected TableHelper table2;
protected TableHelper table4;
- protected void setUp(boolean compress) throws Exception {
+ protected void setUp(boolean compress, boolean useHMAC) throws Exception {
- Module crypto = createCryptoModule(compress);
+ Module crypto = createCryptoModule(compress, useHMAC);
this.runtime = createRuntime(crypto);
setupTestTables(new DBHelper(runtime.getDataSource(null)));
@@ -59,7 +59,7 @@ public class Runtime_AES128_Base {
return ServerRuntime.builder().addConfig("cayenne-crypto.xml").addModule(crypto).build();
}
- protected Module createCryptoModule(boolean compress) {
+ protected Module createCryptoModule(boolean compress, boolean useHMAC) {
URL keyStoreUrl = JceksKeySourceTest.class.getResource(JceksKeySourceTest.KS1_JCEKS);
CryptoModuleBuilder builder = CryptoModule
@@ -69,6 +69,9 @@ public class Runtime_AES128_Base {
if (compress) {
builder.compress();
}
+ if(useHMAC) {
+ builder.useHMAC();
+ }
return builder.build();
}
http://git-wip-us.apache.org/repos/asf/cayenne/blob/4911ad11/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/Runtime_AES128_GZIP_HMAC_IT.java
----------------------------------------------------------------------
diff --git a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/Runtime_AES128_GZIP_HMAC_IT.java b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/Runtime_AES128_GZIP_HMAC_IT.java
new file mode 100644
index 0000000..5b19b42
--- /dev/null
+++ b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/Runtime_AES128_GZIP_HMAC_IT.java
@@ -0,0 +1,34 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.crypto;
+
+import org.junit.Before;
+
+/**
+ * @since 4.0
+ */
+public class Runtime_AES128_GZIP_HMAC_IT extends Runtime_AES128_GZIP_IT {
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp(true, true);
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/cayenne/blob/4911ad11/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/Runtime_AES128_GZIP_IT.java
----------------------------------------------------------------------
diff --git a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/Runtime_AES128_GZIP_IT.java b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/Runtime_AES128_GZIP_IT.java
index 1013c91..69acd55 100644
--- a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/Runtime_AES128_GZIP_IT.java
+++ b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/Runtime_AES128_GZIP_IT.java
@@ -43,7 +43,7 @@ public class Runtime_AES128_GZIP_IT extends Runtime_AES128_Base {
@Before
public void setUp() throws Exception {
- super.setUp(true);
+ super.setUp(true, false);
}
@Test
http://git-wip-us.apache.org/repos/asf/cayenne/blob/4911ad11/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/Runtime_AES128_HMAC_IT.java
----------------------------------------------------------------------
diff --git a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/Runtime_AES128_HMAC_IT.java b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/Runtime_AES128_HMAC_IT.java
new file mode 100644
index 0000000..1d53d37
--- /dev/null
+++ b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/Runtime_AES128_HMAC_IT.java
@@ -0,0 +1,34 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.crypto;
+
+import org.junit.Before;
+
+/**
+ * @since 4.0
+ */
+public class Runtime_AES128_HMAC_IT extends Runtime_AES128_IT {
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp(false, true);
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/cayenne/blob/4911ad11/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/Runtime_AES128_IT.java
----------------------------------------------------------------------
diff --git a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/Runtime_AES128_IT.java b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/Runtime_AES128_IT.java
index 82d2281..1e4b4bc 100644
--- a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/Runtime_AES128_IT.java
+++ b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/Runtime_AES128_IT.java
@@ -40,7 +40,7 @@ public class Runtime_AES128_IT extends Runtime_AES128_Base {
@Before
public void setUp() throws Exception {
- super.setUp(false);
+ super.setUp(false, false);
}
@Test
http://git-wip-us.apache.org/repos/asf/cayenne/blob/4911ad11/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/Runtime_LazyInit_IT.java
----------------------------------------------------------------------
diff --git a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/Runtime_LazyInit_IT.java b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/Runtime_LazyInit_IT.java
index bf79489..988a658 100644
--- a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/Runtime_LazyInit_IT.java
+++ b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/Runtime_LazyInit_IT.java
@@ -43,7 +43,7 @@ public class Runtime_LazyInit_IT extends Runtime_AES128_Base {
@Before
public void before() throws Exception {
- setUp(false);
+ setUp(false, false);
UNLOCKED = false;
}
http://git-wip-us.apache.org/repos/asf/cayenne/blob/4911ad11/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/transformer/bytes/HeaderEncryptorTest.java
----------------------------------------------------------------------
diff --git a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/transformer/bytes/HeaderEncryptorTest.java b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/transformer/bytes/HeaderEncryptorTest.java
index 2360f06..c1878cb 100644
--- a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/transformer/bytes/HeaderEncryptorTest.java
+++ b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/transformer/bytes/HeaderEncryptorTest.java
@@ -30,7 +30,7 @@ public class HeaderEncryptorTest {
@Test
public void testTransform() throws UnsupportedEncodingException {
- Header encryptionHeader = Header.create("mykey", false);
+ Header encryptionHeader = Header.create("mykey", false, false);
BytesEncryptor delegate = SwapBytesTransformer.encryptor();
http://git-wip-us.apache.org/repos/asf/cayenne/blob/4911ad11/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/transformer/bytes/HeaderTest.java
----------------------------------------------------------------------
diff --git a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/transformer/bytes/HeaderTest.java b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/transformer/bytes/HeaderTest.java
index 0249d28..7e5318f 100644
--- a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/transformer/bytes/HeaderTest.java
+++ b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/transformer/bytes/HeaderTest.java
@@ -34,14 +34,21 @@ public class HeaderTest {
assertEquals(1, Header.setCompressed((byte) 1, true));
assertEquals(0, Header.setCompressed((byte) 1, false));
+
+ assertEquals(2, Header.setHaveHMAC((byte) 0, true));
+ assertEquals(0, Header.setHaveHMAC((byte) 0, false));
+
+ assertEquals(3, Header.setHaveHMAC((byte) 3, true));
+ assertEquals(0, Header.setHaveHMAC((byte) 2, false));
}
@Test
public void testCreate_WithKeyName() {
- Header h1 = Header.create("bcd", false);
- Header h2 = Header.create("bc", true);
- Header h3 = Header.create("b", false);
+ Header h1 = Header.create("bcd", false, false);
+ Header h2 = Header.create("bc", true, false);
+ Header h3 = Header.create("b", false, false);
+ Header h4 = Header.create("e", false, true);
assertEquals("bcd", h1.getKeyName());
assertFalse(h1.isCompressed());
@@ -51,6 +58,11 @@ public class HeaderTest {
assertEquals("b", h3.getKeyName());
assertFalse(h3.isCompressed());
+ assertFalse(h3.haveHMAC());
+
+ assertEquals("e", h4.getKeyName());
+ assertFalse(h4.isCompressed());
+ assertTrue(h4.haveHMAC());
}
@Test(expected = CayenneCryptoException.class)
@@ -61,7 +73,7 @@ public class HeaderTest {
buf.append("a");
}
- Header.create(buf.toString(), false);
+ Header.create(buf.toString(), false, false);
}
@Test
@@ -75,5 +87,17 @@ public class HeaderTest {
Header h2 = Header.create(input2, 1);
assertEquals("abcd", h2.getKeyName());
assertTrue(h2.isCompressed());
+
+ byte[] input3 = { 0, 0, 'C', 'C', '1', 9, 2, 'a', 'b', 'c', 'd', 'e' };
+ Header h3 = Header.create(input3, 2);
+ assertEquals("abcd", h3.getKeyName());
+ assertFalse(h3.isCompressed());
+ assertTrue(h3.haveHMAC());
+
+ byte[] input4 = { 0, 0, 0, 'C', 'C', '1', 9, 3, 'a', 'b', 'c', 'd', 'e' };
+ Header h4 = Header.create(input4, 3);
+ assertEquals("abcd", h4.getKeyName());
+ assertTrue(h4.isCompressed());
+ assertTrue(h4.haveHMAC());
}
}
http://git-wip-us.apache.org/repos/asf/cayenne/blob/4911ad11/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/transformer/bytes/HmacCreatorTest.java
----------------------------------------------------------------------
diff --git a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/transformer/bytes/HmacCreatorTest.java b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/transformer/bytes/HmacCreatorTest.java
new file mode 100644
index 0000000..6c228b0
--- /dev/null
+++ b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/transformer/bytes/HmacCreatorTest.java
@@ -0,0 +1,70 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.crypto.transformer.bytes;
+
+import java.security.Key;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.apache.cayenne.crypto.unit.CryptoUnitUtils;
+import org.junit.Test;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyByte;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+/**
+ * @since 4.0
+ */
+public class HmacCreatorTest {
+
+ /**
+ * Sample output from https://en.wikipedia.org/wiki/Hash-based_message_authentication_code
+ */
+ @Test
+ public void createHmac() {
+ final byte[] headerData = "The quick".getBytes();
+ Header header = mock(Header.class);
+
+ doReturn(headerData.length).when(header).size();
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) throws Throwable {
+ byte[] input = (byte[])invocation.getArguments()[0];
+ System.arraycopy(headerData, 0, input, 0, headerData.length);
+ return null;
+ }
+ }).when(header).store(any(byte[].class), anyInt(), anyByte());
+
+ Key key = new SecretKeySpec("key".getBytes(), "AES");
+ HmacCreator creator = new HmacCreator(header, key) {};
+
+ byte[] hmac = creator.createHmac(" brown fox jumps over the lazy dog".getBytes());
+ byte[] hmacExpected = CryptoUnitUtils.hexToBytes("f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8");
+
+ assertArrayEquals(hmacExpected, hmac);
+ }
+
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/cayenne/blob/4911ad11/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/transformer/bytes/HmacDecryptorTest.java
----------------------------------------------------------------------
diff --git a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/transformer/bytes/HmacDecryptorTest.java b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/transformer/bytes/HmacDecryptorTest.java
new file mode 100644
index 0000000..ef155d4
--- /dev/null
+++ b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/transformer/bytes/HmacDecryptorTest.java
@@ -0,0 +1,56 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.crypto.transformer.bytes;
+
+import java.security.Key;
+
+import org.apache.cayenne.crypto.unit.SwapBytesTransformer;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * @since 4.0
+ */
+public class HmacDecryptorTest {
+
+ @Test
+ public void decrypt() throws Exception {
+ HmacDecryptor decryptor = mock(HmacDecryptor.class);
+ decryptor.delegate = SwapBytesTransformer.decryptor();
+ when(decryptor.createHmac(any(byte[].class))).thenReturn(new byte[]{0, 1, 2, 3, 4, 5, 6, 7});
+ when(decryptor.decrypt(any(byte[].class), anyInt(), any(Key.class))).thenCallRealMethod();
+
+ byte[] expectedResult = {-1, -2, -3};
+
+ byte[] input1 = {8, 0, 1, 2, 3, 4, 5, 6, 7, -3, -2, -1};
+ byte[] result1 = decryptor.decrypt(input1, 0, null);
+ assertArrayEquals(expectedResult, result1);
+
+ byte[] input2 = {0, 0, 0, 8, 0, 1, 2, 3, 4, 5, 6, 7, -3, -2, -1};
+ byte[] result2 = decryptor.decrypt(input2, 3, null);
+ assertArrayEquals(expectedResult, result2);
+ }
+
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/cayenne/blob/4911ad11/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/transformer/bytes/HmacEncryptorTest.java
----------------------------------------------------------------------
diff --git a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/transformer/bytes/HmacEncryptorTest.java b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/transformer/bytes/HmacEncryptorTest.java
new file mode 100644
index 0000000..a2db191
--- /dev/null
+++ b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/transformer/bytes/HmacEncryptorTest.java
@@ -0,0 +1,53 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.crypto.transformer.bytes;
+
+import org.apache.cayenne.crypto.unit.SwapBytesTransformer;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * @since 4.0
+ */
+public class HmacEncryptorTest {
+
+ @Test
+ public void encrypt() throws Exception {
+ HmacEncryptor encryptor = mock(HmacEncryptor.class);
+ encryptor.delegate = SwapBytesTransformer.encryptor();
+ when(encryptor.createHmac(any(byte[].class))).thenReturn(new byte[]{0, 1, 2, 3, 4, 5, 6, 7});
+ when(encryptor.encrypt(any(byte[].class), anyInt(), any(byte[].class))).thenCallRealMethod();
+
+ byte[] input = {-1, -2, -3};
+
+ byte[] result1 = encryptor.encrypt(input, 0, new byte[1]);
+ assertArrayEquals(new byte[]{8, 0, 1, 2, 3, 4, 5, 6, 7, -3, -2, -1}, result1);
+
+ byte[] result2 = encryptor.encrypt(input, 5, new byte[1]);
+ assertArrayEquals(new byte[]{0, 0, 0, 0, 0, 8, 0, 1, 2, 3, 4, 5, 6, 7, -3, -2, -1}, result2);
+
+ }
+
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/cayenne/blob/4911ad11/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/unit/CryptoUnitUtils.java
----------------------------------------------------------------------
diff --git a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/unit/CryptoUnitUtils.java b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/unit/CryptoUnitUtils.java
index 0f3e4d0..8bdc191 100644
--- a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/unit/CryptoUnitUtils.java
+++ b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/unit/CryptoUnitUtils.java
@@ -83,9 +83,15 @@ public class CryptoUnitUtils {
Header header = Header.create(source, 0);
+ int offset = header.size();
+ if(header.haveHMAC()) {
+ byte hmacLength = source[offset];
+ offset += hmacLength + 1;
+ }
+
int blockSize = decCipher.getBlockSize();
- byte[] ivBytes = Arrays.copyOfRange(source, header.size(), header.size() + blockSize);
- byte[] cipherText = Arrays.copyOfRange(source, header.size() + blockSize, source.length);
+ byte[] ivBytes = Arrays.copyOfRange(source, offset, offset + blockSize);
+ byte[] cipherText = Arrays.copyOfRange(source, offset + blockSize, source.length);
Key key = runtime.getInjector().getInstance(KeySource.class).getKey(header.getKeyName());
http://git-wip-us.apache.org/repos/asf/cayenne/blob/4911ad11/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/unit/SwapBytesTransformer.java
----------------------------------------------------------------------
diff --git a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/unit/SwapBytesTransformer.java b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/unit/SwapBytesTransformer.java
index ff7f47f..6ce0374 100644
--- a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/unit/SwapBytesTransformer.java
+++ b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/unit/SwapBytesTransformer.java
@@ -44,8 +44,8 @@ public class SwapBytesTransformer implements BytesEncryptor, BytesDecryptor {
@Override
public byte[] decrypt(byte[] input, int inputOffset, Key key) {
- byte[] output = new byte[input.length];
- System.arraycopy(input, inputOffset, output, 0, input.length);
+ byte[] output = new byte[input.length - inputOffset];
+ System.arraycopy(input, inputOffset, output, 0, input.length - inputOffset);
swap(output, 0, output.length - 1);
return output;
http://git-wip-us.apache.org/repos/asf/cayenne/blob/4911ad11/docs/doc/src/main/resources/RELEASE-NOTES.txt
----------------------------------------------------------------------
diff --git a/docs/doc/src/main/resources/RELEASE-NOTES.txt b/docs/doc/src/main/resources/RELEASE-NOTES.txt
index 2e54c9f..b07f2d0 100644
--- a/docs/doc/src/main/resources/RELEASE-NOTES.txt
+++ b/docs/doc/src/main/resources/RELEASE-NOTES.txt
@@ -14,6 +14,7 @@ Date:
Changes/New Features:
CAY-1873 Move DataDomain cache configuration from the Modeler and into DI
+CAY-2109 cayenne-crypto: add value authentication (HMAC)
CAY-2255 ObjectSelect improvement: columns as full entities
CAY-2258 DI: type-safe binding of List and Map
CAY-2266 Move EventBridge implementations into autoloadable modules