Files
archived-teslamate/priv/repo/migrations/20220123131732_encrypt_api_tokens.exs
Adrian Kumpf 0d6e288c70 Encrypt API tokens (#2360)
Store API tokens encrypted in the database.

During the database migration a randomly generated key will be used encrypt the tokens if no ENCRYPTION_KEY environment variable was provided.

If the application is started without the presence of an ENCRYPTION_KEY (or if the key failed to decrypt the existing tokens), the UI will display a warning with further instructions.
2022-02-18 17:03:13 +01:00

124 lines
3.1 KiB
Elixir

defmodule TeslaMate.Repo.Migrations.EncryptApiTokens do
use Ecto.Migration
Code.ensure_loaded!(TeslaMate.Vault)
defmodule Encrypted.Binary do
use Cloak.Ecto.Binary, vault: TeslaMate.Vault
end
defmodule Tokens do
use Ecto.Schema
schema "tokens" do
field(:refresh, :string)
field(:access, :string)
field(:encrypted_refresh, Encrypted.Binary)
field(:encrypted_access, Encrypted.Binary)
end
end
defmodule Encryption do
def setup do
{type, key} =
case System.get_env("ENCRYPTION_KEY") do
key when is_binary(key) and byte_size(key) > 0 -> {:existing, key}
_ -> {:generated, generate_key(64)}
end
setup_vault(key)
{type, key}
end
defp generate_key(length) when length > 31 do
:crypto.strong_rand_bytes(length) |> Base.encode64(padding: false) |> binary_part(0, length)
end
defp setup_vault(key) do
Cloak.Vault.save_config(TeslaMate.Vault.Config,
ciphers: [
default: TeslaMate.Vault.default_chipher(:crypto.hash(:sha256, key))
]
)
end
end
defmodule Cache do
require Logger
def store(encryption_key) do
Enum.each([System.tmp_dir(), import_dir()], fn dir ->
with dir when is_binary(dir) <- dir,
path = Path.join(dir, "tm_encryption.key"),
:ok <- File.write(path, encryption_key) do
Logger.info("Wrote encryption key to #{path}")
end
end)
end
defp import_dir do
path =
System.get_env("IMPORT_DIR", "import")
|> Path.absname()
if File.exists?(path), do: path
end
end
alias TeslaMate.Repo
def change do
alter table(:tokens) do
add :encrypted_refresh, :binary
add :encrypted_access, :binary
end
flush()
with [_ | _] = tokens <- Repo.all(Tokens) do
with {:generated, encryption_key} <- Encryption.setup() do
require Logger
Logger.warning("""
\n------------------------------------------------------------------------------
No ENCRYPTION_KEY was found to encrypt and securly store your API tokens.
Therefore, the following randomly generated key will be used instead:
#{encryption_key}
IMPORTANT: Create an environment variable named "ENCRYPTION_KEY" with the value
set to the key above and pass it to the application from now on.
If you choose to use a different key, a new login with your API tokens will be
required once after starting the application.
------------------------------------------------------------------------------
""")
Cache.store(encryption_key)
end
Enum.each(tokens, fn %Tokens{} = tokens ->
tokens
|> Ecto.Changeset.change(%{
encrypted_access: tokens.access,
encrypted_refresh: tokens.refresh
})
|> Repo.update!()
end)
end
alter table(:tokens) do
remove :access
remove :refresh
end
rename table(:tokens), :encrypted_access, to: :access
rename table(:tokens), :encrypted_refresh, to: :refresh
end
end