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