Symmetric Encryption with AES in Java 8

AES (Advanced Encryption Standard) is Symmetric Encryption specification for the encryption of electronic data established by the U.S. National Institute of Standards and Technology (NIST) in 2001

In Cryptography Fundamentals and Basic Concepts blog I mentioned that there are 2 types of Encryption mechanisms based on whether same key is used for both encryption and decryption or different keys, Symmetric Encryption and Asymmetric Encryption (aka Public Key Encryption). In this blog I am going to explain in detail about the most popular Symmetric Key Encryption called AES with working implementation.

AES is successor of 3DES (Triple Data Encryption Standard) and which is successor of DES (Data Encryption Standard). DES and 3DES are deprecated and should not be used any more. So if you need to implement Symmetric Encryption AES is way to go.

Block Cipher

AES is a block Cipher which means encryption happens on block of fixed number of bits. AES uses block size of 128 bits. If given plain text is of length more than block size, it is divided into multiple blocks and encryption is applied on each block.

Secret Key

The Secret Key is the core secret of the encryption. Secret key must be shared between sender and receiver. Both sender and receiver uses exact same secret key to encrypt and decrypt the text. AES supports key lengths of 128, 192 and 256 bit.

It is worth to mention that key length does not determine or impact block size. You can choose any of 3 key sizes for your encryption with 128 bits block. The key length determines no of processing or transformation cycle to be applied on each block.

Generate or Load Existing Secret Key

You can choose your on secrete key or generate a random key. Following is code to generate a random secret key of 128 bits.

private byte[] generateSecretKeyBytes() throws Exception {
    KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
    keyGenerator.init(128);
    SecretKey secretKey = keyGenerator.generateKey();
    return secretKey.getEncoded();
}

If you want you can write secret key byte to file and share with other party (sender or receiver). You can read the key bytes from file to reuse. It is ok to reuse same key. But its good to have some process defined for key rotation periodically.

Encryption Block Mode

As mentioned earlier AES encrypts one block (128 bits) of data at a time. If plain text more than 128 bits then its divided into in to multiple blocks and we must choose the Block Mode to encrypt multiple blocks. Following 3 available block modes –

  1. Electronic Codebook (ECB)
  2. Cipher Block Chaining (CBC)
  3. Counter Mode (CTR)

Padding

As mentioned above plain text is divided into multiple blocks of 128 bits but what if last block is not of exact 128 bits in length. That’s where concept of Padding comes into play. We need to pad extract empty slots (bits) in the last block. We can simply fill in extra slots with 0 and there is not security concern in doing that.

With AES we don’t do padding manually instead we specific padding schema to be used. 2 available padding schemes are PKCS5Padding and PKCS7Padding. Practically both are same but from core JDK support perspective use PKCS5Padding. If you want to use PKCS7Padding then you will need to put additional bouncycastle jar to classpath.

Read this for subtle Difference between PKCS5Padding and PKCS7Padding

Electronic Codebook (ECB)

This is the simplest and easiest block mode so most developers by default implements it but we must be careful about some facts while choosing ECB block mode. In this block mode exact same key is used to encrypt every block, which means identical blocks of plain text will produce identical cipher (encrypted) text.

If block1 == block2 then encrypted(block1) == encrypted(block2)

Symmetric Encryption with AES ECB
Picture Credit wikipedia

This behavior shows a pattern of similarity between plain text and cipher text, which is dangerous. It could become lead for unauthorized user to decrypt cipher text. So do not use this block mode if you need to encrypt text larger than one block.

Here is code to implement encryption and decryption using Electronic Codebook (ECB) block mode.

EncryptionDecryptionHelper_AES_ECB.java
package com.readtorakesh.java8.encryption;

import java.util.Base64;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

public class EncryptionDecryptionHelper_AES_ECB {

