defmodule SimpleshopTheme.Vault do @moduledoc """ Handles encryption and decryption of sensitive data. Uses AES-256-GCM for authenticated encryption. Keys are derived from the application's secret_key_base. """ @aad "SimpleshopTheme.Vault" @doc """ Encrypts a string value. Returns `{:ok, encrypted_binary}` or `{:error, reason}`. The encrypted binary includes the IV and auth tag. """ @spec encrypt(String.t()) :: {:ok, binary()} | {:error, term()} def encrypt(plaintext) when is_binary(plaintext) do key = derive_key() iv = :crypto.strong_rand_bytes(12) {ciphertext, tag} = :crypto.crypto_one_time_aead(:aes_256_gcm, key, iv, plaintext, @aad, true) # Format: iv (12 bytes) + tag (16 bytes) + ciphertext {:ok, iv <> tag <> ciphertext} rescue e -> {:error, e} end def encrypt(nil), do: {:ok, nil} @doc """ Decrypts an encrypted binary. Returns `{:ok, plaintext}` or `{:error, reason}`. """ @spec decrypt(binary()) :: {:ok, String.t()} | {:error, term()} def decrypt(<>) do key = derive_key() case :crypto.crypto_one_time_aead(:aes_256_gcm, key, iv, ciphertext, @aad, tag, false) do plaintext when is_binary(plaintext) -> {:ok, plaintext} :error -> {:error, :decryption_failed} end rescue e -> {:error, e} end def decrypt(nil), do: {:ok, nil} def decrypt(""), do: {:ok, ""} def decrypt(_invalid) do {:error, :invalid_ciphertext} end @doc """ Encrypts a string value, raising on error. """ @spec encrypt!(String.t()) :: binary() def encrypt!(plaintext) do case encrypt(plaintext) do {:ok, ciphertext} -> ciphertext {:error, reason} -> raise "Encryption failed: #{inspect(reason)}" end end @doc """ Decrypts an encrypted binary, raising on error. """ @spec decrypt!(binary()) :: String.t() def decrypt!(ciphertext) do case decrypt(ciphertext) do {:ok, plaintext} -> plaintext {:error, reason} -> raise "Decryption failed: #{inspect(reason)}" end end # Derives a 32-byte key from the secret_key_base defp derive_key do secret_key_base = get_secret_key_base() :crypto.hash(:sha256, secret_key_base <> "vault_encryption_key") end defp get_secret_key_base do case Application.get_env(:simpleshop_theme, SimpleshopThemeWeb.Endpoint)[:secret_key_base] do nil -> raise """ Secret key base is not configured. Set it in config/runtime.exs or config/dev.exs. """ key when is_binary(key) -> key end end end