E-MailRelay
gsaslserverbasic.cpp
Go to the documentation of this file.
1//
2// Copyright (C) 2001-2023 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
60private:
61 bool m_first_apply ;
62 const SaslServerSecrets & m_secrets ;
63 G::StringArray m_mechanisms_secure ;
64 G::StringArray m_mechanisms_insecure ;
65 std::string m_mechanism ;
66 std::string m_challenge ;
67 std::string m_challenge_domain ;
68 bool m_authenticated ;
69 std::string m_id ;
70 std::string m_trustee ;
71 static G::string_view login_challenge_1 ;
72 static G::string_view login_challenge_2 ;
73} ;
74
75G::string_view GAuth::SaslServerBasicImp::login_challenge_1 {"Username:",9U} ;
76G::string_view GAuth::SaslServerBasicImp::login_challenge_2 {"Password:",9U} ;
77
78// ===
79
80GAuth::SaslServerBasicImp::SaslServerBasicImp( const SaslServerSecrets & secrets ,
81 bool with_apop , const std::string & config , const std::string & challenge_domain ) :
82 m_first_apply(true) ,
83 m_secrets(secrets) ,
84 m_challenge_domain(challenge_domain) ,
85 m_authenticated(false)
86{
87 // prepare a list of mechanisms, but remove any that are completely unusable
88 G::StringArray mechanisms ;
89 {
90 // if there are any plain secrets then all mechanisms are usable
91 if( secrets.contains( "PLAIN" , {} ) )
92 {
93 mechanisms = Cram::hashTypes( "CRAM-" ) ;
94 mechanisms.push_back( "PLAIN" ) ;
95 mechanisms.push_back( "LOGIN" ) ;
96 }
97 else
98 {
99 // if there are any CRAM-X secrets then enable the CRAM-X mechansism
100 for( const auto & cram : Cram::hashTypes({},true) )
101 {
102 if( secrets.contains( cram , {} ) )
103 mechanisms.push_back( std::string("CRAM-").append(cram) ) ;
104 }
105 }
106 if( with_apop )
107 mechanisms.push_back( "APOP" ) ;
108 }
109
110 m_mechanisms_secure = mechanisms ;
111 m_mechanisms_insecure = mechanisms ;
112
113 // RFC-4954 4 p6 -- PLAIN is always an option when secure
114 if( m_mechanisms_secure.empty() && secrets.valid() )
115 {
116 m_mechanisms_secure.push_back( "PLAIN" ) ;
117 }
118
119 // apply the allow/deny configuration
120 //
121 // backwards compatibility if no A or D:
122 // M: for secure/insecure (M)echanisms to allow
123 // X: for secure/insecure mechanisms to e(X)clude
124 //
125 // new behaviour:
126 // M: for insecure (M)echanisms to allow
127 // X: for insecure mechanisms to e(X)clude
128 // A: for the secure (A)llow list
129 // D: for the secure (D)eny list
130 //
131 // eg: "m:;a:plain,login"
132 //
133 G::StringArray config_list = G::Str::splitIntoTokens( G::Str::upper(config) , ";" ) ;
134 bool have_m = G::StringList::headMatch( config_list , "M:" ) ;
135 bool have_a = G::StringList::headMatch( config_list , "A:" ) ;
136 bool have_d = G::StringList::headMatch( config_list , "D:" ) ;
137 std::string m = G::StringList::headMatchResidue( config_list , "M:" ) ;
138 std::string x = G::StringList::headMatchResidue( config_list , "X:" ) ;
139 std::string a = G::StringList::headMatchResidue( config_list , "A:" ) ;
140 std::string d = G::StringList::headMatchResidue( config_list , "D:" ) ;
141 G::optional<std::string> m_( have_m , m ) ;
142 G::optional<std::string> a_( have_a , a ) ;
143 if( have_a || have_d )
144 {
145 G::StringList::Filter(m_mechanisms_insecure).allow(m_).deny(x) ;
146 G::StringList::Filter(m_mechanisms_secure).allow(a_).deny(d) ;
147 }
148 else
149 {
150 G::StringList::Filter(m_mechanisms_insecure).allow(m_).deny(x) ;
151 G::StringList::Filter(m_mechanisms_secure).allow(m_).deny(x) ;
152 }
153}
154
155void GAuth::SaslServerBasicImp::reset()
156{
157 m_first_apply = true ;
158 m_authenticated = false ;
159 m_id.clear() ;
160 m_trustee.clear() ;
161 m_challenge.clear() ;
162 m_mechanism.clear() ;
163}
164
165G::StringArray GAuth::SaslServerBasicImp::mechanisms( bool secure ) const
166{
167 return secure ? m_mechanisms_secure : m_mechanisms_insecure ;
168}
169
170bool GAuth::SaslServerBasicImp::init( bool secure , const std::string & mechanism_in )
171{
172 reset() ;
173
174 std::string mechanism = G::Str::upper( mechanism_in ) ;
175 const G::StringArray & mechanisms = secure ? m_mechanisms_secure : m_mechanisms_insecure ;
176
177 if( mechanism.empty() || std::find(mechanisms.begin(),mechanisms.end(),mechanism) == mechanisms.end() )
178 {
179 G_DEBUG( "GAuth::SaslServerBasicImp::init: requested mechanism [" << mechanism << "] is not in our list" ) ;
180 return false ;
181 }
182 else if( mechanism == "APOP" || mechanism.find("CRAM-") == 0U )
183 {
184 m_mechanism = mechanism ;
185 m_challenge = Cram::challenge( G::Random::rand() , m_challenge_domain ) ;
186 return true ;
187 }
188 else
189 {
190 m_mechanism = mechanism ;
191 return true ;
192 }
193}
194
195std::string GAuth::SaslServerBasicImp::preferredMechanism( bool secure ) const
196{
197 if( !m_id.empty() )
198 {
199 auto mechanism_list = mechanisms( secure ) ;
200 std::reverse( mechanism_list.begin() , mechanism_list.end() ) ;
201 for( const auto & m : mechanism_list )
202 {
203 if( G::Str::headMatch( m , "CRAM-" ) )
204 {
205 std::string type = G::Str::lower( m.substr(5U) ) ; // eg. "sha1"
206 if( m_secrets.contains( type , m_id ) )
207 return m ;
208 }
209 }
210 }
211 return std::string() ;
212}
213
214std::string GAuth::SaslServerBasicImp::initialChallenge() const
215{
216 // see RFC-4422 section 5
217 if( m_mechanism == "PLAIN" ) // "client-first"
218 return std::string() ;
219 else if( m_mechanism == "LOGIN" ) // "variable"
220 return G::sv_to_string(login_challenge_1) ;
221 else // APOP/CRAM-X "server-first"
222 return m_challenge ;
223}
224
225std::string GAuth::SaslServerBasicImp::apply( const std::string & response , bool & done )
226{
227 G_DEBUG( "GAuth::SaslServerBasic::apply: response: \"" << G::Str::printable(response) << "\"" ) ;
228
229 bool first_apply = m_first_apply ;
230 m_first_apply = false ;
231
232 done = false ;
233 std::string id ;
234 Secret secret = Secret::none() ;
235 std::string next_challenge ;
236
237 if( m_mechanism.find("CRAM-") == 0U || m_mechanism == "APOP" )
238 {
239 id = Cram::id( response ) ;
240 secret = Secret::none() ;
241 if( !id.empty() )
242 {
243 if( m_mechanism == "APOP" )
244 {
245 secret = m_secrets.serverSecret( "plain"_sv , id ) ; // (APOP is MD5 but not HMAC)
246 }
247 else
248 {
249 std::string hash_type = m_mechanism.substr(5U) ;
250 secret = m_secrets.serverSecret( hash_type , id ) ;
251 if( !secret.valid() )
252 secret = m_secrets.serverSecret( "plain"_sv , id ) ;
253 }
254 }
255 if( !secret.valid() )
256 {
257 m_authenticated = false ;
258 }
259 else
260 {
261 m_id = id ;
262 m_authenticated =
263 m_mechanism == "APOP" ?
264 Cram::validate( "MD5" , false , secret , m_challenge , response ) :
265 Cram::validate( m_mechanism.substr(5U) , true , secret , m_challenge , response ) ;
266 }
267 done = true ;
268 }
269 else if( m_mechanism == "PLAIN" )
270 {
271 // PLAIN has a single response containing three nul-separated fields
272 G::string_view sep( "\0" , 1U ) ;
273 G::string_view id_pwd_in = G::Str::tailView( response , sep ) ;
274 id = G::Str::head( id_pwd_in , sep ) ;
275 G::string_view pwd_in = G::Str::tailView( id_pwd_in , sep ) ;
276 secret = m_secrets.serverSecret( "plain"_sv , id ) ;
277
278 m_authenticated = secret.valid() && !id.empty() && !pwd_in.empty() && pwd_in == secret.secret() ;
279 m_id = id ;
280 done = true ;
281 }
282 else if( first_apply ) // LOGIN username
283 {
284 // LOGIN uses two prompts; the first response is the username and the second is the password
285 G_ASSERT( m_mechanism == "LOGIN" ) ;
286 id = m_id = response ;
287 if( !m_id.empty() )
288 next_challenge = G::sv_to_string(login_challenge_2) ;
289 }
290 else // LOGIN password
291 {
292 G_ASSERT( m_mechanism == "LOGIN" ) ;
293 id = m_id ;
294 secret = m_secrets.serverSecret( "plain"_sv , m_id ) ;
295 m_authenticated = secret.valid() && !response.empty() && response == secret.secret() ;
296 done = true ;
297 }
298
299 if( done )
300 {
301 std::ostringstream ss ;
302 ss
303 << (m_authenticated?"successful":"failed") << " authentication of remote client using mechanism ["
304 << G::Str::lower(m_mechanism) << "] and " << secret.info(id) ;
305 if( m_authenticated )
306 G_LOG( "GAuth::SaslServerBasicImp::apply: " << ss.str() ) ;
307 else
308 G_WARNING( "GAuth::SaslServerBasicImp::apply: " << ss.str() ) ;
309 }
310
311 return next_challenge ;
312}
313
314bool GAuth::SaslServerBasicImp::trusted( const G::StringArray & address_wildcards , const std::string & address_display ) const
315{
316 return std::any_of( address_wildcards.cbegin() , address_wildcards.cend() ,
317 [&](const std::string &wca){return trustedCore(wca,address_display);} ) ;
318}
319
320bool GAuth::SaslServerBasicImp::trustedCore( const std::string & address_wildcard , const std::string & address_display ) const
321{
322 G_DEBUG( "GAuth::SaslServerBasicImp::trustedCore: \"" << address_wildcard << "\", \"" << address_display << "\"" ) ;
323 std::pair<std::string,std::string> pair = m_secrets.serverTrust( address_wildcard ) ;
324 std::string & trustee = pair.first ;
325 if( !trustee.empty() )
326 {
327 G_LOG( "GAuth::SaslServer::trusted: trusting [" << address_display << "]: "
328 << "matched [" << address_wildcard << "] from " << pair.second ) ;
329 const_cast<SaslServerBasicImp*>(this)->m_trustee = trustee ;
330 return true ;
331 }
332 else
333 {
334 return false ;
335 }
336}
337
338std::string GAuth::SaslServerBasicImp::mechanism() const
339{
340 return m_mechanism ;
341}
342
343std::string GAuth::SaslServerBasicImp::id() const
344{
345 return m_authenticated ? m_id : m_trustee ;
346}
347
348bool GAuth::SaslServerBasicImp::authenticated() const
349{
350 return m_authenticated ;
351}
352
353// ===
354
356 const std::string & config , const std::string & challenge_domain ) :
357 m_imp(std::make_unique<SaslServerBasicImp>(secrets,with_apop,config,challenge_domain))
358{
359}
360
361GAuth::SaslServerBasic::~SaslServerBasic()
362= default ;
363
364G::StringArray GAuth::SaslServerBasic::mechanisms( bool secure ) const
365{
366 return m_imp->mechanisms( secure ) ;
367}
368
369std::string GAuth::SaslServerBasic::mechanism() const
370{
371 return m_imp->mechanism() ;
372}
373
374std::string GAuth::SaslServerBasic::preferredMechanism( bool secure ) const
375{
376 return m_imp->preferredMechanism( secure ) ;
377}
378
379bool GAuth::SaslServerBasic::trusted( const G::StringArray & address_wildcards ,
380 const std::string & address_display ) const
381{
382 return m_imp->trusted( address_wildcards , address_display ) ;
383}
384
385void GAuth::SaslServerBasic::reset()
386{
387 return m_imp->reset() ;
388}
389
390bool GAuth::SaslServerBasic::init( bool secure , const std::string & mechanism )
391{
392 return m_imp->init( secure , mechanism ) ;
393}
394
395bool GAuth::SaslServerBasic::mustChallenge() const
396{
397 const bool plain = G::Str::imatch( m_imp->mechanism() , "PLAIN" ) ;
398 const bool login = !plain && G::Str::imatch( m_imp->mechanism() , "LOGIN" ) ;
399 return !plain && !login ;
400}
401
402std::string GAuth::SaslServerBasic::initialChallenge() const
403{
404 return m_imp->initialChallenge() ;
405}
406
407std::string GAuth::SaslServerBasic::apply( const std::string & response , bool & done )
408{
409 return m_imp->apply( response , done ) ;
410}
411
412bool GAuth::SaslServerBasic::authenticated() const
413{
414 return m_imp->authenticated() ;
415}
416
417std::string GAuth::SaslServerBasic::id() const
418{
419 return m_imp->id() ;
420}
421
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 string_view tailView(string_view in, std::size_t pos, string_view default_={}) noexcept
Like tail() but returning a view into the input string.
Definition: gstr.cpp:1340
static bool imatch(char, char) noexcept
Returns true if the two characters are the same, ignoring seven-bit case.
Definition: gstr.cpp:1418
static void splitIntoTokens(const std::string &in, StringArray &out, string_view ws, char esc='\0')
Splits the string into 'ws'-delimited tokens.
Definition: gstr.cpp:1122
static std::string lower(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:827
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:916
static std::string upper(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:839
static std::string head(string_view in, std::size_t pos, string_view default_={})
Returns the first part of the string up to just before the given position.
Definition: gstr.cpp:1297
static bool headMatch(const std::string &in, string_view head) noexcept
Returns true if the string has the given start (or head is empty).
Definition: gstr.cpp:1362
A class like c++17's std::string_view.
Definition: gstringview.h:51
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
std::string headMatchResidue(const StringArray &list, string_view head)
Returns the unmatched part of the first string in the array that has the given start.
Definition: gstringlist.cpp:92
bool headMatch(const StringArray &list, string_view head)
Returns true if any string in the array has the given start (or 'head' is empty).
Definition: gstringlist.cpp:78
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