Symmetric Encryption Using AES and Java

Sorry, comments are closed.

Symmetric Encryption Basics

The Advanced Encryption Standard (AES) is a symmetric-key encryption algorithm. In symmetric cryptography there is a single private key that is used for both encryption and decryption. AES strength can be specified in one of three bit key lengths: 128, 192 and 256. Even a 128-bit key length is deemed strong enough by the US government to encrypt classified information (at the time of publication).

Regardless of key length, AES is an industry standard for securely encrypting large amounts of data. The algorithm is considered to be a block cipher, which means it operates on groups of bits (blocks) during the encryption process. In the case of AES the block size is 16 bytes or a 128-bit block. The final block is padded with additional bits if it is less than 16 bytes.

In a single domain, like a website or application, symmetric encryption is often sufficient. The same party encrypting is typically the same party decrypting. However, if it becomes necessary to share the key with multiple parties then it is better to use AES encryption in cooperation with another encryption standard like public-key encryption. This would be known as a hybrid cryptosystem.

If you have questions about Java and cryptography after some research using the Java Cryptography Architecture (JCA) reference guide, then you can visit the Cryptography Stack Exchange or StackOverflow.

Encrypting and Decrypting Data With an AES Cipher

The AESCipher class provides a flexible and secure approach to encrypting and decrypting text.

package com.reindel.ciphers;

import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Random;
import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;

public class AESCipher {

    private Cipher cipher;
    private Base64 base64;
    private int passwordLength;
    private int saltLength;
    private int initializationVectorSeedLength;

    public AESCipher()
            throws NoSuchAlgorithmException, NoSuchPaddingException {
        this(new Base64(), 16, 16, 16);
    }
    
    public AESCipher(Base64 base64, int passwordLength, int saltLength, int initializationVectorSeedLength)
            throws NoSuchAlgorithmException, NoSuchPaddingException {

        try {
            this.base64 = base64;
            this.passwordLength = passwordLength;
            this.saltLength = saltLength;
            this.initializationVectorSeedLength = initializationVectorSeedLength;
            cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        } catch (NoSuchAlgorithmException noSuchAlgorithmException) {
            throw noSuchAlgorithmException;
        } catch (NoSuchPaddingException noSuchPaddingException) {
            throw noSuchPaddingException;
        }
    }
    
    private SecretKey secretKey;

    public SecretKey getSecretKey() {
        return secretKey;
    }
    
    public String getEncodedSecretKey(SecretKey secretKey) {
        return base64.encodeToString(secretKey.getEncoded());
    }

    public SecretKey getDecodedSecretKey(String secretKey) {
        return new SecretKeySpec(base64.decode(secretKey), "AES");
    }
    
    public String encrypt(String rawText, int hashIterations, KeyLength keyLength)
            throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {

        SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
        SecureRandom secureRandom = new SecureRandom();

        byte[] seed = secureRandom.generateSeed(initializationVectorSeedLength);
        AlgorithmParameterSpec algorithmParameterSpec = new IvParameterSpec(seed);

        KeySpec keySpec = new PBEKeySpec(getRandomPassword(), secureRandom.generateSeed(saltLength), hashIterations, keyLength.getBits());
        secretKey = new SecretKeySpec(secretKeyFactory.generateSecret(keySpec).getEncoded(), "AES");

        cipher.init(Cipher.ENCRYPT_MODE, secretKey, algorithmParameterSpec);
        byte[] encryptedMessageBytes = cipher.doFinal(rawText.getBytes());

        byte[] bytesToEncode = new byte[seed.length + encryptedMessageBytes.length];
        System.arraycopy(seed, 0, bytesToEncode, 0, seed.length);
        System.arraycopy(encryptedMessageBytes, 0, bytesToEncode, seed.length, encryptedMessageBytes.length);

        return base64.encodeToString(bytesToEncode);
    }

    public String decrypt(String encryptedText, SecretKey secretKey)
            throws InvalidKeyException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {

        byte[] bytesToDecode = base64.decode(encryptedText);

        byte[] emptySeed = new byte[initializationVectorSeedLength];
        System.arraycopy(bytesToDecode, 0, emptySeed, 0, initializationVectorSeedLength);

        cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(emptySeed));

        int messageDecryptedBytesLength = bytesToDecode.length - initializationVectorSeedLength;
        byte[] messageDecryptedBytes = new byte[messageDecryptedBytesLength];
        System.arraycopy(bytesToDecode, initializationVectorSeedLength, messageDecryptedBytes, 0, messageDecryptedBytesLength);

