2016年5月1日 星期日

Java JCE - AES Encryption & Decryption @2016-05-01 (English Version)

Foreword:

Why I write this article ?  In my original thinking, I believe that there is a lot of blogs talk about Java AES example. However, I still found some sample code is not clearly to explain the issues of these sample. Therefore, I am afraid someone may really adopt these samples in their production system.

In addition, in order to simplify the explanation, the following sample will ignore the exception handle.

Content:

Some similar sample code that you often find on internet is as the following:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class AES_DEFAULT {  
 public static byte[] Encrypt(SecretKey secretKey, String msg) throws Exception  
 {  
  Cipher cipher = Cipher.getInstance("AES"); //: default is AES/ECB/PKCS5Padding
  cipher.init(Cipher.ENCRYPT_MODE, secretKey); 
  System.out.println("AES_DEFAULT IV:"+cipher.getIV());
  System.out.println("AES_DEFAULT Algoritm:"+cipher.getAlgorithm());
  byte[] byteCipherText = cipher.doFinal(msg.getBytes());  
  System.out.println("Encrypted result and base64 encoded:" + Base64.getEncoder().encodeToString(byteCipherText));
  return byteCipherText;  
 }  

 public static byte[] Decrypt(SecretKey secretKey, byte[] cipherText) throws Exception  
 {  
  Cipher cipher = Cipher.getInstance("AES"); 
  cipher.init(Cipher.DECRYPT_MODE, secretKey);  
  byte[] decryptedText = cipher.doFinal(cipherText);  
  String strDecryptedText = new String(decryptedText);
  System.out.println("Decrypted result:" + strDecryptedText);
  return decryptedText;  
 }  

 public static void main(String args[]) throws Exception{
  KeyGenerator keyGen = KeyGenerator.getInstance("AES");
  keyGen.init(128,new SecureRandom( ) );
  SecretKey secretKey = keyGen.generateKey();
  byte[] iv = new byte[16]; 
  SecureRandom prng = new SecureRandom();
  prng.nextBytes(iv);

  byte[] cipher = AES_DEFAULT.Encrypt(secretKey, "I am PlainText!!");
  AES_DEFAULT.Decrypt(secretKey, cipher);  
 }
} 

The above sample code can work well for encryption and decryption, but there are some issues:
  1. The first issue is located at the line 4. It uses the ECB cipher mode, however this cipher mode is not a secure cipher mode for AES, because it will cause the cipher block is the same if the input plain-text is the same. You could refer to the Wiki
  2. The second issue is located at the line 25. We should not use the 128 bits length as the AES KEY. Now is 2016, the recommended length of AES key is at least 256 bits.
  3. The third issue is the line 8. It use the msg.getBytes( ). This style will be fine if the program is running at the same platform or machine. However, your program will run at different platform/machine, and you will find the default charset may be different for different platform/machine. Therefore, this may cause the decrypted result is not as you expected.  

The recommended implementation is as the following sample:

Important warring: You should select the correct cipher mode,  for example: CCM or GCM mode. (Update 2022/03/04) The reason is the CBC mode is vulnerable to padding oracle attacks.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class AES_CBC_PKCS5PADDING {
 
 public static byte[] Encrypt(SecretKey secretKey, byte[] iv, String msg) throws Exception{
  Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING"); 
  cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));    
  System.out.println("AES_CBC_PKCS5PADDING IV:"+cipher.getIV());
  System.out.println("AES_CBC_PKCS5PADDING Algoritm:"+cipher.getAlgorithm());
  byte[] byteCipherText = cipher.doFinal(msg.getBytes("UTF-8"));
  System.out.println("Encrypted result and base64 encoded:" + Base64.getEncoder().encodeToString(byteCipherText));
  return byteCipherText;
 }
 
 public static void Decrypt(SecretKey secretKey, byte[] cipherText, byte[] iv) throws Exception{
  Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING"); 
  cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));    
  byte[] decryptedText = cipher.doFinal(cipherText);
  String strDecryptedText = new String(decryptedText);
  System.out.println("Decrypted result:" + strDecryptedText);
 }
 
 public static void main(String args[]) throws Exception{
  KeyGenerator keyGen = KeyGenerator.getInstance("AES");
  keyGen.init(256,new SecureRandom( ) );
  SecretKey secretKey = keyGen.generateKey();
  byte[] iv = new byte[16]; 
  SecureRandom prng = new SecureRandom();
  prng.nextBytes(iv);

  byte[] cipher = AES_CBC_PKCS5PADDING.Encrypt(secretKey, iv, "I am PlainText!!");
  AES_CBC_PKCS5PADDING.Decrypt(secretKey, cipher, iv);  
 }
}
As you seeing at line 3,  we adopt the CBC cipher mode with PKCS5 Padding. You could refer to the  Padding for the detail.  At line 8, we directly invoke msg.getBytes("UTF-8") to avoid some charset issues. Of course,  you could specify it use ANSI , and it is still work well if your plain-text contain ANSI only,

