secretx

package
v0.37.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 7, 2026 License: MIT Imports: 12 Imported by: 0

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

View Source
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 密文不可互解——必须升版本前缀,存量按前缀 识别后重封)。

View Source
const VersionV1 = "v1"

VersionV1 原始主密钥形态的密文版本前缀("v1:")。与 domain-manage 手搓 形态逐字节兼容,存量数据零迁移。

View Source
const VersionV2 = "v2"

VersionV2 passphrase 派生形态的密文版本前缀("v2:",#212):主密钥由 KDF 从 passphrase + 每密文随机 salt 派生,salt 随密文明文前置。KDF 代际 升级(换算法/换参数)走本版本位——升 v3 后旧密文按前缀识别、拒绝解封 并提示重新加密(ErrUnsupportedVersion),迁移路径与主密钥轮换同构。

Variables

View Source
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 判定)。

View Source
var ErrNoPassphrase = errors.New("secretx: passphrase missing")

ErrNoPassphrase passphrase 缺失(空串)。与 ErrNoKey 同 fail-fast 口径: 「环境变量没配就落库明文」不存在合法形态。

Functions

func ParseKey

func ParseKey(encoded string) ([]byte, error)

ParseKey 解析 base64 编码的主密钥:解码 + 16/24/32 字节校验,错误带实际 长度上下文。供「主密钥从环境变量/配置文件进来」的消费方一行完成入口校验。

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

func NewBox(masterKey []byte) (*Box, error)

NewBox 以原始主密钥(16/24/32 字节 AES 钥)创建信封盒——"v1:" 形态。 消费方拿到的是 passphrase 时改用 NewPassphraseBox(派生前置形态,#212)。 缺钥返回 ErrNoKey、长度非法返回 ErrBadKeySize(构造期 fail-fast—— 加密原语没有「带病运行」的合法形态)。

func NewPassphraseBox added in v0.37.0

func NewPassphraseBox(passphrase string) (*Box, error)

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) Open

func (b *Box) Open(ciphertext string) ([]byte, error)

Open 解封 Seal 的产物。

func (*Box) OpenWithAAD

func (b *Box) OpenWithAAD(ciphertext string, aad []byte) ([]byte, error)

OpenWithAAD 解封带 AAD 的密文(AAD 必须与 SealWithAAD 时逐字节一致)。

版本路由(#212):按版本前缀选解封路径——"v1" 原始主密钥形态、 "v2" passphrase 派生形态(salt 前缀取盐重派生)。跨形态解封(原始钥 Box 开 v2 密文 / passphrase Box 开 v1 密文)返回 ErrUnsupportedVersion ——按版本位选 Box 是轮换识别位的设计用法。

错误语义:版本前缀缺失/未识别/跨形态 → ErrUnsupportedVersion(轮换 识别位);base64 非法、负载过短、GCM 认证失败 → 一律 ErrBadCiphertext (统一表述——不向日志泄露「哪条密文是用哪把钥封的」)。

func (*Box) Seal

func (b *Box) Seal(plaintext []byte) (string, error)

Seal 封装明文:原始主密钥形态返回 "v1:" + base64(nonce‖ciphertext+tag), passphrase 派生形态返回 "v2:" + base64(salt‖nonce‖ciphertext+tag)(#212, 见 passphrase.go)。随机 nonce 由 crypto/rand 生成(读取失败返回错误, 绝不降级复用 nonce——GCM 复用 nonce 等于交出明文)。

func (*Box) SealWithAAD

func (b *Box) SealWithAAD(plaintext, aad []byte) (string, error)

SealWithAAD 同 Seal,额外绑定附加认证数据(行 id、用途标签等):AAD 不入 密文但参与认证——密文挪到别的行/别的用途上解封必失败。

版本路由(#212):passphrase 派生形态(NewPassphraseBox)产出 "v2:" 密文 (salt 前置,见 passphrase.go);原始主密钥形态产出 "v1:"(历史行为不变)。

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) Clear added in v0.36.0

func (s *SecureBytes) Clear()

Clear 清除数据(零化 + 置空)。

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)。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL