Golang基于Vault实现敏感数据加解密

 更新时间:2023年07月05日 14:45:13   作者:Shawn27  
数据加密是主要的数据安全防护技术之一,敏感数据应该加密存储在数据库中,降低泄露风险,本文将介绍一下利用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 对 IDcardPhone 进行加密操作,示例代码如下:

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 实现敏感数据加解密的方案和具体实现过程。

到此这篇关于Golang基于Vault实现敏感数据加解密的文章就介绍到这了,更多相关Golang Vault敏感数据加解密内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!  

相关文章

  • golang类型转换之interface转字符串string简单示例

    golang类型转换之interface转字符串string简单示例

    在我们使用Golang进行开发过程中,总是绕不开对字符或字符串的处理,这篇文章主要给大家介绍了关于golang类型转换之interface转字符串string的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2024-01-01
  • go货币计算时如何避免浮点数精度问题

    go货币计算时如何避免浮点数精度问题

    在开发的初始阶段,我们经常会遇到“浮点数精度”和“货币值表示”的问题,那么在golang中如何避免这一方面的问题呢,下面就跟随小编一起来学习一下吧
    2024-02-02
  • mayfly-go部署和使用详解

    mayfly-go部署和使用详解

    这篇文章主要介绍了mayfly-go部署和使用详解,此处部署基于CentOS7.4部署,结合实例代码图文给大家讲解的非常详细,需要的朋友可以参考下
    2022-09-09
  • Go语言append切片添加元素的实现

    Go语言append切片添加元素的实现

    本文主要介绍了Go语言append切片添加元素的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-04-04
  • Go语言学习之结构体和方法使用详解

    Go语言学习之结构体和方法使用详解

    这篇文章主要为大家详细介绍了Go语言中结构体和方法的使用,文中的示例代码讲解详细,对我们学习Go语言有一定的帮助,需要的可以参考一下
    2022-04-04
  • 使用golang实现一个MapReduce的示例代码

    使用golang实现一个MapReduce的示例代码

    这篇文章主要给大家介绍了关于如何使用golang实现一个MapReduce,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-09-09
  • Golang 语言控制并发 Goroutine的方法

    Golang 语言控制并发 Goroutine的方法

    本文我们介绍了不同场景中分别适合哪种控制并发 goroutine 的方式,其中,channel 适合控制少量 并发 goroutine,WaitGroup 适合控制一组并发 goroutine,而 context 适合控制多级并发 goroutine,感兴趣的朋友跟随小编一起看看吧
    2021-06-06
  • Go缓冲channel和非缓冲channel的区别说明

    Go缓冲channel和非缓冲channel的区别说明

    这篇文章主要介绍了Go缓冲channel和非缓冲channel的区别说明,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-04-04
  • Go语言实现简单的一个静态WEB服务器

    Go语言实现简单的一个静态WEB服务器

    这篇文章主要介绍了Go语言实现简单的一个静态WEB服务器,本文给出了实现代码和运行效果,学习Golang的练手作品,需要的朋友可以参考下
    2014-10-10
  • 详解go中的defer链如何被遍历执行

    详解go中的defer链如何被遍历执行

    为了在退出函数前执行一些资源清理的操作,例如关闭文件、释放连接、释放锁资源等,会在函数里写上多个defer语句,多个_defer 结构体形成一个链表,G 结构体中某个字段指向此链表,那么go中的defer链如何被遍历执行,本文将给大家详细的介绍,感兴趣的朋友可以参考下
    2024-01-01

最新评论