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

Made-with: Cursor
This commit is contained in:
voson
2026-03-19 09:17:04 +08:00
parent 8fdb6db87b
commit dc0534cbc9
8 changed files with 152 additions and 111 deletions

View File

@@ -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(())
}

View File

@@ -2,6 +2,5 @@ pub mod add;
pub mod config;
pub mod delete;
pub mod init;
pub mod migrate_encrypt;
pub mod search;
pub mod update;

View File

@@ -37,8 +37,7 @@ pub async fn migrate(pool: &PgPool) -> Result<()> {
-- Migrate encrypted column from JSONB to BYTEA if still JSONB type.
-- After migration, old plaintext rows will have their JSONB data
-- stored as raw bytes (not yet re-encrypted); run `secrets migrate-encrypt`
-- to encrypt them with the master key.
-- stored as raw bytes (UTF-8 encoded).
DO $$ BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns

View File

@@ -60,12 +60,6 @@ enum Commands {
secrets 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.
#[command(after_help = "EXAMPLES:
# Add a server
@@ -330,10 +324,6 @@ async fn main() -> Result<()> {
match &cli.command {
Commands::Init | Commands::Config { .. } => unreachable!(),
Commands::MigrateEncrypt => {
commands::migrate_encrypt::run(&pool, &master_key).await?;
}
Commands::Add {
namespace,
kind,