国密加密算法学习之SM4

国密算法SM4

本人数学渣渣 , 算法原理也是看的别人分析的: 可见相关博客: https://www.cnblogs.com/11sgXL/p/13626483.html?ivk_sa=1024320u

1.导包

pom引入 : 
<!-- SM国密加密 --JDK8 -->
<dependency>
   <groupId>org.bouncycastle</groupId>
   <artifactId>bcprov-jdk15on</artifactId>
   <version>1.56</version>
</dependency>
 

 2.定义算法名枚举

package com.cloudtravel.common.smencrypt;

/**
 * @description: 加密算法名枚举
 * @author: walker
 * @DATE: 2022/9/4
 */
public enum AlgorithmNameEnums {

    SM_4("SM4" , "SM4加密") ,

    SM_3("SM3" , "SM3加密");

    private String name;

    private String desc;

    AlgorithmNameEnums(String name, String desc) {
        this.name = name;
        this.desc = desc;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }
}

3.定义分组加密模式

package com.cloudtravel.common.smencrypt;

/**
 * @description: 分组加密模式枚举
 * 分组:密码加解密模式:大类有:分组密码和流密码两种.
 *      分组密码[BlockCipher]:每次处理特定的数据块.[block],其中一个分组的比特数就成为分组长度[block_size]
 *          例如:DES和SM4都是64bits[8字节].AES可以128/192/256几种选择.这类算法一次只加密对应块大小的明文,
 *          并生成对应块大小的密文.
 *      流密码:是对数据进行连续处理的一类密码算法 . 一般以1bits / 8bits /32bits等为单位进行加密和解密.
 *          这里的分组加密模式就是分组密码.
 * 模式:分组面只能对明文进行固定长度分组进行加密.当明文被拆分成多个分组时,就需要对分组密码进行迭代,便于将所有明文都加密.
 *     而迭代的方法就成为分组密码模式.也就是对分组密码进行加密的方式.
 * @author: walker
 * @DATE: 2022/9/4
 */
public enum AlgorithmModeEnums {

    /** ECB:[Electronic CodeBook Mode]:电子密码本模式
     *  其加密方式为:填充后的各分组各自加密后密文拼接则为输出结果.相同分组的明文加密后的结果相同.因此也称之为密码本加密.
     *  攻击时只需按固定长度密文打乱顺序,解密时的明文顺序就会变化.风险较大
     *  使用分析: 支持并行计算.简单快速 . 但因为明文的重复会在密文中体现出来 . 可攻击性高 . 需要填充 */
    ECB("ECB" , false ,"[Electronic CodeBook Mode]电子密码本模式") ,

    /**
     * CBC:[Cipher Block Chaining mode]:密码分组链接模式
     * 将前密文分组和当前明文分组通过异或处理后再进行加密的.这样就避免了ECB模式的明文分组和密文分组一一对应的风险
     * 因为它的明文分组和密文分组依次进行运算相关联 . 所以这里称之为密码链接模式
     * 第一个明文分组加密时因为没有前一个密文分组 , 会准备一个长度为分组的二进制序列代码需要的密文分组作为初始化向量
     * 解密时:则逆向处理,先将当前密文分组进行解密,再和上一个密文进行异或处理,即得到当前分组的明文.依次而行.
     * 使用分析 : 明文重复不会体现在密文中. 仅解密时支持并行计算 . 需要填充
     * */
    CBC("CBC" , true , "[Cipher Block Chaining Mode]密码分组链接模式"),

    /**
     * CFB:[Cipher FeedBack mode]:密文反馈模式
     * 区别于CBC的地方在于,初始化向量和密钥进行加密后和明文分组进行异或处理直接得到密文分组,然后密文分组再加密后和后续的明文分组进行异或处理,
     * 依次处理.相比而言,每次异或之前上一组的密文分组[或初始化向量]会先进行加密.再进行异或.
     * CFB模式中由密码算法生成的二进制序列类似于时一种流式的数据处理
     * 解密时:则逆向处理,先将上一个密文分组进行加密[注意这里依然是加密],再进行异或处理,即得到当前分组的明文.依次而行.
     * 使用分析 : 不需要填充,解密时支持并行计算 .
     * 区别于CBC的点 : 在于先对初始化向量或者前一组密文进行加密再进行异或处理
     * */
    CFB("CFB" , true , "[Cipher FeedBack Mode]密文反馈模式"),

