Skip to content

Commit

Permalink
Add ability to check ip and update reads/attempts
Browse files Browse the repository at this point in the history
  • Loading branch information
thebugcatcher committed Oct 29, 2023
1 parent e73efd5 commit a26b57f
Show file tree
Hide file tree
Showing 10 changed files with 116 additions and 32 deletions.
2 changes: 1 addition & 1 deletion lib/heimdall/data/secret.ex
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ defmodule Heimdall.Data.Secret do
field(:expires_at, :utc_datetime)
field(:max_reads, :integer)
field(:max_decryption_attempts, :integer)
field(:ip_regex, :string, default: "*")
field(:ip_regex, :string, default: ".*")

has_many(:attempts, __MODULE__.Attempt)
has_many(:reads, __MODULE__.Read)
Expand Down
40 changes: 40 additions & 0 deletions lib/heimdall/secrets.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ defmodule Heimdall.Secrets do

alias Ecto.Changeset
alias Heimdall.Data.Secret
alias Heimdall.Data.Secret.Attempt
alias Heimdall.Data.Secret.Read
alias Heimdall.EncryptionAlgo.AesGcm
alias Heimdall.EncryptionAlgo.Plaintext
alias Heimdall.EncryptionAlgo.RSA
Expand Down Expand Up @@ -71,6 +73,44 @@ defmodule Heimdall.Secrets do
|> Kernel.not()
end

@doc """
Creates a Read record for a Secret
"""
@spec create_secret_read(Secret.t(), String.t(), DateTime.t()) ::
{:ok, Read.t()} | {:error, term()}
def create_secret_read(secret, ip, read_at) do
%{
secret_id: secret.id,
ip_address: ip,
read_at: read_at
}
|> Read.changeset()
|> Repo.insert()
end

@doc """
Creates an Attempt record for a Secret
"""
@spec create_secret_attempt(Secret.t(), String.t(), DateTime.t()) ::
{:ok, Attempt.t()} | {:error, term()}
def create_secret_attempt(secret, ip, attempted_at) do
%{
secret_id: secret.id,
ip_address: ip,
attempted_at: attempted_at
}
|> Attempt.changeset()
|> Repo.insert()
end

@doc """
Checks if an IP address is allowed to view a secret
"""
@spec ip_allowed?(Secret.t(), String.t()) :: boolean()
def ip_allowed?(secret, ip) do
Regex.match?(~r"#{secret.ip_regex}", ip)
end

defp maybe_encrypt_text(%Changeset{valid?: false} = changeset), do: changeset

defp maybe_encrypt_text(changeset) do
Expand Down
4 changes: 4 additions & 0 deletions lib/heimdall_web/controllers/secret_html.ex
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,8 @@ defmodule HeimdallWeb.SecretHTML do
defp humanize_encryption_algo(:rsa) do
"RSA (public key to encrypt & private key to decrypt)"
end

defp remote_ip_to_string({num1, num2, num3, num4}) do
"#{num1}.#{num2}.#{num3}.#{num4}"
end
end
12 changes: 9 additions & 3 deletions lib/heimdall_web/controllers/secret_html/secret_404.html.heex
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
<div class="mt-4 text-lg font-semibold text-center">
Secret doesn't exist or has expired
<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>

<div class="mt-2 text-sm text-center">
<div x-data="{ 'time': 5 }">
<div x-data="{ 'time': 30 }">
Redirecting in
<span
x-text="time"
Expand Down
5 changes: 4 additions & 1 deletion lib/heimdall_web/controllers/secret_html/show.html.heex
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
<%= live_render(@conn, HeimdallWeb.SecretRevealerLive,
session: %{"secret_id" => @secret.id}
session: %{
"secret_id" => @secret.id,
"ip" => remote_ip_to_string(@conn.remote_ip)
}
) %>
67 changes: 49 additions & 18 deletions lib/heimdall_web/live/secret_revealer_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,70 @@ defmodule HeimdallWeb.SecretRevealerLive do

alias Heimdall.Secrets

def mount(_params, %{"secret_id" => secret_id}, socket) do
def mount(_params, %{"secret_id" => secret_id, "ip" => ip}, socket) do
secret = Heimdall.Secrets.get(secret_id)

socket =
socket
|> assign(:secret, secret)
|> assign(:decrypted_text, nil)
|> assign(:ip, ip)

schedule_expiration_check()
if secret_viewable?(secret, socket) do
schedule_expiration_check()

{
:ok,
assign(socket, :secret, secret)
}
{
:ok,
assign(socket, :secret, secret)
}
else
{
:noreply,
socket |> redirect(to: ~p"/secret_404")
}
end
end

def handle_event("decrypt", %{"key" => key}, socket) do
secret = socket.assigns[:secret]

