E-MailRelay
gsaslserverbasic.cpp
Go to the documentation of this file.
1//
2// Copyright (C) 2001-2024 Graeme Walker <graeme_walker@users.sourceforge.net>
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program. If not, see <http://www.gnu.org/licenses/>.
16// ===
17///
18/// \file gsaslserverbasic.cpp
19///
20
21#include "gdef.h"
22#include "gsaslserverbasic.h"
23#include "gmd5.h"
24#include "ghash.h"
25#include "gcram.h"
26#include "gbase64.h"
27#include "gstr.h"
28#include "gtest.h"
29#include "gdatetime.h"
30#include "grandom.h"
31#include "gstringview.h"
32#include "gstringlist.h"
33#include "glog.h"
34#include "gassert.h"
35#include <sstream>
36#include <algorithm>
37#include <functional>
38#include <utility>
39
40//| \class GAuth::SaslServerBasicImp
41/// A private pimple-pattern implementation class used by GAuth::SaslServerBasic.
42///
43class GAuth::SaslServerBasicImp
44{
45public:
46 SaslServerBasicImp( const SaslServerSecrets & , bool ,
47 const std::string & config , const std::string & challenge_domain ) ;
48 G::StringArray mechanisms( bool ) const ;
49 void reset() ;
50 bool init( bool , const std::string & mechanism ) ;
51 std::string mechanism() const ;
52 std::string preferredMechanism( bool ) const ;
53 std::string initialChallenge() const ;
54 std::string apply( const std::string & response , bool & done ) ;
55 bool trusted( const G::StringArray & , const std::string & ) const ;
56 bool trustedCore( const std::string & , const std::string & ) const ;
57 std::string id() const ;
58 bool authenticated() const ;
59
60public:
61 ~SaslServerBasicImp() = default ;
62 SaslServerBasicImp( const SaslServerBasicImp & ) = delete ;
63 SaslServerBasicImp( SaslServerBasicImp && ) = delete ;
64 SaslServerBasicImp & operator=( const SaslServerBasicImp & ) = delete ;
65 SaslServerBasicImp & operator=( SaslServerBasicImp && ) = delete ;
66
67private:
68 bool m_first_apply {true} ;
69 const SaslServerSecrets & m_secrets ;
70 G::StringArray m_mechanisms_secure ;
71 G::StringArray m_mechanisms_insecure ;
72 std::string m_mechanism ;
73 std::string m_challenge ;
74 std::string m_challenge_domain ;
75 bool m_authenticated {false} ;
76 std::string m_id ;
77 std::string m_trustee ;
78 static std::string_view login_challenge_1 ;
79 static std::string_view login_challenge_2 ;
80} ;
81
82std::string_view GAuth::SaslServerBasicImp::login_challenge_1 {"Username:",9U} ;
83std::string_view GAuth::SaslServerBasicImp::login_challenge_2 {"Password:",9U} ;
84
85// ===
86
87GAuth::SaslServerBasicImp::SaslServerBasicImp( const SaslServerSecrets & secrets ,
88 bool with_apop , const std::string & config , const std::string & challenge_domain ) :
89 m_secrets(secrets) ,
90 m_challenge_domain(challenge_domain)
91{
92 // prepare a list of mechanisms, but remove any that are completely unusable
93 G::StringArray mechanisms ;
94 {
95 // if there are any plain secrets then all mechanisms are usable
96 if( secrets.contains( "PLAIN" , {} ) )
97 {
98 mechanisms = Cram::hashTypes( "CRAM-" ) ;
99 mechanisms.emplace_back( "PLAIN" ) ;
100 mechanisms.emplace_back( "LOGIN" ) ;
101 }
102 else
103 {
104 // if there are any CRAM-X secrets then enable the CRAM-X mechansism
105 for( const auto & cram : Cram::hashTypes({},true) )
106 {
107 if( secrets.contains( cram , {} ) )
108 mechanisms.push_back( std::string("CRAM-").append(cram) ) ;
109 }
110 }
111 if( with_apop )
112 mechanisms.emplace_back( "APOP" ) ;
113 }
114
115 m_mechanisms_secure = mechanisms ;
116 m_mechanisms_insecure = mechanisms ;
117
118 // RFC-4954 4 p6 -- PLAIN is always an option when secure
119 if( m_mechanisms_secure.empty() && secrets.valid() )
120 {
121 m_mechanisms_secure.emplace_back( "PLAIN" ) ;
122 }
123
124 // apply the allow/deny configuration
125 //
126 // backwards compatibility if no A or D:
127 // M: for secure/insecure (M)echanisms to allow
128 // X: for secure/insecure mechanisms to e(X)clude
129 //
130 // new behaviour:
131 // M: for insecure (M)echanisms to allow
132 // X: for insecure mechanisms to e(X)clude
133 // A: for the secure (A)llow list
134 // D: for the secure (D)eny list
135 //
136 // eg: "m:;a:plain,login"
137 //
138 G::StringArray config_list = G::Str::splitIntoTokens( G::Str::upper(config) , ";" ) ;
139 bool have_m = G::StringList::headMatch( config_list , "M:" ) ;
140 bool have_a = G::StringList::headMatch( config_list , "A:" ) ;
141 bool have_d = G::StringList::headMatch( config_list , "D:" ) ;
142 std::string m = G::StringList::headMatchResidue( config_list , "M:" ) ;
143 std::string x = G::StringList::headMatchResidue( config_list , "X:" ) ;
144 std::string a = G::StringList::headMatchResidue( config_list , "A:" ) ;
145 std::string d = G::StringList::headMatchResidue( config_list , "D:" ) ;
146 auto m_ = have_m ? std::optional<std::string>(m) : std::optional<std::string>() ;
147 auto a_ = have_a ? std::optional<std::string>(a) : std::optional<std::string>() ;
148 if( have_a || have_d )
149 {
150 G::StringList::Filter(m_mechanisms_insecure).allow(m_).deny(x) ;
151 G::StringList::Filter(m_mechanisms_secure).allow(a_).deny(d) ;
152 }
153 else
154 {
155 G::StringList::Filter(m_mechanisms_insecure).allow(m_).deny(x) ;
156 G::StringList::Filter(m_mechanisms_secure).allow(m_).deny(x) ;
157 }
158}
159
160void GAuth::SaslServerBasicImp::reset()
161{
162 m_first_apply = true ;
163 m_authenticated = false ;
164 m_id.clear() ;
165 m_trustee.clear() ;
166 m_challenge.clear() ;
167 m_mechanism.clear() ;
168}
169
170G::StringArray GAuth::SaslServerBasicImp::mechanisms( bool secure ) const
171{
172 return secure ? m_mechanisms_secure : m_mechanisms_insecure ;
173}
174
175bool GAuth::SaslServerBasicImp::init( bool secure , const std::string & mechanism_in )
176{
177 reset() ;
178
179 std::string mechanism = G::Str::upper( mechanism_in ) ;
180 const G::StringArray & mechanisms = secure ? m_mechanisms_secure : m_mechanisms_insecure ;
181
182 if( mechanism.empty() || std::find(mechanisms.begin(),mechanisms.end(),mechanism) == mechanisms.end() )
183 {
184 G_DEBUG( "GAuth::SaslServerBasicImp::init: requested mechanism [" << mechanism << "] is not in our list" ) ;
185 return false ;
186 }
187 else if( mechanism == "APOP" || mechanism.find("CRAM-") == 0U )
188 {
189 m_mechanism = mechanism ;
190 m_challenge = Cram::challenge( G::Random::rand() , m_challenge_domain ) ;
191 return true ;
192 }
193 else
194 {
195 m_mechanism = mechanism ;
196 return true ;
197 }
198}
199
200std::string GAuth::SaslServerBasicImp::preferredMechanism( bool secure ) const
201{
202 if( !m_id.empty() )
203 {
204 auto mechanism_list = mechanisms( secure ) ;
205 std::reverse( mechanism_list.begin() , mechanism_list.end() ) ;
206 for( const auto & m : mechanism_list )
207 {
208 if( G::Str::headMatch( m , "CRAM-" ) )
209 {
210 std::string type = G::Str::lower( m.substr(5U) ) ; // eg. "sha1"
211 if( m_secrets.contains( type , m_id ) )
212 return m ;
213 }
214 }
215 }
216 return {} ;
217}
218
219std::string GAuth::SaslServerBasicImp::initialChallenge() const
220{
221 // see RFC-4422 section 5
222 if( m_mechanism == "PLAIN" ) // "client-first"
223 return {} ;
224 else if( m_mechanism == "LOGIN" ) // "variable"
225 return G::sv_to_string(login_challenge_1) ;
226 else // APOP/CRAM-X "server-first"
227 return m_challenge ;
228}
229
230std::string GAuth::SaslServerBasicImp::apply( const std::string & response , bool & done )
231{
232 G_DEBUG( "GAuth::SaslServerBasic::apply: response: \"" << G::Str::printable(response) << "\"" ) ;
233
234 bool first_apply = m_first_apply ;
235 m_first_apply = false ;
236
237 done = false ;
238 std::string id ;
239 Secret secret = Secret::none() ;
240 std::string next_challenge ;
241
242 if( m_mechanism.find("CRAM-") == 0U || m_mechanism == "APOP" )
243 {
244 id = Cram::id( response ) ;
245 secret = Secret::none() ;
246 if( !id.empty() )
247 {
248 if( m_mechanism == "APOP" )
249 {
250 secret = m_secrets.serverSecret( "plain"_sv , id ) ; // (APOP is MD5 but not HMAC)
251 }
252 else
253 {
254 std::string hash_type = m_mechanism.substr(5U) ;
255 secret = m_secrets.serverSecret( hash_type , id ) ;
256 if( !secret.valid() )
257 secret = m_secrets.serverSecret( "plain"_sv , id ) ;
258 }
259 }
260 if( !secret.valid() )
261 {
262 m_authenticated = false ;
263 }
264 else
265 {
266 m_id = id ;
267 m_authenticated =
268 m_mechanism == "APOP" ?
269 Cram::validate( "MD5" , false , secret , m_challenge , response ) :
270 Cram::validate( m_mechanism.substr(5U) , true , secret , m_challenge , response ) ;
271 }
272 done = true ;
273 }
274 else if( m_mechanism == "PLAIN" )
275 {
276 // PLAIN has a single response containing three nul-separated fields
277 std::string_view sep( "\0" , 1U ) ;
278 std::string_view id_pwd_in = G::Str::tailView( response , sep ) ;
279 id = G::Str::head( id_pwd_in , sep ) ;
280 std::string_view pwd_in = G::Str::tailView( id_pwd_in , sep ) ;
281 secret = m_secrets.serverSecret( "plain"_sv , id ) ;
282
283 m_authenticated = secret.valid() && !id.empty() && !pwd_in.empty() && pwd_in == secret.secret() ;
284 m_id = id ;
285 done = true ;
286 }
287 else if( first_apply ) // LOGIN username
288 {
289 // LOGIN uses two prompts; the first response is the username and the second is the password
290 G_ASSERT( m_mechanism == "LOGIN" ) ;
291 id = m_id = response ;
292 if( !m_id.empty() )
293 next_challenge = G::sv_to_string(login_challenge_2) ;
294 }
295 else // LOGIN password
296 {
297 G_ASSERT( m_mechanism == "LOGIN" ) ;
298 id = m_id ;
299 secret = m_secrets.serverSecret( "plain"_sv , m_id ) ;
300 m_authenticated = secret.valid() && !response.empty() && response == secret.secret() ;
301 done = true ;
302 }
303
304 if( done )
305 {
306 std::ostringstream ss ;
307 ss
308 << (m_authenticated?"successful":"failed") << " authentication of remote client using mechanism ["
309 << G::Str::lower(m_mechanism) << "] and " << secret.info(id) ;
310 if( m_authenticated )
311 G_LOG( "GAuth::SaslServerBasicImp::apply: " << ss.str() ) ;
312 else
313 G_WARNING( "GAuth::SaslServerBasicImp::apply: " << ss.str() ) ;
314 }
315
316 return next_challenge ;
317}
318
319bool GAuth::SaslServerBasicImp::trusted( const G::StringArray & address_wildcards , const std::string & address_display ) const
320{
321 return std::any_of( address_wildcards.cbegin() , address_wildcards.cend() ,
322 [&](const std::string &wca){return trustedCore(wca,address_display);} ) ;
323}
324
325bool GAuth::SaslServerBasicImp::trustedCore( const std::string & address_wildcard , const std::string & address_display ) const
326{
327 G_DEBUG( "GAuth::SaslServerBasicImp::trustedCore: \"" << address_wildcard << "\", \"" << address_display << "\"" ) ;
328 std::pair<std::string,std::string> pair = m_secrets.serverTrust( address_wildcard ) ;
329 std::string & trustee = pair.first ;
330 if( !trustee.empty() )
331 {
332 G_LOG( "GAuth::SaslServer::trusted: trusting [" << address_display << "]: "
333 << "matched [" << address_wildcard << "] from " << pair.second ) ;
334 const_cast<SaslServerBasicImp*>(this)->m_trustee = trustee ;
335 return true ;
336 }
337 else
338 {
339 return false ;
340 }
341}
342
343std::string GAuth::SaslServerBasicImp::mechanism() const
344{
345 return m_mechanism ;
346}
347
348std::string GAuth::SaslServerBasicImp::id() const
349{
350 return m_authenticated ? m_id : m_trustee ;
351}
352
353bool GAuth::SaslServerBasicImp::authenticated() const
354{
355 return m_authenticated ;
356}
357
358// ===
359
361 const std::string & config , const std::string & challenge_domain ) :
362 m_imp(std::make_unique<SaslServerBasicImp>(secrets,with_apop,config,challenge_domain))
363{
364}
365
366GAuth::SaslServerBasic::~SaslServerBasic()
367= default ;
368
369G::StringArray GAuth::SaslServerBasic::mechanisms( bool secure ) const
370{
371 return m_imp->mechanisms( secure ) ;
372}
373
374std::string GAuth::SaslServerBasic::mechanism() const
375{
376 return m_imp->mechanism() ;
377}
378
379std::string GAuth::SaslServerBasic::preferredMechanism( bool secure ) const
380{
381 return m_imp->preferredMechanism( secure ) ;
382}
383
384bool GAuth::SaslServerBasic::trusted( const G::StringArray & address_wildcards ,
385 const std::string & address_display ) const
386{
387 return m_imp->trusted( address_wildcards , address_display ) ;
388}
389
390void GAuth::SaslServerBasic::reset()
391{
392 m_imp->reset() ;
393}
394
395bool GAuth::SaslServerBasic::init( bool secure , const std::string & mechanism )
396{
397 return m_imp->init( secure , mechanism ) ;
398}
399
400bool GAuth::SaslServerBasic::mustChallenge() const
401{
402 const bool plain = G::Str::imatch( m_imp->mechanism() , "PLAIN" ) ;
403 const bool login = !plain && G::Str::imatch( m_imp->mechanism() , "LOGIN" ) ;
404 return !plain && !login ;
405}
406
407std::string GAuth::SaslServerBasic::initialChallenge() const
408{
409 return m_imp->initialChallenge() ;
410}
411
412std::string GAuth::SaslServerBasic::apply( const std::string & response , bool & done )
413{
414 return m_imp->apply( response , done ) ;
415}
416
417bool GAuth::SaslServerBasic::authenticated() const
418{
419 return m_imp->authenticated() ;
420}
421
422std::string GAuth::SaslServerBasic::id() const
423{
424 return m_imp->id() ;
425}
426
SaslServerBasic(const SaslServerSecrets &, bool allow_pop, const std::string &config, const std::string &challenge_domain)
Constructor.
An interface used by GAuth::SaslServer to obtain authentication secrets.
static std::string lower(std::string_view)
Returns a copy of 's' in which all seven-bit upper-case characters have been replaced by lower-case c...
Definition: gstr.cpp:824
static void splitIntoTokens(const std::string &in, StringArray &out, std::string_view ws, char esc='\0')
Splits the string into 'ws'-delimited tokens.
Definition: gstr.cpp:1119
static bool imatch(char, char) noexcept
Returns true if the two characters are the same, ignoring seven-bit case.
Definition: gstr.cpp:1415
static bool headMatch(std::string_view in, std::string_view head) noexcept
Returns true if the string has the given start (or head is empty).
Definition: gstr.cpp:1359
static std::string printable(const std::string &in, char escape='\\')
Returns a printable representation of the given input string, using chacter code ranges 0x20 to 0x7e ...
Definition: gstr.cpp:913
static std::string upper(std::string_view)
Returns a copy of 's' in which all seven-bit lower-case characters have been replaced by upper-case c...
Definition: gstr.cpp:836
static std::string_view tailView(std::string_view in, std::size_t pos, std::string_view default_={}) noexcept
Like tail() but returning a view into the input string.
Definition: gstr.cpp:1337
static std::string head(std::string_view in, std::size_t pos, std::string_view default_={})
Returns the first part of the string up to just before the given position.
Definition: gstr.cpp:1294
A class template like a simplified c++17 std::optional.
Definition: goptional.h:50
unsigned int rand(unsigned int start=0U, unsigned int end=32767)
Returns a random value, uniformly distributed over the given range (including 'start' and 'end'),...
Definition: grandom.cpp:28
bool headMatch(const StringArray &list, std::string_view head)
Returns true if any string in the array has the given start (or 'head' is empty).
Definition: gstringlist.cpp:78
std::string headMatchResidue(const StringArray &list, std::string_view head)
Returns the unmatched part of the first string in the array that has the given start.
Definition: gstringlist.cpp:92
std::vector< std::string > StringArray
A std::vector of std::strings.
Definition: gstringarray.h:30
STL namespace.
Filters a list of strings with allow and deny lists.
Definition: gstringlist.h:81