0


SM4分组加密算法原理和c语言实现

一、前言

在之前的文章中介绍了《SM3国密杂凑值算法的原理和c语言实现》,这篇文章主要是用c语言撸一个SM4分组加密算法。

随着信息安全的普及以及等级保护等安全政策落地,国密算法越来越频繁的出现在项目开发中,在较新的一些openssl版本中已经有了SM2、SM3、SM4等国密接口,还有Gmssl等开源加密库也均对国密算法进行了支持。

不过在一些底层的开发中,尤其涉及到内核驱动层开发、TCM等密码卡开发,现有的一些加密库由于过多的依赖,是无法直接使用的,这时候恐怕就得要求开发人员对国密接口进行重新实现。

二、SM4分组加密算法

常见的分组加密算法有DES、3DES、AES、SM4等,该类算法的一个特点就是对称加密,既解密过程所使用的密钥与加密过程所使用的密钥是同一个,或者对加密密钥做一些简单的数学换算得到。

使用分组加密算法加密数据之后,信息数据的保护就变成了密钥的保护,谁得到了密钥谁就可以破解密文数据。

SM4分组加密算法可以按照字面意思理解,如果有一批数据需要进行加密,首先需要对数据分成若干小组,每次加密其实处理(经过32轮的非线性迭代运算)的是单个小组中的数据,然后将所有分组的密文放一起就是整个密文了。

SM4分组加密算法的每个分组大小是128bit(16字节),所用的密钥长度也是128bit(16字节),最后生成的密文长度也是128bit(16字节)。我们可以直接认为SM4就是对16字节的数据进行加密,加密之后生成16字节的密文数据。

如果遇到某个分组数据不足16字节怎么办?

那么我们可以填充特定字符串至满16字节为止,只要解密方知道你填充了多少个字节的无用数据,或者知道你填充的特定字符串是什么,解密之后直接去掉就可以了。具体填充的数据可以根据开发人员个人喜好去确定,无实际意义。

三、加解密流程

SM4分组加密算法的详细流程可参照 GM/T0002-2012《SM4分组密码算法》中所述。

1、密钥扩展过程

密钥扩展主要是为了得到轮密钥,而整个加密和解密过程使用的就是轮密钥,轮密钥是一个类似rk[32]数组的形式,每一个元素为32bit(4字节)。

(1)SM4所用密钥ekey长度为16字节,既128bit,首先创建MK[4](单个元素长度为32bit,可当成int型数值),将16字节的ekey放入MK数组中,需要注意的是,要转化为大端格式。

(2)然后可以创建一个K[36]数组(单个元素长度为32bit,可当成int型数值),K数组的0~3下标元素直接从对应下标的MK与对应下标的FK异或得到。FK为系统参数,是一个FK[4]数组类型的格式,每个元素32bit。

而K数组对应的4~35下标的元素则通过K[i+4]  = k[i] ^ T' (K[i+1] ^ K[i+2] ^ k[i+3])获得,同样就可到了对应下标的rk值。

(3)T'是置换函数,置换函数是非线性变换函数和一个线性变换函数合成的。其中非线性变换函数用到了Sbox,可以将Sbox理解为一个16乘16的二维数组,每个元素为8bit。既每个字节的数据(高4位得到行号,低4位可以得到列号)可以通过查看Sbox表变成新的字节。

2、加解密过程

加密过程中,明文数据128bit,既16字节,可以用data[16]来表示。在加密过程中首先要创建一个X[36]的数组,每个元素32bit。和密钥扩展过程类似,真实的data数组填充到X数组的0~3的下标中,赋值过程需要转换为大端。

X其他4~35的下标数据,需要根据 X[i+4] = X[i] ^ T ( X[i+1] ^ X[i+2] ^ X[i+3] ^ rk[i])得到。经过32轮的迭代计算,最后X[35]~X[32]的4个元素既最后的密文结果。

解密过程与加密过程是一致的,只不过解密过程中的源数据是密文数据,目标数据是明文数据,而且所用到的轮密钥rk需要逆序过来。

四、c语言实现

lk_sm4.h头文件,宏定义方式定义了基本密钥扩展接口、循环左移接口,大小端转换接口,置换函数,线性迭代函数,非线性迭代函数,加密接口以及解密接口等。
#ifndef __lk_sm4_h__
#define __lk_sm4_h__

#ifdef __cpluscplus
extern "C" {
#endif

#include <strings.h>
#include <string.h>

#ifdef __cpluscplus
}
#endif

