Documentation
¶
Overview ¶
Package secretx 提供静态敏感凭证的信封加密原语(AES-GCM + 密文版本前缀 + 主密钥解析):把一段敏感字符串(云账号 SecretKey、LDAP bind 密码、渠道 token、git provider token 等)用主密钥封起来存库——此前每个服务各自手搓 AES,密钥/算法语义漂移出四个形态(CFB 无认证、缺钥明文透传、密文无版本位), 本包把它们收成一份正确默认(#177)。
密文格式(与 domain-manage 手搓形态兼容,存量数据零迁移):
"v1:" + base64( nonce(12B) ‖ ciphertext+tag ) // 原始主密钥形态(NewBox) "v2:" + base64( salt(16B) ‖ nonce(12B) ‖ ciphertext+tag ) // passphrase 派生形态(NewPassphraseBox,#212)
设计立场:
- **必须带认证**(GCM),CFB 这类无完整性保护的原语不在可选项里;
- **缺钥 fail-fast**:构造期校验,不提供「缺钥明文透传」降级——那是 octopus 手搓版的真实事故形态(漏到生产即裸奔);本地开发想绕过请 自行用恒等实现,不该由库默认承担;
- **密文版本前缀**是主密钥轮换的识别位:Open 遇到未识别前缀返回哨兵 ErrUnsupportedVersion,下游可按前缀选钥解密重封(v2 落地时零破坏);
- **错误语义统一**:解密失败(密钥不匹配 vs 密文损坏)一律 ErrBadCiphertext ——不向日志/调用方泄露「哪条密文是用哪把钥封的」这类信息。
典型用法:
key, err := secretx.ParseKey(os.Getenv("APP_MASTER_KEY")) // base64(16/24/32B)
if err != nil { panic(err) } // 构造期 fail-fast
box, err := secretx.NewBox(key)
if err != nil { panic(err) }
sealed, err := box.Seal([]byte(plaintext)) // "v1:xxxx" 直接入库
pt, err := box.Open(sealed) // 读出解封
// 可选:绑定行 id/用途做 AAD,防密文在行间挪用(换行 AAD 解封必失败)
sealed, err := box.SealWithAAD(pt, []byte("credential:"+rowID))
pt, err := box.OpenWithAAD(sealed, []byte("credential:"+rowID))
消费方拿到的是 passphrase(环境变量/配置文件口令)而非原始主密钥时, 用派生前置形态 NewPassphraseBox(#212):KDF 从 passphrase 派生主密钥, salt 随密文明文前置、调用方无需管理:
box, err := secretx.NewPassphraseBox(os.Getenv("ENCRYPTION_KEY"))
if err != nil { panic(err) }
sealed, err := box.Seal([]byte(plaintext)) // "v2:xxxx"(KDF+salt 前置)
pt, err := box.Open(sealed)
Index ¶
Constants ¶
const PassphraseKDFIterations = 600_000
PassphraseKDFIterations 是 passphrase 派生的 PBKDF2-HMAC-SHA256 迭代数 (OWASP 2023 对该组合的建议值 600,000)。
KDF 选型说明(与 issue 建议的 argon2id 之别):本仓是**零第三方依赖仓** (go.mod 零 require,CI 强制——x/crypto 进不来),argon2id 无标准库实现; PBKDF2-HMAC-SHA256 是 Go 1.24 标准库(crypto/pbkdf2)提供的 KDF,带 salt、 可调强度、NIST 认可——「salt 随机 + 强度参数 + 版本位」的三件套纪律与 issue 的诉求一致。迭代数与算法代际的升级走 VersionV2 → v3 版本位(改本 常量会使新密文与存量 v2 密文不可互解——必须升版本前缀,存量按前缀 识别后重封)。
const VersionV1 = "v1"
VersionV1 原始主密钥形态的密文版本前缀("v1:")。与 domain-manage 手搓 形态逐字节兼容,存量数据零迁移。
const VersionV2 = "v2"
VersionV2 passphrase 派生形态的密文版本前缀("v2:",#212):主密钥由 KDF 从 passphrase + 每密文随机 salt 派生,salt 随密文明文前置。KDF 代际 升级(换算法/换参数)走本版本位——升 v3 后旧密文按前缀识别、拒绝解封 并提示重新加密(ErrUnsupportedVersion),迁移路径与主密钥轮换同构。
Variables ¶
var ( // ErrNoKey 主密钥缺失(空串/空切片)。构造期 fail-fast,不存在「缺钥 // 明文透传」的降级路径——本地开发需要绕过时请自行用恒等实现。 ErrNoKey = errors.New("secretx: master key missing") // ErrBadKeySize 主密钥长度不在 16/24/32 字节(AES-128/192/256)范围内。 ErrBadKeySize = errors.New("secretx: master key size must be 16, 24 or 32 bytes") // ErrBadCiphertext 密文损坏或主密钥不匹配(GCM 认证失败)。两种情形 // 统一表述,不泄露细节——「哪条密文是用哪把钥封的」不该可被日志反推。 ErrBadCiphertext = errors.New("secretx: bad ciphertext") // ErrUnsupportedVersion 密文版本前缀未识别(主密钥轮换识别位:v2 密钥 // 落地后,下游可按前缀选钥解密重封)。 ErrUnsupportedVersion = errors.New("secretx: unsupported ciphertext version") )
哨兵错误(errors.Is 判定)。
var ErrNoPassphrase = errors.New("secretx: passphrase missing")
ErrNoPassphrase passphrase 缺失(空串)。与 ErrNoKey 同 fail-fast 口径: 「环境变量没配就落库明文」不存在合法形态。
Functions ¶
Types ¶
type Box ¶
type Box struct {
// contains filtered or unexported fields
}
Box 信封盒:两种构造形态共用一个类型(#212)——
- NewBox(masterKey):原始主密钥形态,Seal 产出 "v1:" 密文;
- NewPassphraseBox(passphrase):passphrase 派生形态(KDF 前置), Seal 产出 "v2:" 密文(per-ciphertext 随机 salt 前置)。
主密钥/passphrase 不可变;可并发使用(两形态的加密路径均无共享可变 状态——派生形态每次 Seal 现派现用)。跨形态解封按版本位拒绝 (ErrUnsupportedVersion),见 OpenWithAAD。
func NewBox ¶
NewBox 以原始主密钥(16/24/32 字节 AES 钥)创建信封盒——"v1:" 形态。 消费方拿到的是 passphrase 时改用 NewPassphraseBox(派生前置形态,#212)。 缺钥返回 ErrNoKey、长度非法返回 ErrBadKeySize(构造期 fail-fast—— 加密原语没有「带病运行」的合法形态)。
func NewPassphraseBox ¶ added in v0.37.0
NewPassphraseBox 从 passphrase 构造信封盒(派生前置形态,#212): Seal 产出 "v2:" 密文——主密钥由 PBKDF2-HMAC-SHA256 从 passphrase + **每密文随机 salt** 派生(32 字节 AES-256),salt 随密文明文前置 (解密侧按前缀取 salt 重派生),调用方无需管理 salt 生命周期。
与 NewBox 的关系:不是替代——本构造是 NewBox 的**派生前置形态**,内部 产出主密钥后走完全相同的 GCM Seal/Open + 版本前缀路径;存量原始密钥 调用方不受影响。跨形态解封按版本位拒绝(见 OpenWithAAD)。
设计取舍:
- **每密文随机 salt**(而非每 Box 一个固定 salt):同 passphrase 加密 两条明文得到不同派生键,跨密文并链攻击面更小;代价是每条密文多 16 字节 salt 前缀与每次 Seal/Open 各一次 KDF(~百毫秒级,凭证量级 下可忽略);
- **版本位复用既有纪律**:KDF 代际升级走 VersionV2 → v3,存量密文按 前缀识别(ErrUnsupportedVersion),迁移路径与主密钥轮换同构;
- **不做 passphrase 强度校验**:KDF 就是护栏——弱口令变慢不是变安全, 强度校验是调用方(配置侧)的职责。空串除外:那是「没配」,返回 ErrNoPassphrase(fail-fast,同 ErrNoKey 口径)。
- passphrase 以 Go string 驻留于 Box 生命周期(不可擦除——string 不可变;需要可擦除形态的调用方自行派生字节钥后走 NewBox)。
func (*Box) OpenWithAAD ¶
OpenWithAAD 解封带 AAD 的密文(AAD 必须与 SealWithAAD 时逐字节一致)。
版本路由(#212):按版本前缀选解封路径——"v1" 原始主密钥形态、 "v2" passphrase 派生形态(salt 前缀取盐重派生)。跨形态解封(原始钥 Box 开 v2 密文 / passphrase Box 开 v1 密文)返回 ErrUnsupportedVersion ——按版本位选 Box 是轮换识别位的设计用法。
错误语义:版本前缀缺失/未识别/跨形态 → ErrUnsupportedVersion(轮换 识别位);base64 非法、负载过短、GCM 认证失败 → 一律 ErrBadCiphertext (统一表述——不向日志泄露「哪条密文是用哪把钥封的」)。
type SecureBytes ¶ added in v0.36.0
type SecureBytes struct {
// contains filtered or unexported fields
}
SecureBytes 安全字节持有器:副本隔离存取(Get 返回副本,外部改写不穿透)。
func NewSecureBytes ¶ added in v0.36.0
func NewSecureBytes(data []byte) *SecureBytes
NewSecureBytes 创建安全字节持有器。
func (*SecureBytes) Get ¶ added in v0.36.0
func (s *SecureBytes) Get() []byte
Get 获取字节数组(返回副本,外部修改不影响持有值)。
func (*SecureBytes) Set ¶ added in v0.36.0
func (s *SecureBytes) Set(data []byte)
Set 设置字节数组(旧值零化后重存;nil 等价 Clear)。
type SecureString ¶ added in v0.36.0
type SecureString struct {
// contains filtered or unexported fields
}
SecureString 安全字符串持有器:XOR 混淆存储,防止内存明文扫描。
func NewSecureString ¶ added in v0.36.0
func NewSecureString(plaintext string) *SecureString
NewSecureString 创建安全字符串持有器。
func (*SecureString) Clear ¶ added in v0.36.0
func (s *SecureString) Clear()
Clear 清除敏感数据(存储零化 + 置空)。
func (*SecureString) EqualConstantTime ¶ added in v0.36.0
func (s *SecureString) EqualConstantTime(other string) bool
EqualConstantTime 常量时间比较,防止时序攻击。
func (*SecureString) Get ¶ added in v0.36.0
func (s *SecureString) Get() string
Get 获取字符串值(每次在独立副本上解出)。
func (*SecureString) Set ¶ added in v0.36.0
func (s *SecureString) Set(plaintext string)
Set 设置字符串值(旧值零化后重存;空串等价 Clear)。