mirror of
https://github.com/netfun2000/hipudding-teslamate.git
synced 2026-02-27 09:44:28 +08:00
141 lines
4.2 KiB
Elixir
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
|