diff --git a/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/lib/util/ByteUtils.java b/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/lib/util/ByteUtils.java index a4e785fa7..ef4b8778d 100644 --- a/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/lib/util/ByteUtils.java +++ b/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/lib/util/ByteUtils.java @@ -106,6 +106,15 @@ public static byte[] encodeLong(long n) { return ByteBuffer.allocate(8).putLong(n).array(); } + /** + * Generate zero bytes of given length. + * @param n Number of zero bytes. + * @return Byte aray. + */ + public static byte[] zeroBytes(int n) { + return ByteBuffer.allocate(n).array(); + } + /** * Encode a String into a byte array. * @param s String to encode. @@ -115,6 +124,31 @@ public static byte[] encodeString(String s) { return s.getBytes(StandardCharsets.UTF_8); } + /** + * Return a subarray from an array. + * @param array Input array. + * @param start Subarray start index (included). + * @param length Subarray end index (excluded). + * @return Subarray. + */ + public static byte[] subarray(byte[] array, int start, int length) { + if (array == null) { + throw new IllegalArgumentException("Parameter array cannot be null"); + } + if (start < 0) { + throw new IllegalArgumentException("Invalid start index"); + } + if (length <= 0) { + throw new IllegalArgumentException("Invalid length"); + } + if (length > array.length - start) { + throw new IllegalArgumentException("Invalid slicing of subarray"); + } + byte[] result = new byte[length]; + System.arraycopy(array, start, result, 0, length); + return result; + } + /** * Returns the values from each provided array combined into a single array. For example, {@code * concat(new byte[] {a, b}, new byte[] {}, new byte[] {c}} returns the array {@code {a, b, c}}. diff --git a/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/lib/v4/Aead.java b/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/lib/v4/Aead.java new file mode 100644 index 000000000..cdb5bd5b5 --- /dev/null +++ b/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/lib/v4/Aead.java @@ -0,0 +1,121 @@ +/* + * 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; + +import io.getlime.security.powerauth.crypto.lib.generator.KeyGenerator; +import io.getlime.security.powerauth.crypto.lib.model.exception.CryptoProviderException; +import io.getlime.security.powerauth.crypto.lib.model.exception.GenericCryptoException; +import io.getlime.security.powerauth.crypto.lib.util.AESEncryptionUtils; +import io.getlime.security.powerauth.crypto.lib.util.ByteUtils; +import io.getlime.security.powerauth.crypto.lib.v4.kdf.Kdf; +import io.getlime.security.powerauth.crypto.lib.v4.kdf.Kmac; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.util.Arrays; + +/** + * Implementation of authenticated encryption with associated data. + * + * @author Roman Strobl, roman.strobl@wultra.com + */ +public class Aead { + + private static final int NONCE_LENGTH = 12; + private static final int KEY_LENGTH = 32; + private static final int TAG_LENGTH = 32; + + private static final byte[] CRYPTO4_AEAD_KMAC_CUSTOM_BYTES = "PA4MAC-AEAD".getBytes(StandardCharsets.UTF_8); + private static final long KEY_ENCRYPTION_INDEX = 20_001L; + private static final long KEY_MAC_INDEX = 20_002L; + + private static final KeyGenerator KEY_GENERATOR = new KeyGenerator(); + private static final AESEncryptionUtils aes = new AESEncryptionUtils(); + + /** + * Encrypt provided plaintext data using AEAD. + * + * @param key Secret key used for encryption. + * @param keyContext Context data used during key derivation. + * @param nonce A 12-byte array used as a unique nonce. If not specified, a random nonce is generated. + * @param associatedData Additional data used as additional input for MAC derivation. + * @param plaintext Plaintext data to encrypt. + * @return Byte array with nonce, MAC, and encrypted ciphertext. + * @throws CryptoProviderException Thrown in case the cryptographic provider could not be initialized. + * @throws GenericCryptoException Thrown in case of any cryptography error. + * @throws InvalidKeyException Thrown in case the secret key is invalid. + */ + public static byte[] seal(SecretKey key, byte[] keyContext, byte[] nonce, byte[] associatedData, byte[] plaintext) throws CryptoProviderException, GenericCryptoException, InvalidKeyException { + if (nonce == null) { + nonce = KEY_GENERATOR.generateRandomBytes(NONCE_LENGTH); + } else if (nonce.length != NONCE_LENGTH) { + throw new GenericCryptoException("Invalid nonce length: " + nonce.length); + } + final SecretKey keyEncryption = Kdf.derive(key, KEY_ENCRYPTION_INDEX, KEY_LENGTH, keyContext); + final SecretKey keyMac = Kdf.derive(key, KEY_MAC_INDEX, TAG_LENGTH, keyContext); + final byte[] iv = ByteUtils.concat(nonce, ByteUtils.zeroBytes(4)); + final byte[] encrypted = aes.encrypt(plaintext, iv, keyEncryption, "AES/CTR/NoPadding"); + final byte[] mac = Kmac.kmac256(keyMac, ByteUtils.concat(nonce, associatedData, encrypted), TAG_LENGTH, CRYPTO4_AEAD_KMAC_CUSTOM_BYTES); + return ByteUtils.concat(nonce, mac, encrypted); + } + + /** + * Decrypt provided ciphertext data using AEAD. + * + * @param key Secret key used for decryption. + * @param keyContext Context data used during key derivation. + * @param associatedData AAdditional data used as additional input for MAC derivation. + * @param ciphertext Byte array with nonce, MAC, and encrypted ciphertext. + * @return Byte array with decrypted data. + * @throws CryptoProviderException Thrown in case the cryptographic provider could not be initialized. + * @throws GenericCryptoException Thrown in case of any cryptography error. + * @throws InvalidKeyException Thrown in case the secret key is invalid. + */ + public static byte[] open(SecretKey key, byte[] keyContext, byte[] associatedData, byte[] ciphertext) throws CryptoProviderException, GenericCryptoException, InvalidKeyException { + if (ciphertext.length < NONCE_LENGTH + TAG_LENGTH) { + throw new GenericCryptoException("Invalid ciphertext length: " + ciphertext.length); + } + final byte[] nonce = ByteUtils.subarray(ciphertext, 0, NONCE_LENGTH); + final byte[] tag = ByteUtils.subarray(ciphertext, NONCE_LENGTH, TAG_LENGTH); + final byte[] encrypted = ByteUtils.subarray(ciphertext, NONCE_LENGTH + TAG_LENGTH, ciphertext.length - NONCE_LENGTH - TAG_LENGTH); + final SecretKey keyEncryption = Kdf.derive(key, KEY_ENCRYPTION_INDEX, KEY_LENGTH, keyContext); + final SecretKey keyMac = Kdf.derive(key, KEY_MAC_INDEX, TAG_LENGTH, keyContext); + byte[] mac = Kmac.kmac256(keyMac, ByteUtils.concat(nonce, associatedData, encrypted), TAG_LENGTH, CRYPTO4_AEAD_KMAC_CUSTOM_BYTES); + if (!Arrays.equals(mac, tag)) { + throw new GenericCryptoException("Invalid MAC"); + } + byte[] iv = ByteUtils.concat(nonce, ByteUtils.zeroBytes(4)); + return aes.decrypt(encrypted, iv, keyEncryption, "AES/CTR/NoPadding"); + } + + /** + * Extract the nonce from provided ciphertext. + * + * @param ciphertext Byte array containing the nonce, MAC, and encrypted ciphertext. + * @return Byte array with extracted nonce. + * @throws GenericCryptoException Thrown in case the input ciphertext in invalid. + */ + public static byte[] extractNonce(byte[] ciphertext) throws GenericCryptoException { + if (ciphertext.length < NONCE_LENGTH + TAG_LENGTH) { + throw new GenericCryptoException("Invalid ciphertext length: " + ciphertext.length); + } + return ByteUtils.subarray(ciphertext, 0, NONCE_LENGTH); + } + +} 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 index a0ba7c8e1..ab038035e 100644 --- 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 @@ -20,8 +20,6 @@ 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; @@ -35,7 +33,6 @@ 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(); @@ -63,7 +60,7 @@ public static SecretKey derive(SecretKey key, long index, int outLength, byte[] } else { data = indexBytes; } - final byte[] output = kmac256(key, data, outLength, CRYPTO4_KDF_CUSTOM_BYTES); + final byte[] output = Kmac.kmac256(key, data, outLength, CRYPTO4_KDF_CUSTOM_BYTES); return KEY_CONVERTOR.convertBytesToSharedSecretKey(output); } @@ -88,40 +85,10 @@ public static SecretKey derivePassword(String password, byte[] salt, int outLeng } final byte[] passwordBytes = ByteUtils.encodeString(password); final SecretKey key = KEY_CONVERTOR.convertBytesToSharedSecretKey(passwordBytes); - final byte[] output = kmac256(key, salt, outLength, CRYPTO4_PBKDF_CUSTOM_BYTES); + final byte[] output = Kmac.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; - } + } diff --git a/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/lib/v4/kdf/Kmac.java b/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/lib/v4/kdf/Kmac.java new file mode 100644 index 000000000..24e3c4bf2 --- /dev/null +++ b/powerauth-java-crypto/src/main/java/io/getlime/security/powerauth/crypto/lib/v4/kdf/Kmac.java @@ -0,0 +1,67 @@ +/* + * 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 org.bouncycastle.crypto.macs.KMAC; +import org.bouncycastle.crypto.params.KeyParameter; + +import javax.crypto.SecretKey; + +/** + * KMAC based on Keccak. + * + * @author Roman Strobl, roman.strobl@wultra.com + */ +public class Kmac { + + private static final int KMAC_BIT_LENGTH = 256; + + /** + * 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. + */ + public 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; + } + +} diff --git a/powerauth-java-crypto/src/test/java/io/getlime/security/powerauth/crypto/lib/v4/AeadTest.java b/powerauth-java-crypto/src/test/java/io/getlime/security/powerauth/crypto/lib/v4/AeadTest.java new file mode 100644 index 000000000..0bc7afdac --- /dev/null +++ b/powerauth-java-crypto/src/test/java/io/getlime/security/powerauth/crypto/lib/v4/AeadTest.java @@ -0,0 +1,110 @@ +/* + * 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; + +import io.getlime.security.powerauth.crypto.lib.generator.KeyGenerator; +import io.getlime.security.powerauth.crypto.lib.model.exception.GenericCryptoException; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import javax.crypto.SecretKey; + +import java.security.Security; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test for AEAD encryption and decryption. + * + * @author Roman Strobl, roman.strobl@wultra.com + */ +class AeadTest { + + private static final KeyGenerator KEY_GENERATOR = new KeyGenerator(); + private SecretKey key; + private byte[] plaintext; + private byte[] keyContext; + private byte[] associatedData; + + static { + Security.addProvider(new BouncyCastleProvider()); + } + + @BeforeEach + void setUp() throws Exception { + key = KEY_GENERATOR.generateRandomSecretKey(); + plaintext = KEY_GENERATOR.generateRandomBytes(128); + keyContext = KEY_GENERATOR.generateRandomBytes(16); + associatedData = KEY_GENERATOR.generateRandomBytes(16); + } + + @Test + void testAeadSuccess() throws Exception { + byte[] nonce = new KeyGenerator().generateRandomBytes(12); + byte[] ciphertext = Aead.seal(key, keyContext, nonce, associatedData, plaintext); + assertNotNull(ciphertext); + byte[] decrypted = Aead.open(key, keyContext, associatedData, ciphertext); + assertArrayEquals(plaintext, decrypted); + } + + @Test + void testAeadSuccessGeneratedNonce() throws Exception { + byte[] ciphertext = Aead.seal(key, keyContext, null, associatedData, plaintext); + assertNotNull(ciphertext); + byte[] decrypted = Aead.open(key, keyContext, associatedData, ciphertext); + assertArrayEquals(plaintext, decrypted); + } + + @Test + void testInvalidMac() throws Exception { + byte[] nonce = new KeyGenerator().generateRandomBytes(12); + byte[] ciphertext = Aead.seal(key, keyContext, nonce, associatedData, plaintext); + ciphertext[0] += 1; + assertThrows(GenericCryptoException.class, () -> Aead.open(key, keyContext, associatedData, ciphertext)); + } + + @Test + void testInvalidNonceLength() { + byte[] invalidNonce = new byte[11]; + assertThrows(GenericCryptoException.class, () -> Aead.seal(key, keyContext, invalidNonce, associatedData, plaintext), "Invalid nonce length should throw exception"); + } + + @Test + void testExtractNonce() throws Exception { + byte[] nonce = new KeyGenerator().generateRandomBytes(12); + byte[] ciphertext = Aead.seal(key, keyContext, nonce, associatedData, plaintext); + byte[] extractedNonce = Aead.extractNonce(ciphertext); + assertArrayEquals(nonce, extractedNonce); + } + + @Test + void testInvalidKey() throws Exception { + SecretKey wrongKey = KEY_GENERATOR.generateRandomSecretKey(); + byte[] ciphertext = Aead.seal(key, keyContext, null, associatedData, plaintext); + assertThrows(GenericCryptoException.class, () -> Aead.open(wrongKey, keyContext, associatedData, ciphertext), "Decryption with an invalid key should throw exception"); + } + + @Test + void testNullAssociatedData() throws Exception { + byte[] ciphertext = Aead.seal(key, keyContext, null, null, plaintext); + byte[] decrypted = Aead.open(key, keyContext, null, ciphertext); + assertArrayEquals(plaintext, decrypted); + } + +} \ No newline at end of file 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 index 717cd7879..325310af1 100644 --- 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 @@ -42,7 +42,7 @@ void testKmac256Vector4() throws Exception { int outputLength = 64; byte[] expectedOutput = Hex.decode("20C570C31346F703C9AC36C61C03CB64C3970D0CFC787E9B79599D273A68D2F7F69D4CC3DE9D104A351689F27CF6F5951F0103F33F4F24871024D9C27773A8DD"); SecretKey secretKey = new SecretKeySpec(key, "AES"); - byte[] output = Kdf.kmac256(secretKey, data, outputLength, customString); + byte[] output = Kmac.kmac256(secretKey, data, outputLength, customString); assertArrayEquals(expectedOutput, output, "KMAC256 output does not match expected output."); } @@ -70,7 +70,7 @@ void testKmac256Vector5() throws Exception { "589D27CF5E15369CBBFF8B9A4C2EB178" + "00855D0235FF635DA82533EC6B759B69"); SecretKey secretKey = new SecretKeySpec(key, "AES"); - byte[] output = Kdf.kmac256(secretKey, data, outputLength, customString); + byte[] output = Kmac.kmac256(secretKey, data, outputLength, customString); assertArrayEquals(expectedOutput, output, "KMAC256 output does not match expected output."); } @@ -98,7 +98,7 @@ void testKmac256Vector6() throws Exception { "70FBACFDE50033AEA585F1A2708510C3" + "2D07880801BD182898FE476876FC8965"); SecretKey secretKey = new SecretKeySpec(key, "AES"); - byte[] output = Kdf.kmac256(secretKey, data, outputLength, customString); + byte[] output = Kmac.kmac256(secretKey, data, outputLength, customString); assertArrayEquals(expectedOutput, output, "KMAC256 output does not match expected output."); }