Repository: wultra/powerauth-crypto (java)

Issue:
Add support for Time-Based One-Time Password (TOTP)
https://www.rfc-editor.org/rfc/rfc6238

Pull request:
Fix #471: Add support for Time-Based One-Time Password (TOTP) [backport]
This PR combines cherry-picks of 7340ac01b5e328ff083946fca4d994ddfb7bbcc2 and aed0f9a7853dd4d7e9352ff509b16b60cf6ce409.  The latter is simply a small edit of the first one. I decided to merge them into a single PR because additional changes need to be made following these cherry-picks, specifically adjusting TotpTest.java. An extra commit is included to make code style changes to TotpTest.java, ensuring compliance with the project's Java 8 standards. Changing the Java version just to accommodate one test seemed like overkill.


Patch:
diff --git a/pom.xml b/pom.xml
index 91109173..d20fa1cd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -79,7 +79,7 @@
         <maven-source-plugin.version>3.2.1</maven-source-plugin.version>
         <guava.version>31.1-jre</guava.version>
         <slf4j.version>2.0.5</slf4j.version>
-        <junit.version>5.9.1</junit.version>
+        <junit.version>5.9.2</junit.version>
     </properties>
 
     <dependencyManagement>
@@ -107,6 +107,12 @@
                 <artifactId>junit-jupiter-engine</artifactId>
                 <version>${junit.version}</version>
             </dependency>
+
+            <dependency>
+                <groupId>org.junit.jupiter</groupId>
+                <artifactId>junit-jupiter-params</artifactId>
+                <version>${junit.version}</version>
+            </dependency>
         </dependencies>
     </dependencyManagement>
 
diff --git a/powerauth-java-crypto/pom.xml b/powerauth-java-crypto/pom.xml
index ea21d70c..3f6a33b0 100644
--- a/powerauth-java-crypto/pom.xml
+++ b/powerauth-java-crypto/pom.xml
@@ -43,6 +43,11 @@
 			<artifactId>junit-jupiter-engine</artifactId>
 			<scope>test</scope>
 		</dependency>
+		<dependency>
+			<groupId>org.junit.jupiter</groupId>
+			<artifactId>junit-jupiter-params</artifactId>
+			<scope>test</scope>
+		</dependency>
 		<dependency>
 			<groupId>com.fasterxml.jackson.core</groupId>
 			<artifactId>jackson-databind</artifactId>
