This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/crypto/rekey.go
Jeffrey Smith 680ec3b897
All checks were successful
CI/CD / detect-changes (push) Successful in 3s
CI/CD / test-frontend (push) Successful in 5s
CI/CD / test-go-pg (push) Successful in 2m34s
CI/CD / test-sqlite (push) Successful in 2m46s
CI/CD / build-and-deploy (push) Successful in 1m55s
Feat rebrand armature (#43)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
2026-03-31 23:25:37 +00:00

116 lines
3.0 KiB
Go

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:
//
// armature 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
}