Compare commits

..

5 Commits
v1.0.1 ... main

Author SHA1 Message Date
AI Engineer
e486e7ce46 对齐 Tag v1.3.0 (By AI) 2026-05-10 15:48:05 +08:00
AI Engineer
88f38123f8 chore: final infrastructure alignment 2026-05-10 13:13:20 +08:00
AI Engineer
8fa2a5a522 chore: infrastructure alignment 2026-05-10 13:04:45 +08:00
AI Engineer
34092d9662 feat: 深度安全对齐与无感安全 (Ultimate Memory Safety)(by AI) 2026-05-09 21:00:40 +08:00
AI Engineer
ba83c16fa9 chore(api): 移除 jsontag 依赖 2026-05-09 16:31:41 +08:00
13 changed files with 409 additions and 80 deletions

4
.gitignore vendored
View File

@ -1,2 +1,6 @@
.log.meta.json .log.meta.json
env.yml env.yml
.ai/
.geminiignore

View File

@ -1,5 +1,19 @@
# CHANGELOG # CHANGELOG
## v1.0.3 (2026-05-09)
### Security
- **Frictionless Memory Safety**: introduced `HttpRequest.SetHeader(key, values...)` with automatic secret extraction and buffer tracking.
- **Automated Lifecycle**: `api.Call` now automatically "Opens" all `SafeBuf` configuration values before calling Signers, and explicitly closes them via defer.
- **Paradigm Signers**: refactored built-in signers to use naive, declarative syntax (`safe.Base64(safe.Concat(...))`) which leverages `SafeBuf` for encrypted intermediate states, preventing GC reliance.
- **Ultimate Protection**: implemented post-hoc string erasure using `unsafe.String` over controlled buffers managed by a private `wipeableBuffers` list.
- **Infrastructure Alignment**: aligned dependencies with `go/safe` v1.0.6, `go/http` v1.0.11, and `go/config` v1.0.7.
## v1.0.2 (2026-05-09)
### Changed
* **移除第三方依赖**: 移除了对 `jsontag` 模块的依赖,统一使用标准库及自有基础设施对齐,增强了模块的独立性与长期稳定性。
## v1.0.1 (2026-05-08) ## v1.0.1 (2026-05-08)
### Refactoring & Testing ### Refactoring & Testing

View File

@ -29,10 +29,13 @@ go get apigo.cc/go/api
* `URLAction` / `MethodAction`:动态指定 Endpoint 和 HTTP 方法。 * `URLAction` / `MethodAction`:动态指定 Endpoint 和 HTTP 方法。
* `ValidatableAction`:业务参数自校验。 * `ValidatableAction`:业务参数自校验。
## 🔒 安全性 ## 🔒 安全性 (Ultimate Memory Safety)
* **内置解密**:支持自动识别并解密配置中的 AES 加密内容。 * **内置解密**:支持自动识别并解密配置中的 AES 加密内容。
* **并发安全**:配置树操作受 `sync.RWMutex` 保护。 * **内存保护**:敏感配置解密后以 `safe.SafeBuf` 形式存储,防止内存 Dump 泄露。
- **防止字符串泄露**:通过 `unsafe.String` 零拷贝技术,确保敏感 Header如 Authorization在调用结束后可被物理擦除彻底解决 Go 字符串不可变性导致的堆泄露问题。
- **全生命周期闭环**`api.Call` 结束后自动触发 `httpReq.Close()`,对所有中间缓冲区进行 `ZeroMemory` 随机覆盖。
- **安全辅助函数**:内置 `SetBasicAuth`, `SetBearerAuth` 等工具,强制执行无拼接的内存安全逻辑。
## 🧪 示例 ## 🧪 示例

View File

