Implement public key and add tests

This commit is contained in:
Simponic 2022-12-19 20:45:01 -07:00
parent fe661a935a
commit 110eb0b199
Signed by untrusted user who does not match committer: simponic
GPG Key ID: 52B3774857EB24B1
10 changed files with 133 additions and 11 deletions

View File

@ -8,6 +8,8 @@ config :esshd,
handler: {Chessh.Shell, :on_shell, 4}, handler: {Chessh.Shell, :on_shell, 4},
port: 42069, port: 42069,
password_authenticator: Chessh.Auth.PasswordAuthenticator, password_authenticator: Chessh.Auth.PasswordAuthenticator,
public_key_authenticator: Chessh.Auth.KeyAuthenticator public_key_authenticator: Chessh.Auth.KeyAuthenticator,
# TODO - benchmark
max_sessions: 128
import_config "#{config_env()}.exs" import_config "#{config_env()}.exs"

View File

@ -1,10 +1,19 @@
defmodule Chessh.Auth.KeyAuthenticator do defmodule Chessh.Auth.KeyAuthenticator do
alias Chessh.Key
alias Chessh.Repo
use Sshd.PublicKeyAuthenticator use Sshd.PublicKeyAuthenticator
require Logger require Logger
import Ecto.Query
def authenticate(username, public_key, _opts) do def authenticate(username, public_key) do
Logger.debug("#{inspect(username)}") !!Repo.one(
Logger.debug("#{inspect(public_key)}") from(k in Key,
true join: p in assoc(k, :player),
where: k.key == ^Key.encode_key(public_key),
where: p.username == ^String.Chars.to_string(username)
)
)
end end
def authenticate(username, public_key, _opts), do: authenticate(username, public_key)
end end

View File

@ -2,11 +2,11 @@ defmodule Chessh.Auth.PasswordAuthenticator do
alias Chessh.Player alias Chessh.Player
alias Chessh.Repo alias Chessh.Repo
use Sshd.PasswordAuthenticator use Sshd.PasswordAuthenticator
require Logger
def authenticate(username, password) do def authenticate(username, password) do
case Repo.get_by(Player, username: String.Chars.to_string(username)) do case Repo.get_by(Player, username: String.Chars.to_string(username)) do
nil -> false x -> Player.valid_password?(x, String.Chars.to_string(password))
x -> Player.valid_password?(x, password)
end end
end end
end end

View File

@ -32,6 +32,7 @@ defmodule Chessh.Key do
if is_tuple(key) do if is_tuple(key) do
case key do case key do
{pub, [opts]} -> [{pub, [opts]}] {pub, [opts]} -> [{pub, [opts]}]
{pub, []} -> [{pub, [comment: '']}]
key -> [{key, [comment: '']}] key -> [{key, [comment: '']}]
end end
|> :ssh_file.encode(:openssh_key) |> :ssh_file.encode(:openssh_key)

View File

@ -49,7 +49,7 @@ defmodule Chessh.Player do
defp validate_username(changeset) do defp validate_username(changeset) do
changeset changeset
|> validate_required([:username]) |> validate_required([:username])
|> validate_length(:username, min: 2, max: 12) |> validate_length(:username, min: 2, max: 16)
|> validate_format(:username, ~r/^[a-zA-Z0-9_\-]*$/, |> validate_format(:username, ~r/^[a-zA-Z0-9_\-]*$/,
message: "only letters, numbers, underscores, and hyphens allowed" message: "only letters, numbers, underscores, and hyphens allowed"
) )

View File

@ -0,0 +1,27 @@
defmodule Chessh.Auth.PasswordAuthenticatorTest do
use ExUnit.Case
alias Chessh.Player
alias Chessh.Repo
@valid_user %{username: "logan", password: "password"}
setup do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Chessh.Repo)
{:ok, _user} = Repo.insert(Player.registration_changeset(%Player{}, @valid_user))
:ok
end
test "User can sign in with their password" do
assert Chessh.Auth.PasswordAuthenticator.authenticate(
String.to_charlist(@valid_user.username),
String.to_charlist(@valid_user.password)
)
refute Chessh.Auth.PasswordAuthenticator.authenticate(
String.to_charlist(@valid_user.username),
String.to_charlist("a_bad_password")
)
end
end

