背景
Web应用中对前端的密码进行加密,然后后台再解密,前端加密工具有jsencrypt.min.js可以使用,该工具在前端用公钥加密,后端私钥解密,以提高应用系统敏感数据的安全度。本文将整理RAS非对称加密算法的使用过程及注意事项,作为网络笔记,以备后用。
RSA和Base64
RSA加密算法是对输入的字节数组数据进行加密,输出另一个加密后的字节数组。这个字节数值数据该怎么存储呢?答案是使用Base64编码。
Base64是网络上常见的传送8比特字节码的编码方式之一,使用它可以将RSA算法的加密结果进行编码,转换为字符串存储到文件或其他地方,便于使用。
所以RSA加密算法通常是将加密结果转换为Base64编码后的字符串,在解密时先将Base64的字符串解码还原为字节数组,再调用RSA的解密算法进行解密。
字符串和密钥转换
RSA算法依赖两个密钥对象,即PrivateKey和PublicKey,一种简单的处理方法是只生成一次密钥对,并转换为字符串存储起来,提供给应用。
密钥都有一个getEncoded()方法,其返回该密钥的字节数组,同理利用Base64编码后存储为字符串。几种涉及到的转换过程如下:
第一种,密钥转换为字符串,公钥、私钥方法一样,典型代码:
public static String getKey(Key key) throws Exception {
byte[] keyBytes = key.getEncoded();
return (new BASE64Encoder()).encode(keyBytes);
}
第二种,Base64字符串转换为私钥:
String KEY_PRIVATE = "";//Base64转码生成的字符串。
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(new BASE64Decoder().decodeBuffer(KEY_PRIVATE));
PrivateKey key = KeyFactory.getInstance("RSA").generatePrivate(keySpec);
先对Base64字符串进行解码,得到原始密钥的字节数组,然后再调用算法生成PKCS8EncodedKeySpec类型的私钥,该类继承EncodedKeySpec类,以编码格式来表示私钥。
第三,Base64字符串转换为公钥:
String KEY_PUBLIC = "";//Base64转码生成的字符串。
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(new BASE64Decoder().decodeBuffer(KEY_PUBLIC));
PublicKey key = KeyFactory.getInstance("RSA").generatePublic(keySpec);
同理,先得到密钥的字节数组,再创建X509EncodedKeySpec实例,该类继承自EncodedKeySpec类,以编码格式来表示公钥。
jsencrypt.min.js
一个前端加密工具,在web应用中,如果需要对表单数据进行加密,可以引入该js。它使用公钥进行加密,常见的调用方法如下:
/**
* 密码加密
* @param password 需要加密的明文密码
* @returns 加密之后转换为Base64的字符串
*/
function encryptPassword(password){
var encrypt = new JSEncrypt();
var KEY_PUBLIC = "";//公钥Base64编码后的字符串
encrypt.setPublicKey(KEY_PUBLIC);
var encrypted = encrypt.encrypt(password);
return encrypted;
}
这里使用KEY_PUBLIC就是和后台私钥对应的一对密钥,上述代码使用公钥对表单数据进行RSA加密,并将加密结果存储为一个Base64的字符串,传递给后台处理。
后台解密
私钥保存在服务器端,对前端的数据进行解密,对应的解密代码如下:
public String decrypt(String cipherText){
//页面js加密RSA公钥对应的私钥
String KEY_PRIVATE = "";
try {
//Base64转码为字节数组
byte[] encryptByte = new BASE64Decoder().decodeBuffer(cipherText);
Cipher cipher = Cipher.getInstance("RSA");
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(new BASE64Decoder().decodeBuffer(KEY_PRIVATE));
PrivateKey key;
key = KeyFactory.getInstance("RSA").generatePrivate(keySpec);
/ /用私钥解密,Decrypt模式
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] deBytes = cipher.doFinal(encryptByte);
String plainPwd = new String(deBytes, "UTF-8");
return plainPwd;
} catch (Exception e) {
logger.error("解密密码异常",e);
return "";
}
}
Cipher 对象的初始化方法init需要两个参数,操作模式和密钥信息,必须和目标操作一致,才能正确的加密或者解密。即:此处是解密操作,那么必须传递DECRYPT_MODE这个常量,否则就可能是二次加密了。
注意事项
RSA证书对加密的数据大小有限制,加密时支持的最大字节数=证书位数/8 -11;解密时最初字节数=证书位数/8。
1024位证书,加密时最大支持117个字节,解密时为128,PKCS#1建议的padding就占用11个字节,所以算上padding,最大支持117个字节。
Java的RSA算法默认的就是1024位的证书。
所以在处理超长的明文或密文时,需要按照对应的块最大长度限制,分段处理。
例如:一个密文二进制数组长度超过128位,那么解密时就需要分段处理:
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] data = new byte[cipher.getOutputSize(fis.available())];
int len = 0;
while((len = fis.read(data)) > 0){
bos.write(cipher.doFinal(data, 0, len));
}
cipher.getOutputSize会返回解密操作的最大长度128,然后对输入文件流,逐段进行解密,每次只处理128字节的数据;上述代码如果直接调用doFinal,那么当数据超过128字节时,会出现data must not no longer than 128 byte的异常,导致解密失败。
同理,加密操作也要根据明文长度,逐段处理:
int blockSize = cipher.getBlockSize();
int blocksNum = encryptByte.length % blockSize == 0 ?
encryptByte.length / blockSize : encryptByte.length / blockSize + 1;
for (int i = 0; i < blocksNum; i++) {
if (i < blocksNum - 1) {
out.write(cipher.doFinal(encryptByte, i * blockSize, blockSize));
} else {
out.write(cipher.doFinal(encryptByte, i * blockSize, encryptByte.length - i * blockSize));
}
}
上述代码在处理encryptByte这个明文字节数组时,根据它的分块个数,保证每次处理的长度为117。