【加密算法】SM4国密算法原理、C++跨平台实现(含完整代码和示例)

引言

SM4是中国国家密码管理局(OSCCA)于2006年发布的商用密码算法,原名SMS4,2016年更名为SM4并成为国际标准(ISO/IEC 18033-3),作为国产密码体系(国密)的核心组成部分,其设计目标是为数据加密提供自主可控的解决方案。

SM4广泛用于金融、政务、物联网等领域,是中国国密算法体系(SM1-SM9)中的重要组成部分,尤其在需要符合中国密码法规的场景中(如信创改革、电子认证、数据加密)。

本文将从算法原理、实现技术、应用场景及安全性分析等方面展开,结合图文解析,全面揭示SM4的技术特点。

一、SM4算法基础

1.1 算法概述

SM4是一种对称分组密码算法,分组长度和密钥长度均为128位(16字节),采用32轮非线性迭代结构。其设计兼顾安全性和效率,常用于网络通信、金融支付等场景的数据加密。

1.2 核心原理

  1. 分组密码:分组长度和密钥长度均为128位,加密过程通过32轮迭代完成。
  2. 设计目标:安全性(抗差分/线性攻击)、软硬件实现效率、与现有算法的兼容性。

1.3 核心参数

  1. 分组长度:128位(将明文划分为16字节的块处理)。
  2. 密钥长度:128位,通过密钥扩展算法生成32个32位轮密钥。
  3. 工作模式:支持ECB、CBC、CTR等模式,其中CBC模式因链式加密特性被广泛采用。

1.4 核心加密算法流程

  1. 输入处理:明文分为4个32位字((X_0, X_1, X_2, X_3))。
  2. 32轮迭代:每轮通过轮函数F生成新的字,轮密钥(rk_i)参与运算。
  3. 最终输出:最后一轮后逆序拼接4个字,得到密文。
初始密钥(MK₀, MK₁, MK₂, MK₃)
异或系统参数FK(FK₀, FK₁, FK₂, FK₃)
生成初始轮密钥(k₀, k₁, k₂, k₃)
循环生成轮密钥(32轮)
第i轮输入(kᵢ, kᵢ₊₁, kᵢ₊₂, kᵢ₊₃)
计算sbox_input = kᵢ₊₁ ⊕ kᵢ₊₂ ⊕ kᵢ₊₃ ⊕ CKᵢ
拆分4个8bit数据进S盒
S盒输出合并为32bit
循环左移13位
循环左移23位
异或移位结果
与kᵢ异或
生成kᵢ₊₄

二、算法原理详解

SM4算法原理

2.1 加密流程

SM4的加密过程分为三个阶段:

  1. 密钥扩展:将初始密钥通过非线性变换生成32个轮密钥。
  2. 迭代加密:明文分组经过32轮非线性变换,每轮使用一个轮密钥。
  3. 密文输出:最终迭代结果经逆初始变换生成密文。

2.2 轮函数结构

每轮迭代包含以下操作(如图2):

  • S盒替换:8位输入通过复合域S盒进行非线性替换,增强抗差分攻击能力。
  • 线性变换:包括行移位、列混淆等操作,扩散数据变化。
  • 密钥混合:轮密钥与中间状态进行异或运算。
输入32位字A
分解为4个8位字节
a₀ (8bit)
a₁ (8bit)
a₂ (8bit)
a₃ (8bit)
S盒变换
S盒变换
S盒变换
S盒变换
b₀ (8bit)
b₁ (8bit)
b₂ (8bit)
b₃ (8bit)
合并为32位字B

2.3 加密模式与填充

  1. 常用模式:CBC(需IV)、CTR(计数器模式)、ECB(不推荐,缺乏扩散性)。
  2. 填充方式:PKCS#7为标准填充方案,确保数据长度对齐分组。

2.4 对称加密算法对比

