Bootstrap

【Golang】国密SM2公钥私钥序列化到redis中并加密解密实战

【Golang】国密SM2公钥私钥序列化到redis中并加密解密实战

0.需求描述

①使用同济大学的SM2国密加密解密工具,生成的公钥和私钥想要转为字符串,并存入redis中,用于系统的取用.
②这个生成的字符串公钥和私钥, 将与javaorg.bouncycastle.crypto生成的公私钥完全互通使用.

想要生成的公钥样例(04开头):

04D31AAB6419AE712FA6B773547C2B766AFCD6D43D43E21D2BB07862D8E62BFDFA5920DEDA43C06B67D9D408E285069C936547AED1E00F89F76DF59BF2EA74A702

想要生成私钥的样例:

40EC164EBFF47B4B82B8C09580C730B6872AD023BDF3E83E4F43A336F0ACBF04

1.生成公钥私钥

①将加密解密包引入(我用的版本 v1.4.1):

go get github.com/tjfoc/gmsm

②编写代码

可能需要导入的包:

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"errors"
	"fmt"
	"github.com/tjfoc/gmsm/sm2"
	"math/big"
	"strconv"
	"strings"
)

生成公钥和私钥对象:

// 生成公钥和私钥对象
func generateLoc() (*sm2.PublicKey, *sm2.PrivateKey) {
	// 生成密钥对
	privKey, err := sm2.GenerateKey(rand.Reader)
	if err != nil {
		fmt.Println("生成密钥对失败:", err)
		return nil, nil
	}
	return &privKey.PublicKey, privKey
}

将公钥对象转为字符串:

// PublicKeyToString 公钥sm2.PublicKey转字符串(与java中org.bouncycastle.crypto生成的公私钥完全互通使用)
func PublicKeyToString(publicKey *sm2.PublicKey) string {
	xBytes := publicKey.X.Bytes()
	yBytes := publicKey.Y.Bytes()

	// 确保坐标字节切片长度相同
	byteLen := len(xBytes)
	if len(yBytes) > byteLen {
		byteLen = len(yBytes)
	}

	// 为坐标补齐前导零
	xBytes = append(make([]byte, byteLen-len(xBytes)), xBytes...)
	yBytes = append(make([]byte, byteLen-len(yBytes)), yBytes...)

	// 添加 "04" 前缀
	publicKeyBytes := append([]byte{0x04}, append(xBytes, yBytes...)...)

	return strings.ToUpper(hex.EncodeToString(publicKeyBytes))
}

将私钥对象转为字符串:

// PrivateKeyToString 私钥sm2.PrivateKey 转字符串(与java中org.bouncycastle.crypto生成的公私钥完全互通使用)
func PrivateKeyToString(privateKey *sm2.PrivateKey) string {
	return strings.ToUpper(hex.EncodeToString(privateKey.D.Bytes()))
}

将公钥和私钥字符串载入redis:

// SM2_GenerateKeyPair_Local sm2 本地生成密钥对(与java中org.bouncycastle.crypto生成的公私钥完全互通使用)
//
//	@version	latest
func SM2_GenerateKeyPair_Local() (publicKeyToString, privateKeyToString string) {
	// 生成密钥对
	publicKeyObj, privateKeyObj := generateLoc()
	publicKeyToString = PublicKeyToString(publicKeyObj)
	privateKeyToString = PrivateKeyToString(privateKeyObj)
	redisTemplate := redis.RedisTemplate// 你自己的redis客户端获取方式
	redisTemplate.Set(context.Background(), "publicKey", publicKeyToString, g.OneDayDuration)
	redisTemplate.Set(context.Background(), "privateKey", privateKeyToString, g.OneDayDuration)
	return
}

③测试用例

func TestSM2_Local_GenerateKeyPair(t *testing.T) {
	// 生成密钥对
	publicKeyObj, privateKeyObj := generateLoc()
	// 输出公钥和私钥字符串
	t.Log("公钥字符串:", PublicKeyToString(publicKeyObj))
	// 040a5cccc33685eade33b0a1a40f1eea0f86ae93bd3cbb9f88fa466ca49a87bdbcd1ab65c9cb9f587a3b1f6d143f964acab78a23c2c37b1c16e2d16b796861f7bf
	t.Log("私钥字符串:", PrivateKeyToString(privateKeyObj))
	// 5b8037839b43ee13804e4c9fb3626b8949f0f58729547f0da4e8415481243b03
}

打印结果:

=== RUN   TestSM2_Local_GenerateKeyPair
    sm2_test.go:14: 公钥字符串:040a5cccc33685eade33b0a1a40f1eea0f86ae93bd3cbb9f88fa466ca49a87bdbcd1ab65c9cb9f587a3b1f6d143f964acab78a23c2c37b1c16e2d16b796861f7bf
    sm2_test.go:16: 私钥字符串:5b8037839b43ee13804e4c9fb3626b8949f0f58729547f0da4e8415481243b03
