Skip to content

Commit

Permalink
Wrap up max attempts and reads feature
Browse files Browse the repository at this point in the history
  • Loading branch information
thebugcatcher committed Oct 29, 2023
1 parent a26b57f commit c832b95
Show file tree
Hide file tree
Showing 6 changed files with 272 additions and 117 deletions.
23 changes: 20 additions & 3 deletions lib/heimdall/secrets.ex
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ defmodule Heimdall.Secrets do
end

@doc """
TODO
Attempts to decrypt a secret using the given decryption_key
"""
@spec decrypt(Secret.t(), String.t()) :: {:ok, String.t()} | {:error, term()}
def decrypt(secret, decryption_key) do
Expand All @@ -54,15 +54,15 @@ defmodule Heimdall.Secrets do
end

@doc """
TODO
Returns a secret with the given ID. Returns nil if none exist.
"""
@spec get(Ecto.UUID.t()) :: Secret.t() | nil
def get(secret_id) do
Repo.get(Secret, secret_id)
end

@doc """
TODO
Checks whether secret is not expired or deleted from the database
"""
@spec not_expired?(Secret.t()) :: boolean()
def not_expired?(%Secret{id: secret_id}) do
Expand All @@ -73,6 +73,23 @@ defmodule Heimdall.Secrets do
|> Kernel.not()
end

@doc """
Checks whether secret is not stale:
not maxed out in terms of attempts or reads
"""
@spec not_stale?(Secret.t()) :: boolean()
def not_stale?(%Secret{id: secret_id}) do
secret =
Secret
|> Repo.get(secret_id)
|> Repo.preload([:attempts, :reads])

(is_nil(secret.max_decryption_attempts) or
length(secret.attempts) < secret.max_decryption_attempts) and
(is_nil(secret.max_reads) or
length(secret.reads) < secret.max_reads)
end

@doc """
Creates a Read record for a Secret
"""
Expand Down
44 changes: 34 additions & 10 deletions lib/heimdall_web/controllers/secret_html/secret_404.html.heex
Original file line number Diff line number Diff line change
@@ -1,14 +1,38 @@
<div class="mt-4 text-lg font-semibold">
Secret cannot be accessed due to one of the following reasons:
<ul>
<li>It doesn't exist</li>
<li>It has expired</li>
<li>It has reached max reads or decryption attempts</li>
<li>Your IP doesn't match the IP addresses allowed</li>
</ul>
</div>
<.modal id="secret_404" show={true}>
<div class="mt-4 text-lg mx-8">
Secret cannot be accessed due to one of the following reasons:
<div class="text-md mt-2">
<ul>
<li>- It doesn't exist</li>
<li>- It has expired</li>
<li>- It has reached max reads or decryption attempts</li>
<li>- Your IP doesn't match the IP addresses allowed</li>
</ul>
</div>
</div>

<div class="mt-8 text-sm text-center">
<div x-data="{ 'time': 30 }">
Redirecting in
<span
x-text="time"
x-init="
timer = setInterval(() => {
time -= 1;
if (time <= 0 ){
window.location.href = '/'
}
}, 1000)
"
>
</span>
seconds..
</div>
</div>
</.modal>

<div class="mt-2 text-sm text-center">
<div class="mt-8 text-sm text-center">
<div x-data="{ 'time': 30 }">
Redirecting in
<span
Expand Down
16 changes: 10 additions & 6 deletions lib/heimdall_web/live/secret_revealer_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ defmodule HeimdallWeb.SecretRevealerLive do
socket
|> assign(:secret, secret)
|> assign(:decrypted_text, nil)
|> assign(:redirect, false)
|> assign(:ip, ip)

if secret_viewable?(secret, socket) do
Expand All @@ -20,10 +21,11 @@ defmodule HeimdallWeb.SecretRevealerLive do
assign(socket, :secret, secret)
}
else
{
:noreply,
socket |> redirect(to: ~p"/secret_404")
}
socket = assign(socket, :redirect, true)

Process.send_after(self(), :check_expiration, 1000)

{:ok, socket}
end
end

Expand All @@ -42,8 +44,9 @@ defmodule HeimdallWeb.SecretRevealerLive do

