Golang基于Vault实现敏感数据加解密
Golang基于Vault实现敏感数据加解密
本文是《基于Vault的敏感信息保护》的姊妹篇,文中涉及的配置管理实现方案可以参考《浅谈Golang配置管理》这篇文章。
背景
某些应用程序会处理一些敏感的数据,比如用户的证件号码、手机号等个人隐私数据。如果将这些敏感数据以明文形式存储在数据库中,一旦发生黑客入侵事件,这些数据很容易被窃取、泄露,从而引发用户信任风险和舆情危机,导致平台用户流失,甚至需要承担法律责任。
数据加密是主要的数据安全防护技术之一,敏感数据应该加密存储在数据库中,降低泄露风险。
数据加解密方案
本文采用的是 HashiCorp 公司的 Vault 工具。Vault 通过自带的 Transit 引擎提供加解密即服务(Encryption as a Service),如下图所示,加解密过程为:
- 加密过程:
- App 将需要加密的明文发给 Vault
- Vault 将加密后的密文返给 App
- App 将含有密文的数据存储到数据库中
- 解密过程:
- App 从数据库中读取数据(含密文字段)
- App 将需要解密的密文发给 Vault
- Vault 将解密后的明文返给 App
具体实现过程
1. 准备工作
使用 Vault 提供加解密服务前,需要先启用 Transit 引擎,创建专用的加解密密钥,并赋予对应的 AppRole 加解密相关权限。
# 启用 Transit 引擎
$ vault secrets enable transit
# 创建专用的加解密密钥
$ vault write -f transit/keys/mykey
# 为 AppRole 绑定的权限策略 myapp-policy 添加加解密权限
$ vault policy write myapp-policy -<<EOF
#已有的权限,见《基于Vault的敏感信息保护》这篇文章
#新增加密权限:
path "transit/encrypt/mykey" {
capabilities = [ "update" ]
}
#新增解密权限:
path "transit/decrypt/mykey" {
capabilities = [ "update" ]
}
EOF
# 重新生成 AppRole 的 SecretID
$ vault write -f -field=secret_id auth/approle/role/myapp/secret-id >~/.secretid
2. 初始化Vault客户端
不同于《基于Vault的敏感信息保护》这篇文章,本文采用应用程序与 Vault 直接集成的方案,使用的是 Vault 官方提供的 Go 语言库。
在应用程序与 Vault 交互前,需要初始化 Vault 客户端:登录 Vault 获取 Token,并在 Token 过期前进行续租,当无法续租时重新登录获取新的 Token。示例代码如下:
func VaultInit() {
// 创建 Vault Client
config := vault.DefaultConfig()
config.Address = vaultAddress
var err error
VaultClient, err = vault.NewClient(config)
if err != nil {
log.Fatalf("Failed to create vault client, err: %v", err)
}
// 循环:登录认证,并续租Token
go func() {
for {
vaultLoginResp, err := login(VaultClient)
if err != nil {
log.Printf("Unable to authenticate to Vault: %v", err)
time.Sleep(time.Second * 10)
continue
}
tokenErr := renew(VaultClient, vaultLoginResp)
if tokenErr != nil {
log.Printf("Unable to start managing token lifecycle: %v", tokenErr)
time.Sleep(time.Second * 10)
}
}
}()
}
本文采用的 Vault 相关配置如下:
vault:
address: http://x.x.x.x:8200
transit:
key: mykey
auth:
roleid-file-path: /app/role/roleid
secretid-file-path: /app/role/secretid
3. 登录认证
本文选择 AppRole 认证方法,登录 Vault 的示例代码如下:
func login(client *vault.Client) (*vault.Secret, error) {
// 读取 RoleID
bytes, err := ioutil.ReadFile(vaultRoleIdFilePath)
if err != nil {
return nil, fmt.Errorf("Error reading role ID file: %w", err)
}
roleID := strings.TrimSpace(string(bytes))
if len(roleID) == 0 {
return nil, errors.New("Error: role ID file exists but read empty value")
}
// 指定 SecretID
secretID := &auth.SecretID{FromFile: vaultSecretIdFilePath}
// 初始化 AppRole 认证方法,指定身份凭据
appRoleAuth, err := auth.NewAppRoleAuth(roleID, secretID)
if err != nil {
return nil, fmt.Errorf("unable to initialize AppRole auth method: %w", err)
}
// 通过 AppRole 认证方法登录到 Vault
authInfo, err := client.Auth().Login(context.Background(), appRoleAuth)
if err != nil {
return nil, fmt.Errorf("unable to login to AppRole auth method: %w", err)
}
if authInfo == nil {
return nil, fmt.Errorf("no auth info was returned after login")
}
log.Printf("Successfully (re)logined, lease duration: %ds", authInfo.Auth.LeaseDuration)
return authInfo, nil
}
4. Token续租
renew
函数监听Token的生命周期,在TTL
到期前进行续租操作,直到无法继续续租、续租失败为止,此时需要重新登录,获取新的 Token。renew
函数的示例代码如下:
func renew(client *vault.Client, token *vault.Secret) error {
// 为 Token 创建一个监听器
watcher, err := client.NewLifetimeWatcher(&vault.LifetimeWatcherInput{
Secret: token,
//Increment: 3600,
})
if err != nil {
return fmt.Errorf("unable to initialize new lifetime watcher for renewing auth token: %w", err)
}
// 启动后台续租协程
go watcher.Start()
defer watcher.Stop()
for {
select {
// 续租失败,或者无法继续续租
case err := <-watcher.DoneCh():
//续租失败
if err != nil {
log.Printf("Failed to renew token: %v. Re-attempting login.", err)
return nil
}
// 无法继续续租
log.Printf("Token can no longer be renewed. Re-attempting login.")
return nil
// 成功完成续租
case renewal := <-watcher.RenewCh():
log.Printf("Successfully renewed, lease duration: %ds", renewal.Secret.Auth.LeaseDuration)
}
}
}
5. 加密
本文以 GORM 库为例来说明。GORM 的 Hook 机制允许在数据库 CRUD 操作前后执行预定义的 Hook 方法。对于加密而言,可以为模型类定义 BeforeSave
方法,并在其中完成敏感数据的加密操作。
func (t *Teacher) BeforeSave(*gorm.DB) error {
return t.Encrypt()
}
Teacher 模型包含证件号码IDcard
和手机号Phone
两个敏感数据:
// 此处仅展示 GORM 相关标签,省略其它标签
type Teacher struct {
gorm.Model
Name string
// ... 其余字段省略
//密文
IDcard string `gorm:"unique"`
Phone string
//明文
PlainIDcard string `gorm:"-"`
PlainPhone string `gorm:"-"`
}
加密方法Encrypt
借助 Vault 对 IDcard
和 Phone
进行加密操作,示例代码如下:
func (t *Teacher) Encrypt() error {
path := fmt.Sprintf("/transit/encrypt/%s", config.VaultTransitKey)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
// 批量加密
resp, err := Vault.Logical().WriteWithContext(ctx, path, map[string]interface{}{
"batch_input": []map[string]interface{}{
{
"plaintext": base64.StdEncoding.EncodeToString([]byte(t.PlainIDcard)),
},
{
"plaintext": base64.StdEncoding.EncodeToString([]byte(t.PlainPhone)),
},
},
})
if err != nil {
log.Printf("teacher.Encrypt failed to encrypt data")
return err
}
// 拿到密文
t.IDcard = resp.Data["batch_results"].([]interface{})[0].(map[string]interface{})["ciphertext"].(string)
t.Phone = resp.Data["batch_results"].([]interface{})[1].(map[string]interface{})["ciphertext"].(string)
log.Printf("teacher.Encrypt called")
return nil
}
6. 解密
解密的实现与加密类似,我们可以定义解密方法Decrypt
,当需要进行解密时调用该方法:
- 如果没有使用缓存层,可以在
AfterFind
方法中调用Decrypt
,在查询数据库后完成解密操作 - 如果使用了 Redis 等缓存服务,则需要在更新缓存或命中缓存之后调用
Decrypt
Decrypt
方法的示例代码如下。
func (t *Teacher) Decrypt() error {
path := fmt.Sprintf("/transit/decrypt/%s", config.VaultTransitKey)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
// 批量解密
resp, err := Vault.Logical().WriteWithContext(ctx, path, map[string]interface{}{
"batch_input": []map[string]interface{}{
{
"ciphertext": t.IDcard,
},
{
"ciphertext": t.Phone,
},
},
})
if err != nil {
log.Printf("teacher.Decrypt failed to decrypt data")
return err
}
// 拿到 base64 文本
IDcard_base64 := resp.Data["batch_results"].([]interface{})[0].(map[string]interface{})["plaintext"].(string)
Phone_base64 := resp.Data["batch_results"].([]interface{})[1].(map[string]interface{})["plaintext"].(string)
// 解码拿到明文
IDcard, err1 := base64.StdEncoding.DecodeString(IDcard_base64)
Phone, err2 := base64.StdEncoding.DecodeString(Phone_base64)
if err1 != nil || err2 != nil {
log.Printf("teacher.Decrypt failed to base64 decode")
return errors.New("base64 decode error")
}
t.PlainIDcard = string(IDcard)
t.PlainPhone = string(Phone)
log.Printf("teacher.Decrypt called")
return nil
}
总结
数据加密是主要的数据安全防护技术之一,敏感数据应该加密存储在数据库中,降低泄露风险。本文介绍了 Golang 基于 Vault 实现敏感数据加解密的方案和具体实现过程。
转载自:https://juejin.cn/post/7252172628449738808