Skip to content

敏感数据脱敏

env 提供一组独立于 Loader 的实用工具函数,用于在日志、错误信息和调试输出中防止敏感数据泄露。这些函数无需创建 Loader 即可直接调用,适用于任何需要安全记录配置的场景。

为什么需要脱敏

即使你用 SecureValue 妥善保护了内存中的敏感值,它们仍可能在三个环节泄露:

  • 应用日志 —— 直接打印配置、请求参数、连接字符串
  • 错误消息 —— panic / error 把密钥带进日志收集系统
  • 调试输出 —— fmt.Println 调试时随手打印环境变量
text
log.Printf("加载配置 DB_PASSWORD=%s", pwd)          ← 日志泄露
panic("connect failed: password=hunter2")           ← 错误泄露
fmt.Println(env.GetString("API_KEY"))               ← 调试泄露

一旦这些输出进入日志聚合系统(ELK、Datadog……)或被团队成员、运维、甚至攻击者看到,密钥就等于失窃。env 的脱敏工具让你在记录时自动遮蔽敏感内容,从源头堵住泄露。

函数详解

IsSensitiveKey

go
func IsSensitiveKey(key string) bool

大小写不敏感地检查 key 是否包含敏感模式。检测采用子串匹配——只要键名(转为大写后)包含任一内置模式即判定为敏感。

内置检测模式:

类别模式
认证类PASSWORDSECRETTOKENAUTHCREDENTIALPASSPHRASESESSIONCOOKIE
密钥类API_KEYAPIKEYACCESS_KEYSECRET_KEYPRIVATE_KEYPUBLIC_KEY
加密类PRIVATEENCRYPTION_KEYENCRYPT_KEYDECRYPT_KEYSIGNING_KEYSIGN_KEYVERIFY_KEY
金融 / PIISSNSOCIAL_SECURITYCREDIT_CARDCARD_NUMBERCVVCVCCCVPAN
加密货币MNEMONICSEEDRECOVERYWALLETPRIVATE_ADDRESS
基础设施CONNECTION_STRINGCONN_STRINGDATABASE_URLDB_PASSWORD
云服务AWS_SECRETAZURE_KEYGCP_KEYSERVICE_ACCOUNT

子串匹配的含义

IsSensitiveKey("MY_API_KEY_TOKEN") 会匹配到 API_KEYTOKEN,返回 true。这意味着 AUTHORIZATION 也会因包含 AUTH 而被判定敏感——这正是期望的保守行为。

go
package main

import (
    "fmt"

    "github.com/cybergodev/env"
)

func main() {
    // 认证与密钥类
    fmt.Println(env.IsSensitiveKey("DB_PASSWORD"))  // true
    fmt.Println(env.IsSensitiveKey("API_KEY"))      // true
    fmt.Println(env.IsSensitiveKey("ACCESS_TOKEN")) // true

    // 大小写不敏感
    fmt.Println(env.IsSensitiveKey("api_key")) // true
    fmt.Println(env.IsSensitiveKey("ApiKey"))  // true

    // 非敏感键
    fmt.Println(env.IsSensitiveKey("PORT"))    // false
    fmt.Println(env.IsSensitiveKey("DB_HOST")) // false
}

MaskValue

go
func MaskValue(key, value string) string

根据 key 的敏感性对 value 脱敏,适合记录配置键值对:

