【Golang】国密SM2公钥私钥序列化到redis中并加密解密实战
0.需求描述
①使用同济大学的SM2国密加密解密工具,生成的公钥和私钥想要转为字符串,并存入redis中,用于系统的取用.
②这个生成的字符串公钥和私钥, 将与java
中org.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.总结
主要需要将公钥或私钥对象转为字符串PublicKeyToString
和PrivateKeyToString
函数, 和将字符串转回公钥私钥对象StringToPublicKey
和StringToPrivateKey
函数.这是整个流程的关键点.
5.感悟
在java与golang代码开发的项目, 想要共享使用转为字符串的SM2公钥和私钥, 卡了我两个半月,每天都在问ChatGPT,怎么弄, 怎么解决, 但是提供的代码都是不尽人意.于是我直接上GitHub使用人肉方式摸排代码, 终于看到一些代码给了我灵感和指导.有人一直担心java迁移golang,有安全隐患,我想有两个方面的顾虑吧,第一个是人守旧的思维, 愿意接受稳定,耐用,可靠的.不愿意接受不稳定, 不熟悉的东西.第二认为go的生态不完整, 不成熟.需要的插件与工具不齐全, 功能不完整.但就我来看, go生态生机勃勃, 谷歌官方大力支持.只要谷歌是全球互联网领导者, golang就依然生机勃勃.