Go 实现 Mysql AES 与 Scanner/Valuer 自动加解密

本文将介绍 Go 语言中的 forgoer/openssl 加解密库,并基于它实现 MySQL AES_ENCRYPT 函数的 Go 版本,最后定义一个可自动加解密的自定义数据类型,该类型实现 database/sql 的 Scanner 和 Valuer 接口,使其在数据库读写时自动进行加解密。

forgoer/openssl 加解密库

forgoer/openssl (github.com/forgoer/openssl) 是对 OpenSSL 库的封装,支持对称和非对称加密/解密:

  • 对称加密:AES、DES、3DES
  • 非对称加密:RSA
  • 哈希摘要:HMAC-SHA

安装

arduino 复制代码
go get -u github.com/forgoer/openssl

使用方法

  1. AES 加密 密钥长度可以是 16/24/32 个字符(对应 128/192/256 位)。
  • AES-ECB 模式
less 复制代码
src := []byte("123456")
key := []byte("1234567890123456")
dst, _ := openssl.AesECBEncrypt(src, key, openssl.PKCS7_PADDING)
fmt.Printf(base64.StdEncoding.EncodeToString(dst))  // yXVUkR45PFz0UfpbDB8/ew==

dst, _ = openssl.AesECBDecrypt(dst, key, openssl.PKCS7_PADDING)
fmt.Println(string(dst))  // 123456
  • AES-CBC 模式
less 复制代码
src := []byte("123456")
key := []byte("1234567890123456")
iv := []byte("1234567890123456")
dst, _ := openssl.AesCBCEncrypt(src, key, iv, openssl.PKCS7_PADDING)
fmt.Println(base64.StdEncoding.EncodeToString(dst))  // 1jdzWuniG6UMtoa3T6uNLA==

dst, _ = openssl.AesCBCDecrypt(dst, key, iv, openssl.PKCS7_PADDING)
fmt.Println(string(dst))  // 123456
  1. DES 加密 密钥长度必须为 8 个字符(64 位)。
  • DES-ECB 模式
css 复制代码
openssl.DesECBEncrypt(src, key, openssl.PKCS7_PADDING)
openssl.DesECBDecrypt(src, key, openssl.PKCS7_PADDING)
  • DES-CBC 模式
css 复制代码
openssl.DesCBCEncrypt(src, key, iv, openssl.PKCS7_PADDING)
openssl.DesCBCDecrypt(src, key, iv, openssl.PKCS7_PADDING)
  1. 3DES 加密 密钥长度必须为 24 个字符(192 位)。
  • 3DES-ECB 模式
css 复制代码
openssl.Des3ECBEncrypt(src, key, openssl.PKCS7_PADDING)
openssl.Des3ECBDecrypt(src, key, openssl.PKCS7_PADDING)
  • 3DES-CBC 模式
css 复制代码
openssl.Des3CBCEncrypt(src, key, iv, openssl.PKCS7_PADDING)
openssl.Des3CBCDecrypt(src, key, iv, openssl.PKCS7_PADDING)
  1. RSA 加密
go 复制代码
// 生成 RSA 密钥
openssl.RSAGenerateKey(bits int, out io.Writer)
openssl.RSAGeneratePublicKey(priKey []byte, out io.Writer)

// RSA 加密与解密
openssl.RSAEncrypt(src, pubKey []byte) ([]byte, error)
openssl.RSADecrypt(src, priKey []byte) ([]byte, error)

// RSA 签名与验证
openssl.RSASign(src []byte, priKey []byte, hash crypto.Hash) ([]byte, error)
openssl.RSAVerify(src, sign, pubKey []byte, hash crypto.Hash) error
  1. HMAC-SHA 哈希
scss 复制代码
// SHA1
Sha1(str string) []byte
HmacSha1(key string, data string) []byte
HmacSha1ToString(key string, data string) string

// SHA256
Sha256(str string) []byte
HmacSha256(key string, data string) []byte
HmacSha256ToString(key string, data string) string

Mysql 中 AES_ENCRYPT 函数 对应 Go 实现方式

我们创建一个 test_user 表,其中 phone 字段存储 AES 加密后的手机号:

