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
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:
62
src/commands/init.rs
Normal file
62
src/commands/init.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use anyhow::{Context, Result};
|
||||
use rand::RngExt;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{crypto, db};
|
||||
|
||||
pub async fn run(pool: &PgPool) -> Result<()> {
|
||||
println!("Initializing secrets master key...");
|
||||
println!();
|
||||
|
||||
// Read password (no echo)
|
||||
let password =
|
||||
rpassword::prompt_password("Enter master password: ").context("failed to read password")?;
|
||||
if password.is_empty() {
|
||||
anyhow::bail!("Master password must not be empty.");
|
||||
}
|
||||
let confirm = rpassword::prompt_password("Confirm master password: ")
|
||||
.context("failed to read password confirmation")?;
|
||||
if password != confirm {
|
||||
anyhow::bail!("Passwords do not match.");
|
||||
}
|
||||
|
||||
// Get or create Argon2id salt
|
||||
let salt = match db::load_argon2_salt(pool).await? {
|
||||
Some(existing) => {
|
||||
println!("Found existing salt in database (not the first device).");
|
||||
existing
|
||||
}
|
||||
None => {
|
||||
println!("Generating new Argon2id salt and storing in database...");
|
||||
let mut salt = vec![0u8; 16];
|
||||
rand::rng().fill(&mut salt[..]);
|
||||
db::store_argon2_salt(pool, &salt).await?;
|
||||
salt
|
||||
}
|
||||
};
|
||||
|
||||
// Derive master key
|
||||
print!("Deriving master key (Argon2id, this takes a moment)... ");
|
||||
let master_key = crypto::derive_master_key(&password, &salt)?;
|
||||
println!("done.");
|
||||
|
||||
// Store in OS Keychain
|
||||
crypto::store_master_key(&master_key)?;
|
||||
|
||||
// Self-test: encrypt and decrypt a canary value
|
||||
let canary = b"secrets-cli-canary";
|
||||
let enc = crypto::encrypt(&master_key, canary)?;
|
||||
let dec = crypto::decrypt(&master_key, &enc)?;
|
||||
if dec != canary {
|
||||
anyhow::bail!("Self-test failed: encryption roundtrip mismatch");
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("Master key stored in OS Keychain.");
|
||||
println!("You can now use `secrets add` / `secrets search` commands.");
|
||||
println!();
|
||||
println!("IMPORTANT: Remember your master password — it is not stored anywhere.");
|
||||
println!(" On a new device, run `secrets init` with the same password.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user