Here, allow me to remind you. The recommended length of AES key is 256 bits. In addition, you should always generate a new IV to encrypt if you use the same AES key to encrypt data.

You could refer the below sample code to generate the required Secret Key and IV. In addition, you also need to provide the same IV to the decryption side.

  KeyGenerator keyGen = KeyGenerator.getInstance("AES");
  keyGen.init(256,new SecureRandom( ) );
  SecretKey secretKey = keyGen.generateKey();
  byte[] iv = new byte[16]; 
  SecureRandom prng = new SecureRandom();
  prng.nextBytes(iv);

Sometime, you may see the following error message, the reason is the default Oracle's Java Runtime can not generate the AES 256 bits keys because the policy issue.

1
2
3
4
5
6
Exception in thread "main" java.security.InvalidKeyException: Illegal key size or default parameters
 at javax.crypto.Cipher.checkCryptoPerm(Cipher.java:1026)
 at javax.crypto.Cipher.implInit(Cipher.java:801)
 at javax.crypto.Cipher.chooseProvider(Cipher.java:864)
 at javax.crypto.Cipher.init(Cipher.java:1249)
 at javax.crypto.Cipher.init(Cipher.java:1186)

You need to install the Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files if you have the same problem at your environment. One more thing that you may need to check is the policy file version should be same as the JRE version at your environment. (The Android will not have this issue)

Finally,  you may ask common questions as the followings:
  1. How to encrypt a stream file with large file size (EX:Video or Audio)?
  2. How to randomly access specific block data of the encrypted file ?
The possible solution is as below :
  • The answer for the first one question is easy. Just study the Java Cipher API doc carefully, then you can invoke the update method of Cipher with multiple times , and invoke the doFinal method at the end.
  • The answer for the 2nd question is to change the cipher mode as the CTR cipher mode. In addition, you need to write a calculate IV method for the target block that you want.
