Files
HomeAgent/cmd/homed-key-rotate/main.go
JianFeeeee 4e70b8d5d8 refactor(module): 核心仓 Go module 路径改为 github.com/JianFeeeee/HologramAgent
批 1,不含 SDK 仓与目录改名。

- go.mod 第 1 行 module 路径
- 366 个 .go 文件的完整前缀 github.com/JianFeeeee/HomeAgent 机械替换
  (857 处,仅完整前缀;展示名/注释里的 HomeAgent 字样留给批 5)
- 未改:third_party 目录名、SDK 自身 go.mod module、go.mod replace 目标、
  homed/waiter 可执行名、.hmap 后缀
2026-10-21 12:17:19 +08:00

136 lines
5.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// homed-key-rotate:主密钥轮换(E-1)。
//
// ## 为何必须有这个命令
//
// `security` 包里的 `RotateKey` / `RotateKeyFile` / `RekeySecrets` 只是**零件**。
// 没有一条可执行的路径把它们串起来,E-1 就只是库里的能力,而不是
// 运维真能做的事 —— 而轮换的价值恰恰在于「出事时能执行」。
//
// ## 做三件事,顺序不可换
//
// ① 重加密变量库(security/variables.json)→ 新钥
// ② 写 security.keyring(旧钥降为只读历史)
// ③ 重加密 config.db 里的密文
//
// ★ 为什么必须「先重加密、后写 keyring」:
// 反过来的话,进程在中间崩溃会留下「keyring 说当前是新钥,但数据还是旧钥密文」
// 的状态 —— 而旧钥已不在环里可自动恢复,等于数据全丢。
//
// ## 用法
//
// homed-key-rotate -data /var/lib/homeagent -new-key <64位hex>
// homed-key-rotate -data /var/lib/homeagent -gen # 生成一把新钥并打印
//
// 轮换后**必须重启 homed**(密钥在启动时装配)。
package main
import (
"crypto/rand"
"encoding/hex"
"flag"
"fmt"
"os"
"path/filepath"
"github.com/JianFeeeee/HologramAgent/internal/config"
"github.com/JianFeeeee/HologramAgent/internal/security"
)
func main() {
dataDir := flag.String("data", "", "数据目录(config.db 所在处)")
newKeyHex := flag.String("new-key", "", "新主密钥(64 位 hex);与 -gen 二选一")
gen := flag.Bool("gen", false, "生成一把新主密钥并打印(不执行轮换)")
flag.Parse()
if *gen {
k := make([]byte, 32)
if _, err := rand.Read(k); err != nil {
fail("生成密钥失败: %v", err)
}
fmt.Println(hex.EncodeToString(k))
return
}
if *dataDir == "" || *newKeyHex == "" {
fail("必须给 -data 与 -new-key(或 -gen 生成一个)")
}
// ① 先取当前钥(走与 homed 完全相同的优先级,避免拿错钥)
oldKey, src, err := security.LoadOrCreateKey(*dataDir)
if err != nil {
fail("读取当前主密钥失败: %v", err)
}
newKey, err := hex.DecodeString(*newKeyHex)
if err != nil || len(newKey) != 32 {
fail("-new-key 必须是 64 位 hex(32 字节)")
}
if string(newKey) == string(oldKey) {
fail("新钥与当前钥相同,无需轮换")
}
history, err := security.LoadKeyring(*dataDir, oldKey)
if err != nil {
fail("读取密钥环失败: %v", err)
}
fmt.Printf("当前密钥来源: %s(历史密钥 %d 把)\n", src, len(history))
// ② 重加密变量库
st := security.New(security.Options{
Path: filepath.Join(*dataDir, "security", "variables.json"),
KeyFn: func() ([]byte, error) { return oldKey, nil },
OldKeys: history,
Enabled: true,
})
if err := st.Load(); err != nil {
fail("加载变量库失败(不静默重建): %v", err)
}
n := st.Count()
if err := st.RotateKey(newKey); err != nil {
fail("变量库轮换失败(数据未改动): %v", err)
}
fmt.Printf("① 变量库已重加密(%d 条)\n", n)
// ③ 写 keyring(旧钥转为只读历史)。放在数据重加密之后 —— 见文件头说明。
if err := security.RotateKeyFile(*dataDir, oldKey, newKey); err != nil {
fail("写密钥环失败(变量库已是新钥,此时不可回退): %v", err)
}
fmt.Printf("② 密钥环已更新(%s)\n", filepath.Join(*dataDir, security.KeyringFileName))
// 当前钥来源若是开发/磁盘路径,必须同步替换 security.key;否则下次启动
// LoadOrCreateKey 仍读旧钥,而 keyring current 已是新钥,会按设计响亮拒绝启动。
// systemd credential/env 不能由此命令擅自改:必须由运维更新注入源。
if src == filepath.Join(*dataDir, security.KeyFileName) {
path := filepath.Join(*dataDir, security.KeyFileName)
tmp := path + ".tmp"
if err := os.WriteFile(tmp, []byte(hex.EncodeToString(newKey)), 0o600); err != nil {
fail("更新磁盘主密钥失败: %v", err)
}
if err := os.Chmod(tmp, 0o600); err != nil {
fail("确权新主密钥失败: %v", err)
}
if err := os.Rename(tmp, path); err != nil {
fail("替换磁盘主密钥失败: %v", err)
}
fmt.Printf("③ 磁盘主密钥已替换(%s)\n", path)
} else {
fmt.Printf("③ 当前钥来自 %s:请先更新该注入源为新钥,再重启 homed\n", src)
}
if _, err := security.InitConfigCodecWithOldKeys(newKey, append([][]byte{oldKey}, history...)); err != nil {
fail("装配新配置 codec 失败: %v", err)
}
reg := config.NewConfigRegistry(filepath.Join(*dataDir, "config.db"))
defer reg.Close()
rekeyed, failed, err := reg.RekeySecrets()
if err != nil {
fail("config.db 重加密失败: %v", err)
}
fmt.Printf("③ config.db 已重加密 %d 条(失败 %d 条)\n", rekeyed, failed)
if failed > 0 {
fmt.Fprintf(os.Stderr, "⚠ 有 %d 条密文用新旧钥都解不开 —— 那说明它们来自更早的密钥,\n"+
" 请检查 security.keyring 的 history 是否完整(不要在此状态下删除旧钥)\n", failed)
}
fmt.Println("\n完成。请重启 homed 以使新密钥生效:systemctl restart homeagent.service")
}
func fail(format string, args ...interface{}) {
fmt.Fprintf(os.Stderr, "错误: "+format+"\n", args...)
os.Exit(1)
}