Repository: wultra/powerauth-crypto (java)

Issue:
Crypto 3.1: Increase online signature entropy
In the next version of PowerAuth protocol, we're going to increase entropy of values used in online signatures. This change improves the information entropy in signatures and protection against brute force attacks.

### Proposed solution

The current format is:

- 8 digits, 10<sup>8</sup> combinations
- Example: 1FA: `12345678`, 2FA: `12345678-12345678`
- We'll keep this format for off-line signatures only.

The proposed new format will use Base64 encoding for :

- 16 bytes encoded in Base64, 10<sup>38</sup> combinations
- For 2FA and 3FA, use one Base64 encoded string for 32 or 48 bytes
- Example: 1FA: `MDEyMzQ1Njc4OWFiY2RlZg==`, 2FA: `MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=` 


How to construct such signature value:
```java
public String computeSignature(byte[] data, List<SecretKey> signatureKeys, byte[] CTR_DATA) {

    // ... compute signature components
    boolean isNumericFormat = isOfflineSignature || isOldFormat;
    String[] signatureComponents = new String[signatureKeys.size()];
    byte[] signatureBytes = new byte[signatureKeys.size() * 16];
    
    for (int i = 0; i < signatureKeys.size(); i++) {
        byte[] KEY_SIGNATURE = KeyConversion.secretKeyFromBytes(signatureKey.get(0));
        byte[] KEY_DERIVED = Mac.hmacSha256(KEY_SIGNATURE, CTR_DATA);

        // ... compute signature key using more than one keys, at most 2 extra keys
        // ... this skips the key with index 0 when i == 0
        for (int j = 0; j < i; j++) {
            KEY_SIGNATURE = KeyConversion.secretKeyFromBytes(signatureKey.get(j + 1));
            KEY_DERIVED_CURRENT = Mac.hmacSha256(KEY_SIGNATURE, CTR_DATA);
            KEY_DERIVED = Mac.hmacSha256(KEY_DERIVED_CURRENT, KEY_DERIVED);
        }

        // ... sign the data
        byte[] SIGNATURE_LONG = Mac.hmacSha256(KEY_DERIVED, DATA);

        if (isNumericFormat) {
            // offline signature, or old format ... decimalize the signature component
            int signComponent = (ByteUtils.getInt(ByteUtils.truncate(SIGNATURE_LONG, 4)) & 0x7FFFFFFF) % Math.pow(10,8);
            signatureComponents[i] = String.valueOf(signComponent);
        } else {
            // New online format ... Append last 16 bytes from SIGNATURE_LONG to array with signature bytes
            ByteUtils.copy(SIGNATURE_LONG, 16, signatureBytes, i * 16, 16);
        }
    }
    if (isNumericFormat) {
        // ... join the signature component using "-" character.
        return String.join("-", signatureComponents);   
    } else {
        // New online format
        return Base64.encode(signatureBytes);
    }
}
```

Pull request description:
Fix #280: Signature format validator now supports Base64 signatures.
This change also contains refactoring for `PowerAuthSignatureFormat.getFormatForSignatureVersion()` method and adds new functionality to that enumeration.

Patch:
diff --git a/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/lib/enums/PowerAuthSignatureFormat.java b/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/lib/enums/PowerAuthSignatureFormat.java
index 7daea21e..5b79e037 100644
--- a/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/lib/enums/PowerAuthSignatureFormat.java
+++ b/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/lib/enums/PowerAuthSignatureFormat.java
@@ -19,6 +19,9 @@ package io.getlime.security.powerauth.crypto.lib.enums;
 
 import io.getlime.security.powerauth.crypto.lib.model.exception.GenericCryptoException;
 
+import java.util.HashMap;
+import java.util.Map;
+
 /**
  * Enum with signature format types.
  */