算法 密钥长度 分组长度 性能 安全性 优缺点 典型应用场景
SM4 128位 128位 软件实现中等效率,硬件优化后性能提升;32轮迭代复杂度较高 抗差分/线性攻击,符合中国国密标准,国际认可(ISO) 优点:合规性强,设计简洁,对称加解密流程;
缺点:国际生态支持有限
中国政务/金融系统、物联网设备、合规性要求场景
AES 128/192/256位 128位 硬件加速(如AES-NI指令集)下极快;软件实现效率高,轮数少(10/12/14轮) 抗已知攻击,NIST认证,行业黄金标准,密钥长度灵活 优点:广泛支持、高效灵活;
缺点:依赖硬件加速优化
通用加密(TLS/SSL、磁盘加密、通信协议)
DES 56位(实际64位) 64位 速度慢(密钥短,16轮迭代),已过时 已被破解(暴力攻击可行),不安全 优点:历史兼容性;
缺点:完全淘汰,仅用于遗留系统
老旧系统兼容(不推荐新项目使用)
3DES 168位(有效112位) 64位 比DES慢3倍(3次DES运算) 安全性高于DES但低于AES,NIST已禁用(2023年后) 优点:过渡期兼容;
缺点:效率低,安全性不足
银行旧系统、传统设备
ChaCha20 256位 流密码 纯软件实现快(无硬件依赖),适合移动端/低功耗设备 抗侧信道攻击,安全性等同于AES-256,被TLS 1.3广泛采用 优点:移动端高效,抗时序攻击;
缺点:分组模式需结合Poly1305认证
移动应用、TLS协议、实时通信

三、算法实现技术

3.1 硬件实现

  • ASIC设计:通过流水线技术优化ECB模式吞吐量,但CBC模式因数据依赖性导致效率下降。
  • FPGA实现:利用查找表(LUT)优化S盒运算,降低硬件资源消耗。

3.2 软件实现

  • C/C++实现:通过预计算S盒查找表提升效率,例如将4个32位字并行处理。
  • CBC模式优化:采用数据分块并行策略,缓解链式加密的延迟问题。
// 示例代码:SM4密钥扩展(伪代码)
void SM4KeySchedule(uint32_t key[4], uint32_t roundKeys[32]) 
{
    
    
    uint32_t rk = key[0] ^ key[1] ^ key[2] ^ key[3];
    for (int i = 0; i < 32; i++) 
    {
    
    
        rk = F(rk, CK[i]); // F为轮函数,CK为固定常数
        roundKeys[i] = rk;
    }
}

四、安全性分析

4.1 算法设计安全性

  1. 密钥与分组长度
    SM4采用128位密钥和128位分组长度,密钥空间达到(2^{128})量级,理论上可抵御暴力破解。与AES-128的安全强度相当,且分组长度设计符合现代密码学对抗差分攻击的基本要求。

  2. 非线性迭代结构
    算法采用32轮Feistel结构,每轮包含非线性变换(S盒)和线性变换(L函数)。其中:
    S盒设计:由4个并行8位输入/输出的S盒构成,具备严格的非线性特性(差分均匀性、代数次数等),可抵御差分攻击和线性攻击。
    线性变换L:通过循环左移与异或操作(如(B \oplus (B \ll 2) \oplus (B \ll 10) \oplus (B \ll 18) \oplus (B \ll 24)))实现高分支数扩散,增强雪崩效应。

  3. 密钥扩展机制
    密钥扩展算法同样采用32轮迭代,通过固定参数(CK_i)和系统参数(FK_i)生成轮密钥,确保密钥与轮函数之间的强关联性,增加密钥推导复杂度。

