目录
  • 背景
  • 数据加解密方案
  • 具体实现过程
    • 1. 准备工作
    • 2. 初始化Vault客户端
    • 3. 登录认证
    • 4. Token续租
    • 5. 加密
    • 6. 解密
  • 总结

    本文是《基于Vault的敏感信息保护》的姊妹篇,文中涉及的配置管理实现方案可以参考《浅谈Golang配置管理》这篇文章。

    背景

    某些应用程序会处理一些敏感的数据,比如用户的证件号码、手机号等个人隐私数据。如果将这些敏感数据以明文形式存储在数据库中,一旦发生黑客入侵事件,这些数据很容易被窃取、泄露,从而引发用户信任风险和舆情危机,导致平台用户流失,甚至需要承担法律责任。

    数据加密是主要的数据安全防护技术之一,敏感数据应该加密存储在数据库中,降低泄露风险。

    数据加解密方案

    本文采用的是 HashiCorp 公司的 Vault 工具。Vault 通过自带的 Transit 引擎提供加解密即服务(Encryption as a Service),如下图所示,加解密过程为:

    加密过程:

    App 将需要加密的明文发给 Vault

    Vault 将加密后的密文返给 App

    App 将含有密文的数据存储到数据库中

    解密过程:

    App 从数据库中读取数据(含密文字段)

    App 将需要解密的密文发给 Vault

    Vault 将解密后的明文返给 App

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

    具体实现过程

    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敏感数据加解密内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!  

    声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。