非对称加解密
概述
非对称加解密与对称加解密不同,非对称加解密密钥成对,分为公钥和私钥。根据不同使用场景使用其中一个密钥加密,另外一个密钥进行解密。对数据进行加解密一般使用公钥加密,私钥解密,若是数字签名,那么一般通过私钥进行签名,用公钥进行验签。
算法
本文介绍两种非对称加解密算法:SM2、RSA
RSA 算法
简介
RSA 加密算法的安全性基于大数不能分解质因数为基础,涉及到一些数学原理:互质关系、欧拉函数、欧拉定理、模反元素。密钥分为:公钥和私钥,公钥可以供任何人使用,私钥为自己使用,采用 768位、1024位 和 2048位密钥。RSA 加密速度比较慢适合较小的数据量进行加解密。
特别提醒:相同数据使用加密后的密文不同,但是都可以用私钥解密
小试牛刀
- 基于 JDK 实现完整 demo,示例如下:
public static void main(String[] args) {
String content = "RSA demo";
try {
// 获得密钥对
KeyPair keyPair = getKeyPair();
// 获得进行Base64 加密后的公钥和私钥 String
String privateKeyStr = getPrivateKey(keyPair);
String publicKeyStr = getPublicKey(keyPair);
System.out.println("Base64处理后的私钥:" + privateKeyStr);
System.out.println("Base64处理后的公钥:" + publicKeyStr);
// 获得原始的公钥和私钥
PrivateKey privateKey = string2Privatekey(privateKeyStr);
PublicKey publicKey = string2PublicKey(publicKeyStr);
// 公钥加密/私钥解密
byte[] publicEncryBytes = publicEncrytype(content.getBytes(), publicKey);
System.out.println("公钥加密后的字符串Base64:" + Base64.getEncoder().encodeToString(publicEncryBytes));
byte[] privateDecryBytes = privateDecrypt(publicEncryBytes, privateKey);
System.out.println("私钥解密后的原始字符串:" + new String(privateDecryBytes));
} catch (Exception e) {
log.error("RSA加解密异常", e);
}
}
private static KeyPair getKeyPair() throws NoSuchAlgorithmException, UnsupportedEncodingException {
// 获得RSA密钥对的生成器实例
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
// 安全的随机数
SecureRandom secureRandom = new SecureRandom(String.valueOf(System.currentTimeMillis()).getBytes("utf-8"));
// 这里可以是1024、2048 初始化一个密钥对
keyPairGenerator.initialize(1024, secureRandom);
// 获得密钥对
return keyPairGenerator.generateKeyPair();
}
private static String getPublicKey(KeyPair keyPair) {
PublicKey publicKey = keyPair.getPublic();
byte[] bytes = publicKey.getEncoded();
return Base64.getEncoder().encodeToString(bytes);
}
private static String getPrivateKey(KeyPair keyPair) {
PrivateKey privateKey = keyPair.getPrivate();
byte[] bytes = privateKey.getEncoded();
return Base64.getEncoder().encodeToString(bytes);
}
private static PublicKey string2PublicKey(String pubStr) throws NoSuchAlgorithmException, InvalidKeySpecException {
byte[] bytes = Base64.getDecoder().decode(pubStr);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(bytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePublic(keySpec);
}
private static PrivateKey string2Privatekey(String priStr) throws NoSuchAlgorithmException, InvalidKeySpecException {
byte[] bytes = Base64.getDecoder().decode(priStr);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePrivate(keySpec);
}
private static byte[] publicEncrytype(byte[] content, PublicKey publicKey) throws Exception {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return cipher.doFinal(content);
}
private static byte[] privateDecrypt(byte[] content, PrivateKey privateKey) throws Exception{
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return cipher.doFinal(content);
}
- Hutool 非对称加密章节中也介绍了 RSA 算法使用,通过创建一个 RSA 对象,通过此对象创建密钥对,获取公钥和私钥,然后通过此对象实现加解密,但是实际开发中加密和解密是分开的,所以在 Hutool 文档中有提示。
特别提醒:对于加密和解密可以完全分开,对于 RSA 对象,如果只使用公钥或私钥,另一个参数可以为null
如果加密和解密分开如何使用呢?以下示例是私钥加密,公钥解密,示例如下:
// 私钥 Base64 字符串
private static final String PRIVATE_KEY = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKYj8EMx+TGu+rA9Pv6+jhIKcZtSA7AqenBbfBEnvJc/gvamPfELgDdk7KKiSN+Q1fGquW945UJuGRioy/0qatB7CtFTvVVVcibaTnXHqBoNScJesldOQy0JYlQztEStMVHkG7KdfW178j0iTLKJnvvQM8bAuiQdx64t4QlNbT0BAgMBAAECgYBKQC6NQWTO7RZRtJWWUUB6VJuQYHeQgHOHCoPowNsat4JGzGZLd6neV+cgCipKbFcJchT8+klvxnfF2w6LvyzL9pIGmCQFwv3U3mcOrTJ/+nYql5NrBiufvuAyWCh5bEKJZYQ/6Uzd7N+HTjdpx0m3XxQQtQPrXLCNLbsnlFW5BwJBAM8tO/kjcUhD7RDoH/KymnffdNs8flkmrZbamnwZgZROSizJ1EMV7nu6FuJZkXHuEQ8P0H2o847F6oigtzoeZdcCQQDNSwYo2aKK5XazTye8zJVVlVS7HUSIYDtWnTLTFNQyFMmZG4jwE2CNQzMzagdH1BmObfs5zSE5Bk03RMr+YGjnAkEAxS8gbbe2EjnUYMsN3UjwjDc6WY/yEZgmj/XwIz2Df0wkfQx74n31Rf2P2k+1huI3ikZbAb7UUYc9+lw9CCv2cQJABkvYwoP6QjxLabBxzY6QvfE4igyZv30EFOH5XxPydh7BGBsKFiLiATMgbOFBm+hbaEzjOaCa9j7FO362oxqd3QJALqL5OcHkgj6XVqJPtZSM5cd57Av+51kDPQlUCuPLk1Kda5TxMkMpNtTbCYcunpbZxzBboNFht+EjAZSQddeotA==";
// 公钥 Base64 字符串
private static final String PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCmI/BDMfkxrvqwPT7+vo4SCnGbUgOwKnpwW3wRJ7yXP4L2pj3xC4A3ZOyiokjfkNXxqrlveOVCbhkYqMv9KmrQewrRU71VVXIm2k51x6gaDUnCXrJXTkMtCWJUM7RErTFR5BuynX1te/I9IkyyiZ770DPGwLokHceuLeEJTW09AQIDAQAB";
// 创建 RSA 对象,设置算法、私钥、公钥为null
RSA rsa = new RSA("RSA/ECB/PKCS1Padding",PRIVATE_KEY, null);
String text = "明文测试内容";
// 私钥加密
byte[] encrypt = rsa.encrypt(text, KeyType.PrivateKey);
String s2 = Base64.encodeBase64String(encrypt);
System.out.println("密文:" + s2);
// 创建 RSA 对象,设置算法、私钥为null、公钥
RSA rsa1 = new RSA("RSA/ECB/PKCS1Padding",null, PUBLIC_KEY);
// 公钥解密
String s1 = rsa1.decryptStr(s, KeyType.PublicKey);
System.out.println("明文:" + s1);
Hutool 创建密钥的方式也很简单,示例如下:
// 创建 RSA 对象
RSA rsa = new RSA();
// 获取公钥 Base64 字符串
String publicKeyBase64 = rsa.getPublicKeyBase64();
// 获取私钥 Base64 字符串
String privateKeyBase64 = rsa.getPrivateKeyBase64();
System.out.println("公钥 Base64" + publicKeyBase64);
System.out.println("私钥 Base64" + privateKeyBase64);
---------------------
// 通过工具类 SecureUtil 获取公钥
PublicKey publicKey = SecureUtil.generatePublicKey("RSA", Base64.decodeBase64(key));
// 通过工具类 SecureUtil 获取私钥
PrivateKey privateKey = SecureUtil.generatePrivateKey("RSA", Base64.decodeBase64(key));
密钥转成 Base64 字符串方便存储。
SM2 算法
简介
SM2算法全称是SM2椭圆曲线公钥密码算法,是一种基于椭圆曲线的密码(ECC),用来替换RSA加密算法的。
由于SM2也是非对称加密算法,与RSA相比,复杂度更高,同等安全强度下,密钥长度较短,运算效率都要更优,但是国密算法尚未实现广泛的兼容性,在主流浏览器,操作系统的终端环境中不受信任,面向互联网的产品引用中采用国密算法将无法满足可用性、易用性和全球通用性的需求。
同等安全强度下,密钥长度对比:
RSA key size (bits) | ECC key size (bits) |
1024 | 160 |
2048 | 224 |
3072 | 256 |
7680 | 384 |
15360 | 521 |
SM2 非对称加密的结果由 C1,C2,C3 三部分组成。其中 C1 是生成随机数的计算出的椭圆曲线点,C2 是密文数据,C3 是SM3的摘要值。最开始的国密标准的结果是按 C1C2C3 顺序的,新标准的是按 C1C3C2 顺序存放的。
小试牛刀
- 同样基于 JDK 实现 SM2 加解密,示例如下:
private static X9ECParameters x9ECParameters = GMNamedCurves.getByName("sm2p256v1");
private static ECDomainParameters ecDomainParameters = new ECDomainParameters(x9ECParameters.getCurve(),
x9ECParameters.getG(), x9ECParameters.getN());
private static ECParameterSpec ecParameterSpec = new ECParameterSpec(x9ECParameters.getCurve(),
x9ECParameters.getG(), x9ECParameters.getN());
static {
if (Security.getProvider("BC") == null) {
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
}
}
public static void main(String[] args) {
//生成密钥对
KeyPair kp = generateKeyPair();
//获取公钥
PublicKey publicKey = kp.getPublic();
//获取私钥
PrivateKey privateKey = kp.getPrivate();
//明文
String text = "SM2 Demo";
byte[] encrypt = encrypt(text.getBytes(), publicKey, "C1C3C2");
System.out.println(Base64.encodeBase64String(encrypt));
encrypt = decrypt(encrypt, privateKey, "C1C3C2");
System.out.println(new String(encrypt));
}
private static KeyPair generateKeyPair() {
try {
KeyPairGenerator kpGen = KeyPairGenerator.getInstance("EC", "BC");
kpGen.initialize(ecParameterSpec, new SecureRandom());
return kpGen.generateKeyPair();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private static byte[] encrypt(byte[] data, PublicKey key, String standard) {
if ("C1C2C3".equals(standard)) {
return encryptNew(encryptOld(data, key));
}
return encryptOld(data, key);
}
private static byte[] decrypt(byte[] data, PrivateKey key, String standard) {
if ("C1C2C3".equals(standard)) {
return decryptOld(decryptNew(data), key);
}
return decryptOld(data, key);
}
private static byte[] encryptOld(byte[] data, PublicKey key) {
BCECPublicKey bcecPublicKey = (BCECPublicKey) key;
ECPublicKeyParameters ecPublicKeyParameters = new ECPublicKeyParameters(bcecPublicKey.getQ(), ecDomainParameters);
SM2Engine sm2Engine = new SM2Engine();
sm2Engine.init(true, new ParametersWithRandom(ecPublicKeyParameters, new SecureRandom()));
try {
return sm2Engine.processBlock(data, 0, data.length);
} catch (InvalidCipherTextException e) {
throw new RuntimeException(e);
}
}
private static byte[] encryptNew(byte[] c1c2c3) {
final int c1Len = (x9ECParameters.getCurve().getFieldSize() + 7) / 8 * 2 + 1;
final int c3Len = 32;
byte[] result = new byte[c1c2c3.length];
System.arraycopy(c1c2c3, 0, result, 0, c1Len);
System.arraycopy(c1c2c3, c1c2c3.length - c3Len, result, c1Len, c3Len);
System.arraycopy(c1c2c3, c1Len, result, c1Len + c3Len, c1c2c3.length - c1Len - c3Len);
return result;
}
private static byte[] decryptOld(byte[] data, PrivateKey key) {
BCECPrivateKey bcecPrivateKey = (BCECPrivateKey) key;
ECPrivateKeyParameters ecPrivateKeyParameters = new ECPrivateKeyParameters(bcecPrivateKey.getD(), ecDomainParameters);
SM2Engine sm2Engine = new SM2Engine();
sm2Engine.init(false, ecPrivateKeyParameters);
try {
return sm2Engine.processBlock(data, 0, data.length);
} catch (InvalidCipherTextException e) {
throw new RuntimeException(e);
}
}
private static byte[] decryptNew(byte[] c1c3c2) {
final int c1Len = (x9ECParameters.getCurve().getFieldSize() + 7) / 8 * 2 + 1;
final int c3Len = 32;
byte[] result = new byte[c1c3c2.length];
System.arraycopy(c1c3c2, 0, result, 0, c1Len);
System.arraycopy(c1c3c2, c1Len + c3Len, result, c1Len, c1c3c2.length - c1Len - c3Len);
System.arraycopy(c1c3c2, c1Len, result, c1c3c2.length - c3Len, c3Len);
return result;
}
- 基于 Hutool 实现 SM2 加解密,Hutool 在国密算法章节中介绍了 SM2,并且列举了加解密和签名验签的示例,如何将加密和解密以及签名和验签分开实现呢?示例如下:
String PRIVATE_KEY = "MIGTAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBHkwdwIBAQQgbcopbJUM3mK8e+swrzhgausNC9eSbQA4zyIG5GxnUaWgCgYIKoEcz1UBgi2hRANCAAQRa9Lg5jFl2QL0U4d0FN4zlkpMfXffa4eG2ap7JJa0r5D1mR6z/Zraq3aZstzrUf5QkBfyfSkwaW6smmbqNePG";
String PUBLIC_KEY = "MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEEWvS4OYxZdkC9FOHdBTeM5ZKTH1332uHhtmqeySWtK+Q9Zkes/2a2qt2mbLc61H+UJAX8n0pMGlurJpm6jXjxg==";
String text = "明文测试内容";
// 获取公钥
PublicKey publicKey = SecureUtil.generatePublicKey("SM2", Base64.decodeBase64(PUBLIC_KEY));
SM2 sm2 = new SM2();
//设置标准
sm2.setMode(SM2Engine.Mode.C1C2C3);
//设置公钥
sm2.setPublicKey(publicKey);
//公钥加密
byte[] encrypt = sm2.encrypt(text.getBytes(), KeyType.PublicKey);
System.out.println("密文:" + Base64.encodeBase64String(encrypt));
//获取私钥
PrivateKey privateKey = SecureUtil.generatePrivateKey("SM2", Base64.decodeBase64(PRIVATE_KEY));
SM2 sm21 = new SM2();
//设置标准
sm21.setMode(SM2Engine.Mode.C1C2C3);
//设置私钥
sm21.setPrivateKey(privateKey);
//私钥解密
byte[] decrypt = sm21.decrypt(encrypt, KeyType.PrivateKey);
System.out.println("明文:" + new String(decrypt));
特别提醒:SM2 不能私钥加密,公钥解密,否则会抛异常,例如:Encrypt is only support by public key
- 非对称加解密算法可用于数字签名,一般通过私钥签名,公钥验签,Hutool 使用 SM2 曲线点构建 SM2 的示例变量有误,而且 Hutool 所举示例都是通过一个对象实现签名和验签的,实际开发签名和验签也是分开的,示例如下:
//需要加密的明文
String text = "明文测试内容";
//私钥16进制字符串
String privateKey = "308193020100301306072a8648ce3d020106082a811ccf5501822d047930770201010420f60a8692dc3751a728b48b1418b8d9d9d3debf81d6af8e79887fa2b6ed3b996fa00a06082a811ccf5501822da144034200043e7e702ff276d7f7139b6e61f7b1d3b3dc6364b5fa171355db2d3dd8adf71428780ee85d01a85bba4e2c7210bdd00bf936098c44a86e97c962396e3d04c40350";
//公钥16进制字符串
String publicKey = "3059301306072a8648ce3d020106082a811ccf5501822d034200043e7e702ff276d7f7139b6e61f7b1d3b3dc6364b5fa171355db2d3dd8adf71428780ee85d01a85bba4e2c7210bdd00bf936098c44a86e97c962396e3d04c40350";
//创建 SM2 对象
SM2 sm2 = new SM2(HexUtil.decodeHex(privateKey), null);
//签名
byte[] sign = sm2.sign(text.getBytes(), null);
System.out.println("数据:" + HexUtil.encodeHexStr(text.getBytes()));
String s = Base64.encodeBase64String(sign);
System.out.println("签名 Base64 字符串:" + Base64.encodeBase64String(sign));
//创建 SM2 对象
SM2 sm = new SM2(null, HexUtil.decodeHex(publicKey));
//验签
boolean verify = sm.verify(text.getBytes(), Base64.decodeBase64(s));
System.out.println("结果:" + verify);
本示例私钥和公钥使用了16进制字符串,当然也可以使用 Base64 字符串。
优缺点
-
非对称加密算法的保密性好,它消除了最终用户交换密钥的需要。但是加解密速度要远远慢于对称加密。
-
算法强度复杂、安全性依赖于算法与密钥但是由于其算法复杂,而使得加密解密速度没有对称加密解密的速度快。
场景
- 信息加密
收信者是唯一能够解开加密信息的人,因此收信者手里的是私钥。发信者手里的是公钥,其它人知道公钥没有关系,因为其它人发来的信息对收信者没有意义。
- 登录认证
客户端需要将认证标识传送给服务器,此认证标识(可能是一个随机数)其它客户端可以知道,因此需要用私钥加密,客户端保存的是私钥。服务器端保存的是公钥,其它服务器知道公钥没有关系,因为客户端不需要登录其它服务器。
- 数字签名
数字签名是为了表明信息没有受到伪造,确实是信息拥有者发出来的,附在信息原文的后面。就像手写的签名一样,具有不可抵赖性和简洁性。
简洁性:对信息原文做哈希运算,得到消息摘要,信息越短加密的耗时越少。
不可抵赖性:信息拥有者要保证签名的唯一性,必须是唯一能够加密消息摘要的人,因此必须用私钥加密 (就像字迹他人无法学会一样),得到签名。如果用公钥,那每个人都可以伪造签名了。
- 数字证书
问题起源:对1和3,发信者怎么知道从网上获取的公钥就是真的?没有遭受中间人攻击?
这样就需要第三方机构来保证公钥的合法性,这个第三方机构就是 CA (Certificate Authority),证书中心。
CA 用自己的私钥对信息原文所有者发布的公钥和相关信息进行加密,得出的内容就是数字证书。
信息原文的所有者以后发布信息时,除了带上自己的签名,还带上数字证书,就可以保证信息不被篡改了。信息的接收者先用 CA给的公钥解出信息所有者的公钥,这样可以保证信息所有者的公钥是真正的公钥,然后就能通过该公钥证明数字签名是否真实了。