4.2 抗攻击能力

  1. 理论抗攻击性
    差分攻击与线性攻击:32轮迭代结构配合S盒的非线性特性,使得差分概率和线性偏差显著降低,目前无公开的有效攻击方法。
    代数攻击:S盒的代数复杂度(如高次布尔函数)可抵御代数方程的求解攻击。
    侧信道攻击:标准实现可能受模板攻击等侧信道威胁,但通过掩码技术、冗余编码等防护手段(如白盒实现中的隐式方程组技术),可将攻击复杂度提升至(2^{128})以上。

  2. 实际攻防案例
    根据清华大学的研究,未防护的SM4实现可能被模板攻击破解密钥熵至44.92位,但结合二阶掩码与噪声注入技术后,可实现有效防护。

4.3 标准化与权威认证

  1. 国家与国际标准
    SM4于2012年成为我国密码行业标准(GB/T 32907-2016),2021年纳入国际标准ISO/IEC 18033-3,标志着其设计通过国际密码学界审查。

  2. 安全性评估
    国家密码管理局及多所高校的研究表明,SM4在抗差分攻击、线性攻击等核心指标上达到商用密码算法的高安全要求。

4.4 实际应用中的安全增强

  1. 模式与填充选择
    推荐使用CBC、CTR等模式配合PKCS7填充,避免ECB模式的弱安全性问题。

  2. 实现加固
    • 在资源受限设备(如物联网终端)中,建议结合硬件加密模块或白盒密码技术(如隐式方程与冗余编码)。
    • 金融等高安全场景中,需采用动态密钥更新策略及抗侧信道攻击的防护方案。

4.5、与AES的对比

尽管SM4与AES-128的安全强度相当,但其优势体现在:
自主可控性:完全国产化设计,避免潜在的后门风险。
硬件效率:算法结构简洁,在同等安全强度下硬件实现资源占用更低。


五、SM4的CBC算法实现(C++)

5.1 源码(Header-Only

// sm4.h
#ifndef __SM4_H__
#define __SM4_H__
#include <string.h>

#ifndef GET_ULONG_BE
#define GET_ULONG_BE(n,b,i) \
		{
      
      \
		(n) = ((unsigned long)(b)[(i)]     << 24) \
			| ((unsigned long)(b)[(i) + 1] << 16) \
			| ((unsigned long)(b)[(i) + 2] <<  8) \
			| ((unsigned long)(b)[(i) + 3]      );\
		}
#endif

#ifndef PUT_ULONG_BE
#define PUT_ULONG_BE(n,b,i) \
		{
      
      \
		(b)[(i)    ] = (unsigned char) ( (n) >> 24 );\
		(b)[(i) + 1] = (unsigned char) ( (n) >> 16 );\
		(b)[(i) + 2] = (unsigned char) ( (n) >>  8 );\
		(b)[(i) + 3] = (unsigned char) ( (n)       );\
		}
#endif

#define SHL(x,n) (((x) & 0xFFFFFFFF) << n)
#define ROTL(x,n) (SHL((x),n) | ((x) >> (32 - n)))
#define SWAP(a,b) {
      
       unsigned long t = a; a = b; b = t; t = 0; }

static const unsigned char SboxTable[16][16] =
{
    
    
	{
    
    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}
};

static const unsigned long FK[4] = {
    
     0xa3b1bac6,0x56aa3350,0x677d9197,0xb27022dc };
static const unsigned long 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
};

class SM4
{
    
    
public:
	static void Enc(const unsigned char* src, unsigned char* dst, size_t& len, unsigned char iv[16], const unsigned char key[16])
	{
    
    
		size_t i, n;
		unsigned long sk[32];
		SetKey(sk, key);

		i = 16 - (len % 16);
		char* pBuf = nullptr;
		if (i > 0)
		{
    
    
			pBuf = new char[len + i];
			memset(pBuf, (int)i, len + i);
			memcpy(pBuf, src, len);
			src = (unsigned char*)pBuf;
		}
		len += i;

		i = len;
		while (i > 0)
		{
    
    
			for (n = 0; n < 16; n++) {
    
     dst[n] = src[n] ^ iv[n]; }
			OneRound(sk, dst, dst);
			memcpy(iv, dst, 16);
			src += 16;
			dst += 16;
			i -= 16;
		}
		delete[] pBuf;
	}

