Files
bDS2/lib/bds/ai/secret_migration.ex

68 lines
2.2 KiB
Elixir

defmodule BDS.AI.SecretMigration do
@moduledoc """
Idempotent boot-time re-encryption of stored AI secrets.
Earlier releases encrypted secrets with key material shipped in the
repository (or a deterministic node-name fallback). This pass finds the
`__encrypted_*` rows in `settings`, decrypts them with the legacy keys, and
re-encrypts them with the machine-local key from `BDS.AI.SecretKey`. Rows
already encrypted with the current key are left untouched; rows no known
key can decrypt are left in place and reported, so the user can re-enter
the secret. Runs from `BDS.RepoBootstrap` on every boot; on a migrated
database it is a cheap no-op.
"""
import Ecto.Query
require Logger
alias BDS.AI.SecretBackend
alias BDS.Persistence
alias BDS.Repo
alias BDS.Settings.Setting
@encrypted_prefix "__encrypted_"
@spec migrate_legacy_secrets(module()) ::
{:ok, %{migrated: non_neg_integer(), failed: non_neg_integer()}}
def migrate_legacy_secrets(repo \\ Repo) do
summary =
from(setting in Setting, where: like(setting.key, ^"#{@encrypted_prefix}%"))
|> repo.all()
|> Enum.reduce(%{migrated: 0, failed: 0}, fn setting, acc ->
case migrate_row(repo, setting) do
:current -> acc
:migrated -> %{acc | migrated: acc.migrated + 1}
:failed -> %{acc | failed: acc.failed + 1}
end
end)
{:ok, summary}
end
defp migrate_row(repo, setting) do
with {:error, _no_current_key_match} <- SecretBackend.decrypt_with_current_key(setting.value),
{:ok, plaintext} <- SecretBackend.decrypt_legacy(setting.value),
{:ok, reencrypted} <- SecretBackend.encrypt(plaintext) do
repo.update_all(
from(s in Setting, where: s.key == ^setting.key),
set: [value: reencrypted, updated_at: Persistence.now_ms()]
)
Logger.info("AI secret #{setting.key} re-encrypted with the machine-local key")
:migrated
else
{:ok, _already_current} ->
:current
{:error, reason} ->
Logger.warning(
"AI secret #{setting.key} could not be re-encrypted (#{inspect(reason)}); " <>
"leaving it unchanged — the secret may need to be entered again"
)
:failed
end
end
end