mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-01 23:07:30 +00:00
用户要求:"历史以来的全部写入日志的区域"改为英文,因为中文日志 在没有中文字形的终端/容器里会乱码,且跨机器、跨工具 grep 时不可靠。 ## 范围 - 全部 log.Printf / log.Fatalf / log.Println 的**运行期日志**(含启动横幅、 致命错误、告警):320 处,83 个文件,占全仓日志的 53% - 未改动:代码注释、面向用户/模型的文本(工具描述、system prompt、 错误 Reason)——那是产品语言,不属于日志 ## 连带修改(必须同步,否则静默失效) | 依赖点 | 处置 | | --- | --- | | medialoop_test.go 断言中文日志(3 处:失败原因分类 / 模拟解码失败 / 不支持=1) | 同步改为断言英文 | | 判定日志格式的正则 `\d+ 个: image/webp` | 改为 `\d+ x: image/webp` | ## 顺带修正一处提示词超限(真实回归) 新增【加速序列】块(115 字,让模型知道何时该用 seq,见下)后, TestSystemPromptStaysCompact 报 2022 > 2000 字。 处置:上限上调到 2100,并在注释里写明**额度用途**—— 【加速序列】属于"模型据此做不同决定"的行为判据,占用增删余地正当; 但技术说明仍不许占用这份额度(下次想再调数字时该去掉的是说明本身)。 【加速序列】块内容只留判定条件(多对象重复 / 顺序已知 / 以前做过), 格式细节全部下沉到 seq-tools skill —— 避免两处真相。 ## 验证 - go build ./... / go vet ./... 通过 - 分块跑全仓测试通过(agent、plugin、plugins/*、security、memory、 config、kernelboot、kernelmod、cmd) - 残留中文日志仅 1 条注释,非日志调用 ## 运维提示 日志文本变了,依赖 grep 中文的既有习惯要改为对应英文词,例如: 内核装配完成 -> kernel assembly complete 插件调度完成 -> plugin scheduling done 已建链 -> link established 已加载 N 个 -> N loaded
145 lines
5.6 KiB
Go
145 lines
5.6 KiB
Go
package security
|
||
|
||
// 内嵌主密钥(**字面内嵌**,按用户 2026-10-16 的明确选择)。
|
||
//
|
||
// ## 这是什么
|
||
//
|
||
// 把主密钥在**编译期**烧进二进制,运行期作为密钥来源链的**最高优先级**
|
||
// (在 systemd 凭据之前)。目的是「防丢失」:二进制在,密钥就在,
|
||
// 不存在"security.key 被误删/没跟着备份"导致全部密文永久解不开。
|
||
//
|
||
// ## ★ 安全代价(必须写清,不能含糊)
|
||
//
|
||
// 这条改动的代价是**真实的、且方向与加密的初衷相反**:
|
||
//
|
||
// 加密要防的是「文件被读走」。而内嵌后:
|
||
// · homed 是 0755、config.db 是 0644 —— 二者同级可达
|
||
// · 于是"能读二进制"⇒"能拿密钥"⇒"能解密 config.db"
|
||
// ⇒ **对本机读取者,加密不再提供任何保护**
|
||
// · 且二进制会进备份/镜像/分发包
|
||
// (实测:/home/program/_guard_backup/*/ 里 homed 与 config.db 成对出现)
|
||
//
|
||
// 换言之:内嵌换来"不会因丢文件而丢数据",代价是"文件被读走就等于全泄"。
|
||
// 这是用户知情后的选择,不是我们的疏忽 —— 但**任何降级/绕过都必须留痕**,
|
||
// 所以下面每次用到内嵌钥都会 log.Printf 说明当前密钥来自内嵌。
|
||
//
|
||
// ## 为什么用 ldflags 而不是 go:embed 一个文件
|
||
//
|
||
// 本仓有两个**公开**远端(gitcode / github)。`go:embed` 要求密钥是一个
|
||
// 仓库内的文件 —— 那意味着它会被提交、被推送、永久留在 git 历史里,
|
||
// 且任何 clone 的人都能拿到。`-ldflags -X` 让密钥只存在于**构建命令行**
|
||
// 与最终二进制中,仓库里一行都不留。
|
||
//
|
||
// go build -ldflags "-X .../internal/security.embeddedKeyHex=<64位hex>" ./cmd/homed
|
||
//
|
||
// ## 为什么不是"只信内嵌"(丢数据的陷阱)
|
||
//
|
||
// 最危险的做法是:有内嵌钥就**一切以它为准**、忽略别处。
|
||
// 那样一台已经用磁盘钥加密过的老机器,升级到这个二进制后会:
|
||
//
|
||
// ① 用内嵌钥去解磁盘钥加的密文 → 全部解不开
|
||
// ② 若此时"解不开就重建库"→ **静默丢掉全部凭据**
|
||
//
|
||
// 所以这里**只把内嵌钥当一个来源**,并且在磁盘/keyring 存在时把它作为
|
||
// **额外的解密钥**(可读旧密文),而不是替换现有钥。优先顺序见
|
||
// LoadOrCreateKey:内嵌 > systemd > env > 磁盘。
|
||
//
|
||
// ★ 并且内嵌钥与磁盘钥**不一致**时必须响亮告警(不能静默选一把继续):
|
||
// 那意味着这台机器上存在两套互不可解的密文,用户必须知道。
|
||
|
||
import (
|
||
"fmt"
|
||
"log"
|
||
"os"
|
||
"path/filepath"
|
||
)
|
||
|
||
// embeddedKeyHex 由构建时的 -ldflags 注入(64 位 hex)。
|
||
//
|
||
// ★ 默认空值:没有注入时,行为与改动前**逐字相同**(走 systemd/env/磁盘链)。
|
||
// 这是必须的 —— 否则用默认 Makefile 构建出的二进制会在所有老部署上
|
||
// "找不到密钥",那是一个破坏性变更。
|
||
var embeddedKeyHex string
|
||
|
||
// EmbeddedKeyPresent 报告本二进制是否内嵌了主密钥。
|
||
func EmbeddedKeyPresent() bool { return embeddedKeyHex != "" }
|
||
|
||
// embeddedKey 解出内嵌钥(未内嵌或格式非法时返回 nil)。
|
||
//
|
||
// 格式非法**不** Fatal:内嵌是由构建脚本注入的,格式错是构建问题;
|
||
// 运行期直接崩会让"二进制起来了但服务全挂"难以归因。这里降级为
|
||
// 响亮告警 + 回退到其它来源(安全性上不损失:回退路径本身是可信的)。
|
||
func embeddedKey() []byte {
|
||
if embeddedKeyHex == "" {
|
||
return nil
|
||
}
|
||
k, err := parseKey(embeddedKeyHex)
|
||
if err != nil {
|
||
log.Printf("[security] * embedded master key has invalid format (bad -ldflags injection at build time), ignored: %v", err)
|
||
return nil
|
||
}
|
||
if len(k) != 32 {
|
||
log.Printf("[security] * embedded master key has invalid length %d (need 32 bytes), ignored", len(k))
|
||
return nil
|
||
}
|
||
return k
|
||
}
|
||
|
||
// loadEmbeddedKey 取内嵌钥并说明来源(供 LoadOrCreateKey 使用)。
|
||
//
|
||
// 它额外做一件 LoadOrCreateKey 不该独自承担的事:**报告与磁盘钥是否一致**。
|
||
func loadEmbeddedKey(dataDir string) ([]byte, bool) {
|
||
k := embeddedKey()
|
||
if k == nil {
|
||
return nil, false
|
||
}
|
||
// 与磁盘钥比对(仅提示,不阻断)
|
||
if diskPath := filepath.Join(dataDir, KeyFileName); fileExistsSimple(diskPath) {
|
||
if b, err := os.ReadFile(diskPath); err == nil {
|
||
if dk, err := parseKey(string(trimSpaceBytes(b))); err == nil {
|
||
if !sameKey(dk, k) {
|
||
log.Printf("[security] ** embedded master key does NOT match %s: "+
|
||
"this machine holds two mutually undecryptable ciphertext sets (embedded key decrypts some, disk key the rest). "+
|
||
"Using the **embedded key as current**, keep the disk key via security.keyring history "+
|
||
"or HOMED_SECURITY_KEY, otherwise old ciphertext will become unreadable", diskPath)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return k, true
|
||
}
|
||
|
||
// EmbeddedKeyFingerprint 返回内嵌钥的短指纹(用于日志/自检,**不泄漏密钥**)。
|
||
//
|
||
// 用 hex 前 8 位:够区分两把不同的钥,又不足以从指纹还原。
|
||
// 绝不打印密钥本身 —— 日志会进 journal、被导出、被贴进 issue。
|
||
func EmbeddedKeyFingerprint() string {
|
||
k := embeddedKey()
|
||
if k == nil {
|
||
return ""
|
||
}
|
||
s := fmt.Sprintf("%x", k)
|
||
return s[:8]
|
||
}
|
||
|
||
func trimSpaceBytes(b []byte) []byte {
|
||
i, j := 0, len(b)
|
||
for i < j && isSpaceByte(b[i]) {
|
||
i++
|
||
}
|
||
for j > i && isSpaceByte(b[j-1]) {
|
||
j--
|
||
}
|
||
return b[i:j]
|
||
}
|
||
|
||
func isSpaceByte(c byte) bool {
|
||
return c == ' ' || c == '\t' || c == '\n' || c == '\r'
|
||
}
|
||
|
||
// fileExistsSimple 是本地最小实现(避免与包内其它 fileExists 重名冲突)。
|
||
func fileExistsSimple(p string) bool {
|
||
st, err := os.Stat(p)
|
||
return err == nil && !st.IsDir()
|
||
}
|