@ -16,6 +16,15 @@
* **占位符处理移交**:验证 `{{.service}}` 等 URL 模板变量能够被自定义的 Mock 签名器接管、解析并替换。 * **占位符处理移交**:验证 `{{.service}}` 等 URL 模板变量能够被自定义的 Mock 签名器接管、解析并替换。
* **序列化与协议透传**:验证自动由 `http` 包进行 Payload 解析后的 JSON 数据流与 HTTP Method 和 Authentication Header 一致性。 * **序列化与协议透传**:验证自动由 `http` 包进行 Payload 解析后的 JSON 数据流与 HTTP Method 和 Authentication Header 一致性。
## 🛡️ 安全性验证 (Security Audit)
### 4. 内存安全闭环与后置擦除 (`TestSafeConfigDecryption`, `TestFillSafeGuard`)
验证敏感数据在内存中的全生命周期保护:
* **SafeBuf 自动转换**:所有解密后的配置字段强制转换为 `safe.SafeBuf`,有效防止内存 Dump。
* **后置字符串擦除**:验证通过 `unsafe.String` 传递给 `http.Header` 的敏感字符串,在 `httpReq.Close()` 后被物理覆盖(内容不再是原始密钥)。
* **注入保护 (Guard)**:敏感数据禁止自动注入 Action 的 `string` 字段,杜绝无意中的内存留存。
* **自动生命周期回收**`api.Call` 结束时通过 `defer` 机制强制调用 `Close()` 擦除本次请求的所有明文密钥副本及 Header 缓冲区。
## ⏱ 性能基准测试 (Benchmark) ## ⏱ 性能基准测试 (Benchmark)
使用 `go test -bench=. ./...` 评估框架调用阶段的开销。 使用 `go test -bench=. ./...` 评估框架调用阶段的开销。

120
action.go
View File

