From a4b80570c488536f6f3cc68596f938be05fa227b Mon Sep 17 00:00:00 2001 From: AI Engineer Date: Sat, 9 May 2026 21:00:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E9=9A=90=E5=BC=8F?= =?UTF-8?q?=E5=AE=89=E5=85=A8=E6=8B=BC=E6=8E=A5=E4=B8=8E=E7=BC=96=E7=A0=81?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=20(Frictionless=20Security)=EF=BC=88by=20AI?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 9 ++++ README.md | 7 +++ go.mod | 2 - go.sum | 2 + safe.go | 135 ++++++++++++++++++++++++++++++++++++++++++++++----- 5 files changed, 142 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf884e2..5f1fd61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog: @go/safe +## [v1.0.6] - 2026-05-09 + +### Added +- **安全拼接 (Concat)**:新增 `Concat` 工具,支持对多个敏感部分(SafeBuf, SecretPlaintext, string, []byte)进行零残留安全拼接,返回持久加密的 `SafeBuf`。 +- **安全编码 (Base64/Hex/UrlEncode)**:新增多种编码工具,支持接受 `...any` 变长参数。内置隐式安全拼接能力(无需额外调用 `Concat`),直接返回持久加密的 `SafeBuf` 对象,脱离对 GC Finalizer 的强依赖,安全且可重复使用。 + +### Improved +- **生命周期增强**:优化了 `SecretPlaintext` 的安全性,增强了 `String()` 方法的鲁棒性。 + ## [v1.0.4] - 2026-05-01 ### Fixed diff --git a/README.md b/README.md index 1221826..e99af64 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,13 @@ - `func NewSafeString(data []byte) (*SecretPlaintext, string)`: 创建临时的安全字符串。 - `func MakeSafeToken(size int) []byte`: 生成带随机偏移的安全令牌。 +### 无感安全辅助工具 (Frictionless Security Helpers) +提供基于可变参数 `...any` 的隐式拼接与编码能力。所有操作均在底层私有缓冲区完成,并在返回持久加密的 `*SafeBuf` 后立刻物理擦除中间明文,彻底杜绝字符串拼接造成的内存泄露。 +- `func Concat(parts ...any) *SafeBuf`: 隐式安全拼接。 +- `func Base64(parts ...any) *SafeBuf`: 隐式安全拼接并 Base64 编码。 +- `func Hex(parts ...any) *SafeBuf`: 隐式安全拼接并 Hex 编码。 +- `func UrlEncode(parts ...any) *SafeBuf`: 隐式安全拼接并 URL 编码。 + ### 混淆器与算法 - `func SetSafeBufObfuscator(enc, dec)`: 自定义 SafeBuf 的底层加密逻辑。 - `func EncryptChaCha20(raw, key, salt []byte) []byte` diff --git a/go.mod b/go.mod index e67652f..5fb5160 100644 --- a/go.mod +++ b/go.mod @@ -7,5 +7,3 @@ require ( golang.org/x/crypto v0.50.0 golang.org/x/sys v0.43.0 ) - -replace apigo.cc/go/rand => ../rand diff --git a/go.sum b/go.sum index 035bcee..4dea270 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +apigo.cc/go/rand v1.0.5 h1:AkUoWr0SELgeDmRjLEDjOIp29nXdzqQQvmGRIHpTN7U= +apigo.cc/go/rand v1.0.5/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= diff --git a/safe.go b/safe.go index 0f364ae..b35140a 100644 --- a/safe.go +++ b/safe.go @@ -2,12 +2,16 @@ package safe import ( crand "crypto/rand" + "encoding/base64" "encoding/binary" + "encoding/hex" + "net/url" "runtime" "sync" "time" "unsafe" + "apigo.cc/go/cast" "apigo.cc/go/rand" "golang.org/x/crypto/chacha20" ) @@ -116,7 +120,7 @@ type SecretPlaintext struct { // String 返回明文的字符串表示 func (secret *SecretPlaintext) String() string { - if len(secret.Data) == 0 { + if secret == nil || len(secret.Data) == 0 { return "" } return unsafe.String(&secret.Data[0], len(secret.Data)) @@ -124,12 +128,22 @@ func (secret *SecretPlaintext) String() string { // Close 清除明文数据 func (secret *SecretPlaintext) Close() { - if secret.Data != nil { + if secret != nil && secret.Data != nil { ZeroMemory(secret.Data) secret.Data = nil } } +// newSecretPlaintext 内部辅助函数,统一绑定 Finalizer 作为安全兜底 +func newSecretPlaintext(data []byte) *SecretPlaintext { + secret := &SecretPlaintext{Data: data} + // GC Finalizer 仅作为兜底策略,主生命周期应由调用者显式管理 + runtime.SetFinalizer(secret, func(obj *SecretPlaintext) { + obj.Close() + }) + return secret +} + // NewSafeBuf 创建一个新的 SafeBuf func NewSafeBuf(raw []byte) *SafeBuf { cipher, salt := safeBufEncrypt(raw) @@ -153,20 +167,22 @@ func NewSafeBufFromEncrypted(cipher, salt []byte) *SafeBuf { // Open 解密 SafeBuf 并返回明文副本 func (safeBuf *SafeBuf) Open() *SecretPlaintext { + if safeBuf == nil { + return &SecretPlaintext{} + } data := safeBufDecrypt(safeBuf.buf, safeBuf.salt) if data == nil { return &SecretPlaintext{} } _ = LockMemory(data) - secret := &SecretPlaintext{Data: data} - runtime.SetFinalizer(secret, func(obj *SecretPlaintext) { - obj.Close() - }) - return secret + return newSecretPlaintext(data) } // Close 擦除 SafeBuf 中的加密数据 func (safeBuf *SafeBuf) Close() { + if safeBuf == nil { + return + } _ = UnlockMemory(safeBuf.buf) ZeroMemory(safeBuf.buf) _ = UnlockMemory(safeBuf.salt) @@ -175,9 +191,106 @@ func (safeBuf *SafeBuf) Close() { // NewSafeString 创建一个临时的敏感字符串 func NewSafeString(raw []byte) (*SecretPlaintext, string) { - secret := &SecretPlaintext{Data: raw} - runtime.SetFinalizer(secret, func(obj *SecretPlaintext) { - obj.Close() - }) + secret := newSecretPlaintext(raw) return secret, secret.String() } + +// Concat 安全地拼接多个部分并返回一个持久加密的 SafeBuf 对象 +func Concat(parts ...any) *SafeBuf { + return NewSafeBufAndErase(buildBytes(parts...)) +} + +// Base64 对输入进行拼接与编码,并返回一个持久加密的 SafeBuf 对象 +func Base64(parts ...any) *SafeBuf { + src := buildBytes(parts...) + defer ZeroMemory(src) + + buf := make([]byte, base64.StdEncoding.EncodedLen(len(src))) + base64.StdEncoding.Encode(buf, src) + + return NewSafeBufAndErase(buf) +} + +// Hex 将数据拼接并转换为 Hex 编码,返回一个持久加密的 SafeBuf 对象 +func Hex(parts ...any) *SafeBuf { + src := buildBytes(parts...) + defer ZeroMemory(src) + + buf := make([]byte, hex.EncodedLen(len(src))) + hex.Encode(buf, src) + + return NewSafeBufAndErase(buf) +} + +// UrlEncode 对数据拼接并进行 URL 编码,返回一个持久加密的 SafeBuf 对象 +func UrlEncode(parts ...any) *SafeBuf { + src := buildBytes(parts...) + defer ZeroMemory(src) + + // 使用 unsafe.String 避免对 src 产生额外的 string 分配 + srcStr := unsafe.String(&src[0], len(src)) + encoded := url.QueryEscape(srcStr) + buf := []byte(encoded) + + return NewSafeBufAndErase(buf) +} + +func buildBytes(parts ...any) []byte { + totalLen := 0 + for _, p := range parts { + totalLen += partLen(p) + } + + buf := make([]byte, totalLen) + pos := 0 + for _, p := range parts { + pos += copyPart(buf[pos:], p) + } + return buf +} + +func partLen(v any) int { + switch t := v.(type) { + case string: + return len(t) + case []byte: + return len(t) + case *SecretPlaintext: + if t == nil { + return 0 + } + return len(t.Data) + case *SafeBuf: + if t == nil { + return 0 + } + p := t.Open() + defer p.Close() + return len(p.Data) + default: + return len(cast.String(v)) + } +} + +func copyPart(dst []byte, v any) int { + switch t := v.(type) { + case string: + return copy(dst, t) + case []byte: + return copy(dst, t) + case *SecretPlaintext: + if t == nil { + return 0 + } + return copy(dst, t.Data) + case *SafeBuf: + if t == nil { + return 0 + } + p := t.Open() + defer p.Close() + return copy(dst, p.Data) + default: + return copy(dst, cast.String(v)) + } +}