	static void Dec(const unsigned char* src, unsigned char* dst, size_t& len, unsigned char iv[16], const unsigned char key[16])
	{
    
    
		size_t i, n;
		unsigned long sk[32];
		unsigned char tmp[16];
		SetKey(sk, key);

		for (n = 0; n < 16; n++) {
    
     SWAP(sk[n], sk[31 - n]) }
		unsigned char* fst = dst;
		i = len;
		while (i > 0)
		{
    
    
			memcpy(tmp, src, 16);
			OneRound(sk, src, dst);
			for (n = 0; n < 16; n++) {
    
     dst[n] = dst[n] ^ iv[n]; }
			memcpy(iv, tmp, 16);
			src += 16;
			dst += 16;
			i -= 16;
		}

		i = fst[len - 1];
		if (i >= 1 && i <= 16)
		{
    
    
			memset(fst + len - i, 0, i);
			len -= i;
		}
	}

private:
	static void SetKey(unsigned long SK[32], const unsigned char key[16])
	{
    
    
		unsigned long MK[4];
		unsigned long k[36];
		GET_ULONG_BE(MK[0], key, 0);
		GET_ULONG_BE(MK[1], key, 4);
		GET_ULONG_BE(MK[2], key, 8);
		GET_ULONG_BE(MK[3], key, 12);
		k[0] = MK[0] ^ FK[0];
		k[1] = MK[1] ^ FK[1];
		k[2] = MK[2] ^ FK[2];
		k[3] = MK[3] ^ FK[3];
		for (unsigned long i = 0; i < 32; i++)
		{
    
    
			k[i + 4] = k[i] ^ (CalciRK(k[i + 1] ^ k[i + 2] ^ k[i + 3] ^ CK[i]));
			SK[i] = k[i + 4];
		}
	}

	static void OneRound(unsigned long sk[32], const unsigned char input[16], unsigned char output[16])
	{
    
    
		unsigned long i = 0;
		unsigned long ulbuf[36];
		memset(ulbuf, 0, sizeof(ulbuf));
		GET_ULONG_BE(ulbuf[0], input, 0);
		GET_ULONG_BE(ulbuf[1], input, 4);
		GET_ULONG_BE(ulbuf[2], input, 8);
		GET_ULONG_BE(ulbuf[3], input, 12);
		while (i < 32)
		{
    
    
			ulbuf[i + 4] = RoundF(ulbuf[i], ulbuf[i + 1], ulbuf[i + 2], ulbuf[i + 3], sk[i]);
			i++;
		}
		PUT_ULONG_BE(ulbuf[35], output, 0);
		PUT_ULONG_BE(ulbuf[34], output, 4);
		PUT_ULONG_BE(ulbuf[33], output, 8);
		PUT_ULONG_BE(ulbuf[32], output, 12);
	}


	static unsigned long CalciRK(unsigned long ka)
	{
    
    
		unsigned long bb = 0;
		unsigned long rk = 0;
		unsigned char a[4];
		unsigned char b[4];
		PUT_ULONG_BE(ka, a, 0);
		b[0] = SBox(a[0]);
		b[1] = SBox(a[1]);
		b[2] = SBox(a[2]);
		b[3] = SBox(a[3]);
		GET_ULONG_BE(bb, b, 0);
		rk = bb ^ (ROTL(bb, 13)) ^ (ROTL(bb, 23));
		return rk;
	}

	/// <summary>
	/// 8位输入输出的非线性置换,增强混淆性
	/// </summary>
	/// <param name="inch"></param>
	/// <returns></returns>
	static unsigned char SBox(unsigned char inch)
	{
    
    
		unsigned char* pTable = (unsigned char*)SboxTable;
		unsigned char retVal = (unsigned char)(pTable[inch]);
		return retVal;
	}