--- PASS: TestSM2_Local_GenerateKeyPair (0.00s)
PASS

2.使用公钥加密

①需求描述

要使用上面生成并缓存在redis中的公钥字符串,对内容进行加密.

②编写代码

将公钥字符串反序列化转为公钥对象:

// StringToPublicKey 公钥字符串还原为 sm2.PublicKey 对象(与java中org.bouncycastle.crypto生成的公私钥完全互通使用)
func StringToPublicKey(publicKeyStr string) (*sm2.PublicKey, error) {
	publicKeyBytes, err := hex.DecodeString(publicKeyStr)
	if err != nil {
		return nil, err
	}

	// 提取 x 和 y 坐标字节切片
	curve := sm2.P256Sm2().Params()
	byteLen := (curve.BitSize + 7) / 8
	xBytes := publicKeyBytes[1 : byteLen+1]
	yBytes := publicKeyBytes[byteLen+1 : 2*byteLen+1]

	// 将字节切片转换为大整数
	x := new(big.Int).SetBytes(xBytes)
	y := new(big.Int).SetBytes(yBytes)

	// 创建 sm2.PublicKey 对象
	publicKey := &sm2.PublicKey{
		Curve: curve,
		X:     x,
		Y:     y,
	}

	return publicKey, nil
}

使用公钥对象加密字符串:

// publicKeyStr 公钥字符串, text 待加密明文字符串
func encryptLoc(publicKeyStr, text string) (string, error) {
	publicKey, err := StringToPublicKey(publicKeyStr)
	if err != nil {
		return "", err
	}
	encryptStr, _ := sm2.Encrypt(publicKey, []byte(text), rand.Reader, sm2.C1C2C3)
	encodeToString := hex.EncodeToString(encryptStr)
	fmt.Println("加密后的字符串:", encodeToString)
	return strings.ToUpper(encodeToString), nil
}

redis取公钥并加密明文, 将上面两个函数整合:

// SM2_Encrypt_Local sm2 本地加密
//
//	@version	latest
func SM2_Encrypt_Local(text string) (string, error) {
	// 先检测redis中是否缓存有密钥
	redisTemplate := redis.RedisTemplate// 你自己的redis客户端获取方式
	publicKeyStr, _ := redisTemplate.Get(context.Background(), "publicKey").Result()
	publicKey := strings.ReplaceAll(publicKeyStr, "\"", "")
	if publicKey == "" {
		// 重新生成密钥
		publicKey, _ = SM2_GenerateKeyPair_Local()
	}
	encrypt, err := encryptLoc(publicKey, text)
	if err != nil {
		return "", err
	}
	return encrypt, nil

}

③测试用例

将字符串 ABC123加密

func TestSM2_Local_Encrypt(t *testing.T) {
	publicKeyStr := "040a5cccc33685eade33b0a1a40f1eea0f86ae93bd3cbb9f88fa466ca49a87bdbcd1ab65c9cb9f587a3b1f6d143f964acab78a23c2c37b1c16e2d16b796861f7bf"
	publicKey, _ := StringToPublicKey(publicKeyStr)
	encryptStr, _ := sm2.Encrypt(publicKey, []byte("ABC123"), rand.Reader, sm2.C1C2C3)
	encodeToString := hex.EncodeToString(encryptStr)
	t.Log("加密后的字符串:", encodeToString)
}

打印结果:

=== RUN   TestSM2_Local_Encrypt
    sm2_test.go:25: 加密后的字符串: 04d5bfb00dff8607a07337ff5999cbad8713a286c2655797148211dbf315b616faad48999428faf1597413d5d1cd81a425ccf192783c28cccd7a00ce618c759e29b82e5a34af99c34b0792d81026a6ca16317e76e11190c6564810a0737ceebb26a555498b23e0
--- PASS: TestSM2_Local_Encrypt (0.00s)
PASS

3.使用私钥解密

①需求描述

要使用上面生成并缓存在redis中的私钥字符串,对加密的字符串进行解密

②编写代码

将私钥字符串反序列化转为私钥对象:

// StringToPrivateKey 私钥还原为 sm2.PrivateKey对象(与java中org.bouncycastle.crypto生成的公私钥完全互通使用)
func StringToPrivateKey(privateKeyStr string, publicKey *sm2.PublicKey) (*sm2.PrivateKey, error) {
	privateKeyBytes, err := hex.DecodeString(privateKeyStr)
	if err != nil {
		return nil, err
	}

	// 将字节切片转换为大整数
	d := new(big.Int).SetBytes(privateKeyBytes)

	// 创建 sm2.PrivateKey 对象
	privateKey := &sm2.PrivateKey{
		PublicKey: *publicKey,
		D:         d,
	}

	return privateKey, nil
}

