前言
程式設計中常見的加密演算法有以下幾種,它們在不同場景中分別有應用。除資訊摘要演算法外,其它加密方式都會需要金鑰。
- 資訊摘要演算法
- 對稱加密演算法
- 非對稱加密演算法
金鑰
金鑰(key,又常稱金鑰)是指某個用來完成加密、解密、完整性驗證等密碼學應用的祕密資訊。
金鑰分類
- 加解密中的金鑰:對稱加密中共享相同的金鑰,非對稱加密中分公鑰和私鑰,公鑰加密私鑰解密。
- 訊息認證碼和數字簽名中的金鑰:在訊息認證碼中,訊息傳送方和接收方使用共享金鑰進行認證。在數字簽名中,簽名使用私鑰,而驗證使用公鑰。
- 會話金鑰和主金鑰:每次通訊只使用一次的金鑰稱為會話金鑰(session key)。相對於會話金鑰,重複使用的金鑰稱為主金鑰(master key)。
金鑰和密碼
密碼一般是由使用者生成,具有可讀性,可以記憶和儲存,常用於軟體管理,而金鑰是供實現加密演算法的軟體使用,不需要具備可讀性(不過在程式設計中為了方便閱讀都進行Base64)。我們也可以通過密碼來生成金鑰。
金鑰管理
- 生成金鑰:可以用隨機數生成金鑰,也可以用口令生成金鑰。
- 配送金鑰:可採用事先共享金鑰、使用金鑰分配中心、使用公鑰密碼、使用Diffie-Hellman金鑰交換。
- 更新金鑰
- 儲存金鑰
- 作廢金鑰
金鑰生成
jdk 中 jce (Java Cryptography Extension) 包含了加密相關的所有API。
生成對稱加密演算法的金鑰
public static SecretKey generateKey(int keySize) {
KeyGenerator keyGenerator;
try {
keyGenerator = KeyGenerator.getInstance("AES");
keyGenerator.init(keySize);
return keyGenerator.generateKey();
} catch (NoSuchAlgorithmException e) {
// ignore
return null;
}
}
生成對稱非對稱加密演算法的金鑰
/**
* 生成非對稱金鑰對
*
* @param keySize 金鑰大小
* @param random 指定隨機來源,預設使用 JCAUtil.getSecureRandom()
* @return 非對稱金鑰對
* @throws NoSuchAlgorithmException NoSuchAlgorithm
*/
public static PPKeys genKeysRSA(int keySize, SecureRandom random) throws NoSuchAlgorithmException {
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
if (null != random) {
generator.initialize(keySize, random);
} else {
generator.initialize(keySize);
}
KeyPair pair = generator.generateKeyPair();
PPKeys keys = new PPKeys();
PublicKey publicKey = pair.getPublic();
PrivateKey privateKey = pair.getPrivate();
keys.setPublicKey(Base64.getEncoder().encodeToString(publicKey.getEncoded()));
keys.setPrivateKey(Base64.getEncoder().encodeToString(privateKey.getEncoded()));
return keys;
}
金鑰協商(Diffie-Hellman)
金鑰協商是一種協議,兩方或多方在通過該協議建立相同的共享金鑰,然後通訊內容進行對稱加密傳輸,而不需要交換金鑰。
大致過程:每一方生成一個公私鑰對並將公鑰分發給其它方,當都獲得其他方的公鑰副本後就可以離線計算共享金鑰。
Java中提供了 KeyAgreement
可以實現金鑰協商。
- Alice 和 Bob 分別用他們的私鑰初始化自己的金鑰協商物件
KeyAgreement
,呼叫init()
方法; - 然後將通訊的每一方的公鑰 傳入執行
doPhase(Key key, boolean lastPhase)
; - 各方生成共享金鑰
generateSecret()
。
public static void diffieHellman() throws Exception {
AlgorithmParameterGenerator dhParams = AlgorithmParameterGenerator.getInstance("DH");
dhParams.init(1024);
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("DH");
keyGen.initialize(dhParams.generateParameters().getParameterSpec(DHParameterSpec.class), new SecureRandom());
KeyAgreement aliceKeyAgree = KeyAgreement.getInstance("DH");
KeyPair alicePair = keyGen.generateKeyPair();
KeyAgreement bobKeyAgree = KeyAgreement.getInstance("DH");
KeyPair bobPair = keyGen.generateKeyPair();
aliceKeyAgree.init(alicePair.getPrivate());
bobKeyAgree.init(bobPair.getPrivate());
aliceKeyAgree.doPhase(bobPair.getPublic(), true);
bobKeyAgree.doPhase(alicePair.getPublic(), true);
boolean agree = Base64.getEncoder().encodeToString(aliceKeyAgree.generateSecret()).equals(
Base64.getEncoder().encodeToString(bobKeyAgree.generateSecret())
);
System.out.println(agree);
}
資訊摘要演算法
資訊摘要演算法又叫加密雜湊演算法,加密過程不需要金鑰,常見的加密雜湊演算法有MD系列和SHA系列。
一個理想的加密雜湊函式應該具備以下特性:
- 任何資訊傳入後,輸出的總是長度固定;
- 訊息摘要看起來是“隨機的”,這樣根據原始資訊就很難推測出值;
- 好的雜湊函式碰撞概率應該極低,也就是不同資訊傳入後得到相同值的概率;
MD系列
MD5資訊摘要演算法(MD5 Message-Digest Algorithm),一種被廣泛使用的加密雜湊函式,輸出出一個128位(16位元組)的雜湊值(hash value),MD5最初設計為加密雜湊函式,而目前發現它存在大量漏洞,所以不建議直接用作加密,不過在非加密場景下如:資料完整性校驗,檔案完整性校驗它仍然有廣泛的應用。
public static String md5(String content) {
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
byte[] bytes = digest.digest(content.getBytes(StandardCharsets.UTF_8));
return Hex.encodeHexString(bytes);
} catch (final NoSuchAlgorithmException e) {
throw new IllegalArgumentException(e);
}
}
SHA系列
安全雜湊演算法(Secure Hash Algorithm,縮寫為SHA)是一個加密雜湊函式家族,是FIPS(美國聯邦資訊處理標準)所認證的安全雜湊演算法。能計算出一個數字訊息所對應到的,長度固定的字串(又稱訊息摘要)的演算法。且若輸入的訊息不同,它們對應到不同字串的機率很高。
它們分別包含 SHA-0、SHA-1、SHA-2、SHA-3
,其中 SHA-0、SHA-1
輸出長度是160位,SHA-2
包含 SHA-224、SHA-256、SHA-384、SHA-512、SHA-512/224、SHA-512/256
,我們平時常用 SHA-256
。
public static String sha256(String content) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256);
byte[] bytes = digest.digest(content.getBytes(StandardCharsets.UTF_8));
return Hex.encodeHexString(bytes);
} catch (final NoSuchAlgorithmException e) {
throw new IllegalArgumentException(e);
}
}
對稱加密演算法
對稱加密演算法,雙方持有相同金鑰進行加解密,常見的對稱加密演算法:DES
3DES
AES128
AES192
AES256
。理解對稱加密需要先明白下面幾個概念:
- 分組密碼模式:將明文切割進行加密,再將密文拼接到一起。比如AES中會將明文資料切割為大小16位元組的資料塊,最後一塊不夠16位元組時,使用Padding模式進行補充。
- 填充(Padding):它有三種模式PKCS5、PKCS7和NOPADDING,PKCS5用缺少的位元組數來填充,比如缺少5個位元組就填充5個數字5,PKCS7缺少的位元組數用0來填充。如果資料剛好是16的整數倍,PKCS5和PKCS7會再補充一個16位元組資料來區分填充和有效資料,NOPADDING模式不需要填充。
- 初始化向量:初始向量IV的作用是使加密更加安全可靠,在分組密碼模式下IV大小對應資料塊長度。
- 加密模式:四種加密模式分別是:ECB(電子密碼本模式)、CBC(密碼分組連結模式)、CFB、OFB。ECB模式是僅僅使用明文和金鑰來加密資料,所以該模式下不需要Padding,安全性也較弱,CBC模式資料分塊並且使用傳入IV依次進行異或操作,安全性也相對較高,所以目前一般都選擇CBC模式。
- 加密金鑰:不同加密演算法金鑰長度不同,比如:DES 預設長度56位,3DES預設長度168位,也支援128位,AES預設128位,也支援192位,256位。我們一般根據密碼生成金鑰,密碼長度需要滿足演算法金鑰長度。
DES
DES
是對稱加密演算法領域中的典型演算法,因為金鑰預設長度為56 bit
,所以密碼長度需要大於 8 byte
,DESKeySpec
取前 8 byte
進行金鑰製作。
public static String encryptDES(byte[] content, String password) {
try {
SecureRandom random = new SecureRandom();
DESKeySpec desKeySpec = new DESKeySpec(password.getBytes());
SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("DES");
SecretKey secretKey = secretKeyFactory.generateSecret(desKeySpec);
Cipher cipher = Cipher.getInstance("DES");
cipher.init(Cipher.ENCRYPT_MODE, secretKey, random);
return Base64.getEncoder().encodeToString(cipher.doFinal(content));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static String decryptDES(String content, String password) throws Exception {
SecureRandom random = new SecureRandom();
DESKeySpec desKeySpec = new DESKeySpec(password.getBytes());
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
SecretKey secretKey = keyFactory.generateSecret(desKeySpec);
Cipher cipher = Cipher.getInstance("DES");
cipher.init(Cipher.DECRYPT_MODE, secretKey, random);
return new String(cipher.doFinal(Base64.getDecoder().decode(content)));
}
3DES
3DES(即Triple DES)。是DES演算法的加強,它使用3條56位的金鑰對資料進行三次加密。它以DES為基本模組,通過組合分組方法設計出分組加密演算法。比起最初的DES,3DES更為安全。金鑰預設長度 168 bit
, 密碼需要大於24 byte
,IV 是 8 byte
的隨機數字和字母陣列。
public static String encrypt3DESECB(String content, String key, String iv) {
try {
IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8));
DESedeKeySpec dks = new DESedeKeySpec(key.getBytes(StandardCharsets.UTF_8));
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DESede");
SecretKey secretkey = keyFactory.generateSecret(dks);
Cipher cipher = Cipher.getInstance("DESede/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, secretkey, ivSpec);
return Base64.getEncoder().encodeToString(cipher.doFinal(content.getBytes(StandardCharsets.UTF_8)));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static String decrypt3DESECB(String content, String key, String iv) {
try {
IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8));
DESedeKeySpec dks = new DESedeKeySpec(key.getBytes(StandardCharsets.UTF_8));
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DESede");
SecretKey secretkey = keyFactory.generateSecret(dks);
Cipher cipher = Cipher.getInstance("DESede/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, secretkey, ivSpec);
return new String(cipher.doFinal(Base64.getDecoder().decode(content)), StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
AES
AES 高階資料加密標準,能夠有效抵禦已知的針對DES演算法的所有攻擊,預設金鑰長度為128 bit
,還可以供選擇 192 bit
,256 bit
。AES-128
AES-192
AES-256
預設 AES-128
,使用 PBEKeySpec
生成固定大小的金鑰。
public static String encryptAES128(String plainText, String password, String salt) throws Exception {
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
byte[] saltBytes = salt.getBytes(StandardCharsets.UTF_8);
// AES-128 金鑰長度為128bit
PBEKeySpec spec = new PBEKeySpec(
password.toCharArray(),
saltBytes,
1000,
128
);
SecretKey secretKey = factory.generateSecret(spec);
SecretKeySpec secret = new SecretKeySpec(secretKey.getEncoded(), "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
AlgorithmParameters params = cipher.getParameters();
IvParameterSpec iv = params.getParameterSpec(IvParameterSpec.class);
cipher.init(Cipher.ENCRYPT_MODE, secret, iv);
byte[] encryptedTextBytes = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
String encodedText = Base64.getEncoder().encodeToString(encryptedTextBytes);
String encodedIV = Base64.getEncoder().encodeToString(iv.getIV());
String encodedSalt = Base64.getEncoder().encodeToString(saltBytes);
return encodedSalt + "." + encodedIV + "." + encodedText;
}
public static String decryptAES128(String encryptedText, String password) throws Exception {
String[] fields = encryptedText.split("\\.");
byte[] saltBytes = Base64.getDecoder().decode(fields[0]);
byte[] ivBytes = Base64.getDecoder().decode(fields[1]);
byte[] encryptedTextBytes = Base64.getDecoder().decode(fields[2]);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
PBEKeySpec spec = new PBEKeySpec(
password.toCharArray(),
saltBytes,
1000,
128
);
SecretKey secretKey = factory.generateSecret(spec);
SecretKeySpec secret = new SecretKeySpec(secretKey.getEncoded(), "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(ivBytes));
byte[] decryptedTextBytes;
try {
decryptedTextBytes = cipher.doFinal(encryptedTextBytes);
return new String(decryptedTextBytes);
} catch (IllegalBlockSizeException | BadPaddingException e) {
throw new RuntimeException(e);
}
}
使用 AES-256
時可能會出現下面異常:
java.security.InvalidKeyException: Illegal key size
JDK 1.8.0_161 及以上版本預設已經啟用無限強度加密:
static {
java.security.Security.setProperty("crypto.policy", "unlimited");
}
JDK 1.8.0_161以前版本需要手動安裝 jce 策略檔案(下載地址)
非對稱加密演算法
非對稱加密使用一對金鑰,公鑰用作加密,私鑰則用作解密。關於金鑰大小,截至2020年,公開已知的最大RSA金鑰是破解的是829位的RSA-250,建議至少使用 2048 位金鑰。
public static String encrypt(byte[] publicKey, String plainText) {
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKey);
KeyFactory kf;
try {
kf = KeyFactory.getInstance("RSA");
PublicKey publicKeySecret = kf.generatePublic(keySpec);
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.ENCRYPT_MODE, publicKeySecret);
byte[] encryptedBytes = cipher.doFinal(plainText.getBytes());
return new String(Base64.getEncoder().encode(encryptedBytes));
} catch (Exception e) {
log.error("Rsa encrypt error ", e);
throw new RuntimeException(e);
}
}
public static String decrypt(byte[] privateKey, String encryptedText) {
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKey);
KeyFactory kf;
try {
kf = KeyFactory.getInstance("RSA");
PrivateKey privateKeySecret = kf.generatePrivate(keySpec);
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.DECRYPT_MODE, privateKeySecret);
return new String(cipher.doFinal(Base64.getDecoder().decode(encryptedText)), StandardCharsets.UTF_8);
} catch (Exception e) {
log.error("Rsa decrypt error ", e);
throw new RuntimeException(e);
}
}