@@ -46,10 +49,32 @@ public enum PowerAuthSignatureFormat {
      *     <li>One factor: {@code MDEyMzQ1Njc4OWFiY2RlZg==}</li>
      *     <li>Two factors: {@code MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=}</li>
      * </ul>
-     * This type of formatting is currently used for {@code 3.1} version of online signatures.
+     * This type of formatting is currently used for {@code 3.1} version of online signatures and newer.
      */
     BASE64;
 
+    /**
+     * Map that translates string into {@link PowerAuthSignatureFormat} enumeration.
+     */
+    private final static Map<String, PowerAuthSignatureFormat> stringToEnumMap = new HashMap<>();
+
+    /**
+     * Map that translates version string into {@link PowerAuthSignatureFormat} enumeration.
+     */
+    private final static Map<String, PowerAuthSignatureFormat> versionToEnumMap = new HashMap<>();
+
+    static {
+        // Prepare string to enumeration mapping
+        for (PowerAuthSignatureFormat format : PowerAuthSignatureFormat.values()) {
+            stringToEnumMap.put(format.toString(), format);
+        }
+        // Prepare version to enumeration mapping
+        versionToEnumMap.put("2.0", DECIMAL);
+        versionToEnumMap.put("2.1", DECIMAL);
+        versionToEnumMap.put("3.0", DECIMAL);
+        versionToEnumMap.put("3.1", BASE64);
+    }
+
     /**
      * Get signature format for signature version.
      *
@@ -59,11 +84,10 @@ public enum PowerAuthSignatureFormat {
      */
     public static PowerAuthSignatureFormat getFormatForSignatureVersion(String signatureVersion) throws GenericCryptoException {
         if (signatureVersion != null) {
-            if ("3.1".equals(signatureVersion)) {
-                return BASE64;
-            }
-            if ("3.0".equals(signatureVersion) || "2.1".equals(signatureVersion) || "2.0".equals(signatureVersion)) {
-                return DECIMAL;
+            // Try to translate known version into the format.
+            final PowerAuthSignatureFormat signatureFormat = versionToEnumMap.get(signatureVersion);
+            if (signatureFormat != null) {
+                return signatureFormat;
             }
             // Fallback in case that we increased the general protocol version, but not updated this function.
             // All versions above 3.1 should require Base64 formatting.
@@ -82,4 +106,29 @@ public enum PowerAuthSignatureFormat {
         // Version is not specified.
         throw new GenericCryptoException("Unspecified signature version");
     }
+
+    /**
+     * Converts string into {@link PowerAuthSignatureFormat} enumeration. Function returns {@code null} in case that
+     * such conversion is not possible.
+     * <p>
+     * You can use {@link #toString()} function as opposite, to convert enumeration into its string representation.
+     *
+     * @param value String representation of the enumeration.
+     * @return {@link PowerAuthSignatureFormat} enumeration or {@code null} if the conversion is not possible.
+     */
+    public static PowerAuthSignatureFormat getEnumFromString(String value) {
+        if (value != null) {
+            return stringToEnumMap.get(value.toUpperCase());
+        }
+        return null;
+    }
+
+    /**
+     * Compare enumeration to another string.
+     * @param otherName Other string to compare
+     * @return {@code true} if other string is equal to this enumeration's string representation.
+     */
+    public boolean equalsName(String otherName) {
+        return toString().equalsIgnoreCase(otherName);
+    }
 }
diff --git a/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/lib/enums/PowerAuthSignatureTypes.java b/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/lib/enums/PowerAuthSignatureTypes.java
index 44a05b5b..4ede56e7 100644
--- a/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/lib/enums/PowerAuthSignatureTypes.java
+++ b/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/lib/enums/PowerAuthSignatureTypes.java
@@ -77,12 +77,14 @@ public enum PowerAuthSignatureTypes {
      * @return Enum value.
      */
     public static PowerAuthSignatureTypes getEnumFromString(String value) {
-        PowerAuthSignatureTypes type = map.get(value.toLowerCase());
-        if (type == null) { // try to guess the most usual suspect...
-            return PowerAuthSignatureTypes.POSSESSION_KNOWLEDGE;
-        } else {
-            return type;
+        if (value != null) {
+            final PowerAuthSignatureTypes type = map.get(value.toLowerCase());
+            if (type != null) {
+                return type;
+            }
         }
+        // Otherwise try to guess the most usual suspect...
+        return PowerAuthSignatureTypes.POSSESSION_KNOWLEDGE;
     }
 
     /**
diff --git a/powerauth-java-http/src/main/java/io/getlime/security/powerauth/http/validator/ValueTypeValidator.java b/powerauth-java-http/src/main/java/io/getlime/security/powerauth/http/validator/ValueTypeValidator.java
index 62b64404..2a6b38b6 100644
--- a/powerauth-java-http/src/main/java/io/getlime/security/powerauth/http/validator/ValueTypeValidator.java
+++ b/powerauth-java-http/src/main/java/io/getlime/security/powerauth/http/validator/ValueTypeValidator.java
@@ -97,7 +97,26 @@ public class ValueTypeValidator {
      * @return True if signature candidate has correct format, false otherwise.
      */
     public static boolean isValidSignatureValue(String signature) {
-        return signature != null && signature.matches(signatureRegex);
+        if (signature != null) {
+            switch (signature.length()) {
+                case 8:
+                case 17:
+                case 26:
+                    // "2.0", "2.1", "3.0" signature version uses "DECIMAL" format
+                    return signature.matches(signatureRegex);
+                case 24:
+                case 44:
+                case 64:
+                    // "3.1" and later signatures uses "BASE64" format.
+                    // We don't need to validate an exact number of encoded bytes. This is due to fact,
+                    // that if input string length can only be 24, 44 or 64, then the encoded output length
+                    // must be 16, 32 or 48.
+                    return isValidBase64OfLengthRange(signature, 16, 48);
+                default:
+                    break;
+            }
+        }
+        return false;
     }
 
     /**


Test patch:
diff --git a/powerauth-java-crypto/src/test/java/io/getlime/security/powerauth/crypto/signature/PowerAuthSignatureFormatTest.java b/powerauth-java-crypto/src/test/java/io/getlime/security/powerauth/crypto/signature/PowerAuthSignatureFormatTest.java
index b7aa3127..c03a42d5 100644
--- a/powerauth-java-crypto/src/test/java/io/getlime/security/powerauth/crypto/signature/PowerAuthSignatureFormatTest.java
+++ b/powerauth-java-crypto/src/test/java/io/getlime/security/powerauth/crypto/signature/PowerAuthSignatureFormatTest.java
@@ -22,6 +22,7 @@ import io.getlime.security.powerauth.crypto.lib.model.exception.GenericCryptoExc
 import org.junit.Test;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
 
 /**
  * Test that validates whether signature version to signature format works properly.
@@ -30,6 +31,7 @@ public class PowerAuthSignatureFormatTest {
 
     @Test
     public void testValidVersions() throws Exception {
+        // Transformation from version string.
         assertEquals(PowerAuthSignatureFormat.DECIMAL, PowerAuthSignatureFormat.getFormatForSignatureVersion("2.0"));
         assertEquals(PowerAuthSignatureFormat.DECIMAL, PowerAuthSignatureFormat.getFormatForSignatureVersion("2.1"));
         assertEquals(PowerAuthSignatureFormat.DECIMAL, PowerAuthSignatureFormat.getFormatForSignatureVersion("3.0"));
@@ -67,4 +69,20 @@ public class PowerAuthSignatureFormatTest {
     public void testInvalidFormat4() throws Exception {
         PowerAuthSignatureFormat.getFormatForSignatureVersion(null);
     }
+
+    @Test
+    public void testEnumToStringConversion() {
+        // Regular formats
+        assertEquals(PowerAuthSignatureFormat.DECIMAL, PowerAuthSignatureFormat.getEnumFromString("decimal"));
+        assertEquals(PowerAuthSignatureFormat.DECIMAL, PowerAuthSignatureFormat.getEnumFromString("DECIMAL"));
+        assertEquals(PowerAuthSignatureFormat.BASE64, PowerAuthSignatureFormat.getEnumFromString("BASE64"));
+        assertEquals(PowerAuthSignatureFormat.BASE64, PowerAuthSignatureFormat.getEnumFromString("base64"));
+        // Invalid formats
+        assertNull(PowerAuthSignatureFormat.getEnumFromString(""));
+        assertNull(PowerAuthSignatureFormat.getEnumFromString("foo"));
+        assertNull(PowerAuthSignatureFormat.getEnumFromString(null));
+        // Enum to string conversion
+        assertEquals("DECIMAL", PowerAuthSignatureFormat.DECIMAL.toString());
+        assertEquals("BASE64", PowerAuthSignatureFormat.BASE64.toString());
+    }
 }