Merge branch 'pleroma-password' into 'develop'

Add password module

See merge request pleroma/pleroma!3253
This commit is contained in:
rinpatch 2021-01-14 18:29:25 +00:00
commit 93ce7b0efb
21 changed files with 130 additions and 31 deletions

View file

@ -55,7 +55,7 @@ defp generate_user(i) do
name: "Test テスト User #{i}", name: "Test テスト User #{i}",
email: "user#{i}@example.com", email: "user#{i}@example.com",
nickname: "nick#{i}", nickname: "nick#{i}",
password_hash: Pbkdf2.hash_pwd_salt("test"), password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("test"),
bio: "Tester Number #{i}", bio: "Tester Number #{i}",
local: !remote local: !remote
} }

View file

@ -53,7 +53,7 @@
config :pleroma, :dangerzone, override_repo_pool_size: true config :pleroma, :dangerzone, override_repo_pool_size: true
# Reduce hash rounds for testing # Reduce hash rounds for testing
config :pbkdf2_elixir, rounds: 1 config :pleroma, :password, iterations: 1
config :tesla, adapter: Tesla.Mock config :tesla, adapter: Tesla.Mock

View file

@ -71,7 +71,7 @@ def invalidate_backup_code(%User{} = user, hash_code) do
@spec generate_backup_codes(User.t()) :: {:ok, list(binary)} | {:error, String.t()} @spec generate_backup_codes(User.t()) :: {:ok, list(binary)} | {:error, String.t()}
def generate_backup_codes(%User{} = user) do def generate_backup_codes(%User{} = user) do
with codes <- BackupCodes.generate(), with codes <- BackupCodes.generate(),
hashed_codes <- Enum.map(codes, &Pbkdf2.hash_pwd_salt/1), hashed_codes <- Enum.map(codes, &Pleroma.Password.Pbkdf2.hash_pwd_salt/1),
changeset <- Changeset.cast_backup_codes(user, hashed_codes), changeset <- Changeset.cast_backup_codes(user, hashed_codes),
{:ok, _} <- User.update_and_set_cache(changeset) do {:ok, _} <- User.update_and_set_cache(changeset) do
{:ok, codes} {:ok, codes}

View file

@ -0,0 +1,55 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Password.Pbkdf2 do
@moduledoc """
This module implements Pbkdf2 passwords in terms of Plug.Crypto.
"""
alias Plug.Crypto.KeyGenerator
def decode64(str) do
str
|> String.replace(".", "+")
|> Base.decode64!(padding: false)
end
def encode64(bin) do
bin
|> Base.encode64(padding: false)
|> String.replace("+", ".")
end
def verify_pass(password, hash) do
["pbkdf2-" <> digest, iterations, salt, hash] = String.split(hash, "$", trim: true)
salt = decode64(salt)
iterations = String.to_integer(iterations)
digest = String.to_atom(digest)
binary_hash =
KeyGenerator.generate(password, salt, digest: digest, iterations: iterations, length: 64)
encode64(binary_hash) == hash
end
def hash_pwd_salt(password, opts \\ []) do
salt =
Keyword.get_lazy(opts, :salt, fn ->
:crypto.strong_rand_bytes(16)
end)
digest = Keyword.get(opts, :digest, :sha512)
iterations =
Keyword.get(opts, :iterations, Pleroma.Config.get([:password, :iterations], 160_000))
binary_hash =
KeyGenerator.generate(password, salt, digest: digest, iterations: iterations, length: 64)
"$pbkdf2-#{digest}$#{iterations}$#{encode64(salt)}$#{encode64(binary_hash)}"
end
end

View file

@ -2187,7 +2187,7 @@ def get_ap_ids_by_nicknames(nicknames) do
defp put_password_hash( defp put_password_hash(
%Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
) do ) do
change(changeset, password_hash: Pbkdf2.hash_pwd_salt(password)) change(changeset, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password))
end end
defp put_password_hash(changeset), do: changeset defp put_password_hash(changeset), do: changeset

View file

@ -48,7 +48,7 @@ def checkpw(password, "$2" <> _ = password_hash) do
end end
def checkpw(password, "$pbkdf2" <> _ = password_hash) do def checkpw(password, "$pbkdf2" <> _ = password_hash) do
Pbkdf2.verify_pass(password, password_hash) Pleroma.Password.Pbkdf2.verify_pass(password, password_hash)
end end
def checkpw(_password, _password_hash) do def checkpw(_password, _password_hash) do