    public static void main(String[] args) throws Exception {
        EncryptionDecryptionHelper_AES_ECB mainApp = new EncryptionDecryptionHelper_AES_ECB();
        
        System.out.println("-----------------------------------------------------------------");
        System.out.println("Algoritham \t: AES with ECB (Electronic Codebook) block mode");
        byte[] secretKeyBytes = mainApp.generateSecretKeyBytes();
        System.out.println("Key Length \t: " + (secretKeyBytes.length * 8) + " bits");
        System.out.println("-----------------------------------------------------------------");
        
        String plainText = "read2rakesh";
        System.out.println("Plain Text \t: " + plainText);
        
        String encryptedBase64 = mainApp.encrypt(plainText, secretKeyBytes);
        System.out.println("Encrypted Text \t: " + encryptedBase64);

        String decryptedText = mainApp.decrypt(encryptedBase64, secretKeyBytes);
        System.out.println("Decrypted Text \t: " + decryptedText);
        
        System.out.println("\n");
        System.out.println("Encrypted same text multiple times");
        System.out.println("Plain Text : " + plainText + " | Encrypted Text : " + mainApp.encrypt(plainText, secretKeyBytes));
        System.out.println("Plain Text : " + plainText + " | Encrypted Text : " + mainApp.encrypt(plainText, secretKeyBytes));
        System.out.println("Plain Text : " + plainText + " | Encrypted Text : " + mainApp.encrypt(plainText, secretKeyBytes));
    }
    
    private String encrypt(String plainText, byte[] secretKeyBytes) throws Exception{
        SecretKey secretKey = new SecretKeySpec(secretKeyBytes, "AES");
        
        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, secretKey);
        byte[] encryptedBytes = cipher.doFinal(plainText.getBytes("UTF-8"));
        
        String encryptedMesssageBase64 =  Base64.getEncoder().encodeToString(encryptedBytes);
        return encryptedMesssageBase64;
    }
    
    private String decrypt(String encryptedBase64, byte[] secretKeyBytes) throws Exception {
        byte[] encryptedBytes = Base64.getDecoder().decode(encryptedBase64);
        
        SecretKey secretKey = new SecretKeySpec(secretKeyBytes, "AES");
        
        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, secretKey);
        byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
        
        String decryptedText = new String(decryptedBytes, "UTF-8");
        
        return decryptedText;
    }
    
    private byte[] generateSecretKeyBytes() throws Exception {
        KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
        keyGenerator.init(128);
        SecretKey secretKey = keyGenerator.generateKey();
        return secretKey.getEncoded();
    }
}

/* 
--- Output ---
-----------------------------------------------------------------
Algoritham  : AES with ECB (Electronic Codebook) block mode
Key Length  : 128 bits
-----------------------------------------------------------------
Plain Text  : read2rakesh
Encrypted Text  : w7xLEHSYKh0Hm5Lx0+MpUw==
Decrypted Text  : read2rakesh


Encrypted same text multiple times
Plain Text : read2rakesh | Encrypted Text : w7xLEHSYKh0Hm5Lx0+MpUw==
Plain Text : read2rakesh | Encrypted Text : w7xLEHSYKh0Hm5Lx0+MpUw==
Plain Text : read2rakesh | Encrypted Text : w7xLEHSYKh0Hm5Lx0+MpUw==
*/

Cipher Block Chaining (CBC)

In this block mode, current plain text block is XORed with cipher text of previous block before being encrypted. This way each cipher text block depends on all plain text blocks processed up to that point.

Since there is no cipher text before the plain first block, it uses a block of random bytes, called IV, to XOR with first plain text block before encrypting it. IV (Initialization Vector) should be of same length as the plain text block, which is 128 bits. IV is playing role of salt for encryption to make the cipher text unique even for same key and plain text combination. IV is public, should be random and should not be reused (use new IV every time to encrypt new text). When transmitting or persisting the data it is common to just prepend the IV to the actual cipher text so that other party can use it to decrypt.

Symmetric Encryption with AES CBC
Picture Credit wikipedia