#define LK_WORD_SIZE        32 
#define LK_GCHAR_SIZE       16
#define LK_GWORD_SIZE       4 

typedef void                LK_VOID;
typedef int                 LK_INT;                                                                                                                                         
typedef unsigned int        UINT;
#ifdef i386 
typedef unsigned long long   UWORD;
#else
typedef unsigned long       UWORD;
#endif
typedef unsigned char   UCHAR;

//大端转化
#define LK_GE(c, i) (\
        (c[(i)+0] << 24) | ((c[(i)+1] << 16)) |\
         (c[(i)+2] << 8) | (c[(i)+3]) )
//小端转化
#define LK_LE(c, n, i) {\
        c[(i)+0] = ((n) >> 24) & 0x000000ff;\
        c[(i)+1] = ((n) >> 16) & 0x000000ff;\
        c[(i)+2] = ((n) >> 8) & 0x000000ff;\
        c[(i)+3] = (n) & 0x000000ff; }
//循环左移
#define LOOPSHFT(LK_LSHIFA, LK_LSHIFN) (\
        ((LK_LSHIFA) << (LK_LSHIFN)) | \
        ((LK_LSHIFA) >> (LK_WORD_SIZE - (LK_LSHIFN))))

//线性变换函数L' L'(B)=B^(B<<<13)^(B<<<23);
#define LK_L0(LK_L0B) (\
    (LK_L0B)^(LOOPSHFT((LK_L0B), 13))^(LOOPSHFT((LK_L0B), 23)) )

//线性变换函数L L(B) = B^(B<<<2)^(B<<<10)^(B<<<18)^(B<<<24)
#define LK_L1(LK_L1B) (\
    (LK_L1B)^(LOOPSHFT((LK_L1B), 2))^(LOOPSHFT((LK_L1B), 10))\
    ^(LOOPSHFT((LK_L1B), 18))^(LOOPSHFT((LK_L1B), 24)) )

//非线性变换 τ函数 (b0 , b1, b2 , b3 ) = τ (A) = ( Sbox (a0 ), Sbox (a1 ), Sbox (a2 ), Sbox (a3 ) )
#define LK_ST(LK_STA) ({\
    LK_INT LK_i;\
    UINT LK_STB = LK_STA;\
    UCHAR *pcr = (UCHAR *)(&LK_STB);\
    for (LK_i = 0; LK_i < 4; LK_i++) {\
        UCHAR high_bval = ((pcr[LK_i] >> 4) & 0x0f);\
        UCHAR low_bval = (pcr[LK_i] & 0x0f);\
        *(pcr+LK_i) = lk_sbox[high_bval][low_bval];\
    }\
    LK_STB;\
})

//合成置换 T  T(.)=L(τ(.))
#define LK_T1(LK_TA) (\
    LK_L1(LK_ST(LK_TA)) )
        
//合成置换 T'  T'(.)=L'(τ(.))
#define LK_T0(LK_TA) (\
    LK_L0(LK_ST(LK_TA)) )

//密钥扩展算法生成轮密钥 
#define LK_SM4_INIT(lk_context, ekey) {\
    UINT k[36], mk[4];\
    LK_INT i;\
    lk_sm4_context_t *t = (lk_sm4_context_t *)(lk_context);\
    bzero(t, sizeof(lk_sm4_context_t));\
    for (i = 0; i < 4; i++) {\
        mk[i] = LK_GE(ekey, i * 4);\
        k[i] = mk[i]^lk_fk[i];\
    }\
    for (i = 0; i < 32; i++) {\
        k[i+4] = (k[i] ^ LK_T0(k[i+1]^k[i+2]^k[i+3]^(lk_ck[i])));\
        t->e_rk[i] = k[i+4];\
        t->d_rk[31 - i] = t->e_rk[i];\
    }\
}

