Repository: wultra/powerauth-crypto (java)

Issue:
Use system default time zone fot TOTP
https://github.com/wultra/powerauth-crypto/blob/7a804237a89c1984b5619d18a8bb71771ad8f9de/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/lib/totp/Totp.java#L233

Follow-up to #471

Pull request description:
Fix #483: Use system default time zone fot TOTP

Patch:
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
index 420da575..ab167da8 100644
--- 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
@@ -25,8 +25,7 @@ import org.slf4j.LoggerFactory;
 import javax.crypto.Mac;
 import javax.crypto.spec.SecretKeySpec;
 import java.security.GeneralSecurityException;
-import java.time.LocalDateTime;
-import java.time.ZoneOffset;
+import java.time.Instant;
 import java.util.HexFormat;
 
 /**
@@ -55,26 +54,26 @@ public final class Totp {
      * Generates a TOTP value for the given set of parameters using HmacSHA256 algorithm.
      *
      * @param key           the shared secret
-     * @param localDateTime date time
+     * @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 LocalDateTime localDateTime, final int digitsNumber) throws CryptoProviderException {
-        return generateTotp(key, countTimeSteps(localDateTime), digitsNumber, Algorithm.HMAC_SHA256.code);
+    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 localDateTime date time
+     * @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 LocalDateTime localDateTime, final int digitsNumber) throws CryptoProviderException {
-        return generateTotp(key, countTimeSteps(localDateTime), digitsNumber, Algorithm.HMAC_SHA512.code);
+    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);
     }
 
     /**
@@ -82,14 +81,14 @@ public final class Totp {
      *
      * @param otp           TOTP to validate
      * @param key           the shared secret
-     * @param localDateTime date time
+     * @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[], LocalDateTime, int)
+     * @see #validateTotpSha256(byte[], byte[], Instant, int)
      */
-    public static boolean validateTotpSha256(final byte[] otp, final byte[] key, final LocalDateTime localDateTime, final int digitsNumber) throws CryptoProviderException {
-        return validateTotpSha256(otp, key, localDateTime, digitsNumber, 1);
+    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);
     }
 
     /**
@@ -97,14 +96,14 @@ public final class Totp {
      *
      * @param otp           TOTP to validate
      * @param key           the shared secret
-     * @param localDateTime date time
+     * @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 LocalDateTime localDateTime, final int digitsNumber, final int steps) throws CryptoProviderException {
-        return validateTotp(otp, key, localDateTime, digitsNumber, steps, Algorithm.HMAC_SHA256.code);
+    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);
     }
 
     /**
@@ -112,14 +111,14 @@ public final class Totp {
      *
      * @param otp           TOTP to validate
      * @param key           the shared secret
-     * @param localDateTime date time
+     * @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[], LocalDateTime, int, int)
+     * @see #validateTotpSha512(byte[], byte[], Instant, int, int)
      */