View file

@ -125,7 +125,6 @@ defp deps do
{:postgrex, ">= 0.15.5"}, {:postgrex, ">= 0.15.5"},
{:oban, "~> 2.1.0"}, {:oban, "~> 2.1.0"},
{:gettext, "~> 0.18"}, {:gettext, "~> 0.18"},
{:pbkdf2_elixir, "~> 1.2"},
{:bcrypt_elixir, "~> 2.2"}, {:bcrypt_elixir, "~> 2.2"},
{:trailing_format_plug, "~> 0.0.7"}, {:trailing_format_plug, "~> 0.0.7"},
{:fast_sanitize, "~> 0.2.0"}, {:fast_sanitize, "~> 0.2.0"},

View file

@ -30,8 +30,8 @@ test "returns backup codes" do
{:ok, [code1, code2]} = MFA.generate_backup_codes(user) {:ok, [code1, code2]} = MFA.generate_backup_codes(user)
updated_user = refresh_record(user) updated_user = refresh_record(user)
[hash1, hash2] = updated_user.multi_factor_authentication_settings.backup_codes [hash1, hash2] = updated_user.multi_factor_authentication_settings.backup_codes
assert Pbkdf2.verify_pass(code1, hash1) assert Pleroma.Password.Pbkdf2.verify_pass(code1, hash1)
assert Pbkdf2.verify_pass(code2, hash2) assert Pleroma.Password.Pbkdf2.verify_pass(code2, hash2)
end end
end end

View file

@ -0,0 +1,35 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Password.Pbkdf2Test do
use Pleroma.DataCase, async: true
alias Pleroma.Password.Pbkdf2, as: Password
test "it generates the same hash as pbkd2_elixir" do
# hash = Pbkdf2.hash_pwd_salt("password")
hash =
"$pbkdf2-sha512$1$QJpEYw8iBKcnY.4Rm0eCVw$UBPeWQ91RxSv3snxsb/ZzMeG/2aa03c541bbo8vQudREGNta5t8jBQrd00fyJp8RjaqfvgdZxy2rhSwljyu21g"
# Use the same randomly generated salt
salt = Password.decode64("QJpEYw8iBKcnY.4Rm0eCVw")
assert hash == Password.hash_pwd_salt("password", salt: salt)
end
@tag skip: "Works when Pbkd2 is present. Source: trust me bro"
test "Pbkdf2 can verify passwords generated with it" do
# Commented to prevent warnings.
# hash = Password.hash_pwd_salt("password")
# assert Pbkdf2.verify_pass("password", hash)
end
test "it verifies pbkdf2_elixir hashes" do
# hash = Pbkdf2.hash_pwd_salt("password")
hash =
"$pbkdf2-sha512$1$QJpEYw8iBKcnY.4Rm0eCVw$UBPeWQ91RxSv3snxsb/ZzMeG/2aa03c541bbo8vQudREGNta5t8jBQrd00fyJp8RjaqfvgdZxy2rhSwljyu21g"
assert Password.verify_pass("password", hash)
end
end

View file

@ -11,7 +11,7 @@ test "with HTTP Basic Auth used, grants access to OAuth scope-restricted endpoin
conn: conn conn: conn
} do } do
user = insert(:user) user = insert(:user)
assert Pbkdf2.verify_pass("test", user.password_hash) assert Pleroma.Password.Pbkdf2.verify_pass("test", user.password_hash)
basic_auth_contents = basic_auth_contents =
(URI.encode_www_form(user.nickname) <> ":" <> URI.encode_www_form("test")) (URI.encode_www_form(user.nickname) <> ":" <> URI.encode_www_form("test"))

View file

@ -11,7 +11,13 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticatorTest do
setup do setup do
password = "testpassword" password = "testpassword"
name = "AgentSmith" name = "AgentSmith"
user = insert(:user, nickname: name, password_hash: Pbkdf2.hash_pwd_salt(password))
user =
insert(:user,
nickname: name,
password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password)
)
{:ok, [user: user, name: name, password: password]} {:ok, [user: user, name: name, password: password]}
end end

View file

