Repository: wultra/powerauth-crypto (java)

Issue:
Crypto 3.1: Do not send CTR_DATA in status
The status blob should contain only a hash calculated from `CTR_DATA`, instead of the direct value of the counter:
```java
byte[] CalculateCounterHash(byte[] CTR_DATA) {
    SecretKey KEY_TRANSPORT_CTR = KDF.derive(KEY_TRANSPORT, 4000);
    byte[] CTR_DATA_HASH = KeyConversion.getBytes(KDF_INTERNAL.derive(KEY_TRANSPORT_CTR, CTR_DATA));
    return CTR_DATA_HASH;
}
```

Unfortunately, this change makes the current counter synchronization on mobile clients ineffective. To fix this, we should enhance the status blob with a limited information about the actual counter. Let's call it `CTR_INFO`:

```java
byte CTR_INFO = (byte) (CTR & 0xFF)
```

The mobile client then can synchronize its counter with using the following algorithm:

1. Initial values
   ```java
   byte   local_CTR_INFO = persistentData.CTR_INFO;
   byte[] local_CTR_DATA = persistentData.CTR_DATA;
   byte[] local_CTR_DATA_HASH = CalculateCounterHash(local_CTR_DATA);
   byte   server_CTR_INFO      = statusBlob.CTR_INFO;
   byte[] server_CTR_DATA_HASH = statusBlob.CTR_DATA_HASH;
   ```

1. Determine whether counter is already synchronized
   ```java
   if (local_CTR_INFO == server_CTR_INFO && 
       local_CTR_DATA_HASH.equals(server_CTR_DATA_HASH)) {
       // counter is synchronized, do nothing else...
       return OK;
   }
   ```

1. Determine whether server is ahead:
   ```java
   // This is equal to current impl. We're trying to move local counter close to the server's, 
   // but instead of comparing CTR_DATA, we have to compare CTR_DATA_HASH
   int attempts = 0;
   while (true) {
       // calculate next CTR_DATA and CTR_INFO
       local_CTR_DATA = SHA256(local_CTR_DATA);
       local_CTR_INFO = local_CTR_INFO + 1;
       // calculate next local_CTR_DATA_HASH
       local_CTR_DATA_HASH = CalculateCounterHash(local_CTR_DATA);
       if (local_CTR_DATA_HASH.equals(server_CTR_DATA_HASH) && 
           local_CTR_INFO == server_CTR_INFO) {
           // Hash & Counter equals
           // Keep local_CTR_INFO and local_CTR_DATA in persistent data
           return OK;
       }
       if (++attempts == statusBlob.CTR_LOOK_AHEAD) {
           // We cannot match 
           break;
       }
   }
   ```
   
1. Determine whether client is ahead:
   ```cpp
   // Note that byte is unsigned char. 
   int CalculateCounterDistance(byte local, byte server)
   {
       int L = local;
       int S = server;
       // Calculate possible distances
       int d1 = L - S;
       int d2 = 256 + L - S;
       int d3 = L - (256 + S);
       // Find minimum absolute distance from possible distances
       int d1a = abs(d1);
       int d2a = abs(d2);
       int d3a = abs(d3);
       int distance_abs = min(d1a, min(d2a, d3a));
       // Determine which one is it.
       if (distance_abs == d1a) {
           return d1;
       } else if (distance_abs == d2a) {
           return d2;
       }
       return d3;
   }
   
   int distance = CalculateCounterDistance(local_CTR_INFO, server_CTR_INFO);
   if (distance > 0 && distance < statusBlob.CTR_LOOK_AHEAD) {
       // Client can be synchronized by calculating signature on the server.
       return CALCULATE_SIGN;
   }
   // Client cannot be synchronized.
   return DEADLOCK;
   ```

Pull request description:
Fix #308: Plain CTR_DATA is no longer in status.

Patch:
diff --git a/docs/Activation-Status.md b/docs/Activation-Status.md
index 671b87ca..d52ea24f 100644
--- a/docs/Activation-Status.md
+++ b/docs/Activation-Status.md
@@ -43,7 +43,7 @@ Following sequence diagram shows the activation status check in more detail.
 When obtaining the activation status, application receives the binary status blob. Structure of the 32B long status blob is following:
 
 ```
-0xDEC0DED1 1B:${STATUS} 1B:${CURRENT_VERSION} 1B:${UPGRADE_VERSION} 6B:${RESERVED} 1B:${FAIL_COUNT} 1B:${MAX_FAIL_COUNT} 1B:${CTR_LOOK_AHEAD} 16B:${CTR_DATA}
+0xDEC0DED1 1B:${STATUS} 1B:${CURRENT_VERSION} 1B:${UPGRADE_VERSION} 5B:${RESERVED} 1B:${CTR_BYTE} 1B:${FAIL_COUNT} 1B:${MAX_FAIL_COUNT} 1B:${CTR_LOOK_AHEAD} 16B:${CTR_DATA}
 ```
 
 where:
@@ -60,9 +60,17 @@ where:
     - `0x02` - PowerAuth protocol version `2.x`
     - `0x03` - PowerAuth protocol version `3.x`
 - `${UPGRADE_VERSION}` - 1 byte representing maximum protocol version supported by the PowerAuth Server. The set of possible values is identical to `${CURRENT_VERSION}`
-- `${RESERVED}` - 6 bytes reserved for the future use.
+- `${RESERVED}` - 5 bytes reserved for the future use.
+- `${CTR_BYTE}` - 1 byte representing least significant byte from current value of counter, calculated as:
+    ```java
+    byte CTR_BYTE = (byte)(CTR & 0xFF);
+    ```
 - `${FAIL_COUNT}` - 1 byte representing information about the number of failed attempts at the moment.
 - `${MAX_FAIL_COUNT}` - 1 byte representing information about the maximum allowed number of failed attempts.
 - `${CTR_LOOK_AHEAD}` - 1 byte representing constant for a look ahead window, used on the server to validate the signature.
-- `${CTR_DATA}` - 16 bytes containing current value of hash-based counter.
+- `${CTR_DATA_HASH}` - 16 bytes containing hash from current value of hash-based counter:
+    ```java
+    SecretKey KEY_TRANSPORT_CTR = KDF.derive(KEY_TRANSPORT, 4000);
+    byte[] CTR_DATA_HASH = KeyConversion.getBytes(KDF_INTERNAL.derive(KEY_TRANSPORT_CTR, CTR_DATA));
+    ```
 
