package crypto import ( "database/sql" "fmt" "log" ) // Rekey re-encrypts all global/team API keys from oldKey to newKey. // Personal BYOK keys are unaffected (they're keyed to the user's UEK, // not the environment-derived key). // // Runs in a single transaction — all-or-nothing. On failure, no keys // are modified. // // Usage: // // switchboard vault rekey // ENCRYPTION_KEY = current/old key // NEW_ENCRYPTION_KEY = new key to re-encrypt with func Rekey(db *sql.DB, oldEnvKey, newEnvKey string) error { if oldEnvKey == "" { return fmt.Errorf("ENCRYPTION_KEY (current key) is required") } if newEnvKey == "" { return fmt.Errorf("NEW_ENCRYPTION_KEY is required") } if oldEnvKey == newEnvKey { return fmt.Errorf("ENCRYPTION_KEY and NEW_ENCRYPTION_KEY are identical — nothing to do") } oldKey, err := DeriveKeyFromEnv(oldEnvKey) if err != nil { return fmt.Errorf("failed to derive old key: %w", err) } newKey, err := DeriveKeyFromEnv(newEnvKey) if err != nil { return fmt.Errorf("failed to derive new key: %w", err) } tx, err := db.Begin() if err != nil { return fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback() // no-op after commit // Find all global/team keys that are encrypted rows, err := tx.Query(` SELECT id, api_key_enc, key_nonce FROM provider_configs WHERE key_scope IN ('global', 'team') AND api_key_enc IS NOT NULL `) if err != nil { return fmt.Errorf("failed to query encrypted keys: %w", err) } type rekeyRow struct { id string ciphertext []byte nonce []byte } var toRekey []rekeyRow for rows.Next() { var r rekeyRow if err := rows.Scan(&r.id, &r.ciphertext, &r.nonce); err != nil { rows.Close() return fmt.Errorf("failed to scan row: %w", err) } toRekey = append(toRekey, r) } rows.Close() if err := rows.Err(); err != nil { return fmt.Errorf("row iteration error: %w", err) } if len(toRekey) == 0 { log.Println("No global/team encrypted keys found — nothing to rekey.") return nil } log.Printf("Rekeying %d provider config(s)...", len(toRekey)) // Decrypt with old key, re-encrypt with new key, update in place for _, r := range toRekey { plaintext, err := Decrypt(r.ciphertext, r.nonce, oldKey) if err != nil { return fmt.Errorf("failed to decrypt config %s with old key: %w (is ENCRYPTION_KEY correct?)", r.id, err) } newCiphertext, newNonce, err := Encrypt(plaintext, newKey) if err != nil { return fmt.Errorf("failed to re-encrypt config %s: %w", r.id, err) } _, err = tx.Exec(` UPDATE provider_configs SET api_key_enc = $1, key_nonce = $2 WHERE id = $3 `, newCiphertext, newNonce, r.id) if err != nil { return fmt.Errorf("failed to update config %s: %w", r.id, err) } } if err := tx.Commit(); err != nil { return fmt.Errorf("failed to commit rekey transaction: %w", err) } log.Printf("✅ Successfully rekeyed %d provider config(s).", len(toRekey)) log.Println("Update ENCRYPTION_KEY to the new value and restart the server.") return nil }