@ -34,7 +34,7 @@ test "checks backup codes" do
hashed_codes = hashed_codes =
backup_codes backup_codes
|> Enum.map(&Pbkdf2.hash_pwd_salt(&1)) |> Enum.map(&Pleroma.Password.Pbkdf2.hash_pwd_salt(&1))
user = user =
insert(:user, insert(:user,

View file

@ -41,13 +41,13 @@ test "/user_exists", %{conn: conn} do
end end
test "/check_password", %{conn: conn} do test "/check_password", %{conn: conn} do
user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt("cool")) user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("cool"))
_deactivated_user = _deactivated_user =
insert(:user, insert(:user,
nickname: "konata", nickname: "konata",
deactivated: true, deactivated: true,
password_hash: Pbkdf2.hash_pwd_salt("cool") password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("cool")
) )
res = res =

View file

@ -18,7 +18,7 @@ defmodule Pleroma.Web.OAuth.LDAPAuthorizationTest do
@tag @skip @tag @skip
test "authorizes the existing user using LDAP credentials" do test "authorizes the existing user using LDAP credentials" do
password = "testpassword" password = "testpassword"
user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt(password)) user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password))
app = insert(:oauth_app, scopes: ["read", "write"]) app = insert(:oauth_app, scopes: ["read", "write"])
host = Pleroma.Config.get([:ldap, :host]) |> to_charlist host = Pleroma.Config.get([:ldap, :host]) |> to_charlist
@ -101,7 +101,7 @@ test "creates a new user after successful LDAP authorization" do
@tag @skip @tag @skip
test "disallow authorization for wrong LDAP credentials" do test "disallow authorization for wrong LDAP credentials" do
password = "testpassword" password = "testpassword"
user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt(password)) user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password))
app = insert(:oauth_app, scopes: ["read", "write"]) app = insert(:oauth_app, scopes: ["read", "write"])
host = Pleroma.Config.get([:ldap, :host]) |> to_charlist host = Pleroma.Config.get([:ldap, :host]) |> to_charlist

View file

@ -20,7 +20,7 @@ defmodule Pleroma.Web.OAuth.MFAControllerTest do
insert(:user, insert(:user,
multi_factor_authentication_settings: %MFA.Settings{ multi_factor_authentication_settings: %MFA.Settings{
enabled: true, enabled: true,
backup_codes: [Pbkdf2.hash_pwd_salt("test-code")], backup_codes: [Pleroma.Password.Pbkdf2.hash_pwd_salt("test-code")],
totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
} }
) )
@ -246,7 +246,7 @@ test "returns access token with valid code", %{conn: conn, app: app} do
hashed_codes = hashed_codes =
backup_codes backup_codes
|> Enum.map(&Pbkdf2.hash_pwd_salt(&1)) |> Enum.map(&Pleroma.Password.Pbkdf2.hash_pwd_salt(&1))
user = user =
insert(:user, insert(:user,

View file

@ -316,7 +316,7 @@ test "with valid params, POST /oauth/register?op=connect redirects to `redirect_
app: app, app: app,
conn: conn conn: conn
} do } do
user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt("testpassword")) user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("testpassword"))
registration = insert(:registration, user: nil) registration = insert(:registration, user: nil)
redirect_uri = OAuthController.default_redirect_uri(app) redirect_uri = OAuthController.default_redirect_uri(app)
@ -347,7 +347,7 @@ test "with unlisted `redirect_uri`, POST /oauth/register?op=connect results in H
app: app, app: app,
conn: conn conn: conn
} do } do
user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt("testpassword")) user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("testpassword"))
registration = insert(:registration, user: nil) registration = insert(:registration, user: nil)
unlisted_redirect_uri = "http://cross-site-request.com" unlisted_redirect_uri = "http://cross-site-request.com"
@ -790,7 +790,7 @@ test "issues a token for an all-body request" do
test "issues a token for `password` grant_type with valid credentials, with full permissions by default" do test "issues a token for `password` grant_type with valid credentials, with full permissions by default" do
password = "testpassword" password = "testpassword"
user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt(password)) user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password))
app = insert(:oauth_app, scopes: ["read", "write"]) app = insert(:oauth_app, scopes: ["read", "write"])
@ -818,7 +818,7 @@ test "issues a mfa token for `password` grant_type, when MFA enabled" do
user = user =
insert(:user, insert(:user,
password_hash: Pbkdf2.hash_pwd_salt(password), password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password),
multi_factor_authentication_settings: %MFA.Settings{ multi_factor_authentication_settings: %MFA.Settings{
enabled: true, enabled: true,
totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
@ -927,7 +927,7 @@ test "rejects token exchange for valid credentials belonging to unconfirmed user
password = "testpassword" password = "testpassword"
{:ok, user} = {:ok, user} =
insert(:user, password_hash: Pbkdf2.hash_pwd_salt(password)) insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password))
|> User.confirmation_changeset(need_confirmation: true) |> User.confirmation_changeset(need_confirmation: true)
|> User.update_and_set_cache() |> User.update_and_set_cache()
@ -955,7 +955,7 @@ test "rejects token exchange for valid credentials belonging to deactivated user
user = user =
insert(:user, insert(:user,
password_hash: Pbkdf2.hash_pwd_salt(password), password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password),
deactivated: true deactivated: true
) )
@ -983,7 +983,7 @@ test "rejects token exchange for user with password_reset_pending set to true" d
user = user =
insert(:user, insert(:user,
password_hash: Pbkdf2.hash_pwd_salt(password), password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password),
password_reset_pending: true password_reset_pending: true
) )
@ -1012,7 +1012,7 @@ test "rejects token exchange for user with confirmation_pending set to true" do
user = user =
insert(:user, insert(:user,
password_hash: Pbkdf2.hash_pwd_salt(password), password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password),
confirmation_pending: true confirmation_pending: true
) )
@ -1038,7 +1038,11 @@ test "rejects token exchange for user with confirmation_pending set to true" do
test "rejects token exchange for valid credentials belonging to an unapproved user" do test "rejects token exchange for valid credentials belonging to an unapproved user" do
password = "testpassword" password = "testpassword"
user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt(password), approval_pending: true) user =
insert(:user,
password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password),
approval_pending: true
)
refute Pleroma.User.account_status(user) == :active refute Pleroma.User.account_status(user) == :active