35
test/auth/pubkey_test.exs Normal file
View File

@ -0,0 +1,35 @@
defmodule Chessh.Auth.PublicKeyAuthenticatorTest do
use ExUnit.Case
alias Chessh.Key
alias Chessh.Repo
alias Chessh.Player
@valid_user %{username: "logan", password: "password"}
@valid_key %{
name: "The Gamer Machine",
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ/2LOJGGEd/dhFgRxJ5MMv0jJw4s4pA8qmMbZyulN44"
}
setup do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Chessh.Repo)
{:ok, player} = Repo.insert(Player.registration_changeset(%Player{}, @valid_user))
{:ok, _key} =
Repo.insert(
Key.changeset(%Key{}, @valid_key)
|> Ecto.Changeset.put_assoc(:player, player)
)
:ok
end
test "User can sign in with their ssh key from raw string" do
assert Chessh.Auth.KeyAuthenticator.authenticate(@valid_user.username, @valid_key.key)
end
test "User can sign in with erlang decoded ssh key" do
[key] = :ssh_file.decode(@valid_key.key, :openssh_key)
assert Chessh.Auth.KeyAuthenticator.authenticate(@valid_user.username, key)
end
end

View File

@ -28,7 +28,6 @@ defmodule Chessh.Schema.KeyTest do
@empty_attrs %{} @empty_attrs %{}
test "changeset with valid attributes" do test "changeset with valid attributes" do
IO.puts(inspect(Key.changeset(%Key{}, @valid_attrs)))
assert Key.changeset(%Key{}, @valid_attrs).valid? assert Key.changeset(%Key{}, @valid_attrs).valid?
assert Key.changeset(%Key{}, @valid_key_attrs).valid? assert Key.changeset(%Key{}, @valid_key_attrs).valid?
end end

View File

@ -0,0 +1,51 @@
defmodule Chessh.Auth.UserRegistrationTest do
use Chessh.RepoCase
use ExUnit.Case
alias Chessh.Player
alias Chessh.Repo
@valid_user %{username: "logan", password: "password"}
@invalid_username %{username: "a", password: "password"}
@invalid_password %{username: "aasdf", password: "pass"}
@repeated_username %{username: "LoGan", password: "password"}
test "Password must be at least 8 characters and username must be at least 2" do
refute Player.registration_changeset(%Player{}, @invalid_password).valid?
refute Player.registration_changeset(%Player{}, @invalid_username).valid?
end
test "Password changeset must match" do
refute Player.password_changeset(
%Player{},
Map.put(@valid_user, :password_confirmation,
password_confirmation: @valid_user.password <> "a"
)
).valid?
valid_user_changed_password = Map.put(@valid_user, :password, "a_new_password")
assert Player.password_changeset(
%Player{},
Map.put(
valid_user_changed_password,
:password_confirmation,
valid_user_changed_password.password
)
).valid?
end
test "Password is hashed" do
changeset = Player.registration_changeset(%Player{}, @valid_user)
assert_raise KeyError, fn -> changeset.changes.password end
assert changeset.changes.hashed_password
refute changeset.changes.hashed_password == @valid_user.password
end
test "Username is uniquely case insensitive" do
assert Repo.insert(Player.registration_changeset(%Player{}, @valid_user))
assert {:error,
%{errors: [{:username, {_, [{:constraint, :unique}, {:constraint_name, _}]}}]}} =
Repo.insert(Player.registration_changeset(%Player{}, @repeated_username))
end
end

View File

@ -8,8 +8,6 @@ defmodule Chessh.RepoCase do
import Ecto import Ecto
import Ecto.Query import Ecto.Query
import Chessh.RepoCase import Chessh.RepoCase
# and any other stuff
end end
end end