Skip to content

Commit

Permalink
Encrypt API tokens
Browse files Browse the repository at this point in the history
  • Loading branch information
adriankumpf committed Feb 18, 2022
1 parent 41b9a1c commit 3a643a6
Show file tree
Hide file tree
Showing 37 changed files with 828 additions and 67 deletions.
6 changes: 6 additions & 0 deletions assets/css/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,12 @@ nav.navbar {
}
}

.notification {
.mdi {
font-size: 1em;
}
}

.mdi {
font-size: 1.25em;
}
Expand Down
6 changes: 6 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ config :teslamate, TeslaMateWeb.Endpoint,
pubsub_server: TeslaMate.PubSub,
live_view: [signing_salt: "6nSVV0NtBtBfA9Mjh+7XaZANjp9T73XH"]

config :teslamate,
cloak_repo: TeslaMate.Repo,
cloak_schemas: [
TeslaMate.Auth.Tokens
]

config :logger, :console,
format: "$time $metadata[$level] $message\n",
metadata: [:car_id]
Expand Down
2 changes: 2 additions & 0 deletions config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,5 @@ if config_env() != :test do
end

config :teslamate, :srtm_cache, System.get_env("SRTM_CACHE", ".srtm_cache")

config :teslamate, TeslaMate.Vault, key: Util.get_env("ENCRYPTION_KEY", test: "secret")
4 changes: 4 additions & 0 deletions lib/teslamate/api.ex
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ defmodule TeslaMate.Api do

state

%Tokens{access: :error, refresh: :error} ->
Logger.warning("Could not decrypt API tokens!")
state

_ ->
state
end
Expand Down
2 changes: 2 additions & 0 deletions lib/teslamate/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ defmodule TeslaMate.Application do
nil ->
[
TeslaMate.Repo,
TeslaMate.Vault,
TeslaMate.HTTP,
TeslaMate.Api,
TeslaMate.Updater,
Expand All @@ -36,6 +37,7 @@ defmodule TeslaMate.Application do
import_directory ->
[
TeslaMate.Repo,
TeslaMate.Vault,
TeslaMate.HTTP,
TeslaMate.Api,
TeslaMate.Updater,
Expand Down
14 changes: 13 additions & 1 deletion lib/teslamate/auth.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ defmodule TeslaMate.Auth do
"""

import Ecto.Query, warn: false
require Logger

alias TeslaMate.Repo

### Tokens
Expand All @@ -14,9 +16,19 @@ defmodule TeslaMate.Auth do
%Tokens{} |> Tokens.changeset(attrs)
end

def can_decrypt_tokens? do
case get_tokens() do
%Tokens{} = tokens ->
is_binary(tokens.access) and is_binary(tokens.refresh)

nil ->
true
end
end

def get_tokens do
case Repo.all(Tokens) do
[tokens] ->
[%Tokens{} = tokens] ->
tokens

[_ | _] = tokens ->
Expand Down
7 changes: 5 additions & 2 deletions lib/teslamate/auth/tokens.ex
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
defmodule TeslaMate.Auth.Tokens do
use Ecto.Schema

import Ecto.Changeset

alias TeslaMate.Vault.Encrypted

schema "tokens" do
field :refresh, :string
field :access, :string
field :refresh, Encrypted.Binary, redact: true
field :access, Encrypted.Binary, redact: true

timestamps()
end
Expand Down
140 changes: 140 additions & 0 deletions lib/teslamate/vault.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
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 securly 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
17 changes: 17 additions & 0 deletions lib/teslamate_web/templates/layout/root.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,24 @@
<% show_doante_button = @conn.cookies["donate"] in Enum.map(0..1, &to_string/1) %>
<% update = TeslaMate.Updater.get_update() %>


<main role="main" style="overflow: hidden;" class={["section", (if show_doante_button or update, do: " full-height")]}>
<%= if not TeslaMate.Vault.encryption_key_provided?() or
(not TeslaMate.Api.signed_in?() and not TeslaMate.Auth.can_decrypt_tokens?()) do %>
<% docs_link = link "docs.teslamate.org", to: "https://docs.teslamate.org",
target: "_blank",
rel: "noopener noreferrer" %>

<div class="notification is-warning">
<p class="title is-5 is-spaced">
<span class="icon"><i class="mdi mdi-shield-alert"></i></span>&nbsp;<%= gettext "No encryption key provided" %>
</p>
<p class="subtitle is-6"><%= raw(gettext "To ensure that your <strong>Tesla API tokens are stored securely</strong>, an encryption key must be provided to TeslaMate via the <code>ENCRYPTION_KEY</code> environment variable. Otherwise, a <strong>login will be required after every restart</strong>.") %></p>
<p class="subtitle is-6"><%= raw(gettext "The automatically generated encryption key used for the current session can be found <strong>in the application logs</strong>.") %></p>
<p class="subtitle is-6"><%= raw(gettext "For more information, see the updated installation guides on %{link}", link: safe_to_string(docs_link)) %></p>
</div>
<% end %>

<div class="container">
<%= Enum.map(get_flash(@conn), fn {flash_key, flash_message} -> %>
<p class={"notification is-#{flash_key}"}><%= flash_message %></p>
Expand Down
3 changes: 2 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ defmodule TeslaMate.MixProject do
{:timex, "~> 3.0"},
{:tortoise, "~> 0.10"},
{:tzdata, "~> 1.1"},
{:websockex, "~> 0.4"}
{:websockex, "~> 0.4"},
{:cloak_ecto, "~> 1.2"}
]
end

Expand Down
2 changes: 2 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
"castore": {:hex, :castore, "0.1.15", "dbb300827d5a3ec48f396ca0b77ad47058578927e9ebe792abd99fcbc3324326", [:mix], [], "hexpm", "c69379b907673c7e6eb229f09a0a09b60bb27cfb9625bcb82ea4c04ba82a8442"},
"certifi": {:hex, :certifi, "2.8.0", "d4fb0a6bb20b7c9c3643e22507e42f356ac090a1dcea9ab99e27e0376d695eba", [:rebar3], [], "hexpm", "6ac7efc1c6f8600b08d625292d4bbf584e14847ce1b6b5c44d983d273e1097ea"},
"cldr_utils": {:hex, :cldr_utils, "2.17.0", "05453797e5b89f936c54c5602ac881e46b1ba4423a803c27a414466f4b598c94", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "6077ddaaa155f27755638225617bdc00c004f39b3c9355b688e52a3fc98d57e8"},
"cloak": {:hex, :cloak, "1.1.1", "6f8f6674cacd3c504daf2aaeba8f9cde3ae8009ce01ff854dd3e92fbb7954c69", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "d440c4ea3a5a31baeaea4592b534dfdccc4ded0ee098b92955a5658cbe7be625"},
"cloak_ecto": {:hex, :cloak_ecto, "1.2.0", "e86a3df3bf0dc8980f70406bcb0af2858bac247d55494d40bc58a152590bd402", [:mix], [{:cloak, "~> 1.1.1", [hex: :cloak, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "8bcc677185c813fe64b786618bd6689b1707b35cd95acaae0834557b15a0c62f"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
"connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
"cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"},
Expand Down
24 changes: 22 additions & 2 deletions priv/gettext/da/LC_MESSAGES/default.po
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,7 @@ msgstr[0] "Fandt %{count} fil"
msgstr[1] "Fandt %{count} filer"

#, elixir-format
#: lib/teslamate_web/templates/layout/root.html.heex:87
#: lib/teslamate_web/templates/layout/root.html.heex:104
msgid "Donate"
msgstr "Donér"

Expand Down Expand Up @@ -551,7 +551,7 @@ msgstr "GitHub"

#, elixir-format
#: lib/teslamate_web/live/settings_live/index.html.heex:274
#: lib/teslamate_web/templates/layout/root.html.heex:95
#: lib/teslamate_web/templates/layout/root.html.heex:112
msgid "Update available"
msgstr "Opdatering tilgængelig"

Expand Down Expand Up @@ -615,3 +615,23 @@ msgstr ""
#: lib/teslamate_web/live/car_live/summary.ex:140
msgid "Downloading update"
msgstr ""

#, elixir-format, ex-autogen
#: lib/teslamate_web/templates/layout/root.html.heex:80
msgid "No encryption key provided"
msgstr ""

#, elixir-format, ex-autogen, fuzzy
#: lib/teslamate_web/templates/layout/root.html.heex:84
msgid "For more information, see the updated installation guides on %{link}"
msgstr ""

#, elixir-format, ex-autogen
#: lib/teslamate_web/templates/layout/root.html.heex:83
msgid "The automatically generated encryption key used for the current session can be found <strong>in the application logs</strong>."
msgstr ""

#, elixir-format, ex-autogen, fuzzy
#: lib/teslamate_web/templates/layout/root.html.heex:82
msgid "To ensure that your <strong>Tesla API tokens are stored securely</strong>, an encryption key must be provided to TeslaMate via the <code>ENCRYPTION_KEY</code> environment variable. Otherwise, a <strong>login will be required after every restart</strong>."
msgstr ""
24 changes: 22 additions & 2 deletions priv/gettext/de/LC_MESSAGES/default.po
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,7 @@ msgstr[0] "%{count} Datei gefunden"
msgstr[1] "%{count} Dateien gefunden"

#, elixir-format
#: lib/teslamate_web/templates/layout/root.html.heex:87
#: lib/teslamate_web/templates/layout/root.html.heex:104
msgid "Donate"
msgstr "Spenden"

Expand Down Expand Up @@ -551,7 +551,7 @@ msgstr ""

#, elixir-format
#: lib/teslamate_web/live/settings_live/index.html.heex:274
#: lib/teslamate_web/templates/layout/root.html.heex:95
#: lib/teslamate_web/templates/layout/root.html.heex:112
msgid "Update available"
msgstr "Update verfügbar"

Expand Down Expand Up @@ -615,3 +615,23 @@ msgstr "Ihr Tesla-Konto ist aufgrund von zu vielen fehlgeschlagenen Anmeldeversu
#: lib/teslamate_web/live/car_live/summary.ex:140
msgid "Downloading update"
msgstr "Update wird heruntergeladen"

#, elixir-format, ex-autogen
#: lib/teslamate_web/templates/layout/root.html.heex:80
msgid "No encryption key provided"
msgstr "Kein Verschlüsselungscode angegeben"

#, elixir-format, ex-autogen, fuzzy
#: lib/teslamate_web/templates/layout/root.html.heex:84
msgid "For more information, see the updated installation guides on %{link}"
msgstr "Weitere Informationen findest du in den aktualisierten Installationsanleitungen auf %{link}"

#, elixir-format, ex-autogen
#: lib/teslamate_web/templates/layout/root.html.heex:83
msgid "The automatically generated encryption key used for the current session can be found <strong>in the application logs</strong>."
msgstr "Der automatisch generierte Verschlüsselungscode, der für die aktuelle Sitzung verwendet wird, ist <strong>in den Anwendungslogs einsehbar</strong>."

#, elixir-format, ex-autogen, fuzzy
#: lib/teslamate_web/templates/layout/root.html.heex:82
msgid "To ensure that your <strong>Tesla API tokens are stored securely</strong>, an encryption key must be provided to TeslaMate via the <code>ENCRYPTION_KEY</code> environment variable. Otherwise, a <strong>login will be required after every restart</strong>."
msgstr "Um sicherzustellen, dass deine <strong>Tesla-API-Tokens sicher gespeichert</strong> werden, muss TeslaMate ein Verschlüsselungscode mittels der Umgebungsvariable <code>ENCRYPTION_KEY</code> übergeben werden."
Loading

0 comments on commit 3a643a6

Please sign in to comment.