Packages

phoenix_kit

1.7.21
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
phoenix_kit lib modules storage services url_signer.ex
Raw

lib/modules/storage/services/url_signer.ex

defmodule PhoenixKit.Modules.Storage.URLSigner do
# NOTE: Temporarily supporting the Publishing component system until the storage/media team ships their replacement.
import Bitwise
alias PhoenixKit.Config
alias PhoenixKit.Utils.Routes
@moduledoc """
Token-based URL signing for secure file serving.
Generates and verifies secure tokens that prevent file enumeration attacks.
Each file instance receives a unique 4-character token based on MD5 hashing.
## Token Generation
Token = first 4 chars of MD5(file_id:instance_name + secret_key_base)
This ensures:
- Prevents file enumeration (can't guess URLs)
- Each instance has unique token
- Token changes if secret changes
- Secure comparison prevents timing attacks
- No user-guessable patterns
## Examples
iex> file_id = "018e3c4a-9f6b-7890-abcd-ef1234567890"
iex> PhoenixKit.Modules.Storage.URLSigner.signed_url(file_id, "thumbnail")
"/file/018e3c4a-9f6b-7890-abcd-ef1234567890/thumbnail/a3f2"
iex> PhoenixKit.Modules.Storage.URLSigner.verify_token(file_id, "thumbnail", "a3f2")
true
iex> PhoenixKit.Modules.Storage.URLSigner.verify_token(file_id, "thumbnail", "xxxx")
false
"""
@doc """
Generate a signed URL for a file instance.
## Arguments
- `file_id` (binary) - File UUID v7
- `instance_name` (binary) - Variant name (e.g., "thumbnail", "medium", "large")
## Returns
A relative URL path with prefix: `{url_prefix}/file/{file_id}/{instance_name}/{token}`
## Examples
iex> PhoenixKit.Modules.Storage.URLSigner.signed_url("018e3c4a-9f6b-7890", "thumbnail")
"/phoenix_kit/file/018e3c4a-9f6b-7890/thumbnail/abc1" # With default prefix
"""
def signed_url(file_id, instance_name, opts \\ [])
when is_binary(file_id) and is_binary(instance_name) do
token = generate_token(file_id, instance_name)
file_path = "/file/#{file_id}/#{instance_name}/#{token}"
locale_option = Keyword.get(opts, :locale, :none)
Routes.path(file_path, locale: locale_option)
end
@doc """
Verify a token is valid for the given file and instance.
## Arguments
- `file_id` (binary) - File UUID v7
- `instance_name` (binary) - Variant name
- `token` (binary) - 4-character token from URL
## Returns
Boolean indicating if token is valid.
## Examples
iex> file_id = "018e3c4a-9f6b-7890"
iex> token = PhoenixKit.Modules.Storage.URLSigner.generate_token(file_id, "thumbnail")
iex> PhoenixKit.Modules.Storage.URLSigner.verify_token(file_id, "thumbnail", token)
true
iex> PhoenixKit.Modules.Storage.URLSigner.verify_token(file_id, "thumbnail", "xxxx")
false
"""
def verify_token(file_id, instance_name, token)
when is_binary(file_id) and is_binary(instance_name) and is_binary(token) do
expected_token = generate_token(file_id, instance_name)
# Use constant-time comparison to prevent timing attacks
secure_compare(expected_token, token)
end
@doc """
Generate the 4-character token for a file instance.
Used internally by signed_url/2 and verify_token/4.
## Arguments
- `file_id` (binary) - File UUID v7
- `instance_name` (binary) - Variant name
## Returns
A 4-character hex string token.
## Examples
iex> PhoenixKit.Modules.Storage.URLSigner.generate_token("018e3c4a", "thumbnail")
"abc1"
"""
def generate_token(file_id, instance_name)
when is_binary(file_id) and is_binary(instance_name) do
data = "#{file_id}:#{instance_name}"
# Get secret_key_base if available, otherwise just use data without secret
secret_key_base = get_secret_key_base()
hash_data =
if secret_key_base do
data <> secret_key_base
else
data
end
token =
:crypto.hash(:md5, hash_data)
|> Base.encode16(case: :lower)
|> String.slice(0..3)
token
end
defp get_secret_key_base do
# Try to get secret_key_base from configured sources in order
# 1. Explicitly configured on :phoenix_kit
# 2. From the configured endpoint
# 3. Return nil if not found (will use data without secret)
Config.get(:secret_key_base, nil) ||
get_parent_endpoint_secret()
end
defp get_parent_endpoint_secret do
case Config.get_parent_endpoint() do
{:ok, endpoint} ->
if function_exported?(endpoint, :config, 1) do
endpoint.config(:secret_key_base)
else
nil
end
_ ->
nil
end
end
defp secure_compare(string1, string2) when is_binary(string1) and is_binary(string2) do
# Use constant-time comparison to prevent timing attacks
# Padding strings to same length ensures constant time regardless of length difference
length1 = byte_size(string1)
length2 = byte_size(string2)
max_length = max(length1, length2)
# Pad both strings to max length
padded1 = String.pad_trailing(string1, max_length)
padded2 = String.pad_trailing(string2, max_length)
# XOR all bytes and accumulate result
comparison =
Enum.reduce(
0..(max_length - 1),
0,
fn i, acc ->
<<_::binary-size(i), byte1::8, _::binary>> = padded1
<<_::binary-size(i), byte2::8, _::binary>> = padded2
acc ||| Bitwise.bxor(byte1, byte2)
end
)
comparison == 0 and length1 == length2
end
end