Skip to content

Commit

Permalink
Use Key module for key-related functions
Browse files Browse the repository at this point in the history
  • Loading branch information
lancejjohnson committed Mar 8, 2021
1 parent 3e919f9 commit 6aa6bb7
Show file tree
Hide file tree
Showing 7 changed files with 193 additions and 51 deletions.
2 changes: 1 addition & 1 deletion lib/mix/tasks/redbird/delete_all_sessions.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ defmodule Mix.Tasks.Redbird.DeleteAllSessions do
def run(_args) do
Application.ensure_started(:redbird)

Plug.Session.REDIS.namespace()
Redbird.Key.namespace()
|> delete_all_sessions
end

Expand Down
41 changes: 41 additions & 0 deletions lib/redbird/key.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
defmodule Redbird.Key do
alias Redbird.Crypto

def generate(namespace \\ namespace()) do
namespace <> (96 |> :crypto.strong_rand_bytes() |> Base.encode64())
end

def sign_key(key, conn) do
{:ok, key, namespace} = extract_key(key)
to_string(namespace) <> Crypto.sign_key(key, conn)
end

def verify(key, conn) do
Crypto.verify_key(key, conn)
end

def deletable?(key, conn) do
with {:ok, key, _} <- extract_key(key),
{:ok, _verified_key} <- Crypto.verify_key(key, conn) do
true
else
_ -> false
end
end

def extract_key(candidate) when is_binary(candidate) do
case String.split(candidate, namespace(), parts: 2) do
[key] -> {:ok, key, nil}
[_, key] -> {:ok, key, namespace()}
end
end

def extract_key(nil) do
{:error, :unusable_key, nil}
end

@default_namespace "redbird_session_"
def namespace do
Application.get_env(:redbird, :key_namespace, @default_namespace)
end
end
54 changes: 12 additions & 42 deletions lib/redbird/plug/session/redis.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
defmodule Plug.Session.REDIS do
import Redbird.Redis
alias Redbird.Crypto
alias Redbird.Key

