package main import ( "fmt" "os" "git.wntrmute.dev/kyle/mcias/internal/crypto" "git.wntrmute.dev/kyle/mcias/internal/db" ) // runRekey re-encrypts all secrets under a new passphrase-derived master key. // // The current master key (already loaded in tool.masterKey by openDB) is used // to decrypt every encrypted secret: the Ed25519 signing key, all TOTP secrets, // and all Postgres credential passwords. The operator is then prompted for a // new passphrase (confirmed), a fresh Argon2id salt is generated, a new 256-bit // master key is derived, and all secrets are re-encrypted and written back in a // single atomic SQLite transaction. // // Security: The entire re-encryption happens in memory first; the database is // only updated once all ciphertext has been produced successfully. The new // salt replaces the old salt atomically within the same transaction so the // database is never left in a mixed state. Both the old and new master keys // are zeroed in deferred cleanup. No secret material is logged or printed. func (t *tool) runRekey(_ []string) { // ── 1. Decrypt signing key under old master key ────────────────────── sigKeyEnc, sigKeyNonce, err := t.db.ReadServerConfig() if err != nil { fatalf("read server config: %v", err) } sigKeyPEM, err := crypto.OpenAESGCM(t.masterKey, sigKeyNonce, sigKeyEnc) if err != nil { fatalf("decrypt signing key: %v", err) } // ── 2. Decrypt all TOTP secrets under old master key ───────────────── totpAccounts, err := t.db.ListAccountsWithTOTP() if err != nil { fatalf("list accounts with TOTP: %v", err) } type totpPlain struct { secret []byte accountID int64 } totpPlaintexts := make([]totpPlain, 0, len(totpAccounts)) for _, a := range totpAccounts { pt, err := crypto.OpenAESGCM(t.masterKey, a.TOTPSecretNonce, a.TOTPSecretEnc) if err != nil { fatalf("decrypt TOTP secret for account %s: %v", a.Username, err) } totpPlaintexts = append(totpPlaintexts, totpPlain{accountID: a.ID, secret: pt}) } // ── 3. Decrypt all pg_credentials passwords under old master key ────── pgCreds, err := t.db.ListAllPGCredentials() if err != nil { fatalf("list pg credentials: %v", err) } type pgPlain struct { password []byte credID int64 } pgPlaintexts := make([]pgPlain, 0, len(pgCreds)) for _, c := range pgCreds { pt, err := crypto.OpenAESGCM(t.masterKey, c.PGPasswordNonce, c.PGPasswordEnc) if err != nil { fatalf("decrypt pg password for credential %d: %v", c.ID, err) } pgPlaintexts = append(pgPlaintexts, pgPlain{credID: c.ID, password: pt}) } // ── 4. Prompt for new passphrase (confirmed) ────────────────────────── fmt.Fprintln(os.Stderr, "Enter new master passphrase (will not echo):") newPassphrase, err := readPassword("New passphrase: ") if err != nil { fatalf("read passphrase: %v", err) } if newPassphrase == "" { fatalf("passphrase must not be empty") } confirm, err := readPassword("Confirm passphrase: ") if err != nil { fatalf("read passphrase confirmation: %v", err) } if newPassphrase != confirm { fatalf("passphrases do not match") } // ── 5. Derive new master key ────────────────────────────────────────── // Security: a fresh random salt is generated for every rekey so that the // new key is independent of the old key even if the same passphrase is // reused. The new salt is stored atomically with the re-encrypted secrets. newSalt, err := crypto.NewSalt() if err != nil { fatalf("generate new salt: %v", err) } newKey, err := crypto.DeriveKey(newPassphrase, newSalt) if err != nil { fatalf("derive new master key: %v", err) } // Zero both keys when done, regardless of outcome. defer func() { for i := range newKey { newKey[i] = 0 } }() // ── 6. Re-encrypt signing key ───────────────────────────────────────── newSigKeyEnc, newSigKeyNonce, err := crypto.SealAESGCM(newKey, sigKeyPEM) if err != nil { fatalf("re-encrypt signing key: %v", err) } // ── 7. Re-encrypt TOTP secrets ──────────────────────────────────────── totpRows := make([]db.TOTPRekeyRow, 0, len(totpPlaintexts)) for _, tp := range totpPlaintexts { enc, nonce, err := crypto.SealAESGCM(newKey, tp.secret) if err != nil { fatalf("re-encrypt TOTP secret for account %d: %v", tp.accountID, err) } totpRows = append(totpRows, db.TOTPRekeyRow{ AccountID: tp.accountID, Enc: enc, Nonce: nonce, }) } // ── 8. Re-encrypt pg_credentials passwords ──────────────────────────── pgRows := make([]db.PGRekeyRow, 0, len(pgPlaintexts)) for _, pp := range pgPlaintexts { enc, nonce, err := crypto.SealAESGCM(newKey, pp.password) if err != nil { fatalf("re-encrypt pg password for credential %d: %v", pp.credID, err) } pgRows = append(pgRows, db.PGRekeyRow{ CredentialID: pp.credID, Enc: enc, Nonce: nonce, }) } // ── 9. Atomic commit ────────────────────────────────────────────────── if err := t.db.Rekey(newSalt, newSigKeyEnc, newSigKeyNonce, totpRows, pgRows); err != nil { fatalf("rekey database: %v", err) } if err := t.db.WriteAuditEvent("master_key_rekeyed", nil, nil, "", `{"actor":"mciasdb"}`); err != nil { fmt.Fprintf(os.Stderr, "warning: write audit event: %v\n", err) } fmt.Printf("Rekey complete: %d TOTP secrets and %d pg credentials re-encrypted.\n", len(totpRows), len(pgRows)) fmt.Fprintln(os.Stderr, "Update your mcias.toml or passphrase environment variable to use the new passphrase.") }