I want to decrypt and encrypt a string using chacha20
BouncyCastleProvider is using chacha20 technique. So I included i
Unlike some other modes in AES like CBC, GCM mode does not require the IV to be unpredictable (same as Chacha20-Poly1305). The only requirement is that the IV (for AES) or nonce (for Chacha20-Poly1305) has to be unique for each invocation with a given key. If it repeats once for a given key, security can be compromised. An easy way to achieve this with good probability is to use a random IV or nonce from a strong pseudo random number generator as shown below. Probability of an IV or nonce collision (assuming a strong random source) will be at most 2^-32, which is low enough to deter attackers.
Using a sequence or timestamp as IV or nonce is also possible, but it may not be as trivial as it may sound. For example, if the system does not correctly keep track of the sequences already used as IV in a persistent store, an invocation may repeat an IV after a system reboot. Likewise, there is no perfect clock. Computer clocks readjusts etc.
Also, the key should be rotated after every 2^32 invocations.
SecureRandom.getInstanceStrong()
can be used to generate a cryptographically strong random nonce.
Now that ChaCha20 is supported is Java 11. Here is a sample program for encrypting and decrypting using ChaCha20-Poly1305.
The possible reasons for using ChaCha20-Poly1305 (which is a stream cipher based authenticated encryption algorithm) over AES-GCM (which is an authenticated block cipher algorithm) are:
ChaCha20 is not vulnerable to cache-collision timing attacks unlike AES [1]
package com.sapbasu.javastudy;
import java.lang.reflect.Field;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.Objects;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.security.auth.Destroyable;
/**
*
* The possible reasons for using ChaCha20-Poly1305 which is a
* stream cipher based authenticated encryption algorithm
* 1. If the CPU does not provide dedicated AES instructions,
* ChaCha20 is faster than AES
* 2. ChaCha20 is not vulnerable to cache-collision timing
* attacks unlike AES
* 3. Since the nonce is not required to be random. There is
* no overhead for generating cryptographically secured
* pseudo random number
*
*/
public class CryptoChaCha20 {
private static final String ENCRYPT_ALGO = "ChaCha20-Poly1305/None/NoPadding";
private static final int KEY_LEN = 256;
private static final int NONCE_LEN = 12; //bytes
private static final BigInteger NONCE_MIN_VAL = new BigInteger("100000000000000000000000", 16);
private static final BigInteger NONCE_MAX_VAL = new BigInteger("ffffffffffffffffffffffff", 16);
private static BigInteger nonceCounter = NONCE_MIN_VAL;
public static byte[] encrypt(byte[] input, SecretKeySpec key)
throws Exception {
Objects.requireNonNull(input, "Input message cannot be null");
Objects.requireNonNull(key, "key cannot be null");
if (input.length == 0) {
throw new IllegalArgumentException("Length of message cannot be 0");
}
if (key.getEncoded().length * 8 != KEY_LEN) {
throw new IllegalArgumentException("Size of key must be 256 bits");
}
Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO);
byte[] nonce = getNonce();
IvParameterSpec ivParameterSpec = new IvParameterSpec(nonce);
cipher.init(Cipher.ENCRYPT_MODE, key, ivParameterSpec);
byte[] messageCipher = cipher.doFinal(input);
// Prepend the nonce with the message cipher
byte[] cipherText = new byte[messageCipher.length + NONCE_LEN];
System.arraycopy(nonce, 0, cipherText, 0, NONCE_LEN);
System.arraycopy(messageCipher, 0, cipherText, NONCE_LEN,
messageCipher.length);
return cipherText;
}
public static byte[] decrypt(byte[] input, SecretKeySpec key)
throws Exception {
Objects.requireNonNull(input, "Input message cannot be null");
Objects.requireNonNull(key, "key cannot be null");
if (input.length == 0) {
throw new IllegalArgumentException("Input array cannot be empty");
}
byte[] nonce = new byte[NONCE_LEN];
System.arraycopy(input, 0, nonce, 0, NONCE_LEN);
byte[] messageCipher = new byte[input.length - NONCE_LEN];
System.arraycopy(input, NONCE_LEN, messageCipher, 0, input.length - NONCE_LEN);
IvParameterSpec ivParameterSpec = new IvParameterSpec(nonce);
Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO);
cipher.init(Cipher.DECRYPT_MODE, key, ivParameterSpec);
return cipher.doFinal(messageCipher);
}
/**
*
* This method creates the 96 bit nonce. A 96 bit nonce
* is required for ChaCha20-Poly1305. The nonce is not
* a secret. The only requirement being it has to be
* unique for a given key. The following function implements
* a 96 bit counter which when invoked always increments
* the counter by one.
*
* @return
*/
public static byte[] getNonce() {
if (nonceCounter.compareTo(NONCE_MAX_VAL) == -1) {
return nonceCounter.add(BigInteger.ONE).toByteArray();
} else {
nonceCounter = NONCE_MIN_VAL;
return NONCE_MIN_VAL.toByteArray();
}
}
/**
*
* Strings should not be used to hold the clear text message or the key, as
* Strings go in the String pool and they will show up in a heap dump. For the
* same reason, the client calling these encryption or decryption methods
* should clear all the variables or arrays holding the message or the key
* after they are no longer needed. Since Java 8 does not provide an easy
* mechanism to clear the key from {@code SecretKeySpec}, this method uses
* reflection to clear the key
*
* @param key
* The secret key used to do the encryption
* @throws IllegalArgumentException
* @throws IllegalAccessException
* @throws NoSuchFieldException
* @throws SecurityException
*/
@SuppressWarnings("unused")
public static void clearSecret(Destroyable key)
throws IllegalArgumentException, IllegalAccessException,
NoSuchFieldException, SecurityException {
Field keyField = key.getClass().getDeclaredField("key");
keyField.setAccessible(true);
byte[] encodedKey = (byte[]) keyField.get(key);
Arrays.fill(encodedKey, Byte.MIN_VALUE);
}
}
And, here is a JUnit test:
package com.sapbasu.javastudy;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.security.SecureRandom;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.junit.jupiter.api.Test;
public class CryptoChaCha20Test {
private int KEY_LEN = 256; // bits
@Test
public void whenDecryptCalled_givenEncryptedTest_returnsDecryptedBytes()
throws Exception {
char[] input = {'e', 'n', 'c', 'r', 'y', 'p', 't', 'i', 'o', 'n'};
byte[] inputBytes = convertInputToBytes(input);
KeyGenerator keyGen = KeyGenerator.getInstance("ChaCha20");
keyGen.init(KEY_LEN, SecureRandom.getInstanceStrong());
SecretKey secretKey = keyGen.generateKey();
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(),
"ChaCha20");
CryptoChaCha20.clearSecret(secretKey);
byte[] encryptedBytes = CryptoChaCha20.encrypt(inputBytes, secretKeySpec);
byte[] decryptedBytes = CryptoChaCha20.decrypt(encryptedBytes, secretKeySpec);
CryptoChaCha20.clearSecret(secretKeySpec);
assertArrayEquals(inputBytes, decryptedBytes);
}
private byte[] convertInputToBytes(char[] input) {
CharBuffer charBuf = CharBuffer.wrap(input);
ByteBuffer byteBuf = Charset.forName(Charset.defaultCharset().name())
.encode(charBuf);
byte[] inputBytes = byteBuf.array();
charBuf.clear();
byteBuf.clear();
return inputBytes;
}
}
The output of a cipher consists of random bits (generally limited by implementations to 8-bit bytes). Random bytes are likely to contain invalid characters in any character set. If you require a String, encode the ciphertext to base 64.
Furthermore, you re-generate the IV on decrypt. IV during encryption/decryption should match.