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:
@@ -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 delete;
|
||||
pub mod init;
|
||||
pub mod migrate_encrypt;
|
||||
pub mod search;
|
||||
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.
|
||||
-- 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
|
||||
|
||||
10
src/main.rs
10
src/main.rs
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user