View file

@ -17,7 +17,7 @@ defmodule Pleroma.Web.Plugs.AuthenticationPlugTest do
user = %User{ user = %User{
id: 1, id: 1,
name: "dude", name: "dude",
password_hash: Pbkdf2.hash_pwd_salt("guy") password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("guy")
} }
conn = conn =

View file

@ -92,7 +92,7 @@ test "it returns HTTP 200", %{conn: conn} do
assert response =~ "<h2>Password changed!</h2>" assert response =~ "<h2>Password changed!</h2>"
user = refresh_record(user) user = refresh_record(user)
assert Pbkdf2.verify_pass("test", user.password_hash) assert Pleroma.Password.Pbkdf2.verify_pass("test", user.password_hash)
assert Enum.empty?(Token.get_user_tokens(user)) assert Enum.empty?(Token.get_user_tokens(user))
end end

View file

@ -397,7 +397,7 @@ test "with proper permissions, valid password and matching new password and conf
assert json_response(conn, 200) == %{"status" => "success"} assert json_response(conn, 200) == %{"status" => "success"}
fetched_user = User.get_cached_by_id(user.id) fetched_user = User.get_cached_by_id(user.id)
assert Pbkdf2.verify_pass("newpass", fetched_user.password_hash) == true assert Pleroma.Password.Pbkdf2.verify_pass("newpass", fetched_user.password_hash) == true
end end
end end

View file

@ -7,7 +7,7 @@ def build(data \\ %{}) do
email: "test@example.org", email: "test@example.org",
name: "Test Name", name: "Test Name",
nickname: "testname", nickname: "testname",
password_hash: Pbkdf2.hash_pwd_salt("test"), password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("test"),
bio: "A tester.", bio: "A tester.",
ap_id: "some id", ap_id: "some id",
last_digest_emailed_at: NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second), last_digest_emailed_at: NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second),

View file

@ -29,7 +29,7 @@ def user_factory(attrs \\ %{}) do
name: sequence(:name, &"Test テスト User #{&1}"), name: sequence(:name, &"Test テスト User #{&1}"),
email: sequence(:email, &"user#{&1}@example.com"), email: sequence(:email, &"user#{&1}@example.com"),
nickname: sequence(:nickname, &"nick#{&1}"), nickname: sequence(:nickname, &"nick#{&1}"),
password_hash: Pbkdf2.hash_pwd_salt("test"), password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("test"),
bio: sequence(:bio, &"Tester Number #{&1}"), bio: sequence(:bio, &"Tester Number #{&1}"),
is_discoverable: true, is_discoverable: true,
last_digest_emailed_at: NaiveDateTime.utc_now(), last_digest_emailed_at: NaiveDateTime.utc_now(),