	/// <summary>
	/// 线性扩散层,通过循环移位和异或操作扩散比特。
	/// </summary>
	/// <param name="ka"></param>
	/// <returns></returns>
	static unsigned long Linear(unsigned long ka)
	{
    
    
		unsigned long bb = 0;
		unsigned long c = 0;
		unsigned char a[4];
		unsigned char b[4];
		PUT_ULONG_BE(ka, a, 0);
		b[0] = SBox(a[0]);
		b[1] = SBox(a[1]);
		b[2] = SBox(a[2]);
		b[3] = SBox(a[3]);
		GET_ULONG_BE(bb, b, 0);
		c = bb ^ (ROTL(bb, 2)) ^ (ROTL(bb, 10)) ^ (ROTL(bb, 18)) ^ (ROTL(bb, 24));
		return c;
	}

	static unsigned long RoundF(unsigned long x0, unsigned long x1, unsigned long x2, unsigned long x3, unsigned long rk)
	{
    
    
		return (x0 ^ Linear(x1 ^ x2 ^ x3 ^ rk));
	}
};
#endif
代码分析说明
  1. 头文件与宏定义

    • 使用 #ifndef 防止重复包含。
    • 定义了大端序转换宏 GET_ULONG_BEPUT_ULONG_BE,用于字节与32位无符号整数的转换。
    • 定义位操作宏 SHL(左移)、ROTL(循环左移)和 SWAP(交换变量)。
  2. 核心组件

    • S盒(SboxTable):16×16的字节替换表,用于非线性变换。
    • 固定参数FK(初始密钥扩展常量)和 CK(轮密钥生成常量)。
    • 轮函数与密钥扩展:通过 sm4Ltsm4Fsm4CalciRK 等函数实现。
  3. 功能函数

    • SetKey:生成32轮加密密钥。
    • OneRound:单轮加密/解密操作。
    • EncDec:实现CBC模式的分组处理。
  4. CBC模式实现

    • 加密
      1. 填充数据到16字节倍数(PKCS#7填充)。
      2. 每块与前一块密文(或IV)异或后加密。
    • 解密
      1. 解密后与前一密文块(或IV)异或。
      2. 移除填充,末尾置零。
  5. 跨平台兼容性

    • 通过宏定义处理大端序,确保不同平台数据一致性。

5.2 单元测试

// 测试代码
void Test_SM4()
{
    
    
    const char* pMsg = "Hello, World,I love 中国.";
    unsigned char key[17] = {
    
     1, 2, 3, 4, 5, 6, 7, 8,
                     'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 0
    };

    unsigned char ciphertext[256] = {
    
     0 };
    unsigned char iv[16] = {
    
     0 };
    size_t out_len = strlen(pMsg);
    SM4::Enc((unsigned char*)pMsg, ciphertext, out_len, iv, key);
    printf("明文数据:%d,%s\n加密结果:%d,", (int)strlen(pMsg), pMsg, (int)out_len);
    for (size_t i = 0; i < out_len; i++)
    {
    
    
        printf("%02X ", ciphertext[i]);
    }
    printf("\n");

    unsigned char text[256] = {
    
     0 };
    size_t text_len = out_len;
    memset(iv, 0, sizeof(iv));
    SM4::Dec((unsigned char*)ciphertext, text, text_len, iv, key);
    text[text_len] = '\0';
    printf("解密结果:%d,%s", (int)text_len, text);
}

示例中对Hello, World,I love 中国.分别进行了SM4的加密和解密。

5.2 测试结果

明文数据:26,Hello, World,I love 中国.
加密结果:32,9F 0C D6 19 50 33 2F DB 4B 29 10 8E 42 AE 03 C8 01 CB 04 7C 8B 80 E3 C6 B0 22 AF 6D A2 2E 52 67
解密结果:26,Hello, World,I love 中国.