File 5052-ssh-hashed-password-storage-and-compare.patch of Package erlang
From 032d1bc9491a3975c68faf9bc7776115d6ae3005 Mon Sep 17 00:00:00 2001
From: Maria Scott <maria-12648430@hnc-agency.org>
Date: Wed, 11 Feb 2026 13:18:15 +0100
Subject: [PATCH 2/2] ssh: hashed password storage and compare
---
lib/ssh/src/ssh.hrl | 5 +++
lib/ssh/src/ssh_auth.erl | 17 ++++++----
lib/ssh/src/ssh_lib.erl | 23 -------------
lib/ssh/src/ssh_options.erl | 61 +++++++++++++++++++++++++++++++----
lib/ssh/src/ssh_transport.erl | 16 +++++++--
5 files changed, 82 insertions(+), 40 deletions(-)
diff --git a/lib/ssh/src/ssh.hrl b/lib/ssh/src/ssh.hrl
index 239c0e0a11..7540eb9895 100644
--- a/lib/ssh/src/ssh.hrl
+++ b/lib/ssh/src/ssh.hrl
@@ -111,6 +111,11 @@
-define(SSH_CIPHER_3DES, 3).
-define(SSH_CIPHER_AUTHFILE, ?SSH_CIPHER_3DES).
+%% PKBDF2 password hashing parameters
+-define(SSH_PKBDF2_DIGEST, sha256).
+-define(SSH_PKBDF2_ITERATIONS, 600_000).
+-define(SSH_PKBDF2_KEYLENGTH, 32). %% matches digest output length
+
%% Option access macros
-define(do_get_opt(C,K,O), ssh_options:get_value(C,K,O, ?MODULE,?LINE)).
-define(do_get_opt(C,K,O,D), ssh_options:get_value(C,K,O,?LAZY(D),?MODULE,?LINE)).
diff --git a/lib/ssh/src/ssh_auth.erl b/lib/ssh/src/ssh_auth.erl
index 4646df9550..9d9c48f403 100644
--- a/lib/ssh/src/ssh_auth.erl
+++ b/lib/ssh/src/ssh_auth.erl
@@ -236,9 +236,8 @@ handle_userauth_request(#ssh_msg_service_request{name = Name = "ssh-userauth"},
handle_userauth_request(#ssh_msg_userauth_request{user = User,
service = "ssh-connection",
method = "password",
- data = <<?FALSE, ?UINT32(Sz), BinPwd:Sz/binary>>}, _,
+ data = <<?FALSE, ?UINT32(Sz), Password:Sz/binary>>}, _,
#ssh{userauth_supported_methods = Methods} = Ssh) ->
- Password = unicode:characters_to_list(BinPwd),
case check_password(User, Password, Ssh) of
{true,Ssh1} ->
{authorized, User,
@@ -454,7 +453,7 @@ handle_userauth_info_response(#ssh_msg_userauth_info_response{num_responses = 1,
orelse
proplists:get_value(one_empty, ?GET_OPT(tstflg,Opts), false),
- case check_password(User, unicode:characters_to_list(Password), Ssh) of
+ case check_password(User, Password, Ssh) of
{true,Ssh1} when SendOneEmpty==true ->
{authorized_but_one_more, User,
{#ssh_msg_userauth_info_request{name = "",
@@ -516,17 +515,21 @@ check_password(User, Password, #ssh{opts=Opts} = Ssh) ->
end;
undefined ->
- Static = get_password_option(Opts, User),
- {ssh_lib:comp(Password,Static), Ssh};
+ case get_password_option(Opts, User) of
+ Checker when is_function(Checker, 1) ->
+ {Checker(Password), Ssh};
+ _ ->
+ {false, Ssh}
+ end;
Checker when is_function(Checker,2) ->
- {Checker(User, Password), Ssh};
+ {Checker(User, unicode:characters_to_list(Password)), Ssh};
Checker when is_function(Checker,4) ->
#ssh{pwdfun_user_state = PrivateState,
peer = {_,PeerAddr={_,_}}
} = Ssh,
- case Checker(User, Password, PeerAddr, PrivateState) of
+ case Checker(User, unicode:characters_to_list(Password), PeerAddr, PrivateState) of
true ->
{true,Ssh};
false ->
diff --git a/lib/ssh/src/ssh_lib.erl b/lib/ssh/src/ssh_lib.erl
index d6cac06b03..677ab5d657 100644
--- a/lib/ssh/src/ssh_lib.erl
+++ b/lib/ssh/src/ssh_lib.erl
@@ -27,8 +27,7 @@
-export([
format_address_port/2, format_address_port/1,
format_address/1,
- format_time_ms/1,
- comp/2
+ format_time_ms/1
]).
-include("ssh.hrl").
@@ -65,25 +64,3 @@ format_time_ms(T) when is_integer(T) ->
%%%----------------------------------------------------------------
-
-%% Compares X1 and X2 such that X1 (but not X2) is always iterated fully,
-%% ie without returning early on the first difference.
-comp(X1, X2) ->
- comp(X1, X2, 0).
-
-comp(<<B1, R1/binary>>, <<B2, R2/binary>>, Diff) ->
- comp(R1, R2, Diff bor (B1 bxor B2));
-comp(<<B1, R1/binary>>, <<>>, _Diff) ->
- comp(R1, <<>>, 1 bor (B1 bxor 0));
-comp(<<>>, <<>>, Diff) ->
- Diff =:= 0;
-
-comp([H1|T1], [H2|T2], Diff) ->
- comp(T1, T2, Diff bor (H1 bxor H2));
-comp([H1|T1], [], _Diff) ->
- comp(T1, [], 1 bor (H1 bxor 0));
-comp([], [], Diff) ->
- Diff =:= 0;
-
-comp(_X1, _X2, _Diff) ->
- false.
diff --git a/lib/ssh/src/ssh_options.erl b/lib/ssh/src/ssh_options.erl
index d0d73a2f04..4f32e320ef 100644
--- a/lib/ssh/src/ssh_options.erl
+++ b/lib/ssh/src/ssh_options.erl
@@ -286,7 +286,7 @@ check_fun(Key, Defs) ->
#{chk := Fun} = maps:get(Key, Defs),
Fun;
true ->
- fun(_,_) -> forbidden end
+ fun(_) -> forbidden end
end.
%%%================================================================
@@ -483,11 +483,28 @@ default(server) ->
user_passwords =>
#{default => [],
chk => fun(V) ->
- is_list(V) andalso
- lists:all(fun({S1,S2}) ->
- check_string(S1) andalso
- check_string(S2)
- end, V)
+ is_list(V) andalso
+ lists:foldr(
+ fun
+ (_, false) ->
+ false;
+ ({User, Passwd}, {true, Acc}) ->
+ case
+ check_string(User) andalso
+ check_string(Passwd) andalso
+ make_passwd_fun(Passwd)
+ of
+ Fun when is_function(Fun, 1) ->
+ {true, [{User, Fun} | Acc]};
+ _ ->
+ false
+ end;
+ (_, _) ->
+ false
+ end,
+ {true, []},
+ V
+ )
end,
class => user_option
},
@@ -506,7 +523,17 @@ default(server) ->
password =>
#{default => undefined,
- chk => fun(V) -> check_string(V) end,
+ chk => fun(V) ->
+ case
+ check_string(V) andalso
+ make_passwd_fun(V)
+ of
+ Fun when is_function(Fun, 1) ->
+ {true, Fun};
+ _ ->
+ false
+ end
+ end,
class => user_option
},
@@ -911,6 +938,26 @@ default(common) ->
}
}.
+make_passwd_fun(PlainPwd) ->
+ PlainPwdBin = unicode:characters_to_binary(PlainPwd),
+ Salt = crypto:strong_rand_bytes(?SSH_PKBDF2_KEYLENGTH),
+ HashedPwd = crypto:pbkdf2_hmac(?SSH_PKBDF2_DIGEST,
+ PlainPwdBin, Salt,
+ ?SSH_PKBDF2_ITERATIONS,
+ ?SSH_PKBDF2_KEYLENGTH),
+ fun
+ Chk(PlainCheckPwd) when is_binary(PlainCheckPwd) ->
+ HashedCheckPwd = crypto:pbkdf2_hmac(?SSH_PKBDF2_DIGEST,
+ PlainCheckPwd, Salt,
+ ?SSH_PKBDF2_ITERATIONS,
+ ?SSH_PKBDF2_KEYLENGTH),
+ crypto:hash_equals(HashedPwd, HashedCheckPwd);
+ Chk(PlainCheckPwd) when is_list(PlainCheckPwd) ->
+ Chk(unicode:characters_to_binary(PlainCheckPwd));
+ Chk(_) ->
+ false
+ end.
+
%%%================================================================
%%%================================================================
diff --git a/lib/ssh/src/ssh_transport.erl b/lib/ssh/src/ssh_transport.erl
index 47eb31d97f..151aa17368 100644
--- a/lib/ssh/src/ssh_transport.erl
+++ b/lib/ssh/src/ssh_transport.erl
@@ -316,7 +316,12 @@ is_valid_mac(_, _ , #ssh{recv_mac_size = 0}) ->
true;
is_valid_mac(Mac, Data, #ssh{recv_mac = Algorithm,
recv_mac_key = Key, recv_sequence = SeqNum}) ->
- ssh_lib:comp(Mac, mac(Algorithm, Key, SeqNum, Data)).
+ try
+ crypto:hash_equals(Mac, mac(Algorithm, Key, SeqNum, Data))
+ catch
+ _:_ ->
+ false
+ end.
handle_hello_version(Version) ->
try
@@ -1945,11 +1950,13 @@ decrypt(#ssh{decrypt = 'chacha20-poly1305@openssh.com',
%% The length is decrypted separately in a first step
PacketLenBin = crypto:crypto_one_time(chacha20, K1, <<0:8/unit:8, Seq:8/unit:8>>, EncryptedLen, false),
{Ssh, PacketLenBin};
- {AAD,Ctext,Ctag} ->
+ {AAD,Ctext,Ctag} ->
%% The length is already decrypted and used to divide the input
%% Check the mac (important that it is timing-safe):
PolyKey = crypto:crypto_one_time(chacha20, K2, <<0:8/unit:8,Seq:8/unit:8>>, <<0:32/unit:8>>, false),
- case ssh_lib:comp(Ctag, crypto:mac(poly1305, PolyKey, <<AAD/binary,Ctext/binary>>)) of
+ try
+ crypto:hash_equals(Ctag, crypto:mac(poly1305, PolyKey, <<AAD/binary,Ctext/binary>>))
+ of
true ->
%% MAC is ok, decode
IV2 = <<1:8/little-unit:8, Seq:8/unit:8>>,
@@ -1957,6 +1964,9 @@ decrypt(#ssh{decrypt = 'chacha20-poly1305@openssh.com',
{Ssh, PlainText};
false ->
{Ssh,error}
+ catch
+ _:_ ->
+ {Ssh, error}
end
end;
--
2.51.0