Update on 12-DEC-2019
Unlike some other modes like CBC, GCM mode does not require the IV to be unpredictable. The only requirement is that the IV 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 is to use a random IV from a strong pseudo random number generator as shown below.
Using a sequence or timestamp as IV 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 clock readjusts etc.
Also, the key should be rotated after every 2^32 invocations. For further details on the IV requirement, refer to this answer and the NIST recommendations.
This is the encryption & decryption code I just wrote in Java 8 considering the following points. Hope someone would find this useful:
Encryption Algorithm: Block cipher AES with 256 bits key is considered secure enough. To encrypt a complete message, a mode needs to be selected. Authenticated encryption (which provides both confidentiality and integrity) is recommended. GCM, CCM and EAX are most commonly used authenticated encryption modes. GCM is usually preferred and it performs well in Intel architectures which provide dedicated instructions for GCM. All these three modes are CTR-based (counter-based) modes and therefore they do not need padding. As a result they are not vulnerable to padding related attacks
An initialization Vector (IV) is required for GCM. The IV is not a secret. The only requirement being it has to be random or unpredictable. In Java, the SecuredRandom
class is meant to produce cryptographically strong pseudo random numbers. The pseudo-random number generation algorithm can be specified in the getInstance()
method. However, since Java 8, the recommended way is to use getInstanceStrong()
method which will use the strongest algorithm configured and provided by the Provider
NIST recommends 96 bit IV for GCM to promote interoperability, efficiency, and simplicity of design
To ensure additional security, in the following implementation SecureRandom
is re-seeded after producing every 2^16 bytes of pseudo random byte generation
The recipient needs to know the IV to be able to decrypt the cipher text. Therefore the IV needs to be transferred along with the cipher text. Some implementations send the IV as AD (Associated Data) which means that the authentication tag will be calculated on both the cipher text and the IV. However, that is not required. The IV can be simply pre-pended with the cipher text because if the IV is changed during transmission due to a deliberate attack or network/file system error, the authentication tag validation will fail anyway
Strings should not be used to hold the clear text message or the key as Strings are immutable and thus we cannot clear them after use. These uncleared Strings then linger in the memory and may 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.
No provider is hard coded in the code following the general recommendations
Finally for transmission over network or storage, the key or the cipher text should be encoded using Base64 encoding. The details of Base64 can be found here. The Java 8 approach should be followed
Byte arrays can be cleared using:
Arrays.fill(clearTextMessageByteArray, Byte.MIN_VALUE);
However, as of Java 8, there is no easy way to clear SecretKeyspec
and SecretKey
as the implementations of these two interfaces do not seem to have implemented the method destroy()
of the interface Destroyable
. In the following code, a separate method is written to clear the SecretKeySpec
and SecretKey
using reflection.
Key should be generated using one of the two approaches mentioned below.
Note that keys are secrets like passwords, but unlike passwords which are meant for human use, keys are meant to be used by cryptographic algorithms and hence should be generated using the above way only.
package com.sapbasu.javastudy;
import java.lang.reflect.Field;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class Crypto {
private static final int AUTH_TAG_SIZE = 128; // bits
// NIST recommendation: "For IVs, it is recommended that implementations
// restrict support to the length of 96 bits, to
// promote interoperability, efficiency, and simplicity of design."
private static final int IV_LEN = 12; // bytes
// number of random number bytes generated before re-seeding
private static final double PRNG_RESEED_INTERVAL = Math.pow(2, 16);
private static final String ENCRYPT_ALGO = "AES/GCM/NoPadding";
private static final List<Integer> ALLOWED_KEY_SIZES = Arrays
.asList(new Integer[] {128, 192, 256}); // bits
private static SecureRandom prng;
// Used to keep track of random number bytes generated by PRNG
// (for the purpose of re-seeding)
private static int bytesGenerated = 0;
public 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 (!ALLOWED_KEY_SIZES.contains(key.getEncoded().length * 8)) {
throw new IllegalArgumentException("Size of key must be 128, 192 or 256");
}
Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO);
byte[] iv = getIV(IV_LEN);
GCMParameterSpec gcmParamSpec = new GCMParameterSpec(AUTH_TAG_SIZE, iv);
cipher.init(Cipher.ENCRYPT_MODE, key, gcmParamSpec);
byte[] messageCipher = cipher.doFinal(input);
// Prepend the IV with the message cipher
byte[] cipherText = new byte[messageCipher.length + IV_LEN];
System.arraycopy(iv, 0, cipherText, 0, IV_LEN);
System.arraycopy(messageCipher, 0, cipherText, IV_LEN,
messageCipher.length);
return cipherText;
}
public 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[] iv = new byte[IV_LEN];
System.arraycopy(input, 0, iv, 0, IV_LEN);
byte[] messageCipher = new byte[input.length - IV_LEN];
System.arraycopy(input, IV_LEN, messageCipher, 0, input.length - IV_LEN);
GCMParameterSpec gcmParamSpec = new GCMParameterSpec(AUTH_TAG_SIZE, iv);
Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO);
cipher.init(Cipher.DECRYPT_MODE, key, gcmParamSpec);
return cipher.doFinal(messageCipher);
}
public byte[] getIV(int bytesNum) {
if (bytesNum < 1) throw new IllegalArgumentException(
"Number of bytes must be greater than 0");
byte[] iv = new byte[bytesNum];
prng = Optional.ofNullable(prng).orElseGet(() -> {
try {
prng = SecureRandom.getInstanceStrong();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Wrong algorithm name", e);
}
return prng;
});
if (bytesGenerated > PRNG_RESEED_INTERVAL || bytesGenerated == 0) {
prng.setSeed(prng.generateSeed(bytesNum));
bytesGenerated = 0;
}
prng.nextBytes(iv);
bytesGenerated = bytesGenerated + bytesNum;
return iv;
}
private 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);
}
}
The encryption key can be generated primarily in two ways:
Without any password
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(KEY_LEN, SecureRandom.getInstanceStrong());
SecretKey secretKey = keyGen.generateKey();
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(),
"AES");
Crypto.clearSecret(secretKey);
// After encryption or decryption with key
Crypto.clearSecret(secretKeySpec);
With password
SecureRandom random = SecureRandom.getInstanceStrong();
byte[] salt = new byte[32];
random.nextBytes(salt);
PBEKeySpec keySpec = new PBEKeySpec(password, salt, iterations,
keyLength);
SecretKeyFactory keyFactory =
SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
SecretKey secretKey = keyFactory.generateSecret(keySpec);
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(),
"AES");
Crypto.clearSecret(secretKey);
// After encryption or decryption with key
Crypto.clearSecret(secretKeySpec);
As pointed out by @MaartenBodewes, my answer did not handle any String
as is required by the question. Therefore, I'll make an attempt to fill that gap just in case someone stumbles upon this answer and leaves wondering about handling String
.
As indicated earlier in the answer, handling sensitive information in a String
is, in general, not a good idea because String
is immutable and thus we cannot clear it off after use. And as we know, even when a String
doesn't have a strong reference, the garbage collector does not immediately rush to remove it off heap. Thus, the String
continues to be around in the memory for an unknown window of time even though it is not accessible to the program. The issue with that is, a heap dump during that time frame would reveal the sensitive information. Therefore, it is always better to handle all sensitive information in a byte array or char array and then fill the array with 0s once their purpose is served.
However, with all that knowledge, if we still end up in a situation where the sensitive information to be encrypted is in a String
, we first need to convert it into a byte array and invoke the encrypt
and decrypt
functions introduced above. (The other input key can be generated using the code snippet provided above).
A String
can be converted into bytes in the following way:
byte[] inputBytes = inputString.getBytes(StandardCharsets.UTF_8);
As of Java 8, String
is internally stored in heap with UTF-16
encoding. However, we have used UTF-8
here as it usually takes less space than UTF-16
, especially for ASCII characters.
Likewise, the encrypted byte array can also be converted into a String as below:
String encryptedString = new String(encryptedBytes, StandardCharsets.UTF_8);