@moduledoc """
Stores the session in a redis store.
Expand All @@ -14,39 +15,33 @@ defmodule Plug.Session.REDIS do
opts
end

def get(conn, namespaced_key, _init_options) do
# IO.inspect(namespaced_key, label: "#{__MODULE__}.get/3")
with key when is_binary(key) <- remove_namespace(namespaced_key),
{:ok, _verified_key} <- Crypto.verify_key(key, conn),
value when is_binary(value) <- get(namespaced_key) do
# TODO: I think the 2nd ticket is suggesting that this pass back the key without the namespace
{namespaced_key, Crypto.safe_binary_to_term(value)}
def get(conn, prospective_key, _init_options) do
with {:ok, key, _} <- Key.extract_key(prospective_key),
{:ok, _verified_key} <- Key.verify(key, conn),
value when is_binary(value) <- get(prospective_key) do
{prospective_key, Crypto.safe_binary_to_term(value)}
else
_ -> {nil, %{}}
end
end

# TODO: It looks like it respects the raw key if one is given
def put(conn, nil, data, init_options) do
# IO.inspect(nil, label: "#{__MODULE__} put with nil")
put(conn, prepare_key(conn), data, init_options)
put(conn, Key.generate(), data, init_options)
end

# TODO: The key ALWAYS has to be signed
def put(_conn, key, data, init_options) do
IO.inspect(key, label: "#{__MODULE__} put with key")
set_key_with_retries(key, :erlang.term_to_binary(data), session_expiration(init_options), 1)
def put(conn, key, data, init_options) do
key
|> Key.sign_key(conn)
|> set_key_with_retries(:erlang.term_to_binary(data), session_expiration(init_options), 1)
end

def delete(conn, redis_key, _init_options) do
if deletable_key?(redis_key, conn), do: del(redis_key)
if Key.deletable?(redis_key, conn), do: del(redis_key)

:ok
end

defp set_key_with_retries(key, value, seconds, counter) do
# IO.inspect({key, Crypto.safe_binary_to_term(value)}, label: "#{__MODULE__}.set_key_with_retries/4")

case setex(%{key: key, value: value, seconds: seconds}) do
:ok ->
key
Expand All @@ -60,31 +55,6 @@ defmodule Plug.Session.REDIS do
end
end

defp remove_namespace(key) do
key |> String.split(namespace(), parts: 2) |> tl() |> hd()
end

@default_namespace "redbird_session_"
def namespace do
Application.get_env(:redbird, :key_namespace, @default_namespace)
end

defp prepare_key(conn) do
namespace() <> Crypto.sign_key(generate_random_key(), conn)
end

defp deletable_key?(key, conn) do
key
|> remove_namespace()
|> Crypto.verify_key(conn)
|> elem(0)
|> Kernel.==(:ok)
end

defp generate_random_key do
:crypto.strong_rand_bytes(96) |> Base.encode64()
end

defp session_expiration(opts) do
case opts[:expiration_in_seconds] do
seconds when is_integer(seconds) -> seconds
Expand Down
2 changes: 0 additions & 2 deletions lib/redis.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ defmodule Redbird.Redis do
end

def get(key) do
# IO.inspect(key, label: "#{__MODULE__}.get/1")
pid()
|> Redix.command!(["GET", key])
|> case do
Expand All @@ -21,7 +20,6 @@ defmodule Redbird.Redis do
end

def setex(%{key: key, value: value, seconds: seconds}) do
# IO.inspect(key, label: "#{__MODULE__}.setex/1")
pid()
|> Redix.command(["SETEX", key, seconds, value])
|> case do
Expand Down
137 changes: 137 additions & 0 deletions test/redbird/key_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
defmodule Redbird.KeyTest do
use ExUnit.Case, async: false

alias Redbird.{Crypto, Key}

describe "generate/0" do
test "generates a random key" do
expected = ~r/[A-Za-z0-9\/\+\=]{128}/

actual = Key.generate()

assert actual =~ expected
end
end

describe "generate/1" do
test "generates a random string prefixed by a namespace" do
namespace = "redbird_"
expected = ~r/#{namespace}[A-Za-z0-9\/\+\=]{128}/

actual = Key.generate(namespace)

assert actual =~ expected
end
end

describe "namespace/0" do
setup do
on_exit(fn -> Application.delete_env(:redbird, :key_namespace) end)
end

test "provides the default namespace when it is not set in the env" do
expected = "redbird_session_"

actual = Key.namespace()

assert actual == expected
end

test "provides the namespace set in the env" do
expected = "user_set_namespace_"

Application.put_env(:redbird, :key_namespace, expected)

actual = Key.namespace()

assert actual == expected
end
end

describe "extract_key/1" do
test "extracts the key from the namespace when is contains the namespace" do
expected_namespace = Key.namespace()
expected_key = "abcdef1234"

actual = Key.extract_key(expected_namespace <> expected_key)

assert {:ok, ^expected_key, ^expected_namespace} = actual
end

test "extracts the key when it does not contain the namespace" do
expected_key = "abcdef1234"

actual = Key.extract_key(expected_key)

assert {:ok, ^expected_key, nil} = actual
end

test "provides an error for nil keys" do
actual = Key.extract_key(nil)

assert {:error, :unusable_key, nil} = actual
end
end

describe "sign_key/2" do
test "signs the key without signing the namespace" do
conn = Redbird.ConnCase.signed_conn()
generated_key = Key.generate(Key.namespace())
{:ok, original_key, expected_namespace} = Key.extract_key(generated_key)

actual = Key.sign_key(generated_key, conn)
{:ok, actual_key, actual_namespace} = Key.extract_key(actual)

assert expected_namespace == actual_namespace
assert original_key != actual_key
assert {:ok, _} = Crypto.verify_key(actual_key, conn)
end

test "signs the key when it does not include a namespace" do
conn = Redbird.ConnCase.signed_conn()
generated_key = Key.generate("")
{:ok, original_key, expected_namespace} = Key.extract_key(generated_key)

actual = Key.sign_key(generated_key, conn)
{:ok, actual_key, actual_namespace} = Key.extract_key(actual)

assert expected_namespace == actual_namespace
assert actual_namespace == nil
assert original_key != actual_key
assert {:ok, _} = Crypto.verify_key(actual_key, conn)
end
end

describe "deletable?/2" do
test "verifiable keys with a namespace are deletable" do
conn = Redbird.ConnCase.signed_conn()
generated_key = Key.generate(Key.namespace())
signed_key = Key.sign_key(generated_key, conn)

actual = Key.deletable?(signed_key, conn)

assert actual == true
end

test "verifiable keys without a namespace are deletable" do
conn = Redbird.ConnCase.signed_conn()
generated_key = Key.generate("")
signed_key = Key.sign_key(generated_key, conn)

actual = Key.deletable?(signed_key, conn)

assert actual == true
end

test "non-verifiable keys are not-deletable" do
conn = Redbird.ConnCase.signed_conn()
generated_key = Key.generate("")
signed_key = Key.sign_key(generated_key, conn)
tampered_key = signed_key <> "sneaky"

actual = Key.deletable?(tampered_key, conn)

assert actual == false
end
end
end
4 changes: 1 addition & 3 deletions test/redbird_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,12 @@ defmodule RedbirdTest do

setup do
on_exit(fn ->
Redbird.Redis.keys(Plug.Session.REDIS.namespace() <> "*")
Redbird.Redis.keys(Redbird.Key.namespace() <> "*")
|> Redbird.Redis.del()
end)
end

describe "get" do
# TODO: This is failing because the secret key on the conn is not the same
# between the put and the get
test "when there is value stored it is retrieved" do
secret = generate_secret()

Expand Down
4 changes: 1 addition & 3 deletions test/support/conn_case.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@ defmodule Redbird.ConnCase do
end

def sign_conn(conn, options \\ []) do
put_in(conn.secret_key_base, generate_secret())
|> Plug.Session.call(sign_plug(options))
|> Plug.Conn.fetch_session()
sign_conn_with(conn, generate_secret(), options)
end

def sign_conn_with(conn, secret, opts \\ []) do
Expand Down

0 comments on commit 6aa6bb7

Please sign in to comment.