package controlplane import ( "context" "fmt" ) type encryptedColumn struct{ table, key, column string } // RotateCredentials re-encrypts every control-plane secret with the primary // key in the configured keyring. Run all gateway instances with both new and // previous keys before invoking this operation. func (s *Store) RotateCredentials(ctx context.Context) (int, error) { tx, err := s.db.Begin(ctx) if err != nil { return 0, err } defer tx.Rollback(ctx) columns := []encryptedColumn{ {"providers", "id", "api_key_ciphertext"}, {"console_mail_outbox", "id", "body_ciphertext"}, {"console_totp_credentials", "user_id", "secret_ciphertext"}, {"console_passkeys", "id", "credential_ciphertext"}, {"console_webauthn_challenges", "id", "session_ciphertext"}, } total := 0 for _, item := range columns { rows, queryErr := tx.Query(ctx, fmt.Sprintf(`SELECT %s::text,%s FROM %s`, item.key, item.column, item.table)) if queryErr != nil { return total, queryErr } type record struct { id string ciphertext []byte } records := []record{} for rows.Next() { var value record if err := rows.Scan(&value.id, &value.ciphertext); err != nil { rows.Close() return total, err } records = append(records, value) } if err := rows.Err(); err != nil { rows.Close() return total, err } rows.Close() for _, value := range records { plaintext, err := s.cipher.Decrypt(value.ciphertext) if err != nil { return total, fmt.Errorf("decrypt %s %s: %w", item.table, value.id, err) } ciphertext, err := s.cipher.Encrypt(plaintext) if err != nil { return total, err } if _, err := tx.Exec(ctx, fmt.Sprintf(`UPDATE %s SET %s=$2 WHERE %s=$1`, item.table, item.column, item.key), value.id, ciphertext); err != nil { return total, err } total++ } } if err := tx.Commit(ctx); err != nil { return total, err } return total, nil }