def handle_info(:check_expiration, socket) do
secret = socket.assigns[:secret]
redirect = socket.assigns[:redirect]

if secret_viewable?(secret, socket) do
if !redirect and Secrets.not_expired?(secret) do
schedule_expiration_check()

{:noreply, socket}
Expand Down Expand Up @@ -86,7 +89,8 @@ defmodule HeimdallWeb.SecretRevealerLive do
defp secret_viewable?(secret, socket) do
ip = socket.assigns[:ip]

Secrets.not_expired?(secret) and Secrets.ip_allowed?(secret, ip)
Secrets.not_expired?(secret) and Secrets.not_stale?(secret) and
Secrets.ip_allowed?(secret, ip)
end

defp schedule_expiration_check do
Expand Down
176 changes: 89 additions & 87 deletions lib/heimdall_web/live/secret_revealer_live.html.heex
Original file line number Diff line number Diff line change
@@ -1,104 +1,106 @@
<div class="border rounded-xl px-6 py-8">
<div class="text-center text-xl mb-4">
Showing Secret: "<i><%= @secret.title %></i>"
</div>
<%= unless @redirect do %>
<div class="border rounded-xl px-6 py-8">
<div class="text-center text-xl mb-4">
Showing Secret: "<i><%= @secret.title %></i>"
</div>

<%= if @decrypted_text do %>
<div class="mt-2">
<.label for="decrypted_secret">Decrypted Secret</.label>
<%= if @decrypted_text do %>
<div class="mt-2">
<.label for="decrypted_secret">Decrypted Secret</.label>

<.input type="textarea" name="key" value={@decrypted_text} readonly />
</div>
<% else %>
<div class="mt-2">
<.form for={%{}} phx-submit="decrypt">
<%= if @secret.encryption_algo == :rsa do %>
<div x-data="{ show: false }">
<div class="grid grid-cols-4">
<div class="mt-0.5">
<.label for="decryption_key">RSA Private Key PEM</.label>
</div>
<.input type="textarea" name="key" value={@decrypted_text} readonly />
</div>
<% else %>
<div class="mt-2">
<.form for={%{}} phx-submit="decrypt">
<%= if @secret.encryption_algo == :rsa do %>
<div x-data="{ show: false }">
<div class="grid grid-cols-4">
<div class="mt-0.5">
<.label for="decryption_key">RSA Private Key PEM</.label>
</div>

<div
class="col-start-6 text-right rounded rounded-xl text-blue-800 hover:text-blue-400 cursor-pointer"
x-show="!show"
@click="show = true"
>
<.icon name="hero-eye" class="h-5 w-5" />
</div>
<div
class="col-start-6 text-right rounded rounded-xl text-blue-800 hover:text-blue-400 cursor-pointer"
x-show="!show"
@click="show = true"
>
<.icon name="hero-eye" class="h-5 w-5" />
</div>

<div
class="col-start-6 text-right rounded rounded-xl text-blue-800 hover:text-blue-400 cursor-pointer"
x-show="show"
@click="show = false"
style="display: none;"
>
<.icon name="hero-eye-slash" class="h-5 w-5" />
<div
class="col-start-6 text-right rounded rounded-xl text-blue-800 hover:text-blue-400 cursor-pointer"
x-show="show"
@click="show = false"
style="display: none;"
>
<.icon name="hero-eye-slash" class="h-5 w-5" />
</div>
</div>

<.input
type="textarea"
autocomplete="off"
name="key"
spellcheck="false"
value=""
style="color: transparent;text-shadow: 0 0 8px rgba(0,0,0,0.5);"
x-bind:style="!show && 'color: transparent;text-shadow: 0 0 8px rgba(0,0,0,0.5);'"
/>
</div>

<.input
type="textarea"
autocomplete="off"
name="key"
spellcheck="false"
value=""
style="color: transparent;text-shadow: 0 0 8px rgba(0,0,0,0.5);"
x-bind:style="!show && 'color: transparent;text-shadow: 0 0 8px rgba(0,0,0,0.5);'"
/>
</div>
<br />
<% end %>

