feat: 客户端加密 encrypted 字段,数据库只存密文 (v0.5.0)
Some checks failed
Secrets CLI - Build & Release / 质量检查 (fmt / clippy / test) (push) Successful in 1m27s
Secrets CLI - Build & Release / 版本 & Release (push) Successful in 2s
Secrets CLI - Build & Release / Build (x86_64-unknown-linux-musl) (push) Successful in 1m14s
Secrets CLI - Build & Release / 发布草稿 Release (push) Successful in 2s
Secrets CLI - Build & Release / Build (aarch64-apple-darwin) (push) Failing after 11m1s
Secrets CLI - Build & Release / Build (x86_64-pc-windows-msvc) (push) Has been cancelled

- 新增 src/crypto.rs:AES-256-GCM 加解密 + Argon2id 密钥派生 + OS Keychain 读写
- 新增 `secrets init` 命令:输入 Master Password,派生 Master Key 存入 Keychain
- 新增 `secrets migrate-encrypt` 命令:将旧明文 JSONB 数据批量加密
- 修改 db.rs:encrypted 列 JSONB → BYTEA,新增 kv_config 表(存 Argon2id salt)
- 修改 models.rs:encrypted 字段类型 Value → Vec<u8>
- 修改 add/update:写入前 encrypt_json,update 读取后 decrypt → 合并 → 重新加密
- 修改 search:按需解密,未解密时显示 _encrypted:true/_key_count:N
- 通过 6 个 crypto 单元测试(加解密、JSON roundtrip、Argon2id 确定性)

Made-with: Cursor
This commit is contained in:
voson
2026-03-18 20:10:13 +08:00
parent 1f7984d798
commit 8fdb6db87b
12 changed files with 828 additions and 66 deletions

View File

@@ -4,13 +4,14 @@ use sqlx::{FromRow, PgPool};
use uuid::Uuid;
use super::add::parse_kv;
use crate::crypto;
#[derive(FromRow)]
struct UpdateRow {
id: Uuid,
tags: Vec<String>,
metadata: Value,
encrypted: Value,
encrypted: Vec<u8>,
}
pub struct UpdateArgs<'a> {
@@ -25,7 +26,7 @@ pub struct UpdateArgs<'a> {
pub remove_secrets: &'a [String],
}
pub async fn run(pool: &PgPool, args: UpdateArgs<'_>) -> Result<()> {
pub async fn run(pool: &PgPool, args: UpdateArgs<'_>, master_key: &[u8; 32]) -> Result<()> {
let row: Option<UpdateRow> = sqlx::query_as(
r#"
SELECT id, tags, metadata, encrypted
@@ -71,8 +72,13 @@ pub async fn run(pool: &PgPool, args: UpdateArgs<'_>) -> Result<()> {
}
let metadata = Value::Object(meta_map);
// Merge encrypted
let mut enc_map: Map<String, Value> = match row.encrypted {
// Decrypt existing encrypted blob, merge changes, re-encrypt
let existing_json = if row.encrypted.is_empty() {
Value::Object(Map::new())
} else {
crypto::decrypt_json(master_key, &row.encrypted)?
};
let mut enc_map: Map<String, Value> = match existing_json {
Value::Object(m) => m,
_ => Map::new(),
};
@@ -83,7 +89,8 @@ pub async fn run(pool: &PgPool, args: UpdateArgs<'_>) -> Result<()> {
for key in args.remove_secrets {
enc_map.remove(key);
}
let encrypted = Value::Object(enc_map);
let secret_json = Value::Object(enc_map);
let encrypted_bytes = crypto::encrypt_json(master_key, &secret_json)?;
tracing::debug!(
namespace = args.namespace,
@@ -101,7 +108,7 @@ pub async fn run(pool: &PgPool, args: UpdateArgs<'_>) -> Result<()> {
)
.bind(&tags)
.bind(metadata)
.bind(encrypted)
.bind(encrypted_bytes)
.bind(row.id)
.execute(pool)
.await?;