Packages
phoenix_kit
1.7.206
1.7.208
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/phoenix_kit/integrations/encryption.ex
defmodule PhoenixKit.Integrations.Encryption do
@moduledoc """
AES-256-GCM encryption for sensitive integration credentials.
Encrypts fields like `access_token`, `refresh_token`, `client_secret`,
`api_key`, `bot_token`, `secret_key`, `password` before storing in the
database. Decrypts them when reading.
Uses the application's `secret_key_base` as the root key, deriving a
dedicated integration encryption key via a SHA-256 hash.
> #### Key rotation {: .warning}
> The key is derived deterministically from `secret_key_base`. Rotating
> `secret_key_base` makes every existing `enc:v1:` value undecryptable
> (reads fall back to the stored ciphertext, effectively data loss). There
> is no re-wrap/migration path today; the `enc:v1:` prefix reserves room
> for a future versioned KDF that could re-encrypt on read.
## Configuration
Encryption is enabled by default when `secret_key_base` is configured.
To disable, set:
config :phoenix_kit, integration_encryption_enabled: false
"""
@sensitive_fields ~w(
access_token refresh_token client_secret
api_key bot_token secret_key password
)
# Prefix to identify encrypted values
@encrypted_prefix "enc:v1:"
@doc """
Returns the list of field keys that are encrypted.
"""
@spec sensitive_fields() :: [String.t()]
def sensitive_fields, do: @sensitive_fields
@doc """
Encrypt sensitive fields in an integration data map before saving.
Non-sensitive fields and nil/empty values are left unchanged.
Already-encrypted values (with `enc:v1:` prefix) are not re-encrypted.
"""
@spec encrypt_fields(map()) :: map()
def encrypt_fields(data) when is_map(data) do
case encryption_key() do
nil -> data
key -> do_encrypt_fields(data, key)
end
end
@doc """
Decrypt sensitive fields in an integration data map after reading.
Only values with the `enc:v1:` prefix are decrypted.
Non-encrypted values are returned as-is for backwards compatibility.
"""
@spec decrypt_fields(map()) :: map()
def decrypt_fields(data) when is_map(data) do
case encryption_key() do
nil -> data
key -> do_decrypt_fields(data, key)
end
end
def decrypt_fields(other), do: other
@doc """
Check if encryption is available and enabled.
"""
@spec enabled?() :: boolean()
def enabled? do
encryption_key() != nil
end
# ---------------------------------------------------------------------------
# Private
# ---------------------------------------------------------------------------
defp do_encrypt_fields(data, key) do
Enum.reduce(@sensitive_fields, data, fn field, acc ->
case Map.get(acc, field) do
nil ->
acc
"" ->
acc
value when is_binary(value) ->
if String.starts_with?(value, @encrypted_prefix) do
# Already encrypted
acc
else
Map.put(acc, field, encrypt_value(value, key))
end
_ ->
acc
end
end)
end
defp do_decrypt_fields(data, key) do
Enum.reduce(@sensitive_fields, data, fn field, acc ->
case Map.get(acc, field) do
value when is_binary(value) and value != "" ->
maybe_decrypt_field(acc, field, value, key)
_ ->
acc
end
end)
end
defp maybe_decrypt_field(acc, field, value, key) do
if String.starts_with?(value, @encrypted_prefix) do
case decrypt_value(value, key) do
{:ok, plaintext} -> Map.put(acc, field, plaintext)
{:error, _} -> acc
end
else
acc
end
end
defp encrypt_value(plaintext, key) do
iv = :crypto.strong_rand_bytes(12)
{ciphertext, tag} = :crypto.crypto_one_time_aead(:aes_256_gcm, key, iv, plaintext, "", true)
encoded = Base.encode64(iv <> tag <> ciphertext)
@encrypted_prefix <> encoded
end
defp decrypt_value(@encrypted_prefix <> encoded, key) do
with {:ok, binary} <- Base.decode64(encoded),
<<iv::binary-12, tag::binary-16, ciphertext::binary>> <- binary do
case :crypto.crypto_one_time_aead(:aes_256_gcm, key, iv, ciphertext, "", tag, false) do
plaintext when is_binary(plaintext) -> {:ok, plaintext}
:error -> {:error, :decryption_failed}
end
else
_ -> {:error, :invalid_format}
end
end
defp decrypt_value(_, _key), do: {:error, :not_encrypted}
defp encryption_key do
if Application.get_env(:phoenix_kit, :integration_encryption_enabled, true) do
case secret_key_base() do
secret when is_binary(secret) and secret != "" -> derive_key(secret)
_ -> nil
end
else
nil
end
end
# Flat `config :phoenix_kit, secret_key_base: ...` keeps precedence — it's
# what an operator who deliberately set it expects to keep working. The
# installer never stamps that key, though, so most host apps never set
# it and encryption silently stayed disabled (secrets stored in
# plaintext). Fall back to the host app's own Endpoint secret_key_base,
# which every Phoenix app has — same discovery `Config.get_parent_endpoint/0`
# already uses elsewhere (derived from `:parent_module`, which the
# installer does set).
defp secret_key_base do
case PhoenixKit.Config.get(:secret_key_base) do
{:ok, secret} when is_binary(secret) and secret != "" -> secret
_ -> endpoint_secret_key_base()
end
end
defp endpoint_secret_key_base do
case PhoenixKit.Config.get_parent_endpoint() do
{:ok, endpoint} -> endpoint.config(:secret_key_base)
:error -> nil
end
rescue
# The endpoint may be loaded but not started (early boot), or its
# config table may not exist yet — either way, no key means no
# encryption, not a crash.
_ -> nil
end
defp derive_key(secret) do
# Derive a dedicated 32-byte key for integration encryption
:crypto.hash(:sha256, "phoenix_kit_integrations:" <> secret)
end
end