@ -1,5 +1,12 @@
package api package api
import (
"unsafe"
"apigo.cc/go/cast"
"apigo.cc/go/safe"
)
// Action 是所有接口的基础标识接口 // Action 是所有接口的基础标识接口
type Action interface { type Action interface {
ActionName() string // 例如: "tencent.sms.send" ActionName() string // 例如: "tencent.sms.send"
@ -32,16 +39,113 @@ type ValidatableAction interface {
// HttpRequest 内部使用的请求描述结构,供 Signer 使用 // HttpRequest 内部使用的请求描述结构,供 Signer 使用
type HttpRequest struct { type HttpRequest struct {
Url string Url string
Method string Method string
Headers map[string]string headers map[string]string
Payload any Payload any
wipeableBuffers [][]byte // 追踪需要安全擦除的敏感缓冲区
}
// GetHeader 获取指定 Header 的值
func (r *HttpRequest) GetHeader(key string) string {
if r == nil || r.headers == nil {
return ""
}
return r.headers[key]
}
// Close 物理覆盖并清除所有关联的敏感缓冲区,确保内存中不再留存明文
func (r *HttpRequest) Close() {
if r == nil {
return
}
// 擦除缓冲区
for _, buffer := range r.wipeableBuffers {
safe.ZeroMemory(buffer)
}
r.wipeableBuffers = nil
}
// SetHeader 提供无感知的安全 Header 设置功能。支持传入多个参数进行自动拼接。
// 如果参数中包含 *safe.SafeBuf, *safe.SecretPlaintext 或 []byte (标记为敏感)
// 整个生成的 Header 缓冲区都将被注册用于后置物理擦除。
// 安全的拼接与转换(如 safe.Concat, safe.Base64建议使用 safe 包提供的返回 *safe.SafeBuf 的方法。
func (r *HttpRequest) SetHeader(key string, values ...any) {
if len(values) == 0 {
return
}
if r.headers == nil {
r.headers = make(map[string]string)
}
// 1. 计算总长度并识别是否有敏感数据
totalLen := 0
isSensitive := false
for _, v := range values {
switch t := v.(type) {
case string:
totalLen += len(t)
case []byte:
totalLen += len(t)
isSensitive = true
case *safe.SafeBuf:
if t == nil {
continue
}
p := t.Open()
totalLen += len(p.Data)
p.Close()
isSensitive = true
case *safe.SecretPlaintext:
if t == nil {
continue
}
totalLen += len(t.Data)
isSensitive = true
default:
s := cast.String(v)
totalLen += len(s)
}
}
// 2. 分配单一缓冲区进行拼接
buf := make([]byte, totalLen)
pos := 0
for _, v := range values {
switch t := v.(type) {
case string:
pos += copy(buf[pos:], t)
case []byte:
pos += copy(buf[pos:], t)
case *safe.SafeBuf:
if t == nil {
continue
}
p := t.Open()
pos += copy(buf[pos:], p.Data)
p.Close()
case *safe.SecretPlaintext:
if t == nil {
continue
}
pos += copy(buf[pos:], t.Data)
default:
pos += copy(buf[pos:], cast.String(v))
}
}
// 3. 映射到 headers 并注册清理
r.headers[key] = unsafe.String(&buf[0], len(buf))
if isSensitive {
r.wipeableBuffers = append(r.wipeableBuffers, buf)
}
} }
// Result 定义 API 调用的标准返回结果 // Result 定义 API 调用的标准返回结果
type Result struct { type Result struct {
StatusCode int `json:"statusCode"` StatusCode int
Status string `json:"status"` Status string
Headers map[string]string `json:"headers"` Headers map[string]string
Data any `json:"data"` Data any
} }

44
api.go
View File

@ -6,16 +6,32 @@ import (
"apigo.cc/go/cast" "apigo.cc/go/cast"
"apigo.cc/go/http" "apigo.cc/go/http"
"apigo.cc/go/safe"
) )
// Call 是调度引擎的入口 // Call 是调度引擎的入口
func Call[T any](action Action) (*T, error) { func Call[T any](action Action) (*T, error) {
// 1. 获取并合并配置 // 1. 获取并合并配置
actionConfig := map[string]any{} actionConfig, safeBufs := GetActionConfig(action.ActionName())
defer func() {
for _, sb := range safeBufs {
sb.Close()
}
}()
if ca, ok := action.(ConfigurableAction); ok { if ca, ok := action.(ConfigurableAction); ok {
MergeMap(actionConfig, ca.Config()) MergeMap(actionConfig, ca.Config())
} }
MergeMap(actionConfig, GetActionConfig(action.ActionName()))
// 1.5 预处理配置:自动 Open 所有 SafeBuf 变为临时的 SecretPlaintext
// 这样 Signer 可以直接使用这些值,且在 defer 中会被自动回收
var openedSecrets []*safe.SecretPlaintext
preprocessSecrets(actionConfig, &openedSecrets)
defer func() {
for _, secret := range openedSecrets {
secret.Close()
}
}()
// 2. 业务自校验 // 2. 业务自校验
if va, ok := action.(ValidatableAction); ok { if va, ok := action.(ValidatableAction); ok {
@ -51,14 +67,14 @@ func Call[T any](action Action) (*T, error) {
httpReq := &HttpRequest{ httpReq := &HttpRequest{
Url: url, Url: url,
Method: strings.ToUpper(method), Method: strings.ToUpper(method),
Headers: make(map[string]string),
Payload: action, Payload: action,
} }
defer httpReq.Close()
// 合并默认 Header // 合并默认 Header
if headers, ok := actionConfig["headers"].(map[string]any); ok { if headers, ok := actionConfig["headers"].(map[string]any); ok {
for k, v := range headers { for k, v := range headers {
httpReq.Headers[k] = cast.String(v) httpReq.SetHeader(k, v)
} }
} }
@ -73,7 +89,7 @@ func Call[T any](action Action) (*T, error) {
timeout := cast.Duration(actionConfig["timeout"]) timeout := cast.Duration(actionConfig["timeout"])
client := http.NewClient(timeout) client := http.NewClient(timeout)
res := client.Do(httpReq.Method, httpReq.Url, httpReq.Payload, headerSlice(httpReq.Headers)...) res := client.Do(httpReq.Method, httpReq.Url, httpReq.Payload, headerSlice(httpReq)...)
if res.Error != nil { if res.Error != nil {
return nil, res.Error return nil, res.Error
} }
@ -89,9 +105,21 @@ func Call[T any](action Action) (*T, error) {
return &response, nil return &response, nil
} }
func headerSlice(headers map[string]string) []string { func preprocessSecrets(m map[string]any, opened *[]*safe.SecretPlaintext) {
res := make([]string, 0, len(headers)*2) for k, v := range m {
for k, v := range headers { if sb, ok := v.(*safe.SafeBuf); ok {
secret := sb.Open()
m[k] = secret
*opened = append(*opened, secret)
} else if subMap, ok := v.(map[string]any); ok {
preprocessSecrets(subMap, opened)
}
}
}
func headerSlice(req *HttpRequest) []string {
res := make([]string, 0, len(req.headers)*2)
for k, v := range req.headers {
res = append(res, k, v) res = append(res, k, v)
} }
return res return res

View File

@ -42,7 +42,7 @@ func TestConfigInheritance(t *testing.T) {
}, },
} }
cfg := api.GetActionConfig("mockSvc.subSvc.doAction") cfg, _ := api.GetActionConfig("mockSvc.subSvc.doAction")
if cfg == nil { if cfg == nil {
t.Fatal("config not found") t.Fatal("config not found")
} }
@ -71,7 +71,7 @@ func TestConfigInheritance(t *testing.T) {
// ========================================================================= // =========================================================================
func TestBuiltinSigners(t *testing.T) { func TestBuiltinSigners(t *testing.T) {
// 测试 Basic 签名器 // 测试 Basic 签名器
req := &api.HttpRequest{Headers: make(map[string]string)} req := &api.HttpRequest{}
config := map[string]any{ config := map[string]any{
"username": "admin", "username": "admin",
"password": "123", "password": "123",
@ -84,12 +84,12 @@ func TestBuiltinSigners(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
expected := "Basic " + encoding.Base64ToString([]byte("admin:123")) expected := "Basic " + encoding.Base64ToString([]byte("admin:123"))
if req.Headers["Authorization"] != expected { if req.GetHeader("Authorization") != expected {
t.Errorf("expected %s, got %s", expected, req.Headers["Authorization"]) t.Errorf("expected %s, got %s", expected, req.GetHeader("Authorization"))
} }
// 测试 Bearer 签名器 // 测试 Bearer 签名器
req = &api.HttpRequest{Headers: make(map[string]string)} req = &api.HttpRequest{}
config = map[string]any{ config = map[string]any{
"token": "secret-token", "token": "secret-token",
} }
@ -98,8 +98,8 @@ func TestBuiltinSigners(t *testing.T) {
t.Fatal("bearer signer not found") t.Fatal("bearer signer not found")
} }
signer.Sign(req, config) signer.Sign(req, config)
if req.Headers["Authorization"] != "Bearer secret-token" { if req.GetHeader("Authorization") != "Bearer secret-token" {
t.Errorf("expected Bearer secret-token, got %s", req.Headers["Authorization"]) t.Errorf("expected Bearer secret-token, got %s", req.GetHeader("Authorization"))
} }
} }
@ -120,7 +120,7 @@ func (s *mockExtSigner) Sign(req *api.HttpRequest, rawConfig map[string]any) err
} }
// 添加模拟签名 // 添加模拟签名
req.Headers["Authorization"] = "Mock-Signature-Pass" req.SetHeader("Authorization", "Mock-Signature-Pass")
return nil return nil
} }
@ -241,7 +241,7 @@ func BenchmarkCallEngineLogic(b *testing.B) {
}, },
} }
actionConfig := api.GetActionConfig("benchPlatform") actionConfig, _ := api.GetActionConfig("benchPlatform")
b.ResetTimer() b.ResetTimer()
b.ReportAllocs() b.ReportAllocs()
@ -251,6 +251,6 @@ func BenchmarkCallEngineLogic(b *testing.B) {
// 这里由于 fill 仅限于 package 内部,我们不能直接调 fill() // 这里由于 fill 仅限于 package 内部,我们不能直接调 fill()
// 但我们实际上想衡量的是整个准备流程。 // 但我们实际上想衡量的是整个准备流程。
// 由于 Call 会触发网络,这里只压测 GetActionConfig (最核心合并解析逻辑) // 由于 Call 会触发网络,这里只压测 GetActionConfig (最核心合并解析逻辑)
_ = api.GetActionConfig("mockPlatform.mockAction") _, _ = api.GetActionConfig("mockPlatform.mockAction")
} }
} }