We can use SecureRandom class to generate random IV of given length. Following is code to generate IV of 128 bits.

private byte[] generateIvBytes() {
    byte[] ivBytes = new byte[128 / 8]; // 128 bit iv

    SecureRandom secureRandom = new SecureRandom();
    secureRandom.nextBytes(ivBytes);

    return ivBytes;
}

Here is code to implement encryption and decryption using Cipher Block Chaining (CBC) block mode.

EncryptionDecryptionHelper_AES_CBC.java
package com.readtorakesh.java8.encryption;

import java.nio.ByteBuffer;
import java.security.SecureRandom;
import java.util.Base64;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

public class EncryptionDecryptionHelper_AES_CBC {

    public static void main(String[] args) throws Exception {
        EncryptionDecryptionHelper_AES_CBC mainApp = new EncryptionDecryptionHelper_AES_CBC();
        
        System.out.println("-----------------------------------------------------------------");
        System.out.println("Algoritham \t: AES with CBC (Cipher Block Chaining) block mode");
        byte[] secretKeyBytes = mainApp.generateSecretKeyBytes();
        System.out.println("Key Length \t: " + (secretKeyBytes.length * 8) + " bits");
        System.out.println("-----------------------------------------------------------------");
        
        String plainText = "read2rakesh";
        System.out.println("Plain Text \t: " + plainText);
        
        String encryptedBase64 = mainApp.encrypt(plainText, secretKeyBytes);
        System.out.println("Encrypted Text \t: " + encryptedBase64);

        String decryptedText = mainApp.decrypt(encryptedBase64, secretKeyBytes);
        System.out.println("Decrypted Text \t: " + decryptedText);
        
        System.out.println("\n");
        System.out.println("Encrypted same text multiple times");
        System.out.println("Plain Text : " + plainText + " | Encrypted Text : " + mainApp.encrypt(plainText, secretKeyBytes));
        System.out.println("Plain Text : " + plainText + " | Encrypted Text : " + mainApp.encrypt(plainText, secretKeyBytes));
        System.out.println("Plain Text : " + plainText + " | Encrypted Text : " + mainApp.encrypt(plainText, secretKeyBytes));
    }
    
    private String encrypt(String plainText, byte[] secretKeyBytes) throws Exception{
        SecretKey secretKey = new SecretKeySpec(secretKeyBytes, "AES");
        
        byte[] ivBytes = generateIvBytes();
        IvParameterSpec ivParameterSpec = new IvParameterSpec(ivBytes);
        
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec);
        byte[] encryptedBytes = cipher.doFinal(plainText.getBytes("UTF-8"));
        
        ByteBuffer encryptedMessage = ByteBuffer.allocate(1 + ivBytes.length  + encryptedBytes.length);
        encryptedMessage.put((byte)ivBytes.length);
        encryptedMessage.put(ivBytes);
        encryptedMessage.put(encryptedBytes);
        
        byte[] encryptedMesssageBytes = encryptedMessage.array();
        String encryptedMesssageBase64 =  Base64.getEncoder().encodeToString(encryptedMesssageBytes);
        
        return encryptedMesssageBase64;
    }
    
    private String decrypt(String encryptedBase64, byte[] secretKeyBytes) throws Exception {
        byte[] encryptedMesssageBytes = Base64.getDecoder().decode(encryptedBase64);
        ByteBuffer encryptedMessageByteBuffer = ByteBuffer.wrap(encryptedMesssageBytes);
        
        byte ivLength =  encryptedMessageByteBuffer.get();
        if(ivLength != 16) {
            throw new IllegalArgumentException("Incorrect IV length");
        }
        
        byte[] ivBytes = new byte[ivLength];
        encryptedMessageByteBuffer.get(ivBytes);
        
        byte[] encryptedBytes = new byte[encryptedMessageByteBuffer.remaining()];
        encryptedMessageByteBuffer.get(encryptedBytes);
        
        SecretKey secretKey = new SecretKeySpec(secretKeyBytes, "AES");
        IvParameterSpec ivParameterSpec = new IvParameterSpec(ivBytes);
        
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec);
        byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
        
        
        String decryptedText = new String(decryptedBytes, "UTF-8");
        
        return decryptedText;
    }

    private byte[] generateIvBytes() {
        byte[] ivBytes = new byte[128 / 8]; // 128 bit iv

        SecureRandom secureRandom = new SecureRandom();
        secureRandom.nextBytes(ivBytes);

        return ivBytes;
    }
    
    private byte[] generateSecretKeyBytes() throws Exception {
        KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
        keyGenerator.init(128);
        SecretKey secretKey = keyGenerator.generateKey();
        return secretKey.getEncoded();
    }
    
}

