顯示具有 ECB 標籤的文章。 顯示所有文章
顯示具有 ECB 標籤的文章。 顯示所有文章

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

Java JCE - AES 的 Encryption & Decryption @2016-05-01

前言:

都已經2016年了,本來不打算寫這篇的,但偶然發現網路上有多個中文部落格,甚至是論壇上分享或是討論 Java 的 AES 的程式,都沒有討論一些問題,深怕一堆人看到這種範例程式就放到你開發的系統上。另外,為了簡化程式來說明,以下的程式並不考慮Exception處理方式。

內容:

先來看網路上常見的程式的寫法:
 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"); //: 等同 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("加密結果的Base64編碼:" + 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("解密結果:" + 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);  
 }
} 
上面這種寫法,這個程式對於加解密的運作是正常的,但會有潛在的3個問題:


  1. 第 1 個問題在第 4 行 這個用法所使用的 Cipher mode 是 ECB,也就是比較不安全的方式。原因是 ECB 對於相同的資料加密後的結果會是一樣的,有興趣可以參考(Wiki上的那張企鵝圖)。 如果你的應用是每次加密時 secret key 都是重新產生的,而且需要被加密的資料每次都完全不同的時候,各自的資料內容本身也是異質性相當高,如果採用這種做法也沒有太大的問題,但還是不建議。因為以加密的應用來說,常見的對象就是檔案或是運用在傳輸加密。而這兩種方式,大部分都會有相同的資料。以檔案來說,相同類型的檔案你用binary編輯器打開檔案,你就可以觀察到檔案的前面都會有雷同的資料。通訊協定更是如此,例如:HTTP通訊協定。
  2. 第 2 個問題在第 25 行 secret key 長度的問題,不應該使用 128 bits 長度,強度太弱。
  3. 第 3 個問題在第 8 行的 msg.getBytes( ),不同的作業系統所使用的預設 charset 可能是不同的。這樣的做法,有可能會造成不同平台在加密時的行為不如你所預期,也就是解密後的內容可能跟你當初要加密的資料不同。


比較建議的寫法是採用下面這種:

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("加密結果的Base64編碼:" + 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("解密結果:" + 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);  
 }
}
在第 3 行明確指定採用 CBC 的 cipher mode,並且指定 Padding 方式,有興趣可參考這篇 Padding 運作方式。在第 8 行,直接指定用 msg.getBytes("UTF-8"),避免一些問題,如果需要加密的內容都是純ANSI,你可以指定用 ANSI 就可以。

此處,還是要強調 AES的加密 Secret Key 的長度建議至少要 256 bit 以上,用同一把 Secret Key 做加密時候,應該都要產生新的 IV 來加密可以參考下面這種寫法來產生所需要的 AES Secret Key 和 IV。另外,IV 在解密時候,也要用當初加密使用的相同IV才可以。

  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);

另外,由於 Oracle 官方預設標準的Java執行環境沒法支援產生 AES 256 bits 長度的 secret key。所以,你會遇到下面這種錯訊息:

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)

這表示你的執行環境需要安裝 Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files。這裡要特別注意,你務必要安裝跟執行環境JVM相同版本的JCE Policy,否則可能會遇到一些怪異的現象。(Android執行環境不在此限)

最後,常問遇到的問題就是:

  1. 如果檔案很大的Stream類型的檔案(如:Video or Audio)要如何做到加密?
  2. 想要隨意位置讀取已經加密的內容要怎麼做?

建議的做法:


  • 第 1 個問題,很簡單請看清楚 Cipher 的 API doc,採用多次呼叫 update 的方法,最後再呼叫 doFinal 方法即可。
  • 第 2 個問題,只要將 Cipher mode 改為 CTR 即可,另外如果是要隨機存取某個 Block 的資料,必須要自己重新計算那個 Block 開始的 IV。
請參考類似下面的寫法:
(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));
  }
 }



最後:

這邊我沒有提到另外一種 GCM 的 Cipher Mode,原則上,如果你沒有 Authentication 的需要時候,就不需要用到 GCM。

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

2013年8月18日 星期日

Python M2Crypto - AES 的 Encrypt 與 Decrypt

        這邊要介紹有關 AES 的 Encrypt 和 Decrypt ,其中會介紹一些 AES 的基本知識和相關名詞以及一個簡單的範例程式說明。

AES的基本知識

        AES(Advanced Encryption Standard)是一種對稱式(symmetric)的加密演算法,是透過對每個固定大小的4x4位元矩陣區塊(block = 128 bits = 16 bytes),對其每一個元素進行多次交互置換和XOR運算。簡單來說,就是用同一把 secret key 進行Encrypt和Decrypt的動作。優點是對於資料大的檔案加解密的速度較快,而且容易透過硬體實作,運算所需要的記憶體較少,但是缺點是在資料交換的過程中,雙方必須取得這把相同的secret key。否則,接收到加密檔案的接收者,無法解開加密後的檔案。而它的安全性強度取決於金鑰(secret key)的長度,目前美國國防安全局審核認定的AES標準演算法secret key長度有128 bits, 192 bits和256 bits 三種。secret key的長度越長就越安全,但是相對的加密所需的時間就越多,然而實際上secret key長度對於加密時間的影響並不大,主要還是取決於所選擇的 Block Cipher Mode以及需要加密的資料大小。