You could refer to the following implementation:
(Note: This sample code does not testing well, please don't adopt it on production system.)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
class AES_CTR_PKCS5PADDING {
 private static final int BLOCK_SIZE = 16;
 
 public static void Encrypt(SecretKey secretKey, byte[] iv, File plainTextFile, File encryptedFile) throws Exception{
  Cipher cipher = Cipher.getInstance("AES/CTR/PKCS5PADDING"); 
  cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));    
  System.out.println("AES_CTR_PKCS5PADDING IV:"+cipher.getIV());
  System.out.println("AES_CTR_PKCS5PADDING Algoritm:"+cipher.getAlgorithm());
  byte buf[] = new byte[4096];
  try (InputStream in = new FileInputStream(plainTextFile);
    OutputStream out = new FileOutputStream(encryptedFile);){
   int readBytes = in.read(buf);   
   while(readBytes > 0){
    byte[] cipherBytes = cipher.update(buf, 0 , readBytes);
    out.write(cipherBytes);
    readBytes = in.read(buf);
   }
   cipher.doFinal();
  }
 }
 
 public static void Decrypt(SecretKey secretKey, byte[] iv, File cipherTextFile, File decryptedFile) throws Exception{
  Cipher cipher = Cipher.getInstance("AES/CTR/PKCS5PADDING"); 
  cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));    
  
  if(!decryptedFile.exists()){
   decryptedFile.createNewFile(); //: Here, it may be fail if ...
  }
  
  byte buf[] = new byte[4096];
  try (InputStream in = new FileInputStream(cipherTextFile);
    OutputStream out = new FileOutputStream(decryptedFile);){
   int readBytes = in.read(buf);   
   while(readBytes > 0){
    byte[] decryptedBytes = cipher.update(buf, 0 , readBytes);
    out.write(decryptedBytes);
    readBytes = in.read(buf);
   }
   cipher.doFinal();
  }
 }
  
 public static byte[] DecryptPartial(SecretKey secretKey, byte[] iv, File cipherTextFile, int blockIndex, int blockCount ) throws Exception{
  final int offset = blockIndex * BLOCK_SIZE;
  final int bufSize = blockCount * BLOCK_SIZE;

  Cipher cipher = Cipher.getInstance("AES/CTR/PKCS5PADDING"); 
  cipher.init(Cipher.DECRYPT_MODE, secretKey, calculateIVForBlock(new IvParameterSpec(iv), blockIndex ));

  byte[] decryptedBytes = new byte[bufSize];
  try (FileInputStream in = new FileInputStream(cipherTextFile)){
   byte inputBuf[] = new byte[bufSize];
   in.skip(offset);
   int readBytes = in.read(inputBuf);
   decryptedBytes = cipher.update(inputBuf, 0, readBytes);
  }
  return decryptedBytes;
 } 

 private static IvParameterSpec calculateIVForBlock(final IvParameterSpec iv,
         final long blockIndex) {  
     final BigInteger biginIV = new BigInteger(1, iv.getIV());
     final BigInteger blockIV = biginIV.add(BigInteger.valueOf(blockIndex));
     final byte[] blockIVBytes = blockIV.toByteArray();

     // Normalize the blockIVBytes as 16 bytes for IV
     if(blockIVBytes.length == BLOCK_SIZE){
      return new IvParameterSpec(blockIVBytes);
     }
     if(blockIVBytes.length > BLOCK_SIZE ){
      // For example: if the blockIVBytes length is 18, blockIVBytes is [0],[1],...[16],[17]
      // We have to remove [0],[1] , so we change the offset = 2
      int offset = blockIVBytes.length - BLOCK_SIZE;
      return new IvParameterSpec(blockIVBytes, offset, BLOCK_SIZE);
     }
     else{
      // For example: if the blockIVBytes length is 14, blockIVBytes is [0],[1],...[12],[13]
      // We have to insert 2 bytes at head
      final byte[] newBlockIV = new byte[BLOCK_SIZE]; //: default set to 0 for 16 bytes
      int offset = blockIVBytes.length - BLOCK_SIZE;
      System.arraycopy(blockIVBytes, 0, newBlockIV, offset, blockIVBytes.length);
      return new IvParameterSpec(newBlockIV);
     }
 }
 
 private static void createTestFile(String path) throws Exception{
  File test = new File(path);  
  try(FileOutputStream out = new FileOutputStream(test)){

   StringBuffer buf = new StringBuffer(16);

   int blockCount = 100000;
   for(int i = 0 ; i < blockCount ; i ++){
    buf.append(i);
    int size = buf.length();
    for(int j = 0; j < (14-size); j++ ){
     buf.append('#');
    }
    out.write(buf.toString().getBytes());
    out.write("\r\n".getBytes());
    buf.delete(0, 16);
   }   
  }  
 }
 
 public static void main(String args[]) throws Exception{
  KeyGenerator keyGen = KeyGenerator.getInstance("AES");
  keyGen.init(256,new SecureRandom( ) );
  SecretKey secretKey = keyGen.generateKey();
  byte[] iv = new byte[16]; 
  SecureRandom prng = new SecureRandom();
  prng.nextBytes(iv);
  
  {
   String originalFile = "~/PlainText.txt";
   String encryptedFile = "~/CipherText.enc"; 
   String deryptedFile = "~/Decrypted.txt";   

   AES_CTR_PKCS5PADDING.createTestFile(originalFile); //: Create Testing Data
   
   AES_CTR_PKCS5PADDING.Encrypt(secretKey, iv, new File(originalFile), new File(encryptedFile));
   AES_CTR_PKCS5PADDING.Decrypt(secretKey, iv, new File(encryptedFile), new File(deryptedFile));
   byte[] ret = AES_CTR_PKCS5PADDING.DecryptPartial(secretKey, iv, new File(encryptedFile), 100, 10);   
   System.out.println(new String(ret));
  }
 }

Final:

I don't talk about the GCM cipher mode here. In principle, you don't need the GCM if you don't need the authentication

Reference:

* https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Electronic_Codebook_.28ECB.29
* https://docs.oracle.com/javase/8/docs/technotes/guides/security/SunProviders.html

沒有留言:

張貼留言

歡迎留言討論與指教