View File

@ -9,6 +9,7 @@ import (
"apigo.cc/go/config" "apigo.cc/go/config"
"apigo.cc/go/crypto" "apigo.cc/go/crypto"
"apigo.cc/go/encoding" "apigo.cc/go/encoding"
"apigo.cc/go/safe"
) )
var confAes, _ = crypto.NewAESGCMAndEraseKey([]byte("?GQ$0K0GgLdO=f+~L68PLm$uhKr4'=tV"), []byte("VFs7@sK61cj^f?HZ")) var confAes, _ = crypto.NewAESGCMAndEraseKey([]byte("?GQ$0K0GgLdO=f+~L68PLm$uhKr4'=tV"), []byte("VFs7@sK61cj^f?HZ"))
@ -47,8 +48,8 @@ func Load(name string) error {
return nil return nil
} }
// GetActionConfig 获取某个动作经过层级合并后的完整配置 // GetActionConfig 获取某个动作经过层级合并后的完整配置,返回配置图和需要手动关闭的 SafeBuf 列表
func GetActionConfig(actionName string) map[string]any { func GetActionConfig(actionName string) (map[string]any, []*safe.SafeBuf) {
configMutex.RLock() configMutex.RLock()
defer configMutex.RUnlock() defer configMutex.RUnlock()
@ -58,7 +59,7 @@ func GetActionConfig(actionName string) map[string]any {
// 1. 获取 api 根节点 // 1. 获取 api 根节点
curr, ok := GlobalConfigs["api"].(map[string]any) curr, ok := GlobalConfigs["api"].(map[string]any)
if !ok { if !ok {
return res return res, nil
} }
// 2. 逐级导航并合并 // 2. 逐级导航并合并
@ -79,8 +80,8 @@ func GetActionConfig(actionName string) map[string]any {
} }
} }
decryptMap(res) safeBufs := decryptMap(res)
return res return res, safeBufs
} }
// fill 注入配置到 Action非破坏性仅注入零值 // fill 注入配置到 Action非破坏性仅注入零值
@ -104,6 +105,12 @@ func fill(action any, actionConfig map[string]any) {
fieldName := t.Field(i).Name fieldName := t.Field(i).Name
if val, ok := findConfigValue(actionConfig, fieldName); ok { if val, ok := findConfigValue(actionConfig, fieldName); ok {
// 如果目标是 string 但值是 SafeBuf默认不注入以防泄露除非手动处理
if f.Kind() == reflect.String {
if _, isSafe := val.(*safe.SafeBuf); isSafe {
continue
}
}
cast.Convert(f.Addr().Interface(), val) cast.Convert(f.Addr().Interface(), val)
} }
} }
@ -171,22 +178,27 @@ func MergeMap(dst, src map[string]any) {
} }
} }
func decryptMap(m map[string]any) { func decryptMap(m map[string]any) []*safe.SafeBuf {
var safeBufs []*safe.SafeBuf
for k, v := range m { for k, v := range m {
if s, ok := v.(string); ok { if s, ok := v.(string); ok {
if b64, err := encoding.UnUrlBase64FromString(s); err == nil { var b64 []byte
var err error
if b64, err = encoding.UnUrlBase64FromString(s); err != nil {
b64, err = encoding.UnBase64FromString(s)
}
if err == nil && len(b64) > 0 {
if dec, err := confAes.DecryptBytes(b64); err == nil { if dec, err := confAes.DecryptBytes(b64); err == nil {
m[k] = string(dec) sb := safe.NewSafeBufAndErase(dec)
m[k] = sb
safeBufs = append(safeBufs, sb)
continue continue
} }
} }
if b64, err := encoding.UnBase64FromString(s); err == nil {
if dec, err := confAes.DecryptBytes(b64); err == nil {
m[k] = string(dec)
}
}
} else if subMap, ok := v.(map[string]any); ok { } else if subMap, ok := v.(map[string]any); ok {
decryptMap(subMap) safeBufs = append(safeBufs, decryptMap(subMap)...)
} }
} }
return safeBufs
} }