    /**
     * OFB:[OutPut FeedBack mode]:输出反馈模式
     * 加密时: 初始化向量先进行加密,和第一个明文分组异或处理后得到当前明文分组对应的密文分组.
     * 下一个明文分组则 :再次对加密后的向量密文进行加密,再和明文分组进行异或处理得到密文分组.区别于CFB的是每次加密对象
     * 都是上一个初始化向量加密后的密文.而CFB则是对密文分组进行加密
     * 解密时:也是对初始化向量进行加密.再和密文分组进行异或处理得到明文分组 , 依次而行
     * 类似于于流密码
     * 使用分析 : 不需要填充,加密解密时可实现准备好对初始化向量的加密序列 .不支持并行
     * 区别于CFB的点在于:每次加密都是针对初始化向量加密后的密文
     * */
    OFB("OFB" , true , "[Output FeedBack Mode]输出反馈模式") ,

    /**
     *  CTR:[Counter Mode]:计数器模式
     *  每个分组对应一个逐次累加的计数器,并通过计数器进行加密生成密钥流.再和明文分组进行异或处理得到密文分组.
     *  解密时:同样是对计数器进行加密处理而不是解密
     *  类似于于流密码
     *  使用分析 : 不需要填充,持并行计算(加密、解密)
     */
    CTR("CTR" , true , "[Counter mode]计数器模式");


    private String modeName;

    private Boolean needIV;

    private String desc;


    AlgorithmModeEnums(String modeName, Boolean needIV, String desc) {
        this.modeName = modeName;
        this.needIV = needIV;
        this.desc = desc;
    }

    public String getModeName() {
        return modeName;
    }

    public void setModeName(String modeName) {
        this.modeName = modeName;
    }

    public Boolean getNeedIV() {
        return needIV;
    }

    public void setNeedIV(Boolean needIV) {
        this.needIV = needIV;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }
}

4.分组填充模式

package com.cloudtravel.common.smencrypt;

/**
 * @description: 加密分组填充模式,
 * 分组填充方式: 加密数据时用来填充数据的一种模式.PKCS7Padding/PKCS5Padding/ZeroPadding
 *    block_size:块大小 , 进行加解密时 , 为避免明文长度过长导致一次加密的数据量太大. 会进行分块处理.
 *               而分块的时候则是参照block_size进行分.大多数分块大小默认都是64bits[8个字节] .
 *               如果明文的大小不是分块的整数倍,就需要在末尾进行填充.
 *    PKCS5Padding:可以理解为为PKCS7的子集 , 分块大小标准为8个字节.那么当最后一块的数据长度为n[n<8]时,
 *               则需要填充8-n个差值数据. 例:最后一块数据有5个字节,则需要填充3个0x03 .
 *               若明文数据为分块大小的整数倍,当使用PKCS5填充时 , 同样需要填充一组[8个]0x08
 *    PKCS7Padding:原理和PKCS5Padding一样,只是分块长度不一样 , 它的分块大小可以在1~255bytes之间.
 *    ZeroPadding : 数据长度不对齐时用0填充 . 否则不填充
 * 解密时: 由于最后一个字节肯定为填充数据的长度 , 所以在解密时可以准确删除填充的数据 . 而使用ZeroPadding
 *      的时候,由于最后填充的是0,没有办法区分真实数据和填充数据,所以只适合以\0结尾的字符串加解密
 *          \0: ASCII码为0,表示一个字符串结尾的标志.这是转义字符,底层处理时会视为整体的一个字符.内存中表现为8个0.
 *          0:位数字 . 内存中为32位的
 *         '0';字符.ASCII表示48.内存中为00110000
 * @author: walker
 * @DATE: 2022/9/4
 */
public enum AlgorithmPaddingModeEnums {

    /**
     * 分块标准再1~255bytes之间 .
     * 对明文按照块大小进行分组后,最后一组明文长度差N个不足块大小时.
     * 则在明文后填充n个0X0N进行补足
     */
    PKCS7_PADDING("PKCS7Padding"),