使用私钥对象解密密文字符串:

func decryptLoc(publicKeyStr, privateKeyStr, cipherText string) (string, error) {
	publicKeyObj, err := StringToPublicKey(publicKeyStr)
	if err != nil {
		fmt.Println(err)
	}
	privateKeyObj, err := StringToPrivateKey(privateKeyStr, publicKeyObj)
	if err != nil {
		fmt.Println(err)
	}
	decodeString, err := hex.DecodeString(cipherText)
	decrypt, err := sm2.Decrypt(privateKeyObj, decodeString, sm2.C1C2C3)
	if err != nil {
		fmt.Println(err)
	}
	resultStr := string(decrypt)
	fmt.Println("解密后的字符串:", resultStr)
	return resultStr, nil
}

redis取私钥字符串并解密密文, 将上面两个函数整合:

// SM2_Decrypt_Local sm2 本地解密
//
//	@version	latest
func SM2_Decrypt_Local(cipherText string) (string, error) {
	if cipherText == "" {
		return "", nil
	}
	// 先检测redis中是否缓存有密钥
	redisTemplate := redis.RedisTemplate
	publicKeyStr, _ := redisTemplate.Get(context.Background(), g.PUBLICKEY).Result()
	publicKey := strings.ReplaceAll(publicKeyStr, "\"", "")
	privateKeyStr, _ := redisTemplate.Get(context.Background(), g.PRIVATEKEY).Result()
	privateKey := strings.ReplaceAll(privateKeyStr, "\"", "")

	if publicKey == "" || privateKey == "" {
		return "", errors.New("公钥或私钥缺失, 请重新生成密钥后再操作")
	}
	decrypt, err := decryptLoc(publicKey, privateKey, cipherText)
	if err != nil {
		return "", err
	}
	return decrypt, nil
}

③测试用例

将字符串解密

04d5bfb00dff8607a07337ff5999cbad8713a286c2655797148211dbf315b616faad48999428faf1597413d5d1cd81a425ccf192783c28cccd7a00ce618c759e29b82e5a34af99c34b0792d81026a6ca16317e76e11190c6564810a0737ceebb26a555498b23e0
func TestSM2_Local_Decrypt(t *testing.T) {
	publicKeyStr := "040a5cccc33685eade33b0a1a40f1eea0f86ae93bd3cbb9f88fa466ca49a87bdbcd1ab65c9cb9f587a3b1f6d143f964acab78a23c2c37b1c16e2d16b796861f7bf"
	privateKeyStr := "5b8037839b43ee13804e4c9fb3626b8949f0f58729547f0da4e8415481243b03"
	cipherText := "04d5bfb00dff8607a07337ff5999cbad8713a286c2655797148211dbf315b616faad48999428faf1597413d5d1cd81a425ccf192783c28cccd7a00ce618c759e29b82e5a34af99c34b0792d81026a6ca16317e76e11190c6564810a0737ceebb26a555498b23e0"
	publicKeyObj, err := StringToPublicKey(publicKeyStr)
	if err != nil {
		t.Fatal(err)
	}
	privateKeyObj, err := StringToPrivateKey(privateKeyStr, publicKeyObj)
	if err != nil {
		t.Fatal(err)
	}
	decodeString, err := hex.DecodeString(cipherText)
	decrypt, err := sm2.Decrypt(privateKeyObj, decodeString, sm2.C1C2C3)
	if err != nil {
		t.Fatal(err)
	}
	resultStr := string(decrypt)
	t.Log("解密后的字符串:", resultStr)
}

打印结果:

=== RUN   TestSM2_Local_Decrypt
    sm2_test.go:47: 解密后的字符串: ABC123
--- PASS: TestSM2_Local_Decrypt (0.00s)
PASS

4.总结

主要需要将公钥或私钥对象转为字符串PublicKeyToStringPrivateKeyToString函数, 和将字符串转回公钥私钥对象StringToPublicKeyStringToPrivateKey函数.这是整个流程的关键点.

5.感悟

在java与golang代码开发的项目, 想要共享使用转为字符串的SM2公钥和私钥, 卡了我两个半月,每天都在问ChatGPT,怎么弄, 怎么解决, 但是提供的代码都是不尽人意.于是我直接上GitHub使用人肉方式摸排代码, 终于看到一些代码给了我灵感和指导.有人一直担心java迁移golang,有安全隐患,我想有两个方面的顾虑吧,第一个是人守旧的思维, 愿意接受稳定,耐用,可靠的.不愿意接受不稳定, 不熟悉的东西.第二认为go的生态不完整, 不成熟.需要的插件与工具不齐全, 功能不完整.但就我来看, go生态生机勃勃, 谷歌官方大力支持.只要谷歌是全球互联网领导者, golang就依然生机勃勃.

;