Repository: wultra/powerauth-crypto (java)

Issue:
New KDF for crypto4
### Description

New universal KDF uses KMAC-256 defined as:

- `byte[] Mac.kmac256(SecretKey key, byte[] data, byte[] custom, int outLength)` where
  - `key` (K) is a key of any length
  - `data` (X) is the main input data. It may be of any length, including zero.
  - `custom` (S) is an optional customization string. Use UTF-8 to reinterpret string into bytes.
  - `outLength` (L) is an integer representing the requested output length in bytes.

Then KDF implementation is defined as:

- `SecretKey KDF.derive(SecretKey key, long index, byte[] context = null, int outLength = 32)`
  - `key` - is key to be derived
  - `index` - is numeric index to distinguish multiple keys.
  - `context` - optional byte array containing the information related to the derived keying material
  - `outLength` - integer representing the requested output length in bytes.

Cover the new KDF by tests.

### Acceptance criteria

KDF function is implemented and covered by tests.

### Technical specification

- Pseudocode:
  ```java
    byte[] INDEX_BYTES = ByteUtils.encode(index);
  byte[] CTX;
  if (context != null) {
      CTX = ByteUtils.concat(INDEX_BYTES, ByteUtils.concatWithSizes(context));
  } else {
      CTX = INDEX_BYTES;
  }
  byte[] K_DERIVED = Mac.kmac256(key, CTX, "PA4KDF", outLength);
  ```

### QA specification

Tested by unit tests.


Pull request description:
Fix #676: New KDF for crypto4

