在项目当中可能需要使用到GMSSL国密算法库,笔者先在Ubuntu下进行了尝试,但一直搞不定环境,发现OpenEuler下自带国密算法库,进行了初步的调用尝试并取得成功,现分享一下经验。
首先,我们可以在官网文档( https://docs.openeuler.org/zh/docs/24.03_LTS_SP1/docs/ShangMi/%E7%AE%97%E6%B3%95%E5%BA%93.html)当中看到对OpenEuelr封装国密算法的一些简要介绍
可惜的是并未对API使用举例,本人Linux基础薄弱,官方文档也有些看不懂,所以就拿着材料去找了DEEPSEEK,最终也是完成了SM4算法的使用,详情如下。
首先呢,如官方文档所言,我们需要开启SM4加密
在我的代码中,go使用GMSSL,主要是通过CGO来实现的。
package sm4
/*
#cgo LDFLAGS: -lssl -lcrypto
#include <openssl/evp.h>
#include <openssl/rand.h>
#include <string.h>
// 加密函数
void sm4_encrypt(const unsigned char *plaintext, int plaintext_len,
const unsigned char *key, const unsigned char *iv,
unsigned char *ciphertext, int *ciphertext_len) {
EVP_CIPHER_CTX *ctx;
int len;
// 创建并初始化上下文
if(!(ctx = EVP_CIPHER_CTX_new())) {
return;
}
// 初始化加密操作,启用填充
if(1 != EVP_EncryptInit_ex(ctx, EVP_sm4_cbc(), NULL, key, iv)) {
EVP_CIPHER_CTX_free(ctx);
return;
}
// 提供要加密的数据并进行加密
if(1 != EVP_EncryptUpdate(ctx, ciphertext, &len, plaintext, plaintext_len)) {
EVP_CIPHER_CTX_free(ctx);
return;
}
*ciphertext_len = len;
// 完成加密操作
if(1 != EVP_EncryptFinal_ex(ctx, ciphertext + len, &len)) {
EVP_CIPHER_CTX_free(ctx);
return;
}
*ciphertext_len += len;
// 清理上下文
EVP_CIPHER_CTX_free(ctx);
}
// 解密函数
void sm4_decrypt(const unsigned char *ciphertext, int ciphertext_len,
const unsigned char *key, const unsigned char *iv,
unsigned char *plaintext, int *plaintext_len) {
EVP_CIPHER_CTX *ctx;
int len;
// 创建并初始化上下文
if(!(ctx = EVP_CIPHER_CTX_new())) {
return;
}
// 初始化解密操作,启用填充
if(1 != EVP_DecryptInit_ex(ctx, EVP_sm4_cbc(), NULL, key, iv)) {
EVP_CIPHER_CTX_free(ctx);
return;
}
// 提供要解密的数据并进行解密
if(1 != EVP_DecryptUpdate(ctx, plaintext, &len, ciphertext, ciphertext_len)) {
EVP_CIPHER_CTX_free(ctx);
return;
}
*plaintext_len = len;
// 完成解密操作
if(1 != EVP_DecryptFinal_ex(ctx, plaintext + len, &len)) {
EVP_CIPHER_CTX_free(ctx);
return;
}
*plaintext_len += len;
// 清理上下文
EVP_CIPHER_CTX_free(ctx);
}
*/
import "C"
import (
"crypto/rand"
"errors"
"unsafe"
)
const SM4KeySize = 16
const SM4IVSize = 16
// SM4Encrypt 使用 SM4 算法进行加密
func SM4Encrypt(plaintext []byte, key []byte) ([]byte, []byte, error) {
if len(key) != SM4KeySize {
return nil, nil, errors.New("SM4 key must be 16 bytes")
}
iv := make([]byte, 16)
_, err := rand.Read(iv)
if err != nil {
return nil, nil, err
}
const IVSize = 16
ciphertext := make([]byte, len(plaintext)+IVSize)
var ciphertextLen C.int
C.sm4_encrypt(
(*C.uchar)(unsafe.Pointer(&plaintext[0])),
C.int(len(plaintext)),
(*C.uchar)(unsafe.Pointer(&key[0])),
(*C.uchar)(unsafe.Pointer(&iv[0])),
(*C.uchar)(unsafe.Pointer(&ciphertext[0])),
&ciphertextLen,
)
return ciphertext[:ciphertextLen], iv, nil
}
// SM4Decrypt 使用 SM4 算法进行解密
func SM4Decrypt(ciphertext []byte, key []byte, iv []byte) ([]byte, error) {
if len(key) != SM4KeySize {
return nil, errors.New("SM4 key must be 16 bytes")
}
if len(iv) != SM4IVSize {
return nil, errors.New("SM4 IV must be 16 bytes")
}
plaintext := make([]byte, len(ciphertext))
var plaintextLen C.int
C.sm4_decrypt(
(*C.uchar)(unsafe.Pointer(&ciphertext[0])),
C.int(len(ciphertext)),
(*C.uchar)(unsafe.Pointer(&key[0])),
(*C.uchar)(unsafe.Pointer(&iv[0])),
(*C.uchar)(unsafe.Pointer(&plaintext[0])),
&plaintextLen,
)
return plaintext[:plaintextLen], nil
}
代码结构分析
C语言部分
-
头文件引入:
- 引入了OpenSSL的EVP(Envelope)和随机数生成相关头文件
- EVP提供了统一的加密接口
-
加密函数
sm4_encrypt
:- 使用EVP接口初始化SM4-CBC加密上下文
- 处理加密数据并返回结果
- 自动处理PKCS#7填充
-
解密函数
sm4_decrypt
:- 使用EVP接口初始化SM4-CBC解密上下文
- 处理解密数据并返回结果
- 自动去除PKCS#7填充
Go语言封装部分
-
常量定义:
SM4KeySize = 16
:SM4密钥长度16字节(128位)SM4IVSize = 16
:初始化向量长度16字节
-
SM4Encrypt函数:
- 参数:明文和密钥
- 功能:
- 检查密钥长度
- 生成随机IV
- 调用C函数进行加密
- 返回:密文和IV
-
SM4Decrypt函数:
- 参数:密文、密钥和IV
- 功能:
- 检查密钥和IV长度
- 调用C函数进行解密
- 返回:明文
关键实现细节
-
CGO调用:
- 使用
// #cgo
指令指定链接的OpenSSL库 - 通过
unsafe.Pointer
在Go和C之间传递数据指针
- 使用
-
加密模式:
- 使用CBC模式(密码分组链接模式)
- 需要初始化向量(IV)来增强安全性
-
错误处理:
- 检查密钥和IV长度
- 处理随机数生成错误
-
内存管理:
- OpenSSL的EVP接口会自动管理加密上下文内存
- Go的切片操作确保返回正确长度的数据
使用示例
key := []byte("1234567890abcdef") // 16字节密钥
plaintext := []byte("Hello, SM4!")
// 加密
ciphertext, iv, err := SM4Encrypt(plaintext, key)
if err != nil {
panic(err)
}
// 解密
decrypted, err := SM4Decrypt(ciphertext, key, iv)
if err != nil {
panic(err)
}
fmt.Println(string(decrypted)) // 输出: Hello, SM4!
安全性考虑
- 每次加密使用随机IV,避免相同明文产生相同密文
- 自动处理填充,确保数据块对齐
- 使用经过验证的OpenSSL实现,避免密码学实现错误
这种封装方式结合了Go的易用性和OpenSSL的成熟密码学实现,适合需要SM4算法的Go应用程序。
可以写一个测试程序,看看是否能成功运行
package sm4
import (
"bytes"
"crypto/rand"
"testing"
)
// 测试辅助函数:生成随机字节
func randomBytes(n int) ([]byte, error) {
b := make([]byte, n)
_, err := rand.Read(b)
if err != nil {
return nil, err
}
return b, nil
}
// 测试用例1:常规短文本加密解密
func TestShortText(t *testing.T) {
plaintext := []byte("SM4测试")
const KeySize = 16
key, err := randomBytes(KeySize)
if err != nil {
t.Fatalf("生成随机密钥失败: %v", err)
}
ciphertext, iv, err := SM4Encrypt(plaintext, key)
if err != nil {
t.Fatalf("加密失败: %v", err)
}
decrypted, err := SM4Decrypt(ciphertext, key, iv)
if err != nil {
t.Fatalf("解密失败: %v", err)
}
if !bytes.Equal(plaintext, decrypted) {
t.Fatal("解密结果与原文不符")
}
}
// 测试用例2:长文本加密解密
func TestLongText(t *testing.T) {
plaintext := bytes.Repeat([]byte("Go语言SM4测试-"), 100) // 约1.5KB数据
const KeySize = 16
key, err := randomBytes(KeySize)
if err != nil {
t.Fatalf("生成随机密钥失败: %v", err)
}
ciphertext, iv, err := SM4Encrypt(plaintext, key)
if err != nil {
t.Fatal(err)
}
decrypted, err := SM4Decrypt(ciphertext, key, iv)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(plaintext, decrypted) {
t.Fatal("长文本解密结果不一致")
}
}
// 测试用例3:二进制数据加密解密
func TestBinaryData(t *testing.T) {
plaintext, err := randomBytes(512) // 512字节随机数据
if err != nil {
t.Fatalf("生成随机数据失败: %v", err)
}
const KeySize = 16
key, err := randomBytes(KeySize)
if err != nil {
t.Fatalf("生成随机密钥失败: %v", err)
}
ciphertext, iv, err := SM4Encrypt(plaintext, key)
if err != nil {
t.Fatal(err)
}
decrypted, err := SM4Decrypt(ciphertext, key, iv)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(plaintext, decrypted) {
t.Fatal("二进制数据解密失败")
}
}
// 测试用例4:多次加密结果不同(因IV随机)
func TestRandomIV(t *testing.T) {
plaintext := []byte("相同输入不同IV")
const KeySize = 16
key, err := randomBytes(KeySize)
if err != nil {
t.Fatalf("生成随机密钥失败: %v", err)
}
// 第一次加密
ciphertext1, iv1, err := SM4Encrypt(plaintext, key)
if err != nil {
t.Fatal(err)
}
// 第二次加密
ciphertext2, iv2, err := SM4Encrypt(plaintext, key)
if err != nil {
t.Fatal(err)
}
// IV必须不同
if bytes.Equal(iv1, iv2) {
t.Fatal("IV未随机化")
}
// 密文应该不同
if bytes.Equal(ciphertext1, ciphertext2) {
t.Fatal("相同输入生成相同密文,存在安全风险")
}
}
可以看到测试也是被顺利通过了
第二种方法是调用国密算法的另一个go语言封装,为什么不用官网的封装呢?反正我是在调用的时候碰到了一点问题哈,我安装GMSSL库的时候就出现了一些有关权限的问题,官网的go封装又依赖GMSSL库,所以一直没搞定,最后又发现了一个https://github.com/tjfoc/gmsm,这里的加密算法可以直接在go环境当中使用,非常简单友好
package main
import (
"crypto/rand"
"fmt"
"log"
"github.com/tjfoc/gmsm/sm4"
)
func main() {
// 原始数据
plaintext := []byte("这是一段需要加密的敏感数据,SM4是中国商用密码算法")
// 生成16字节的随机密钥(SM4密钥长度为128位)
key := make([]byte, 16)
if _, err := rand.Read(key); err != nil {
log.Fatalf("生成随机密钥失败: %v", err)
}
// 加密
ciphertext, err := sm4.Sm4Ecb(key, plaintext, true)
if err != nil {
log.Fatalf("加密失败: %v", err)
}
fmt.Printf("原始数据: %s\n", plaintext)
fmt.Printf("加密密钥: %x\n", key)
fmt.Printf("加密结果: %x\n", ciphertext)
// 解密
decrypted, err := sm4.Sm4Ecb(key, ciphertext, false)
if err != nil {
log.Fatalf("解密失败: %v", err)
}
fmt.Printf("解密结果: %s\n", decrypted)
// 验证解密结果是否与原始数据一致
if string(decrypted) != string(plaintext) {
log.Fatal("解密结果与原始数据不匹配")
} else {
fmt.Println("验证通过: 解密结果与原始数据一致")
}
}
可以看到也是可以正常使用的,代码比系统调用要简洁一些