mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-15 08:51:49 +08:00
Codec 原有 7 条保留。新增模块职责层判据:
TestGateReplacesSecretOutboundAndBackfillsForExecution
出向文本里出现真实凭据 → 被替换成 @{alias},明文不再出现
执行前 FillForExecution 把别名换回真实凭据
TestGateScrubsSecretInbound
入向泄漏的凭据被清洗,订阅侧看不到明文
TestGateDisabledPassesTextThrough
关闭状态原样透传、拦截计数 0
TestStoreAliasRoundTripSurvivesReopen
Submit 拿别名 → 重新打开同一存储 → Fill 回填出原文
变异验证:
去掉回填分支 → 别名不再换回真实凭据(先前的失败)
出向不产出别名 → outbound secret was not intercepted
过程中修正了两处我自己写错的期望(回填目标必须先取别名、
引用格式是 @{alias}),不是放宽实现。
249 lines
7.0 KiB
Go
249 lines
7.0 KiB
Go
package test
|
||
|
||
// internal/security 的黑盒判据:只用导出构造 + 公开方法,输入 → 输出。
|
||
// 不读源码、不碰 Codec/Store 内部字段、不用任何 ForTest 钩子。
|
||
|
||
import (
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
|
||
"github.com/JianFeeeee/HologramAgent/internal/security"
|
||
)
|
||
|
||
func fixedKey(byteVal byte) []byte {
|
||
k := make([]byte, 32)
|
||
for i := range k {
|
||
k[i] = byteVal
|
||
}
|
||
return k
|
||
}
|
||
|
||
func newCodec(t *testing.T, key []byte, domain string) *security.Codec {
|
||
t.Helper()
|
||
c, err := security.NewCodec(key, domain)
|
||
if err != nil {
|
||
t.Fatalf("NewCodec: %v", err)
|
||
}
|
||
return c
|
||
}
|
||
|
||
// TestCodecSealOpenRoundTrip:Seal → Open 得回原文。
|
||
func TestCodecSealOpenRoundTrip(t *testing.T) {
|
||
c := newCodec(t, fixedKey(7), "test-domain")
|
||
const plain = "sk-super-secret-value-123"
|
||
sealed, err := c.Seal(plain)
|
||
if err != nil {
|
||
t.Fatalf("Seal: %v", err)
|
||
}
|
||
if sealed == plain {
|
||
t.Fatal("★ Seal 返回了原文 —— 没有加密")
|
||
}
|
||
got, err := c.Open(sealed)
|
||
if err != nil {
|
||
t.Fatalf("Open: %v", err)
|
||
}
|
||
if got != plain {
|
||
t.Fatalf("★ Open = %q,期望 %q", got, plain)
|
||
}
|
||
}
|
||
|
||
// TestCodecDomainSeparation:不同 domain 的密文互不通用。
|
||
func TestCodecDomainSeparation(t *testing.T) {
|
||
a := newCodec(t, fixedKey(7), "domain-a")
|
||
b := newCodec(t, fixedKey(7), "domain-b")
|
||
sealed, err := a.Seal("payload")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := b.Open(sealed); err == nil {
|
||
t.Fatal("★ 另一个 domain 能解开密文 —— 域隔离失效")
|
||
}
|
||
}
|
||
|
||
// TestCodecWrongKeyCannotOpen:换钥后旧密文不可读。
|
||
func TestCodecWrongKeyCannotOpen(t *testing.T) {
|
||
a := newCodec(t, fixedKey(1), "d")
|
||
b := newCodec(t, fixedKey(2), "d")
|
||
sealed, err := a.Seal("payload")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := b.Open(sealed); err == nil {
|
||
t.Fatal("★ 错误密钥能解开密文")
|
||
}
|
||
}
|
||
|
||
// TestCodecEnabledAndDomain:元信息输出。
|
||
func TestCodecEnabledAndDomain(t *testing.T) {
|
||
c := newCodec(t, fixedKey(3), "my-domain")
|
||
if !c.Enabled() {
|
||
t.Fatal("★ 有效密钥下 Enabled()=false")
|
||
}
|
||
if got := c.Domain(); got != "my-domain" {
|
||
t.Fatalf("★ Domain() = %q,期望 my-domain", got)
|
||
}
|
||
var nilCodec *security.Codec
|
||
if nilCodec.Enabled() {
|
||
t.Fatal("★ nil Codec 报告 Enabled=true")
|
||
}
|
||
}
|
||
|
||
// TestCodecOldKeysStillDecrypt:轮换后历史密文仍可读。
|
||
func TestCodecOldKeysStillDecrypt(t *testing.T) {
|
||
old := fixedKey(4)
|
||
cur := fixedKey(5)
|
||
oldCodec := newCodec(t, old, "d")
|
||
sealed, err := oldCodec.Seal("legacy-value")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
rotated, err := security.NewCodecWithOldKeys(cur, "d", [][]byte{old})
|
||
if err != nil {
|
||
t.Fatalf("NewCodecWithOldKeys: %v", err)
|
||
}
|
||
got, err := rotated.Open(sealed)
|
||
if err != nil {
|
||
t.Fatalf("★ 轮换后旧密文读不出:%v", err)
|
||
}
|
||
if got != "legacy-value" {
|
||
t.Fatalf("★ 旧密文解出 %q", got)
|
||
}
|
||
}
|
||
|
||
// TestCodecOpenLegacyPlaintextPassthrough:无密文前缀的旧数据原样透传。
|
||
//
|
||
// 这是明确的兼容契约:加密是后来加的,库里存在未加密的历史值。
|
||
// 把它们判成错误会让整库读不出来。
|
||
func TestCodecOpenLegacyPlaintextPassthrough(t *testing.T) {
|
||
c := newCodec(t, fixedKey(9), "d")
|
||
for _, in := range []string{"", "legacy-plain-value", "abc"} {
|
||
got, err := c.Open(in)
|
||
if err != nil {
|
||
t.Fatalf("★ Open(%q) 报错:%v —— 旧明文应原样透传", in, err)
|
||
}
|
||
if got != in {
|
||
t.Fatalf("★ Open(%q) = %q,期望原样返回", in, got)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestCodecOpenRejectsCorruptedCiphertext:带密文前缀但内容坏掉必须报错。
|
||
func TestCodecOpenRejectsCorruptedCiphertext(t *testing.T) {
|
||
c := newCodec(t, fixedKey(9), "d")
|
||
for _, in := range []string{
|
||
security.EncPrefixV1 + "!!!not-base64!!!",
|
||
security.EncPrefixV1,
|
||
} {
|
||
if _, err := c.Open(in); err == nil {
|
||
t.Fatalf("★ Open(%q) 没报错 —— 损坏密文被静默吞下", in)
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---- Gate 的模块职责:明文凭据在出向被换成别名,执行前再回填原值 ----
|
||
|
||
func newGateStore(t *testing.T) *security.Store {
|
||
t.Helper()
|
||
st := security.New(security.Options{
|
||
Path: filepath.Join(t.TempDir(), "vars.json"),
|
||
KeyFn: func() ([]byte, error) { return fixedKey(11), nil },
|
||
Enabled: true,
|
||
})
|
||
if err := st.Load(); err != nil {
|
||
t.Fatalf("Load: %v", err)
|
||
}
|
||
return st
|
||
}
|
||
|
||
const contractSecret = "sk-proj-abcdefghijklmnopqrstuvwxyz0123456789"
|
||
|
||
func TestGateReplacesSecretOutboundAndBackfillsForExecution(t *testing.T) {
|
||
store := newGateStore(t)
|
||
gate := security.NewGate(store)
|
||
|
||
out, n := gate.PrepareOutbound(security.Inspection{Stage: "to_llm"}, "key="+contractSecret)
|
||
if n == 0 {
|
||
t.Fatal("outbound secret was not intercepted")
|
||
}
|
||
if strings.Contains(out, contractSecret) {
|
||
t.Fatalf("outbound text still carries plaintext: %q", out)
|
||
}
|
||
|
||
// 出向文本里给出的是别名;执行前必须换回真实凭据。
|
||
alias := extractAlias(out)
|
||
if alias == "" {
|
||
t.Fatalf("alias not found in outbound text: %q", out)
|
||
}
|
||
back, filled := gate.FillForExecution(security.Inspection{Stage: "to_plugin"}, "key=@{"+alias+"}")
|
||
if filled != 1 {
|
||
t.Fatalf("backfill reported %d places", filled)
|
||
}
|
||
if back != "key="+contractSecret {
|
||
t.Fatalf("backfilled text = %q", back)
|
||
}
|
||
}
|
||
|
||
func TestGateScrubsSecretInbound(t *testing.T) {
|
||
gate := security.NewGate(newGateStore(t))
|
||
out, n := gate.ScrubInbound(security.Inspection{Stage: "tool_result"}, "leaked "+contractSecret)
|
||
if n == 0 || strings.Contains(out, contractSecret) {
|
||
t.Fatalf("inbound scrub failed: n=%d out=%q", n, out)
|
||
}
|
||
}
|
||
|
||
func TestGateDisabledPassesTextThrough(t *testing.T) {
|
||
st := security.New(security.Options{Enabled: false,
|
||
KeyFn: func() ([]byte, error) { return fixedKey(11), nil }})
|
||
gate := security.NewGate(st)
|
||
in := "key=" + contractSecret
|
||
if out, n := gate.PrepareOutbound(security.Inspection{}, in); out != in || n != 0 {
|
||
t.Fatalf("disabled gate modified text: %q n=%d", out, n)
|
||
}
|
||
if !gate.Enabled() == true {
|
||
t.Fatal("unexpected Enabled() result for disabled store")
|
||
}
|
||
}
|
||
|
||
func TestStoreAliasRoundTripSurvivesReopen(t *testing.T) {
|
||
dir := t.TempDir()
|
||
path := filepath.Join(dir, "vars.json")
|
||
first := security.New(security.Options{Path: path,
|
||
KeyFn: func() ([]byte, error) { return fixedKey(11), nil }, Enabled: true})
|
||
if err := first.Load(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
alias, err := first.Submit(contractSecret, "contract")
|
||
if err != nil {
|
||
t.Fatalf("Submit: %v", err)
|
||
}
|
||
if alias == "" || alias == contractSecret {
|
||
t.Fatalf("alias = %q", alias)
|
||
}
|
||
|
||
second := security.New(security.Options{Path: path,
|
||
KeyFn: func() ([]byte, error) { return fixedKey(11), nil }, Enabled: true})
|
||
if err := second.Load(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
back, n, _ := second.Fill("@{" + alias + "}")
|
||
if n != 1 || !strings.Contains(back, contractSecret) {
|
||
t.Fatalf("fill after reopen = %q (n=%d)", back, n)
|
||
}
|
||
}
|
||
|
||
// extractAlias 从安全模块产物里取出别名(公开契约:@\{alias\})。
|
||
func extractAlias(text string) string {
|
||
const marker = "@{"
|
||
i := strings.Index(text, marker)
|
||
if i < 0 {
|
||
return ""
|
||
}
|
||
rest := text[i+len(marker):]
|
||
j := strings.Index(rest, "}")
|
||
if j < 0 {
|
||
return ""
|
||
}
|
||
return rest[:j]
|
||
}
|