京东科技 姚永健
导读:金融场景里如何让收银和支付更安全?背后的基础原件是加密方式的设计架构。本文重点讲解在京东金融中,我们如何用Java来实现支付加密。
一、术语表:
1.对称算法
加密解密密钥是相同的。这些算法也叫秘密密钥算法或单密钥算法,它要求发送者和接收者在安全通信之前,商定一个密钥。对称算法的安全性依赖于密钥,泄漏密钥就意味着任何人都能对消息进行加密解密。只要通信需要保密,密钥就必须保密。
对称算法可分为两类。一次只对明文中的单个位(有时对字节)运算的算法称为序列算法或序列密码。另一类算法是对明文的一组位进行运算,这些位组称为分组,相应的算法称为分组算法或分组密码。现代计算机密码算法的典型分组长度为64位――这个长度大到足以防止分析破译,但又小到足以方便作用。
2.非对称算法
非对称算法也叫公开密钥加密,它是用两个数学相关的密钥对信息进行编码。在此系统中,其中一个密钥叫公开密钥,可随意发给期望与密钥持有者进行安全通信的人。公开密钥用于对信息加密。第二个密钥是私有密钥,属于密钥持有者,此人要仔细保存私有密钥。密钥持有者用私有密钥对收到的信息进行解密。
一般来说,都是公钥加密,私钥解密。如果系统双方需要相互通讯,可以生成两对密钥对。各自保存好自己的私钥和对方的公钥,用公钥加密,私钥进行解密
3.可逆加密算法
一般来说,涉及秘钥之类的算法,都是可逆的。意思就是通过算法和秘钥加密之后,可以再次通过解密算法还原。常见的有DES、3DES、AES128、AES192、AES256 。其中AES后面的数字代表的是密钥长度。对称加密算法的安全性相对较低,比较适用的场景就是内网环境中的加解密。
4.不可逆算法
常见的不可逆加密算法有MD5,HMAC,SHA1、SHA-224、SHA-256、SHA-384,和SHA-512,其中SHA-224、SHA-256、SHA-384,和SHA-512我们可以统称为SHA2加密算法,SHA加密算法的安全性要比MD5更高,而SHA2加密算法比SHA1的要高。其中SHA后面的数字表示的是加密后的字符串长度,SHA1默认会产生一个160位的信息摘要。
不可逆加密算法最大的特点就是不需要密钥
5.加密盐
加密盐也是比较常听到的一个概念,盐就是一个随机字符串用来和我们的加密串拼接后进行加密。加盐主要是为了保证加密字符串的安全性。假如有一个加盐后的加密串,黑客通过一定手段得到这个加密串,他解密后拿到的明文,并不是我们加密前的字符串,而是加密前的字符串和盐组合的字符串,这样相对来说又增加了字符串的安全性
或者也可以用在签名,例如签名是对明文或者密文加盐后的签名,有人想串改数据,如果不知道这个盐和规则,那么接收方验签就会不通过,从而保证通讯的安全。
二、传统加密算法介绍
DES(Data Encryption Standard):
对称算法,数据加密标准,速度较快,适用于加密大量数据的场合。
AES算法:
是DES的升级版,属于对称算法。可逆
代码:AESUtil
package AES;import java.io.UnsupportedEncodingException;import java.nio.charset.StandardCharsets;import java.security.InvalidKeyException;import java.security.NoSuchAlgorithmException;import java.security.SecureRandom;import java.util.Base64;import javax.crypto.BadPaddingException;import javax.crypto.Cipher;import javax.crypto.IllegalBlockSizeException;import javax.crypto.KeyGenerator;import javax.crypto.NoSuchPaddingException;import javax.crypto.SecretKey;import javax.crypto.spec.SecretKeySpec;public class AESUtil { /** * AES加密字符串 * * @param content * 需要被加密的字符串 * @param password * 加密需要的密码 * @return 密文 */ public static byte[] encrypt(String content, String password) { try { KeyGenerator kgen = KeyGenerator.getInstance("AES");// 创建AES的Key生产者 kgen.init(128, new SecureRandom(password.getBytes()));// 利用用户密码作为随机数初始化出 //加密没关系,SecureRandom是生成安全随机数序列,password.getBytes()是种子,只要种子相同,序列就一样,所以解密只要有password就行 SecretKey secretKey = kgen.generateKey();// 根据用户密码,生成一个密钥 byte[] enCodeFormat = secretKey.getEncoded();// 返回基本编码格式的密钥,如果此密钥不支持编码,则返回 SecretKeySpec key = new SecretKeySpec(enCodeFormat, "AES");// 转换为AES专用密钥 Cipher cipher = Cipher.getInstance("AES");// 创建密码器 byte[] byteContent = content.getBytes("utf-8"); cipher.init(Cipher.ENCRYPT_MODE, key);// 初始化为加密模式的密码器 byte[] result = cipher.doFinal(byteContent);// 加密 return result; } catch (NoSuchPaddingException e) { e.printStackTrace(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } catch (InvalidKeyException e) { e.printStackTrace(); } catch (IllegalBlockSizeException e) { e.printStackTrace(); } catch (BadPaddingException e) { e.printStackTrace(); } return null; } /** * 解密AES加密过的字符串 * * @param content * AES加密过过的内容 * @param password * 加密时的密码 * @return 明文 */ public static byte[] decrypt(byte[] content, String password) { try { KeyGenerator kgen = KeyGenerator.getInstance("AES");// 创建AES的Key生产者 kgen.init(128, new SecureRandom(password.getBytes())); SecretKey secretKey = kgen.generateKey();// 根据用户密码,生成一个密钥 byte[] enCodeFormat = secretKey.getEncoded();// 返回基本编码格式的密钥 SecretKeySpec key = new SecretKeySpec(enCodeFormat, "AES");// 转换为AES专用密钥 Cipher cipher = Cipher.getInstance("AES");// 创建密码器 cipher.init(Cipher.DECRYPT_MODE, key);// 初始化为解密模式的密码器 byte[] result = cipher.doFinal(content); return result; // 明文 } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (NoSuchPaddingException e) { e.printStackTrace(); } catch (InvalidKeyException e) { e.printStackTrace(); } catch (IllegalBlockSizeException e) { e.printStackTrace(); } catch (BadPaddingException e) { e.printStackTrace(); } return null; } public static void main(String[] args) throws Exception { String a = Base64.getEncoder().encodeToString("435678(*&*&^*&^%&^$%^#%#!@#$%^&%%90|8752$".getBytes(StandardCharsets.UTF_8)); String b = new String(Base64.getDecoder().decode(a.getBytes(StandardCharsets.UTF_8))); System.out.println(a+":"+b); String content = "{'aaa':'111','bbb':'222'}"; String secretKey = "43567890|8752$"; System.out.println("需要加密的内容:" + content); byte[] encrypt = encrypt(content, secretKey); System.out.println("加密后的2进制密文:"); System.out.println(new String(encrypt)); String hexStr = Base64.getEncoder().encodeToString(encrypt); System.out.println("base64编码后密文:" + hexStr); byte[] byte2 = Base64.getDecoder().decode(hexStr); System.out.println("解码后转换为2进制后密文:"); System.out.println(new String(byte2)); byte[] decrypt = decrypt(byte2, secretKey); System.out.println("解密后的内容:" + new String(decrypt,"utf-8")); }}
RSA算法:
公钥加密算法,非对称,可逆
代码:RSAUtil
package RSASha256;import javax.crypto.BadPaddingException;import javax.crypto.Cipher;import javax.crypto.IllegalBlockSizeException;import javax.crypto.NoSuchPaddingException;import java.io.UnsupportedEncodingException;import java.security.*;import java.security.interfaces.RSAPrivateKey;import java.security.interfaces.RSAPublicKey;import java.security.spec.InvalidKeySpecException;import java.security.spec.PKCS8EncodedKeySpec;import java.security.spec.X509EncodedKeySpec;import java.util.Base64;import java.util.HashMap;import java.util.Map;public class RSAUtil { private static final int DEFAULT_RSA_KEY_SIZE = 2048; private static final String KEY_ALGORITHM = "RSA"; private static final String SIGNATURE_ALGORITHM = "MD5withRSA"; public static void main(String [] args){ Map<String,String> result = generateRsaKey(DEFAULT_RSA_KEY_SIZE); String publicKey = result.get("publicKey"); String privateKey = result.get("privateKey");// String publicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuazymB25s/ueIfwMM2H74736dQQE4bvRDgNfWltM4PzduNC84ntav41qEgbZtby47O57m/YDVf6vMXJH25ejlsBBhls7FD7xHTkOrskZqLekH0xCs2xa/akdwllbewUNzvWMuQy8X3j2VnrILXzVjvqYxkFY0itMo8fq1xSO6B/S5/RaeKpTtIepFB2cIU9vMrtLoCsnjVoTuPdWcoC79LtS0g1Q5BV4dn+Uca+8/gUbhS7vbth3oLZt6gXTRjJrPxhT4lqWsCZqwkQHPG/8JQgnFqs7+R0KcyemG/411IaZI9WYRGTsQji8sQ2q7HAlZRXvJn+As7jJPvyi67JGKwIDAQAB";// String privateKey = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC5rPKYHbmz+54h/AwzYfvjvfp1BAThu9EOA19aW0zg/N240Lzie1q/jWoSBtm1vLjs7nub9gNV/q8xckfbl6OWwEGGWzsUPvEdOQ6uyRmot6QfTEKzbFr9qR3CWVt7BQ3O9Yy5DLxfePZWesgtfNWO+pjGQVjSK0yjx+rXFI7oH9Ln9Fp4qlO0h6kUHZwhT28yu0ugKyeNWhO491ZygLv0u1LSDVDkFXh2f5Rxr7z+BRuFLu9u2Hegtm3qBdNGMms/GFPiWpawJmrCRAc8b/wlCCcWqzv5HQpzJ6Yb/jXUhpkj1ZhEZOxCOLyxDarscCVlFe8mf4CzuMk+/KLrskYrAgMBAAECggEABjC+8dVj4J1N+2IU4g2tQT2PQSF+LCx/3tC7+B49JO8pUUUcVwy3zNUhKTKzRXziSXv2ARAlslNIcgSWYrreiGMmjB00jgs/LLM/SxKHWXmt7iEzxBmjuvtNc7JY+3QCrti+9Vh4W1KEHAQB8opL8HVobIu3M2KgLoG20a7syM5gPiCUb3EQ6P30u47y7I2wsbMnMmu79rPu3Q7lSNeoB9KdVV6EA6vukL3chY1QxFh6oFZS1yNjFjxy5RxT8U5Juhr5LYRnOP8MEVptbtrh34SSm0uyckkgJ6jOsrDJqo/DJxjPxixj+BWgG5/XbNq3PFEyEAqkrTZY+FMHSufoaQKBgQDpqPxpl05mDUXCnc7I6F8LZqjnjZU1h9zwKuQe94Fs3I3siWS62sJx1pZ66P9eeIOsT0T4Ye9nDx2x04yyfUcENhydFTQ5M5M9e2q1kKdNeBUT6Xy4WcRibKozUq07mLYSn0kDbuimp/Erp2w3hl5nZu/eLKDjt7yM0otv3uPp5wKBgQDLbYC4hcvk+f1kd/xJQ2ljwF+c8kHHFrlLRipf7qTB24dcrauWXbAGbbcKXx2G+3Waswyf09NdALiWab/jUUUHJm0YGxyr0k5lHci0/eC7hD3tWTL/HRyeF3umKZEkBQypzNeymE6t0lSqjL3MXFQLqumu4h677qA9/DNh7DYhHQKBgQCfAm/bj6s7iba6jVfmozPi91bkVRaAWlgBXL7nT/nU0ncGzC0vd6WxgJ3hQORgLtU0krFV8pfP45qKpHNwGA8XD5gDUiW68500zuM8chdYgeqeJVvJvNUHQfnFeXMIRpFJNPqkCnrqxwk5cvMTCi7+YS/FW0uWDDiVAMcBN4aUawKBgCOgvAiVNk6WEfEEqqTSL6UOzjAYpbiOnEk4src2fpiNMDnlGMYvBmM51/LzEaLQa5p6fV2Ipd4GAE4nmznew+4qprSwGudk3+IJw1sfk7qDwKzPEIVpvddaWYeShB8A22TpwWVAE5eR3M459AvUp8ubVW4RoDxd4Ka6gu1Fh31pAoGAYNPKJNCrdOsQH0nkH0Ld44cH6HD+zcZtoad2eQk0T2SPnKBsIS1S9W0adreOXUv1edO/hN3P/BcTuCaax7ijTscS2f0atc/NIa6LjBnK7oUBBzib9v21L72ZVZ5st4c/H7IzbQCXfS81489a7TTHP+e1HzS/XePftO0pAkr1GJ0="; System.out.println("公钥为:" + publicKey); System.out.println("私钥为:" + privateKey); String plaintext = "{'a':'1111','b':'2222'}"; String ciphertext = null; try { System.out.println("开始加密明文:"+plaintext); ciphertext = encrypt(plaintext,publicKey); } catch (Exception e) { System.out.println("加密失败"); throw new RuntimeException(e); } System.out.println("得到的密文为:"+ciphertext); String deciphering = decrypt(ciphertext,privateKey); System.out.println("解密后:"+deciphering); } /** * 生成RSA 公私钥,可选长度为1025,2048位. */ public static Map<String,String> generateRsaKey(int keySize) { Map<String,String> result = new HashMap<>(2); try { KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(KEY_ALGORITHM); // 初始化密钥对生成器,密钥大小为1024 2048位 keyPairGen.initialize(keySize, new SecureRandom()); // 生成一个密钥对,保存在keyPair中 KeyPair keyPair = keyPairGen.generateKeyPair(); // 得到公钥字符串 result.put("publicKey", new String(Base64.getEncoder().encode(keyPair.getPublic().getEncoded()))); // 得到私钥字符串 result.put("privateKey", new String(Base64.getEncoder().encode(keyPair.getPrivate().getEncoded()))); } catch (GeneralSecurityException e) { e.printStackTrace(); } return result; } /** * RSA私钥解密 * @param str 解密字符串 * @param privateKey 私钥 * @return 明文 */ public static String decrypt(String str, String privateKey) { //64位解码加密后的字符串 byte[] inputByte; String outStr = ""; try { inputByte = Base64.getDecoder().decode(str.getBytes("UTF-8")); //base64编码的私钥 byte[] decoded = Base64.getDecoder().decode(privateKey); RSAPrivateKey priKey = (RSAPrivateKey) KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(decoded)); //RSA解密 Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.DECRYPT_MODE, priKey); outStr = new String(cipher.doFinal(inputByte)); } catch (UnsupportedEncodingException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException | InvalidKeySpecException | NoSuchAlgorithmException e) { e.printStackTrace(); } return outStr; } /** * RSA公钥加密 * @param str 需要加密的字符串 * @param publicKey 公钥 * @return 密文 * @throws Exception 加密过程中的异常信息 */ public static String encrypt(String str, String publicKey) throws Exception { //base64编码的公钥 byte[] decoded = Base64.getDecoder().decode(publicKey); RSAPublicKey pubKey = (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(decoded)); //RSA加密 Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.ENCRYPT_MODE, pubKey); String outStr = Base64.getEncoder().encodeToString(cipher.doFinal(str.getBytes("UTF-8"))); return outStr; } public static String sign(String data, String priKey) throws Exception { byte[] decoded = Base64.getDecoder().decode(priKey); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); PrivateKey privateKey = keyFactory.generatePrivate(new X509EncodedKeySpec(decoded)); Signature signature = Signature.getInstance("MD5withRSA"); signature.initSign(privateKey); signature.update(data.getBytes()); return new String(Base64.getEncoder().encode(signature.sign())); } public static boolean verify(String pubKey, String sign, String data) throws Exception{ //获取KeyFactory,指定RSA算法 KeyFactory keyFactory = KeyFactory.getInstance("RSA"); //将BASE64编码的公钥字符串进行解码 byte[] encodeByte = Base64.getDecoder().decode(pubKey); //将BASE64解码后的字节数组,构造成X509EncodedKeySpec对象,生成公钥对象 PublicKey publicKey = keyFactory.generatePublic(new X509EncodedKeySpec(encodeByte)); Signature signature = Signature.getInstance("MD5withRSA"); //加载公钥 signature.initVerify(publicKey); //更新原数据 signature.update(data.getBytes("UTF-8")); //公钥验签(true-验签通过;false-验签失败) return signature.verify(Base64.getDecoder().decode(sign)); }}
MD5算法:
信息摘要(Hash安全散列)算法,也叫哈希算法,哈希值也叫散列值,不可逆,不需要秘钥。
代码: MD5Util
package MD5;import java.nio.charset.StandardCharsets;import java.security.MessageDigest;import java.security.NoSuchAlgorithmException;public class MD5Util { public static void main(String[] args) { String sha256Str = getSha256Str("123456"); System.out.println(sha256Str); } /** * sha256加密 * * @param str 要加密的字符串 * @return 加密后的字符串 */ public static String getSha256Str(String str) { MessageDigest messageDigest; String encodeStr = ""; try { messageDigest = MessageDigest.getInstance("MD5"); messageDigest.update(str.getBytes(StandardCharsets.UTF_8)); encodeStr = byte2Hex(messageDigest.digest()); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } return encodeStr; } /** * sha256加密 将byte转为16进制 * * @param bytes 字节码 * @return 加密后的字符串 */ private static String byte2Hex(byte[] bytes) { StringBuilder stringBuilder = new StringBuilder(); String temp; for (byte aByte : bytes) { temp = Integer.toHexString(aByte & 0xFF); if (temp.length() == 1) { //1得到一位的进行补0操作 stringBuilder.append("0"); } stringBuilder.append(temp); } return stringBuilder.toString(); }}
SHA-256算法:
sha256算法也是一种密码散列函数,对于任意长度的消息,SHA256都会产生一个256bit长的散列值(哈希值),用于确保信息传输完整一致,称作消息摘要。这个摘要相当于是个长度为32个字节的数组,通常用一个长度为64的十六进制字符串来表示。
和MD5算法对比
相同点:
1、都是密码散列函数,加密不可逆。
2、都可以实现对任意长度对象加密,都不能防止碰撞。
安全性方面:
1、SHA256(称SHA2)的安全性最高,但是耗时要其他两种多很多。
2、md5相对来说比较容易碰撞,安全性没这么高。
性能方面:
以个60M的件为测试样本,经过1000次的测试平均值,这两种算法的表现如下:
MD5算法运1000次的平均时间为:226ms
SHA256算法运1000次的平均时间为:473ms
总而言之,md5和sha256都是密码散列函数,加密不可逆。虽然都不能防止碰撞,但是相对而言,md5比较容易碰撞,安全性没有sha256高。
代码:SHA256Util
package RSASha256;import java.nio.charset.StandardCharsets;import java.security.MessageDigest;import java.security.NoSuchAlgorithmException;public class SHA256Util { public static void main(String[] args) { String sha256Str = getSha256Str("123456"); System.out.println(sha256Str); } /** * sha256加密 * * @param str 要加密的字符串 * @return 加密后的字符串 */ public static String getSha256Str(String str) { MessageDigest messageDigest; String encodeStr = ""; try { messageDigest = MessageDigest.getInstance("SHA-256"); messageDigest.update(str.getBytes(StandardCharsets.UTF_8)); encodeStr = byte2Hex(messageDigest.digest()); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } return encodeStr; } /** * sha256加密 将byte转为16进制 * * @param bytes 字节码 * @return 加密后的字符串 */ private static String byte2Hex(byte[] bytes) { StringBuilder stringBuilder = new StringBuilder(); String temp; for (byte aByte : bytes) { temp = Integer.toHexString(aByte & 0xFF); if (temp.length() == 1) { //1得到一位的进行补0操作 stringBuilder.append("0"); } stringBuilder.append(temp); } return stringBuilder.toString(); }}
三、国密算法介绍
简介:
为保障国家密码应用安全,2011年GJMM管理局发布《关于做好公钥密码算法升级工作的通知》,
要求自2011年3月1日起在建和拟建公钥密码基础设施电子认证系统和密钥管理系统应使用国密算法。
《金融和重要领域密码应用与创新发展工作规划(2018-2022年) 》以及相关法规文件也要求我国金融
和重要领域密码应用采用SM2国产密码算法体系。
国密算法是指GJMM管理局认定的一系列国产密码算法,包括SM1-SM9以及ZUC等。
其中SM1、SM4、SM5、SM6、SM7、SM8、ZUC等属于对称密码,SM2、SM9等属于公钥密码,SM3属于单向散列函数。
目前我国主要使用公开的SM2、SM3、SM4作为商用密码。
SM1:
SM1也叫商密1号算法,是一种国产的对称算法,分组长度和密钥长度都为 128 比特,该算法不公开,调用该算法时,需要通过加密芯片的接口进行调用。算法安全保密强度及相关软硬件实现性能与 AES 相当
SM2:
SM2算法和RSA算法都是公钥密码算法,SM2算法是一种更先进安全的算法,在我们国家商用密码体系中被用来替换RSA算法。
随着密码技术和计算机技术的发展,目前常用的1024位RSA算法面临严重的安全威胁,我们国家密码管理部门经过研究,决定采用SM2椭圆曲线算法替换RSA算法。
代码:SM2Util
package GMSM;import org.bouncycastle.asn1.gm.GMNamedCurves;import org.bouncycastle.asn1.x9.X9ECParameters;import org.bouncycastle.crypto.CryptoException;import org.bouncycastle.crypto.InvalidCipherTextException;import org.bouncycastle.crypto.engines.SM2Engine;import org.bouncycastle.crypto.params.*;import org.bouncycastle.crypto.signers.SM2Signer;import org.bouncycastle.jce.provider.BouncyCastleProvider;import org.bouncycastle.math.ec.ECCurve;import org.bouncycastle.math.ec.ECPoint;import org.bouncycastle.util.Strings;import org.bouncycastle.util.encoders.Base64;import org.bouncycastle.util.encoders.Hex;import java.math.BigInteger;import java.nio.charset.Charset;import java.security.KeyPair;import java.security.KeyPairGenerator;import java.security.NoSuchAlgorithmException;import java.security.SecureRandom;import java.security.spec.ECGenParameterSpec;/** * SM2 工具类 * */public class SM2Util { //SM2推荐曲线名称 static String SM2_CURVE_NAME = "sm2p256v1"; public static final Charset UTF_8 = Charset.forName("utf-8"); /** * 生成密钥 * * @return * @throws Exception */ public static KeyPair genKeyPair() throws Exception { final ECGenParameterSpec sm2Spec = new ECGenParameterSpec(SM2_CURVE_NAME); // 获取一个椭圆曲线类型的密钥对生成器 final KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC", new BouncyCastleProvider()); SecureRandom random = new SecureRandom(); // 使用SM2的算法区域初始化密钥生成器 kpg.initialize(sm2Spec, random); // 获取密钥对 KeyPair keyPair = kpg.generateKeyPair(); return keyPair; } /**SM2根据公钥加密 * param: message 待加密内容 , publicKey 加密公钥(BASE64编码) * return: 加密信息的Base64编码 * @throws InvalidCipherTextException * */ public static String encryptBySM2(String message, String publicKey) throws InvalidCipherTextException { ECDomainParameters domin = getDomain(); //公钥对象 ECPublicKeyParameters pubKeyParameters = getPubKey(publicKey,domin); byte[] cipherBytes = new byte[0]; cipherBytes = encrypt(SM2Engine.Mode.C1C3C2, pubKeyParameters, message.getBytes(UTF_8)); return Base64.toBase64String(cipherBytes); } /** * SM2根据私钥解密 * param: cipherText 待解密密文 privateKey-私钥(BASE64编码) * */ public static String decryptBySM2(String cipherText, String privateKey) throws InvalidCipherTextException { BigInteger d = new BigInteger(1,Base64.decode(privateKey)); ECDomainParameters domin = getDomain(); //私钥对象 ECPrivateKeyParameters ecPrivateKeyParameters = new ECPrivateKeyParameters(d, domin); byte[] decrypt = decrypt(SM2Engine.Mode.C1C3C2, ecPrivateKeyParameters, Base64.decode(cipherText)); return new String(decrypt,UTF_8); } /** * 根据公钥字符串创建公钥对象 * * */ public static ECPublicKeyParameters getPubKey(String publicKey, ECDomainParameters domain) { ECCurve curve = domain.getCurve(); ECPoint point = curve.decodePoint(Base64.decode(publicKey)); ECPublicKeyParameters PublicKey = new ECPublicKeyParameters(point, domain); return PublicKey; } /** * @param mode 指定密文结构,旧标准的为C1C2C3,新的[《SM2密码算法使用规范》 GM/T 0009-2012]标准为C1C3C2 * @param pubKeyParameters 公钥 * @param srcData 原文 * @return 根据mode不同,输出的密文C1C2C3排列顺序不同。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。 * @throws InvalidCipherTextException */ public static byte[] encrypt(SM2Engine.Mode mode, ECPublicKeyParameters pubKeyParameters, byte[] srcData) throws InvalidCipherTextException { SM2Engine engine = new SM2Engine(mode); ParametersWithRandom pwr = new ParametersWithRandom(pubKeyParameters, new SecureRandom()); engine.init(true, pwr); return engine.processBlock(srcData, 0, srcData.length); } /** * @param mode 指定密文结构,旧标准的为C1C2C3,新的[《SM2密码算法使用规范》 GM/T 0009-2012]标准为C1C3C2 * @param priKeyParameters 私钥 * @param sm2Cipher 根据mode不同,需要输入的密文C1C2C3排列顺序不同。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。 * @return 原文。SM2解密返回了数据则一定是原文,因为SM2自带校验,如果密文被篡改或者密钥对不上,都是会直接报异常的。 * @throws InvalidCipherTextException */ public static byte[] decrypt(SM2Engine.Mode mode, ECPrivateKeyParameters priKeyParameters, byte[] sm2Cipher) throws InvalidCipherTextException { SM2Engine engine = new SM2Engine(mode); engine.init(false, priKeyParameters); return engine.processBlock(sm2Cipher, 0, sm2Cipher.length); } public static ECDomainParameters getDomain() { //获取一条SM2曲线参数 X9ECParameters x9ECParameters = GMNamedCurves.getByName(SM2_CURVE_NAME); //构造domain参数 ECDomainParameters domain = new ECDomainParameters( x9ECParameters.getCurve(), x9ECParameters.getG(), x9ECParameters.getN(), x9ECParameters.getH() ); return domain; } /** * 私钥签名 * @param privateKey 私钥 * @param content 待签名内容 * @return */ public static String sign(String privateKey, String content) throws CryptoException, CryptoException { //待签名内容转为字节数组 byte[] message = content.getBytes(); BigInteger domainParameters = new BigInteger(1,Base64.decode(privateKey)); ECDomainParameters domin = getDomain(); //私钥对象 ECPrivateKeyParameters privateKeyParameters = new ECPrivateKeyParameters(domainParameters, domin); //创建签名实例 SM2Signer sm2Signer = new SM2Signer(); //初始化签名实例,带上ID,国密的要求,ID默认值:1234567812345678 try { sm2Signer.init(true, new ParametersWithID(new ParametersWithRandom(privateKeyParameters, SecureRandom.getInstance("SHA1PRNG")), Strings.toByteArray("1234567812345678"))); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } sm2Signer.update(message, 0, message.length); //生成签名,签名分为两部分r和s,分别对应索引0和1的数组 byte[] signBytes = sm2Signer.generateSignature(); String sign = Base64.toBase64String(signBytes); return sign; } /** * 验证签名 * @param publicKey 公钥 * @param content 待签名内容 * @param sign 签名值 * @return */ public static boolean verify(String publicKey, String content, String sign) { //待签名内容 byte[] message = content.getBytes(); byte[] signData = Base64.decode(sign); // 获取一条SM2曲线参数 X9ECParameters sm2ECParameters = GMNamedCurves.getByName(SM2_CURVE_NAME); // 构造domain参数 ECDomainParameters domainParameters = new ECDomainParameters(sm2ECParameters.getCurve(), sm2ECParameters.getG(), sm2ECParameters.getN()); //提取公钥点 ECPoint pukPoint = sm2ECParameters.getCurve().decodePoint(java.util.Base64.getDecoder().decode(publicKey)); // 公钥前面的02或者03表示是压缩公钥,04表示未压缩公钥, 04的时候,可以去掉前面的04 ECPublicKeyParameters publicKeyParameters = new ECPublicKeyParameters(pukPoint, domainParameters); //创建签名实例 SM2Signer sm2Signer = new SM2Signer(); ParametersWithID parametersWithID = new ParametersWithID(publicKeyParameters, Strings.toByteArray("1234567812345678")); sm2Signer.init(false, parametersWithID); sm2Signer.update(message, 0, message.length); //验证签名结果 boolean verify = sm2Signer.verifySignature(signData); return verify; }}
SM3:
国产哈希算法,也叫消息摘要算法,可以用MD5作为对比理解。该算法已公开。校验结果为256位。SM3是中华人民共和国政府采用的一种密码散列函数标准,由GJMM管理局于2010年12月17日发布。相关标准为“GM/T 0004-2012 《SM3密码杂凑算法》”。
在商用密码体系中,SM3主要用于数字签名及验证、消息认证码生成及验证、随机数生成等,其算法公开。据GJMM管理局表示,其安全性及效率与SHA-256相当。
代码:SM3Util
package GMSM;import org.bouncycastle.crypto.digests.SM3Digest;import org.bouncycastle.util.encoders.Hex;import java.util.Arrays;public class SM3Util { /** * 计算SM3摘要值 * * @param srcData 原文 * @return 摘要值,对于SM3算法来说是32字节 */ public static byte[] hash(byte[] srcData) { SM3Digest digest = new SM3Digest(); digest.update(srcData, 0, srcData.length); byte[] hash = new byte[digest.getDigestSize()]; digest.doFinal(hash, 0); return hash; } /** * 验证摘要 * * @param srcData 原文 * @param sm3Hash 摘要值 * @return 返回true标识验证成功,false标识验证失败 */ public static boolean verify(byte[] srcData, byte[] sm3Hash) { byte[] newHash = hash(srcData); System.out.println(Hex.toHexString(newHash)); System.out.println(Hex.toHexString(sm3Hash)); if (Arrays.equals(newHash, sm3Hash)) { return true; } else { return false; } }}
SM4:
无线局域网标准的分组数据算法。对称加密,密钥长度和分组长度均为128位。对标AES
代码:SM4Util
package GMSM;import org.bouncycastle.jce.provider.BouncyCastleProvider;import javax.crypto.Cipher;import javax.crypto.KeyGenerator;import javax.crypto.spec.IvParameterSpec;import javax.crypto.spec.SecretKeySpec;import java.security.*;public class SM4Util { static { Security.addProvider(new BouncyCastleProvider()); } public static final String ALGORITHM_NAME = "SM4"; // AES public static final String DEFAULT_KEY = "random_seed"; // 128-32位16进制;256-64位16进制 public static final int DEFAULT_KEY_SIZE = 128; public static byte[] generateKey() throws NoSuchAlgorithmException, NoSuchProviderException { return generateKey(DEFAULT_KEY, DEFAULT_KEY_SIZE); } public static byte[] generateKey(String seed) throws NoSuchAlgorithmException, NoSuchProviderException { return generateKey(seed, DEFAULT_KEY_SIZE); } public static byte[] generateKey(String seed, int keySize) throws NoSuchAlgorithmException, NoSuchProviderException { KeyGenerator kg = KeyGenerator.getInstance(ALGORITHM_NAME, BouncyCastleProvider.PROVIDER_NAME); SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); if (null != seed && !"".equals(seed)) { random.setSeed(seed.getBytes()); } kg.init(keySize, random); return kg.generateKey().getEncoded(); } /** * @description 加密 */ public static byte[] encrypt(String algorithmName, byte[] key, byte[] iv, byte[] data) throws Exception { return sm4core(algorithmName, Cipher.ENCRYPT_MODE, key, iv, data); } /** * @description 解密 */ public static byte[] decrypt(String algorithmName, byte[] key, byte[] iv, byte[] data) throws Exception { return sm4core(algorithmName, Cipher.DECRYPT_MODE, key, iv, data); } private static byte[] sm4core(String algorithmName, int type, byte[] key, byte[] iv, byte[] data) throws Exception { Cipher cipher = Cipher.getInstance(algorithmName, BouncyCastleProvider.PROVIDER_NAME); Key sm4Key = new SecretKeySpec(key, ALGORITHM_NAME); if (algorithmName.contains("/ECB/")) { cipher.init(type, sm4Key); } else { if(iv == null ){ cipher.init(type, sm4Key); }else{ IvParameterSpec ivParameterSpec = new IvParameterSpec(iv); cipher.init(type, sm4Key, ivParameterSpec); } } return cipher.doFinal(data); }}
四、各类支付系统常见加密方式组合介绍
在支付系统交互中,有多种多样的加密方式组合,这里就简单介绍两钟常用的。
1.传统方式
用可逆,非对称算法(rsa,sm2等)使用对方公钥对报文内的关键信息进行加密,得到的密文进行编码,然后再对密文编码后的串加盐后使用不可逆算法(sha256,md5,sm3等)进行签名,使用这些算法签名后得到的是一个16进制的串,签名放到另一个字段,一般是和加密后的信息并列的。
步骤:
1.使用对方的RSA公钥对明文进行加密,得到的密文进行base64编码,作为data
2.将data的值加上约定好的盐,使用sha256算法进行签名,得到的签名是16进制的串,放到sign
加密后的json:
{ "data": "auwPDINowtdseZTfcCR8B1OPsevJE1MqIVOLn56WDvw3gjksSjkpMGp4lpYiqZkJ9G2PTaYC2DZSiyhbjtLvfnJE54mZz649gTWQVIHAn5fzV8Vs3JL3Kf9WB0Br9EbR07qrfGlWCvkkgktmTfDS/4qQGywn7TVQ3EH6HwQI7kUf0GlZ9CYcYHKi4/F61s6H9Hws+CPrhchGfJmHnu+835dSVS4IJOnA+0S5JXWVEN/is5i5cmxFb3RUYZbfpu+7JLDgcXLC8oNKIZxSE1aXhoFMj4CTTYCyRxKNCD015fnBMKh+7TYHEksyyybtVjkPqDeczgT3z+Qvj9vdyUZB1Q==", "sign": "d3b36e7a837d6241445e506faf72c8d8e3467ee9c11efae0c67dfb5b15f44b6e"}
接收后,先拿到密文,加入约定好的盐进行验签,验签通过后再用私钥进行解密。返回报文同理,只不过返回报文用的也是对方的公钥加密,对方接收后用自己的私钥解密。
demo:RSASha256Test
package RSASha256;import java.util.HashMap;import java.util.Map;public class RSASha256Test { public static void main(String[] args) { Map<String,String> jsonData = new HashMap<>(); String publicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuazymB25s/ueIfwMM2H74736dQQE4bvRDgNfWltM4PzduNC84ntav41qEgbZtby47O57m/YDVf6vMXJH25ejlsBBhls7FD7xHTkOrskZqLekH0xCs2xa/akdwllbewUNzvWMuQy8X3j2VnrILXzVjvqYxkFY0itMo8fq1xSO6B/S5/RaeKpTtIepFB2cIU9vMrtLoCsnjVoTuPdWcoC79LtS0g1Q5BV4dn+Uca+8/gUbhS7vbth3oLZt6gXTRjJrPxhT4lqWsCZqwkQHPG/8JQgnFqs7+R0KcyemG/411IaZI9WYRGTsQji8sQ2q7HAlZRXvJn+As7jJPvyi67JGKwIDAQAB"; String privateKey = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC5rPKYHbmz+54h/AwzYfvjvfp1BAThu9EOA19aW0zg/N240Lzie1q/jWoSBtm1vLjs7nub9gNV/q8xckfbl6OWwEGGWzsUPvEdOQ6uyRmot6QfTEKzbFr9qR3CWVt7BQ3O9Yy5DLxfePZWesgtfNWO+pjGQVjSK0yjx+rXFI7oH9Ln9Fp4qlO0h6kUHZwhT28yu0ugKyeNWhO491ZygLv0u1LSDVDkFXh2f5Rxr7z+BRuFLu9u2Hegtm3qBdNGMms/GFPiWpawJmrCRAc8b/wlCCcWqzv5HQpzJ6Yb/jXUhpkj1ZhEZOxCOLyxDarscCVlFe8mf4CzuMk+/KLrskYrAgMBAAECggEABjC+8dVj4J1N+2IU4g2tQT2PQSF+LCx/3tC7+B49JO8pUUUcVwy3zNUhKTKzRXziSXv2ARAlslNIcgSWYrreiGMmjB00jgs/LLM/SxKHWXmt7iEzxBmjuvtNc7JY+3QCrti+9Vh4W1KEHAQB8opL8HVobIu3M2KgLoG20a7syM5gPiCUb3EQ6P30u47y7I2wsbMnMmu79rPu3Q7lSNeoB9KdVV6EA6vukL3chY1QxFh6oFZS1yNjFjxy5RxT8U5Juhr5LYRnOP8MEVptbtrh34SSm0uyckkgJ6jOsrDJqo/DJxjPxixj+BWgG5/XbNq3PFEyEAqkrTZY+FMHSufoaQKBgQDpqPxpl05mDUXCnc7I6F8LZqjnjZU1h9zwKuQe94Fs3I3siWS62sJx1pZ66P9eeIOsT0T4Ye9nDx2x04yyfUcENhydFTQ5M5M9e2q1kKdNeBUT6Xy4WcRibKozUq07mLYSn0kDbuimp/Erp2w3hl5nZu/eLKDjt7yM0otv3uPp5wKBgQDLbYC4hcvk+f1kd/xJQ2ljwF+c8kHHFrlLRipf7qTB24dcrauWXbAGbbcKXx2G+3Waswyf09NdALiWab/jUUUHJm0YGxyr0k5lHci0/eC7hD3tWTL/HRyeF3umKZEkBQypzNeymE6t0lSqjL3MXFQLqumu4h677qA9/DNh7DYhHQKBgQCfAm/bj6s7iba6jVfmozPi91bkVRaAWlgBXL7nT/nU0ncGzC0vd6WxgJ3hQORgLtU0krFV8pfP45qKpHNwGA8XD5gDUiW68500zuM8chdYgeqeJVvJvNUHQfnFeXMIRpFJNPqkCnrqxwk5cvMTCi7+YS/FW0uWDDiVAMcBN4aUawKBgCOgvAiVNk6WEfEEqqTSL6UOzjAYpbiOnEk4src2fpiNMDnlGMYvBmM51/LzEaLQa5p6fV2Ipd4GAE4nmznew+4qprSwGudk3+IJw1sfk7qDwKzPEIVpvddaWYeShB8A22TpwWVAE5eR3M459AvUp8ubVW4RoDxd4Ka6gu1Fh31pAoGAYNPKJNCrdOsQH0nkH0Ld44cH6HD+zcZtoad2eQk0T2SPnKBsIS1S9W0adreOXUv1edO/hN3P/BcTuCaax7ijTscS2f0atc/NIa6LjBnK7oUBBzib9v21L72ZVZ5st4c/H7IzbQCXfS81489a7TTHP+e1HzS/XePftO0pAkr1GJ0="; String salt = "E4bvRDg"; System.out.println("公钥为:" + publicKey); String plaintext = "{'a':'1111','b':'2222'}"; String ciphertext = null; String sign = null; try { ciphertext = RSAUtil.encrypt(plaintext,publicKey); jsonData.put("data",ciphertext); System.out.println("第一步,使用对方的RSA公钥对明文进行加密,得到的密文进行base64编码,作为data:"+jsonData.get("data")); sign = SHA256Util.getSha256Str(ciphertext+salt); jsonData.put("sign",sign); System.out.println("第二步,将data的值加上约定好的盐,使用sha256算法进行签名,得到的签名是16进制的串,放到sign:"+jsonData.get("sign")); } catch (Exception e) { System.out.println("加密失败"); throw new RuntimeException(e); } System.out.println("得到的密文和签名为:"+jsonData); System.out.println("模拟另一方收到密文和签名之后.........."); System.out.println("私钥为:" + privateKey); String ciphertext1 = jsonData.get("data"); String sign1 = jsonData.get("sign"); String deciphering1 = null; if(sign1.equals(SHA256Util.getSha256Str(ciphertext1+salt))){ System.out.println("签名验证成功"); deciphering1 = RSAUtil.decrypt(ciphertext1,privateKey); System.out.printf("数据解密成功,data值为:"+deciphering1); }else{ System.out.println("签名验证失败,解密失败"); } }}
2. 一次一密+签名身份验证
生成一个对称算法秘钥,使用该秘钥对明文进行加密,然后将该秘钥使用对方的公钥进行加密,加密后编码,然后再对明文使用自己的私钥进行签名,因为私钥只有自己有,所以用自己的私钥签名后,对方使用你提供的公钥进行验签,就可以验证你的身份。
步骤:
1. 随机生成一个SM4密钥;
2. 使用SM4密钥加密密文,得到的密文进行base64编码,作为textToDecrypt
3. 将SM4密钥进行base64编码后,再使用对方的SM2公钥对这个base64串进行加密,得到的密文进行base64编码,作为keyCiphertext;
4. 对第二步的明文使用第三方sm2私钥签名,得到的签名进行base64编码后作为signature。
加密后json:
{ "textToDecrypt": "cU0ymFMho3HXmVq0hwDHvZg9oLsuZT19GLBKcvHzdZ4=", "signature": "MEUCIA8nO1g705B3zzmaYv7yK8jajz9r2fKuvSPZliY8k1xBAiEA0noAEiBos3fU7RwEt81jTjwlrQL+tbQfy77VK/h/ES4=", "keyCiphertext": "BJv91ULAd6fNchtKF/+b1JW2o7il0Hjbr6erQrY96S8QrDrkE7Y6EpgzM/eY1OumNsr6VbRuK0wx6yo8fq3QapYHZU5X1LzjgyMC+AiCnhzut5V3xxz4Yd0M1Zx2D4ljaOnJTDED4nS5kU2+C5VFF81DyzVDmWw6ZQ=="}
对方接收后,按照以上步骤再进行验签,解密等操作。
demo:GMSMTest
package GMSM;import java.util.HashMap;import java.util.Map;import java.util.Base64;public class GMSMTest { public static void main(String[] args) throws Exception {// 你这边拿到的sm2公私钥,对方的公钥和自己的私钥 String priK = "AMz5m/WOgpgYiXnlckC7+zdvZXnQYMBcWXt1n7khfdVO"; String otherPubK = "BJkVGU87s7xhLrFCI8MGPzp2X+lizte+2So52CQ1tVEDDqOe+Q4vdQXglV2JtIOZeWc/bE8Rzdo29svmTj8+7mA="; String messge = "{'aaa':'111','bbb':'222'}"; Map<String, String> jsonData = new HashMap<>(); byte[] keyB = SM4Util.generateKey(null); String key = Base64.getEncoder().encodeToString(keyB); String algorithmName = ("SM4/ECB/PKCS5PADDING"); System.out.println("第一步,随机生成一个SM4秘钥:"+key); System.out.println("选定算法:"+algorithmName); // 用随机生成的SM4秘钥加密 byte[] encryptMSG = SM4Util.encrypt(algorithmName, keyB, null, messge.getBytes()); jsonData.put("textToDecrypt",Base64.getEncoder().encodeToString(encryptMSG)); System.out.println("第二步,使用SM4密钥加密明文,得到的密文进行base64编码,作为textToDecrypt :"+jsonData.get("textToDecrypt")); // 将随机生成的SM4秘钥,使用对方的SM2公钥进行加密 String keyCiphertext = SM2Util.encryptBySM2(key, otherPubK); jsonData.put("keyCiphertext",keyCiphertext); System.out.println("第三步,将SM4密钥进行base64编码后,再使用对方的SM2公钥对这个base64串进行加密,得到的密文进行base64编码,作为 keyCiphertext;"+jsonData.get("keyCiphertext")); // 对明文使用自己的sm2私钥进行签名 如果有需要,可以加点盐 String signature = SM2Util.sign(priK,messge); jsonData.put("signature",signature); System.out.println("第四步,对第二步的明文使用对方sm2私钥签名,得到的签名进行base64编码后作为 signature:"+jsonData.get("signature")); System.out.println("完整json串:"+jsonData); // 模拟接到json传之后的处理 // 对方拿到的,你的公钥,和对方自己的私钥 String PubK = "BNTFr3oo1xm3biGoQQnk20QJfbFvK4HNFgs1DkZJuUT9pW8I7K0D9L9P4mW+tQhWwfJlD/Nsx3BY+oriF1B25bA="; String otherPriK = "I0sHzcDm+oS2FIAHFLxxkWJi1AbrHRxEYs1AxpZTlNw="; // 将sm4秘钥进行解密 String keyCiphertext1 = SM2Util.decryptBySM2(jsonData.get("keyCiphertext"), otherPriK); System.out.println("对方使用sm2私钥解密sm4秘钥:"+keyCiphertext1); // 通过sm4秘钥解密数据密文 byte[] textToDecrypts = SM4Util.decrypt(algorithmName, Base64.getDecoder().decode(keyCiphertext1), null, Base64.getDecoder().decode(jsonData.get("textToDecrypt").getBytes())); String textToDecryptsS = new String(textToDecrypts); System.out.println("对方使用sm4秘钥解密后的数据:"+ textToDecryptsS); // 验证签名 boolean proving = SM2Util.verify(PubK, textToDecryptsS, jsonData.get("signature")); System.out.println("验证签名是否通过:"+proving); }}
国密依赖pom
<dependencies> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>30.1-jre</version> </dependency> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.70</version> </dependency></dependencies>