Skip to content

Commit

Permalink
Fix #680: Update encryption algorithms for crypto4
Browse files Browse the repository at this point in the history
  • Loading branch information
romanstrobl committed Jan 16, 2025
1 parent 13ead9e commit cbc9a09
Show file tree
Hide file tree
Showing 6 changed files with 338 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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}}.
Expand Down
Original file line number Diff line number Diff line change
@@ -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, [email protected]
*/
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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();

Expand Down Expand Up @@ -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);
}

Expand All @@ -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;
}


}
Original file line number Diff line number Diff line change
@@ -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, [email protected]
*/
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;
}

}
Loading

0 comments on commit cbc9a09

Please sign in to comment.