/* 
--- Output ---
-----------------------------------------------------------------
Algoritham  : AES with CBC (Cipher Block Chaining) block mode
Key Length  : 128 bits
-----------------------------------------------------------------
Plain Text  : read2rakesh
Encrypted Text  : EClGh792b1NPLW3e4I0mK63R24vRSU4Rk7SctctyCCoQ
Decrypted Text  : read2rakesh


Encrypted same text multiple times
Plain Text : read2rakesh | Encrypted Text : EAbXA5AU+pvJ1tfHQlT68/9uMa4Pq7El5mtSSVzoGlXE
Plain Text : read2rakesh | Encrypted Text : EA5RSzFWsIA8OyrThRWTqw2zLuMvc+yt2VG8hwWdS829
Plain Text : read2rakesh | Encrypted Text : ENnZSPIq4OgwEkaRhhy5f8pbeswrxo3N7w4MFk0Js8RP
*/

Counter Mode (CTR)

In this block mode, a Counter is used to encrypt each block. Counter could be any function which produces a sequence of elements, which are guaranteed to be unique for each block. Counter could be as simple as a numbered sequence from 0 to n. Every block of plain text is encrypted with IV (initialization vector) aka Nonce in this mode, Counter and Secrete Key.

Symmetric Encryption with AES CTR
Picture Credit wikipedia

Since encryption of one block is not dependent on any other block, this block mode a stream cipher. Since blocks can be encrypted in parallel, this block mode is best suited for multi-processor machine for faster result.

Here is code to implement encryption and decryption using Counter Mode (CTR) block mode.

EncryptionDecryptionHelper_AES_CTR.java
package com.readtorakesh.java8.encryption;

import java.nio.ByteBuffer;
import java.security.SecureRandom;
import java.util.Base64;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

public class EncryptionDecryptionHelper_AES_CTR {

    public static void main(String[] args) throws Exception {
        EncryptionDecryptionHelper_AES_CTR mainApp = new EncryptionDecryptionHelper_AES_CTR();
        
        System.out.println("-----------------------------------------------------------------");
        System.out.println("Algoritham \t: AES with CTR (Counter Mode) block mode");
        byte[] secretKeyBytes = mainApp.generateSecretKeyBytes();
        System.out.println("Key Length \t: " + (secretKeyBytes.length * 8) + " bits");
        System.out.println("-----------------------------------------------------------------");
        
        String plainText = "read2rakesh";
        System.out.println("Plain Text \t: " + plainText);
        
        String encryptedBase64 = mainApp.encrypt(plainText, secretKeyBytes);
        System.out.println("Encrypted Text \t: " + encryptedBase64);

        String decryptedText = mainApp.decrypt(encryptedBase64, secretKeyBytes);
        System.out.println("Decrypted Text \t: " + decryptedText);
        
        System.out.println("\n");
        System.out.println("Encrypted same text multiple times");
        System.out.println("Plain Text : " + plainText + " | Encrypted Text : " + mainApp.encrypt(plainText, secretKeyBytes));
        System.out.println("Plain Text : " + plainText + " | Encrypted Text : " + mainApp.encrypt(plainText, secretKeyBytes));
        System.out.println("Plain Text : " + plainText + " | Encrypted Text : " + mainApp.encrypt(plainText, secretKeyBytes));
    }
    