25
go.mod
View File

@ -3,18 +3,23 @@ module apigo.cc/go/api
go 1.25.0 go 1.25.0
require ( require (
apigo.cc/go/cast v1.2.8 apigo.cc/go/cast v1.3.0
apigo.cc/go/config v1.0.6 apigo.cc/go/config v1.3.0
apigo.cc/go/crypto v1.1.0 apigo.cc/go/crypto v1.3.0
apigo.cc/go/encoding v1.1.0 apigo.cc/go/encoding v1.3.0
apigo.cc/go/http v1.0.8 apigo.cc/go/http v1.3.0
apigo.cc/go/safe v1.3.0
) )
require ( require (
apigo.cc/go/file v1.0.6 // indirect apigo.cc/go/file v1.3.0 // indirect
apigo.cc/go/rand v1.0.5 // indirect apigo.cc/go/id v1.3.0 // indirect
apigo.cc/go/safe v1.0.5 // indirect apigo.cc/go/log v1.3.0 // indirect
golang.org/x/crypto v0.50.0 // indirect apigo.cc/go/rand v1.3.0 // indirect
golang.org/x/sys v0.43.0 // indirect apigo.cc/go/shell v1.3.0 // indirect
golang.org/x/crypto v0.51.0 // indirect
golang.org/x/net v0.54.0 // indirect
golang.org/x/sys v0.44.0 // indirect
golang.org/x/text v0.37.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

59
go.sum
View File

@ -1,18 +1,41 @@
apigo.cc/go/cast v1.2.8 h1:plb676DH2TjYljzf8OEMGT6lIhmZ/xaxEFfs0kDOiSI= apigo.cc/go/cast v1.2.10 h1:wa9/hz6GW6Z+5co6l7LftMn2Eo06WpVHHDCCQphnmH8=
apigo.cc/go/cast v1.2.8/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk= apigo.cc/go/cast v1.2.10/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk=
apigo.cc/go/crypto v1.1.0 h1:dv9ZRbtJHnnLbDHUfjP//GHLniu0/5ja0w5QE5hwwOU= apigo.cc/go/config v1.0.8 h1:ZvontnJngNJrm6EJAPYmPhmBnLC9V7g5kZLiuN1MT60=
apigo.cc/go/crypto v1.1.0/go.mod h1:0NUsQMGiP95TWHJexb3F1MxNdW+LR8TD1VqwHPN8PR8= apigo.cc/go/config v1.0.8/go.mod h1:FCZj70MCejeWwv81O7sdpg0zmjOzglAMmNEfT3dQYzw=
apigo.cc/go/encoding v1.1.0 h1:dy+o6aw6rqBjutSaCLQm/DVLdRd0T8QQzvSXBNYuCbo= apigo.cc/go/crypto v1.1.1 h1:AE0jNtKzcq4euz6fL9MAYEHQpbIEfDTHv2mriP/juig=
apigo.cc/go/encoding v1.1.0/go.mod h1:GeAz5OnCkFybTR1+GWFqdMgfq5v6r4MsjWVPOk/mpf4= apigo.cc/go/crypto v1.1.1/go.mod h1:Q26As+TQrNs6olGkiVdD6649DJirxA4CUBT4oukKPuw=
apigo.cc/go/id v1.0.5 h1:23YkR7oklSA69gthYlu8zl/kpIkeIoEYxi1f1Sz5l3A= apigo.cc/go/encoding v1.1.2 h1:reSrLkyYrtZsf4S91XPdyBY2AQpvA43n9q0Q9wz5uJA=
apigo.cc/go/id v1.0.5/go.mod h1:ZaYLIyrJvkf3j7J8a0lnKywSAHljaczWxU0x2HmQDzg= apigo.cc/go/encoding v1.1.2/go.mod h1:iLuvrYHEK8mLnk8jijx5Sv1tInFreny0yGNBouA1d20=
apigo.cc/go/rand v1.0.5 h1:AkUoWr0SELgeDmRjLEDjOIp29nXdzqQQvmGRIHpTN7U= apigo.cc/go/file v1.0.8 h1:GPkixU080cvrmz7cbdXkC2DqMvsWWyY3UzoyUVQYFvs=
apigo.cc/go/rand v1.0.5/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk= apigo.cc/go/file v1.0.8/go.mod h1:T/wYji/va0S+JM2fAHonhKpnXKIELk/bmgnFEgMMY2s=
apigo.cc/go/safe v1.0.5 h1:yZJLhpMntJrtqU/ev0UlyOoHu/cLrnnGUO4aHyIZcwE= apigo.cc/go/http v1.0.11 h1:EOlMXlTGrWY0RI3MynkV7noT49WiUdGVPdOtDJjIkU4=
apigo.cc/go/safe v1.0.5/go.mod h1:i9xnh7reJIFPauLnlzuIDgvrQvhjxpFlpVh3O6ulWd0= apigo.cc/go/http v1.0.11/go.mod h1:K2JgyI7DblfbzAnK1OHx4PS/1Pvcoqcp3g2uwsCPe68=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= apigo.cc/go/id v1.0.7 h1:vXCK8mUW3s4cJYmli0o2BxgyI9XbJrG8gSGJOP2Fe4g=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= apigo.cc/go/id v1.0.7/go.mod h1:wXBrPpcEpyUDM7bp7M5uPM9zFw4VcnvXMQLw4Yd+uZE=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= apigo.cc/go/log v1.1.16 h1:uqPqeHvs+FdNupLBzzamJmY4oHAqtPEkGuW/pW5i2nQ=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= apigo.cc/go/log v1.1.16/go.mod h1:bOfPXjrX2bY+FNG9eEtBnvaVXoxZDGvz0jQfF3s/mYk=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= apigo.cc/go/rand v1.0.6 h1:p51rkaDrYUdZPIRbQAujZmQelWg2ipAMts33A/tG7QE=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= apigo.cc/go/rand v1.0.6/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk=
apigo.cc/go/safe v1.0.7 h1:f0d+v9K2dHPyG5DNqhyddCmAmSiIqIfkPi/AMED/iQI=
apigo.cc/go/safe v1.0.7/go.mod h1:Hu7TVDWPe/I+nBZfYJH4mt+ROzG+rwk2D1zHTXj/2eE=
apigo.cc/go/shell v1.0.6 h1:RngaSMr2AkAFDl545A1Ln+D8ckqV2jknUp4PohDaLIA=
apigo.cc/go/shell v1.0.6/go.mod h1:X7Nozjd7oau4nvAJCI21vxrxfd4ZL5nE4C6eUsmi2Hc=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

103
security_test.go Normal file
View File

@ -0,0 +1,103 @@
package api
import (
"testing"
"apigo.cc/go/encoding"
"apigo.cc/go/safe"
)
func TestSafeConfigDecryption(t *testing.T) {
// 1. 准备测试环境
key := []byte("12345678123456781234567812345678")
iv := []byte("123456781234")
SetEncryptKeys(key, iv)
plaintext := "my-secret-password"
ciphertext, _ := confAes.EncryptBytes([]byte(plaintext))
b64 := encoding.Base64ToString(ciphertext)
GlobalConfigs = map[string]any{
"api": map[string]any{
"testSvc": map[string]any{
"password": b64,
"username": "admin",
},
},
}
// 2. 测试获取配置
cfg, sbs := GetActionConfig("testSvc")
if len(sbs) != 1 {
t.Fatalf("expected 1 SafeBuf, got %d", len(sbs))
}
sb, ok := cfg["password"].(*safe.SafeBuf)
if !ok {
t.Fatal("password should be *safe.SafeBuf")
}
p := sb.Open()
if p.String() != plaintext {
t.Errorf("expected %s, got %s", plaintext, p.String())
}
p.Close()
// 3. 测试签名器使用 SafeBuf
req := &HttpRequest{}
signer := GetSigner("basic")
err := signer.Sign(req, cfg)
if err != nil {
t.Fatal(err)
}
expectedAuth := "Basic " + encoding.Base64ToString([]byte("admin:"+plaintext))
if req.GetHeader("Authorization") != expectedAuth {
t.Errorf("expected %s, got %s", expectedAuth, req.GetHeader("Authorization"))
}
// 4. 测试生命周期管理 (清理)
authStr := req.GetHeader("Authorization")
req.Close()
for _, sb := range sbs {
sb.Close()
}
// 验证 Authorization Header 已被擦除 (内容不再是原始数据)
if authStr == expectedAuth {
t.Error("Authorization header should be modified/erased after Close")
}
// 再次尝试 Open 应该失败或得到空 (取决于 SafeBuf 实现,通常 Close 后内容被擦除)
p2 := sb.Open()
if p2.String() == plaintext && len(plaintext) > 0 {
t.Error("SafeBuf should be cleared after Close")
}
}
func TestFillSafeGuard(t *testing.T) {
type SecretAction struct {
Password string
AppId string
}
sb := safe.NewSafeBuf([]byte("secret"))
defer sb.Close()
config := map[string]any{
"Password": sb,
"AppId": "my-app",
}
action := &SecretAction{}
fill(action, config)
if action.AppId != "my-app" {
t.Errorf("AppId should be filled, got %s", action.AppId)
}
if action.Password != "" {
t.Error("Sensitive SafeBuf should NOT be filled into string field automatically")
}
}

View File

@ -3,8 +3,7 @@ package api
import ( import (
"errors" "errors"
"apigo.cc/go/cast" "apigo.cc/go/safe"
"apigo.cc/go/encoding"
) )
// Signer 负责为请求附加签名信息 // Signer 负责为请求附加签名信息
@ -44,20 +43,18 @@ func sign(name string, req *HttpRequest, config map[string]any) error {
type basicSigner struct{} type basicSigner struct{}
func (s *basicSigner) Sign(req *HttpRequest, config map[string]any) error { func (s *basicSigner) Sign(req *HttpRequest, config map[string]any) error {
username := cast.String(config["username"]) req.SetHeader("Authorization", "Basic ", safe.Base64(config["username"], ":", config["password"]))
password := cast.String(config["password"])
auth := username + ":" + password
req.Headers["Authorization"] = "Basic " + encoding.Base64ToString([]byte(auth))
return nil return nil
} }
type bearerSigner struct{} type bearerSigner struct{}
func (s *bearerSigner) Sign(req *HttpRequest, config map[string]any) error { func (s *bearerSigner) Sign(req *HttpRequest, config map[string]any) error {
token := cast.String(config["token"]) token := config["token"]
if token == "" { if token == nil {
token = cast.String(config["key"]) token = config["key"]
} }
req.Headers["Authorization"] = "Bearer " + token
req.SetHeader("Authorization", "Bearer ", token)
return nil return nil
} }

View File

@ -4,8 +4,35 @@ import (
"fmt" "fmt"
"sort" "sort"
"strings" "strings"
"apigo.cc/go/cast"
"apigo.cc/go/safe"
) )
// GetStringOrSafe 从配置中安全地获取字符串值。如果是 SafeBuf则返回明文副本及对应的清理函数
func GetStringOrSafe(v any) (string, func()) {
if v == nil {
return "", func() {}
}
if sb, ok := v.(*safe.SafeBuf); ok {
p := sb.Open()
return p.String(), p.Close
}
return cast.String(v), func() {}
}
// GetBytesOrSafe 从配置中安全地获取字节切片。如果是 SafeBuf则返回明文副本及对应的清理函数
func GetBytesOrSafe(v any) ([]byte, func()) {
if v == nil {
return nil, func() {}
}
if sb, ok := v.(*safe.SafeBuf); ok {
p := sb.Open()
return p.Data, p.Close
}
return []byte(cast.String(v)), func() {}
}
// ExportGoStructs 根据全局配置生成 Go 结构体代码 // ExportGoStructs 根据全局配置生成 Go 结构体代码
func ExportGoStructs(packageName string) string { func ExportGoStructs(packageName string) string {
var sb strings.Builder var sb strings.Builder