国密SM4算法加密解密实现以及与Spring Security集成实现

Posted ricardo.M.Yu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了国密SM4算法加密解密实现以及与Spring Security集成实现相关的知识,希望对你有一定的参考价值。

目录

简介

项目集成

引入依赖

测试

编写 PasswordEncoder

定义SM4PasswordEncoder

配置文件定义

properties类

注册为Bean

密码加密


简介

项目中要求密码加密算法使用国家的密码标准SM4.0,下面来介绍下。

SM4算法是我国发布的商用密码算法中的分组密码算法,是一种迭代分组密码算法,由加解密算法和密钥扩展算法组成。其分组长度和密钥长度均为128比特,加密算法和密钥扩展算法迭代轮数均为32轮。SM4加解密过程的算法相同但是轮密钥的使用顺序相反。SM4算法具有安全高效的功能特点,在设计和实现方面具有一定的优势。

项目集成

在项目中登录以及鉴权使用的是Spring Security,与框架的集成如下。

引入依赖

首先需要引入如下两个依赖

<dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>$hutool.version</version>
        </dependency>
        <!-- 加密解密包 -->
        <dependency>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bcprov-jdk15to18</artifactId>
            <version>$bcprov.version</version>
        </dependency>

测试

工具类中可以使用如下方法自行测试加密效果

public static void main(String[] args) 
    try 

        //自定义秘钥
        String key = "6gsx@654hs68456!";
        String password="123456";
        SymmetricCrypto crypto = SmUtil.sm4(key.getBytes());
        String s2 = crypto.encryptHex(password);
        String s3 = crypto.decryptStr(s2);


        System.out.println(s2);
        System.out.println(s3);

        //用工具类生成,此处使用随机秘钥,加解密需要使用同一个示例才可得到原始密码
        SM4 sm4 = SmUtil.sm4();

     catch (Exception e) 
        throw new RuntimeException(e);
    

编写 PasswordEncoder

Spring Security默认支持的密码算法如下:

public static PasswordEncoder createDelegatingPasswordEncoder() 
		String encodingId = "bcrypt";
		Map<String, PasswordEncoder> encoders = new HashMap<>();
		encoders.put(encodingId, new BCryptPasswordEncoder());
		encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
		encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
		encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
		encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
		encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
		encoders.put("scrypt", new SCryptPasswordEncoder());
		encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
		encoders.put("SHA-256",
				new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
		encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
		encoders.put("argon2", new Argon2PasswordEncoder());
		return new DelegatingPasswordEncoder(encodingId, encoders);
	

在我们集成登录时候,会将登录的密码与用户的密码作比较,因此我们需要自定义一个新的PasswordEncoder来替换掉默认的。

定义SM4PasswordEncoder

public class SM4PasswordEncoder implements PasswordEncoder 

    private final SymmetricCrypto sm4;

    public SM4PasswordEncoder(String key) 
        SymmetricCrypto crypto = SmUtil.sm4(key.getBytes());
        this.sm4 = crypto;
    

    @Override
    public String encode(CharSequence rawPassword) 
        return sm4.encryptHex(rawPassword.toString());
    

    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) 
        return sm4.encryptHex(rawPassword.toString()).equals(encodedPassword);
    

matches方法即会在登录时调用此方法比较。

配置文件定义

因为我们需要自定义一个加密的秘钥,在实际项目中我们根据实际情况灵活配置,所以我们定义一个配置文件 application-sm4.yml

te
  system:
     sm4-key: z^cnmuop2372a375

注意: key的秘钥位数需要是 16因为   128位/8 = 16

properties类

@Data
@Component
@ConfigurationProperties(prefix = "te.system")
public class SystemEnvProperties 

   
    @NotNull
    @ApiModelProperty(notes = "sm4加密秘钥-需要16byte")
    private String sm4Key;

  