    private String encrypt(String plainText, byte[] secretKeyBytes) throws Exception{
        SecretKey secretKey = new SecretKeySpec(secretKeyBytes, "AES");
        
        byte[] ivBytes = generateIvBytes();
        IvParameterSpec ivParameterSpec = new IvParameterSpec(ivBytes);
        
        Cipher cipher = Cipher.getInstance("AES/CTR/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec);
        byte[] encryptedBytes = cipher.doFinal(plainText.getBytes("UTF-8"));
        
        ByteBuffer encryptedMessage = ByteBuffer.allocate(1 + ivBytes.length  + encryptedBytes.length);
        encryptedMessage.put((byte)ivBytes.length);
        encryptedMessage.put(ivBytes);
        encryptedMessage.put(encryptedBytes);
        
        byte[] encryptedMesssageBytes = encryptedMessage.array();
        String encryptedMesssageBase64 =  Base64.getEncoder().encodeToString(encryptedMesssageBytes);
        
        return encryptedMesssageBase64;
    }
    
    private String decrypt(String encryptedBase64, byte[] secretKeyBytes) throws Exception {
        byte[] encryptedMesssageBytes = Base64.getDecoder().decode(encryptedBase64);
        ByteBuffer encryptedMessageByteBuffer = ByteBuffer.wrap(encryptedMesssageBytes);
        
        byte ivLength =  encryptedMessageByteBuffer.get();
        if(ivLength != 16) {
            throw new IllegalArgumentException("Incorrect IV length");
        }
        
        byte[] ivBytes = new byte[ivLength];
        encryptedMessageByteBuffer.get(ivBytes);
        
        byte[] encryptedBytes = new byte[encryptedMessageByteBuffer.remaining()];
        encryptedMessageByteBuffer.get(encryptedBytes);
        
        SecretKey secretKey = new SecretKeySpec(secretKeyBytes, "AES");
        IvParameterSpec ivParameterSpec = new IvParameterSpec(ivBytes);
        
        Cipher cipher = Cipher.getInstance("AES/CTR/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec);
        byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
        
        
        String decryptedText = new String(decryptedBytes, "UTF-8");
        
        return decryptedText;
    }

    private byte[] generateIvBytes() {
        byte[] ivBytes = new byte[128 / 8]; // 128 bit iv

        SecureRandom secureRandom = new SecureRandom();
        secureRandom.nextBytes(ivBytes);

        return ivBytes;
    }
    
    private byte[] generateSecretKeyBytes() throws Exception {
        KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
        keyGenerator.init(128);
        SecretKey secretKey = keyGenerator.generateKey();
        return secretKey.getEncoded();
    }
    
}

/* 
--- Output ---
-----------------------------------------------------------------
Algoritham  : AES with CTR (Counter Mode) block mode
Key Length  : 128 bits
-----------------------------------------------------------------
Plain Text  : read2rakesh
Encrypted Text  : EAalzVIBffg7s7eVISp39xLw2vvkCYw86s73+g==
Decrypted Text  : read2rakesh


Encrypted same text multiple times
Plain Text : read2rakesh | Encrypted Text : EFPhpXKTaj4TxHkSySfFoEKvxnZL0LeQOT1Zjw==
Plain Text : read2rakesh | Encrypted Text : EGgJc3HEo/xR/m/QKC//dTCDLFcBlE81pLzMAQ==
Plain Text : read2rakesh | Encrypted Text : ENdP2aWfQ7VgUnXskiFl0USAPymHYybfDCCtfw==
*/

 

You can download code from github https://github.com/rakeshprajapati1982/java8

Reference:

https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_Block_Chaining_(CBC)

Please share it and help others if you found this blog helpful. Feedback, questions and comments are always welcome.

Further Reading

2 Comments

Comments