条件返回值
IsSensitiveKey(key) 为 true[MASKED:N chars](N = len(value)
非敏感且 len(value) ≤ 20原始值
非敏感且 len(value) > 20value[:17] + "..."
go
package main

import (
    "fmt"

    "github.com/cybergodev/env"
)

func main() {
    // 敏感值 → 脱敏(保留长度信息便于排查)
    fmt.Println(env.MaskValue("DB_PASSWORD", "p@ssw0rd123"))
    // 输出: [MASKED:11 chars]

    // 非敏感短值 → 原样返回
    fmt.Println(env.MaskValue("PORT", "8080"))
    // 输出: 8080

    // 非敏感长值(>20 字符)→ 截断
    fmt.Println(env.MaskValue("DESCRIPTION", "this-is-a-very-long-description-value"))
    // 输出: this-is-a-very-lo...
}

保留长度的用意

[MASKED:N chars] 暴露的是值的长度而非内容。这在排查「密码是否被截断」「密钥是否完整」时很有用,同时不泄露明文。

MaskKey

go
func MaskKey(key string) string

脱敏键名本身,用于需要展示键存在但不暴露键含义的场景(内部调用 DefaultMaskKey):

条件返回值
len(key) ≤ 3***
len(key) > 3key[:2] + "***"
go
package main

import (
    "fmt"

    "github.com/cybergodev/env"
)

func main() {
    fmt.Println(env.MaskKey("DB_PASSWORD")) // DB***
    fmt.Println(env.MaskKey("API_KEY"))     // AP***
    fmt.Println(env.MaskKey("TOKEN"))       // TO***
    fmt.Println(env.MaskKey("AB"))          // ***
    fmt.Println(env.MaskKey("XYZ"))         // ***(长度 ≤ 3)
}

与 MaskValue 的搭配

MaskKey 只取键名前 2 个字符,因此 DB_HOSTDB_PASSWORD 都会变成 DB***。若需在日志中区分两者,请配合 MaskValue 一同输出,或仅在键名无关紧要时单独使用。

MaskSensitiveInString

go
func MaskSensitiveInString(s string) string

对长字符串截断,防止在日志中输出过多内容(可能间接泄露信息或撑爆日志):

条件返回值
len(s) > 50s[:47] + "..."
否则原始值
go
package main

import (
    "fmt"

    "github.com/cybergodev/env"
)

func main() {
    long := "012345678901234567890123456789012345678901234567890123456789"
    fmt.Println(env.MaskSensitiveInString(long))
    // 输出: 012345678901234567890123456789012345678901234567...

    short := "hello world"
    fmt.Println(env.MaskSensitiveInString(short))
    // 输出: hello world
}

SanitizeForLog

go
func SanitizeForLog(s string) string

扫描字符串中的 key=value 模式,将敏感键对应的 key=value 整体替换为 [MASKED],同时移除控制字符(保留 \n\t)。适合处理连接字符串、错误消息等内联键值。

检测的赋值模式: password=secret=token=auth=credential=passphrase=session=cookie=api_key=apikey=access_key=secret_key=private_key=public_key=encrypt_key=decrypt_key=signing_key=ssn=credit_card=card_number=cvv=cvc=mnemonic=seed=recovery=wallet=connection_string=database_url=db_password=

go
package main

import (
    "fmt"

    "github.com/cybergodev/env"
)

func main() {
    fmt.Println(env.SanitizeForLog("user=admin password=s3cret"))
    // 输出: user=admin [MASKED]

    fmt.Println(env.SanitizeForLog("token=abc123 host=localhost"))
    // 输出: [MASKED] host=localhost

    // 多个敏感值全部脱敏
    fmt.Println(env.SanitizeForLog("user=pguser password=hunter2 api_key=sk_123"))
    // 输出: user=pguser [MASKED] [MASKED]
}

替换粒度

SanitizeForLogpassword=s3cret 整体替换为单个 [MASKED](连同键名一起),而非保留 password=[MASKED]。这样日志中连「这里有个密码」这一信息都不暴露。

ClearBytes

go
func ClearBytes(b []byte)

将字节切片全部置零。用于手动清理由 Reveal() 获取、并以 []byte 形式处理的敏感数据,避免明文在内存中残留。

go
package main

import (
    "fmt"

    "github.com/cybergodev/env"
)

func main() {
    // 模拟以 []byte 形式处理的敏感数据
    secret := []byte("secret123")
    fmt.Printf("清零前: %s\n", secret)
    // 输出: 清零前: secret123

    env.ClearBytes(secret)
    fmt.Printf("清零后: %q\n", secret)
    // 输出: 清零后: "\x00\x00\x00\x00\x00\x00\x00\x00\x00"
}

ClearBytes 的局限

ClearBytes 只清零你传入的那个切片。如果同一份敏感数据被多次复制(如 string 与 []byte 之间的转换会产生新副本),这些副本无法被一并清零。敏感数据应尽量减少复制,并配合 SecureValueRelease() / Close() 使用。

实战示例

下面演示在应用启动时安全地打印配置,并处理含内联凭据的错误消息——涵盖 MaskValueSanitizeForLogIsSensitiveKeyMaskKey 的协同使用:

go
package main

import (
    "errors"
    "fmt"

    "github.com/cybergodev/env"
)

func main() {
    // 模拟从环境变量加载的配置
    config := []struct{ key, value string }{
        {"PORT", "8080"},
        {"DB_HOST", "localhost"},
        {"DB_PASSWORD", "super-secret-pwd"},
        {"API_KEY", "sk_live_1234567890abcdef"},
    }

    fmt.Println("=== 启动配置(已脱敏)===")
    for _, c := range config {
        fmt.Printf("%-15s = %s\n", c.key, env.MaskValue(c.key, c.value))
    }

    fmt.Println("\n=== 错误日志(自动脱敏)===")
    err := errors.New("failed to connect: user=admin password=hunter2 host=db.local")
    fmt.Println(env.SanitizeForLog(err.Error()))

    fmt.Println("\n=== 敏感键清单(键名脱敏)===")
    for _, c := range config {
        if env.IsSensitiveKey(c.key) {
            fmt.Printf("敏感配置: %s\n", env.MaskKey(c.key))
        }
    }
}

输出:

text
=== 启动配置(已脱敏)===
PORT            = 8080
DB_HOST         = localhost
DB_PASSWORD     = [MASKED:16 chars]
API_KEY         = [MASKED:24 chars]

=== 错误日志(自动脱敏)===
failed to connect: user=admin [MASKED] host=db.local

=== 敏感键清单(键名脱敏)===
敏感配置: DB***
敏感配置: AP***

与 SecureValue 的关系

env 的安全体系由两道互补的防线组成:

防线保护对象工具
内存保护运行时驻留在内存中的值GetSecure / Reveal / Masked / Release
输出脱敏写入日志、错误、调试输出的值IsSensitiveKey / MaskValue / SanitizeForLog
go
// 1. 内存保护:用 SecureValue 读取
secret := env.GetSecure("API_KEY")
defer secret.Release()
key := secret.Reveal()

// 2. 输出脱敏:记录时遮蔽
log.Printf("使用 %s 连接", secret.Masked())
// 或手动脱敏任意来源的值(不限于 SecureValue)
log.Printf("配置 %s", env.MaskValue("API_KEY", key))

分工明确

  • SecureValue 的 Masked() 输出形如 [SECURE:32 bytes locked],专用于它管理的值。
  • 脱敏工具函数(MaskValue 等)可用于任何来源的值——不限于 SecureValue,也不依赖 Loader。

相关文档