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/backfill.go
2026-02-24 10:44:12 +00:00

132 lines
3.7 KiB
Go

package crypto
import (
"database/sql"
"fmt"
"log"
)
// BackfillEncryptedKeys encrypts any remaining plaintext API keys in
// provider_configs.api_key_plain using the env-derived key, writing the
// result to api_key_enc (BYTEA) + key_nonce. Clears api_key_plain after.
//
// This runs once on upgrade from v0.9.3 → v0.9.4. Subsequent starts are
// a no-op (api_key_plain will be NULL for all rows).
//
// Personal-scope keys are temporarily encrypted with the env key. They get
// re-encrypted with the user's UEK on next login (see VaultUpgradeOnLogin).
func BackfillEncryptedKeys(db *sql.DB, encryptionKey string) error {
envKey, err := DeriveKeyFromEnv(encryptionKey)
if err != nil {
return fmt.Errorf("vault backfill: %w", err)
}
// Check if api_key_plain column exists (it won't on fresh installs after
// the column is eventually dropped)
var colExists bool
db.QueryRow(`
SELECT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'provider_configs' AND column_name = 'api_key_plain'
)
`).Scan(&colExists)
if !colExists {
return nil // Column already dropped — nothing to backfill
}
// Find rows with plaintext keys that haven't been encrypted yet
rows, err := db.Query(`
SELECT id, api_key_plain
FROM provider_configs
WHERE api_key_plain IS NOT NULL
AND api_key_plain != ''
AND api_key_enc IS NULL
`)
if err != nil {
return fmt.Errorf("vault backfill query: %w", err)
}
defer rows.Close()
var count int
for rows.Next() {
var id, plaintext string
if err := rows.Scan(&id, &plaintext); err != nil {
return fmt.Errorf("vault backfill scan: %w", err)
}
ciphertext, nonce, err := Encrypt(plaintext, envKey)
if err != nil {
return fmt.Errorf("vault backfill encrypt %s: %w", id, err)
}
_, err = db.Exec(`
UPDATE provider_configs
SET api_key_enc = $1, key_nonce = $2, api_key_plain = NULL
WHERE id = $3
`, ciphertext, nonce, id)
if err != nil {
return fmt.Errorf("vault backfill update %s: %w", id, err)
}
count++
}
if count > 0 {
log.Printf(" 🔐 Vault backfill: encrypted %d API key(s)", count)
}
return nil
}
// EnforceEncryptionKey checks startup preconditions:
// - If encrypted keys exist in DB and ENCRYPTION_KEY is empty → fatal.
// - If no keys exist and ENCRYPTION_KEY is empty → warn (fresh install OK).
func EnforceEncryptionKey(db *sql.DB, encryptionKey string) error {
if db == nil {
return nil // No DB, no keys to protect
}
// Check if any encrypted keys exist
var hasEncrypted bool
db.QueryRow(`
SELECT EXISTS (
SELECT 1 FROM provider_configs WHERE api_key_enc IS NOT NULL
)
`).Scan(&hasEncrypted)
// Check if any plaintext keys exist (pre-backfill)
var hasPlaintext bool
// api_key_plain column may not exist on fresh v0.9.4+ installs
var colExists bool
db.QueryRow(`
SELECT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'provider_configs' AND column_name = 'api_key_plain'
)
`).Scan(&colExists)
if colExists {
db.QueryRow(`
SELECT EXISTS (
SELECT 1 FROM provider_configs
WHERE api_key_plain IS NOT NULL AND api_key_plain != ''
)
`).Scan(&hasPlaintext)
}
if encryptionKey == "" {
if hasEncrypted {
return fmt.Errorf(
"ENCRYPTION_KEY is not set but encrypted API keys exist in the database. " +
"Set ENCRYPTION_KEY to the value used when keys were encrypted, or data will be unrecoverable",
)
}
if hasPlaintext {
return fmt.Errorf(
"ENCRYPTION_KEY is not set but plaintext API keys need encryption. " +
"Set ENCRYPTION_KEY before starting (required for v0.9.4+)",
)
}
log.Println(" ⚠ ENCRYPTION_KEY not set — API key encryption disabled (OK for fresh install with no providers)")
}
return nil
}