Golang基于Vault實現(xiàn)敏感數據加解密
本文是《基于Vault的敏感信息保護》的姊妹篇,文中涉及的配置管理實現(xiàn)方案可以參考《淺談Golang配置管理》這篇文章。
背景
某些應用程序會處理一些敏感的數據,比如用戶的證件號碼、手機號等個人隱私數據。如果將這些敏感數據以明文形式存儲在數據庫中,一旦發(fā)生黑客入侵事件,這些數據很容易被竊取、泄露,從而引發(fā)用戶信任風險和輿情危機,導致平臺用戶流失,甚至需要承擔法律責任。
數據加密是主要的數據安全防護技術之一,敏感數據應該加密存儲在數據庫中,降低泄露風險。
數據加解密方案
本文采用的是 HashiCorp 公司的 Vault 工具。Vault 通過自帶的 Transit 引擎提供加解密即服務(Encryption as a Service),如下圖所示,加解密過程為:
加密過程:
App 將需要加密的明文發(fā)給 Vault
Vault 將加密后的密文返給 App
App 將含有密文的數據存儲到數據庫中
解密過程:
App 從數據庫中讀取數據(含密文字段)
App 將需要解密的密文發(fā)給 Vault
Vault 將解密后的明文返給 App
具體實現(xiàn)過程
1. 準備工作
使用 Vault 提供加解密服務前,需要先啟用 Transit 引擎,創(chuàng)建專用的加解密密鑰,并賦予對應的 AppRole 加解密相關權限。
# 啟用 Transit 引擎 $ vault secrets enable transit # 創(chuàng)建專用的加解密密鑰 $ 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 過期前進行續(xù)租,當無法續(xù)租時重新登錄獲取新的 Token。示例代碼如下:
func VaultInit() { // 創(chuàng)建 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) } // 循環(huán):登錄認證,并續(xù)租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續(xù)租
renew
函數監(jiān)聽Token的生命周期,在TTL
到期前進行續(xù)租操作,直到無法繼續(xù)續(xù)租、續(xù)租失敗為止,此時需要重新登錄,獲取新的 Token。renew
函數的示例代碼如下:
func renew(client *vault.Client, token *vault.Secret) error { // 為 Token 創(chuàng)建一個監(jiān)聽器 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) } // 啟動后臺續(xù)租協(xié)程 go watcher.Start() defer watcher.Stop() for { select { // 續(xù)租失敗,或者無法繼續(xù)續(xù)租 case err := <-watcher.DoneCh(): //續(xù)租失敗 if err != nil { log.Printf("Failed to renew token: %v. Re-attempting login.", err) return nil } // 無法繼續(xù)續(xù)租 log.Printf("Token can no longer be renewed. Re-attempting login.") return nil // 成功完成續(xù)租 case renewal := <-watcher.RenewCh(): log.Printf("Successfully renewed, lease duration: %ds", renewal.Secret.Auth.LeaseDuration) } } }
5. 加密
本文以 GORM 庫為例來說明。GORM 的 Hook 機制允許在數據庫 CRUD 操作前后執(zhí)行預定義的 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. 解密
解密的實現(xiàn)與加密類似,我們可以定義解密方法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 實現(xiàn)敏感數據加解密的方案和具體實現(xiàn)過程。
到此這篇關于Golang基于Vault實現(xiàn)敏感數據加解密的文章就介紹到這了,更多相關Golang Vault敏感數據加解密內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
go-zero創(chuàng)建RESTful API 服務的方法
文章介紹了如何使用go-zero框架和goctl工具快速創(chuàng)建RESTfulAPI服務,通過定義.api文件并使用goctl命令,可以自動生成項目結構、路由、請求和響應模型以及處理邏輯,感興趣的朋友一起看看吧2024-11-11Golang實現(xiàn)Directional Channel(定向通道)
這篇文章主要介紹了Golang實現(xiàn)Directional Channel(定向通道),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2021-02-02如何使用Go語言獲取當天、昨天、明天、某天0點時間戳以及格式化時間
這篇文章主要給大家介紹了關于如何使用Go語言獲取當天、昨天、明天、某天0點時間戳以及格式化時間的相關資料,格式化時間戳是將時間戳轉換為特定的日期和時間格式,文中通過代碼示例介紹的非常詳細,需要的朋友可以參考下2023-10-10