使用加密狗的国密算法实现B/S架构数字信封

一、程序逻辑

使用加密狗的国密算法实现B/S架构数字信封_第1张图片
加密-上传
  1. 浏览器发起A用户的上传请求
  2. 服务端生成会话密钥Ks,用A用户的公钥Ka进行SM2加密为密钥Ks’,发给浏览器
  3. 浏览器收到加密后的会话 密钥Ks’,用A用户的私钥Ka’进行SM2解密得到会话密钥Ks
  4. 浏览器使用会话密钥Ks,对上传文件进行SM4加密得到密文M’,发给服务端
  5. 服务端保存密文M’以及会话密钥Ks

二、技术实现

2.1 密钥传输部分(步骤1-3)

  1. SM2证书解析
    首先测试浏览器端加密狗ocx插件,可以使用ocx获取证书。


    使用加密狗的国密算法实现B/S架构数字信封_第2张图片
    加密狗ocx测试页

    经厂家确认,证书格式为X.509。将证书直接写在服务端代码内,尝试解析证书并获取公钥Ka。代码如下:

// 证书
String cert64 = "MIIBvzCCAWa…F2yvFQ==";
// base64解码
byte[] certBytes = Base64.getDecoder().decode(cert64);
InputStream inStream = new ByteArrayInputStream(certBytes);
CertificateFactory cf = CertificateFactory.getInstance("X.509");
// x509解析
X509Certificate cert = (X509Certificate)cf.generateCertificate(inStream);
// 打印证书到期时间
System.out.println(cert.getNotAfter());

运行后报错:

Caused by: java.io.IOException: Unknown named curve: 1.2.156.10197.1.301
    at sun.security.ec.ECParameters.engineInit(ECParameters.java:143)
    at java.security.AlgorithmParameters.init(AlgorithmParameters.java:293)
    at sun.security.x509.AlgorithmId.decodeParams(AlgorithmId.java:132)
    at sun.security.x509.AlgorithmId.(AlgorithmId.java:114)
    at sun.security.x509.AlgorithmId.parse(AlgorithmId.java:372)
    at sun.security.x509.X509Key.parse(X509Key.java:168)
    at sun.security.x509.CertificateX509Key.(CertificateX509Key.java:75)
    at sun.security.x509.X509CertInfo.parse(X509CertInfo.java:667)
    at sun.security.x509.X509CertInfo.(X509CertInfo.java:167)

上网搜索得知:国密证书使用了自有的椭圆曲线,所以无法使用JDK自带的java.security解析证书(http://www.jonllen.com/jonllen/work/162.aspx)。经过调研,需要引入BouncyCastle,BC库支持国密算法。参考这篇文章(https://blog.csdn.net/jinhill/article/details/9996117)修改代码,在pom.xml增加BC库依赖:



   org.bouncycastle
   bcprov-jdk15on
   1.61

修改测试代码为:

// 引入BC库
Security.addProvider(new BouncyCastleProvider());
// 使用BC解析X.509证书
CertificateFactory cf = CertificateFactory.getInstance("X.509", "BC");

经测试,可以正常读取证书内容。

  1. SM2公钥加密
    参考(https://blog.csdn.net/pridas/article/details/86118774)编写加密代码如下:
// 测试会话密钥
String ks = "666666";
byte[] data = ks.getBytes();
// 获取A的公钥Ka
BCECPublicKey publicKey = (BCECPublicKey) cert.getPublicKey();
// 加密引擎
SM2Engine sm2Engine = new SM2Engine();
// 加密参数
X9ECParameters sm2p256v1 = GMNamedCurves.getByName("sm2p256v1");
ECDomainParameters parameters = new ECDomainParameters(sm2p256v1.getCurve(), sm2p256v1.getG(), sm2p256v1.getN());
// 引擎初始化
sm2Engine.init(true, new ParametersWithRandom(new ECPublicKeyParameters(publicKey.getQ(), parameters), new SecureRandom()));
// 加密
byte[] encrypt = sm2Engine.processBlock(data, 0, data.length);
// base64编码
String encrypt64 = Base64.getEncoder().encodeToString(encrypt);
System.out.println(encrypt64);

可以生成加密后的密文,但尝试使用ocx插件进行解密时,解密失败。


使用加密狗的国密算法实现B/S架构数字信封_第3张图片
解密失败

参考这篇文章所述,考虑密文组织顺序,旧标准:C1+C2+C3,新标准C1+C3+C2。增加密文结果调整函数,调整密文组织顺序。

/**
 * 安全芯片SM2加密处理的密文顺序是 C1+C3+C2  而BC库加密出来的顺序是C1+C2+C3
 * 此方法在加密后调用,将结果转化为 C1+C3+C2
 */
static byte[] changeC123toC132(byte[] src) {
    //sm2p256v1的这个固定65。可看GMNamedCurves、ECCurve代码。
    final int c1len = (sm2p256v1.getCurve().getFieldSize() + 7) / 8 * 2 + 1;
    //new SM3Digest().getDigestSize();
    final int c3len = 32;
    byte[] dst = new byte[src.length];
    System.arraycopy(src, 0, dst, 0, c1len); //c1
    System.arraycopy(src, src.length - c3len, dst, c1len, c3len); //c3
    System.arraycopy(src, c1len, dst, c1len + c3len, src.length - c1len - c3len); //c2
    return dst;
}

再次尝试使用ocx插件进行解密,仍失败。比对ocx通过加密狗生成的密文与代码生成的密文,发现ocx的比代码生成的密文短一个字节。上网搜索问题原因,终于找到这篇(https://my.oschina.net/547217475/blog/2252583)。通过debug,发现加密后密文的第一个字节的确是04。

加密后数据

增加一个函数,去除密文的第一个字节。

/**
 * 安全芯片在C1数据最前面没有04,而BC库加密出来的数据前面有04
 * 去除BC加密后C1数据的第一个字节
 */
static byte[] removeFirstByte(byte[] src) {
    byte[] dst = new byte[src.length - 1];
    System.arraycopy(src, 1, dst, 0, dst.length);
    return dst;
}

经测试,ocx可以正确解密获得服务器端生成的会话密钥ks。

你可能感兴趣的:(使用加密狗的国密算法实现B/S架构数字信封)