//加密函数
#define LK_SM4_ENC_F(lk_context) {\
    LK_INT i;\
    UINT x[36];\
    lk_sm4_context_t *t = (lk_sm4_context_t *)(lk_context);\
    for (i = 0; i < 4; i++) {\
        x[i] = LK_GE(t->buf, i * 4);\
    }\
    for (i = 0; i < 32; i++) {\
        x[i + 4] = x[i]^(LK_T1(x[i+1]^x[i+2]^x[i+3]^t->e_rk[i]));\
    }\
    LK_LE(t->ebuf, x[35], 0)\
    LK_LE(t->ebuf, x[34], 4)\
    LK_LE(t->ebuf, x[33], 8)\
    LK_LE(t->ebuf, x[32], 12)\
}
//解密函数
#define LK_SM4_DEC_F(lk_context) {\
    LK_INT i;\
    UINT x[36];\
    lk_sm4_context_t *t = (lk_sm4_context_t *)(lk_context);\
    for (i = 0; i < 4; i++) {\
        x[i] = LK_GE(t->ebuf, i * 4);\
    }\
    for (i = 0; i < 32; i++) {\
        x[i + 4] = x[i]^LK_T1(x[i+1]^x[i+2]^x[i+3]^t->d_rk[i]);\
    }\
    LK_LE(t->buf, x[35], 0)\
    LK_LE(t->buf, x[34], 4)\
    LK_LE(t->buf, x[33], 8)\
    LK_LE(t->buf, x[32], 12)\
}

typedef struct lk_sm4_context_s
{
    //需要加密的明文,加密之前需要填充
    UCHAR    buf[LK_GCHAR_SIZE];
    //加密之后的密文,如果是解密的话,解密之前需要填充
    UCHAR    ebuf[LK_GCHAR_SIZE];
    //原密钥经过扩展之后的轮密钥,初始化接口自行填充
    UINT    e_rk[LK_WORD_SIZE];
    //解密过程需要的轮密钥,初始化接口自行填充
    UINT    d_rk[LK_WORD_SIZE];
} lk_sm4_context_t;
#ifdef __cpluscplus
extern "C" {
#endif

extern LK_VOID lk_sm4_enc(LK_VOID *p_context);
extern LK_VOID lk_sm4_dec(LK_VOID *p_context);

#ifdef __cpluscplus
}
#endif

#endif
lk_sm4.c文件,定义了Sbox、FK以及CK等数组,同时也定义了lk_sm4_enc加密接口和lk_sm4_dec解密接口,加解密函数具体的实现在头文件中。
#include <stdio.h>
#include "lk_sm4.h"
//非线性转化用到的sbox对照表                                                                                                                                                
UCHAR lk_sbox[LK_GCHAR_SIZE][LK_GCHAR_SIZE] =  {
    {0xd6,0x90,0xe9,0xfe,0xcc,0xe1,0x3d,0xb7,0x16,0xb6,0x14,0xc2,0x28,0xfb,0x2c,0x05},
    {0x2b,0x67,0x9a,0x76,0x2a,0xbe,0x04,0xc3,0xaa,0x44,0x13,0x26,0x49,0x86,0x06,0x99},
    {0x9c,0x42,0x50,0xf4,0x91,0xef,0x98,0x7a,0x33,0x54,0x0b,0x43,0xed,0xcf,0xac,0x62},
    {0xe4,0xb3,0x1c,0xa9,0xc9,0x08,0xe8,0x95,0x80,0xdf,0x94,0xfa,0x75,0x8f,0x3f,0xa6},
    {0x47,0x07,0xa7,0xfc,0xf3,0x73,0x17,0xba,0x83,0x59,0x3c,0x19,0xe6,0x85,0x4f,0xa8},
    {0x68,0x6b,0x81,0xb2,0x71,0x64,0xda,0x8b,0xf8,0xeb,0x0f,0x4b,0x70,0x56,0x9d,0x35},
    {0x1e,0x24,0x0e,0x5e,0x63,0x58,0xd1,0xa2,0x25,0x22,0x7c,0x3b,0x01,0x21,0x78,0x87},
    {0xd4,0x00,0x46,0x57,0x9f,0xd3,0x27,0x52,0x4c,0x36,0x02,0xe7,0xa0,0xc4,0xc8,0x9e},
    {0xea,0xbf,0x8a,0xd2,0x40,0xc7,0x38,0xb5,0xa3,0xf7,0xf2,0xce,0xf9,0x61,0x15,0xa1},
    {0xe0,0xae,0x5d,0xa4,0x9b,0x34,0x1a,0x55,0xad,0x93,0x32,0x30,0xf5,0x8c,0xb1,0xe3},
    {0x1d,0xf6,0xe2,0x2e,0x82,0x66,0xca,0x60,0xc0,0x29,0x23,0xab,0x0d,0x53,0x4e,0x6f},
    {0xd5,0xdb,0x37,0x45,0xde,0xfd,0x8e,0x2f,0x03,0xff,0x6a,0x72,0x6d,0x6c,0x5b,0x51},
    {0x8d,0x1b,0xaf,0x92,0xbb,0xdd,0xbc,0x7f,0x11,0xd9,0x5c,0x41,0x1f,0x10,0x5a,0xd8},
    {0x0a,0xc1,0x31,0x88,0xa5,0xcd,0x7b,0xbd,0x2d,0x74,0xd0,0x12,0xb8,0xe5,0xb4,0xb0},
    {0x89,0x69,0x97,0x4a,0x0c,0x96,0x77,0x7e,0x65,0xb9,0xf1,0x09,0xc5,0x6e,0xc6,0x84},
    {0x18,0xf0,0x7d,0xec,0x3a,0xdc,0x4d,0x20,0x79,0xee,0x5f,0x3e,0xd7,0xcb,0x39,0x48}};
