defmodule ExSecrets.Cache do
@moduledoc """
Cache module provides helper functions for caching secrets.
"""
use GenServer
@store_name :ex_secrets_cache_store
def start_link(default) when is_list(default) do
GenServer.start_link(__MODULE__, [], name: @store_name)
end
@impl true
def init(_) do
table = :ets.new(:ex_secrets_ets_table, [:set, read_concurrency: true])
{:ok, %{table: table, master_key: :crypto.strong_rand_bytes(16)}}
end
@impl true
def handle_call({:get, key}, _from, %{table: table, master_key: master_key} = state) do
case :ets.lookup(table, key) do
[{_, value}] -> {:reply, decrypt(value, master_key), state}
_ -> {:reply, nil, state}
end
end
@impl true
def handle_call({:save, key, value}, _from, %{table: table, master_key: master_key} = state) do
encrypted_value = encrypt(value, master_key)
true = :ets.insert(table, {key, encrypted_value})
{:reply, value, state}
end
@impl true
def handle_call(:clear, _from, %{table: table} = state) do
n = :ets.select_delete(table, [{{:_, :_}, [], [true]}])
{:reply, n, state}
end
def get(key) do
case GenServer.whereis(@store_name) do
nil -> nil
_ -> GenServer.call(@store_name, {:get, key})
end
end
@spec save(any, any) :: any
def save(key, value) do
case GenServer.whereis(@store_name) do
nil ->
value
_ ->
GenServer.call(@store_name, {:save, key, value})
end
end
def pass_by(_, nil), do: nil
def pass_by(nil, value), do: value
def pass_by(key, value) do
case get(key) do
nil -> save(key, value)
value -> value
end
end
defp encrypt(plaintext, key) do
iv = :crypto.strong_rand_bytes(16)
ciphertext = :crypto.crypto_one_time(:aes_128_ctr, key, iv, plaintext, true)
iv <> ciphertext
end
defp decrypt(ciphertext, key) do
<<iv::binary-16, ciphertext::binary>> = ciphertext
:crypto.crypto_one_time(:aes_128_ctr, key, iv, ciphertext, false)
end
end