Patch:
diff --git a/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/lib/v4/kdf/Kdf.java b/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/lib/v4/kdf/Kdf.java
new file mode 100644
index 00000000..a0ba7c8e
--- /dev/null
+++ b/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/lib/v4/kdf/Kdf.java
@@ -0,0 +1,127 @@
+/*
+ * PowerAuth Crypto Library
+ * Copyright 2024 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.v4.kdf;
+
+import io.getlime.security.powerauth.crypto.lib.model.exception.GenericCryptoException;
+import io.getlime.security.powerauth.crypto.lib.util.ByteUtils;
+import io.getlime.security.powerauth.crypto.lib.util.KeyConvertor;
+import org.bouncycastle.crypto.macs.KMAC;
+import org.bouncycastle.crypto.params.KeyParameter;
+
+import javax.crypto.SecretKey;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Universal KDF based on KMAC-256 (Keccak).
+ *
+ * @author Roman Strobl, roman.strobl@wultra.com
+ */
+public class Kdf {
+
+    private static final byte[] CRYPTO4_KDF_CUSTOM_BYTES = "PA4KDF".getBytes(StandardCharsets.UTF_8);
+    private static final byte[] CRYPTO4_PBKDF_CUSTOM_BYTES = "PA4PBKDF".getBytes(StandardCharsets.UTF_8);
+    private static final int KMAC_BIT_LENGTH = 256;
+
+    private static final KeyConvertor KEY_CONVERTOR = new KeyConvertor();
+
+    /**
+     * Derive a secret key based on an input key, numeric key index, requested key size and optional context.
+     *
+     * @param key Secret key to be used for key derivation.
+     * @param index Key index (numeric).
+     * @param outLength Requested derived key size.
+     * @param context Optional context to use during key derivation.
+     * @return Derived secret key.
+     * @throws GenericCryptoException Thrown in case of any cryptography error.
+     */
+    public static SecretKey derive(SecretKey key, long index, int outLength, byte[] context) throws GenericCryptoException {
+        if (key == null) {
+            throw new GenericCryptoException("Missing secret key for key derivation");
+        }
+        if (index < 0L) {
+            throw new GenericCryptoException("Invalid index used for key derivation");
+        }
+        final byte[] indexBytes = ByteUtils.encodeLong(index);
+        final byte[] data;
+        if (context != null) {
+            data = ByteUtils.concat(indexBytes, ByteUtils.concatWithSizes(context));
+        } else {
+            data = indexBytes;
+        }
+        final byte[] output = kmac256(key, data, outLength, CRYPTO4_KDF_CUSTOM_BYTES);
+        return KEY_CONVERTOR.convertBytesToSharedSecretKey(output);
+    }
+
+    /**
+     * Derive a key using password-based key derivation.
+     *
+     * @param password Password used for the key derivation.
+     * @param salt Salt used for the key derivation.
+     * @param outLength Requested output length.
+     * @return Derived secret key.
+     * @throws GenericCryptoException Thrown in case of any cryptography error.
+     */
+    public static SecretKey derivePassword(String password, byte[] salt, int outLength) throws GenericCryptoException {
+        if (password == null || password.isEmpty()) {
+            throw new GenericCryptoException("Missing password for key derivation");
+        }
+        if (salt == null) {
+            throw new GenericCryptoException("Missing salt for key derivation");
+        }
+        if (salt.length < 32) {
+            throw new GenericCryptoException("Insufficient salt length");
+        }
+        final byte[] passwordBytes = ByteUtils.encodeString(password);
+        final SecretKey key = KEY_CONVERTOR.convertBytesToSharedSecretKey(passwordBytes);
+        final byte[] output = kmac256(key, salt, outLength, CRYPTO4_PBKDF_CUSTOM_BYTES);
+        return KEY_CONVERTOR.convertBytesToSharedSecretKey(output);
+    }
+
+    /**
+     * Compute the KMAC256 of the given data using provided secret key, output length and optional customization string.
+     *
+     * @param key          The secret key, must be a valid {@link SecretKey} with a 256-bit key length.
+     * @param data         The input data used for the KMAC.
+     * @param outLength    The length of generated output bytes.
+     * @param customString An optional customization string, use null value for no customization.
+     * @return KMAC256 output byte array.
+     * @throws GenericCryptoException Thrown in case of any cryptography error.
+     */
+    static byte[] kmac256(SecretKey key, byte[] data, int outLength, byte[] customString) throws GenericCryptoException {
+        if (key == null) {
+            throw new GenericCryptoException("Missing secret key for KDF");
+        }
+        if (data == null) {
+            throw new GenericCryptoException("Missing data for KDF");
+        }
+        if (outLength <= 0) {
+            throw new GenericCryptoException("Invalid output length for KDF");
+        }
+        final KMAC kmac = new KMAC(KMAC_BIT_LENGTH, customString);
+        final byte[] keyBytes = key.getEncoded();
+        if (keyBytes == null) {
+            throw new GenericCryptoException("Secret key encoding is null");
+        }
+        kmac.init(new KeyParameter(keyBytes));
+        kmac.update(data, 0, data.length);
+        final byte[] output = new byte[outLength];
+        kmac.doFinal(output, 0, outLength);
+        return output;
+    }
+
+}


