refactor(secrets): remove migrate_encrypt command
Some checks failed
Secrets CLI - Build & Release / 版本 & Release (push) Successful in 3s
Secrets CLI - Build & Release / 质量检查 (fmt / clippy / test) (push) Successful in 1m38s
Secrets CLI - Build & Release / Build (x86_64-unknown-linux-musl) (push) Successful in 1m9s
Secrets CLI - Build & Release / 发布草稿 Release (push) Successful in 5s
Secrets CLI - Build & Release / Build (aarch64-apple-darwin) (push) Failing after 7m27s
Secrets CLI - Build & Release / Build (x86_64-pc-windows-msvc) (push) Has been cancelled
Some checks failed
Secrets CLI - Build & Release / 版本 & Release (push) Successful in 3s
Secrets CLI - Build & Release / 质量检查 (fmt / clippy / test) (push) Successful in 1m38s
Secrets CLI - Build & Release / Build (x86_64-unknown-linux-musl) (push) Successful in 1m9s
Secrets CLI - Build & Release / 发布草稿 Release (push) Successful in 5s
Secrets CLI - Build & Release / Build (aarch64-apple-darwin) (push) Failing after 7m27s
Secrets CLI - Build & Release / Build (x86_64-pc-windows-msvc) (push) Has been cancelled
Made-with: Cursor
This commit is contained in:
47
AGENTS.md
47
AGENTS.md
@@ -1,6 +1,6 @@
|
|||||||
# Secrets CLI — AGENTS.md
|
# Secrets CLI — AGENTS.md
|
||||||
|
|
||||||
跨设备密钥与配置管理 CLI 工具,将 refining / ricnsmart 两个项目的服务器信息、服务凭据存储到 PostgreSQL 18,供 AI 工具读取上下文。
|
跨设备密钥与配置管理 CLI 工具,将 refining / ricnsmart 两个项目的服务器信息、服务凭据存储到 PostgreSQL 18,供 AI 工具读取上下文。敏感数据(encrypted 字段)使用 AES-256-GCM 加密,主密钥由 Argon2id 从主密码派生并存入平台安全存储(macOS Keychain / Windows Credential Manager / Linux keyutils)。
|
||||||
|
|
||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
@@ -10,10 +10,12 @@ secrets/
|
|||||||
main.rs # CLI 入口,clap 命令定义,auto-migrate,--verbose 全局参数
|
main.rs # CLI 入口,clap 命令定义,auto-migrate,--verbose 全局参数
|
||||||
output.rs # OutputMode 枚举 + TTY 检测(TTY→text,非 TTY→json-compact)
|
output.rs # OutputMode 枚举 + TTY 检测(TTY→text,非 TTY→json-compact)
|
||||||
config.rs # 配置读写:~/.config/secrets/config.toml(database_url)
|
config.rs # 配置读写:~/.config/secrets/config.toml(database_url)
|
||||||
db.rs # PgPool 创建 + 建表/索引(幂等,含 audit_log)
|
db.rs # PgPool 创建 + 建表/索引(幂等,含 audit_log + kv_config)
|
||||||
|
crypto.rs # AES-256-GCM 加解密、Argon2id 派生、OS 钥匙串
|
||||||
models.rs # Secret 结构体(sqlx::FromRow + serde)
|
models.rs # Secret 结构体(sqlx::FromRow + serde)
|
||||||
audit.rs # 审计写入:向 audit_log 表记录所有写操作
|
audit.rs # 审计写入:向 audit_log 表记录所有写操作
|
||||||
commands/
|
commands/
|
||||||
|
init.rs # init 命令:主密钥初始化(每台设备一次)
|
||||||
add.rs # add 命令:upsert,支持 --meta key=value / --secret key=@file / -o json
|
add.rs # add 命令:upsert,支持 --meta key=value / --secret key=@file / -o json
|
||||||
config.rs # config 命令:set-db / show / path(持久化 database_url)
|
config.rs # config 命令:set-db / show / path(持久化 database_url)
|
||||||
search.rs # search 命令:多条件查询,-f/-o/--summary/--limit/--offset/--sort
|
search.rs # search 命令:多条件查询,-f/-o/--summary/--limit/--offset/--sort
|
||||||
@@ -31,7 +33,7 @@ secrets/
|
|||||||
- **Host**: `<host>:<port>`
|
- **Host**: `<host>:<port>`
|
||||||
- **Database**: `secrets`
|
- **Database**: `secrets`
|
||||||
- **连接串**: `postgres://postgres:<password>@<host>:<port>/secrets`
|
- **连接串**: `postgres://postgres:<password>@<host>:<port>/secrets`
|
||||||
- **表**: `secrets`(主表)+ `audit_log`(审计表),首次连接自动建表(auto-migrate)
|
- **表**: `secrets`(主表)+ `audit_log`(审计表)+ `kv_config`(Argon2 salt 等),首次连接自动建表(auto-migrate)
|
||||||
|
|
||||||
### 表结构
|
### 表结构
|
||||||
|
|
||||||
@@ -43,13 +45,20 @@ secrets (
|
|||||||
name VARCHAR(256) NOT NULL, -- 人类可读标识
|
name VARCHAR(256) NOT NULL, -- 人类可读标识
|
||||||
tags TEXT[] NOT NULL DEFAULT '{}', -- 灵活标签: ["aliyun","hongkong"]
|
tags TEXT[] NOT NULL DEFAULT '{}', -- 灵活标签: ["aliyun","hongkong"]
|
||||||
metadata JSONB NOT NULL DEFAULT '{}', -- 明文描述: ip, desc, domains, location...
|
metadata JSONB NOT NULL DEFAULT '{}', -- 明文描述: ip, desc, domains, location...
|
||||||
encrypted JSONB NOT NULL DEFAULT '{}', -- 敏感数据: ssh_key, password, token...
|
encrypted BYTEA NOT NULL DEFAULT '\x', -- AES-256-GCM 密文: nonce(12B)||ciphertext+tag
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
UNIQUE(namespace, kind, name)
|
UNIQUE(namespace, kind, name)
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```sql
|
||||||
|
kv_config (
|
||||||
|
key TEXT PRIMARY KEY, -- 如 'argon2_salt'
|
||||||
|
value BYTEA NOT NULL -- Argon2id salt,首台设备 init 时生成
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
### audit_log 表结构
|
### audit_log 表结构
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
@@ -74,7 +83,7 @@ audit_log (
|
|||||||
| `name` | 唯一标识名 | `i-uf63f2uookgs5uxmrdyc`, `gitea` |
|
| `name` | 唯一标识名 | `i-uf63f2uookgs5uxmrdyc`, `gitea` |
|
||||||
| `tags` | 多维分类标签 | `["aliyun","hongkong","ricn"]` |
|
| `tags` | 多维分类标签 | `["aliyun","hongkong","ricn"]` |
|
||||||
| `metadata` | 明文非敏感信息 | `{"ip":"47.243.154.187","desc":"Grafana","domains":["..."]}` |
|
| `metadata` | 明文非敏感信息 | `{"ip":"47.243.154.187","desc":"Grafana","domains":["..."]}` |
|
||||||
| `encrypted` | 敏感凭据(MVP 阶段明文存储,后续对 value 加密) | `{"ssh_key":"-----BEGIN...","password":"..."}` |
|
| `encrypted` | 敏感凭据,AES-256-GCM 加密存储 | 二进制密文,解密后为 `{"ssh_key":"...","password":"..."}` |
|
||||||
|
|
||||||
## 数据库配置
|
## 数据库配置
|
||||||
|
|
||||||
@@ -88,6 +97,21 @@ secrets config path # 打印配置文件路径
|
|||||||
|
|
||||||
配置文件:`~/.config/secrets/config.toml`,权限 0600。`--db-url` 参数可一次性覆盖。
|
配置文件:`~/.config/secrets/config.toml`,权限 0600。`--db-url` 参数可一次性覆盖。
|
||||||
|
|
||||||
|
## 主密钥与加密
|
||||||
|
|
||||||
|
首次使用(每台设备各执行一次):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
secrets config set-db "postgres://postgres:<password>@<host>:<port>/secrets"
|
||||||
|
secrets init # 提示输入主密码,Argon2id 派生主密钥后存入 OS 钥匙串
|
||||||
|
```
|
||||||
|
|
||||||
|
主密码不存储;salt 存于 `kv_config`,首台设备生成后共享,确保同一主密码在所有设备派生出相同主密钥。
|
||||||
|
|
||||||
|
主密钥存储后端:macOS Keychain、Windows Credential Manager、Linux keyutils(会话级,重启后需再次 `secrets init`)。
|
||||||
|
|
||||||
|
**从旧版(明文 JSONB)升级**:升级后执行 `secrets init` 即可(明文记录需手动重新 add 或通过 update 更新)。
|
||||||
|
|
||||||
## CLI 命令
|
## CLI 命令
|
||||||
|
|
||||||
### AI 使用主路径
|
### AI 使用主路径
|
||||||
@@ -102,6 +126,16 @@ secrets config path # 打印配置文件路径
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### init — 主密钥初始化(每台设备一次)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 首次设备:生成 Argon2id salt 并存库,派生主密钥后存 OS 钥匙串
|
||||||
|
secrets init
|
||||||
|
|
||||||
|
# 后续设备:复用已有 salt,派生主密钥后存钥匙串(主密码需与首台相同)
|
||||||
|
secrets init
|
||||||
|
```
|
||||||
|
|
||||||
### search — 发现与读取
|
### search — 发现与读取
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -250,7 +284,7 @@ secrets delete -n ricnsmart --kind server --name i-old-server-id
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### config — 配置管理
|
### config — 配置管理(无需主密钥)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 设置数据库连接(每台设备执行一次,之后永久生效)
|
# 设置数据库连接(每台设备执行一次,之后永久生效)
|
||||||
@@ -289,6 +323,7 @@ secrets --db-url "postgres://..." search -n refining
|
|||||||
- 字段命名:CLI 短标志 `-n`=namespace,`-m`=meta,`-s`=secret,`-q`=query,`-v`=verbose,`-f`=field,`-o`=output
|
- 字段命名:CLI 短标志 `-n`=namespace,`-m`=meta,`-s`=secret,`-q`=query,`-v`=verbose,`-f`=field,`-o`=output
|
||||||
- 日志:用户可见输出用 `println!`;调试/运维信息用 `tracing::debug!`/`info!`/`warn!`/`error!`
|
- 日志:用户可见输出用 `println!`;调试/运维信息用 `tracing::debug!`/`info!`/`warn!`/`error!`
|
||||||
- 审计:`add`/`update`/`delete` 成功后调用 `audit::log()`,写入 `audit_log` 表;失败只 warn 不中断
|
- 审计:`add`/`update`/`delete` 成功后调用 `audit::log()`,写入 `audit_log` 表;失败只 warn 不中断
|
||||||
|
- 加密:`encrypted` 列存储 AES-256-GCM 密文;`add`/`update`/`search`/`delete` 需主密钥(`secrets init` 后从 OS 钥匙串加载)
|
||||||
- 输出:读命令通过 `OutputMode` 支持 text/json/json-compact/env;写命令 `add` 同样支持 `-o json`
|
- 输出:读命令通过 `OutputMode` 支持 text/json/json-compact/env;写命令 `add` 同样支持 `-o json`
|
||||||
|
|
||||||
## 提交前检查(必须全部通过)
|
## 提交前检查(必须全部通过)
|
||||||
|
|||||||
89
Cargo.lock
generated
89
Cargo.lock
generated
@@ -909,9 +909,12 @@ version = "3.6.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c"
|
checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"linux-keyutils",
|
||||||
"log",
|
"log",
|
||||||
"security-framework 2.11.1",
|
"security-framework 2.11.1",
|
||||||
"security-framework 3.7.0",
|
"security-framework 3.7.0",
|
||||||
|
"windows-sys 0.60.2",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -964,6 +967,16 @@ dependencies = [
|
|||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linux-keyutils"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "761e49ec5fd8a5a463f9b84e877c373d888935b71c6be78f3767fe2ae6bed18e"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "litemap"
|
name = "litemap"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
@@ -2463,6 +2476,15 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.60.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets 0.53.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.61.2"
|
version = "0.61.2"
|
||||||
@@ -2496,13 +2518,30 @@ dependencies = [
|
|||||||
"windows_aarch64_gnullvm 0.52.6",
|
"windows_aarch64_gnullvm 0.52.6",
|
||||||
"windows_aarch64_msvc 0.52.6",
|
"windows_aarch64_msvc 0.52.6",
|
||||||
"windows_i686_gnu 0.52.6",
|
"windows_i686_gnu 0.52.6",
|
||||||
"windows_i686_gnullvm",
|
"windows_i686_gnullvm 0.52.6",
|
||||||
"windows_i686_msvc 0.52.6",
|
"windows_i686_msvc 0.52.6",
|
||||||
"windows_x86_64_gnu 0.52.6",
|
"windows_x86_64_gnu 0.52.6",
|
||||||
"windows_x86_64_gnullvm 0.52.6",
|
"windows_x86_64_gnullvm 0.52.6",
|
||||||
"windows_x86_64_msvc 0.52.6",
|
"windows_x86_64_msvc 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-targets"
|
||||||
|
version = "0.53.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
"windows_aarch64_gnullvm 0.53.1",
|
||||||
|
"windows_aarch64_msvc 0.53.1",
|
||||||
|
"windows_i686_gnu 0.53.1",
|
||||||
|
"windows_i686_gnullvm 0.53.1",
|
||||||
|
"windows_i686_msvc 0.53.1",
|
||||||
|
"windows_x86_64_gnu 0.53.1",
|
||||||
|
"windows_x86_64_gnullvm 0.53.1",
|
||||||
|
"windows_x86_64_msvc 0.53.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_aarch64_gnullvm"
|
name = "windows_aarch64_gnullvm"
|
||||||
version = "0.48.5"
|
version = "0.48.5"
|
||||||
@@ -2515,6 +2554,12 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_gnullvm"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_aarch64_msvc"
|
name = "windows_aarch64_msvc"
|
||||||
version = "0.48.5"
|
version = "0.48.5"
|
||||||
@@ -2527,6 +2572,12 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_msvc"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_gnu"
|
name = "windows_i686_gnu"
|
||||||
version = "0.48.5"
|
version = "0.48.5"
|
||||||
@@ -2539,12 +2590,24 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnu"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_gnullvm"
|
name = "windows_i686_gnullvm"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnullvm"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_msvc"
|
name = "windows_i686_msvc"
|
||||||
version = "0.48.5"
|
version = "0.48.5"
|
||||||
@@ -2557,6 +2620,12 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_msvc"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnu"
|
name = "windows_x86_64_gnu"
|
||||||
version = "0.48.5"
|
version = "0.48.5"
|
||||||
@@ -2569,6 +2638,12 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnu"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnullvm"
|
name = "windows_x86_64_gnullvm"
|
||||||
version = "0.48.5"
|
version = "0.48.5"
|
||||||
@@ -2581,6 +2656,12 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnullvm"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_msvc"
|
name = "windows_x86_64_msvc"
|
||||||
version = "0.48.5"
|
version = "0.48.5"
|
||||||
@@ -2593,6 +2674,12 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_msvc"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winnow"
|
name = "winnow"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ argon2 = { version = "0.5.3", features = ["std"] }
|
|||||||
chrono = { version = "0.4.44", features = ["serde"] }
|
chrono = { version = "0.4.44", features = ["serde"] }
|
||||||
clap = { version = "4.6.0", features = ["derive", "env"] }
|
clap = { version = "4.6.0", features = ["derive", "env"] }
|
||||||
dirs = "6.0.0"
|
dirs = "6.0.0"
|
||||||
keyring = { version = "3.6.3", features = ["apple-native"] }
|
keyring = { version = "3.6.3", features = ["apple-native", "windows-native", "linux-native"] }
|
||||||
rand = "0.10.0"
|
rand = "0.10.0"
|
||||||
rpassword = "7.4.0"
|
rpassword = "7.4.0"
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
|||||||
26
README.md
26
README.md
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
跨设备密钥与配置管理 CLI,基于 Rust + PostgreSQL 18。
|
跨设备密钥与配置管理 CLI,基于 Rust + PostgreSQL 18。
|
||||||
|
|
||||||
将服务器信息、服务凭据统一存入数据库,供本地工具和 AI 读取上下文。
|
将服务器信息、服务凭据统一存入数据库,供本地工具和 AI 读取上下文。敏感数据(`encrypted` 字段)使用 AES-256-GCM 加密存储,主密钥由 Argon2id 从主密码派生并存入系统钥匙串。
|
||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
|
|
||||||
@@ -11,12 +11,22 @@ cargo build --release
|
|||||||
# 或从 Release 页面下载预编译二进制
|
# 或从 Release 页面下载预编译二进制
|
||||||
```
|
```
|
||||||
|
|
||||||
配置数据库连接(首次使用需执行一次,之后在该设备上持久生效):
|
## 首次使用(每台设备各执行一次)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# 1. 配置数据库连接
|
||||||
secrets config set-db "postgres://postgres:<password>@<host>:<port>/secrets"
|
secrets config set-db "postgres://postgres:<password>@<host>:<port>/secrets"
|
||||||
|
|
||||||
|
# 2. 初始化主密钥(提示输入主密码,派生后存入 OS 钥匙串)
|
||||||
|
secrets init
|
||||||
```
|
```
|
||||||
|
|
||||||
|
主密码不会存储,仅用于派生主密钥。同一主密码在所有设备上会得到相同主密钥(salt 存于数据库,首台设备生成后共享)。
|
||||||
|
|
||||||
|
**主密钥存储**:macOS → Keychain;Windows → Credential Manager;Linux → keyutils(会话级,重启后需再次 `secrets init`)。
|
||||||
|
|
||||||
|
**从旧版(明文存储)升级**:升级后首次运行需执行 `secrets init` 即可(明文记录需手动重新 add 或通过 update 更新)。
|
||||||
|
|
||||||
## AI Agent 快速指南
|
## AI Agent 快速指南
|
||||||
|
|
||||||
这个 CLI 以 AI 使用优先设计。核心路径只有一条:**读取用 `search`,写入用 `add` / `update`**。
|
这个 CLI 以 AI 使用优先设计。核心路径只有一条:**读取用 `search`,写入用 `add` / `update`**。
|
||||||
@@ -80,6 +90,7 @@ secrets search -n refining --kind service --name gitea -o env --show-secrets \
|
|||||||
```bash
|
```bash
|
||||||
# 查看帮助(包含各子命令 EXAMPLES)
|
# 查看帮助(包含各子命令 EXAMPLES)
|
||||||
secrets --help
|
secrets --help
|
||||||
|
secrets init --help # 主密钥初始化
|
||||||
secrets search --help
|
secrets search --help
|
||||||
secrets add --help
|
secrets add --help
|
||||||
secrets update --help
|
secrets update --help
|
||||||
@@ -116,6 +127,9 @@ secrets update -n refining --kind service --name mqtt --remove-meta old_port --r
|
|||||||
# ── delete ───────────────────────────────────────────────────────────────────
|
# ── delete ───────────────────────────────────────────────────────────────────
|
||||||
secrets delete -n refining --kind service --name legacy-mqtt
|
secrets delete -n refining --kind service --name legacy-mqtt
|
||||||
|
|
||||||
|
# ── init ─────────────────────────────────────────────────────────────────────
|
||||||
|
secrets init # 主密钥初始化(每台设备一次,主密码派生后存钥匙串)
|
||||||
|
|
||||||
# ── config ───────────────────────────────────────────────────────────────────
|
# ── config ───────────────────────────────────────────────────────────────────
|
||||||
secrets config set-db "postgres://postgres:<password>@<host>:<port>/secrets"
|
secrets config set-db "postgres://postgres:<password>@<host>:<port>/secrets"
|
||||||
secrets config show # 密码脱敏展示
|
secrets config show # 密码脱敏展示
|
||||||
@@ -137,9 +151,9 @@ RUST_LOG=secrets=trace secrets search
|
|||||||
| `name` | 人类可读唯一标识 |
|
| `name` | 人类可读唯一标识 |
|
||||||
| `tags` | 多维标签,如 `["aliyun","hongkong"]` |
|
| `tags` | 多维标签,如 `["aliyun","hongkong"]` |
|
||||||
| `metadata` | 明文描述信息(ip、desc、domains 等) |
|
| `metadata` | 明文描述信息(ip、desc、domains 等) |
|
||||||
| `encrypted` | 敏感凭据(ssh_key、password、token 等),MVP 阶段明文存储,预留加密字段 |
|
| `encrypted` | 敏感凭据(ssh_key、password、token 等),AES-256-GCM 加密存储 |
|
||||||
|
|
||||||
`-m` / `--meta` 写入 `metadata`,`-s` / `--secret` 写入 `encrypted`,`value=@file` 从文件读取内容。
|
`-m` / `--meta` 写入 `metadata`,`-s` / `--secret` 写入 `encrypted`,`value=@file` 从文件读取内容。加解密使用主密钥(由 `secrets init` 设置)。
|
||||||
|
|
||||||
## 审计日志
|
## 审计日志
|
||||||
|
|
||||||
@@ -160,10 +174,12 @@ src/
|
|||||||
main.rs # CLI 入口(clap),含各子命令 after_help 示例
|
main.rs # CLI 入口(clap),含各子命令 after_help 示例
|
||||||
output.rs # OutputMode 枚举 + TTY 检测
|
output.rs # OutputMode 枚举 + TTY 检测
|
||||||
config.rs # 配置读写(~/.config/secrets/config.toml)
|
config.rs # 配置读写(~/.config/secrets/config.toml)
|
||||||
db.rs # 连接池 + auto-migrate(secrets + audit_log)
|
db.rs # 连接池 + auto-migrate(secrets + audit_log + kv_config)
|
||||||
|
crypto.rs # AES-256-GCM 加解密、Argon2id 派生、OS 钥匙串
|
||||||
models.rs # Secret 结构体
|
models.rs # Secret 结构体
|
||||||
audit.rs # 审计日志写入(audit_log 表)
|
audit.rs # 审计日志写入(audit_log 表)
|
||||||
commands/
|
commands/
|
||||||
|
init.rs # 主密钥初始化(首次/新设备)
|
||||||
add.rs # upsert,支持 -o json
|
add.rs # upsert,支持 -o json
|
||||||
config.rs # config set-db/show/path
|
config.rs # config set-db/show/path
|
||||||
search.rs # 多条件查询,支持 -f/-o/--summary/--limit/--offset/--sort
|
search.rs # 多条件查询,支持 -f/-o/--summary/--limit/--offset/--sort
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
use anyhow::Result;
|
|
||||||
use sqlx::PgPool;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::crypto;
|
|
||||||
|
|
||||||
/// Row fetched for migration
|
|
||||||
#[derive(sqlx::FromRow)]
|
|
||||||
struct MigrateRow {
|
|
||||||
id: Uuid,
|
|
||||||
namespace: String,
|
|
||||||
kind: String,
|
|
||||||
name: String,
|
|
||||||
encrypted: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Encrypt any records whose `encrypted` column contains raw (unencrypted) bytes.
|
|
||||||
///
|
|
||||||
/// After the schema migration, old JSONB rows were stored as raw UTF-8 bytes.
|
|
||||||
/// A valid AES-256-GCM blob is always at least 28 bytes (12 nonce + 16 tag).
|
|
||||||
/// We attempt to decrypt each row; if decryption fails, we assume it's plaintext
|
|
||||||
/// JSON and re-encrypt it.
|
|
||||||
pub async fn run(pool: &PgPool, master_key: &[u8; 32]) -> Result<()> {
|
|
||||||
println!("Scanning for unencrypted secret rows...");
|
|
||||||
|
|
||||||
let rows: Vec<MigrateRow> =
|
|
||||||
sqlx::query_as("SELECT id, namespace, kind, name, encrypted FROM secrets")
|
|
||||||
.fetch_all(pool)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let total = rows.len();
|
|
||||||
let mut migrated = 0usize;
|
|
||||||
let mut already_encrypted = 0usize;
|
|
||||||
let mut skipped_empty = 0usize;
|
|
||||||
|
|
||||||
for row in rows {
|
|
||||||
if row.encrypted.is_empty() {
|
|
||||||
skipped_empty += 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to decrypt; success → already encrypted, skip
|
|
||||||
if crypto::decrypt_json(master_key, &row.encrypted).is_ok() {
|
|
||||||
already_encrypted += 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Treat as plaintext JSON bytes (from schema migration copy)
|
|
||||||
let json_str = String::from_utf8(row.encrypted.clone()).map_err(|_| {
|
|
||||||
anyhow::anyhow!(
|
|
||||||
"Row [{}/{}/{}]: encrypted column contains non-UTF-8 bytes that are also not valid ciphertext. Manual inspection required.",
|
|
||||||
row.namespace, row.kind, row.name
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let value: serde_json::Value = serde_json::from_str(&json_str).map_err(|e| {
|
|
||||||
anyhow::anyhow!(
|
|
||||||
"Row [{}/{}/{}]: failed to parse as JSON: {}",
|
|
||||||
row.namespace,
|
|
||||||
row.kind,
|
|
||||||
row.name,
|
|
||||||
e
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let encrypted_bytes = crypto::encrypt_json(master_key, &value)?;
|
|
||||||
|
|
||||||
sqlx::query("UPDATE secrets SET encrypted = $1, updated_at = NOW() WHERE id = $2")
|
|
||||||
.bind(&encrypted_bytes)
|
|
||||||
.bind(row.id)
|
|
||||||
.execute(pool)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
println!(" Encrypted: [{}/{}] {}", row.namespace, row.kind, row.name);
|
|
||||||
migrated += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
println!();
|
|
||||||
println!(
|
|
||||||
"Done. Total: {total}, encrypted this run: {migrated}, \
|
|
||||||
already encrypted: {already_encrypted}, empty (skipped): {skipped_empty}"
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,5 @@ pub mod add;
|
|||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod delete;
|
pub mod delete;
|
||||||
pub mod init;
|
pub mod init;
|
||||||
pub mod migrate_encrypt;
|
|
||||||
pub mod search;
|
pub mod search;
|
||||||
pub mod update;
|
pub mod update;
|
||||||
|
|||||||
@@ -37,8 +37,7 @@ pub async fn migrate(pool: &PgPool) -> Result<()> {
|
|||||||
|
|
||||||
-- Migrate encrypted column from JSONB to BYTEA if still JSONB type.
|
-- Migrate encrypted column from JSONB to BYTEA if still JSONB type.
|
||||||
-- After migration, old plaintext rows will have their JSONB data
|
-- After migration, old plaintext rows will have their JSONB data
|
||||||
-- stored as raw bytes (not yet re-encrypted); run `secrets migrate-encrypt`
|
-- stored as raw bytes (UTF-8 encoded).
|
||||||
-- to encrypt them with the master key.
|
|
||||||
DO $$ BEGIN
|
DO $$ BEGIN
|
||||||
IF EXISTS (
|
IF EXISTS (
|
||||||
SELECT 1 FROM information_schema.columns
|
SELECT 1 FROM information_schema.columns
|
||||||
|
|||||||
10
src/main.rs
10
src/main.rs
@@ -60,12 +60,6 @@ enum Commands {
|
|||||||
secrets init")]
|
secrets init")]
|
||||||
Init,
|
Init,
|
||||||
|
|
||||||
/// Encrypt any pre-existing plaintext records in the database.
|
|
||||||
///
|
|
||||||
/// Run this once after upgrading from a version that stored secrets as
|
|
||||||
/// plaintext JSONB. Requires `secrets init` to have been run first.
|
|
||||||
MigrateEncrypt,
|
|
||||||
|
|
||||||
/// Add or update a record (upsert). Use -m for plaintext metadata, -s for secrets.
|
/// Add or update a record (upsert). Use -m for plaintext metadata, -s for secrets.
|
||||||
#[command(after_help = "EXAMPLES:
|
#[command(after_help = "EXAMPLES:
|
||||||
# Add a server
|
# Add a server
|
||||||
@@ -330,10 +324,6 @@ async fn main() -> Result<()> {
|
|||||||
match &cli.command {
|
match &cli.command {
|
||||||
Commands::Init | Commands::Config { .. } => unreachable!(),
|
Commands::Init | Commands::Config { .. } => unreachable!(),
|
||||||
|
|
||||||
Commands::MigrateEncrypt => {
|
|
||||||
commands::migrate_encrypt::run(&pool, &master_key).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Commands::Add {
|
Commands::Add {
|
||||||
namespace,
|
namespace,
|
||||||
kind,
|
kind,
|
||||||
|
|||||||
Reference in New Issue
Block a user