From 95f71519d30c06ce3a2ec39660a6c8a62e8cb91a Mon Sep 17 00:00:00 2001 From: Claudio Ortolina Date: Tue, 6 May 2025 15:18:14 +0100 Subject: [PATCH] Store Last.fm API key with encrypted secret --- config/runtime.exs | 15 +++++++++++++++ lib/music_library/application.ex | 1 + lib/music_library/encrypted/binary.ex | 3 +++ lib/music_library/secrets.ex | 14 ++++++++++++++ lib/music_library/secrets/secret.ex | 17 +++++++++++++++++ lib/music_library/vault.ex | 3 +++ .../controllers/last_fm_controller.ex | 18 +++++++++--------- mix.exs | 1 + mix.lock | 2 ++ priv/gettext/default.pot | 5 +++++ priv/gettext/en/LC_MESSAGES/default.po | 5 +++++ .../20250506085759_create_secrets.exs | 12 ++++++++++++ 12 files changed, 87 insertions(+), 9 deletions(-) create mode 100644 lib/music_library/encrypted/binary.ex create mode 100644 lib/music_library/secrets.ex create mode 100644 lib/music_library/secrets/secret.ex create mode 100644 lib/music_library/vault.ex create mode 100644 priv/repo/migrations/20250506085759_create_secrets.exs diff --git a/config/runtime.exs b/config/runtime.exs index c3a193e3..7f39d9d9 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -49,6 +49,21 @@ config :music_library, MusicLibrary.Repo, MusicLibrary.Repo.extension_path("unicode") ] +cloak_encryption_key = + System.get_env("CLOAK_ENCRYPTION_KEY") || + raise """ + environment variable CLOAK_ENCRYPTION_KEY is missing. + Generate one with: iex> 32 |> :crypto.strong_rand_bytes() |> Base.encode64() + """ + +config :music_library, MusicLibrary.Vault, + json_library: JSON, + ciphers: [ + default: + {Cloak.Ciphers.AES.GCM, + tag: "AES.GCM.V1", key: Base.decode64!(cloak_encryption_key), iv_length: 12} + ] + if config_env() == :prod do database_path = System.get_env("DATABASE_PATH") || diff --git a/lib/music_library/application.ex b/lib/music_library/application.ex index 0437af83..bcb274bc 100644 --- a/lib/music_library/application.ex +++ b/lib/music_library/application.ex @@ -9,6 +9,7 @@ defmodule MusicLibrary.Application do def start(_type, _args) do children = [ MusicLibraryWeb.Telemetry, + MusicLibrary.Vault, MusicLibrary.Repo, MusicLibrary.ErrorRepo, MusicLibrary.BackgroundRepo, diff --git a/lib/music_library/encrypted/binary.ex b/lib/music_library/encrypted/binary.ex new file mode 100644 index 00000000..8ca1c59f --- /dev/null +++ b/lib/music_library/encrypted/binary.ex @@ -0,0 +1,3 @@ +defmodule MusicLibrary.Encrypted.Binary do + use Cloak.Ecto.Binary, vault: MusicLibrary.Vault +end diff --git a/lib/music_library/secrets.ex b/lib/music_library/secrets.ex new file mode 100644 index 00000000..5aa46246 --- /dev/null +++ b/lib/music_library/secrets.ex @@ -0,0 +1,14 @@ +defmodule MusicLibrary.Secrets do + alias MusicLibrary.Repo + alias MusicLibrary.Secrets.Secret + + def store(name, value) do + %Secret{} + |> Secret.changeset(%{name: name, value: value}) + |> Repo.insert(on_conflict: :replace_all) + end + + def get!(name) do + Repo.get!(Secret, name) + end +end diff --git a/lib/music_library/secrets/secret.ex b/lib/music_library/secrets/secret.ex new file mode 100644 index 00000000..2b48b343 --- /dev/null +++ b/lib/music_library/secrets/secret.ex @@ -0,0 +1,17 @@ +defmodule MusicLibrary.Secrets.Secret do + use Ecto.Schema + import Ecto.Changeset + + @primary_key {:name, :string, autogenerate: false} + schema "secrets" do + field :value, MusicLibrary.Encrypted.Binary + + timestamps(type: :utc_datetime) + end + + def changeset(secret, attrs) do + secret + |> cast(attrs, [:name, :value]) + |> validate_required([:name, :value]) + end +end diff --git a/lib/music_library/vault.ex b/lib/music_library/vault.ex new file mode 100644 index 00000000..82b12104 --- /dev/null +++ b/lib/music_library/vault.ex @@ -0,0 +1,3 @@ +defmodule MusicLibrary.Vault do + use Cloak.Vault, otp_app: :music_library +end diff --git a/lib/music_library_web/controllers/last_fm_controller.ex b/lib/music_library_web/controllers/last_fm_controller.ex index 35eeaef6..7638950e 100644 --- a/lib/music_library_web/controllers/last_fm_controller.ex +++ b/lib/music_library_web/controllers/last_fm_controller.ex @@ -1,18 +1,18 @@ defmodule MusicLibraryWeb.LastFmController do use MusicLibraryWeb, :controller - def callback(conn, %{"token" => token}) do - case LastFm.get_session(token) do - {:ok, session} -> - conn - |> put_session(:last_fm_user, session.name) - |> put_session(:last_fm_key, session.key) - |> put_flash(:info, "Successfully authenticated with Last.fm.") - |> redirect(to: ~p"/") + alias MusicLibrary.Secrets + def callback(conn, %{"token" => token}) do + with {:ok, session} <- LastFm.get_session(token), + {:ok, _secret} <- Secrets.store("last_fm_key", session.key) do + conn + |> put_flash(:info, gettext("Successfully connected your Last.fm account")) + |> redirect(to: ~p"/") + else {:error, reason} -> conn - |> put_flash(:error, "Failed to authenticate with Last.fm: #{reason}") + |> put_flash(:error, "Failed to connect your Last.fm account: #{reason}") |> redirect(to: ~p"/") end end diff --git a/mix.exs b/mix.exs index 99898cee..3ae35c16 100644 --- a/mix.exs +++ b/mix.exs @@ -50,6 +50,7 @@ defmodule MusicLibrary.MixProject do {:ecto_sql, "~> 3.10"}, {:ecto_sqlite3, ">= 0.0.0"}, {:ecto_sqlite3_extras, "~> 1.2.2"}, + {:cloak_ecto, "~> 1.3"}, # UI {:phoenix_html, "~> 4.2"}, diff --git a/mix.lock b/mix.lock index b1556ca9..7434276f 100644 --- a/mix.lock +++ b/mix.lock @@ -4,6 +4,8 @@ "castore": {:hex, :castore, "1.0.12", "053f0e32700cbec356280c0e835df425a3be4bc1e0627b714330ad9d0f05497f", [:mix], [], "hexpm", "3dca286b2186055ba0c9449b4e95b97bf1b57b47c1f2644555879e659960c224"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"}, "circular_buffer": {:hex, :circular_buffer, "0.4.1", "477f370fd8cfe1787b0a1bade6208bbd274b34f1610e41f1180ba756a7679839", [:mix], [], "hexpm", "633ef2e059dde0d7b89bbab13b1da9d04c6685e80e68fbdf41282d4fae746b72"}, + "cloak": {:hex, :cloak, "1.1.4", "aba387b22ea4d80d92d38ab1890cc528b06e0e7ef2a4581d71c3fdad59e997e7", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "92b20527b9aba3d939fab0dd32ce592ff86361547cfdc87d74edce6f980eb3d7"}, + "cloak_ecto": {:hex, :cloak_ecto, "1.3.0", "0de127c857d7452ba3c3367f53fb814b0410ff9c680a8d20fbe8b9a3c57a1118", [:mix], [{:cloak, "~> 1.1.1", [hex: :cloak, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "314beb0c123b8a800418ca1d51065b27ba3b15f085977e65c0f7b2adab2de1cc"}, "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index ea7286a7..7900d46e 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -779,3 +779,8 @@ msgstr "" #, elixir-autogen, elixir-format msgid "Disc %{no}" msgstr "" + +#: lib/music_library_web/controllers/last_fm_controller.ex +#, elixir-autogen, elixir-format +msgid "Successfully connected your Last.fm account" +msgstr "" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index 4f181082..d4566ecd 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -779,3 +779,8 @@ msgstr "" #, elixir-autogen, elixir-format msgid "Disc %{no}" msgstr "" + +#: lib/music_library_web/controllers/last_fm_controller.ex +#, elixir-autogen, elixir-format +msgid "Successfully connected your Last.fm account" +msgstr "" diff --git a/priv/repo/migrations/20250506085759_create_secrets.exs b/priv/repo/migrations/20250506085759_create_secrets.exs new file mode 100644 index 00000000..ca64ffb8 --- /dev/null +++ b/priv/repo/migrations/20250506085759_create_secrets.exs @@ -0,0 +1,12 @@ +defmodule MusicLibrary.Repo.Migrations.CreateSecrets do + use Ecto.Migration + + def change do + create table(:secrets, primary_key: false) do + add :name, :string, primary_key: true + add :value, :binary, null: false + + timestamps() + end + end +end