Test patch:
diff --git a/powerauth-java-crypto/src/test/java/io/getlime/security/powerauth/crypto/lib/v4/kdf/KdfTest.java b/powerauth-java-crypto/src/test/java/io/getlime/security/powerauth/crypto/lib/v4/kdf/KdfTest.java
new file mode 100644
index 00000000..717cd787
--- /dev/null
+++ b/powerauth-java-crypto/src/test/java/io/getlime/security/powerauth/crypto/lib/v4/kdf/KdfTest.java
@@ -0,0 +1,105 @@
+/*
+ * PowerAuth Crypto Library
+ * Copyright 2024 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.v4.kdf;
+
+import org.bouncycastle.util.encoders.Hex;
+import org.junit.jupiter.api.Test;
+
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+
+/**
+ * Tests for KDF based on KMAC256 using NIST test vectors.
+ *
+ * @author Roman Strobl, roman.strobl@wultra.com
+ */
+class KdfTest {
+
+    @Test
+    void testKmac256Vector4() throws Exception {
+        // Test Vector 4 (https://csrc.nist.gov/csrc/media/projects/cryptographic-standards-and-guidelines/documents/examples/kmac_samples.pdf)
+        byte[] key = Hex.decode("404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F");
+        byte[] data = Hex.decode("00010203");
+        byte[] customString = "My Tagged Application".getBytes(StandardCharsets.UTF_8);
+        int outputLength = 64;
+        byte[] expectedOutput = Hex.decode("20C570C31346F703C9AC36C61C03CB64C3970D0CFC787E9B79599D273A68D2F7F69D4CC3DE9D104A351689F27CF6F5951F0103F33F4F24871024D9C27773A8DD");
+        SecretKey secretKey = new SecretKeySpec(key, "AES");
+        byte[] output = Kdf.kmac256(secretKey, data, outputLength, customString);
+        assertArrayEquals(expectedOutput, output, "KMAC256 output does not match expected output.");
+    }
+
+    @Test
+    void testKmac256Vector5() throws Exception {
+        // Test Vector 5 (https://csrc.nist.gov/csrc/media/projects/cryptographic-standards-and-guidelines/documents/examples/kmac_samples.pdf)
+        byte[] key = Hex.decode("404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F");
+        byte[] data = Hex.decode("000102030405060708090A0B0C0D0E0F" +
+                "101112131415161718191A1B1C1D1E1F" +
+                "202122232425262728292A2B2C2D2E2F" +
+                "303132333435363738393A3B3C3D3E3F" +
+                "404142434445464748494A4B4C4D4E4F" +
+                "505152535455565758595A5B5C5D5E5F" +
+                "606162636465666768696A6B6C6D6E6F" +
+                "707172737475767778797A7B7C7D7E7F" +
+                "808182838485868788898A8B8C8D8E8F" +
+                "909192939495969798999A9B9C9D9E9F" +
+                "A0A1A2A3A4A5A6A7A8A9AAABACADAEAF" +
+                "B0B1B2B3B4B5B6B7B8B9BABBBCBDBEBF" +
+                "C0C1C2C3C4C5C6C7");
+        byte[] customString = null;
+        int outputLength = 64;
+        byte[] expectedOutput = Hex.decode("75358CF39E41494E949707927CEE0AF2" +
+                "0A3FF553904C86B08F21CC414BCFD691" +
+                "589D27CF5E15369CBBFF8B9A4C2EB178" +
+                "00855D0235FF635DA82533EC6B759B69");
+        SecretKey secretKey = new SecretKeySpec(key, "AES");
+        byte[] output = Kdf.kmac256(secretKey, data, outputLength, customString);
+        assertArrayEquals(expectedOutput, output, "KMAC256 output does not match expected output.");
+    }
+
+    @Test
+    void testKmac256Vector6() throws Exception {
+        // Test Vector 6 (https://csrc.nist.gov/csrc/media/projects/cryptographic-standards-and-guidelines/documents/examples/kmac_samples.pdf)
+        byte[] key = Hex.decode("404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F");
+        byte[] data = Hex.decode("000102030405060708090A0B0C0D0E0F" +
+                "101112131415161718191A1B1C1D1E1F" +
+                "202122232425262728292A2B2C2D2E2F" +
+                "303132333435363738393A3B3C3D3E3F" +
+                "404142434445464748494A4B4C4D4E4F" +
+                "505152535455565758595A5B5C5D5E5F" +
+                "606162636465666768696A6B6C6D6E6F" +
+                "707172737475767778797A7B7C7D7E7F" +
+                "808182838485868788898A8B8C8D8E8F" +
+                "909192939495969798999A9B9C9D9E9F" +
+                "A0A1A2A3A4A5A6A7A8A9AAABACADAEAF" +
+                "B0B1B2B3B4B5B6B7B8B9BABBBCBDBEBF" +
+                "C0C1C2C3C4C5C6C7");
+        byte[] customString = "My Tagged Application".getBytes(StandardCharsets.UTF_8);
+        int outputLength = 64;
+        byte[] expectedOutput = Hex.decode("B58618F71F92E1D56C1B8C55DDD7CD18" +
+                "8B97B4CA4D99831EB2699A837DA2E4D9" +
+                "70FBACFDE50033AEA585F1A2708510C3" +
+                "2D07880801BD182898FE476876FC8965");
+        SecretKey secretKey = new SecretKeySpec(key, "AES");
+        byte[] output = Kdf.kmac256(secretKey, data, outputLength, customString);
+        assertArrayEquals(expectedOutput, output, "KMAC256 output does not match expected output.");
+    }
+
+}
\ No newline at end of file