//系统参数FK
UINT lk_fk[4] = { 
    0xA3B1BAC6,0x56AA3350,
    0x677D9197,0xB27022DC
};
//固定参数CK
UINT lk_ck[32] = { 
    0x00070e15,0x1c232a31,0x383f464d,0x545b6269,
    0x70777e85,0x8c939aa1,0xa8afb6bd,0xc4cbd2d9,
    0xe0e7eef5,0xfc030a11,0x181f262d,0x343b4249,
    0x50575e65,0x6c737a81,0x888f969d,0xa4abb2b9,
    0xc0c7ced5,0xdce3eaf1,0xf8ff060d,0x141b2229,
    0x30373e45,0x4c535a61,0x686f767d,0x848b9299,
    0xa0a7aeb5,0xbcc3cad1,0xd8dfe6ed,0xf4fb0209,
    0x10171e25,0x2c333a41,0x484f565d,0x646b7279
};

LK_VOID lk_sm4_init(LK_VOID *p_context, UCHAR *ekey)
{
    //进行密钥扩展,生成轮转密钥rk
    LK_SM4_INIT(p_context, ekey)
}

LK_VOID lk_sm4_enc(LK_VOID *p_context)
{
    LK_SM4_ENC_F(p_context)
}

LK_VOID lk_sm4_dec(LK_VOID *p_context)
{
    LK_SM4_DEC_F(p_context)
}
test.c是测试例程,具体测试数据参照了SM3分组加密算法中的例子程序。

测试代码:
#include <stdio.h>
#include "lk_sm4.h"
                                                                                                                                                                            
LK_INT main(LK_VOID)
{
    LK_INT i;
    lk_sm4_context_t context;
    UCHAR ekey[16] = { 
        0x01,0x23,0x45,0x67,
        0x89,0xab,0xcd,0xef,
        0xfe,0xdc,0xba,0x98,
        0x76,0x54,0x32,0x10 };
    UCHAR data[16] = { 
        0x01,0x23,0x45,0x67,
        0x89,0xab,0xcd,0xef,
        0xfe,0xdc,0xba,0x98,
        0x76,0x54,0x32,0x10 };
    //初始化
    lk_sm4_init(&context, ekey);
    //待加密数据填充
    memcpy(context.buf, data, sizeof(data));
    //对数据进行加密
    lk_sm4_enc(&context);
    //打印密文值
    printf("enc data:");
    for (i = 0; i < LK_GCHAR_SIZE; i++)
        printf(" %02x ", context.ebuf[i]); 
    printf("\n");
    //对数据进行解密
    memset(context.buf, 0x0, sizeof(context.buf));
    lk_sm4_dec(&context); 
    //打印解密数据
    printf("dec data:");
    for (i = 0; i < LK_GCHAR_SIZE; i++)
        printf(" %02x ", context.buf[i]); 
    printf("\n");
    return 0;
}
执行结果:

五、关于填充

在上面提到的测试例子中,对于分组加密的数据,如果最后的一组不足16字节,其实没有进行填充,也可以理解为填充了0,当然也可以按照自己的规则去随意填充,解密的时候只要能去掉自己的规则就可以。

在一些已有的SM4的实现库中,一般采用了PKCS#7填充方式:

1、最后一组待加密数据不足16字节,比如还差一个字节则填充0x1,还差5个字节则填充5个0x5,还差10个字节则填充10个0xA......填充值跟待填充字节数是一致的。

2、如果待加密数据的最后一个分组正好也是16字节,则补位16个字节的0xF。

本文转载自: https://blog.csdn.net/u013250169/article/details/121166949
版权归原作者 6月的夕夕 所有, 如有侵权,请联系我们删除。

“SM4分组加密算法原理和c语言实现”的评论:

还没有评论