-    public static boolean validateTotpSha512(final byte[] otp, final byte[] key, final LocalDateTime localDateTime, final int digitsNumber) throws CryptoProviderException {
-        return validateTotpSha512(otp, key, localDateTime, digitsNumber, 1);
+    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);
     }
 
     /**
@@ -127,14 +126,14 @@ public final class Totp {
      *
      * @param otp           TOTP to validate
      * @param key           the shared secret
-     * @param localDateTime date time
+     * @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 LocalDateTime localDateTime, final int digitsNumber, final int steps) throws CryptoProviderException {
-        return validateTotp(otp, key, localDateTime, digitsNumber, steps, Algorithm.HMAC_SHA512.code);
+    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);
     }
 
     /**
@@ -142,15 +141,15 @@ public final class Totp {
      *
      * @param otp           TOTP to validate
      * @param key           the shared secret
-     * @param localDateTime date time
+     * @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 LocalDateTime localDateTime, final int digitsNumber, final int backwardSteps, final String algorithm) throws CryptoProviderException {
-        logger.debug("Validating TOTP for localDateTime={}, algorithm={}, steps={}", localDateTime, algorithm, backwardSteps);
+    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");
@@ -164,9 +163,9 @@ public final class Totp {
             throw new CryptoProviderException("Steps must not be negative number");
         }
 
-        final long currentTimeStep = countTimeSteps(localDateTime);
+        final long currentTimeStep = countTimeSteps(instant);
         for (int i = 0; i <= backwardSteps; i++) {
-            logger.debug("Validating TOTP for localDateTime={}, algorithm={}, step={} out of allowed backward steps={}", localDateTime, algorithm, i, backwardSteps);
+            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)) {
@@ -225,12 +224,12 @@ public final class Totp {
         return padWithZeros(Integer.toString(otp), digitsNumber).getBytes();
     }
 
-    private static long countTimeSteps(final LocalDateTime localDateTime) throws CryptoProviderException {
-        if (localDateTime == null) {
-            throw new CryptoProviderException("LocalDateTime is mandatory");
+    private static long countTimeSteps(final Instant instant) throws CryptoProviderException {
+        if (instant == null) {
+            throw new CryptoProviderException("Instant is mandatory");
         }
 
-        return localDateTime.toEpochSecond(ZoneOffset.UTC) / TIME_STEP_X;
+        return instant.getEpochSecond() / TIME_STEP_X;
     }
 
     private static String padWithZeros(final String source, final int length) {


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
index fd76a0d5..9f2b7f80 100644
--- 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
@@ -26,7 +26,8 @@ import org.junit.jupiter.params.converter.TypedArgumentConverter;
 import org.junit.jupiter.params.provider.CsvFileSource;
 import org.opentest4j.AssertionFailedError;
 
-import java.time.LocalDateTime;
+import java.time.*;
+import java.time.format.DateTimeFormatter;
 import java.util.HexFormat;
 
 import static org.junit.jupiter.api.Assertions.*;
@@ -44,9 +45,10 @@ class TotpTest {
     @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);
         final byte[] result = switch (algorithm) {
-            case "HmacSHA256" -> Totp.generateTotpSha256(fromHex(seed), localDateTime, DIGITS_NUMBER);
-            case "HmacSHA512" -> Totp.generateTotpSha512(fromHex(seed), localDateTime, DIGITS_NUMBER);
+            case "HmacSHA256" -> Totp.generateTotpSha256(fromHex(seed), instant, DIGITS_NUMBER);
+            case "HmacSHA512" -> Totp.generateTotpSha512(fromHex(seed), instant, DIGITS_NUMBER);
             default -> throw new AssertionFailedError("Not supported algorithm " + algorithm);
         };
         assertEquals(otp, new String(result));
@@ -55,9 +57,10 @@ class TotpTest {
     @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);
         final boolean result = switch (algorithm) {
-            case "HmacSHA256" -> Totp.validateTotpSha256(otp.getBytes(), fromHex(seed), localDateTime, DIGITS_NUMBER);
-            case "HmacSHA512" -> Totp.validateTotpSha512(otp.getBytes(), fromHex(seed), localDateTime, DIGITS_NUMBER);
+            case "HmacSHA256" -> Totp.validateTotpSha256(otp.getBytes(), fromHex(seed), instant, DIGITS_NUMBER);
+            case "HmacSHA512" -> Totp.validateTotpSha512(otp.getBytes(), fromHex(seed), instant, DIGITS_NUMBER);
             default -> throw new AssertionFailedError("Not supported algorithm " + algorithm);
         };
         assertTrue(result);
@@ -66,11 +69,11 @@ class TotpTest {
     @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 LocalDateTime movedLocalDateTime = localDateTime.plusSeconds(30);
+        final Instant movedInstant = localDateTime.plusSeconds(30).toInstant(ZoneOffset.UTC);
 
         final boolean result = switch (algorithm) {
-            case "HmacSHA256" -> Totp.validateTotpSha256(otp.getBytes(), fromHex(seed), movedLocalDateTime, DIGITS_NUMBER);
-            case "HmacSHA512" -> Totp.validateTotpSha512(otp.getBytes(), fromHex(seed), movedLocalDateTime, DIGITS_NUMBER);
+            case "HmacSHA256" -> Totp.validateTotpSha256(otp.getBytes(), fromHex(seed), movedInstant, DIGITS_NUMBER);
+            case "HmacSHA512" -> Totp.validateTotpSha512(otp.getBytes(), fromHex(seed), movedInstant, DIGITS_NUMBER);
             default -> throw new AssertionFailedError("Not supported algorithm " + algorithm);
         };
         assertTrue(result);
@@ -79,11 +82,11 @@ class TotpTest {
     @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 LocalDateTime movedLocalDateTime = localDateTime.plusSeconds(60);
+        final Instant movedInstant = localDateTime.plusSeconds(60).toInstant(ZoneOffset.UTC);
 
         final boolean result = switch (algorithm) {
-            case "HmacSHA256" -> Totp.validateTotpSha256(otp.getBytes(), fromHex(seed), movedLocalDateTime, DIGITS_NUMBER);
-            case "HmacSHA512" -> Totp.validateTotpSha512(otp.getBytes(), fromHex(seed), movedLocalDateTime, DIGITS_NUMBER);
+            case "HmacSHA256" -> Totp.validateTotpSha256(otp.getBytes(), fromHex(seed), movedInstant, DIGITS_NUMBER);
+            case "HmacSHA512" -> Totp.validateTotpSha512(otp.getBytes(), fromHex(seed), movedInstant, DIGITS_NUMBER);
             default -> throw new AssertionFailedError("Not supported algorithm " + algorithm);
         };
         assertFalse(result);
@@ -91,15 +94,15 @@ class TotpTest {
 
     @Test
     void testGenerateTotpLeftPaddedWithZero() throws Exception {
-        final LocalDateTime localDateTime = LocalDateTime.parse("2023-04-27T01:26:29");
-        final String result = new String(Totp.generateTotpSha256("12345678901234567890".getBytes(), localDateTime, DIGITS_NUMBER));
+        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() {
         var exception = Assertions.assertThrows(CryptoProviderException.class, () ->
-            Totp.validateTotpSha256("1".getBytes(), "12345678901234567890".getBytes(), LocalDateTime.now(), DIGITS_NUMBER));
+            Totp.validateTotpSha256("1".getBytes(), "12345678901234567890".getBytes(), Instant.now(), DIGITS_NUMBER));
         assertEquals("Otp length 1 is different from expected 8", exception.getMessage());
     }
 
@@ -107,6 +110,11 @@ class TotpTest {
         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);
@@ -115,10 +123,11 @@ class TotpTest {
         @Override
         protected LocalDateTime convert(final String source) throws ArgumentConversionException {
             try {
-                return LocalDateTime.parse(source);
+                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
index 82797303..6e0b481b 100644
--- 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
@@ -1,13 +1,13 @@
- Time (sec) ,  UTC Time           , Value of T (hex) ,  TOTP    , Algorithm  , Seed
-         59 , 1970-01-01T00:00:59 , 0000000000000001 , 46119246 , HmacSHA256 , 3132333435363738393031323334353637383930313233343536373839303132
-         59 , 1970-01-01T00:00:59 , 0000000000000001 , 90693936 , HmacSHA512 , 31323334353637383930313233343536373839303132333435363738393031323334353637383930313233343536373839303132333435363738393031323334
- 1111111109 , 2005-03-18T01:58:29 , 00000000023523EC , 68084774 , HmacSHA256 , 3132333435363738393031323334353637383930313233343536373839303132
- 1111111109 , 2005-03-18T01:58:29 , 00000000023523EC , 25091201 , HmacSHA512 , 31323334353637383930313233343536373839303132333435363738393031323334353637383930313233343536373839303132333435363738393031323334
- 1111111111 , 2005-03-18T01:58:31 , 00000000023523ED , 67062674 , HmacSHA256 , 3132333435363738393031323334353637383930313233343536373839303132
- 1111111111 , 2005-03-18T01:58:31 , 00000000023523ED , 99943326 , HmacSHA512 , 31323334353637383930313233343536373839303132333435363738393031323334353637383930313233343536373839303132333435363738393031323334
- 1234567890 , 2009-02-13T23:31:30 , 000000000273EF07 , 91819424 , HmacSHA256 , 3132333435363738393031323334353637383930313233343536373839303132
- 1234567890 , 2009-02-13T23:31:30 , 000000000273EF07 , 93441116 , HmacSHA512 , 31323334353637383930313233343536373839303132333435363738393031323334353637383930313233343536373839303132333435363738393031323334
- 2000000000 , 2033-05-18T03:33:20 , 0000000003F940AA , 90698825 , HmacSHA256 , 3132333435363738393031323334353637383930313233343536373839303132
- 2000000000 , 2033-05-18T03:33:20 , 0000000003F940AA , 38618901 , HmacSHA512 , 31323334353637383930313233343536373839303132333435363738393031323334353637383930313233343536373839303132333435363738393031323334
-20000000000 , 2603-10-11T11:33:20 , 0000000027BC86AA , 77737706 , HmacSHA256 , 3132333435363738393031323334353637383930313233343536373839303132
-20000000000 , 2603-10-11T11:33:20 , 0000000027BC86AA , 47863826 , HmacSHA512 , 31323334353637383930313233343536373839303132333435363738393031323334353637383930313233343536373839303132333435363738393031323334
\ No newline at end of file
+ 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