注册为Bean

   /**
     * 密码明文加密方式配置(使用国密SM4)
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(SystemEnvProperties envProperties) 
        return new SM4PasswordEncoder(envProperties.getSm4Key());
    

密码加密

在创建用户及修改密码时候,调用加密方法

  @Autowired
    private PasswordEncoder passwordEncoder;

userInfo.setPassword(passwordEncoder.encode(vo.getPassword()));

至此,集成完成

openssl 加解密以及国密算法

国密算法

国密即国家密码局认定的国产密码算法。主要有SM1,SM2,SM3,SM4。密钥长度和分组长度均为128位。

1、SM1 为对称加密。其加密强度与AES相当。该算法不公开,调用该算法时,需要通过加密芯片的接口进行调用。
2、SM2为非对称加密,基于ECC。该算法已公开。由于该算法基于ECC,故其签名速度与秘钥生成速度都快于RSA。ECC 256位(SM2采用的就是ECC 256位的一种)安全强度比RSA 2048位高,但运算速度快于RSA。
3、SM3 消息摘要。可以用MD5作为对比理解。该算法已公开。校验结果为256位,本身是散列算法,官方叫杂凑类算法,根据SHA256算法改进而来。
4、SM4 无线局域网标准的分组数据算法。对称加密,密钥长度和分组长度均为128位。

5、SM7适用于非接触式IC卡的对称算法,秘钥长度128bit
6、SM9是标识算法,支持加密、签名、交换。

国密芯片算法一般是对称算法,密钥不公开,当使用特定的芯片进行SM1或其他国密算法加密时,若用多个线程调用加密卡的API时,要考虑芯片对于多线程的支持情况。

生成sm2密钥对

openssl ecparam -genkey -name SM2 -out SM2PrivateKey.pem
openssl ec -in SM2PrivateKey.pem -pubout -out SM2PublicKey.pem

AES 加密

堆成算法比较成熟的是AES算法,AES加密数据块和密钥长度可以是128比特、192比特、256比特中的任意一个。实际上,国密算法一般都是根据这种算法改进产生的。

AES加密有很多轮的重复和变换。大致步骤如下:

1、密钥扩展(KeyExpansion),

2、初始轮(Initial Round),

3、重复轮(Rounds),每一轮又包括:SubBytes、ShiftRows、MixColumns、AddRoundKey,

4、最终轮(Final Round),最终轮没有MixColumns。

1、使用AES加解密数据

省略,这个比较简单

使用RSA加解密数据

RSA 主要是公钥和私钥的概念,产生一个私钥
$ openssl genrsa -out Key.pem -f4 2048
从私钥可以导出公钥
$ openssl rsa -in Key.pem -pubout -out Key_pub.pem

python 比较方便,使用python实例一下

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import padding

#加密函数
def encrypt(src_file_name, dst_file_name, public_key_file_name):
"""
对原始数据文件使用指定的公钥进行加密,并将加密输出到目标文件中
:param src_file_name: 原始数据文件
:param dst_file_name: 加密输出文件
:param public_key_file_name: 用于加密的公钥
:return: 加密结果的bytes数组
"""
#读取数据,需要加密的内容
data_file = open(src_file_name, 'rb')
data = data_file.read()
data_file.close()

#读取公钥文件
key_file = open(public_key_file_name, 'rb')
key_data = key_file.read()
key_file.close()

#加载公钥
public_key = serialization.load_pem_public_key(
key_data,
backend=default_backend()
)

#使用公钥对原始数据进行加密,使用PKCS#1 v1.5的填充方式
out_data = public_key.encrypt(
data,
padding.PKCS1v15()
)

#将加密结果输出到目标文件中
#write encrypted data
out_data_file = open(dst_file_name, 'wb')
out_data_file.write(out_data)
out_data_file.close()

# 返回加密结果
return out_data
# 解密函数
def decrypt(src_file_name, dst_file_name, private_key_file_name):
"""
对原始数据文件使用指定的私钥进行解密,并将结果输出到目标文件中
:param src_file_name: 原始数据文件
:param dst_file_name: 解密输出文件
:param private_key_file_name: 用于解密的私钥
:return: 解密结果的bytes数组
"""
# 读取原始数据
data_file = open(src_file_name, 'rb')
data = data_file.read()
data_file.close()

# 读取私钥数据
key_file = open(private_key_file_name, 'rb')
key_data = key_file.read()
key_file.close()

# 从私钥数据中加载私钥
private_key = serialization.load_pem_private_key(
key_data,
password=None,
backend=default_backend()
)

# 使用私钥对数据进行解密,使用PKCS#1 v1.5的填充方式
out_data = private_key.decrypt(
data,
padding.PKCS1v15()
)

# 将解密结果输出到目标文件中
out_data_file = open(dst_file_name, 'wb')
out_data_file.write(out_data)
out_data_file.close()

# 返回解密结果
return out_data

if __name__ == "__main__":
data_file_name = r'msg.bin'
encrypted_file_name = r'msg.bin.encrypted'
decrypted_file_name = r'msg.bin.decrypted'

private_key_file_name = r'Key.pem'
public_key_file_name = r'Key_pub.pem'

# 先对数据加密
data = encrypt(data_file_name, encrypted_file_name, public_key_file_name)
# 打印加密结果
print("encrypted data:")
dump_hex(data)

# 对数据进行解密
data = decrypt(encrypted_file_name, decrypted_file_name, private_key_file_name)
# 打印解密结果
print("decrypted data:")
dump_hex(data)

java AES

import java.security.Key;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

public class AESCoder 

   private static final String KEY_ALGORITHM = "AES";
   private static final String DEFAULT_CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";//默认的加密算法

   public static byte[] initSecretKey() 

       //返回生成指定算法密钥生成器的 KeyGenerator 对象
       KeyGenerator kg = null;
       try 
           kg = KeyGenerator.getInstance(KEY_ALGORITHM);
        catch (NoSuchAlgorithmException e) 
           e.printStackTrace();
           return new byte[0];
       
       //初始化此密钥生成器,使其具有确定的密钥大小
       //AES 要求密钥长度为 128
       kg.init(128);
       //生成一个密钥
       SecretKey  secretKey = kg.generateKey();
       return secretKey.getEncoded();
   

   private static Key toKey(byte[] key)
       //生成密钥
       return new SecretKeySpec(key, KEY_ALGORITHM);
   

   public static byte[] encrypt(byte[] data,Key key) throws Exception
       return encrypt(data, key,DEFAULT_CIPHER_ALGORITHM);
   

   public static byte[] encrypt(byte[] data,byte[] key) throws Exception
       return encrypt(data, key,DEFAULT_CIPHER_ALGORITHM);
   

   public static byte[] encrypt(byte[] data,byte[] key,String cipherAlgorithm) throws Exception
       //还原密钥
       Key k = toKey(key);
       return encrypt(data, k, cipherAlgorithm);
   

   public static byte[] encrypt(byte[] data,Key key,String cipherAlgorithm) throws Exception
       //实例化
       Cipher cipher = Cipher.getInstance(cipherAlgorithm);
       //使用密钥初始化,设置为加密模式
       cipher.init(Cipher.ENCRYPT_MODE, key);
       //执行操作
       return cipher.doFinal(data);
   

   public static byte[] decrypt(byte[] data,byte[] key) throws Exception
       return decrypt(data, key,DEFAULT_CIPHER_ALGORITHM);
   

   public static byte[] decrypt(byte[] data,Key key) throws Exception
       return decrypt(data, key,DEFAULT_CIPHER_ALGORITHM);
   

   public static byte[] decrypt(byte[] data,byte[] key,String cipherAlgorithm) throws Exception
       //还原密钥
       Key k = toKey(key);
       return decrypt(data, k, cipherAlgorithm);
   

   public static byte[] decrypt(byte[] data,Key key,String cipherAlgorithm) throws Exception
       //实例化
       Cipher cipher = Cipher.getInstance(cipherAlgorithm);
       //使用密钥初始化,设置为解密模式
       cipher.init(Cipher.DECRYPT_MODE, key);
       //执行操作
       return cipher.doFinal(data);
   

   private static String  showByteArray(byte[] data)
       if(null == data)
           return null;
       
       StringBuilder sb = new StringBuilder("");
       for(byte b:data)
           sb.append(b).append(",");
       
       sb.deleteCharAt(sb.length()-1);
       sb.append("");
       return sb.toString();
   

   public static void main(String[] args) throws Exception 
       byte[] key = initSecretKey();
       System.out.println("key:"+showByteArray(key));
       Key k = toKey(key); //生成秘钥
       String data ="AES数据";
       System.out.println("加密前数据: string:"+data);
       System.out.println("加密前数据: byte[]:"+showByteArray(data.getBytes()));
       System.out.println();
       byte[] encryptData = encrypt(data.getBytes(), k);//数据加密
       System.out.println("加密后数据: byte[]:"+showByteArray(encryptData));
//       System.out.println("加密后数据: hexStr:"+Hex.encodeHexStr(encryptData));
       System.out.println();
       byte[] decryptData = decrypt(encryptData, k);//数据解密
       System.out.println("解密后数据: byte[]:"+showByteArray(decryptData));
       System.out.println("解密后数据: string:"+new String(decryptData));
   

国密加密过程

加解密都使用openssl 去做,openssl封装比较好,可以直接使用

unsigned char* t, *hm;
    BIGNUM* rand;
    EC_POINT* rG, *rK;
    BIGNUM *rKx, *rKy, *rGx, *rGy;

    unsigned char bK[65] = 0;
    unsigned char C3[33] = 0;

    rG = EC_POINT_new(this->mGroup);
    rK = EC_POINT_new(this->mGroup);
    rand = BN_new();

    BN_rand_range(rand, this->z);
    EC_POINT_mul(this->mGroup, rG, NULL,
        this->mGP, rand, this->ctx);

    rGx = BN_new();
    rGy = BN_new();
    if(!EC_POINT_get_affine_coordinates_GFp(this->mGroup, 
        rG, rGx, rGy, this->ctx))
    
        return -3;
    

    BN_bn2bin(rGx, pd);
    BN_bn2bin(rGy, &pd[32]);

    //[k]PB=(x2,y2)
    EC_POINT_mul(this->mGroup, rK, NULL, 
        EC_KEY_get0_public_key(this->mKey), 
        rand, this->ctx);

    rKx = BN_new();
    rKy = BN_new();
    if(!EC_POINT_get_affine_coordinates_GFp(this->mGroup, 
        rK, rKx, rKy, this->ctx))
    
        return -3;
    

    //t=KDF(x2||y2, klen)   
    BN_bn2bin(rKx, bK);
    BN_bn2bin(rKy, &bK[32]);

    t = new BYTE[elen + 1];
    memset(t, 0, elen + 1);

    this->mKDF(bK, 64, elen, t);

    for (int i = elen; i--;)
    
        t[i] = t[i]^pe[i];
    

    //C3 = Hash(x2||M||y2)
    hm = new unsigned char[elen + 65];
    memset(hm, 0, elen + 65);

    memcpy(hm, bK, 32);
    memcpy(&hm[32], pe, elen);
    memcpy(&hm[elen + 32], &bK[32], 32);

    hash(hm, elen + 64, C3, "sha256");

    //C = C1||C2||C3
    memcpy(&pd[64], t,  elen);
    memcpy(&pd[64 + elen], C3, 32);

    delete[] t;
    delete[] hm;

    t = NULL;
    hm = NULL;

    EC_POINT_free(rG);
    EC_POINT_free(rK);

    return 0;

国密解密过程

unsigned char* t, *c2, *hm;
    unsigned char bC1x[65] = 0;
    unsigned char bC1y[65] = 0;
    unsigned char bK[65] = 0;
    unsigned char u[33] = 0; 

    unsigned int mlen, hm_len;  

    EC_POINT *rG, *rK;
    BIGNUM *C1x, *C1y, *rKx, *rKy;

    //取出rG
    C1x = BN_new();
    C1y = BN_new();

    memcpy(&bC1x[32], pe, 32);
    memcpy(&bC1y[32], &pe[32], 32);

    BN_bin2bn(bC1x, 64, C1x);
    BN_bin2bn(bC1y, 64, C1y);

    rG = EC_POINT_new(this->mGroup);
    if(!EC_POINT_set_affine_coordinates_GFp(this->mGroup, 
        rG, C1x, C1y, this->ctx))
    
        EC_POINT_free(rG);
        return -1;
    

    //求得rK
    rK = EC_POINT_new(this->mGroup);
    EC_POINT_mul(this->mGroup, rK, NULL, rG, 
        EC_KEY_get0_private_key(this->mKey), 
        this->ctx);

    rKx = BN_new();
    rKy = BN_new();
    if(!EC_POINT_get_affine_coordinates_GFp(this->mGroup, 
        rK, rKx, rKy, this->ctx))
    
        EC_POINT_free(rG);
        EC_POINT_free(rK);
        return -2;
    

    //求取hv 解密 
    BN_bn2bin(rKx, bK);
    BN_bn2bin(rKy, &bK[32]);

    mlen = elen - 96;

    c2 = new unsigned char[mlen + 1];
    memset(c2, 0, mlen + 1);
    memcpy(c2以上是关于国密SM4算法加密解密实现以及与Spring Security集成实现的主要内容,如果未能解决你的问题,请参考以下文章

国密SM4分组密码算法(对称加密)的JS和JAVA类库

国密gmssl介绍(SM2SM3SM4算法)

国密算法说明SM2SM3SM4

密码技术--国密SM4分组密码算法及Go语言应用

openssl 加解密以及国密算法

应用国产高安全性加密芯片RJWF303助力无线门铃,支持对称加密算法DES3DES,国密SM4