Elixir (and erlang and other beam languages) are concurrency first. Processes are very lightweight “micro-threads” each with its own state and garbage collection. They are the building blocks for most components in elixir/erlang. On top of bare processes you have the OTP libraries, which abstracts away the underlying details and lets you use best practices when writing concurrent code.
I think going through the getting started guide on the https://elixir-lang.org/getting-started should give you a good idea, especially https://elixir-lang.org/getting-started/processes.html and the Mix and OTP sections.
In terms of one-time link generation, my idea was to start one process per code, each which will timeout and die (and clean up after itself) after a predetermined period of time.
An example which uses elixir Registry (https://hexdocs.pm/elixir/Registry.html) and GenServer (https://hexdocs.pm/elixir/GenServer.html) to provide generation and verification of one time code could look like this:
defmodule OnetimeServer do
use GenServer
@default_timeout 10_000
@doc """
For testing, start this in your supervision tree
"""
def start() do
Registry.start_link(name: Registry.OnetimeServer, keys: :unique)
end
@doc """
Generate code and start a GenServer and register in the registry
"""
def make_code(timeout \\ @default_timeout) do
code = :crypto.strong_rand_bytes(32) |> Base.encode64()
GenServer.start(__MODULE__, [code, timeout], name: {:via, Registry, {Registry.OnetimeServer, code}})
code
end
@doc """
Verify the code. Looks up the code in the process register and calls it
for verifcation
"""
def verify_code(code) do
case Registry.lookup(Registry.OnetimeServer, code) do
[{pid, _}] ->
GenServer.call(pid, {:verify_code, code})
[] ->
{:error, :invalid_code}
end
end
#
# Gen Server calls backs. THe init returns the timeout
#
def init([code, timeout]), do: {:ok, code, timeout}
def handle_call({:verify_code, code}, _, code), do: {:stop, :normal, :ok, :done}
def handle_call({:verify_code, _other}, _, _code), do: {:stop, :normal, :error, :done}
def handle_info(:timeout, _code), do: {:stop, :normal, :done}
end
And then you can use this code:
ex(1)> OnetimeServer.start()
{:ok, #PID<0.199.0>}
iex(2)> c = OnetimeServer.make_code()
"W4x46tZfJenEjwqQcJDYngm+Vp80k5XP+VJoMIOrNZI="
iex(3)> OnetimeServer.verify_code(c)
:ok
iex(4)> OnetimeServer.verify_code(c)
{:error, :invalid_code}
iex(5)> c = OnetimeServer.make_code(1_000) # Shorter time out
"vXUj1AbOfWNpfEAoMIHmepXiXbN9PxZsaI7I68rnaXc="
iex(6)> OnetimeServer.verify_code(c)
{:error, :invalid_code}
There are many other ways to implement these in elixir, ets tables, mnesia, and other types of process registers, and even without process register just using process primitives.
Here is another example without the process registry which relies on sending a serialized pid to the client which is de-serialized on return. The one thing to be careful about here is accepting data from non-trusted clients. I slapped on an hmac on the process to make sure it is not tampered with but might give you an idea what is possible
defmodule OneTimeGenerator do
# Should be fetched from secure configuration
@hmackey :crypto.strong_rand_bytes(32)
def create_code() do
pid = Process.spawn(&OneTimeGenerator.code_checker/0, [])
encode_pid(pid)
end
def verify_code(code) do
case decode_pid(code) do
{:ok, pid} ->
verify_code(Process.alive?(pid), pid)
{:error, :invalid_code} ->
{:error, :invalid_code}
end
end
def verify_code(_alive = false, _), do: {:error, :invalid_code}
def verify_code(_alive = true, pid) do
Process.send(pid, {:verify, self(), pid}, [])
receive do
:ok -> :ok
{:error, _} -> {:error, :invalid_code}
after
1000 ->
{:error, :timeout}
end
end
def code_checker() do
pid = self()
receive do
{:verify, from, ^pid} ->
Process.send(from, :ok, [])
{:verify, from, invalid} ->
Process.send(from, {:error, invalid}, [])
after
10_000 ->
:die
end
end
def encode_pid(pid) do
code = pid |> :erlang.term_to_binary()
mac = :crypto.hmac(:sha256, @hmackey, code)
Base.encode64(<<code::binary,mac::binary>>)
end
def decode_pid(code) do
binary_code = Base.decode64!(code)
data_size = byte_size(binary_code) - 32
<<data::bytes-size(data_size),mac::bytes-size(32)>> = binary_code
case secure_compare(:crypto.hmac(:sha256, @hmackey, data), mac) do
true ->
{:ok, :erlang.binary_to_term(data, [:safe])}
false ->
{:error, :invalid_code}
end
end
def secure_compare(a, b) do
# This should use a secury hash compare. There is one built-in in
# erlang > 23 or Plug.Crypto has one. For demonstration purposes:
a == b # This allows timing attacks so don't do it
end
end
and to run it
iex(1)> c = OneTimeGenerator.create_code()
"g2dkAA1ub25vZGVAbm9ob3N0AAAAxwAAAAAA/yYJ7Hmp9rykBEKnl2Zj22XkZa4u6fdRZV/HXhf9ENI="
iex(2)> OneTimeGenerator.verify_code(c)
:ok
iex(3)> OneTimeGenerator.verify_code(c)
{:error, :invalid_code}
iex(4)> c = OneTimeGenerator.create_code()
"g2dkAA1ub25vZGVAbm9ob3N0AAAAzAAAAAAAJhODibe0ocK/vYVetGKfO3dEVOmMXxDX7dhbE1QHQcI="
iex(5)> Process.sleep(10_000)
:ok
iex(6)> OneTimeGenerator.verify_code(c)
{:error, :invalid_code}
iex(7)>