Java AES加密和解密
高级加密标准 (AES,Rijndael)是一种分组密码加密和解密算法,是全球使用最广泛的加密算法。 AES使用128、192或256位的**来处理128位的块。
本文向您展示了一些Java AES加密和解密示例:
- AES字符串加密–(加密和解密字符串)。
- AES基于密码的加密–(**将从给定的密码派生)。
- AES文件加密。 (基于密码)。
在本文中,我们重点介绍通过Galois Counter Mode(GCM)进行的256位AES加密。
GCM = CTR + Authentication.
进一步阅读
阅读本– NIST – Galois /计数器模式(GCM)的建议
不要使用AES电子密码本(ECB)模式
AES ECB模式或AES/ECB/PKCS5Padding
(在Java中)在语义上并不安全 – ECB加密的密文可能泄漏有关纯文本的信息。 这是关于为什么不应该使用ECB加密的讨论。
1. Java和AES加密输入。
在AES加密和解密中,我们需要以下输入:
AES加密最佳做法
不要重复使用具有相同**的IV。
1.1 IV(初始值或初始向量),它是随机字节,通常为12个字节或16个字节。 在Java中,我们可以使用SecureRandom
生成随机IV。
// 16 bytes IV
public static byte[] getRandomNonce() {
byte[] nonce = new byte[16];
new SecureRandom().nextBytes(nonce);
return nonce;
}
// 12 bytes IV
public static byte[] getRandomNonce() {
byte[] nonce = new byte[12];
new SecureRandom().nextBytes(nonce);
return nonce;
}
1.2 AES**,即AES-128
或AES-256
。 在Java中,我们可以使用KeyGenerator
生成AES**。
// 256 bits AES secret key
public static SecretKey getAESKey() throws NoSuchAlgorithmException {
KeyGenerator ****** = KeyGenerator.getInstance("AES");
******.init(256, SecureRandom.getInstanceStrong());
return ******.generateKey();
}
1.3从给定密码派生的AES**。 在Java中,我们可以使用SecretKeyFactory
和PBKDF2WithHmacSHA256
从给定的密码生成AES**。
// AES key derived from a password
public static SecretKey getAESKeyFromPassword(char[] password, byte[] salt)
throws NoSuchAlgorithmException, InvalidKeySpecException {
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
// iterationCount = 65536
// keyLength = 256
KeySpec spec = new PBEKeySpec(password, salt, 65536, 256);
SecretKey secret = new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES");
return secret;
}
我们使用salt
来保护彩虹攻击,它也是一个随机字节,我们可以使用相同的1.1 getRandomNonce
生成它。
1.4我们将上述方法分组为一个util
类,这样我们就不会一次又一次重复相同的代码。
package com.mkyong.crypto.utils;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.ArrayList;
import java.util.List;
public class CryptoUtils {
public static byte[] getRandomNonce(int numBytes) {
byte[] nonce = new byte[numBytes];
new SecureRandom().nextBytes(nonce);
return nonce;
}
// AES secret key
public static SecretKey getAESKey(int keysize) throws NoSuchAlgorithmException {
KeyGenerator ****** = KeyGenerator.getInstance("AES");
******.init(keysize, SecureRandom.getInstanceStrong());
return ******.generateKey();
}
// Password derived AES 256 bits secret key
public static SecretKey getAESKeyFromPassword(char[] password, byte[] salt)
throws NoSuchAlgorithmException, InvalidKeySpecException {
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
// iterationCount = 65536
// keyLength = 256
KeySpec spec = new PBEKeySpec(password, salt, 65536, 256);
SecretKey secret = new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES");
return secret;
}
// hex representation
public static String hex(byte[] bytes) {
StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(String.format("%02x", b));
}
return result.toString();
}
// print hex with block size split
public static String hexWithBlockSize(byte[] bytes, int blockSize) {
String hex = hex(bytes);
// one hex = 2 chars
blockSize = blockSize * 2;
// better idea how to print this?
List<String> result = new ArrayList<>();
int index = 0;
while (index < hex.length()) {
result.add(hex.substring(index, Math.min(index + blockSize, hex.length())));
index += blockSize;
}
return result.toString();
}
}
2. AES加密和解密。
AES-GSM是使用最广泛的认证密码。 本示例将在Galois计数器模式(GCM)中使用256位AES加密和解密字符串。
AES-GCM输入:
- AES**(256位)
- IV – 96位(12字节)
- 身份验证标签的长度(以位为单位)– 128位(16字节)
2.1在Java中,我们使用AES/GCM/NoPadding
表示AES-GCM
算法。 对于加密的输出,我们将16字节的IV前缀到加密的文本(密文)之前,因为解密需要相同的IV。
如果IV是众所周知的,可以吗?
IV公开是可以的,唯一的秘诀就是**,对它保密并保密。
本示例将使用AES加密纯文本Hello World AES-GCM
,然后将其解密回原始纯文本。
package com.mkyong.crypto.encryptor;
import com.mkyong.crypto.utils.CryptoUtils;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
/**
* AES-GCM inputs - 12 bytes IV, need the same IV and secret keys for encryption and decryption.
* <p>
* The output consist of iv, encrypted content, and auth tag in the following format:
* output = byte[] {i i i c c c c c c ...}
* <p>
* i = IV bytes
* c = content bytes (encrypted content, auth tag)
*/
public class EncryptorAesGcm {
private static final String ENCRYPT_ALGO = "AES/GCM/NoPadding";
private static final int TAG_LENGTH_BIT = 128;
private static final int IV_LENGTH_BYTE = 12;
private static final int AES_KEY_BIT = 256;
private static final Charset UTF_8 = StandardCharsets.UTF_8;
// AES-GCM needs GCMParameterSpec
public static byte[] encrypt(byte[] pText, SecretKey secret, byte[] iv) throws Exception {
Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO);
cipher.init(Cipher.ENCRYPT_MODE, secret, new GCMParameterSpec(TAG_LENGTH_BIT, iv));
byte[] encryptedText = cipher.doFinal(pText);
return encryptedText;
}
// prefix IV length + IV bytes to cipher text
public static byte[] encryptWithPrefixIV(byte[] pText, SecretKey secret, byte[] iv) throws Exception {
byte[] cipherText = encrypt(pText, secret, iv);
byte[] cipherTextWithIv = ByteBuffer.allocate(iv.length + cipherText.length)
.put(iv)
.put(cipherText)
.array();
return cipherTextWithIv;
}
public static String decrypt(byte[] cText, SecretKey secret, byte[] iv) throws Exception {
Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO);
cipher.init(Cipher.DECRYPT_MODE, secret, new GCMParameterSpec(TAG_LENGTH_BIT, iv));
byte[] plainText = cipher.doFinal(cText);
return new String(plainText, UTF_8);
}
public static String decryptWithPrefixIV(byte[] cText, SecretKey secret) throws Exception {
ByteBuffer bb = ByteBuffer.wrap(cText);
byte[] iv = new byte[IV_LENGTH_BYTE];
bb.get(iv);
//bb.get(iv, 0, iv.length);
byte[] cipherText = new byte[bb.remaining()];
bb.get(cipherText);
String plainText = decrypt(cipherText, secret, iv);
return plainText;
}
public static void main(String[] args) throws Exception {
String OUTPUT_FORMAT = "%-30s:%s";
String pText = "Hello World AES-GCM, Welcome to Cryptography!";
// encrypt and decrypt need the same key.
// get AES 256 bits (32 bytes) key
SecretKey secretKey = CryptoUtils.getAESKey(AES_KEY_BIT);
// encrypt and decrypt need the same IV.
// AES-GCM needs IV 96-bit (12 bytes)
byte[] iv = CryptoUtils.getRandomNonce(IV_LENGTH_BYTE);
byte[] encryptedText = EncryptorAesGcm.encryptWithPrefixIV(pText.getBytes(UTF_8), secretKey, iv);
System.out.println("\n------ AES GCM Encryption ------");
System.out.println(String.format(OUTPUT_FORMAT, "Input (plain text)", pText));
System.out.println(String.format(OUTPUT_FORMAT, "Key (hex)", CryptoUtils.hex(secretKey.getEncoded())));
System.out.println(String.format(OUTPUT_FORMAT, "IV (hex)", CryptoUtils.hex(iv)));
System.out.println(String.format(OUTPUT_FORMAT, "Encrypted (hex) ", CryptoUtils.hex(encryptedText)));
System.out.println(String.format(OUTPUT_FORMAT, "Encrypted (hex) (block = 16)", CryptoUtils.hexWithBlockSize(encryptedText, 16)));
System.out.println("\n------ AES GCM Decryption ------");
System.out.println(String.format(OUTPUT_FORMAT, "Input (hex)", CryptoUtils.hex(encryptedText)));
System.out.println(String.format(OUTPUT_FORMAT, "Input (hex) (block = 16)", CryptoUtils.hexWithBlockSize(encryptedText, 16)));
System.out.println(String.format(OUTPUT_FORMAT, "Key (hex)", CryptoUtils.hex(secretKey.getEncoded())));
String decryptedText = EncryptorAesGcm.decryptWithPrefixIV(encryptedText, secretKey);
System.out.println(String.format(OUTPUT_FORMAT, "Decrypted (plain text)", decryptedText));
}
}
输出量
纯文本: Hello World AES-GCM
------ AES GCM Encryption ------
Input (plain text) :Hello World AES-GCM
Key (hex) :603d87185bf855532f14a77a91ec7b025c004bf664e9f5c6e95613ee9577f436
IV (hex) :bdb271ce5235996a0709e09c
Encrypted (hex) :bdb271ce5235996a0709e09c2d03eefe319e9329768724755c56291aecaef88cd1e6bdf72b8c7b54d75a94e66b0cd3
Encrypted (hex) (block = 16) :[bdb271ce5235996a0709e09c2d03eefe, 319e9329768724755c56291aecaef88c, d1e6bdf72b8c7b54d75a94e66b0cd3]
------ AES GCM Decryption ------
Input (hex) :bdb271ce5235996a0709e09c2d03eefe319e9329768724755c56291aecaef88cd1e6bdf72b8c7b54d75a94e66b0cd3
Input (hex) (block = 16) :[bdb271ce5235996a0709e09c2d03eefe, 319e9329768724755c56291aecaef88c, d1e6bdf72b8c7b54d75a94e66b0cd3]
Key (hex) :603d87185bf855532f14a77a91ec7b025c004bf664e9f5c6e95613ee9577f436
Decrypted (plain text) :Hello World AES-GCM
纯文本: Hello World AES-GCM, Welcome to Cryptography!
------ AES GCM Encryption ------
Input (plain text) :Hello World AES-GCM, Welcome to Cryptography!
Key (hex) :ddc24663d104e1c2f81f11aef98156503dafdc435f81e3ac3d705015ebab095c
IV (hex) :b05d6aedf023f73b9e1e2d11
Encrypted (hex) :b05d6aedf023f73b9e1e2d11f6f5137d971aea8c5cdd5b045e0960eb4408e0ee4635cccc2dfeec2c13a89bd400f659be82dc2329e9c36e3b032f38bd42296a8495ac840b0625c097d9
Encrypted (hex) (block = 16) :[b05d6aedf023f73b9e1e2d11f6f5137d, 971aea8c5cdd5b045e0960eb4408e0ee, 4635cccc2dfeec2c13a89bd400f659be, 82dc2329e9c36e3b032f38bd42296a84, 95ac840b0625c097d9]
------ AES GCM Decryption ------
Input (hex) :b05d6aedf023f73b9e1e2d11f6f5137d971aea8c5cdd5b045e0960eb4408e0ee4635cccc2dfeec2c13a89bd400f659be82dc2329e9c36e3b032f38bd42296a8495ac840b0625c097d9
Input (hex) (block = 16) :[b05d6aedf023f73b9e1e2d11f6f5137d, 971aea8c5cdd5b045e0960eb4408e0ee, 4635cccc2dfeec2c13a89bd400f659be, 82dc2329e9c36e3b032f38bd42296a84, 95ac840b0625c097d9]
Key (hex) :ddc24663d104e1c2f81f11aef98156503dafdc435f81e3ac3d705015ebab095c
Decrypted (plain text) :Hello World AES-GCM, Welcome to Cryptography!
3.基于AES密码的加密和解密。
对于基于密码的加密,我们可以使用定义为RFC 8018的基于密码的密码规范(PKCS)从给定的密码生成**。
对于PKCS输入:
- 密码,您提供。
- 盐–至少64位(8字节)随机字节。
- 迭代计数–建议最小迭代计数为1,000。
什么是盐和迭代计数?
-
salt
会为给定的密码生成广泛的**集。 例如,如果盐是128位,则每个密码将有多达2 ^ 128个**。 因此,它增加了彩虹攻击的难度。 此外,攻击者为一个用户的密码构建的彩虹表对于另一用户变得毫无用处。 -
iteration count
增加了从密码生成**的成本,因此增加了难度并减慢了攻击速度。
3.1对于加密的输出,我们在密文前面加上12 bytes IV
和password salt
,因为我们需要相同的IV和密码盐(用于**)进行解密。 此外,我们使用Base64
编码器将加密的文本编码为字符串表示形式,以便我们可以以字符串格式(字节数组)发送加密的文本或密文。
如果密码盐是众所周知的,可以吗?
与IV相同,并且可以公开知道密码盐,唯一的秘诀就是**,并对其进行保密和保密。
package com.mkyong.crypto.encryptor;
import com.mkyong.crypto.utils.CryptoUtils;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
/**
* AES-GCM inputs - 12 bytes IV, need the same IV and secret keys for encryption and decryption.
* <p>
* The output consist of iv, password's salt, encrypted content and auth tag in the following format:
* output = byte[] {i i i s s s c c c c c c ...}
* <p>
* i = IV bytes
* s = Salt bytes
* c = content bytes (encrypted content)
*/
public class EncryptorAesGcmPassword {
private static final String ENCRYPT_ALGO = "AES/GCM/NoPadding";
private static final int TAG_LENGTH_BIT = 128; // must be one of {128, 120, 112, 104, 96}
private static final int IV_LENGTH_BYTE = 12;
private static final int SALT_LENGTH_BYTE = 16;
private static final Charset UTF_8 = StandardCharsets.UTF_8;
// return a base64 encoded AES encrypted text
public static String encrypt(byte[] pText, String password) throws Exception {
// 16 bytes salt
byte[] salt = CryptoUtils.getRandomNonce(SALT_LENGTH_BYTE);
// GCM recommended 12 bytes iv?
byte[] iv = CryptoUtils.getRandomNonce(IV_LENGTH_BYTE);
// secret key from password
SecretKey aesKeyFromPassword = CryptoUtils.getAESKeyFromPassword(password.toCharArray(), salt);
Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO);
// ASE-GCM needs GCMParameterSpec
cipher.init(Cipher.ENCRYPT_MODE, aesKeyFromPassword, new GCMParameterSpec(TAG_LENGTH_BIT, iv));
byte[] cipherText = cipher.doFinal(pText);
// prefix IV and Salt to cipher text
byte[] cipherTextWithIvSalt = ByteBuffer.allocate(iv.length + salt.length + cipherText.length)
.put(iv)
.put(salt)
.put(cipherText)
.array();
// string representation, base64, send this string to other for decryption.
return Base64.getEncoder().encodeToString(cipherTextWithIvSalt);
}
// we need the same password, salt and iv to decrypt it
private static String decrypt(String cText, String password) throws Exception {
byte[] decode = Base64.getDecoder().decode(cText.getBytes(UTF_8));
// get back the iv and salt from the cipher text
ByteBuffer bb = ByteBuffer.wrap(decode);
byte[] iv = new byte[IV_LENGTH_BYTE];
bb.get(iv);
byte[] salt = new byte[SALT_LENGTH_BYTE];
bb.get(salt);
byte[] cipherText = new byte[bb.remaining()];
bb.get(cipherText);
// get back the aes key from the same password and salt
SecretKey aesKeyFromPassword = CryptoUtils.getAESKeyFromPassword(password.toCharArray(), salt);
Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO);
cipher.init(Cipher.DECRYPT_MODE, aesKeyFromPassword, new GCMParameterSpec(TAG_LENGTH_BIT, iv));
byte[] plainText = cipher.doFinal(cipherText);
return new String(plainText, UTF_8);
}
public static void main(String[] args) throws Exception {
String OUTPUT_FORMAT = "%-30s:%s";
String PASSWORD = "this is a password";
String pText = "AES-GSM Password-Bases encryption!";
String encryptedTextBase64 = EncryptorAesGcmPassword.encrypt(pText.getBytes(UTF_8), PASSWORD);
System.out.println("\n------ AES GCM Password-based Encryption ------");
System.out.println(String.format(OUTPUT_FORMAT, "Input (plain text)", pText));
System.out.println(String.format(OUTPUT_FORMAT, "Encrypted (base64) ", encryptedTextBase64));
System.out.println("\n------ AES GCM Password-based Decryption ------");
System.out.println(String.format(OUTPUT_FORMAT, "Input (base64)", encryptedTextBase64));
String decryptedText = EncryptorAesGcmPassword.decrypt(encryptedTextBase64, PASSWORD);
System.out.println(String.format(OUTPUT_FORMAT, "Decrypted (plain text)", decryptedText));
}
}
输出量
------ AES GCM Password-based Encryption ------
Input (plain text) :AES-GSM Password-Bases encryption!
Encrypted (base64) :KmrvjnMusJTQo/hB7T5BvlQpvi3bVbdjpZP51NT7I/enrIfSQuDfSK6iXgdPzvUP2IE54mwrKiyHqMkG8224lRZ9tXHcclmdh98I8b3B
------ AES GCM Password-based Decryption ------
Input (base64) :KmrvjnMusJTQo/hB7T5BvlQpvi3bVbdjpZP51NT7I/enrIfSQuDfSK6iXgdPzvUP2IE54mwrKiyHqMkG8224lRZ9tXHcclmdh98I8b3B
Decrypted (plain text) :AES-GSM Password-Bases encryption!
3.2如果密码不匹配,Java会抛出AEADBadTagException: Tag mismatch!
// change the password to something else
String decryptedText = EncryptorAesGcmPassword.decrypt(encryptedTextBase64, "other password");
System.out.println(String.format(OUTPUT_FORMAT, "Decrypted (plain text)", decryptedText));
输出量
Exception in thread "main" javax.crypto.AEADBadTagException: Tag mismatch!
at java.base/com.sun.crypto.provider.GaloisCounterMode.decryptFinal(GaloisCounterMode.java:623)
at java.base/com.sun.crypto.provider.CipherCore.finalNoPadding(CipherCore.java:1118)
at java.base/com.sun.crypto.provider.CipherCore.fillOutputBuffer(CipherCore.java:1055)
at java.base/com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:855)
at java.base/com.sun.crypto.provider.AESCipher.engineDoFinal(AESCipher.java:446)
at java.base/javax.crypto.Cipher.doFinal(Cipher.java:2207)
at com.mkyong.crypto.encryptor.EncryptorAesGcmPassword.decrypt(EncryptorAesGcmPassword.java:88)
at com.mkyong.crypto.encryptor.EncryptorAesGcmPassword.main(EncryptorAesGcmPassword.java:109)
4. AES文件加密和解密。
此示例是基于AES密码的文件加密。 想法是相同的,但是我们需要一些IO类来处理资源或文件。
这是resources
文件夹中的文本文件。
This is line 1.
This is line 2.
This is line 3.
This is line 4.
This is line 5.
This is line 9.
This is line 10.
4.1此示例类似于3.1 EncryptorAesGcmPassword.java
,但有一些小的更改,例如返回byte[]
而不是base64编码的字符串。
public static byte[] encrypt(byte[] pText, String password) throws Exception {
//...
// prefix IV and Salt to cipher text
byte[] cipherTextWithIvSalt = ByteBuffer.allocate(iv.length + salt.length + cipherText.length)
.put(iv)
.put(salt)
.put(cipherText)
.array();
// it works, even if we save the based64 encoded string into a file.
// return Base64.getEncoder().encodeToString(cipherTextWithIvSalt);
// we save the byte[] into a file.
return cipherTextWithIvSalt;
}
添加encryptFile
和decryptFile
工作与文件。
public static void encryptFile(String fromFile, String toFile, String password) throws Exception {
// read a normal txt file
byte[] fileContent = Files.readAllBytes(Paths.get(ClassLoader.getSystemResource(fromFile).toURI()));
// encrypt with a password
byte[] encryptedText = EncryptorAesGcmPasswordFile.encrypt(fileContent, password);
// save a file
Path path = Paths.get(toFile);
Files.write(path, encryptedText);
}
public static byte[] decryptFile(String fromEncryptedFile, String password) throws Exception {
// read a file
byte[] fileContent = Files.readAllBytes(Paths.get(fromEncryptedFile));
return EncryptorAesGcmPasswordFile.decrypt(fileContent, password);
}
4.2从类路径中读取以上readme.txt
文件,对其进行加密,然后将加密的数据保存到新文件c:\test\readme.encrypted.txt
。
String password = "password123";
String fromFile = "readme.txt"; // from resources folder
String toFile = "c:\\test\\readme.encrypted.txt";
// encrypt file
EncryptorAesGcmPasswordFile.encryptFile(fromFile, toFile, password);
输出量
4.3读取加密的文件,解密并打印输出。
String password = "password123";
String toFile = "c:\\test\\readme.encrypted.txt";
// decrypt file
byte[] decryptedText = EncryptorAesGcmPasswordFile.decryptFile(toFile, password);
String pText = new String(decryptedText, UTF_8);
System.out.println(pText);
输出量
This is line 1.
This is line 2.
This is line 3.
This is line 4.
This is line 5.
This is line 9.
This is line 10.
PS AES图像加密是相同的概念。
下载源代码
$ git clone https://github.com/mkyong/core-java
$ cd java-crypto
让我知道文章是否需要改进。 谢谢。
参考文献
- *–密码JavaDoc
- *–密码块链接(CBC)
- *-Galois / Counter Mode(GCM)
- Oracle – KeyGenerator算法JavaDoc
- Java –如何生成随机的12个字节?
- 为什么不应该使用ECB加密?
- Spring Security加密模块
- *– PBKDF2
- RFC 8018 – PKCS
- Java –如何连接和分割字节数组
- Java安全标准算法名称
- NIST – Galois /计数器模式(GCM)的建议
From: https://mkyong.com/java/java-aes-encryption-and-decryption/
上一篇: Java AES对称加密解密
下一篇: JAVA的对称加密算法AES—加密和解密