sql 复制代码
CREATE TABLE `test_user` (
  `id` int NOT NULL AUTO_INCREMENT,
  `phone` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

使用 AES_ENCRYPT 进行数据插入和查询:(密钥 1234567890abcdef ,需要加密的字符串 15000000xxx)

sql 复制代码
-- 插入加密手机号
INSERT INTO test_user (id, phone) VALUES (1, HEX(AES_ENCRYPT('15000000xxx', '1234567890abcdef')));

-- 查询手机号
SELECT * FROM test_user WHERE AES_DECRYPT(UNHEX(phone), '1234567890abcdef') = '15000000xxx';

示例数据:

diff 复制代码
+----+----------------------------------+
| id | phone                            |
+----+----------------------------------+
|  1 | 42A8B627F45DEF560DA50C9B0727AC67 |
+----+----------------------------------+

Go 语言实现 Mysql 中 AES_ENCRYPT 函数:

go 复制代码
package main

import (
 "encoding/hex"
 "fmt"
 "strings"

 "github.com/forgoer/openssl"
)

// SqlAesECBEncryptAndHex 对应 Mysql 函数 HEX(AES_ENCRYPT('明文','密钥'))
func SqlAesECBEncryptAndHex(src, key string) string {
 dst, _ := openssl.AesECBEncrypt([]byte(src), []byte(key), openssl.PKCS7_PADDING)
 return strings.ToUpper(hex.EncodeToString(dst))
}

// SqlHexAndAesECBDecrypt 对应 Mysql 函数 AES_DECRYPT(UNHEX(字段),'密钥')
func SqlHexAndAesECBDecrypt(src, key string) string {
 b, _ := hex.DecodeString(src)
 dst, _ := openssl.AesECBDecrypt(b, []byte(key), openssl.PKCS7_PADDING)
 return string(dst)
}

func main() {
 key := "1234567890abcdef" // 对称密钥长度必须是16的倍数
 src := "15000000xxx"      // 需要加密的字符串

 dst := SqlAesECBEncryptAndHex(src, key)
 s := SqlHexAndAesECBDecrypt(dst, key)

 fmt.Println(dst) // 42A8B627F45DEF560DA50C9B0727AC67
 fmt.Println(s)   // 15000000xxx
}

自定义类型 实现 Scanner & Valuer 接口自动进行加解密

基于上述对 Mysql 中 AES_ENCRYPT 函数的实现,自定义加密类型 Cipher ,实现 database/sql Scanner 和 Valuer 接口:

go 复制代码
var defaultKey atomic.Value

func DefaultKey() string { return defaultKey.Load().(string) }

// SetKey 设置密钥
func SetKey(s string) {
 defaultKey.Store(s)
}

type Cipher string

// Value implements the driver Valuer interface.
func (c Cipher) Value() (driver.Value, error) {
 if len(c) == 0 {
  return "", nil
 }
 return SqlAesECBEncryptAndHex(string(c), DefaultKey()), nil
}

// Scan implements the Scanner interface.
func (c *Cipher) Scan(src any) error {
 switch val := src.(type) {
 case nil:
  return nil
 case []byte:
  if len(val) == 0 {
   return nil
  }
  *c = Cipher(SqlHexAndAesECBDecrypt(string(val), DefaultKey()))
  return nil
 }
 return fmt.Errorf("failed to scan value: %v", src)
}

// SqlAesECBEncryptAndHex 对应 Mysql 函数 HEX(AES_ENCRYPT('明文','密钥'))
func SqlAesECBEncryptAndHex(src, key string) string {
 dst, _ := openssl.AesECBEncrypt([]byte(src), []byte(key), openssl.PKCS7_PADDING)
 return strings.ToUpper(hex.EncodeToString(dst))
}

// SqlHexAndAesECBDecrypt 对应 Mysql 函数 AES_DECRYPT(UNHEX(字段),'密钥')
func SqlHexAndAesECBDecrypt(src, key string) string {
 b, _ := hex.DecodeString(src)
 dst, _ := openssl.AesECBDecrypt(b, []byte(key), openssl.PKCS7_PADDING)
 return string(dst)
}

在 GORM 中使用 Cipher 类型:

scss 复制代码
func init() {
 SetKey("1234567890abcdef") // 设置密钥
}

type TESTUser struct {
 ID    int    `gorm:"column:id" json:"id"`
 Phone Cipher `gorm:"column:phone" json:"phone"`
}

func (T TESTUser) TableName() string {
 return "test_user"
}

// 使用了 GORM 这里省略 db 连接到数据库部分代码
func Create(res TESTUser) {
 db.Create(&res)
}

func TestCreate(t *testing.T) {
 Create(TESTUser{ID: 1, Phone: "15000000xxx"})
 // INSERT INTO `test_user` (`phone`,`id`) VALUES ('42A8B627F45DEF560DA50C9B0727AC67',1)
}

func Find(id int) (res TESTUser) {
 db.Where("id = ?", id).Find(&res)
 return res
}

func TestFind(t *testing.T) {
 res := Find(1)
 // SELECT * FROM `test_user` WHERE id = 1
 fmt.Println(res) // {1 15000000xxx}
}

func FindByPhone(phone Cipher) (res TESTUser) {
 db.Model(&TESTUser{}).Where("phone = ?", phone).Find(&res)
 return res
}

func TestFindByPhone(t *testing.T) {
 res := FindByPhone("15000000xxx")
 // SELECT * FROM `test_user` WHERE phone = '42A8B627F45DEF560DA50C9B0727AC67'
 fmt.Println(res) // {1 15000000xxx}
}

func Update(id int, phone Cipher) {
 db.Model(&TESTUser{}).Where("id = ?", id).Update("phone", phone)
}

func TestUpdate(t *testing.T) {
 Update(1, "99900000xxx")
 // UPDATE `test_user` SET `phone`='B9D93E92750E2CF2C0E49E6198FFBCA3' WHERE id = 1
}

References

github.com/forgoer/ope...

blog.itpub.net/29773961/vi...

相关推荐
Go_error24 分钟前
Badu/bus:Go 轻量级泛型发布/订阅事件总线
后端·go
Go_error27 分钟前
Go-redis:执行 Lua 脚本
后端·go
stark张宇4 小时前
Go并发调度器源码探秘:GMP模型之G/M/P底层数据结构完全拆解
go
学习星球1 天前
# 6G通感一体化(ISAC)技术深度解析——从原理到实战> <br />
go·信息与通信·媒体
小满zs1 天前
Go语言第十章(指针)
后端·google·go
学习星球2 天前
6G核心网架构深度解析——AI Native时代的网络变革
网络·人工智能·5g·架构·go·信息与通信
ylj_dev3 天前
从 0 构建 AI Workload Platform(九):真实场景、最小控制台与开源发布
go·react·开源项目·工作流·ai agent
tyung3 天前
znet 数据编解码:字节流怎么变成消息
后端·网络协议·go
leeyi3 天前
数据库迁移不翻车:golang-migrate 实战,143 个 DDL 有序执行(第97篇-E83)
go·aigc·agent