本文将介绍 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
使用方法
- 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
- 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)
- 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)
- 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
- 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