diff --git a/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/client/activation/PowerAuthClientActivation.java b/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/client/activation/PowerAuthClientActivation.java
index aee08939..8d7c6173 100644
--- a/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/client/activation/PowerAuthClientActivation.java
+++ b/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/client/activation/PowerAuthClientActivation.java
@@ -269,7 +269,7 @@ public class PowerAuthClientActivation {
 
         // Decrypt the status blob
         AESEncryptionUtils aes = new AESEncryptionUtils();
-        byte[] iv = new KeyDerivationUtils(keyGenerator).deriveIvForStatusBlobEncryption(challenge, nonce, transportKey);
+        byte[] iv = new KeyDerivationUtils().deriveIvForStatusBlobEncryption(challenge, nonce, transportKey);
         byte[] statusBlob = aes.decrypt(cStatusBlob, iv, transportKey, "AES/CBC/NoPadding");
 
         // Prepare objects to read status info into
@@ -289,6 +289,9 @@ public class PowerAuthClientActivation {
         // fetch the upgrade version status byte
         statusInfo.setUpgradeVersion(buffer.get(6));
 
+        // fetch ctr byte value
+        statusInfo.setCtrByte(buffer.get(12));
+
         // fetch the failed attempt count
         statusInfo.setFailedAttempts(buffer.get(13));
 
@@ -300,9 +303,26 @@ public class PowerAuthClientActivation {
 
         // extract counter data from second half of status blob
         byte[] ctrData = Arrays.copyOfRange(statusBlob, 16, 32);
-        statusInfo.setCtrData(ctrData);
+        statusInfo.setCtrDataHash(ctrData);
 
         return statusInfo;
     }
 
+    /**
+     * Verify whether client's value of hash based counter is equal to the value received from the server. The value
+     * received from the server is already hashed, so the function has to calculate hash from the client's counter
+     * and then compare both values.
+     *
+     * @param receivedCtrDataHash Value received from the server, containing hash, calculated from hash based counter.
+     * @param expectedCtrData Expected hash based counter.
+     * @param transportKey Transport key.
+     * @return {@code true} in case that received hash equals to hash calculated from counter data.
+     * @throws InvalidKeyException When invalid key is provided.
+     * @throws GenericCryptoException In case key derivation fails.
+     * @throws CryptoProviderException In case cryptography provider is incorrectly initialized.
+     */
+    public boolean verifyHashForHashBasedCounter(byte[] receivedCtrDataHash, byte[] expectedCtrData, SecretKey transportKey)
+            throws CryptoProviderException, InvalidKeyException, GenericCryptoException {
+        return new HashBasedCounterUtils().verifyHashForHashBasedCounter(receivedCtrDataHash, expectedCtrData, transportKey);
+    }
 }
diff --git a/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/lib/model/ActivationStatusBlobInfo.java b/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/lib/model/ActivationStatusBlobInfo.java
index b08d1978..0398242f 100644
--- a/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/lib/model/ActivationStatusBlobInfo.java
+++ b/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/lib/model/ActivationStatusBlobInfo.java
@@ -34,7 +34,8 @@ public class ActivationStatusBlobInfo {
     private byte failedAttempts;
     private byte maxFailedAttempts;
     private byte ctrLookAhead;
-    private byte[] ctrData;
+    private byte ctrByte;
+    private byte[] ctrDataHash;
 
     /**
      * Return true in case the parsed data was valid (correctly decrypted using transport key), false otherwise.
@@ -150,18 +151,34 @@ public class ActivationStatusBlobInfo {
     }
 
     /**
-     * Get counter data.
-     * @return Counter data.
+     * Get least significant byte from the counter.
+     * @return Least significant byte from the counter.
      */
-    public byte[] getCtrData() {
-        return ctrData;
+    public byte getCtrByte() {
+        return ctrByte;
     }
 
     /**
-     * Set counter data.
-     * @param ctrData Counter data.
+     * Set least significant byte from the counter.
+     * @param ctrByte Least significant byte from the counter.
      */
-    public void setCtrData(byte[] ctrData) {
-        this.ctrData = ctrData;
+    public void setCtrByte(byte ctrByte) {
+        this.ctrByte = ctrByte;
+    }
+
+    /**
+     * Get counter data hash.
+     * @return Counter data hash.
+     */
+    public byte[] getCtrDataHash() {
+        return ctrDataHash;
+    }
+
+    /**
+     * Set counter data hash.
+     * @param ctrDataHash Counter data.
+     */
+    public void setCtrDataHash(byte[] ctrDataHash) {
+        this.ctrDataHash = ctrDataHash;
     }
 }
diff --git a/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/lib/util/HashBasedCounterUtils.java b/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/lib/util/HashBasedCounterUtils.java
new file mode 100644
index 00000000..ec62ece9
--- /dev/null
+++ b/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/lib/util/HashBasedCounterUtils.java
@@ -0,0 +1,109 @@
+/*
+ * PowerAuth Crypto Library
+ * Copyright 2019 Wultra s.r.o.
+ *
+ * Licensed 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 io.getlime.security.powerauth.crypto.lib.util;
+
+import io.getlime.security.powerauth.crypto.lib.config.PowerAuthConfiguration;
+import io.getlime.security.powerauth.crypto.lib.generator.KeyGenerator;
+import io.getlime.security.powerauth.crypto.lib.model.exception.GenericCryptoException;
+import io.getlime.security.powerauth.provider.exception.CryptoProviderException;
+
+import javax.crypto.SecretKey;
+import java.nio.ByteBuffer;
+import java.security.InvalidKeyException;
+import java.util.Arrays;
+
+/**
+ * The {@code HashBasedCounterUtils} class provides additional functionality that allows secure transfer
+ * and verification of hash based counter over the network.
+ */
+public class HashBasedCounterUtils {
+
+    private final KeyGenerator keyGenerator = new KeyGenerator();
+
+    /**
+     * Derivation index used to derive KEY_TRANSPORT_CTR from KEY_TRANSPORT
+     */
+    private static final long STATUS_BLOB_TRANSPORT_CTR_INDEX = 4000;
+    /**
+     * Number of bytes allocated for KEY_TRANSPORT_CTR.
+     */
+    private static final int STATUS_BLOB_TRANSPORT_CTR_LENGTH = 16;
+    /**
+     * Number of bytes expected for CTR_DATA
+     */
+    private static final int CTR_DATA_LENGTH = 16;
+
+    /**
+     * Calculate hash from value representing the hash based counter. HMAC-SHA256 is currently used as a hashing
+     * function.
+     *
+     * @param ctrData Hash-based counter.
+     * @param transportKey Transport key.
+     * @return Hash calculated from provided hash-based counter.
+     * @throws GenericCryptoException In case that key derivation fails or you provided invalid ctrData.
+     * @throws CryptoProviderException In case cryptography provider is incorrectly initialized.
+     * @throws InvalidKeyException In case that transport key is not valid.
+     */
+    public byte[] calculateHashFromHashBasedCounter(byte[] ctrData, SecretKey transportKey)
+            throws CryptoProviderException, InvalidKeyException, GenericCryptoException {
+        if (ctrData == null || ctrData.length != CTR_DATA_LENGTH) {
+            throw new GenericCryptoException("Invalid ctrData provided");
+        }
+        if (transportKey == null) {
+            throw new GenericCryptoException("Invalid transport key");
+        }
+        // Derive KEY_TRANSPORT_CTR from KEY_TRANSPORT
+        final byte[] derivationIndex = ByteBuffer.allocate(STATUS_BLOB_TRANSPORT_CTR_LENGTH)
+                .putLong(0L)
+                .putLong(STATUS_BLOB_TRANSPORT_CTR_INDEX)
+                .array();
+        final SecretKey transportCtr = keyGenerator.deriveSecretKey(transportKey, derivationIndex);
+        // Derive CTR_DATA_HASH from KEY_TRANSPORT_CTR and CTR_DATA
+        final SecretKey ctrDataHashKey = keyGenerator.deriveSecretKeyHmac(transportCtr, ctrData);
+        return PowerAuthConfiguration.INSTANCE.getKeyConvertor().convertSharedSecretKeyToBytes(ctrDataHashKey);
+    }
+
+    /**
+     * Verify whether client's value of hash based counter is equal to the value received from the server. The value
+     * received from the server is already hashed, so the function has to calculate hash from the client's counter
+     * and then compare both values.
+     *
+     * @param receivedCtrDataHash Value received from the server, containing hash, calculated from hash based counter.
+     * @param expectedCtrData Expected hash based counter.
+     * @param transportKey Transport key.
+     * @return {@code true} in case that received hash equals to hash calculated from counter data.
+     * @throws InvalidKeyException When invalid key is provided.
+     * @throws GenericCryptoException In case key derivation fails.
+     * @throws CryptoProviderException In case cryptography provider is incorrectly initialized.
+     */
+    public boolean verifyHashForHashBasedCounter(byte[] receivedCtrDataHash, byte[] expectedCtrData, SecretKey transportKey)
+            throws CryptoProviderException, InvalidKeyException, GenericCryptoException {
+        if (expectedCtrData == null || expectedCtrData.length != CTR_DATA_LENGTH) {
+            throw new GenericCryptoException("Invalid expected counter data");
+        }
+        if (receivedCtrDataHash == null || receivedCtrDataHash.length != CTR_DATA_LENGTH) {
+            throw new GenericCryptoException("Invalid received counter data hash");
+        }
+        if (transportKey == null) {
+            throw new GenericCryptoException("Invalid transport key");
+        }
+        // Calculate hash from current hash based counter
+        final byte[] expectedCtrDataHash = calculateHashFromHashBasedCounter(expectedCtrData, transportKey);
+        // Compare both hashed values
+        return Arrays.equals(expectedCtrDataHash, receivedCtrDataHash);
+    }
+}
diff --git a/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/lib/util/KeyDerivationUtils.java b/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/lib/util/KeyDerivationUtils.java
index 4bdb6a76..e9656b80 100644
--- a/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/lib/util/KeyDerivationUtils.java
+++ b/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/lib/util/KeyDerivationUtils.java
@@ -31,14 +31,7 @@ import java.security.InvalidKeyException;
  */
 public class KeyDerivationUtils {
 
-    private final KeyGenerator keyGenerator;
-
-    /**
-     * @param keyGenerator {@link KeyGenerator} instance to be used in the class.
-     */
-    public KeyDerivationUtils(KeyGenerator keyGenerator) {
-        this.keyGenerator = keyGenerator;
-    }
+    private final KeyGenerator keyGenerator = new KeyGenerator();
 
     /**
      * Derivation index used to derive KEY_TRANSPORT_IV from KEY_TRANSPORT
diff --git a/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/server/activation/PowerAuthServerActivation.java b/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/server/activation/PowerAuthServerActivation.java
index 695abf4b..06ca07ba 100644
--- a/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/server/activation/PowerAuthServerActivation.java
+++ b/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/server/activation/PowerAuthServerActivation.java
@@ -238,6 +238,7 @@ public class PowerAuthServerActivation {
 
     /**
      * Returns an encrypted status blob as described in PowerAuth Specification.
+     *
      * @param statusBlobInfo {@link ActivationStatusBlobInfo} object with activation status data to be encrypted.
      * @param challenge Challenge for activation status blob encryption. If non-null, then also {@code nonce} parameter must be provided.
      * @param nonce Nonce for activation status blob encryption. If non-null, then also {@code challenge} parameter must be provided.
@@ -256,25 +257,67 @@ public class PowerAuthServerActivation {
         if (transportKey == null) {
             throw new GenericCryptoException("Required transportKey parameter is missing");
         }
-        if (statusBlobInfo.getCtrData() == null) {
-            throw new GenericCryptoException("Missing ctrData in statusBlobInfo object");
+        // Prepare variables that has different meaning, depended on the protocol version.
+        final byte[] reserved;
+        final byte[] ctrDataHash;
+        final byte ctrByte;
+        final byte ctrLookAhead;
+        if (challenge != null) {
+            // Protocol V3.1+, use values provided in the status blob info object.
+            if (statusBlobInfo.getCtrDataHash() == null) {
+                throw new GenericCryptoException("Missing ctrDataHash in statusBlobInfo object");
+            }
+            reserved = keyGenerator.generateRandomBytes(5);
+            ctrDataHash = statusBlobInfo.getCtrDataHash();
+            ctrByte = statusBlobInfo.getCtrByte();
+            ctrLookAhead = statusBlobInfo.getCtrLookAhead();
+        } else {
+            // Legacy protocol versions (2.x, 3.0)
+            //
+            // In this case, ctrDataHash, ctrInfo, ctrLookAhead should be completely random values, because
+            // mobile clients don't use them. The older protocols also use zero-IV for the encryption, so the first
+            // block encrypted by AES should have as much entropy as possible.
+            //
+            final byte[] randomBytes = keyGenerator.generateRandomBytes(5 + 2 + 16);
+            reserved = Arrays.copyOf(randomBytes, 5);
+            ctrDataHash = Arrays.copyOfRange(randomBytes, 5 + 2, 5 + 2 + 16);
+            ctrByte = randomBytes[5];
+            ctrLookAhead = randomBytes[6];
         }
-        final byte[] iv = new KeyDerivationUtils(keyGenerator).deriveIvForStatusBlobEncryption(challenge, nonce, transportKey);
-        final byte[] randomBytes = keyGenerator.generateRandomBytes(6);
+        // Prepare status blob data.
         final byte[] statusBlob = ByteBuffer.allocate(32)
                 .putInt(ActivationStatusBlobInfo.ACTIVATION_STATUS_MAGIC_VALUE)     // 4 bytes
                 .put(statusBlobInfo.getActivationStatus())   // 1 byte
                 .put(statusBlobInfo.getCurrentVersion())     // 1 byte
                 .put(statusBlobInfo.getUpgradeVersion())     // 1 byte
-                .put(randomBytes)                            // 6 bytes
+                .put(reserved)                               // 5 bytes
+                .put(ctrByte)                                // 1 byte
                 .put(statusBlobInfo.getFailedAttempts())     // 1 byte
                 .put(statusBlobInfo.getMaxFailedAttempts())  // 1 byte
-                .put(statusBlobInfo.getCtrLookAhead())       // 1 byte
-                .put(statusBlobInfo.getCtrData())            // 16 bytes
+                .put(ctrLookAhead)                           // 1 byte
+                .put(ctrDataHash)                            // 16 bytes
                 .array();
+        // Derive IV and encrypt status blob data.
+        final byte[] iv = new KeyDerivationUtils().deriveIvForStatusBlobEncryption(challenge, nonce, transportKey);
         return new AESEncryptionUtils().encrypt(statusBlob, iv, transportKey, "AES/CBC/NoPadding");
     }
 
+    /**
+     * Calculate hash from value representing the hash based counter. HMAC-SHA256 is currently used as a hashing
+     * function.
+     *
+     * @param ctrData Hash-based counter.
+     * @param transportKey Transport key.
+     * @return Hash calculated from provided hash-based counter.
+     * @throws GenericCryptoException In case that key derivation fails or you provided invalid ctrData.
+     * @throws CryptoProviderException In case cryptography provider is incorrectly initialized.
+     * @throws InvalidKeyException In case that transport key is not valid.
+     */
+    public byte[] calculateHashFromHashBasedCounter(byte[] ctrData, SecretKey transportKey)
+            throws CryptoProviderException, InvalidKeyException, GenericCryptoException {
+        return new HashBasedCounterUtils().calculateHashFromHashBasedCounter(ctrData, transportKey);
+    }
+
     /**
      * Compute an activation ID and encrypted server public key signature
      * using the Master Private Key.


Test patch:
diff --git a/powerauth-docs/test-vectors/activation-status-blob-data.json b/powerauth-docs/test-vectors/activation-status-blob-data.json
new file mode 100644
index 00000000..4ad524b9
--- /dev/null
+++ b/powerauth-docs/test-vectors/activation-status-blob-data.json
@@ -0,0 +1,156 @@
+{
+  "description" : "Status blob test data.",
+  "data" : [ {
+    "input" : {
+      "transportKey" : "gXqfNj6hC8yMlVpDET4S5Q==",
+      "challenge" : "h9ZX6Xjunqly71KgfgorRQ==",
+      "nonce" : "MtfHnxCDmJuuejhSOgM9Yg==",
+      "ctrData" : "hkIpYfIqQsMrj1Nbuh/BbA==",
+      "encryptedStatusBlob" : "ldIgTphu1GlOHhnY7GbZD6oub8N4KXOqfay41zrMxTU="
+    },
+    "output" : {
+      "activationStatus" : "2",
+      "currentVersion" : "2",
+      "upgradeVersion" : "3",
+      "failedAttempts" : "0",
+      "maxFailedAttempts" : "5",
+      "ctrLookAhead" : "20",
+      "ctrByte" : "1",
+      "ctrDataHash" : "c25pnWvjJTzl4Kv3McaGkA==",
+      "counterDistance" : "0"
+    }
+  }, {
+    "input" : {
+      "transportKey" : "WxXuivtAXftYrynUWg30Qg==",
+      "challenge" : "LhIFvNQHSxOQopRkZi+fnQ==",
+      "nonce" : "FaWmhpUOZjqB+5F63gDCOw==",
+      "ctrData" : "GPkNk4HviJVcdLhydCQaqg==",
+      "encryptedStatusBlob" : "HL8o9m2yOz37lSg4KaUUOYhmu/5ZbSh4gOWAK7SCp2k="
+    },
+    "output" : {
+      "activationStatus" : "3",
+      "currentVersion" : "3",
+      "upgradeVersion" : "3",
+      "failedAttempts" : "0",
+      "maxFailedAttempts" : "5",
+      "ctrLookAhead" : "33",
+      "ctrByte" : "13",
+      "ctrDataHash" : "8ucL70oYQuQFv8hR/R1oNA==",
+      "counterDistance" : "30"
+    }
+  }, {
+    "input" : {
+      "transportKey" : "so9FkduOZnByMtZFPXUotA==",
+      "challenge" : "F85MRfV68PsK1lInBGOtqg==",
+      "nonce" : "poQievUB+cPhRvTRZlNRDw==",
+      "ctrData" : "wGnsC1qaUfoxo/FMfFkT/g==",
+      "encryptedStatusBlob" : "H69FpaV1XceeBOTt3EuHG/n2cnpzMa1lpu5UyFb/iKQ="
+    },
+    "output" : {
+      "activationStatus" : "4",
+      "currentVersion" : "3",
+      "upgradeVersion" : "3",
+      "failedAttempts" : "1",
+      "maxFailedAttempts" : "5",
+      "ctrLookAhead" : "20",
+      "ctrByte" : "133",
+      "ctrDataHash" : "81tzkHEOyDPjlbLBovUBtg==",
+      "counterDistance" : "0"
+    }
+  }, {
+    "input" : {
+      "transportKey" : "7DBv+MtW9TTQyRSRQp16pQ==",
+      "challenge" : "vmtyoUP7v3LhfCFtMaAbYg==",
+      "nonce" : "0cVyYu5vMwNOOS2VqEhlQA==",
+      "ctrData" : "zPJfgBzVT4XduxtF3OB00Q==",
+      "encryptedStatusBlob" : "QINRZzV77AiWfGLq7O5Iwkg2xffC65qJSGdwdd4lbUE="
+    },
+    "output" : {
+      "activationStatus" : "5",
+      "currentVersion" : "3",
+      "upgradeVersion" : "3",
+      "failedAttempts" : "0",
+      "maxFailedAttempts" : "10",
+      "ctrLookAhead" : "20",
+      "ctrByte" : "63",
+      "ctrDataHash" : "gV+DVeHsQvGO9l8XpexxLw==",
+      "counterDistance" : "0"
+    }
+  }, {
+    "input" : {
+      "transportKey" : "zQrtuT15KtVQAru3v4Ga5g==",
+      "challenge" : "unKuSbe7uROEXlatacy2Ww==",
+      "nonce" : "vVKBCcc4aR3YDk/+3mRTwQ==",
+      "ctrData" : "fVC885q139++PuAgPFHu6Q==",
+      "encryptedStatusBlob" : "XwZDvKlTA4EkyrS9mHC6mSOUrHLJNDgWIObCG0Zk2Vk="
+    },
+    "output" : {
+      "activationStatus" : "3",
+      "currentVersion" : "3",
+      "upgradeVersion" : "3",
+      "failedAttempts" : "5",
+      "maxFailedAttempts" : "10",
+      "ctrLookAhead" : "20",
+      "ctrByte" : "127",
+      "ctrDataHash" : "UKDfGvI8AXYgL0x2+5Ggeg==",
+      "counterDistance" : "0"
+    }
+  }, {
+    "input" : {
+      "transportKey" : "2VWsTkyPw2Aq9HXkBq77+Q==",
+      "challenge" : "Mg34CI6vbPh28tVTLSSdcQ==",
+      "nonce" : "gwsI6CAXPe7hYE4kWpc3+Q==",
+      "ctrData" : "wpL9bF0YLwR377NhwqFQ8w==",
+      "encryptedStatusBlob" : "A83lNiJzUYzz3COZNbqqrGO+G4t4aH6mg6Vi8tNzBX8="
+    },
+    "output" : {
+      "activationStatus" : "3",
+      "currentVersion" : "3",
+      "upgradeVersion" : "3",
+      "failedAttempts" : "6",
+      "maxFailedAttempts" : "6",
+      "ctrLookAhead" : "20",
+      "ctrByte" : "253",
+      "ctrDataHash" : "1yv/yiv5LKGrPhaqPyZRdw==",
+      "counterDistance" : "0"
+    }
+  }, {
+    "input" : {
+      "transportKey" : "abddUTRgKu4tRyCtWXVrhg==",
+      "challenge" : "CIlHXaGx9tskubTBtQo27g==",
+      "nonce" : "39VJXfdqe1mweCqJSf6SLg==",
+      "ctrData" : "t8vgsV4vLhfgSuVj243bFw==",
+      "encryptedStatusBlob" : "GvZ4fByaL0Lbsx3Mt4uyKxv3KqokEwkAIYvg+x1yni4="
+    },
+    "output" : {
+      "activationStatus" : "4",
+      "currentVersion" : "3",
+      "upgradeVersion" : "3",
+      "failedAttempts" : "0",
+      "maxFailedAttempts" : "5",
+      "ctrLookAhead" : "20",
+      "ctrByte" : "128",
+      "ctrDataHash" : "HI2M1kUlJy6HwdvoHT7/Xg==",
+      "counterDistance" : "4"
+    }
+  }, {
+    "input" : {
+      "transportKey" : "ETgxJRh3C5pCtbAETc75KQ==",
+      "challenge" : "dn7W9vC0WgTBMnip5WbUHA==",
+      "nonce" : "NEk+kNUvjeq6XuNez7qm8Q==",
+      "ctrData" : "iMaVG4sGCx7SnW7rRYKLtQ==",
+      "encryptedStatusBlob" : "QKzaedpfAYIZ4Xkl+tN/fT8mzxo03xz6p7bgXmCRcCQ="
+    },
+    "output" : {
+      "activationStatus" : "5",
+      "currentVersion" : "3",
+      "upgradeVersion" : "3",
+      "failedAttempts" : "0",
+      "maxFailedAttempts" : "5",
+      "ctrLookAhead" : "20",
+      "ctrByte" : "77",
+      "ctrDataHash" : "41pS6JhBPNj/PNIG5PMDGg==",
+      "counterDistance" : "1"
+    }
+  } ]
+}
\ No newline at end of file
diff --git a/powerauth-java-crypto/src/test/java/io/getlime/security/powerauth/crypto/activation/ActivationStatusBlobInfoTest.java b/powerauth-java-crypto/src/test/java/io/getlime/security/powerauth/crypto/activation/ActivationStatusBlobInfoTest.java
index f2e53013..a6a7f8e0 100644
--- a/powerauth-java-crypto/src/test/java/io/getlime/security/powerauth/crypto/activation/ActivationStatusBlobInfoTest.java
+++ b/powerauth-java-crypto/src/test/java/io/getlime/security/powerauth/crypto/activation/ActivationStatusBlobInfoTest.java
@@ -70,8 +70,6 @@ public class ActivationStatusBlobInfoTest {
         final SecretKey masterSecretKey = powerAuthServerKeyFactory.generateServerMasterSecretKey(keyPairServer.getPrivate(), keyPairDevice.getPublic());
         // Derive transport key
         final SecretKey transportKey = powerAuthServerKeyFactory.generateServerTransportKey(masterSecretKey);
-        // Generate hash based counter
-        byte[] ctrData = new HashBasedCounter().init();
         // Encrypt status blob with transport key
         ActivationStatusBlobInfo serverStatusBlob = new ActivationStatusBlobInfo();
         serverStatusBlob.setActivationStatus((byte)3);
@@ -80,11 +78,10 @@ public class ActivationStatusBlobInfoTest {
         serverStatusBlob.setFailedAttempts((byte)1);
         serverStatusBlob.setMaxFailedAttempts((byte)5);
         serverStatusBlob.setCtrLookAhead((byte)20);
-        serverStatusBlob.setCtrData(ctrData);
         byte[] encryptedStatusBlob = serverActivation.encryptedStatusBlob(serverStatusBlob, null, null, transportKey);
         // Decrypt status blob with transport key
         AESEncryptionUtils aes = new AESEncryptionUtils();
-        byte[] zeroIv = new KeyDerivationUtils(keyGenerator).deriveIvForStatusBlobEncryption(null, null, transportKey);
+        byte[] zeroIv = new KeyDerivationUtils().deriveIvForStatusBlobEncryption(null, null, transportKey);
         byte[] statusBlob = aes.decrypt(encryptedStatusBlob, zeroIv, transportKey, "AES/CBC/NoPadding");
         ByteBuffer buffer = ByteBuffer.wrap(statusBlob);
         // Status blob bytes 0 ... 6 are deterministic, verify them
@@ -95,11 +92,6 @@ public class ActivationStatusBlobInfoTest {
         // Status blob bytes 13 ... 14 contain version, verify them
         assertEquals((byte) 1, buffer.get(13));
         assertEquals((byte) 5, buffer.get(14));
-        // Look ahead window
-        assertEquals((byte) 20, buffer.get(15));
-        // Status blob bytes 16 ... 31 contain ctrData, verify them
-        byte[] ctrDataFromStatus = Arrays.copyOfRange(statusBlob, 16, 32);
-        assertArrayEquals(ctrData, ctrDataFromStatus);
 
         // Verify decoded status blob used in client activation
         final ActivationStatusBlobInfo statusBlobDecoded = clientActivation.getStatusFromEncryptedBlob(encryptedStatusBlob, null, null, transportKey);
@@ -108,8 +100,6 @@ public class ActivationStatusBlobInfoTest {
         assertEquals(3, statusBlobDecoded.getUpgradeVersion());
         assertEquals(1, statusBlobDecoded.getFailedAttempts());
         assertEquals(5, statusBlobDecoded.getMaxFailedAttempts());
-        assertEquals(20, statusBlobDecoded.getCtrLookAhead());
-        assertArrayEquals(ctrData, statusBlobDecoded.getCtrData());
         assertTrue(statusBlobDecoded.isValid());
     }
 
@@ -128,7 +118,7 @@ public class ActivationStatusBlobInfoTest {
         // Derive transport key
         final SecretKey transportKey = powerAuthServerKeyFactory.generateServerTransportKey(masterSecretKey);
         // Generate hash based counter
-        byte[] ctrData = new HashBasedCounter().init();
+        byte[] ctrDataHash = keyGenerator.generateRandomBytes(16);
         // Encrypt status blob with transport key
         ActivationStatusBlobInfo serverStatusBlob = new ActivationStatusBlobInfo();
         serverStatusBlob.setActivationStatus((byte)3);
@@ -137,11 +127,12 @@ public class ActivationStatusBlobInfoTest {
         serverStatusBlob.setFailedAttempts((byte)1);
         serverStatusBlob.setMaxFailedAttempts((byte)5);
         serverStatusBlob.setCtrLookAhead((byte)20);
-        serverStatusBlob.setCtrData(ctrData);
+        serverStatusBlob.setCtrByte((byte)33);
+        serverStatusBlob.setCtrDataHash(ctrDataHash);
         byte[] encryptedStatusBlob = serverActivation.encryptedStatusBlob(serverStatusBlob, challenge, nonce, transportKey);
         // Decrypt status blob with transport key
         AESEncryptionUtils aes = new AESEncryptionUtils();
-        byte[] zeroIv = new KeyDerivationUtils(keyGenerator).deriveIvForStatusBlobEncryption(challenge, nonce, transportKey);
+        byte[] zeroIv = new KeyDerivationUtils().deriveIvForStatusBlobEncryption(challenge, nonce, transportKey);
         byte[] statusBlob = aes.decrypt(encryptedStatusBlob, zeroIv, transportKey, "AES/CBC/NoPadding");
         ByteBuffer buffer = ByteBuffer.wrap(statusBlob);
         // Status blob bytes 0 ... 6 are deterministic, verify them
@@ -149,6 +140,8 @@ public class ActivationStatusBlobInfoTest {
         assertEquals((byte) 3, buffer.get(4));
         assertEquals((byte) 2, buffer.get(5));
         assertEquals((byte) 3, buffer.get(6));
+        // ctr byte is at position 12
+        assertEquals((byte)33, buffer.get(12));
         // Status blob bytes 13 ... 14 contain version, verify them
         assertEquals((byte) 1, buffer.get(13));
         assertEquals((byte) 5, buffer.get(14));
@@ -156,7 +149,7 @@ public class ActivationStatusBlobInfoTest {
         assertEquals((byte) 20, buffer.get(15));
         // Status blob bytes 16 ... 31 contain ctrData, verify them
         byte[] ctrDataFromStatus = Arrays.copyOfRange(statusBlob, 16, 32);
-        assertArrayEquals(ctrData, ctrDataFromStatus);
+        assertArrayEquals(ctrDataHash, ctrDataFromStatus);
 
         // Verify decoded status blob used in client activation
         final ActivationStatusBlobInfo statusBlobDecoded = clientActivation.getStatusFromEncryptedBlob(encryptedStatusBlob, challenge, nonce, transportKey);
@@ -166,7 +159,8 @@ public class ActivationStatusBlobInfoTest {
         assertEquals(1, statusBlobDecoded.getFailedAttempts());
         assertEquals(5, statusBlobDecoded.getMaxFailedAttempts());
         assertEquals(20, statusBlobDecoded.getCtrLookAhead());
-        assertArrayEquals(ctrData, statusBlobDecoded.getCtrData());
+        assertEquals(33, statusBlobDecoded.getCtrByte());
+        assertArrayEquals(ctrDataHash, statusBlobDecoded.getCtrDataHash());
         assertTrue(statusBlobDecoded.isValid());
     }
 }
diff --git a/powerauth-java-crypto/src/test/java/io/getlime/security/powerauth/crypto/lib/util/GenerateVectorDataTest.java b/powerauth-java-crypto/src/test/java/io/getlime/security/powerauth/crypto/lib/util/GenerateVectorDataTest.java
index 7f1f4d95..d7fb4207 100644
--- a/powerauth-java-crypto/src/test/java/io/getlime/security/powerauth/crypto/lib/util/GenerateVectorDataTest.java
+++ b/powerauth-java-crypto/src/test/java/io/getlime/security/powerauth/crypto/lib/util/GenerateVectorDataTest.java
@@ -27,6 +27,7 @@ import io.getlime.security.powerauth.crypto.lib.enums.PowerAuthSignatureFormat;
 import io.getlime.security.powerauth.crypto.lib.generator.HashBasedCounter;
 import io.getlime.security.powerauth.crypto.lib.generator.IdentifierGenerator;
 import io.getlime.security.powerauth.crypto.lib.generator.KeyGenerator;
+import io.getlime.security.powerauth.crypto.lib.model.ActivationStatusBlobInfo;
 import io.getlime.security.powerauth.crypto.lib.model.ActivationVersion;
 import io.getlime.security.powerauth.crypto.lib.util.model.TestSet;
 import io.getlime.security.powerauth.crypto.server.activation.PowerAuthServerActivation;
@@ -893,7 +894,7 @@ public class GenerateVectorDataTest {
         TestSet testSet = new TestSet("activation-status-blob-iv.json", "Initialization Vectors for activation status blob symmetric encryption.");
 
         KeyGenerator keyGenerator = new KeyGenerator();
-        KeyDerivationUtils keyDerivationUtils = new KeyDerivationUtils(keyGenerator);
+        KeyDerivationUtils keyDerivationUtils = new KeyDerivationUtils();
         CryptoProviderUtil keyConvertor = PowerAuthConfiguration.INSTANCE.getKeyConvertor();
 
         for (int i = 0; i < 32; i++) {
@@ -912,6 +913,169 @@ public class GenerateVectorDataTest {
         writeTestVector(testSet);
     }
 
+    /**
+     * Generate test data for status blob encryption.
+     *
+     * <p><b>PowerAuth protocol versions:</b>
+     * <ul>
+     *     <li>3.1</li>
+     * </ul>
+     *
+     * @throws Exception In case any unknown error occurs.
+     */
+    @Test
+    public void testActivationStatusBlobEncoding() throws Exception {
+        TestSet testSet = new TestSet("activation-status-blob-data.json", "Status blob test data.");
+
+        final KeyGenerator keyGenerator = new KeyGenerator();
+        final CryptoProviderUtil keyConvertor = PowerAuthConfiguration.INSTANCE.getKeyConvertor();
+        final PowerAuthServerActivation activation = new PowerAuthServerActivation();
+        final BaseEncoding b64 = BaseEncoding.base64();
+
+        final ActivationStatusBlobInfoBuilder[] testData = {
+                new ActivationStatusBlobInfoBuilder().ctrByte(1)  .status(2).currentVersion(2),
+                new ActivationStatusBlobInfoBuilder().ctrByte(13) .status(3).ctrLookAhead(33).counterDistance(30),
+                new ActivationStatusBlobInfoBuilder().ctrByte(133).status(4).failedAttempts(1),
+                new ActivationStatusBlobInfoBuilder().ctrByte(63) .status(5).maxFailedAttempts(10),
+                new ActivationStatusBlobInfoBuilder().ctrByte(127).status(3).failedAttempts(5).maxFailedAttempts(10),
+                new ActivationStatusBlobInfoBuilder().ctrByte(253).status(3).failedAttempts(6).maxFailedAttempts(6),
+                new ActivationStatusBlobInfoBuilder().ctrByte(128).status(4).counterDistance(4),
+                new ActivationStatusBlobInfoBuilder().ctrByte(77) .status(5).counterDistance(1)
+        };
+
+        for (final ActivationStatusBlobInfoBuilder builder : testData) {
+
+            final SecretKey transportKey = keyGenerator.generateRandomSecretKey();
+            final byte[] transportKeyBytes = keyConvertor.convertSharedSecretKeyToBytes(transportKey);
+            final byte[] ctrData = keyGenerator.generateRandomBytes(16);
+            final byte[] challenge = keyGenerator.generateRandomBytes(16);
+            final byte[] nonce = keyGenerator.generateRandomBytes(16);
+
+            // Move hash based counter forward for required counter distance times.
+            byte[] ctrDataMoved = ctrData;
+            for (int i = 0; i < builder.counterDistance; i++) {
+                ctrDataMoved = keyGenerator.convert32Bto16B(Hash.sha256(ctrDataMoved));
+            }
+            final byte[] ctrDataHash = activation.calculateHashFromHashBasedCounter(ctrDataMoved, transportKey);
+
+            final ActivationStatusBlobInfo info = builder.ctrDataHash(ctrDataHash).build();
+            final byte[] encryptedStatusBlob = activation.encryptedStatusBlob(info, challenge, nonce, transportKey);
+
+            Map<String, String> input = new LinkedHashMap<>();
+            input.put("transportKey", b64.encode(transportKeyBytes));
+            input.put("challenge", b64.encode(challenge));
+            input.put("nonce", b64.encode(nonce));
+            input.put("ctrData", b64.encode(ctrData));
+            input.put("encryptedStatusBlob", BaseEncoding.base64().encode(encryptedStatusBlob));
+
+            Map<String, String> output = builder.toMap();
+            testSet.addData(input, output);
+        }
+
+        writeTestVector(testSet);
+    }
+
+    /**
+     * Helper class that helps with ActivationStatusBlobInfo object construction.
+     */
+    private static class ActivationStatusBlobInfoBuilder {
+
+        final ActivationStatusBlobInfo info;
+        int counterDistance;
+
+        ActivationStatusBlobInfoBuilder() {
+            info = new ActivationStatusBlobInfo();
+            info.setActivationStatus((byte)3);
+            info.setCurrentVersion((byte)3);
+            info.setUpgradeVersion((byte)3);
+            info.setFailedAttempts((byte)0);
+            info.setMaxFailedAttempts((byte)5);
+            info.setCtrLookAhead((byte)20);
+            info.setCtrDataHash(new byte[16]);
+            counterDistance = 0;
+        }
+
+        ActivationStatusBlobInfoBuilder status(int status) {
+            info.setActivationStatus((byte)status);
+            return this;
+        }
+
+        ActivationStatusBlobInfoBuilder currentVersion(int value) {
+            info.setCurrentVersion((byte)value);
+            return this;
+        }
+
+        ActivationStatusBlobInfoBuilder upgradeVersion(int value) {
+            info.setCurrentVersion((byte)value);
+            return this;
+        }
+
+        ActivationStatusBlobInfoBuilder failedAttempts(int value) {
+            info.setFailedAttempts((byte)value);
+            return this;
+        }
+
+        ActivationStatusBlobInfoBuilder maxFailedAttempts(int value) {
+            info.setMaxFailedAttempts((byte)value);
+            return this;
+        }
+
+        ActivationStatusBlobInfoBuilder ctrLookAhead(int value) {
+            info.setCtrLookAhead((byte)value);
+            return this;
+        }
+
+        ActivationStatusBlobInfoBuilder ctrByte(int value) {
+            info.setCtrByte((byte)value);
+            return this;
+        }
+
+        ActivationStatusBlobInfoBuilder ctrDataHash(byte[] value) {
+            info.setCtrDataHash(value);
+            return this;
+        }
+
+        ActivationStatusBlobInfoBuilder counterDistance(int distance) {
+            counterDistance = distance;
+            return this;
+        }
+
+        /**
+         * @return ActivationStatusBlobInfo object constructed from values set to builder.
+         */
+        ActivationStatusBlobInfo build() {
+            return info;
+        }
+
+        /**
+         * @return Map with values stored in ActivationStatusBlobInfo
+         */
+        Map<String, String> toMap() {
+            final Map<String, String> map = new LinkedHashMap<>();
+            map.put("activationStatus", toUnsignedByteString(info.getActivationStatus()));
+            map.put("currentVersion", toUnsignedByteString(info.getCurrentVersion()));
+            map.put("upgradeVersion", toUnsignedByteString(info.getUpgradeVersion()));
+            map.put("failedAttempts", toUnsignedByteString(info.getFailedAttempts()));
+            map.put("maxFailedAttempts", toUnsignedByteString(info.getMaxFailedAttempts()));
+            map.put("ctrLookAhead", toUnsignedByteString(info.getCtrLookAhead()));
+            map.put("ctrByte", toUnsignedByteString(info.getCtrByte()));
+            map.put("ctrDataHash", BaseEncoding.base64().encode(info.getCtrDataHash()));
+            map.put("counterDistance", String.valueOf(counterDistance));
+            return map;
+        }
+
+        /**
+         * Helper function that converts byte to its unsigned string representation. For example, {@code -1} should be
+         * converted to {@code "255"}.
+         * @param value Byte to convert.
+         * @return String with unsigned value of byte.
+         */
+        private String toUnsignedByteString(byte value) {
+            int intValue = value & 0xFF;
+            return String.valueOf(intValue);
+        }
+    }
+
     /**
      * Convert EC public key to byte array.
      *
diff --git a/powerauth-java-crypto/src/test/java/io/getlime/security/powerauth/crypto/lib/util/HashBasedCounterUtilsTest.java b/powerauth-java-crypto/src/test/java/io/getlime/security/powerauth/crypto/lib/util/HashBasedCounterUtilsTest.java
new file mode 100644
index 00000000..c909616b
--- /dev/null
+++ b/powerauth-java-crypto/src/test/java/io/getlime/security/powerauth/crypto/lib/util/HashBasedCounterUtilsTest.java
@@ -0,0 +1,96 @@
+/*
+ * PowerAuth Crypto Library
+ * Copyright 2019 Wultra s.r.o.
+ *
+ * Licensed 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 io.getlime.security.powerauth.crypto.lib.util;
+
+import io.getlime.security.powerauth.crypto.lib.config.PowerAuthConfiguration;
+import io.getlime.security.powerauth.crypto.lib.generator.KeyGenerator;
+import io.getlime.security.powerauth.crypto.lib.model.exception.GenericCryptoException;
+import io.getlime.security.powerauth.provider.CryptoProviderUtilFactory;
+import io.getlime.security.powerauth.provider.exception.CryptoProviderException;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.crypto.SecretKey;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.security.InvalidKeyException;
+import java.security.Security;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Test verification of hash calculated from the hash based counter.
+ */
+public class HashBasedCounterUtilsTest {
+
+    private final KeyGenerator keyGenerator;
+
+    /**
+     * Default constructor
+     */
+    public HashBasedCounterUtilsTest() {
+        this.keyGenerator = new KeyGenerator();
+    }
+
+    /**
+     * Set up crypto providers
+     */
+    @Before
+    public void setUp() {
+        // Add Bouncy Castle Security Provider
+        Security.addProvider(new BouncyCastleProvider());
+        PowerAuthConfiguration.INSTANCE.setKeyConvertor(CryptoProviderUtilFactory.getCryptoProviderUtils());
+    }
+
+    @Test
+    public void testCounterHashCalculationAndVerification()
+            throws IOException, CryptoProviderException, InvalidKeyException, GenericCryptoException {
+        final HashBasedCounterUtils hashBasedCounterUtils = new HashBasedCounterUtils();
+        for (int i = 0; i < 100; i++) {
+            // Generate random transport key
+            final SecretKey transportKey = PowerAuthConfiguration.INSTANCE.getKeyConvertor().convertBytesToSharedSecretKey(keyGenerator.generateRandomBytes(16));
+            // Generate random CTR_DATA
+            final byte[] ctrData = keyGenerator.generateRandomBytes(16);
+            final byte[] ctrDataHashExpected = calculateCtrDataHash(ctrData, transportKey);
+            final byte[] ctrDataHashCalculated = hashBasedCounterUtils.calculateHashFromHashBasedCounter(ctrData, transportKey);
+            assertArrayEquals(ctrDataHashExpected, ctrDataHashCalculated);
+            final boolean ctrDataHashVerified = hashBasedCounterUtils.verifyHashForHashBasedCounter(ctrDataHashCalculated, ctrData, transportKey);
+            assertTrue(ctrDataHashVerified);
+        }
+    }
+
+    /**
+     * Helper function that calculates CTR_DATA_HASH from CTR_DATA and KEY_TRANSPORT.
+     *
+     * @param ctrData Counter data.
+     * @param transportKey Transport key.
+     * @return Hash calculated from counter data.
+     * @throws CryptoProviderException In case that cryptographic provider is not configured properly.
+     * @throws InvalidKeyException In case that invalid transport key is provided.
+     * @throws GenericCryptoException In case that underlying key generator failed.
+     */
+    private byte[] calculateCtrDataHash(byte[] ctrData, SecretKey transportKey)
+            throws CryptoProviderException, InvalidKeyException, GenericCryptoException {
+        final byte[] index = ByteBuffer.allocate(16).putLong(0L).putLong(4000).array();
+        final SecretKey ctrInfoKey = keyGenerator.deriveSecretKey(transportKey, index);
+        final SecretKey ctrDataHash = keyGenerator.deriveSecretKeyHmac(ctrInfoKey, ctrData);
+        return PowerAuthConfiguration.INSTANCE.getKeyConvertor().convertSharedSecretKeyToBytes(ctrDataHash);
+    }
+
+}