国密算法SM2、SM3的使用

一、SM2、SM3介绍:

1. SM2是非对称加密算法

        它是基于椭圆曲线密码的公钥密码算法标准,其秘钥长度256bit,包含数字签名、密钥交换和公钥加密,用于替换RSA/DH/ECDSA/ECDH等国际算法。可以满足电子认证服务系统等应用需求,由国家密码管理局于2010年12月17号发布。

2.SM3是一种密码杂凑算法

        用于替代MD5/SHA-1/SHA-2等国际算法,适用于数字签名和验证、消息认证码的生成与验证以及随机数的生成,可以满足电子认证服务系统等应用需求,于2010年12月17日发布。它是在SHA-256基础上改进实现的一种算法,采用Merkle-Damgard结构,消息分组长度为512bit,输出的摘要值长度为256bit。

二、SM2、SM3在项目中的使用:

        SM2用于前后端密码加密传输,SM3用于数据库密码加密。

三、SM2在项目中具体使用

1. 前端部分关键代码

1.1 npm安装国密

npm install --save sm-crypto

1.2 vue中引用国密

const sm2 = require('sm-crypto').sm2;  // 引用国密
const cipherMode = 1;  // 密文排列方式0-C1C2C3;1-C1C3C2

1.3 data中定义公钥public

data() {
    return {
      codeUrl: "",
      loginForm: {
        username: "admin",
        password: "admin",
        rememberMe: false,
        code: "",
        uuid: "",
        pubKey:"", // 自己的公钥,这里后端返回后赋值
      },
    }
}

1.4将密码加密后传到后端

const passwordEn = sm2.doEncrypt(password,pubKey,cipherMode);

2.后端关键代码

2.1 依赖 pom.xml

        
            org.bouncycastle
            bcprov-jdk15on
            1.57
        
        
            org.bouncycastle
            bcprov-ext-jdk15on
            1.57
        

2.2 扩展类 SM2EngineExtend.java