AES演算法中常見的專有名詞

        在AES演算法中,不管是網路上的文章或是API文件中,常見到以下這些名詞:

Initialization vector (IV)

        "初始化向量"或稱"起始變數",它的用途主要在於避免相同的資料加密多次都產生相同的密文(Cipher Text)。因此,使用上必須要注意的是,相同的一把金鑰(secret key)在加密的時候,不可以使用相同的 IV,否則就破壞了AES的安全性。此外 IV 本身並不需要保護,它是可以被公開的。而IV的最大長度必須是 16 bytes,而且產生IV的方式必須是無法預測的,也就是隨機產生即可。

以下是關於幾種常見的IV類型:
(1) Fixed IV
  顧名思義就是,在使用同一把金鑰(secret key)在加密的時候,所使用的IV都是固定的。這會造成兩個相同的 plain-text 的區塊,產生的 cipher-text 區塊是相同的。

(2) Counter IV
  是指在使用同一把金鑰(secret key)在加密的時候,每次加密一個訊息時,都將 IV 累加 1。

(3) Random IV
  是指隨機產生一個亂數當作 IV。建議的作法是將第一個 IV 當作密文的第一個 cipher-text 區塊。從第二個 cipher-text 區塊才是真正的加密資料。

(4) Nonce-Generated IV
  是指使用相同的一把金鑰(secret key)在加密的時候,用一個 nonce 來產生 IV 。nonce 是指 number used once 的意思,主要的精神在於同一把金鑰(secret key)不可以使用相同的 nonce。例如:假設選擇 nonce = 0 開始後,使用相同的一把金鑰(secret key)在加密的時候,不可以出現使用 nonce = 0 第 2 次。

Padding

        由於AES加密過程,是針對每個固定大小的區塊(16 bytes),進行多次的交互置換和XOR運算,因此當需要被加密的資料小於矩陣區塊16 bytes 的時候,或是資料的size 不是 16 bytes的倍數時,為了讓加密能夠順利進行,必須將資料的 size 補齊到能夠被 16 bytes 整除的大小。舉例來說,假設需要保護的資料總長度只有 5 bytes,那在進行AES加密之前,必須補齊資料長度達到16 bytes。至於,不同補齊的方式可以參考 Use Padding in Encryption 這篇文章。

特別註記,以下的圖出自於維基百科 (https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation)

Block cipher mode 

  • ECB (Electronic codebook,ECB)
    ECB是對於每一個資料區塊都用同一把金鑰(secret key)去加密,而且沒有使用 IV。缺點是在於相同的資料區塊,加密後的密文(Cipher Text)會是相同的。優點是每個區塊都可以獨立進行加密,因此可同時對每個區塊進行加密。

  • CBC (Cipher-block chaining)
    CBC是一種串鏈的加密方式,第一個資料區塊必須加入IV和金鑰(secret key)進行加密,之後將加密後的密文(Cipher Text)作為第二個資料區塊的IV再加上金鑰進行加密,以此類推下去,直到所有區塊都被加密完成。這種方式在加密過程當中,下一個區塊必須依賴這個區塊加密後的結果才能夠得到IV,因此無法同時進行。但是在解密的時候,可以同時對所有區塊進行解密,因為前後兩個密文(Cipher Text)區塊,後面的密文區塊要解密時候所需要的IV就是前一個密文區塊。

  • CFB (Cipher feedback)
    CFB 則是將 IV 和金鑰(secret key) 產生出 密文(Cipher Text)區塊,然後把密文(Cipher Text)區塊和明文資料區塊進行XOR運算後的值,當做下一個資料區塊的IV再和金鑰(secret key)產生出下一個密文區塊,以此類推下去做相同的運算,直到所有區塊都被加密完成。

  • OFB (Output feedback)
    OFB 則是將 IV 和 金鑰 (secret key) 產生密文區塊(Cipher Block),然後將Cipher Block和明文區塊 PlainText 進行 XOR 運算。而這個區塊產生的 Cipher Block 則當作下一個資料區塊加密處理所需的 IV。


  • CTR (Counter)
    CTR 則是先透過所謂的 nonce (其實就是IV) 加上可以在長時間內每次都產生不重復 sequence 整數的 counter 所組合出來的一個整數值, 接著用同一把金鑰 (secret key) 對這個整數值加密後的產生一個密文區塊 (Cipher Block), 最後把這個密文區塊和明文區塊(PlainText)進行XOR運算。這種做法每個區塊都可以獨立地進行加密或是解密,因此可運用在平行處理的加解密運算。

AES Encrypt / Decrypt 基礎範例程式解說


 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
