作为支付机构,传输的数据大多是非常隐私的,比如身份证号、银行卡号、银行卡密码等。一旦这些信息被不法分子截获,就可能直接被盗刷银行卡,给消费者造成巨大损失。如果不法分子获取的信息是加密的,且没有解密的秘钥,那么对于不法分子来说这些信息就是一堆乱码,这就是加码最重要的意义。
目前最重用的加密、解密算法主要有两类:对称加密算法和非对称加密算法。两者的主要区别在于加密和解密的秘钥是否一致,一致就是对称加密,不一致就是非对称加密。对称加密常用的是AES
加密算法,非对称加密算法常用的是RSA
加密算法,下面分别介绍 RSA
加密算法和AES
加密算法在支付项目中的应用。
RSA
是一种非对称加密算法,可以在不传递秘钥的情况下完成解密,避免了对称加密直接传递秘钥造成被破解的 风险。RSA
加密/解密由一对由公钥和私钥组成的秘钥共同完成加密和解密,公钥是公开的,用来加密,私钥是保密的,用来解密。两者之间通过一定的算法关联,最核心的思想是利用一对极大整数做因数分解的困难性来保证安全。
假设甲是支付机构,乙是支付机构的商户,甲乙之间需要进行数据的传输。如果要对数据进行加密/解密,则要先生成密钥:甲生成一对秘钥(公钥和私钥),公钥给乙,私钥自己保留;同样乙也生成一对公私钥,公钥给甲,私钥留给自己。具体过程如下图所示:
有了秘钥之后就可以对传输的数据进行加密了。数据传输是双向的,所以支付行业数据的加密/解密也是双向的,具体步骤如下:
1)乙使用甲的公钥加密要传输的数据,并把加密后的数据上送给甲;
2)甲收到乙传来的加密数据,使用自己的私钥解密;
3)甲将处理后的数据使用乙的公钥进行加密后返回给乙;
4)乙接受返回的数据,并使用自己的私钥解密。
以上步骤是一个支付机构一个比较标准的加密/解密流程,甲乙双方分别使用对方的公钥加密,然后使用自己的私钥解密,具体流程如下图所示:
JDK
已经封装好了 RSA
加密/解密的方法,如果要对数据加密/解密,则需要先生成一对秘钥。生成密钥的代码如下所示:
package org.sang.utils;
import java.security.*;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
public class keyPairGenUtil {
// 秘钥大小
public static int KEY_LENGTH = 1024;
// 算法类型
public static String ALGORITHM_TYPE = "RSA";
public static Map<String, String> genkeyPair() throws NoSuchAlgorithmException {
// 存储公钥和私钥
Map<String, String> keyPairMap = new HashMap<>();
// 为 RSA 算法创建keyPairGenerator 对象
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM_TYPE);
// 创建可信任的随机数源
SecureRandom secureRandom = new SecureRandom();
// 使用随机数据源初始化keyPairGenerator 对象
keyPairGenerator.initialize(KEY_LENGTH, secureRandom);
// 生成密钥对
KeyPair keypair = keyPairGenerator.generateKeyPair();
// 获取公钥
PublicKey publicKey = keypair.getPublic();
// 获取私钥
PrivateKey privateKey = keypair.getPrivate();
// 使用 Base64 将公钥和私钥转化为字符串
String publicKeyStr = Base64.getEncoder().encodeToString(publicKey.getEncoded());
String privateKeyStr = Base64.getEncoder().encodeToString(privateKey.getEncoded());
keyPairMap.put("publicKey", publicKeyStr);
keyPairMap.put("privateKey", privateKeyStr);
return keyPairMap;
}
}
生成公钥和私钥之后就可以加密解密了,加密/解密的示例如下所示:
package org.sang.utils;
import javax.crypto.Cipher;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public class RSAUtil {
public static String encrypt(String data, String publicKey) throws Exception {
// 使用Base64 编码的公钥解析为二进制
byte[] publicKeyByte = Base64.getDecoder().decode(publicKey);
// 得到公钥
PublicKey pubKey = KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(publicKeyByte));
// 加密数据
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, pubKey);
// 得到加密后的数据
return Base64.getEncoder().encodeToString(cipher.doFinal(data.getBytes()));
}
public static String decrypt(String data, String privateKey) throws Exception{
// 将 Base64 编码的私钥解析为二进制
byte[] privateKeyByte = Base64.getDecoder().decode(privateKey);
// 使用 Base64 解析后得到的加密数据
byte[] dataByte = Base64.getDecoder().decode(data.getBytes());
// 获取私钥
PrivateKey priKey = KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(privateKeyByte));
// RSA 解密
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, priKey);
// 得到解密后的数据
return new String(cipher.doFinal(dataByte));
}
}
然后写一个测试类完成生成公私钥并对银行转账数据进行加密和解密的测试方法,如下所示:
package org.sang.test;
import org.sang.utils.KeyPairGenUtil;
import org.sang.utils.RSAUtil;
import java.util.Map;
public class RSATest {
public static void main(String[] args) throws Exception {
Map<String, String> keyPairMap = KeyPairGenUtil.genkeyPair();
String publicKey = keyPairMap.get("publicKey");
System.out.println("publicKey="+publicKey);
String privateKey = keyPairMap.get("privateKey");
System.out.println("privateKey="+privateKey);
String jsonData = "{\"fromCardNum\": \"622135689832498329\", \"payAmount\": 1000.00, \"toCardNum\": \"623269874369721685\"}";
// 加密开始时间戳
long encryptStartTime = System.currentTimeMillis();
String encryptData = RSAUtil.encrypt(jsonData, publicKey);
// 解密结束时间戳
long encryptEndTime = System.currentTimeMillis();
System.out.println("RSA算法加密耗时:"+(encryptEndTime-encryptStartTime)+"ms");
System.out.println("encryptData="+encryptData);
// 解密开始时间戳
long decryptStartTime = System.currentTimeMillis();
String decryptData = RSAUtil.decrypt(encryptData, privateKey);
// 解密结束时间戳
long decryptEndTime = System.currentTimeMillis();
System.out.println("解密耗时:"+(decryptEndTime-decryptStartTime)+"ms");
System.out.println("decryptData="+decryptData);
}
}
运行 RSATest 类中的 main 方法,可以看到控制台中成功打印出公钥、私钥以及加密数据和解密数据,由此说明我们写的生成公私钥和 `RSA`算法加密解密工具方法是没有问题的。
publicKey=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCcy+2sKc+lTYrFe4RuiNWc62VAPtueoPDFP3UzCmhBp+zbTRR5U6uXeGEmQrKzTrFCH2yoDoKaxdFOfnjmV94zrc54WUpTWI5XXOgSBrmFhJBqoBgkRDv7M3oNw5FHnGWXJZ+ydk3fZVrcvw5qJDucAkG90/JvAW+kwL0PuI//3wIDAQAB
privateKey=MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAJzL7awpz6VNisV7hG6I1ZzrZUA+256g8MU/dTMKaEGn7NtNFHlTq5d4YSZCsrNOsUIfbKgOgprF0U5+eOZX3jOtznhZSlNYjldc6BIGuYWEkGqgGCREO/szeg3DkUecZZcln7J2Td9lWty/DmokO5wCQb3T8m8Bb6TAvQ+4j//fAgMBAAECgYA3+r7CJrNRyxtuYijn5caOHaSqiUaTndYYNg27yU1rk26G5UAYCP1MONhGdq2iQsgaWWnLnlKWu2V85r53TouvzH1dmnR/s9mguVbjhYYsQ7zTbbAjQSImtNcWfkoGpPMR7uglkHfUlX16xF9iRfZN5pcZm/Sp8nQ7wVVXkIUBIQJBANFUiKhIlR7us+xL1Je7ZxgRg+KaIs7MDATNKtcf5FiJx0QHFvi0ezpDHIvBXhVIYgVm7EYOiEYCzb06jweZgAkCQQC/wQ1uawneOpobg76OVNGH28G1upC6xD+LeuSSU4e7Z3+YfJ1o1u83IJagNQNsDXb8S/gklw1R829iYQj0DCqnAkEAkcItDhDMVSedfRIoTCcf2DCKBwWQ6zJFxCoghH8ef1Agwou1QSRbEeydOetBWcx3BI/wQa/oz+cv322hHoeSEQJBAJm/7UkPwkXRrydIp03wbGEGr3dLNCjMmjb4PrWlDDwTbJeTs5MQY5ZMJvomB6xnz3PUZg7QnvmKu1CihU9JQhkCQD/dOHBP+/SN+NuBNJY9Ilj2sfkoH7psCAm57okSmTD6nJsqW9DPJJIXFnueKDN8SKg5QGmUri+swOFfqHPjpGM=
RSA算法加密耗时:378ms
encryptData=VGPHIUyMEHmipPai1Lz+jkET3QjTopIggS6I8AtvzeoIwTqR9ZWXy+fWaYN7L8na+bbd1NSpAx6V97Eb0doVTTwKtwBF7peG2ofFpz6ZqqLzo9ShpYKOY7CELFY/a40NOK8VxF7zz5iOdKVNgAGh7ei119bntApDkWypPA+cNk8=
解密耗时:6ms
decryptData={"fromCardNum": "622135689832498329", "payAmount": 1000.00, "toCardNum": "623269874369721685"}
这里RSA
算法加密用时378ms
, 解密用时了6ms
, 当然加密解密的耗时也和被加密和解密的内容长度有关。
AES
是一种经典的加密/解密算法,使用加密函数和解密密钥来完成对明文的加密,然后使用相同的秘钥和对应的函数来完成解密。AES
的优点在于效率非常高,相比RSA
算法的效率要高得多。AES
算法 加密/解密流程如下图所示:
AES
的加密和解密需要借助秘钥,秘钥是提前生成的,支付机构根据一定的规则生成密钥之后,传输给商户,商户上送的数据需要使用秘钥进行加密,支付机构收到密文之后使用相同的秘钥进行解密。
package org.sang.utils;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
public class AESUtil {
// 指定算法类型
public static String AES = "AES";
// 指定秘钥长度
public static int KEY_LEN = 128;
// 指定编码格式
public static String UTF_8 = "UTF-8";
/**
* 生成 AES 秘钥
* @return aesKey
*/
public static String genAESKey() throws Exception{
// 构造秘钥生成器,指定 AES 算法
KeyGenerator keyGenerator = KeyGenerator.getInstance(AES);
// 生成一个指定位数的随机源,KEY_LEN=128 就是 128位
keyGenerator.init(KEY_LEN);
// 生成对称秘钥
SecretKey secretKey = keyGenerator.generateKey();
return Base64.getEncoder().encodeToString(secretKey.getEncoded());
}
/**
* AES 算法加密
* @param key 秘钥
* @param data 待加密字符串数据
* @return encryptData
*/
public static String encrypt(String key, String data) throws Exception{
// 获取key
SecretKey secretKey = new SecretKeySpec(Base64.getDecoder().decode(key.getBytes()), AES);
// 根据指定算法生成密码器
Cipher cipher = Cipher.getInstance(AES);
// 初始化密码器
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
// 将加密内容转成字节数组
byte[] byteData = data.getBytes(UTF_8);
// 将字节数组加密
byte[] aesData = cipher.doFinal(byteData);
return new String(Base64.getEncoder().encode(aesData));
}
/**
* AES 算法解密
* @param key 秘钥
* @param data 待解密字符串数据
* @return decryptData
*/
public static String decrypt(String key, String data) throws Exception{
// 获取 key
SecretKey secretKey = new SecretKeySpec(Base64.getDecoder().decode(key.getBytes()), AES);
// 根据指定算法生成密码器
Cipher cipher = Cipher.getInstance(AES);
// 初始化密码器
cipher.init(Cipher.DECRYPT_MODE, secretKey);
// 将加密内容转换为字节数组,因数据是使用 Base64 转换过的,所以需要使用Base64解密
byte[] dataByte = Base64.getDecoder().decode(data.getBytes(UTF_8));
// 解密字节数组
byte[] decryptData = cipher.doFinal(dataByte);
return new String(decryptData);
}
}
接下来我们写一个测试类来测试一下AES加密/解密的效果,测试类代码如下:
package org.sang.test;
import org.sang.utils.AESUtil;
public class AESTest {
public static void main(String[] args) throws Exception {
String aesKey = AESUtil.genAESKey();
System.out.println("aesKey:"+aesKey);
String jsonData = "{\"fromCardNum\": \"622135689832498329\", \"payAmount\": 1000.00, \"toCardNum\": \"623269874369721685\"}";
// AES 算法加密算法开始时间戳
long aesEncryptStartTime = System.currentTimeMillis();
String encryptData = AESUtil.encrypt(aesKey, jsonData);
// AES 算法加密算法结束时间戳
long aesEncryptEndTime = System.currentTimeMillis();
System.out.println("AES算法加密耗时:"+(aesEncryptEndTime-aesEncryptStartTime)+"ms");
System.out.println("encryptData:"+encryptData);
// AES 算法解密开始时间戳
long aesDecryptStartTime = System.currentTimeMillis();
String decryptData = AESUtil.decrypt(aesKey, encryptData);
// AES 算法解密结束时间戳
long aesDecryptEndTime = System.currentTimeMillis();
System.out.println("AES算法解密耗时:"+(aesDecryptEndTime-aesDecryptStartTime)+"ms");
System.out.println("decryptData:"+decryptData);
}
}
执行 main
方法后控制台打印出如下结果:
aesKey:NLrMFzsrl4w+z1eLEjYbtQ==
AES算法加密耗时:5ms
encryptData:mZTTCFlc6IUci5ryw5WDGQzHZl2DzGqlX7pg3iVDZJJdtkM5QFsKSbaUsRG3lDn74BxskH+nwUzXVK2zng3kxBR6e5C52gsV/XTb4pS85omfa/VNwJ+JEvqakshCfl93
AES算法解密耗时:0ms
decryptData:{"fromCardNum": "622135689832498329", "payAmount": 1000.00, "toCardNum": "623269874369721685"}
可以看到 AES
算法生成的秘钥长度比起RSA
算法生成的秘钥来说要短得多,与RSA
算法同样的加密内容,加密只耗时5ms
, 而解密更是不足1ms
, 效率明显高于RSA
算法。
在支付领域,考虑到对安全性和高效性的要求,通常不会只采用一种加密算法,而是采用多种加密算法组合加密的方式。RSA
加密算法虽然安全,但是计算量非常大,效率比较低,在高并发情况下会面临严重的性能问题。
AES
加密的秘钥 key
在网络传输中有被拦截的风险,存在很大的安全隐患。所以,通常的办法是使用 RSA
算法生成的公钥来加密 AES
的秘钥,然后使用RSA
算法生成的私钥来解密经过网络传输过来经过加密的AES
秘钥, 最后用解密后的AES
秘钥来对报文进行加密传输。这样既保证了AES
秘钥在网络传输过程中的安全性,也保证了高并发场景下加密和解密的高效,安全性和高效性得到了兼顾。