Files
archived-hipudding-teslamate/lib/teslamate/vault.ex
Adrian Kumpf 8756c66bca Fix typos
2023-02-03 21:20:10 +01:00

141 lines
4.2 KiB
Elixir

defmodule TeslaMate.Vault do
use Cloak.Vault,
otp_app: :teslamate
defmodule Encrypted.Binary do
use Cloak.Ecto.Binary, vault: TeslaMate.Vault
end
require Logger
# With AES.GCM, 12-byte IV length is necessary for interoperability reasons.
# See https://github.com/danielberkompas/cloak/issues/93
@iv_length 12
@doc """
The default cipher used to encrypt values is AES-265 in GCM mode.
A random IV is generated for every encryption, and prepends the key tag, IV,
and ciphertag to the beginning of the ciphertext:
+----------------------------------------------------------+----------------------+
| HEADER | BODY |
+-------------------+---------------+----------------------+----------------------+
| Key Tag (n bytes) | IV (12 bytes) | Ciphertag (16 bytes) | Ciphertext (n bytes) |
+-------------------+---------------+----------------------+----------------------+
|_________________________________
|
+---------------+-----------------+-------------------+
| Type (1 byte) | Length (1 byte) | Key Tag (n bytes) |
+---------------+-----------------+-------------------+
The `Key Tag` component of the header consists of a `Type`, `Length`, and
`Value` triplet for easy decoding.
For more information see `Cloak.Ciphers.AES.GCM`.
"""
def default_chipher(key) do
{Cloak.Ciphers.AES.GCM, tag: "AES.GCM.V1", key: key, iv_length: @iv_length}
end
def encryption_key_provided? do
case get_encryption_key_from_config() do
{:ok, _key} -> true
:error -> false
end
end
@impl GenServer
def init(config) do
encryption_key =
with :error <- get_encryption_key_from_config(),
:error <- get_encryption_key_from(System.tmp_dir()),
:error <- get_encryption_key_from(import_dir()) do
key_length = 48 + :rand.uniform(16)
random_key = generate_random_key(key_length)
Logger.warning("""
\n------------------------------------------------------------------------------
No ENCRYPTION_KEY was found to encrypt and securely store your API tokens.
Therefore, the following randomly generated key will be used instead for this
session:
#{pad(random_key, 80)}
Create an environment variable named "ENCRYPTION_KEY" with the value set to
the key above (or choose your own) and pass it to the application from now on.
OTHERWISE, A LOGIN WITH YOUR API TOKENS WILL BE REQUIRED AFTER EVERY RESTART!
------------------------------------------------------------------------------
""")
random_key
else
{:ok, key} -> key
end
config =
Keyword.put(config, :ciphers,
default: default_chipher(:crypto.hash(:sha256, encryption_key))
)
{:ok, config}
end
defp pad(string, width) do
case String.length(string) do
len when len < width ->
string
|> String.pad_leading(div(width - len, 2) + len)
|> String.pad_trailing(width)
_ ->
string
end
end
defp get_encryption_key_from_config do
Application.get_env(:teslamate, TeslaMate.Vault)
|> Access.fetch!(:key)
|> case do
key when is_binary(key) and byte_size(key) > 0 -> {:ok, key}
_ -> :error
end
end
# the database migration writes the generated key into a tmp dir and a local
# 'import' dir if possible. The latter is likely a persistent volume for a
# lot of users of the Docker image.
# see priv/migrations/20220123131732_encrypt_api_tokens.exs
defp get_encryption_key_from(dir) do
with dir when is_binary(dir) <- dir,
path = Path.join(dir, "tm_encryption.key"),
{:ok, encryption_key} <- File.read(path) do
Logger.info("""
Restored encryption key from #{path}:
#{encryption_key}
""")
{:ok, encryption_key}
else
_ -> :error
end
end
defp import_dir do
path =
System.get_env("IMPORT_DIR", "import")
|> Path.absname()
if File.exists?(path), do: path
end
defp generate_random_key(length) when length > 31 do
:crypto.strong_rand_bytes(length) |> Base.encode64(padding: false) |> binary_part(0, length)
end
end