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

openSUSE Build Service is sponsored by