summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md3
-rw-r--r--config/config.exs8
-rw-r--r--config/test.exs5
-rw-r--r--lib/chessh/application.ex2
-rw-r--r--lib/chessh/auth/password.ex3
-rw-r--r--lib/chessh/schema/key.ex3
-rw-r--r--lib/chessh/ssh/daemon.ex60
-rw-r--r--lib/chessh/ssh/server.ex0
-rw-r--r--lib/chessh/ssh/server_key.ex11
-rw-r--r--mix.exs5
-rw-r--r--mix.lock4
-rw-r--r--priv/test_keys/client_keys/.gitignore1
-rw-r--r--priv/test_keys/client_keys/id_ed255197
-rw-r--r--priv/test_keys/client_keys/id_ed25519.pub1
-rw-r--r--test/auth/password_test.exs7
-rw-r--r--test/auth/pubkey_test.exs6
-rw-r--r--test/schema/register_test.exs3
-rw-r--r--test/ssh/ssh_auth_test.exs94
18 files changed, 203 insertions, 20 deletions
diff --git a/README.md b/README.md
index 967c4dc..ae1b5a0 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,5 @@
# CheSSH
+Features:
+- [X] SSH Key & Password authentication
+- [ ] Rate limiting \ No newline at end of file
diff --git a/config/config.exs b/config/config.exs
index daffcad..2136a60 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -2,8 +2,12 @@ import Config
config :chessh,
ecto_repos: [Chessh.Repo],
- priv_dir: Path.join(Path.dirname(__DIR__), "priv/keys"),
- port: 42069,
+ key_dir: Path.join(Path.dirname(__DIR__), "priv/keys"),
+ max_password_attempts: 3,
+ port: 42_069,
max_sessions: 255
+config :hammer,
+ backend: {Hammer.Backend.ETS, [expiry_ms: 60_000 * 60 * 4, cleanup_interval_ms: 60_000 * 10]}
+
import_config "#{config_env()}.exs"
diff --git a/config/test.exs b/config/test.exs
index c1d70dd..db11bb4 100644
--- a/config/test.exs
+++ b/config/test.exs
@@ -1,5 +1,8 @@
import Config
+config :hammer,
+ backend: {Hammer.Backend.ETS, [expiry_ms: 60_000 * 60 * 4, cleanup_interval_ms: 60_000 * 10]}
+
config :chessh, Chessh.Repo,
database: "chessh-test",
username: "postgres",
@@ -8,4 +11,4 @@ config :chessh, Chessh.Repo,
pool: Ecto.Adapters.SQL.Sandbox
config :chessh,
- priv_dir: Path.join(Path.dirname(__DIR__), "priv/keys")
+ key_dir: Path.join(Path.dirname(__DIR__), "priv/test_keys")
diff --git a/lib/chessh/application.ex b/lib/chessh/application.ex
index c760532..847dd98 100644
--- a/lib/chessh/application.ex
+++ b/lib/chessh/application.ex
@@ -2,7 +2,7 @@ defmodule Chessh.Application do
use Application
def start(_, _) do
- children = [Chessh.Repo]
+ children = [Chessh.Repo, Chessh.SSH.Daemon]
opts = [strategy: :one_for_one, name: Chessh.Supervisor]
Supervisor.start_link(children, opts)
end
diff --git a/lib/chessh/auth/password.ex b/lib/chessh/auth/password.ex
index 8a6c683..c3b03f5 100644
--- a/lib/chessh/auth/password.ex
+++ b/lib/chessh/auth/password.ex
@@ -1,6 +1,5 @@
defmodule Chessh.Auth.PasswordAuthenticator do
- alias Chessh.Player
- alias Chessh.Repo
+ alias Chessh.{Player, Repo}
def authenticate(username, password) do
case Repo.get_by(Player, username: String.Chars.to_string(username)) do
diff --git a/lib/chessh/schema/key.ex b/lib/chessh/schema/key.ex
index 765c83b..adf018d 100644
--- a/lib/chessh/schema/key.ex
+++ b/lib/chessh/schema/key.ex
@@ -16,7 +16,7 @@ defmodule Chessh.Key do
|> cast(update_encode_key(attrs, :key), [:key])
|> cast(attrs, [:name])
|> validate_required([:key, :name])
- |> validate_format(:key, ~r/[\-\w\d]+ [^ ]+$/, message: "invalid ssh key")
+ |> validate_format(:key, ~r/[\-\w\d]+ [^ ]+$/, message: "invalid public ssh key")
|> validate_format(:key, ~r/^(?!ssh-dss).+/, message: "DSA keys are not supported")
end
@@ -41,7 +41,6 @@ defmodule Chessh.Key do
end
# Remove comment at end of key
|> String.replace(~r/ [^ ]+\@[^ ]+$/, "")
- # Remove potential spaces / newline
|> String.trim()
end
end
diff --git a/lib/chessh/ssh/daemon.ex b/lib/chessh/ssh/daemon.ex
new file mode 100644
index 0000000..acb6bea
--- /dev/null
+++ b/lib/chessh/ssh/daemon.ex
@@ -0,0 +1,60 @@
+defmodule Chessh.SSH.Daemon do
+ use GenServer
+
+ def start_link(_) do
+ GenServer.start_link(__MODULE__, %{
+ pid: nil
+ })
+ end
+
+ def init(state) do
+ GenServer.cast(self(), :start)
+ {:ok, state}
+ end
+
+ def pwd_authenticate(username, password, _address, attempts) do
+ if Chessh.Auth.PasswordAuthenticator.authenticate(username, password) do
+ true
+ else
+ newAttempts =
+ case attempts do
+ :undefined -> 0
+ _ -> attempts
+ end
+
+ if Application.fetch_env!(:chessh, :max_password_attempts) <= newAttempts do
+ :disconnect
+ else
+ {false, newAttempts + 1}
+ end
+ end
+ end
+
+ def handle_cast(:start, state) do
+ port = Application.fetch_env!(:chessh, :port)
+ key_dir = String.to_charlist(Application.fetch_env!(:chessh, :key_dir))
+ max_sessions = Application.fetch_env!(:chessh, :max_sessions)
+
+ case :ssh.daemon(
+ port,
+ system_dir: key_dir,
+ pwdfun: &pwd_authenticate/4,
+ key_cb: Chessh.SSH.ServerKey,
+ id_string: :random,
+ subsystems: [],
+ parallel_login: true,
+ max_sessions: max_sessions
+ ) do
+ {:ok, pid} ->
+ Process.link(pid)
+ {:noreply, %{state | pid: pid}, :hibernate}
+
+ {:error, err} ->
+ raise inspect(err)
+ end
+
+ {:noreply, state}
+ end
+
+ def handle_info(_, state), do: {:noreply, state}
+end
diff --git a/lib/chessh/ssh/server.ex b/lib/chessh/ssh/server.ex
deleted file mode 100644
index e69de29..0000000
--- a/lib/chessh/ssh/server.ex
+++ /dev/null
diff --git a/lib/chessh/ssh/server_key.ex b/lib/chessh/ssh/server_key.ex
new file mode 100644
index 0000000..1096e09
--- /dev/null
+++ b/lib/chessh/ssh/server_key.ex
@@ -0,0 +1,11 @@
+defmodule Chessh.SSH.ServerKey do
+ @behaviour :ssh_server_key_api
+
+ def is_auth_key(key, username, _daemon_options) do
+ Chessh.Auth.KeyAuthenticator.authenticate(username, key)
+ end
+
+ def host_key(algorithm, daemon_options) do
+ :ssh_file.host_key(algorithm, daemon_options)
+ end
+end
diff --git a/mix.exs b/mix.exs
index 4d91dd2..500d0b2 100644
--- a/mix.exs
+++ b/mix.exs
@@ -17,7 +17,7 @@ defmodule Chessh.MixProject do
def application do
[
mod: {Chessh.Application, []},
- extra_applications: [:logger, :crypto, :ssh]
+ extra_applications: [:logger, :crypto, :ssh, :iex]
]
end
@@ -31,7 +31,8 @@ defmodule Chessh.MixProject do
{:ecto, "~> 3.9"},
{:ecto_sql, "~> 3.9"},
{:postgrex, "~> 0.16.5"},
- {:bcrypt_elixir, "~> 3.0"}
+ {:bcrypt_elixir, "~> 3.0"},
+ {:hammer, "~> 6.0"}
]
end
diff --git a/mix.lock b/mix.lock
index ec97627..a956b4d 100644
--- a/mix.lock
+++ b/mix.lock
@@ -5,10 +5,14 @@
"connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
"db_connection": {:hex, :db_connection, "2.4.3", "3b9aac9f27347ec65b271847e6baeb4443d8474289bd18c1d6f4de655b70c94d", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c127c15b0fa6cfb32eed07465e05da6c815b032508d4ed7c116122871df73c12"},
"decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"},
+ "dialyxir": {:hex, :dialyxir, "1.2.0", "58344b3e87c2e7095304c81a9ae65cb68b613e28340690dfe1a5597fd08dec37", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "61072136427a851674cab81762be4dbeae7679f85b1272b6d25c3a839aff8463"},
"ecto": {:hex, :ecto, "3.9.2", "017db3bc786ff64271108522c01a5d3f6ba0aea5c84912cfb0dd73bf13684108", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "21466d5177e09e55289ac7eade579a642578242c7a3a9f91ad5c6583337a9d15"},
"ecto_sql": {:hex, :ecto_sql, "3.9.1", "9bd5894eecc53d5b39d0c95180d4466aff00e10679e13a5cfa725f6f85c03c22", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5fd470a4fff2e829bbf9dcceb7f3f9f6d1e49b4241e802f614de6b8b67c51118"},
"elixir_make": {:hex, :elixir_make, "0.7.2", "e83548b0500e654d1a595f1134af4862a2e92ec3282ec4c2a17641e9aa45ee73", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "05fb44abf9582381c2eb1b73d485a55288c581071de0ee3ee1084ee69d6a8e5f"},
+ "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
"esshd": {:hex, :esshd, "0.2.1", "cded6a329c32bc3b3c15828bcd34203227bbef310db3c39a6f3c55cf5b29cd34", [:mix], [], "hexpm", "b058b56af53aba1c23522d72a3c39ab7f302e509af1c0ba1a748f00d93053c4d"},
+ "hammer": {:hex, :hammer, "6.1.0", "f263e3c3e9946bd410ea0336b2abe0cb6260af4afb3a221e1027540706e76c55", [:make, :mix], [{:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}], "hexpm", "b47e415a562a6d072392deabcd58090d8a41182cf9044cdd6b0d0faaaf68ba57"},
+ "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
"postgrex": {:hex, :postgrex, "0.16.5", "fcc4035cc90e23933c5d69a9cd686e329469446ef7abba2cf70f08e2c4b69810", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "edead639dc6e882618c01d8fc891214c481ab9a3788dfe38dd5e37fd1d5fb2e8"},
"telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"},
}
diff --git a/priv/test_keys/client_keys/.gitignore b/priv/test_keys/client_keys/.gitignore
new file mode 100644
index 0000000..6599fc4
--- /dev/null
+++ b/priv/test_keys/client_keys/.gitignore
@@ -0,0 +1 @@
+known_hosts \ No newline at end of file
diff --git a/priv/test_keys/client_keys/id_ed25519 b/priv/test_keys/client_keys/id_ed25519
new file mode 100644
index 0000000..af061d7
--- /dev/null
+++ b/priv/test_keys/client_keys/id_ed25519
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACDyhmROJ9PPZsBpG46n+FCLn+mP0nncwSPgXO9xRRsKPQAAAJAJnUS4CZ1E
+uAAAAAtzc2gtZWQyNTUxOQAAACDyhmROJ9PPZsBpG46n+FCLn+mP0nncwSPgXO9xRRsKPQ
+AAAEBjR5Cy8SHUtrIf6aHJGXA/kgesZzxxjH15E4wj1DESh/KGZE4n089mwGkbjqf4UIuf
+6Y/SedzBI+Bc73FFGwo9AAAABm5vbmFtZQECAwQFBgc=
+-----END OPENSSH PRIVATE KEY----- \ No newline at end of file
diff --git a/priv/test_keys/client_keys/id_ed25519.pub b/priv/test_keys/client_keys/id_ed25519.pub
new file mode 100644
index 0000000..e97b08e
--- /dev/null
+++ b/priv/test_keys/client_keys/id_ed25519.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPKGZE4n089mwGkbjqf4UIuf6Y/SedzBI+Bc73FFGwo9 \ No newline at end of file
diff --git a/test/auth/password_test.exs b/test/auth/password_test.exs
index 974f2fa..1516bdf 100644
--- a/test/auth/password_test.exs
+++ b/test/auth/password_test.exs
@@ -1,11 +1,10 @@
defmodule Chessh.Auth.PasswordAuthenticatorTest do
use ExUnit.Case
- alias Chessh.Player
- alias Chessh.Repo
+ alias Chessh.{Player, Repo}
@valid_user %{username: "logan", password: "password"}
- setup do
+ setup_all do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Chessh.Repo)
{:ok, _user} = Repo.insert(Player.registration_changeset(%Player{}, @valid_user))
@@ -13,7 +12,7 @@ defmodule Chessh.Auth.PasswordAuthenticatorTest do
:ok
end
- test "User can sign in with their password" do
+ test "Password can authenticate a hashed password" do
assert Chessh.Auth.PasswordAuthenticator.authenticate(
String.to_charlist(@valid_user.username),
String.to_charlist(@valid_user.password)
diff --git a/test/auth/pubkey_test.exs b/test/auth/pubkey_test.exs
index 78eecfb..d8236e3 100644
--- a/test/auth/pubkey_test.exs
+++ b/test/auth/pubkey_test.exs
@@ -1,8 +1,6 @@
defmodule Chessh.Auth.PublicKeyAuthenticatorTest do
use ExUnit.Case
- alias Chessh.Key
- alias Chessh.Repo
- alias Chessh.Player
+ alias Chessh.{Key, Repo, Player}
@valid_user %{username: "logan", password: "password"}
@valid_key %{
@@ -10,7 +8,7 @@ defmodule Chessh.Auth.PublicKeyAuthenticatorTest do
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ/2LOJGGEd/dhFgRxJ5MMv0jJw4s4pA8qmMbZyulN44"
}
- setup do
+ setup_all do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Chessh.Repo)
{:ok, player} = Repo.insert(Player.registration_changeset(%Player{}, @valid_user))
diff --git a/test/schema/register_test.exs b/test/schema/register_test.exs
index 5705d31..0e9fdf1 100644
--- a/test/schema/register_test.exs
+++ b/test/schema/register_test.exs
@@ -1,8 +1,7 @@
defmodule Chessh.Auth.UserRegistrationTest do
use Chessh.RepoCase
use ExUnit.Case
- alias Chessh.Player
- alias Chessh.Repo
+ alias Chessh.{Player, Repo}
@valid_user %{username: "logan", password: "password"}
@invalid_username %{username: "a", password: "password"}
diff --git a/test/ssh/ssh_auth_test.exs b/test/ssh/ssh_auth_test.exs
new file mode 100644
index 0000000..c3ced20
--- /dev/null
+++ b/test/ssh/ssh_auth_test.exs
@@ -0,0 +1,94 @@
+defmodule Chessh.SSH.AuthTest do
+ use ExUnit.Case
+ alias Chessh.{Player, Repo, Key}
+
+ @localhost '127.0.0.1'
+ @key_name "The Gamer Machine"
+ @valid_user %{username: "logan", password: "password"}
+ @client_test_keys_dir Path.join(Application.compile_env!(:chessh, :key_dir), "client_keys")
+ @client_pub_key 'id_ed25519.pub'
+
+ setup_all do
+ case Ecto.Adapters.SQL.Sandbox.checkout(Repo) do
+ :ok -> nil
+ {:already, :owner} -> nil
+ end
+
+ Ecto.Adapters.SQL.Sandbox.mode(Repo, {:shared, self()})
+
+ {:ok, player} = Repo.insert(Player.registration_changeset(%Player{}, @valid_user))
+
+ {:ok, key_text} = File.read(Path.join(@client_test_keys_dir, @client_pub_key))
+
+ {:ok, _key} =
+ Repo.insert(
+ Key.changeset(%Key{}, %{key: key_text, name: @key_name})
+ |> Ecto.Changeset.put_assoc(:player, player)
+ )
+
+ :ok
+ end
+
+ test "Fails to authenticate after configured max password attempt" do
+ assert :disconnect ==
+ Enum.reduce(
+ 1..Application.fetch_env!(:chessh, :max_password_attempts),
+ %{attempts: 0},
+ fn acc, _ ->
+ case Chessh.SSH.Daemon.pwd_authenticate(
+ @valid_user.username,
+ 'wrong_password',
+ @localhost,
+ acc
+ ) do
+ {false, state} -> state
+ x -> x
+ end
+ end
+ )
+ end
+
+ test "INTEGRATION TEST - Can ssh into daemon with password or public key" do
+ {:ok, sup} = Task.Supervisor.start_link()
+ test_pid = self()
+
+ Task.Supervisor.start_child(sup, fn ->
+ {:ok, _pid} =
+ :ssh.connect(@localhost, Application.fetch_env!(:chessh, :port),
+ user: String.to_charlist(@valid_user.username),
+ password: String.to_charlist(@valid_user.password),
+ auth_methods: 'password',
+ silently_accept_hosts: true
+ )
+
+ send(test_pid, :connected_via_password)
+ end)
+
+ Task.Supervisor.start_child(sup, fn ->
+ {:ok, _pid} =
+ :ssh.connect(@localhost, Application.fetch_env!(:chessh, :port),
+ user: String.to_charlist(@valid_user.username),
+ auth_methods: 'publickey',
+ silently_accept_hosts: true,
+ user_dir: String.to_charlist(@client_test_keys_dir)
+ )
+
+ send(test_pid, :connected_via_public_key)
+ end)
+
+ assert_receive(:connected_via_password, 500)
+ assert_receive(:connected_via_public_key, 500)
+ end
+
+ test "Hosts are rate limited via password attempts" do
+ :ok
+ end
+
+ test "Hosts are also rate limited with public keys" do
+ :ok
+ end
+
+ test "User cannot have more than one current session" do
+ :ok
+ end
+end