    /**
     * 可以看做为PKCS7Padding的子集.只是其块大小固定为8.
     */
    PKCS5_PADDING("PKCS5Padding"),

    /**
     * 零填充.数据不为0时填充.否则不填充 .只适合明文结尾为:\0的密文.基本不咋用
     */
    Zero_Padding("ZeroPadding");

    private String code;

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    AlgorithmPaddingModeEnums(String code) {
        this.code = code;
    }
}

5.加密工具类


package com.cloudtravel.common.smencrypt;

import com.cloudtravel.common.exception.CloudTravelException;
import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.pqc.math.linearalgebra.ByteUtils;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.security.SecureRandom;
import java.security.Security;

/**
* @description : SM4加密算法工具类--有些涉及线性代数的数学方面的可能理解有点跑偏.凑活看
* 属于对称加密算法,可用于替代DES/AES等算法,采用分组加密 ,且SM4算法同AES算法具有相同的密钥长度和分组长度,都是128bits[16进制==>32位].
* 整个加解密部分可以拆分成密钥扩展和加解密两步
* 密钥扩展:
* 一堆线性函数我是没看太懂.简单理解下感觉就是
* 1.对密钥进行分组后得到MK=(MK0,MK1,MK2,MK3)
* 2.根据(MK0,MK1,MK2,MK3)得到(K0,K1,K2,K3)=(MK0 ⊕ FK0,MK1 ⊕ FK1 , MK2 ⊕ FK2 , MK3 ⊕ FK3)
* 其中:FK为系统参数.16进制表示类似于: FK0=(a3b1c6)知道这个就好了.具体的数学学得太垃圾.搞不懂
* 3.因为加密时要进行非线性迭代32次 . 所以这里生成32位轮密钥:rk0~rk31
* rk[i] = k[i+4] = K[i] ⊕ T'(K[i+1] ⊕ K[i+2] ⊕ k[i+3] ⊕ CKi)
* 其中:
* 1.T变换和加密算法中的轮函数基本相同 , 只是将其中的线性变换L换成了:L[B] = B ⊕ (B <<< 13) ⊕ (B <<< 23)
* 2. 固定参数CK的取值方法为:设ck[i,j]为CK[i]的第j字节(i=0~31 , j=0~3),
* 即CK[i] = (ck[i,0],ck[i,1],ck[i,2],ck[i,3])∈(Z[下标2,上标8]4次方)-依稀记得以前见过这个写法.忘记代表啥了
* 其中:ck[i,j] = (4i + j) * 7 (mod 256)共得到32个CKi
* 加密过程:包括32次轮迭代[就是非线性迭代]和反序迭代[异或]两步
* 32次轮迭代:
* 分组明文拆分为(A0 ,A1,A2,A3)结合密码扩展得到的32组轮密码.轮函数F进行32次迭代
* Xi+4=F(Xi,Xi+1,Xi+2,Xi+3,rki)=Xi⊕T(Xi+1⊕Xi+2⊕Xi+3⊕rki),i=0,1,⋯,31
* 合成置换 T:Z322→Z322 是一个可逆变换,由非线性变换 τ 和线性变换 L复合而成,即 T(⋅)=L(τ(⋅))。
* 线性变换 L: L(B)=B⊕(B<<<2)⊕(B<<<10)⊕(B<<<18)⊕(B<<<24)。
* 非线性变换 τ: τ(B),τ 由4个并行的S盒构成。
* 反序迭代:得到的x4~x35中得到x32~x35
* 总结下来就是
* 1. 先将128比特密钥 MK 扩展为32个轮密钥 rk,
* 2. 再将该轮密钥与128比特明文 X 经过轮函数进行32次迭代后,选取最后四次迭代生成的结果 X32,X33,X34,X35 进行反序变换,
* 该变换结果作为最终的密文 Y 输出
* 解密时算法内容大致一样 . 只不过轮密钥顺序相反
* @author : walker
* @date : 2022/8/24 23:45
*/
public class Sm4AlgorithmUtil {

/** 密钥 */
public static final String PASSWORD = "5m28850d763e8748ff2f8d83530e0cf2";

/** 128-32位16进制;256-64位16进制 */
public static final int DEFAULT_KEY_SIZE = 128;

public static final String APPEND_SEPARATOR = "/";

private static final String EN_CODING = "UTF-8";

static {
//为防止当前加密支持器影响该算法,先将当前加密支持器删除
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
//添加SM加密算法相关支持器
Security.addProvider(new BouncyCastleProvider());
}

/**
* 加密
* @param algorithmName 算法名
* @param algorithmMode 分组加密模式
* @param algorithmPadding 分组填充算法枚举
* @param param 待加密参数
* @return
*/
public static String encrypt(AlgorithmNameEnums algorithmName ,
AlgorithmModeEnums algorithmMode,
AlgorithmPaddingModeEnums algorithmPadding ,
String param) {

String cipherText = "";
try {
if(StringUtils.isNotBlank(param)) {
byte [] pwdBytes = ByteUtils.fromHexString(PASSWORD);
byte [] paramBytes = param.getBytes(EN_CODING);
byte [] cipherArray = null;
boolean needIV = algorithmMode.getNeedIV();
Cipher cipher = null;
if(needIV){
byte [] iv = generateKey(algorithmName.getName() ,DEFAULT_KEY_SIZE);
cipher = generateCipher(algorithmName , algorithmMode , algorithmPadding , Cipher.ENCRYPT_MODE , pwdBytes , iv);
cipherArray = cipher.doFinal(paramBytes);
cipherText = ByteUtils.toHexString(cipherArray);
cipherText = cipherText + "_" + ByteUtils.toHexString(iv);
}else {
cipher = generateCipher(algorithmName , algorithmMode , algorithmPadding , Cipher.ENCRYPT_MODE ,pwdBytes , null);
cipherArray = cipher.doFinal(paramBytes);
cipherText = ByteUtils.toHexString(cipherArray);
}
}else {
return param;
}
}catch (Exception e) {
throw new CloudTravelException(String.format("加密异常, algorithmName={%s}, algorithmMode={%s} ," +
"algorithmPadding={%s} ,param={%s},message={%s}" , algorithmName.getName(),
algorithmMode.getModeName() , algorithmPadding.name() , param , e.getMessage()));
}
return cipherText;
}

/**
* 解密
* @param algorithmName 算法名
* @param algorithmMode 分组加密模式
* @param algorithmPadding 填充方式---解密时需逆向截取加密的明文
* @param param 待解密参数
* @return
*/
public static String decrypt(AlgorithmNameEnums algorithmName ,
AlgorithmModeEnums algorithmMode,
AlgorithmPaddingModeEnums algorithmPadding ,
String param) {
if(StringUtils.isNotBlank(param)) {
try {
String decryptStr = "";
byte [] pwdBytes = ByteUtils.fromHexString(PASSWORD);
byte [] paramBytes = new byte [0];
byte [] cipherArray = null;
boolean containsIV = algorithmMode.getNeedIV();
Cipher cipher = null;
if(containsIV) {
String paramStr = param.substring(0 , param.indexOf("_"));
String ivStr = param.substring(param.indexOf("_") + 1);
byte [] ivBytes = ByteUtils.fromHexString(ivStr);
paramBytes = ByteUtils.fromHexString(paramStr);
cipher = generateCipher(algorithmName , algorithmMode , algorithmPadding , Cipher.DECRYPT_MODE , pwdBytes , ivBytes);
}else {
paramBytes = ByteUtils.fromHexString(param);
cipher = generateCipher(algorithmName , algorithmMode , algorithmPadding , Cipher.DECRYPT_MODE , pwdBytes , null);
}
cipherArray = cipher.doFinal(paramBytes);
decryptStr = new String(cipherArray , EN_CODING);
return decryptStr;
}catch (Exception e) {
throw new CloudTravelException(String.format("解密异常, algorithmName={%s}, algorithmMode={%s} ," +
"algorithmPadding={%s} ,param={%s},message={%s}" , algorithmName.getName(),
algorithmMode.getModeName() , algorithmPadding.name() , param , e.getMessage()));
}
}else {
return param;
}
}


/**
* 初始化加解密算法执行器
* @param algorithmName 算法名
* @param algorithmMode 分组加密模式
* @param algorithmPadding 分组填充模式
* @param cipherMode 加密/解密
* @param key 密钥
* @param iv 初始化向量
* @return 执行器
* @throws Exception
*/
private static Cipher generateCipher(AlgorithmNameEnums algorithmName ,
AlgorithmModeEnums algorithmMode,
AlgorithmPaddingModeEnums algorithmPadding,
int cipherMode ,
byte[] key ,
byte[] iv) throws Exception {
String algorithmNameModePadding = algorithmName.getName() + APPEND_SEPARATOR +
algorithmMode.getModeName() + APPEND_SEPARATOR + algorithmPadding.getCode();
//Cipher:为加密和解密提供密码功能. 是JCE的核心.
//初始化该类需提供需要转换的算法名
Cipher cipher = Cipher.getInstance(algorithmNameModePadding, BouncyCastleProvider.PROVIDER_NAME);
Key sm4Key = new SecretKeySpec(key , algorithmName.getName());
boolean needIV = algorithmMode.getNeedIV();
if(needIV){
//指定一个初始化向量 . 防止后续加密的过程中被篡改 . 主要用于反馈模式的加密.如CBC,CFB等
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
cipher.init(cipherMode , sm4Key , ivParameterSpec);
}else {
cipher.init(cipherMode , sm4Key);
}
return cipher;
}



/**
* 生成初始化向量--CBC/CFB等模式下使用
* @param keySize
* @return
* @throws Exception
*/
public static byte[] generateKey (String algorithmName , int keySize)throws Exception {
KeyGenerator kg = KeyGenerator.getInstance(algorithmName, BouncyCastleProvider.PROVIDER_NAME);
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
kg.init(keySize, random);
return kg.generateKey().getEncoded();
}

public static void main(String[] args) {
String a = "快乐编码 , 生活愉快!";

String ecbEncrypt = encrypt(AlgorithmNameEnums.SM_4 , AlgorithmModeEnums.ECB ,
AlgorithmPaddingModeEnums.PKCS5_PADDING , a);
System.out.println("ECB加密:" + ecbEncrypt);
System.out.println("ECB解密:" + decrypt(AlgorithmNameEnums.SM_4 , AlgorithmModeEnums.ECB ,
AlgorithmPaddingModeEnums.PKCS5_PADDING , ecbEncrypt));

String cbcEncrypt = encrypt(AlgorithmNameEnums.SM_4 , AlgorithmModeEnums.CBC ,
AlgorithmPaddingModeEnums.PKCS7_PADDING , a);
System.out.println("CBC加密: " + cbcEncrypt);
System.out.println("CBC解密" + decrypt(AlgorithmNameEnums.SM_4 , AlgorithmModeEnums.CBC ,
AlgorithmPaddingModeEnums.PKCS7_PADDING , cbcEncrypt));

String cfbEncrypt = encrypt(AlgorithmNameEnums.SM_4 , AlgorithmModeEnums.CFB ,
AlgorithmPaddingModeEnums.PKCS7_PADDING , a);
System.out.println("CFB加密: " + cfbEncrypt);
System.out.println("CFB解密" + decrypt(AlgorithmNameEnums.SM_4 , AlgorithmModeEnums.CFB ,
AlgorithmPaddingModeEnums.PKCS7_PADDING , cfbEncrypt));

String ofbEncrypt = encrypt(AlgorithmNameEnums.SM_4 , AlgorithmModeEnums.OFB ,
AlgorithmPaddingModeEnums.PKCS7_PADDING , a);
System.out.println("OFB加密: " + ofbEncrypt);
System.out.println("OFB解密: " + decrypt(AlgorithmNameEnums.SM_4 , AlgorithmModeEnums.OFB ,
AlgorithmPaddingModeEnums.PKCS7_PADDING , ofbEncrypt));

String ctrEncrypt = encrypt(AlgorithmNameEnums.SM_4 , AlgorithmModeEnums.CTR ,
AlgorithmPaddingModeEnums.PKCS7_PADDING , a);
System.out.println("CTR加密: " + ctrEncrypt);
System.out.println("CTR解密:" + decrypt(AlgorithmNameEnums.SM_4 , AlgorithmModeEnums.CTR ,
AlgorithmPaddingModeEnums.PKCS7_PADDING , ctrEncrypt));


}
}
 

6.测试结果

 

posted @ 2022-08-28 01:37  每天学习1点点  阅读(7652)  评论(0编辑  收藏  举报