/**
 * 对org.bouncycastle:bcprov-jdk15on:1.57扩展
 * 
BC库加密结果是按C1C2C3,国密标准是C1C3C2(加密芯片也是这个排列), *
本扩展主要实现加密结果排列方式可选 */ public class SM2EngineExtend { private final Digest digest; /**是否为加密模式*/ private boolean forEncryption; private ECKeyParameters ecKey; private ECDomainParameters ecParams; private int curveLength; private SecureRandom random; /**密文排序方式*/ private int cipherMode; /**BC库默认排序方式-C1C2C3*/ public static int CIPHERMODE_BC = 0; /**国密标准排序方式-C1C3C2*/ public static int CIPHERMODE_NORM = 1; public SM2EngineExtend() { this(new SM3Digest()); } public SM2EngineExtend(Digest digest) { this.digest = digest; } /** * 设置密文排序方式 * @param cipherMode */ public void setCipherMode(int cipherMode){ this.cipherMode = cipherMode; } /** * 默认初始化方法,使用国密排序标准 * @param forEncryption - 是否以加密模式初始化 * @param param - 曲线参数 */ public void init(boolean forEncryption, CipherParameters param) { init(forEncryption, CIPHERMODE_NORM, param); } /** * 默认初始化方法,使用国密排序标准 * @param forEncryption 是否以加密模式初始化 * @param cipherMode 加密数据排列模式:1-标准排序;0-BC默认排序 * @param param 曲线参数 */ public void init(boolean forEncryption, int cipherMode, CipherParameters param) { this.forEncryption = forEncryption; this.cipherMode = cipherMode; if (forEncryption) { ParametersWithRandom rParam = (ParametersWithRandom) param; ecKey = (ECKeyParameters) rParam.getParameters(); ecParams = ecKey.getParameters(); ECPoint s = ((ECPublicKeyParameters) ecKey).getQ().multiply(ecParams.getH()); if (s.isInfinity()) { throw new IllegalArgumentException("invalid key: [h]Q at infinity"); } random = rParam.getRandom(); } else { ecKey = (ECKeyParameters) param; ecParams = ecKey.getParameters(); } curveLength = (ecParams.getCurve().getFieldSize() + 7) / 8; } /** * 加密或解密输入数据 * @param in * @param inOff * @param inLen * @return * @throws InvalidCipherTextException */ public byte[] processBlock( byte[] in, int inOff, int inLen) throws InvalidCipherTextException { if (forEncryption) { // 加密 return encrypt(in, inOff, inLen); } else { return decrypt(in, inOff, inLen); } } /** * 加密实现,根据cipherMode输出指定排列的结果,默认按标准方式排列 * @param in * @param inOff * @param inLen * @return * @throws InvalidCipherTextException */ private byte[] encrypt(byte[] in, int inOff, int inLen) throws InvalidCipherTextException { byte[] c2 = new byte[inLen]; System.arraycopy(in, inOff, c2, 0, c2.length); byte[] c1; ECPoint kPB; do { BigInteger k = nextK(); ECPoint c1P = ecParams.getG().multiply(k).normalize(); c1 = c1P.getEncoded(false); kPB = ((ECPublicKeyParameters) ecKey).getQ().multiply(k).normalize(); kdf(digest, kPB, c2); } while (notEncrypted(c2, in, inOff)); byte[] c3 = new byte[digest.getDigestSize()]; addFieldElement(digest, kPB.getAffineXCoord()); digest.update(in, inOff, inLen); addFieldElement(digest, kPB.getAffineYCoord()); digest.doFinal(c3, 0); if (cipherMode == CIPHERMODE_NORM){ return Arrays.concatenate(c1, c3, c2); } return Arrays.concatenate(c1, c2, c3); } /** * 解密实现,默认按标准排列方式解密,解密时解出c2部分原文并校验c3部分 * @param in * @param inOff * @param inLen * @return * @throws InvalidCipherTextException */ private byte[] decrypt(byte[] in, int inOff, int inLen) throws InvalidCipherTextException { byte[] c1 = new byte[curveLength * 2 + 1]; System.arraycopy(in, inOff, c1, 0, c1.length); ECPoint c1P = ecParams.getCurve().decodePoint(c1); ECPoint s = c1P.multiply(ecParams.getH()); if (s.isInfinity()) { throw new InvalidCipherTextException("[h]C1 at infinity"); } c1P = c1P.multiply(((ECPrivateKeyParameters) ecKey).getD()).normalize(); byte[] c2 = new byte[inLen - c1.length - digest.getDigestSize()]; if (cipherMode == CIPHERMODE_BC) { System.arraycopy(in, inOff + c1.length, c2, 0, c2.length); }else{ // C1 C3 C2 System.arraycopy(in, inOff + c1.length + digest.getDigestSize(), c2, 0, c2.length); } kdf(digest, c1P, c2); byte[] c3 = new byte[digest.getDigestSize()]; addFieldElement(digest, c1P.getAffineXCoord()); digest.update(c2, 0, c2.length); addFieldElement(digest, c1P.getAffineYCoord()); digest.doFinal(c3, 0); int check = 0; // 检查密文输入值C3部分和由摘要生成的C3是否一致 if (cipherMode == CIPHERMODE_BC) { for (int i = 0; i != c3.length; i++) { check |= c3[i] ^ in[c1.length + c2.length + i]; } }else{ for (int i = 0; i != c3.length; i++) { check |= c3[i] ^ in[c1.length + i]; } } clearBlock(c1); clearBlock(c3); if (check != 0) { clearBlock(c2); throw new InvalidCipherTextException("invalid cipher text"); } return c2; } private boolean notEncrypted(byte[] encData, byte[] in, int inOff) { for (int i = 0; i != encData.length; i++) { if (encData[i] != in[inOff]) { return false; } } return true; } private void kdf(Digest digest, ECPoint c1, byte[] encData) { int ct = 1; int v = digest.getDigestSize(); byte[] buf = new byte[digest.getDigestSize()]; int off = 0; for (int i = 1; i <= ((encData.length + v - 1) / v); i++) { addFieldElement(digest, c1.getAffineXCoord()); addFieldElement(digest, c1.getAffineYCoord()); digest.update((byte) (ct >> 24)); digest.update((byte) (ct >> 16)); digest.update((byte) (ct >> 8)); digest.update((byte) ct); digest.doFinal(buf, 0); if (off + buf.length < encData.length) { xor(encData, buf, off, buf.length); } else { xor(encData, buf, off, encData.length - off); } off += buf.length; ct++; } } private void xor(byte[] data, byte[] kdfOut, int dOff, int dRemaining) { for (int i = 0; i != dRemaining; i++) { data[dOff + i] ^= kdfOut[i]; } } private BigInteger nextK() { int qBitLength = ecParams.getN().bitLength(); BigInteger k; do { k = new BigInteger(qBitLength, random); } while (k.equals(ECConstants.ZERO) || k.compareTo(ecParams.getN()) >= 0); return k; } private void addFieldElement(Digest digest, ECFieldElement v) { byte[] p = BigIntegers.asUnsignedByteArray(curveLength, v.toBigInteger()); digest.update(p, 0, p.length); } /** * clear possible sensitive data */ private void clearBlock( byte[] block) { for (int i = 0; i != block.length; i++) { block[i] = 0; } } }

2.3 SM2加解密工具类Sm2Util.java

public class Sm2Util {
    /**
     * 获取sm2密钥对
     * BC库使用的公钥=64个字节+1个字节(04标志位),BC库使用的私钥=32个字节
     * SM2秘钥的组成部分有 私钥D 、公钥X 、 公钥Y , 他们都可以用长度为64的16进制的HEX串表示,
     * 
SM2公钥并不是直接由X+Y表示 , 而是额外添加了一个头,当启用压缩时:公钥=有头+公钥X ,即省略了公钥Y的部分 * * @param compressed 是否压缩公钥(加密解密都使用BC库才能使用压缩) * @return */ public static String[] getSm2Keys(boolean compressed) { //获取一条SM2曲线参数 X9ECParameters sm2ECParameters = GMNamedCurves.getByName(Constant.CRYPTO_NAME_SM2); //构造domain参数 ECDomainParameters domainParameters = new ECDomainParameters(sm2ECParameters.getCurve(), sm2ECParameters.getG(), sm2ECParameters.getN()); //1.创建密钥生成器 ECKeyPairGenerator keyPairGenerator = new ECKeyPairGenerator(); //2.初始化生成器,带上随机数 try { keyPairGenerator.init(new ECKeyGenerationParameters(domainParameters, SecureRandom.getInstance("SHA1PRNG"))); } catch (NoSuchAlgorithmException e) { System.out.println(e.getMessage()); } //3.生成密钥对 AsymmetricCipherKeyPair asymmetricCipherKeyPair = keyPairGenerator.generateKeyPair(); ECPublicKeyParameters publicKeyParameters = (ECPublicKeyParameters) asymmetricCipherKeyPair.getPublic(); ECPoint ecPoint = publicKeyParameters.getQ(); // 把公钥放入map中,默认压缩公钥 // 公钥前面的02或者03表示是压缩公钥,04表示未压缩公钥,04的时候,可以去掉前面的04 String publicKey = Hex.toHexString(ecPoint.getEncoded(compressed)); ECPrivateKeyParameters privateKeyParameters = (ECPrivateKeyParameters) asymmetricCipherKeyPair.getPrivate(); BigInteger intPrivateKey = privateKeyParameters.getD(); // 把私钥放入map中 String privateKey = intPrivateKey.toString(16); String[] KeyPairOfString = new String[2]; KeyPairOfString[0] = publicKey; KeyPairOfString[1] = privateKey; return KeyPairOfString; } /** * SM2加密算法 * @param publicKey 公钥 * @param data 待加密的数据 * @return 密文,BC库产生的密文带由04标识符,与非BC库对接时需要去掉开头的04 */ public static String encrypt(String publicKey, String data){ // 按国密排序标准加密 return encrypt(publicKey, data, SM2EngineExtend.CIPHERMODE_NORM); } /** * SM2加密算法 * @param publicKey 公钥 * @param data 待加密的数据 * @param cipherMode 密文排列方式0-C1C2C3;1-C1C3C2; * @return 密文,BC库产生的密文带由04标识符,与非BC库对接时需要去掉开头的04 */ public static String encrypt(String publicKey, String data, int cipherMode) { // 获取一条SM2曲线参数 X9ECParameters sm2ECParameters = GMNamedCurves.getByName(Constant.CRYPTO_NAME_SM2); // 构造ECC算法参数,曲线方程、椭圆曲线G点、大整数N ECDomainParameters domainParameters = new ECDomainParameters(sm2ECParameters.getCurve(), sm2ECParameters.getG(), sm2ECParameters.getN()); //提取公钥点 ECPoint pukPoint = sm2ECParameters.getCurve().decodePoint(Hex.decode(publicKey)); // 公钥前面的02或者03表示是压缩公钥,04表示未压缩公钥, 04的时候,可以去掉前面的04 ECPublicKeyParameters publicKeyParameters = new ECPublicKeyParameters(pukPoint, domainParameters); SM2EngineExtend sm2Engine = new SM2EngineExtend(); // 设置sm2为加密模式 sm2Engine.init(true, cipherMode, new ParametersWithRandom(publicKeyParameters, new SecureRandom())); byte[] arrayOfBytes = null; try { byte[] in = data.getBytes(); arrayOfBytes = sm2Engine.processBlock(in, 0, in.length); } catch (Exception e) { System.out.println(e.getMessage()); } return Hex.toHexString(arrayOfBytes); } /** * SM2解密算法 * @param privateKey 私钥 * @param cipherData 密文数据 * @return */ public static String decrypt(String privateKey, String cipherData) { // // 按国密排序标准解密 return decrypt(privateKey, cipherData, SM2EngineExtend.CIPHERMODE_NORM); } /** * SM2解密算法 * @param privateKey 私钥 * @param cipherData 密文数据 * @param cipherMode 密文排列方式0-C1C2C3;1-C1C3C2; * @return */ public static String decrypt(String privateKey, String cipherData, int cipherMode) { // 使用BC库加解密时密文以04开头,传入的密文前面没有04则补上 if (!cipherData.startsWith("04")) { cipherData = "04" + cipherData; } byte[] cipherDataByte = Hex.decode(cipherData); //获取一条SM2曲线参数 X9ECParameters sm2ECParameters = GMNamedCurves.getByName(Constant.CRYPTO_NAME_SM2); //构造domain参数 ECDomainParameters domainParameters = new ECDomainParameters(sm2ECParameters.getCurve(), sm2ECParameters.getG(), sm2ECParameters.getN()); BigInteger privateKeyD = new BigInteger(privateKey, 16); ECPrivateKeyParameters privateKeyParameters = new ECPrivateKeyParameters(privateKeyD, domainParameters); SM2EngineExtend sm2Engine = new SM2EngineExtend(); // 设置sm2为解密模式 sm2Engine.init(false, cipherMode, privateKeyParameters); String result = ""; try { byte[] arrayOfBytes = sm2Engine.processBlock(cipherDataByte, 0, cipherDataByte.length); return new String(arrayOfBytes); } catch (Exception e) { System.out.println(e.getMessage()); } return result; } /** * 私钥签名 * * @param data 未加密的密码明文 * @param privateKey * @return * @throws Exception */ public static byte[] signByPrivateKey(byte[] data, PrivateKey privateKey) throws Exception { Signature sig = Signature.getInstance(GMObjectIdentifiers.sm2sign_with_sm3.toString(), BouncyCastleProvider.PROVIDER_NAME); sig.initSign(privateKey); sig.update(data); byte[] ret = sig.sign(); return ret; } /** * 公钥验签 * * @param data 未加密的密码明文 * @param publicKey * @param signature 签名 * @return * @throws Exception */ public static boolean verifyByPublicKey(byte[] data, PublicKey publicKey, byte[] signature) throws Exception { Signature sig = Signature.getInstance(GMObjectIdentifiers.sm2sign_with_sm3.toString(), BouncyCastleProvider.PROVIDER_NAME); sig.initVerify(publicKey); sig.update(data); boolean ret = sig.verify(signature); return ret; } }

2.4 测试类 EncryptAndDecryptTest.java

@SpringBootTest
public class EncryptAndDecryptTest {

    private String pwd = "123456";

    @Test
    void testEncryptAndDecrypt(){
        System.out.println("未加密密码:"+pwd);
        String[] sm2Keys = Sm2Util.getSm2Keys(false);
        String pubKey = sm2Keys[0];
        System.out.println("公钥:"+pubKey);
        String priKey = sm2Keys[1];
        System.out.println("私钥:"+priKey);

        // 国密加密
        String encrypt = Sm2Util.encrypt(pubKey, pwd);
        System.out.println("国密加密后密码:"+encrypt);

        // 国密解密
        String decrypt = Sm2Util.decrypt(priKey, encrypt);
        System.out.println("国密解密后密码:"+decrypt);

        // BC库加密
        String encrypt1 = Sm2Util.encrypt(pubKey, pwd, 1);
        System.out.println("BC加密:"+encrypt1);
        String decrypt1 = Sm2Util.decrypt(priKey, encrypt1, 1);
        System.out.println("BC解密:"+decrypt1);

    }
}

2.5 测试结果

未加密密码:123456
公钥:04bd133a440cd5ebaeeb68c958953a7794134c3992d2c3fd006571c7f1c7b3ebef484595ecd298d6c22fd1c0f84ba7ca8595761c271c959dc1370d7528c55b1ccb
私钥:835b53881c3e391ba430022119bf6b0f7fd7764ec9808d305ea5b620c531363e
国密加密后密码:04795bd0129b2b08891eb1b24567b5d026f3f56e42bee356dc120fc5bf699baac5b818ab2f3086791ee8bab8868a301e9e2b2884f2ef67ab6b615b6fe235f7c204ad520dfcd82c945420979d12accf014d4fe75724987b9ae8d96a5e1099af45b0ac970eceac9e
国密解密后密码:123456
BC加密:0464fcab91fa522f877f45c204e1043f3c9124d290f4b68c6c1bd5f420ce184c560d2654d781924b0d0d24affe32aac569f7c12a0c951e02478c50cf6e0bf241f5d225178c7c35a2cd5aa39d2381e27e74701b9c6efd439882775a33d1ce2367c51c2669f248d7
BC解密:123456

四、SM3在项目中具体使用

        使用hutool工具类

1. 依赖 pom.xml

        
            cn.hutool
            hutool-all
            5.1.4
        
        
        
            org.bouncycastle
            bcprov-jdk15to18
            1.69
        

2. 测试类 Sm3Test.java

@SpringBootTest
public class Sm3Test {

    @Test
    void encryTest(){
        String encryptStr = SmUtil.sm3("123");
        System.out.println("未加密的明文:123");
        System.out.println("SM3加密后的密文:"+encryptStr);
    }
}

3. 测试结果

未加密的明文:123
SM3加密后的密文:6e0f9e14344c5406a0cf5a3b4dfb665f87f4a771a31f7edbb5c72874a32b2957

借鉴博客:国产加密实际运用:使用SM3加盐存储密码,并且使用SM2进行登录认证_MrZhouGx的博客-CSDN博客_sm3 加盐

你可能感兴趣的:(登录,算法,java,vue)