        return new String(cipher.doFinal(messageDecryptedBytes));
    }

    public enum KeyLength {

        ONE_TWENTY_EIGHT(128),
        ONE_NINETY_TWO(192),
        TWO_FIFTY_SIX(256);
        
        private int bits;

        KeyLength(int bits) {
            this.bits = bits;
        }

        public int getBits() {
            return bits;
        }
    }
    
    protected char[] getRandomPassword() {
        
        char[] randomPassword = new char[passwordLength];
        
        Random random = new Random();
        for(int i = 0; i < passwordLength; i++) {
            randomPassword[i] = (char)(random.nextInt('~' - '!' + 1) + '!');
        }
        
        return randomPassword;
    }
}

The following test demonstrates usage.

package com.reindel.ciphers;

import org.junit.Assert;
import org.junit.Test;

public class AESCipherTest {
    
    @Test
    public void testEncryptionDecryption()
            throws Exception {
    
        AESCipher aesCipher = new AESCipher();
        String encrypted = aesCipher.encrypt("John has a long mustache.", 10000, AESCipher.KeyLength.TWO_FIFTY_SIX);
        String decrypted = aesCipher.decrypt(encrypted, aesCipher.getSecretKey());
        Assert.assertEquals(decrypted, "John has a long mustache."); 
    }
}

Cipher Transformation

There are a few key aspects of the AESCipher that need to be discussed that are primarily related to the cipher’s transformation. The transformation is a set of characteristics for a cipher that makes it unique. Not only can it be symmetric or asymmetric (the algorithm), but it can also be constructed with a particular “mode” and “padding”. Out of the box the JDK supports several transformations, each with an algorithm, mode and padding definition.

In the case of the AESCipher, the instance is constructed with the transformation “AES/CBC/PKCS5Padding”. The mode and padding can have an impact on the security of the cipher, and the chosen values CBC (Cipher Block Chaining) and PKCS5Padding (Password-Based Encryption Standard) are considered a secure choice for AES. There is a lot that can be said about the mode and padding in a cipher, and it’s important to understand the advantages or disadvantages of the various implementations.

To ensure a high level of security, and when given the choice, the following default provided Java transformations should not be used: “AES/CBC/NoPadding”, “AES/ECB/NoPadding”, “AES/ECB/PKCS5Padding”.

Password, Salt, and Iterations

The password in the AESCipher is often referred to as the key. However, in a secure implementation of a cipher, the real secret key will contain more than just a password. It will also contain a salt and iterations. How the password is generated, stored and shared can also affect the security of the cipher. To ensure the highest level of security the password itself should be random, and each call to the encrypt method will use a different password. Some flexibility is sacrificed with this approach. If the key is lost it can never be retrieved. The alternative is to store the same password as a private field on the cipher, as a system property, or through some other means.

The salt introduces another component of randomness. In the case of the AESCipher, and when generating a secret key, it is appended to the password. In order for it to be of any use the salt should always be random, regardless of the password. When this is the case a hacker can’t use a rainbow table to guess the value of a pre-computed password (and therefore secret key). The same salt used for encryption must be used for decryption. It’s only necessary to know this when regenerating a secret key. The salt is most useful when the password to encrypt is always the same.

The final component used to make up the secret key is iterations. Just like the salt, the number of iterations prevents a hacker from using pre-computed values for given passwords in order to guess a secret key. If the hacker doesn’t know the salt and password then the iterations slows down an attack so that brute force guessing takes an unreasonable amount of time. A single iteration will take a hash of the password and salt, and hash it with the previously hashed password and salt. Each iteration takes minimal effort and time, and a few thousand iterations is considered the bare minimum.

Initialization Vector

An initialization vector, much like a salt, introduces randomness. However, unlike a salt, which is applied to the secret key, the initialization vector is applied to the data to be encrypted. For example, an initialization vector will prevent identical output when encrypting two identical strings with the same cipher. This variation eliminates the ability to determine the value of raw data based upon knowledge of patterns in the encrypted cipher text.

The same initialization vector must be used during encryption and decryption. However, sharing the initialization vector along with the secret key is not necessary. In the case of the AESCipher, the bytes of the initialization vector are prepended to the encrypted bytes. When the decrypt method is called those same bytes are copied, used to initialize the cipher, and then they are removed from the encrypted bytes (using another array copy) before final decryption takes place.

Revision History

Revision 1.0, 03/12/2014 – Publication.

Code examples assume JDK 1.6+, and tests assume JUnit 4+.