<br />
<% end %>
<%= if @secret.encryption_algo == :aes_gcm do %>
<div x-data="{ show: false }">
<div class="grid grid-cols-4">
<div class="mt-0.5">
<.label for="decryption_key">Password</.label>
</div>

<%= if @secret.encryption_algo == :aes_gcm do %>
<div x-data="{ show: false }">
<div class="grid grid-cols-4">
<div class="mt-0.5">
<.label for="decryption_key">Password</.label>
</div>
<div
class="col-start-6 text-right rounded rounded-xl text-blue-800 hover:text-blue-400 cursor-pointer"
x-show="!show"
@click="show = true"
>
<.icon name="hero-eye" class="h-5 w-5" />
</div>

<div
class="col-start-6 text-right rounded rounded-xl text-blue-800 hover:text-blue-400 cursor-pointer"
x-show="!show"
@click="show = true"
>
<.icon name="hero-eye" class="h-5 w-5" />
<div
class="col-start-6 text-right rounded rounded-xl text-blue-800 hover:text-blue-400 cursor-pointer"
x-show="show"
@click="show = false"
style="display: none;"
>
<.icon name="hero-eye-slash" class="h-5 w-5" />
</div>
</div>

<div
class="col-start-6 text-right rounded rounded-xl text-blue-800 hover:text-blue-400 cursor-pointer"
x-show="show"
@click="show = false"
style="display: none;"
>
<.icon name="hero-eye-slash" class="h-5 w-5" />
</div>
<.input
autocomplete="off"
name="key"
spellcheck="false"
value=""
style="color: transparent;text-shadow: 0 0 8px rgba(0,0,0,0.5);"
x-bind:style="!show && 'color: transparent;text-shadow: 0 0 8px rgba(0,0,0,0.5);'"
/>
</div>

<.input
autocomplete="off"
name="key"
spellcheck="false"
value=""
style="color: transparent;text-shadow: 0 0 8px rgba(0,0,0,0.5);"
x-bind:style="!show && 'color: transparent;text-shadow: 0 0 8px rgba(0,0,0,0.5);'"
/>
</div>
<br />
<% end %>

<br />
<% end %>
<%= if @secret.encryption_algo == :plaintext do %>
<div class="hidden">
<.input name="key" value="plaintext" />
</div>
<% end %>

<%= if @secret.encryption_algo == :plaintext do %>
<div class="hidden">
<.input name="key" value="plaintext" />
<div class="text-center">
<.button>Reveal Secret</.button>
</div>
<% end %>

<div class="text-center">
<.button>Reveal Secret</.button>
</div>
</.form>
</div>
<% end %>
</div>
</.form>
</div>
<% end %>
</div>
<% end %>
43 changes: 43 additions & 0 deletions test/heimdall/secrets_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -176,4 +176,47 @@ defmodule Heimdall.SecretsTest do
assert error == "Error in decryption"
end
end

describe "not_stale?/1" do
test "returns true when max reads are greater than secret reads" do
{:ok, secret} = Factory.encrypt_and_create(%{max_reads: 2})

assert Secrets.not_stale?(secret)
end

test "returns false when max reads are less than or eq secret reads" do
{:ok, secret} = Factory.encrypt_and_create(%{max_reads: 1})

_read = Secrets.create_secret_read(secret, "ip", DateTime.utc_now())

refute Secrets.not_stale?(secret)
end

test "returns true when max reads are nil" do
{:ok, secret} = Factory.encrypt_and_create(%{max_reads: nil})

assert Secrets.not_stale?(secret)
end

test "returns true when max attempts are greater than secret attempts" do
{:ok, secret} = Factory.encrypt_and_create(%{max_decryption_attempts: 2})

assert Secrets.not_stale?(secret)
end

test "returns false when max attempts are less than or eq secret attempts" do
{:ok, secret} = Factory.encrypt_and_create(%{max_decryption_attempts: 1})

_attempt = Secrets.create_secret_attempt(secret, "ip", DateTime.utc_now())

refute Secrets.not_stale?(secret)
end

test "returns true when max attempts are nil" do
{:ok, secret} =
Factory.encrypt_and_create(%{max_decryption_attempts: nil})

assert Secrets.not_stale?(secret)
end
end
end
Loading

0 comments on commit c832b95

Please sign in to comment.