if secret_viewable?(secret, socket) do
do_decrypt(secret, socket, key)
else
{
:noreply,
socket |> redirect(to: ~p"/secret_404")
}
end
end

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

if secret_viewable?(secret, socket) do
schedule_expiration_check()

{:noreply, socket}
else
{
:noreply,
socket |> redirect(to: ~p"/secret_404")
}
end
end

defp do_decrypt(secret, socket, key) do
ip = socket.assigns[:ip]

case Secrets.decrypt(secret, key) do
{:ok, decrypted_text} ->
socket =
socket
|> put_flash(:info, "Successfully decrypted")
|> assign(:decrypted_text, decrypted_text)

Secrets.create_secret_read(secret, ip, DateTime.utc_now())

schedule_expiration_check()

{:noreply, socket}
Expand All @@ -39,23 +77,16 @@ defmodule HeimdallWeb.SecretRevealerLive do
|> put_flash(:error, "Error in decryption:\n #{error}")
|> assign(:decrypted_text, nil)

Secrets.create_secret_attempt(secret, ip, DateTime.utc_now())

{:noreply, socket}
end
end

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

if Secrets.not_expired?(secret) do
schedule_expiration_check()
defp secret_viewable?(secret, socket) do
ip = socket.assigns[:ip]

{:noreply, socket}
else
{
:noreply,
socket |> redirect(to: ~p"/secret_404")
}
end
Secrets.not_expired?(secret) and Secrets.ip_allowed?(secret, ip)
end

defp schedule_expiration_check do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ defmodule Heimdall.Repo.Migrations.CreateSecretAttempts do
create table(:attempts, primary_key: false) do
add(:id, :uuid, primary_key: true)

add(:secret_id, references(:secrets, type: :uuid))
add(:secret_id, references(:secrets, type: :uuid, on_delete: :delete_all))

add(:ip_address, :string)
add(:attempted_at, :utc_datetime)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ defmodule Heimdall.Repo.Migrations.CreateSecretReads do
create table(:reads, primary_key: false) do
add(:id, :uuid, primary_key: true)

add(:secret_id, references(:secrets, type: :uuid))
add(:secret_id, references(:secrets, type: :uuid, on_delete: :delete_all))

add(:ip_address, :string)
add(:read_at, :utc_datetime)
Expand Down
2 changes: 1 addition & 1 deletion test/heimdall_web/controllers/secret_controller_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ defmodule HeimdallWeb.SecretControllerTest do
test "renders a page with expected content", %{conn: conn} do
conn = get(conn, ~p"/secret_404")

assert html_response(conn, 200) =~ "Secret doesn't exist"
assert html_response(conn, 200) =~ "doesn't exist"
end
end
end
12 changes: 6 additions & 6 deletions test/heimdall_web/live/secret_revealer_live_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ defmodule HeimdallWeb.SecretRevealerLiveTest do
live_isolated(
conn,
SecretRevealerLive,
session: %{"secret_id" => secret.id}
session: %{"secret_id" => secret.id, "ip" => "ip"}
)

assert html =~ secret.title
Expand All @@ -43,7 +43,7 @@ defmodule HeimdallWeb.SecretRevealerLiveTest do
live_isolated(
conn,
SecretRevealerLive,
session: %{"secret_id" => secret.id}
session: %{"secret_id" => secret.id, "ip" => "ip"}
)

# Secret isn't visible until form is submitted
Expand All @@ -69,7 +69,7 @@ defmodule HeimdallWeb.SecretRevealerLiveTest do
live_isolated(
conn,
SecretRevealerLive,
session: %{"secret_id" => secret.id}
session: %{"secret_id" => secret.id, "ip" => "ip"}
)

# Secret isn't visible until form is submitted
Expand Down Expand Up @@ -103,7 +103,7 @@ defmodule HeimdallWeb.SecretRevealerLiveTest do
live_isolated(
conn,
SecretRevealerLive,
session: %{"secret_id" => secret.id}
session: %{"secret_id" => secret.id, "ip" => "ip"}
)

html =
Expand Down Expand Up @@ -135,7 +135,7 @@ defmodule HeimdallWeb.SecretRevealerLiveTest do
live_isolated(
conn,
SecretRevealerLive,
session: %{"secret_id" => secret.id}
session: %{"secret_id" => secret.id, "ip" => "ip"}
)

html =
Expand Down Expand Up @@ -169,7 +169,7 @@ defmodule HeimdallWeb.SecretRevealerLiveTest do
live_isolated(
conn,
SecretRevealerLive,
session: %{"secret_id" => secret.id}
session: %{"secret_id" => secret.id, "ip" => "ip"}
)

html =
Expand Down

0 comments on commit a26b57f

Please sign in to comment.