diff --git a/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/lib/totp/Totp.java b/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/lib/totp/Totp.java
new file mode 100644
index 00000000..ab167da8
--- /dev/null
+++ b/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/lib/totp/Totp.java
@@ -0,0 +1,270 @@
+/*
+ * PowerAuth Crypto Library
+ * Copyright 2023 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.totp;
+
+import com.google.common.base.Strings;
+import io.getlime.security.powerauth.crypto.lib.model.exception.CryptoProviderException;
+import org.bouncycastle.util.Arrays;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.security.GeneralSecurityException;
+import java.time.Instant;
+import java.util.HexFormat;
+
+/**
+ * TOTP: Time-Based One-Time Password Algorithm according to <a href="https://www.rfc-editor.org/rfc/rfc6238">RFC 6238</a>.
+ *
+ * @author Lubos Racansky, lubos.racansky@wultra.com
+ */
+public final class Totp {
+
+    private static final Logger logger = LoggerFactory.getLogger(Totp.class);
+
+    /**
+     * Default time-step size of 30 seconds recommended by RFC. The value is selected as a balance between security and usability.
+     */
+    private static final int TIME_STEP_X = 30;
+
+    private Totp() {
+        throw new IllegalStateException("Should not be instantiated");
+    }
+
+    private static final int[] DIGITS_POWER
+            // 0  1   2    3      4       5        6          7           8
+            = {1, 10, 100, 1_000, 10_000, 100_000, 1_000_000, 10_000_000, 100_000_000};
+
+    /**
+     * Generates a TOTP value for the given set of parameters using HmacSHA256 algorithm.
+     *
+     * @param key           the shared secret
+     * @param instant       current Unix time
+     * @param digitsNumber  number of digits to return
+     * @return a numeric String in base 10 that includes truncation digits
+     * @throws CryptoProviderException in case of any crypto error
+     */
+    public static byte[] generateTotpSha256(final byte[] key, final Instant instant, final int digitsNumber) throws CryptoProviderException {
+        return generateTotp(key, countTimeSteps(instant), digitsNumber, Algorithm.HMAC_SHA256.code);
+    }
+
+    /**
+     * Generates a TOTP value for the given set of parameters using HmacSHA512 algorithm.
+     *
+     * @param key           the shared secret
+     * @param instant       current Unix time
+     * @param digitsNumber  number of digits to return
+     * @return a numeric String in base 10 that includes truncation digits
+     * @throws CryptoProviderException in case of any crypto error
+     */
+    public static byte[] generateTotpSha512(final byte[] key, final Instant instant, final int digitsNumber) throws CryptoProviderException {
+        return generateTotp(key, countTimeSteps(instant), digitsNumber, Algorithm.HMAC_SHA512.code);
+    }
+
+    /**
+     * Validate a TOTP value for the given set of parameters using HmacSHA256 algorithm. Validates one time step backward.
+     *
+     * @param otp           TOTP to validate
+     * @param key           the shared secret
+     * @param instant       current Unix time
+     * @param digitsNumber  expected length of the TOTP
+     * @return true if OTP is valid
+     * @throws CryptoProviderException in case of any crypto error
+     * @see #validateTotpSha256(byte[], byte[], Instant, int)
+     */
+    public static boolean validateTotpSha256(final byte[] otp, final byte[] key, final Instant instant, final int digitsNumber) throws CryptoProviderException {
+        return validateTotpSha256(otp, key, instant, digitsNumber, 1);
+    }
+
+    /**
+     * Validate a TOTP value for the given set of parameters using HmacSHA256 algorithm.
+     *
+     * @param otp           TOTP to validate
+     * @param key           the shared secret
+     * @param instant       current Unix time
+     * @param digitsNumber  expected length of the TOTP
+     * @param steps         number of backward time steps allowed to validate
+     * @return true if OTP is valid
+     * @throws CryptoProviderException in case of any crypto error
+     */
+    public static boolean validateTotpSha256(final byte[] otp, final byte[] key, final Instant instant, final int digitsNumber, final int steps) throws CryptoProviderException {
+        return validateTotp(otp, key, instant, digitsNumber, steps, Algorithm.HMAC_SHA256.code);
+    }
+
+    /**
+     * Validate a TOTP value for the given set of parameters using HmacSHA512 algorithm. Validates one time step backward.
+     *
+     * @param otp           TOTP to validate
+     * @param key           the shared secret
+     * @param instant       current Unix time
+     * @param digitsNumber  expected length of the TOTP
+     * @return true if OTP is valid
+     * @throws CryptoProviderException in case of any crypto error
+     * @see #validateTotpSha512(byte[], byte[], Instant, int, int)
+     */
+    public static boolean validateTotpSha512(final byte[] otp, final byte[] key, final Instant instant, final int digitsNumber) throws CryptoProviderException {
+        return validateTotpSha512(otp, key, instant, digitsNumber, 1);
+    }
+
+    /**
+     * Validate a TOTP value for the given set of parameters using HmacSHA512 algorithm.
+     *
+     * @param otp           TOTP to validate
+     * @param key           the shared secret
+     * @param instant       current Unix time
+     * @param digitsNumber  expected length of the TOTP
+     * @param steps         number of backward time steps allowed to validate
+     * @return true if OTP is valid
+     * @throws CryptoProviderException in case of any crypto error
+     */
+    public static boolean validateTotpSha512(final byte[] otp, final byte[] key, final Instant instant, final int digitsNumber, final int steps) throws CryptoProviderException {
+        return validateTotp(otp, key, instant, digitsNumber, steps, Algorithm.HMAC_SHA512.code);
+    }
+
+    /**
+     * Validate a TOTP value for the given set of parameters.
+     *
+     * @param otp           TOTP to validate
+     * @param key           the shared secret
+     * @param instant       current Unix time
+     * @param digitsNumber  expected length of the TOTP
+     * @param backwardSteps number of backward time steps allowed to validate
+     * @param algorithm     the algorithm to use
+     * @return true if OTP is valid
+     * @throws CryptoProviderException in case of any crypto error
+     */
+    private static boolean validateTotp(final byte[] otp, final byte[] key, final Instant instant, final int digitsNumber, final int backwardSteps, final String algorithm) throws CryptoProviderException {
+        logger.debug("Validating TOTP for instant={}, algorithm={}, steps={}", instant, algorithm, backwardSteps);
+
+        if (otp == null) {
+            throw new CryptoProviderException("Otp is mandatory");
+        }
+
+        if (otp.length != digitsNumber) {
+            throw new CryptoProviderException("Otp length %d is different from expected %d".formatted(otp.length, digitsNumber));
+        }
+
+        if (backwardSteps < 0) {
+            throw new CryptoProviderException("Steps must not be negative number");
+        }
+
+        final long currentTimeStep = countTimeSteps(instant);
+        for (int i = 0; i <= backwardSteps; i++) {
+            logger.debug("Validating TOTP for instant={}, algorithm={}, step={} out of allowed backward steps={}", instant, algorithm, i, backwardSteps);
+            final long step = currentTimeStep - i;
+            final byte[] expectedOtp = generateTotp(key, step, otp.length, algorithm);
+            if (Arrays.constantTimeAreEqual(expectedOtp, otp)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Generates a TOTP value for the given set of parameters.
+     *
+     * @param key          the shared secret
+     * @param timeStep     number of time step
+     * @param digitsNumber number of digits to return
+     * @param algorithm    the algorithm to use
+     * @return a numeric String in base 10 that includes truncation digits
+     * @throws CryptoProviderException in case of any crypto error
+     */
+    private static byte[] generateTotp(final byte[] key, final long timeStep, final int digitsNumber, final String algorithm) throws CryptoProviderException {
+        logger.debug("Generating TOTP for timeStep={}, algorithm={}", timeStep, algorithm);
+
+        if (key == null) {
+            throw new CryptoProviderException("Key is mandatory");
+        }
+
+        if (algorithm == null) {
+            throw new CryptoProviderException("Algorithm is mandatory");
+        }
+
+        if (digitsNumber <= 0 || digitsNumber >= DIGITS_POWER.length) {
+            throw new CryptoProviderException("DigitsNumber must be positive number and smaller than " + DIGITS_POWER.length);
+        }
+
+        // Using the counter
+        // First 8 bytes are for the movingFactor
+        // Compliant with base RFC4226 (HOTP)
+        final String hexPaddedTimeStep = padWithZeros(Long.toHexString(timeStep), 16);
+        
+        final byte[] data = HexFormat.of().parseHex(hexPaddedTimeStep);
+        final byte[] hash = computeHash(algorithm, key, data);
+
+        // Last four bits of the hash is offset (last byte masked by 0xf)
+        final int offset = hash[hash.length - 1] & 0xf;
+
+        // The reason for masking the most significant bit (0x7f) is to avoid confusion about signed vs. unsigned modulo computations.
+        // Different processors perform these operations differently, and masking out the signed bit removes all ambiguity.
+        final int binaryCode = ((hash[offset] & 0x7f) << 24) |
+                ((hash[offset + 1] & 0xff) << 16) |
+                ((hash[offset + 2] & 0xff) << 8) |
+                (hash[offset + 3] & 0xff);
+
+        final int otp = binaryCode % DIGITS_POWER[digitsNumber];
+
+        return padWithZeros(Integer.toString(otp), digitsNumber).getBytes();
+    }
+
+    private static long countTimeSteps(final Instant instant) throws CryptoProviderException {
+        if (instant == null) {
+            throw new CryptoProviderException("Instant is mandatory");
+        }
+
+        return instant.getEpochSecond() / TIME_STEP_X;
+    }
+
+    private static String padWithZeros(final String source, final int length) {
+        return Strings.padStart(source, length, '0');
+    }
+
+    /**
+     * Computes a Hashed Message Authentication Code with the give hash algorithm as a parameter.
+     *
+     * @param algorithm the algorithm
+     * @param keyBytes  the bytes to use for the HMAC key
+     * @param data      data to be hashed
+     * @throws CryptoProviderException in case of any crypto error
+     */
+    @SuppressWarnings("java:S2139") // NOSONAR We need to be sure that the exception is logged, better twice than never
+    private static byte[] computeHash(final String algorithm, final byte[] keyBytes, final byte[] data) throws CryptoProviderException {
+        try {
+            final Mac hmac = Mac.getInstance(algorithm);
+            final SecretKeySpec macKey = new SecretKeySpec(keyBytes, "RAW");
+            hmac.init(macKey);
+            return hmac.doFinal(data);
+        } catch (GeneralSecurityException e) {
+            logger.error("Problem to compute hash for algorithm={}", algorithm, e);
+            throw new CryptoProviderException("Problem to compute hash for algorithm=" + algorithm, e);
+        }
+    }
+
+    private enum Algorithm {
+        HMAC_SHA256("HmacSHA256"),
+        HMAC_SHA512("HmacSHA512");
+
+        private final String code;
+
+        Algorithm(String code) {
+            this.code = code;
+        }
+    }
+}


Test patch:
diff --git a/powerauth-java-crypto/src/test/java/io/getlime/security/powerauth/crypto/lib/totp/TotpTest.java b/powerauth-java-crypto/src/test/java/io/getlime/security/powerauth/crypto/lib/totp/TotpTest.java
new file mode 100644
index 00000000..0031b1c1
--- /dev/null
+++ b/powerauth-java-crypto/src/test/java/io/getlime/security/powerauth/crypto/lib/totp/TotpTest.java
@@ -0,0 +1,157 @@
+/*
+ * PowerAuth Crypto Library
+ * Copyright 2023 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.totp;
+
+import io.getlime.security.powerauth.crypto.lib.model.exception.CryptoProviderException;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.converter.ArgumentConversionException;
+import org.junit.jupiter.params.converter.ConvertWith;
+import org.junit.jupiter.params.converter.TypedArgumentConverter;
+import org.junit.jupiter.params.provider.CsvFileSource;
+import org.opentest4j.AssertionFailedError;
+
+import java.time.*;
+import java.time.format.DateTimeFormatter;
+import java.util.HexFormat;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Test for {@link Totp}.
+ *
+ * @author Lubos Racansky, lubos.racansky@wultra.com
+ *
+ */
+class TotpTest {
+
+    private static final int DIGITS_NUMBER = 8;
+
+    @ParameterizedTest
+    @CsvFileSource(resources = "/io/getlime/security/powerauth/crypto/lib/totp/data.csv", useHeadersInDisplayName=true)
+    void testGenerateTotp(final long seconds, final @ConvertWith(DateTimeConverter.class) LocalDateTime localDateTime, final String step, final String otp, final String algorithm, final String seed) throws Exception {
+        final Instant instant = localDateTime.toInstant(ZoneOffset.UTC);
+        byte[] result;
+        switch (algorithm) {
+            case "HmacSHA256":
+                result = Totp.generateTotpSha256(fromHex(seed), instant, DIGITS_NUMBER);
+                break;
+            case "HmacSHA512":
+                result = Totp.generateTotpSha512(fromHex(seed), instant, DIGITS_NUMBER);
+                break;
+            default:
+                throw new AssertionFailedError("Not supported algorithm " + algorithm);
+        }
+        assertEquals(otp, new String(result));
+    }
+
+    @ParameterizedTest
+    @CsvFileSource(resources = "/io/getlime/security/powerauth/crypto/lib/totp/data.csv", useHeadersInDisplayName=true)
+    void testValidateTotpCurrentStep(final long seconds, final @ConvertWith(DateTimeConverter.class) LocalDateTime localDateTime, final String step, final String otp, final String algorithm, final String seed) throws Exception {
+        final Instant instant = localDateTime.toInstant(ZoneOffset.UTC);
+        boolean result;
+        switch (algorithm) {
+            case "HmacSHA256":
+                result = Totp.validateTotpSha256(otp.getBytes(), fromHex(seed), instant, DIGITS_NUMBER);
+                break;
+            case "HmacSHA512":
+                result = Totp.validateTotpSha512(otp.getBytes(), fromHex(seed), instant, DIGITS_NUMBER);
+                break;
+            default:
+                throw new AssertionFailedError("Not supported algorithm " + algorithm);
+        }
+        assertTrue(result);
+    }
+
+    @ParameterizedTest
+    @CsvFileSource(resources = "/io/getlime/security/powerauth/crypto/lib/totp/data.csv", useHeadersInDisplayName=true)
+    void testValidateTotpOneStepBack(final long seconds, final @ConvertWith(DateTimeConverter.class) LocalDateTime localDateTime, final String step, final String otp, final String algorithm, final String seed) throws Exception {
+        final Instant movedInstant = localDateTime.plusSeconds(30).toInstant(ZoneOffset.UTC);
+
+        boolean result;
+        switch (algorithm) {
+            case "HmacSHA256":
+                result = Totp.validateTotpSha256(otp.getBytes(), fromHex(seed), movedInstant, DIGITS_NUMBER);
+                break;
+            case "HmacSHA512":
+                result = Totp.validateTotpSha512(otp.getBytes(), fromHex(seed), movedInstant, DIGITS_NUMBER);
+                break;
+            default:
+                throw new AssertionFailedError("Not supported algorithm " + algorithm);
+        }
+        assertTrue(result);
+    }
+
+    @ParameterizedTest
+    @CsvFileSource(resources = "/io/getlime/security/powerauth/crypto/lib/totp/data.csv", useHeadersInDisplayName=true)
+    void testValidateTotpTwoStepsBack(final long seconds, final @ConvertWith(DateTimeConverter.class) LocalDateTime localDateTime, final String step, final String otp, final String algorithm, final String seed) throws Exception {
+        final Instant movedInstant = localDateTime.plusSeconds(60).toInstant(ZoneOffset.UTC);
+
+        boolean result;
+        switch (algorithm) {
+            case "HmacSHA256":
+                result = Totp.validateTotpSha256(otp.getBytes(), fromHex(seed), movedInstant, DIGITS_NUMBER);
+                break;
+            case "HmacSHA512":
+                result = Totp.validateTotpSha512(otp.getBytes(), fromHex(seed), movedInstant, DIGITS_NUMBER);
+                break;
+            default:
+                throw new AssertionFailedError("Not supported algorithm " + algorithm);
+        }
+        assertFalse(result);
+    }
+
+    @Test
+    void testGenerateTotpLeftPaddedWithZero() throws Exception {
+        final Instant instant = parse("2023-04-27T01:26:29Z").toInstant(ZoneOffset.UTC);
+        final String result = new String(Totp.generateTotpSha256("12345678901234567890".getBytes(), instant, DIGITS_NUMBER));
+        assertEquals("01760428", result);
+    }
+
+    @Test
+    void testValidateTotpInvalidLength() {
+        CryptoProviderException exception = Assertions.assertThrows(CryptoProviderException.class, () ->
+                Totp.validateTotpSha256("1".getBytes(), "12345678901234567890".getBytes(), Instant.now(), DIGITS_NUMBER));
+        assertEquals("Otp length 1 is different from expected 8", exception.getMessage());
+    }
+
+    private static byte[] fromHex(final String source) {
+        return HexFormat.of().parseHex(source);
+    }
+
+    private static LocalDateTime parse(String source) {
+        return ZonedDateTime.parse(source, DateTimeFormatter.ISO_DATE_TIME)
+                .toLocalDateTime();
+    }
+
+    static class DateTimeConverter extends TypedArgumentConverter<String, LocalDateTime> {
+        protected DateTimeConverter() {
+            super(String.class, LocalDateTime.class);
+        }
+
+        @Override
+        protected LocalDateTime convert(final String source) throws ArgumentConversionException {
+            try {
+                return parse(source);
+            } catch (Exception e) {
+                throw new IllegalArgumentException("Failed to convert", e);
+            }
+        }
+    }
+
+}
diff --git a/powerauth-java-crypto/src/test/resources/io/getlime/security/powerauth/crypto/lib/totp/data.csv b/powerauth-java-crypto/src/test/resources/io/getlime/security/powerauth/crypto/lib/totp/data.csv
new file mode 100644
index 00000000..6e0b481b
--- /dev/null
+++ b/powerauth-java-crypto/src/test/resources/io/getlime/security/powerauth/crypto/lib/totp/data.csv
@@ -0,0 +1,13 @@
+ Time (sec) ,  UTC Time            , Value of T (hex) ,  TOTP    , Algorithm  , Seed
+         59 , 1970-01-01T00:00:59Z , 0000000000000001 , 46119246 , HmacSHA256 , 3132333435363738393031323334353637383930313233343536373839303132
+         59 , 1970-01-01T00:00:59Z , 0000000000000001 , 90693936 , HmacSHA512 , 31323334353637383930313233343536373839303132333435363738393031323334353637383930313233343536373839303132333435363738393031323334
+ 1111111109 , 2005-03-18T01:58:29Z , 00000000023523EC , 68084774 , HmacSHA256 , 3132333435363738393031323334353637383930313233343536373839303132
+ 1111111109 , 2005-03-18T01:58:29Z , 00000000023523EC , 25091201 , HmacSHA512 , 31323334353637383930313233343536373839303132333435363738393031323334353637383930313233343536373839303132333435363738393031323334
+ 1111111111 , 2005-03-18T01:58:31Z , 00000000023523ED , 67062674 , HmacSHA256 , 3132333435363738393031323334353637383930313233343536373839303132
+ 1111111111 , 2005-03-18T01:58:31Z , 00000000023523ED , 99943326 , HmacSHA512 , 31323334353637383930313233343536373839303132333435363738393031323334353637383930313233343536373839303132333435363738393031323334
+ 1234567890 , 2009-02-13T23:31:30Z , 000000000273EF07 , 91819424 , HmacSHA256 , 3132333435363738393031323334353637383930313233343536373839303132
+ 1234567890 , 2009-02-13T23:31:30Z , 000000000273EF07 , 93441116 , HmacSHA512 , 31323334353637383930313233343536373839303132333435363738393031323334353637383930313233343536373839303132333435363738393031323334
+ 2000000000 , 2033-05-18T03:33:20Z , 0000000003F940AA , 90698825 , HmacSHA256 , 3132333435363738393031323334353637383930313233343536373839303132
+ 2000000000 , 2033-05-18T03:33:20Z , 0000000003F940AA , 38618901 , HmacSHA512 , 31323334353637383930313233343536373839303132333435363738393031323334353637383930313233343536373839303132333435363738393031323334
+20000000000 , 2603-10-11T11:33:20Z , 0000000027BC86AA , 77737706 , HmacSHA256 , 3132333435363738393031323334353637383930313233343536373839303132
+20000000000 , 2603-10-11T11:33:20Z , 0000000027BC86AA , 47863826 , HmacSHA512 , 31323334353637383930313233343536373839303132333435363738393031323334353637383930313233343536373839303132333435363738393031323334
\ No newline at end of file
diff --git a/powerauth-java-crypto/src/test/resources/simplelogger.properties b/powerauth-java-crypto/src/test/resources/simplelogger.properties
new file mode 100644
index 00000000..3a33d130
--- /dev/null
+++ b/powerauth-java-crypto/src/test/resources/simplelogger.properties
@@ -0,0 +1,1 @@
+org.slf4j.simpleLogger.log.io.getlime.security.powerauth.crypto=DEBUG