一. RSA加密算法的介绍
RSA加密算法是一种非对称加密算法。在公开密钥加密和电子商业中RSA被广泛使用。RSA是1977年由罗纳德·李维斯特(Ron Rivest)、阿迪·萨莫尔(Adi Shamir)和伦纳德·阿德曼(Leonard Adleman)一起提出的。当时他们三人都在麻省理工学院工作。RSA就是他们三人姓氏开头字母拼在一起组成的。
1973年,在英国政府通讯总部工作的数学家克利福德·柯克斯(Clifford Cocks)在一个内部文件中提出了一个与之等效的算法,但该算法被列入机密,直到1997年才得到公开。
对极大整数做因数分解的难度决定了RSA算法的可靠性。换言之,对一极大整数做因数分解愈困难,RSA算法愈可靠。假如有人找到一种快速因数分解的算法的话,那么用RSA加密的信息的可靠性就肯定会极度下降。但找到这样的算法的可能性是非常小的。今天只有短的RSA钥匙才可能被强力方式解破。到当前为止,世界上还没有任何可靠的攻击RSA算法的方式。只要其钥匙的长度足够长,用RSA加密的信息实际上是不能被解破的。
1983年9月12日麻省理工学院在美国为RSA算法申请了专利。这个专利2000年9月21日失效。[4]由于该算法在申请专利前就已经被发表了,在世界上大多数其它地区这个专利权不被承认。
二. RSA加密算法的在项目中使用背景
在项目的登录模块中, 需要输入用户名和密码,即使是post请求,传递的用户名和密码通过浏览器的开发者工具, 一样可以明文显示, 如下图所示. 这样明文传递是极其不安全的.
因此决定采用RSA加密算法, 对传递的数据进行加密.
具体的思路如下
1、后端传递公钥给前端
2、前端把密码用公钥进行加密传递到后端
3、后台接收前端传递加密的密码,用私钥进行解密
三. RSA公钥和私钥的生成
3.1 生成私钥
在Linux系统中, 可以直接通过命令,进行公钥和私钥的生成
输入如下的命令,进行私钥的生成
openssl genrsa -out rsa_private.pem 1024
如下图所示, 执行完上面的命令后,生成了rsa_private.pem文件, 用cat命令查看该文件,即可查看到私钥的内容
如下图所示为私钥的内容
私钥整体的内容如下
MIICXQIBAAKBgQC3y5OIKGTa7+FpNklLVn3XB55qc8MQjh1R/MlJ3Uab29Tuufk8
Y3TfS4RVC9E8wxyC2l3XEyE6Xo2Kv5xAOdPlFXWloUJjA80QKJp8imhnNtxYNSql
lWfQJEZhNIejE5INhsuEmwALJZp3oG/rxqpWrVsZzPzOuMhLvKWBnD2kxQIDAQAB
AoGBALeF76rlqs9y+AG20zkHBGDSmrxxGzZMsbpMUDIRYY//0n8N9HD4XpsZLOo/
Ao9JxA5lPY6k62j9QRhqe/slww4pvBIbfDS/IbmLrf09sr+PJYbtgQOXURDMw1fJ
PkKpmq2B5T4Cr8MGDgA+/F8PCldEFdI7LgjB5pTckTH7qAOBAkEA7DEHahlaWtty
kynsvHWo1gQy54lnAZSZ3/iR1v4xCbaRriV5yt5x6RYC8tx84jDteTligaKJNej7
2HxHKSBWywJBAMc1nL8NriusKt0D3h+hYpyJcbdA6VuJcn0O8z5nuXzJPeTX3Zc+
flyfeIIGe/35kvVdFz5VH6S8rCOa9ozM8K8CQQCcjiMMAKITd0IKqc9xW8v9j+rt
7fWI5qbX/jss9nAqsAkAFXcVMTzv8tchg2SDyUqe/5p7svCf+z5z0GyapgThAkAy
/XcUtCS7ywLhtaa6g+2g0dGIrZXNt13VwujiNVyWI1Czbcmrd/SSMsN+zLgaJzLF
mUFk1BcYbK7HIlgvbBnDAkB6GlmYZgrbRMJG+eXa4hYNY8zoAzhmxL40xbhS0ikM
8Rzp6Oq9DaVaiAEIiFlmnopR+8p4KJAQYqrzTfTYrOzO
3.2 生成公钥
生成公钥文件 需要在私钥的基础上生成.
执行如下的命令
openssl rsa -in rsa_private.pem -pubout -out rsa_public_key.pem
如下图所示, 执行完命令后, 生成了rsa_public_key.pem文件
通过cat命令, 查看公钥的内容如下.
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC3y5OIKGTa7+FpNklLVn3XB55q
c8MQjh1R/MlJ3Uab29Tuufk8Y3TfS4RVC9E8wxyC2l3XEyE6Xo2Kv5xAOdPlFXWl
oUJjA80QKJp8imhnNtxYNSqllWfQJEZhNIejE5INhsuEmwALJZp3oG/rxqpWrVsZ
zPzOuMhLvKWBnD2kxQIDAQAB
四. 前端用公钥进行加密
在前端加入jsencrypt.js,该js是用于执行OpenSSL RSA加密,解密和密钥生成的Javascript库.
该js库的GitHub地址如下https://github.com/travist/jsencrypt
在前端页面中,用input的隐藏域, 存储公钥
<input id=“pubkey” type=“hidden” value="${publicKey}" /
在html的登录表单中, 把用户输入的密码的值, 进行加密, 然后,传递到隐藏域的input中, 此隐藏的input, 有name值设置为了password, 即最终传递的参数为隐藏域的input框中加密后的密码的值.
<!-- 用户输入的密码 -->
<input type="password" id="password" class="form-control" placeholder="密码" required="">
<!-- 加密后的密码,用于登录 -->
<input type="hidden" name="password" id="pass" class="form-control">
进行密码加密的js如下
<script>
// 密码加密
function verify() {
var encrypt = new JSEncrypt();
encrypt.setPublicKey('-----BEGIN PUBLIC KEY-----' + $('#pubkey').val() + '-----END PUBLIC KEY-----');
var encrypted = encrypt.encrypt($('#password').val());
$('#pass').val(encrypted);
}
</script>
五. 后端用私钥进行解密
后端使用的是SpringBoot项目,JDK采用1.8
在后端java中, 首先要引入了如下的依赖,用于RSA的加密,解密
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-ext-jdk15on</artifactId>
<version>1.59</version>
</dependency>
在application.yml中配置私钥和公钥
在代码中, 采用如下的RSAUtil 工具类,进行加密解密的操作.
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.encoders.Base64;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;
import org.bouncycastle.util.io.pem.PemWriter;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
public class RSAUtil {
// 签名算法
public static final String SHA1_WITH_RSA = "SHA1withRSA";
public static final String MD5_WITH_RSA = "MD5withRSA";
private static final Provider pro = new BouncyCastleProvider();
// 统一以16进制解析modulus和exponent字符串
private static final int BIGINTEGER_RADIX = 16;
// RSA算法
private static final String ALGORITHM = "RSA";
// Padding
private static final String NONE_PADDING = "RSA/None/PKCS1Padding";
// 随机数算法
private static final String RANDOM_ALGORITHM = "SHA1PRNG";
// 编码默认格式
private static final String UTF8 = "UTF-8";
// 种子,改变后,生成的密钥对会发生变化
private static final String SEEDKEY = "seedKey";
// 密钥默认长度
private static final int KEYSIZE = 1024;
// 公钥PEM头部
private static final String PUBLIC_KEY_PEM_HEADER_PKCS1 = "RSA PUBLIC KEY";
private static final String PUBLIC_KEY_PEM_HEADER_PKCS8 = "PUBLIC KEY";
// 私钥PEM头部
private static final String PRIVATE_KEY_PEM_HEADER_PKCS1 = "RSA PRIVATE KEY";
private static final String PRIVATE_KEY_PEM_HEADER_PKCS8 = "PRIVATE KEY";
/**
* 生成公私钥对
*
* @return
* @throws Exception
*/
public static KeyPair generateKeyPair() throws NoSuchAlgorithmException {
return generateKeyPair(KEYSIZE);
}
/**
* 生成公私钥对
*
* @param keySize 密钥长度
* @return
* @throws Exception
*/
public static KeyPair generateKeyPair(int keySize) throws NoSuchAlgorithmException {
KeyPairGenerator kpg = KeyPairGenerator.getInstance(ALGORITHM, pro);
kpg.initialize(keySize);
KeyPair kp = kpg.generateKeyPair();
return kp;
}
/**
* 生成公私钥对,使用特定种子生成
*
* @param seedKey 种子,当种子相同时,生成的密钥相同
* @return
* @throws Exception
*/
public static KeyPair generateKeyPair(String seedKey) throws NoSuchAlgorithmException {
return generateKeyPair(seedKey, KEYSIZE);
}
/**
* 生成公私钥对,使用特定种子生成
*
* @param seedKey 种子,当种子相同时,生成的密钥相同
* @param keySize 密钥长度
* @return
* @throws Exception
*/
public static KeyPair generateKeyPair(String seedKey, int keySize) throws NoSuchAlgorithmException {
KeyPairGenerator kpg = KeyPairGenerator.getInstance(ALGORITHM, pro);
// windows和linux下SecureRandom的行为不一致
// 如果使用new SecureRandom(seedKey.getBytes()),在windows会生成相同密钥,在linux会生成不同密钥
// 因此使用如下方法,确保在相同seedKey下,windows和linux都能生成相同密钥
byte seedKeyBytes[] = seedKey.getBytes();
SecureRandom secureRandom = SecureRandom.getInstance(RANDOM_ALGORITHM);
secureRandom.setSeed(seedKeyBytes);
// SecureRandom secureRandom = new SecureRandom(seedKey.getBytes());
kpg.initialize(keySize, secureRandom);
KeyPair kp = kpg.generateKeyPair();
return kp;
}
/**
* bytes转公钥
*
* @param bytes
* @return
* @throws InvalidKeySpecException
*/
private static PublicKey bytesToPublicKey(byte[] bytes) throws InvalidKeySpecException {
try {
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(bytes);
KeyFactory kf = KeyFactory.getInstance(ALGORITHM, pro);
return kf.generatePublic(keySpec);
} catch (NoSuchAlgorithmException e) {
// Can't happen
}
return null;
}
/**
* bytes转私钥
*
* @param bytes
* @return
* @throws InvalidKeySpecException
*/
private static PrivateKey bytesToPrivateKey(byte[] bytes) throws InvalidKeySpecException {
try {
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bytes);
KeyFactory kf = KeyFactory.getInstance(ALGORITHM, pro);
return kf.generatePrivate(keySpec);
} catch (NoSuchAlgorithmException e) {
// Can't happen
}
return null;
}
/**
* String转公钥
*
* @param key
* @return
* @throws InvalidKeySpecException
*/
public static PublicKey getPublicRSAKey(String key) throws InvalidKeySpecException {
return bytesToPublicKey(Base64.decode(key));
}
/**
* String转私钥
*
* @param key
* @return
* @throws InvalidKeySpecException
*/
public static PrivateKey getPrivateRSAKey(String key) throws InvalidKeySpecException {
return bytesToPrivateKey(Base64.decode(key));
}
/**
* 公私钥转String格式
*
* @param key
* @return
* @throws Exception
*/
public static String toBase64(Key key) {
byte[] bytes = key.getEncoded();
return Base64.toBase64String(bytes);
}
/**
* String转bytes
*
* @param text
* @return
* @throws Exception
*/
public static byte[] toBytes(String text) throws UnsupportedEncodingException {
return toBytes(text, UTF8);
}
/**
* String转bytes
*
* @param text
* @param charSet 编码
* @return
* @throws Exception
*/
public static byte[] toBytes(String text, String charSet) throws UnsupportedEncodingException {
return text.getBytes(charSet);
}
/**
* 使用密钥加密bytes,返回bytes
*
* @param text
* @param key 密钥(公钥/私钥)
* @return
* @throws Exception
*/
private static byte[] encrypt(byte[] text, Key key) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
Cipher cipher = Cipher.getInstance(NONE_PADDING, pro);
cipher.init(Cipher.ENCRYPT_MODE, key);
return cipher.doFinal(text);
}
/**
* 使用密钥加密String,返回bytes
*
* @param text
* @param key 密钥(公钥/私钥)
* @return
* @throws Exception
*/
private static byte[] encryptToBytes(String text, Key key) throws UnsupportedEncodingException, IllegalBlockSizeException, InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException {
return encrypt(toBytes(text), key);
}
/**
* 使用密钥加密String,返回String
*
* @param text
* @param key 密钥(公钥/私钥)
* @return
* @throws Exception
*/
public static String encryptToString(String text, Key key) throws BadPaddingException, UnsupportedEncodingException, IllegalBlockSizeException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException {
byte[] en = encryptToBytes(text, key);
return Base64.toBase64String(en);
}
/**
* 使用密钥解密bytes,返回bytes
*
* @param text
* @param key 密钥(公钥/私钥)
* @return
* @throws Exception
*/
private static byte[] decrypt(byte[] text, Key key) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
Cipher cipher = Cipher.getInstance(NONE_PADDING, pro);
cipher.init(Cipher.DECRYPT_MODE, key);
return cipher.doFinal(text);
}
/**
* 使用密钥解密bytes,返回String
*
* @param text
* @param key 密钥(公钥/私钥)
* @return
* @throws Exception
*/
private static String decryptToString(byte[] text, Key key) throws IllegalBlockSizeException, InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException {
return new String(decrypt(text, key));
}
/**
* 使用密钥解密String,返回String
*
* @param text
* @param key 私钥
* @return
* @throws Exception
*/
public static String decryptToString(String text, Key key) throws InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, IllegalBlockSizeException, NoSuchPaddingException {
byte[] en = Base64.decode(text);
return decryptToString(en, key);
}
/**
* 将Pem从String格式转为bytes
*
* @param keyPem
* @return
* @throws InvalidKeySpecException
*/
private static byte[] getBytesFromPem(String keyPem) throws InvalidKeySpecException {
try {
StringReader stringReader = new StringReader(keyPem);
PemReader pemReader = new PemReader(stringReader);
PemObject pemObject = pemReader.readPemObject();
pemReader.close();
return pemObject.getContent();
} catch (IOException e) {
throw new InvalidKeySpecException("Could not get key pem");
}
}
/**
* 从Pem中解析公钥
*
* @param keyPem
* @return
* @throws InvalidKeySpecException
*/
public static PublicKey getPublicRSAKeyFromPem(String keyPem) throws InvalidKeySpecException {
return bytesToPublicKey(getBytesFromPem(keyPem));
}
/**
* 从Pem中解析私钥
*
* @param keyPem
* @return
* @throws InvalidKeySpecException
*/
public static PrivateKey getPrivateRSAKeyFromPem(String keyPem) throws InvalidKeySpecException {
return bytesToPrivateKey(getBytesFromPem(keyPem));
}
/**
* 获取Pem格式的RSAKey
*
* @param type
* @param keyBytes
* @return
* @throws InvalidKeySpecException
*/
private static String generateKeyPem(String type, byte[] keyBytes) throws InvalidKeySpecException {
try {
StringWriter stringWriter = new StringWriter();
PemWriter pemWriter = new PemWriter(stringWriter);
pemWriter.writeObject(new PemObject(type, keyBytes));
pemWriter.close();
String keyPem = stringWriter.toString();
stringWriter.close();
return keyPem;
} catch (IOException e) {
throw new InvalidKeySpecException("Could not generate key pem");
}
}
/**
* 获取Pem格式的RSAKey
*
* @param type
* @param key
* @return
* @throws InvalidKeySpecException
*/
private static String generateKeyPem(String type, Key key) throws InvalidKeySpecException {
return generateKeyPem(type, key.getEncoded());
}
/**
* 获取Pem格式的公钥
*
* @param publicKey
* @return
* @throws InvalidKeySpecException
*/
public static String generateKeyPemPKCS8(PublicKey publicKey) throws InvalidKeySpecException {
return generateKeyPem(PUBLIC_KEY_PEM_HEADER_PKCS8, publicKey);
}
/**
* 获取Pem格式的私钥
*
* @param privateKey
* @return
* @throws InvalidKeySpecException
*/
public static String generateKeyPemPKCS8(PrivateKey privateKey) throws InvalidKeySpecException {
return generateKeyPem(PRIVATE_KEY_PEM_HEADER_PKCS8, privateKey);
}
/**
* PKCS#8转换为PKCS#1
*
* @param privateKey
* @throws IOException
*/
public static String generateKeyPemPKCS1(PrivateKey privateKey) throws IOException, InvalidKeySpecException {
byte[] privBytes = privateKey.getEncoded();
PrivateKeyInfo pkInfo = PrivateKeyInfo.getInstance(privBytes);
ASN1Encodable encodable = pkInfo.parsePrivateKey();
ASN1Primitive primitive = encodable.toASN1Primitive();
byte[] privateKeyPKCS1 = primitive.getEncoded();
return generateKeyPem(PRIVATE_KEY_PEM_HEADER_PKCS1, privateKeyPKCS1);
}
/**
* X509转换为PKCS#1
*
* @param publicKey
* @throws IOException
*/
public static String generateKeyPemPKCS1(PublicKey publicKey) throws IOException, InvalidKeySpecException {
byte[] pubBytes = publicKey.getEncoded();
SubjectPublicKeyInfo spkInfo = SubjectPublicKeyInfo.getInstance(pubBytes);
ASN1Primitive primitive = spkInfo.parsePublicKey();
byte[] publicKeyPKCS1 = primitive.getEncoded();
return generateKeyPem(PUBLIC_KEY_PEM_HEADER_PKCS1, publicKeyPKCS1);
}
/**
* 签名,使用SHA1摘要
*
* @param data
* @param privateKey
* @return
* @throws SignatureException
* @throws NoSuchAlgorithmException
* @throws InvalidKeyException
* @throws UnsupportedEncodingException
*/
public static String sign(String data, PrivateKey privateKey) throws SignatureException, NoSuchAlgorithmException, InvalidKeyException, UnsupportedEncodingException {
return sign(data, privateKey, SHA1_WITH_RSA);
}
/**
* 验签,使用SHA1摘要
*
* @param data
* @param sign
* @param publicKey
* @return
* @throws NoSuchAlgorithmException
* @throws InvalidKeyException
* @throws SignatureException
* @throws UnsupportedEncodingException
*/
public static boolean verify(String data, String sign, PublicKey publicKey) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, UnsupportedEncodingException {
return verify(data, sign, publicKey, SHA1_WITH_RSA);
}
/**
* 签名
*
* @param data
* @param privateKey
* @param algorithm 指定摘要算法
* @return
* @throws SignatureException
* @throws NoSuchAlgorithmException
* @throws InvalidKeyException
* @throws UnsupportedEncodingException
*/
public static String sign(String data, PrivateKey privateKey, String algorithm) throws SignatureException, NoSuchAlgorithmException, InvalidKeyException, UnsupportedEncodingException {
// 设置签名算法
Signature signature = Signature.getInstance(algorithm);
// 设置签名加密方式
signature.initSign(privateKey);//设置私钥
// 签名和加密一样 要以字节形式 utf-8字符集得到字节
signature.update(toBytes(data));
// 得到base64编码的签名后的字段
return Base64.toBase64String(signature.sign());
}
/**
* 验签
*
* @param data
* @param sign
* @param publicKey
* @param algorithm 指定摘要算法
* @return
* @throws NoSuchAlgorithmException
* @throws InvalidKeyException
* @throws SignatureException
* @throws UnsupportedEncodingException
*/
public static boolean verify(String data, String sign, PublicKey publicKey, String algorithm) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, UnsupportedEncodingException {
// 指定签名类型
Signature signature = Signature.getInstance(algorithm);
// 放入公钥
signature.initVerify(publicKey);
// 放入数据
signature.update(toBytes(data));
// 验签结果
return signature.verify(Base64.decode(sign));
}
}
在业务代码在中, 可以使用@Value注解,把配置文件中的公钥和私钥进行注入到代码中
/**
* rsa 加密的公钥
*/
@Value("${rsa.public.key}")
private String RSA_PUBLIC_KEY;
/**
* rsa 加密的私钥
*/
@Value("${rsa.private.key}")
private String RSA_PRIVATE_KEY;
编写一个RsaConfig 配置类,进行 PrivateKey 对象的生成.
@Configuration
public class RsaConfig {
/**
* rsa 加密的私钥
*/
@Value("${rsa.private.key}")
private String RSA_PRIVATE_KEY;
/**
* 生成 PrivateKey 对象
* @return
* @throws InvalidKeySpecException
*/
@Bean
public PrivateKey getPrivateRSAKey() throws InvalidKeySpecException {
PrivateKey privateRSAKey = RSAUtil.getPrivateRSAKey(RSA_PRIVATE_KEY);
return privateRSAKey;
}
}
如下的代码所示,调用RSAUtil.decryptToString方法,即可用对加密的密码进行解密
@Autowired
private PrivateKey privateRSAKey;
@RequestMapping(value = "/login", method = RequestMethod.POST)
public String loginVali(HttpServletRequest request) {
//获取加密的密码
String password = super.getPara("password").trim();
//解密密码
try {
password = RSAUtil.decryptToString(password, privateRSAKey);
System.out.println("解密后的密码: " +password);
} catch (Exception e) {
e.printStackTrace();
}
}
六. 效果演示
发送登录的请求, 可以看到密码已经加密
加密的内容如下
bFKaTE2K/mxc5rbz2dCOLSYw5drQDWvW5YRv9+cM6z6wSuvUTY2haipIFYd8EQZwpke7RnO8UeNxlce7bADrDGzIqJtKhSm6KGO69flCPy6bXJ2h3rJD5yCplKDCCy0Jdq+WZNZLlpchF2t2SJUKogWrvMeJCqesYil9za2CKXc=
在后台控制台中打印如下, 说明后台成功进行了解密!