1. Overview
In this article, we’ll deep dive into the purpose of keys in AES or Ciphers in general. We’ll go over the best practices to keep in mind while generating one.
Finally, we’ll look at the various ways to generate one and weigh them against the guidelines.
2. AES
Advanced Encryption Standard (AES) is the successor of the Data Encryption Standard(DES), published in 2001 by the National Institute of Standards and Technology(NIST). It’s classified as a symmetric block cipher.
A symmetric cipher uses the same secret key for both encryption and decryption. A block cipher means it works on 128 bits blocks of the input Plaintext:
2.1. AES Variants
Based on the key size, AES supports three variants: AES-128 (128 bits), AES-192 (192 bits), and AES-256 (256 bits). Increasing the key size increases the strength of encryption as a larger key size means the number of possible keys is larger. Consequently, the number of rounds to be performed during the execution of the algorithm increases as well and hence the compute required:
Key Size
Block Size
# Rounds
128
128
10
192
128
12
256
128
14
2.2. How Secure Is AES?
The AES algorithm is public information – it’s the AES key that is a secret and must be known to successfully decipher. So, it boils down to cracking the AES keys. Assuming the key is securely preserved, an attacker would have to try to guess the key.
Let’s see how the brute-force approach fares in guessing the key.
AES-128 keys are 128 bits, which means there are 2^128 possible values. It’d take a humongous and infeasible amount of time and money to search through this. Hence, AES is practically unbreakable by a brute-force approach.
There have been a few non-brute-force approaches but these could only reduce the possible key lookup space by a couple of bits.
All this means is that with zero knowledge about the key, AES is practically impossible to break.
3. Properties of a Good Key
Let’s now look at some of the important guidelines to follow while generating an AES key.
3.1. Key Size
Since AES supports three key sizes, we should choose the right key size for the use case. AES-128 is the most common choice in commercial applications. It offers a balance between security and speed. National Governments typically make use of AES-192 and AES-256 to have maximum security. We can use AES-256 if we want to have an extra level of security.
The quantum computers do pose a threat of being able to reduce the compute required for large keyspaces. Hence, having an AES-256 key would be more future-proof, although as of now, they’re out of the reach of any threat actors of commercial applications.
3.2. Entropy
Entropy refers to randomness in the key. If the generated key isn’t random enough and has some co-relation with being time-dependent, machine-dependent, or a dictionary word, for example, it becomes vulnerable. An attacker would be able to narrow down the key search space, robbing the strength of AES. Hence, it’s of utmost importance that the keys are truly random.
4. Generating AES Keys
Now, armed with the guidelines for generating an AES key, let’s see the various approaches to generating them.
For all the code snippets, we define our cipher as:
private static final String CIPHER = "AES";
4.1. Random
Let’s use the Random class in Java to generate the key:
private static Key getRandomKey(String cipher, int keySize) {
byte[] randomKeyBytes = new byte[keySize / 8];
Random random = new Random();
random.nextBytes(randomKeyBytes);
return new SecretKeySpec(randomKeyBytes, cipher);
}
We create a byte array of desired key size and fill it with random bytes obtained from random.nextBytes(). The random byte array is then used to create a SecretKeySpec.
The Java Random class is a Pseudo-Random Number Generator (PRNG), also known as Deterministic Random Number Generator (DRNG). This means it’s not truly random. The sequence of random numbers in a PRNG can be completely determined based on its seed. Java doesn’t recommend using Random for cryptographic applications.
With that said, NEVER use Random for generating keys.
4.2. SecureRandom
We’ll now use SecureRandom class in Java to generate the key:
private static Key getSecureRandomKey(String cipher, int keySize) {
byte[] secureRandomKeyBytes = new byte[keySize / 8];
SecureRandom secureRandom = new SecureRandom();
secureRandom.nextBytes(secureRandomKeyBytes);
return new SecretKeySpec(secureRandomKeyBytes, cipher);
}
Similar to the previous example, we instantiate a byte array of the desired key size. Now, instead of using Random, we use SecureRandom to generate the random bytes for our byte array. SecureRandom is recommended by Java for generating a random number for cryptographic applications. It minimally complies with FIPS 140-2, Security Requirements for Cryptographic Modules.
Clearly, in Java, SecureRandom is the de-facto standard for obtaining randomness. But is it the best way to generate keys? Let’s move on to the next approach.
4.3. KeyGenerator
Next, let’s generate a key using the KeyGenerator class:
private static Key getKeyFromKeyGenerator(String cipher, int keySize) throws NoSuchAlgorithmException {
KeyGenerator keyGenerator = KeyGenerator.getInstance(cipher);
keyGenerator.init(keySize);
return keyGenerator.generateKey();
}
We get an instance of KeyGenerator for the cipher we’re working with. We then initialize the keyGenerator object with the desired keySize. Finally, we invoke the generateKey method to generate our secret key. So, how’s it different from the Random and SecureRandom approaches?
There are two crucial differences worth highlighting.
For one, neither the Random nor SecureRandom approach can tell whether we’re generating keys of the right sizes as per the Cipher specification. It’s only when we go for encryption that we’ll encounter exceptions if the keys are of an unsupported size.
Using SecureRandom with invalid keySize throws an exception when we initialize the cipher for encryption:
encrypt(plainText, getSecureRandomKey(CIPHER, 111));
java.security.InvalidKeyException: Invalid AES key length: 13 bytes
at java.base/com.sun.crypto.provider.AESCrypt.init(AESCrypt.java:90)
at java.base/com.sun.crypto.provider.GaloisCounterMode.init(GaloisCounterMode.java:321)
at java.base/com.sun.crypto.provider.CipherCore.init(CipherCore.java:592)
at java.base/com.sun.crypto.provider.CipherCore.init(CipherCore.java:470)
at java.base/com.sun.crypto.provider.AESCipher.engineInit(AESCipher.java:322)
at java.base/javax.crypto.Cipher.implInit(Cipher.java:867)
at java.base/javax.crypto.Cipher.chooseProvider(Cipher.java:929)
at java.base/javax.crypto.Cipher.init(Cipher.java:1299)
at java.base/javax.crypto.Cipher.init(Cipher.java:1236)
at com.baeldung.secretkey.Main.encrypt(Main.java:59)
at com.baeldung.secretkey.Main.main(Main.java:51)
Using KeyGenerator, on the other hand, fails during key generation itself, allowing us to handle it more appropriately:
encrypt(plainText, getKeyFromKeyGenerator(CIPHER, 111));
java.security.InvalidParameterException: Wrong keysize: must be equal to 128, 192 or 256
at java.base/com.sun.crypto.provider.AESKeyGenerator.engineInit(AESKeyGenerator.java:93)
at java.base/javax.crypto.KeyGenerator.init(KeyGenerator.java:539)
at java.base/javax.crypto.KeyGenerator.init(KeyGenerator.java:516)
at com.baeldung.secretkey.Main.getKeyFromKeyGenerator(Main.java:89)
at com.baeldung.secretkey.Main.main(Main.java:58)
The other key difference is the default use of SecureRandom. The KeyGenerator class is part of Java’s crypto package javax.crypto, which ensures the usage of SecureRandom for randomness. We can see the definition of the init method in the KeyGenerator class:
public final void init(int keysize) {
init(keysize, JCAUtil.getSecureRandom());
}
Hence, using a KeyGenerator as a practice ensures we never use a Random class object for key generation.
4.4. Password-Based Key
So far, we’ve been generating keys from random and not-so-human-friendly byte arrays. Password-Based Key (PBK) offers us the ability to generate a SecretKey based on a human-readable password:
private static Key getPasswordBasedKey(String cipher, int keySize, char[] password) throws NoSuchAlgorithmException, InvalidKeySpecException {
byte[] salt = new byte[100];
SecureRandom random = new SecureRandom();
random.nextBytes(salt);
PBEKeySpec pbeKeySpec = new PBEKeySpec(password, salt, 1000, keySize);
SecretKey pbeKey = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(pbeKeySpec);
return new SecretKeySpec(pbeKey.getEncoded(), cipher);
}
We’ve got quite a few things going on here. Let’s break it down.
We start with our human-readable password. This is a secret and must be protected. The password guidelines must be followed, such as a minimum length of 8 characters, the use of special characters, the combination of uppercase and lowercase letters, digits, and so on. Additionally, OWASP guidelines suggest checking against already exposed passwords.
A user-friendly password doesn’t have enough entropy. Hence, we add additional randomly generated bytes called a salt to make it harder to guess. The minimum salt length should be 128 bits. We used SecureRandom to generate our salt. The salt isn’t a secret and is stored as plaintext. We should generate salt in pairs with each password and not use the same salt globally. This’ll protect from Rainbow Table attacks, which use lookups from a precomputed hash table for cracking the passwords.
The iteration count is the number of times the secret generation algorithm applies the transformation function. It should be as large as feasible. The minimum recommended iteration count is 1,000. A higher iteration count increases the complexity for the attacker while performing a brute-force check for all possible passwords.
The key size is the same we discussed earlier, which can be 128, 192, or 256 for AES.
We’ve wrapped all the four elements discussed above into a PBEKeySpec object. Next, using the SecretKeyFactory, we get an instance of PBKDF2WithHmacSHA256 algorithm to generate the key.
Finally, invoking generateSecret with the PBEKeySpec, we generate a SecretKey based on a human-readable password.
5. Conclusion
There are two primary bases for generating a key. It could be a random key or a key based on a human-readable password. We’ve discussed three approaches to generating a random key. Among them, KeyGenerator provides true randomness and also offers checks and balances. Hence, KeyGenerator is a better option.
For a key based on a human-readable password, we can use SecretKeyFactory along with a salt generated using SecureRandom and high iteration count.
As always, the complete code is available over on GitHub.