1、概述
非對稱加密算法
加密和解密用的密鑰是不同的,這種加密方式是用數學上的難解問題構造的,通常加密解密的速度比較慢,適合偶爾發送數據的場合。優點是密鑰傳輸方便。常見的非對稱加密算法爲RSA、ECC和EIGamal。
具體詳細分析參考文章Java 實現 RSA 非對稱加密算法
2、工具類
public class RSAUtils {
private static final String PROVIDER_NAME = "BC";
public final static String MAP_KEY_PUBLIC_KEY = "publicKey";
public final static String MAP_KEY_PRIVATE_KEY = "privateKey";
/** 數字簽名算法:SHA1withRSA **/
public final static String SIGNATURE_ALGORITHM_SHA1WITHRSA = "SHA1withRSA";
/** 數字簽名算法:MD5withRSA **/
public final static String SIGNATURE_ALGORITHM_MD5WITHRSA = "MD5withRSA";
/** 加密算法RSA **/
public static final String KEY_ALGORITHM = "RSA";
public static final String TRANSFORMATION_PKCS1PADDING = "RSA/ECB/PKCS1Padding";
public static final String TRANSFORMATION_NOPADDING = "RSA/ECB/NOPADDING";
/**
* 生成RSA公私鑰對
* Map.keySet()
*/
public static Map<String, String> generateRSAKeyPlain() {
Map<String, String> map = new HashMap<String, String>();
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KEY_ALGORITHM);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
map.put(MAP_KEY_PUBLIC_KEY, getPublicKeyPlain(keyPair.getPublic()));
map.put(MAP_KEY_PRIVATE_KEY, getPrivateKeyPlain(keyPair.getPrivate()));
} catch (NoSuchAlgorithmException e) {
System.err.println("無此算法");
e.printStackTrace();
}
return map;
}
/**
* RSA加密
* @param key 公鑰
* @param data 數據
*/
public static byte[] encrypt(Key key, byte[] data) throws Exception {
return encrypt(key, "RSA/ECB/PKCS1Padding", data);
}
/**
* RSA加密
* @param key 加密
* @param 算法
* @param data 數據
*/
public static byte[] encrypt(Key key, String transformation, byte[] data) throws Exception {
try {
Cipher cipher = Cipher.getInstance(transformation, PROVIDER_NAME);
cipher.init(Cipher.ENCRYPT_MODE, key);
int blockSize = cipher.getBlockSize();
int outputSize = cipher.getOutputSize(data.length);
int leavedSize = data.length % blockSize;
int blocksSize = leavedSize != 0 ? data.length / blockSize + 1
: data.length / blockSize;
byte[] raw = new byte[outputSize * blocksSize];
int i = 0;
while (data.length - i * blockSize > 0) {
if (data.length - i * blockSize > blockSize) {
cipher.doFinal(data, i * blockSize, blockSize, raw, i
* outputSize);
} else {
cipher.doFinal(data, i * blockSize, data.length - i
* blockSize, raw, i * outputSize);
}
i++;
}
return raw;
} catch (Exception e) {
throw e;
}
}
/**
* RSA解密
*/
public static byte[] decrypt(Key key, byte[] data) throws Exception {
return decrypt(key, "RSA/ECB/PKCS1Padding", data);
}
/**
* RSA解密
*/
public static byte[] decrypt(Key key, String transformation, byte[] data) throws Exception {
try {
Cipher cipher = Cipher.getInstance(transformation, PROVIDER_NAME);
cipher.init(Cipher.DECRYPT_MODE, key);
int blockSize = cipher.getBlockSize(); // 密文數據分塊
if (data.length % blockSize != 0)
throw new AppBizException("RSA descrypt:unexpected data length:" + data.length);
int blocks = data.length / blockSize; // 獲得分塊數據
int inputOffset = 0;
int outputOffset = 0;
byte[] buffer = new byte[blocks * blockSize];
for (int i = 0; i < blocks; i++) {
int hasDone = cipher.doFinal(data, inputOffset, blockSize,
buffer, outputOffset);
outputOffset += hasDone;
inputOffset += blockSize;
}
return ArrayUtils.subarray(buffer, 0, outputOffset);
} catch (Exception e) {
throw e;
}
}
/**
* 數據簽名驗籤
* @param publicKey 公鑰
* @param algorithm 數字簽名算法 "MD5withRSA","SHA1withRSA" 等
* @param data 數據
* @param signData 驗籤數據
*/
public static boolean verify(PublicKey publicKey, String algorithm, byte[] data, byte[] signData) throws NoSuchAlgorithmException,
InvalidKeyException, SignatureException {
Signature signature = Signature.getInstance(algorithm);
signature.initVerify(publicKey);
signature.update(data);
return signature.verify(signData);
}
/**
* 進行數字簽名
* @param privateKey 私鑰
* @param data 簽名數據
* @param algorithm 數字簽名算法 "MD5withRSA","SHA1withRSA" 等
*/
public static byte[] sign(PrivateKey privateKey, byte[] data,String algorithm) throws NoSuchAlgorithmException,
InvalidKeyException, SignatureException {
Signature signature = Signature.getInstance(algorithm);
signature.initSign(privateKey);
signature.update(data);
return signature.sign();
}
/**
* 獲取公鑰Base64編碼的字符串
*/
public static String getPublicKeyPlain(PublicKey publicKey) {
byte[] pbk = publicKey.getEncoded();
return Base64.encodeBase64String(pbk);
}
/**
* 獲取私鑰Base64編碼的字符串
*/
public static String getPrivateKeyPlain(PrivateKey privateKey) {
byte[] prk = privateKey.getEncoded();
return Base64.encodeBase64String(prk);
}
/**
* Base64編碼的字符串 轉 公鑰對象
*/
public static PublicKey loadPublicKey(String publicKeyStr) {
byte[] buffer = Base64.decodeBase64(publicKeyStr);
PublicKey publicKey = null;
try {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(buffer);
publicKey = keyFactory.generatePublic(keySpec);
} catch (NoSuchAlgorithmException e) {
System.err.println("無此算法");
e.printStackTrace();
} catch (InvalidKeySpecException e) {
System.err.println("非法公鑰");
e.printStackTrace();
}
return publicKey;
}
/**
* Base64編碼的字符串 轉 私鑰對象
*/
public static PrivateKey loadPrivateKey(String privateKeyStr) {
PrivateKey privateKey = null;
try {
byte[] buffer = Base64.decodeBase64(privateKeyStr);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(buffer);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
privateKey = keyFactory.generatePrivate(keySpec);
} catch (NoSuchAlgorithmException e) {
System.err.println("無此算法");
e.printStackTrace();
} catch (InvalidKeySpecException e) {
System.err.println("非法私鑰");
e.printStackTrace();
}
return privateKey;
}
}
3、RSA 加密與解密數據
RSA 非對稱加密在使用中通常公鑰公開,私鑰保密,使用公鑰加密,私鑰解密。例如 客戶端 給 服務端 加密發送數據:
- 客戶端從服務端獲取公鑰;
- 客戶端用公鑰先加密要發送的數據,加密後發送給服務端;
- 服務端拿到加密後的數據,用私鑰解密得到原文。
公鑰加密後的數據,只有用私鑰才能解,只有服務端纔有對應的私鑰,因此只有服務端能解密,中途就算數據被截獲,沒有私鑰依然不知道數據的原文內容,因此達到數據安全傳輸的目的
Map<String,String> keyMap = RSAUtils.generateRSAKeyPlain();
keyMap.get(RSAUtils.MAP_KEY_PRIVATE_KEY)
keyMap.get(RSAUtils.MAP_KEY_PUBLIC_KEY)
String data = "你好, World!";
// 客戶端: 加密
byte[] cipherData = RSAUtils.encrypt(keyMap.get(RSAUtils.MAP_KEY_PUBLIC_KEY), data.getBytes());
// 服務端: 解密
byte[] plainData = RSAUtils.decrypt(keyMap.get(RSAUtils.MAP_KEY_PUBLIC_KEY), cipherData);
4、簽名與驗籤
RSA 非對稱加密算法,除了用來加密/解密數據外,還可以用於對數據(文件)的 簽名 和 驗籤,可用於確認數據或文件的完整性與簽名者(所有者)
RSA 密鑰對在使用時通常:
- 加密/解密:通常使用 公鑰加密,私鑰解密。
- 簽名/驗籤:通常使用 私鑰簽名,公鑰驗籤。
Android 安裝包 APK 文件的簽名,是 RSA 簽名驗籤的典型應用:Android 打包後,用私鑰對 APK 文件進行簽名,並把公鑰和簽名結果放到 APK 包中。下次客戶端升級 APK 包時,根據新的 APK 包和包內的簽名信息,用 APK 包內的公鑰驗籤校驗是否和本地已安裝的 APK 包使用的是同一個私鑰簽名,如果是,則允許安裝升級。
Map<String,String> keyMap = RSAUtils.generateRSAKeyPlain();
keyMap.get(RSAUtils.MAP_KEY_PRIVATE_KEY)
keyMap.get(RSAUtils.MAP_KEY_PUBLIC_KEY)
File file = new File("demo.jpg");
// 私鑰簽名(文件): 對文件進行簽名, 返回簽名結果
byte[] fileSignInfo = RSAUtils.sign(file.getBytes(), keyMap.get(RSAUtils.MAP_KEY_PRIVATE_KEY),"SHA1withRSA");
System.out.println("文件簽名信息:" + new BASE64Encoder().encode(fileSignInfo));
// 公鑰驗籤(文件): 用公鑰校驗文件的簽名是否來自公鑰對應的私鑰
boolean fileVerify = RSAUtils.verify(keyMap.get(RSAUtils.MAP_KEY_PUBLIC_KEY),"SHA1withRSA",file.getBytes(), signInfo);
System.out.println("文件驗簽結果:" + fileVerify);