import M2Crypto.EVP as EVP
import cStringIO

ENCRYPT_OP = 1
DECRYPT_OP = 0

def aes_encrypt(iv, secret_key, plain_text):
    cipher = EVP.Cipher(alg='aes_128_cbc', 
                        key=secret_key, 
                        iv=iv,
                        op=ENCRYPT_OP)

    input_buffer  = cStringIO.StringIO(plain_text)        
    cipher_data1 = cipher.update(input_buffer.read())
    cipher_data2 = cipher.final()
    input_buffer.close()
        
    output_buffer = cStringIO.StringIO()
    output_buffer.write(cipher_data1)
    output_buffer.write(cipher_data2)
    cipher_text = output_buffer.getvalue()    
    output_buffer.close()
    
    return cipher_text
    
def aes_decrypt(iv, secret_key, cipher_text):
    cipher = EVP.Cipher(alg='aes_128_cbc', 
                        key=secret_key, 
                        iv=iv, 
                        op=DECRYPT_OP)
    
    input_buffer = cStringIO.StringIO(cipher_text)
    plain_text1 = cipher.update(input_buffer.read())
    plain_text2 = cipher.final()
    input_buffer.close()
    
    output_buffer = cStringIO.StringIO()
    output_buffer.write(plain_text1)
    output_buffer.write(plain_text2)
    plain_text = output_buffer.getvalue()    
    output_buffer.close()
    
    return plain_text

if __name__ == '__main__':

    IV = 'c782dc4c098c66cb' # 16 bytes
    secrect_key = 'c286696d887c9aa0' # 16 bytes
    
    data = 'This is a 48-byte message (exactly 3 AES blocks)'

    cipher_text = aes_encrypt(IV, secrect_key, data)
    plain_text = aes_decrypt(IV, secrect_key, cipher_text)

    print('Cipher Text length is %d bytes'% len(cipher_text))    
    print('Cipher Text is as following')
    print('####################')
    print(cipher_text)    
    print('####################')            
    print('Plain Text=%s'% plain_text)

        EVP是OpenSSL針對對稱式加密所提供的模組,它針對某些對稱式(Symmetric)密碼學演算法在處理加密或是解密時,所需要的API規格。

  1. 因此在加密或是解密之前,需要產生一個Cipher的物件來負責加密或是解密。
  2. 初始化Cipher物件時,第 8 和 27 行程式的 alg 參數指定使用 128-bits 長度的 AES-CBC模式的演算法。這個 alg 參數在AES演算法的使用上,可以指定以下幾種分別代表不同長度和模式:
    • ECB
      • aes_128_ecb 
      • aes_192_ecb
      • aes_256_ecb
    • CBC
      • aes_128_cbc
      • aes_192_cbc
      • aes_256_cbc
    • CFB
      • aes_128_cfb
      • aes_192_cfb
      • aes_256_cfb
    • OFB
      • aes_128_ofb
      • aes_192_ofb
      • aes_256_ofb
  3. 初始化Cipher物件時,第 9 和 28 行程式的 key 參數是用來指定加密或是解密時,所需要用到的金鑰(secret key)。它的長度至少需要128 bits 也就是16 bytes。若你是使用aes_256_cbc模式,則金鑰長度必須為256 bits 等於 32 bytes長度。
  4. 初始化Cipher物件時,第 10 和 29 行程式的 iv 參數是用來指定初始向量(Initialization vector),它的最大長度為 16 bytes。
  5. 初始化Cipher物件時,根據要做加密或是解密來決定 op的參數。若是用於加密,則 op 參數要設定為 1 。反之,用於解密時,則 op 參數必須指定為 0。
  6. 在程式碼的的 第 47 行,我們選定的 IV 長度為 16 bytes 。根據我的實驗結果,在M2Crypto API 使用上,即使所給的 IV不足 16 bytes,它還是可以執行。在 secret key不變的情況下,每次執行後的密文都會改變。但是,如果指定的 IV 剛好是 16 bytes長度時,執行多次後的密文是相同的。如果指定的 IV 長度大於  16 bytes,則超過16 bytes後的資料將不會被當做 IV 使用。然而,演算法上 IV 的長度應該要取決於你所使用的 AES mode 與金鑰長度來決定。
  7. 在程式碼的第 48 行,我們選定的 secret key 長度為 16 bytes 。因為這個範例程式指定的 AES模式為 aes_128_cbc,就 AES 128 bits 加密所需要的長度就是16 bytes 。但是,在 M2Crypto API 使用上,它的行為和 IV 有類似的狀況。以這個範例來說,如果 secret key 少於 16 bytes 而 IV 不變的情況下,對相同的資料加密後所產生的密文,對相同的資料執行多次後的密文都會不同。但是,如果 secret key 剛好指定 16 bytes 時候,對相同資料執行多次加密後所得到的密文是相同的。如果指定的 secret key 超過 16 bytes 時候,超過16 bytes 後的資料不會被當作 secret key來使用。

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.

Reference: