E-MailRelay
gsaslserverbasic.cpp
Go to the documentation of this file.
1//
2// Copyright (C) 2001-2021 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 "glog.h"
32#include "gassert.h"
33#include <sstream>
34#include <algorithm>
35#include <functional>
36
37//| \class GAuth::SaslServerBasicImp
38/// A private pimple-pattern implementation class used by GAuth::SaslServerBasic.
39///
41{
42public:
43 SaslServerBasicImp( const SaslServerSecrets & , const std::string & , bool ) ;
44 std::string mechanisms( const std::string & ) const ;
45 bool init( const std::string & mechanism ) ;
46 std::string mechanism() const ;
47 std::string initialChallenge() const ;
48 std::string apply( const std::string & response , bool & done ) ;
49 bool trusted( const GNet::Address & ) const ;
50 bool trustedCore( const std::string & , const GNet::Address & ) const ;
51 bool active() const ;
52 std::string id() const ;
53 bool authenticated() const ;
54
55private:
56 bool m_allow_apop ;
57 bool m_first_apply ;
58 const SaslServerSecrets & m_secrets ;
59 G::StringArray m_mechanisms ; // that have secrets
60 std::string m_mechanism ;
61 std::string m_challenge ;
62 bool m_authenticated ;
63 std::string m_id ;
64 std::string m_trustee ;
65 static const char * login_challenge_1 ;
66 static const char * login_challenge_2 ;
67} ;
68
69const char * GAuth::SaslServerBasicImp::login_challenge_1 = "Username:" ;
70const char * GAuth::SaslServerBasicImp::login_challenge_2 = "Password:" ;
71
72// ===
73
74GAuth::SaslServerBasicImp::SaslServerBasicImp( const SaslServerSecrets & secrets , const std::string & sasl_server_config , bool allow_apop ) :
75 m_allow_apop(allow_apop) ,
76 m_first_apply(true) ,
77 m_secrets(secrets) ,
78 m_authenticated(false)
79{
80 m_mechanisms = Cram::hashTypes( "CRAM-" ) ;
81 m_mechanisms.push_back( "PLAIN" ) ;
82 m_mechanisms.push_back( "LOGIN" ) ;
83 if( G::Test::enabled("sasl-server-oauth") )
84 m_mechanisms.push_back( "XOAUTH2" ) ; // to allow testing of the client-side
85
86 // use the configuration string as a mechanism whitelist and/or blacklist
87 if( !sasl_server_config.empty() )
88 {
89 G::StringArray list = G::Str::splitIntoTokens( G::Str::upper(sasl_server_config) , ";" ) ;
90 G::StringArray whitelist = G::Str::splitIntoTokens( G::Str::headMatchResidue( list , "M:" ) , "," ) ;
91 G::StringArray blacklist = G::Str::splitIntoTokens( G::Str::headMatchResidue( list , "X:" ) , "," ) ;
92 m_mechanisms.erase( G::Str::keepMatch( m_mechanisms.begin() , m_mechanisms.end() , whitelist , true ) , m_mechanisms.end() ) ;
93 m_mechanisms.erase( G::Str::removeMatch( m_mechanisms.begin() , m_mechanisms.end() , blacklist , true ) , m_mechanisms.end() ) ;
94 if( m_mechanisms.empty() )
95 throw SaslServerBasic::NoMechanisms() ;
96 }
97}
98
99std::string GAuth::SaslServerBasicImp::mechanisms( const std::string & sep ) const
100{
101 return G::Str::join( sep , m_mechanisms ) ;
102}
103
104bool GAuth::SaslServerBasicImp::init( const std::string & mechanism_in )
105{
106 m_first_apply = true ;
107 m_authenticated = false ;
108 m_id.erase() ;
109 m_trustee.erase() ;
110 m_challenge.erase() ;
111 m_mechanism.erase() ;
112
113 std::string mechanism = G::Str::upper( mechanism_in ) ;
114 if( m_allow_apop && mechanism == "APOP" )
115 {
116 m_mechanism = mechanism ;
117 m_challenge = Cram::challenge( G::Random::rand() ) ;
118 return true ;
119 }
120 else if( std::find(m_mechanisms.begin(),m_mechanisms.end(),mechanism) == m_mechanisms.end() )
121 {
122 G_DEBUG( "GAuth::SaslServerBasicImp::init: requested mechanism [" << mechanism << "] is not in our list" ) ;
123 return false ;
124 }
125 else if( mechanism.find("CRAM-") == 0U )
126 {
127 m_mechanism = mechanism ;
128 m_challenge = Cram::challenge( G::Random::rand() ) ;
129 return true ;
130 }
131 else
132 {
133 m_mechanism = mechanism ;
134 return true ;
135 }
136}
137
138std::string GAuth::SaslServerBasicImp::initialChallenge() const
139{
140 // see RFC-4422 section 5
141 if( m_mechanism == "PLAIN" ) // "client-first"
142 return std::string() ;
143 else if( m_mechanism == "XOAUTH2" ) // for testing -- "client-first"
144 return std::string() ;
145 else if( m_mechanism == "LOGIN" ) // "variable"
146 return login_challenge_1 ;
147 else // CRAM-X "server-first"
148 return m_challenge ;
149}
150
151std::string GAuth::SaslServerBasicImp::apply( const std::string & response , bool & done )
152{
153 G_DEBUG( "GAuth::SaslServerBasic::apply: response: \"" << G::Str::printable(response) << "\"" ) ;
154
155 bool first_apply = m_first_apply ;
156 m_first_apply = false ;
157
158 done = false ;
159 std::string id ;
160 Secret secret = Secret::none() ;
161 std::string next_challenge ;
162
163 if( m_mechanism.find("CRAM-") == 0U || m_mechanism == "APOP" )
164 {
165 id = Cram::id( response ) ;
166 secret = Secret::none() ;
167 if( !id.empty() )
168 {
169 if( m_mechanism == "APOP" )
170 {
171 secret = m_secrets.serverSecret( "plain" , id ) ; // (APOP is MD5 but not HMAC)
172 }
173 else
174 {
175 std::string hash_type = m_mechanism.substr(5U) ;
176 secret = m_secrets.serverSecret( hash_type , id ) ;
177 if( !secret.valid() )
178 secret = m_secrets.serverSecret( "plain" , id ) ;
179 }
180 }
181 if( !secret.valid() )
182 {
183 m_authenticated = false ;
184 }
185 else
186 {
187 m_id = id ;
188 m_authenticated =
189 m_mechanism == "APOP" ?
190 Cram::validate( "MD5" , false , secret , m_challenge , response ) :
191 Cram::validate( m_mechanism.substr(5U) , true , secret , m_challenge , response ) ;
192 }
193 done = true ;
194 }
195 else if( m_mechanism == "PLAIN" )
196 {
197 // PLAIN has a single response containing three nul-separated fields
198 std::string sep( 1U , '\0' ) ;
199 std::string id_pwd_in = G::Str::tail( response , sep ) ;
200 id = G::Str::head( id_pwd_in , sep ) ;
201 std::string pwd_in = G::Str::tail( id_pwd_in , sep ) ;
202 secret = m_secrets.serverSecret( "plain" , id ) ;
203
204 m_authenticated = secret.valid() && !id.empty() && !pwd_in.empty() && pwd_in == secret.key() ;
205 m_id = id ;
206 done = true ;
207 }
208 else if( m_mechanism == "XOAUTH2" && first_apply ) // for testing
209 {
210 if( G::Test::enabled("sasl-server-oauth-pass") )
211 {
212 m_id = "test" ;
213 m_authenticated = true ;
214 done = true ;
215 }
216 else
217 {
218 next_challenge = "not authenticated, send empty response" ;
219 done = false ;
220 }
221 }
222 else if( m_mechanism == "XOAUTH2" ) // for testing
223 {
224 m_authenticated = false ;
225 done = true ;
226 }
227 else if( first_apply ) // LOGIN username
228 {
229 // LOGIN uses two prompts; the first response is the username and the second is the password
230 G_ASSERT( m_mechanism == "LOGIN" ) ;
231 id = m_id = response ;
232 if( !m_id.empty() )
233 next_challenge = login_challenge_2 ;
234 }
235 else // LOGIN password
236 {
237 G_ASSERT( m_mechanism == "LOGIN" ) ;
238 id = m_id ;
239 secret = m_secrets.serverSecret( "plain" , m_id ) ;
240 m_authenticated = secret.valid() && !response.empty() && response == secret.key() ;
241 done = true ;
242 }
243
244 if( done )
245 {
246 std::ostringstream ss ;
247 ss
248 << (m_authenticated?"successful":"failed") << " authentication of remote client using mechanism ["
249 << G::Str::lower(m_mechanism) << "] and " << secret.info(id) ;
250 if( m_authenticated )
251 G_LOG( "GAuth::SaslServerBasicImp::apply: " << ss.str() ) ;
252 else
253 G_WARNING( "GAuth::SaslServerBasicImp::apply: " << ss.str() ) ;
254 }
255
256 return next_challenge ;
257}
258
259bool GAuth::SaslServerBasicImp::trusted( const GNet::Address & address ) const
260{
261 G_DEBUG( "GAuth::SaslServerBasicImp::trusted: \"" << address.hostPartString() << "\"" ) ;
262 G::StringArray wildcards = address.wildcards() ;
263 return std::any_of( wildcards.cbegin() , wildcards.cend() ,
264 [&](const std::string &wca){return trustedCore(wca,address);} ) ;
265}
266
267bool GAuth::SaslServerBasicImp::trustedCore( const std::string & address_wildcard , const GNet::Address & address ) const
268{
269 G_DEBUG( "GAuth::SaslServerBasicImp::trustedCore: \"" << address_wildcard << "\", \"" << address.hostPartString() << "\"" ) ;
270 std::pair<std::string,std::string> pair = m_secrets.serverTrust( address_wildcard ) ;
271 std::string & trustee = pair.first ;
272 if( !trustee.empty() )
273 {
274 G_LOG( "GAuth::SaslServer::trusted: trusting [" << address.hostPartString() << "]: "
275 << "matched [" << address_wildcard << "] from " << pair.second ) ;
276 const_cast<SaslServerBasicImp*>(this)->m_trustee = trustee ;
277 return true ;
278 }
279 else
280 {
281 return false ;
282 }
283}
284
285bool GAuth::SaslServerBasicImp::active() const
286{
287 return m_secrets.valid() ;
288}
289
290std::string GAuth::SaslServerBasicImp::mechanism() const
291{
292 return m_mechanism ;
293}
294
295std::string GAuth::SaslServerBasicImp::id() const
296{
297 return m_authenticated ? m_id : m_trustee ;
298}
299
300bool GAuth::SaslServerBasicImp::authenticated() const
301{
302 return m_authenticated ;
303}
304
305// ===
306
307GAuth::SaslServerBasic::SaslServerBasic( const SaslServerSecrets & secrets , const std::string & config , bool allow_apop ) :
308 m_imp(std::make_unique<SaslServerBasicImp>(secrets,config,allow_apop))
309{
310}
311
312GAuth::SaslServerBasic::~SaslServerBasic()
313= default ;
314
315std::string GAuth::SaslServerBasic::mechanisms( char c ) const
316{
317 return m_imp->mechanisms( std::string(1U,c) ) ;
318}
319
320std::string GAuth::SaslServerBasic::mechanism() const
321{
322 return m_imp->mechanism() ;
323}
324
325bool GAuth::SaslServerBasic::trusted( const GNet::Address & address ) const
326{
327 return m_imp->trusted( address ) ;
328}
329
330bool GAuth::SaslServerBasic::active() const
331{
332 return m_imp->active() ;
333}
334
335bool GAuth::SaslServerBasic::init( const std::string & mechanism )
336{
337 return m_imp->init( mechanism ) ;
338}
339
340bool GAuth::SaslServerBasic::mustChallenge() const
341{
342 std::string m = G::Str::upper( m_imp->mechanism() ) ; // upper() just in case
343 return m != "PLAIN" && m != "LOGIN" && m != "XOAUTH2" ;
344}
345
346std::string GAuth::SaslServerBasic::initialChallenge() const
347{
348 return m_imp->initialChallenge() ;
349}
350
351std::string GAuth::SaslServerBasic::apply( const std::string & response , bool & done )
352{
353 return m_imp->apply( response , done ) ;
354}
355
356bool GAuth::SaslServerBasic::authenticated() const
357{
358 return m_imp->authenticated() ;
359}
360
361std::string GAuth::SaslServerBasic::id() const
362{
363 return m_imp->id() ;
364}
365
366bool GAuth::SaslServerBasic::requiresEncryption() const
367{
368 return false ;
369}
370
static std::string challenge(unsigned int random)
Returns a challenge string that incorporates the given random number and the current time.
Definition: gcram.cpp:225
static std::string id(const std::string &response)
Returns the leading id part of the response.
Definition: gcram.cpp:142
static bool validate(const std::string &hash_type, bool hmac, const Secret &secret, const std::string &challenge, const std::string &response)
Validates the response with respect to the original challenge.
Definition: gcram.cpp:118
static G::StringArray hashTypes(const std::string &prefix=std::string(), bool require_state=false)
Returns a list of supported hash types, such as "MD5" and "SHA1", ordered with the strongest first.
Definition: gcram.cpp:199
A private pimple-pattern implementation class used by GAuth::SaslServerBasic.
SaslServerBasic(const SaslServerSecrets &, const std::string &config, bool allow_apop)
Constructor.
An interface used by GAuth::SaslServer to obtain authentication secrets.
static Secret none()
Factory function that returns a secret that is not valid() and has an empty id().
Definition: gsecret.cpp:94
The GNet::Address class encapsulates a TCP/UDP transport address.
Definition: gaddress.h:53
G::StringArray wildcards() const
Returns an ordered list of wildcard strings that match this address.
Definition: gaddress.cpp:509
std::string hostPartString(bool raw=false) const
Returns a string which represents the network address.
Definition: gaddress.cpp:384
static std::string join(const std::string &sep, const StringArray &strings)
Concatenates an array of strings with separators.
Definition: gstr.cpp:1195
static std::string tail(const std::string &in, std::size_t pos, const std::string &default_=std::string())
Returns the last part of the string after the given position.
Definition: gstr.cpp:1287
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:1073
static StringArray::iterator removeMatch(StringArray::iterator begin, StringArray::iterator end, const StringArray &match_list, bool ignore_case=false)
Removes items in the begin/end list that match one of the elements in the match-list (blocklist).
Definition: gstr.cpp:1508
static std::string upper(const std::string &s)
Returns a copy of 's' in which all Latin-1 lower-case characters have been replaced by upper-case cha...
Definition: gstr.cpp:754
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:885
static std::string headMatchResidue(const StringArray &in, const std::string &head)
Returns the unmatched part of the first string in the array that has the given start.
Definition: gstr.cpp:1335
static std::string head(const std::string &in, std::size_t pos, const std::string &default_=std::string())
Returns the first part of the string up to just before the given position.
Definition: gstr.cpp:1273
static StringArray::iterator keepMatch(StringArray::iterator begin, StringArray::iterator end, const StringArray &match_list, bool ignore_case=false)
Removes items in the begin/end list that do not match any of the elements in the match-list (whitelis...
Definition: gstr.cpp:1498
static std::string lower(const std::string &s)
Returns a copy of 's' in which all Latin-1 upper-case characters have been replaced by lower-case cha...
Definition: gstr.cpp:741
static bool enabled() noexcept
Returns true if test features are enabled.
Definition: gtest.cpp:79
std::vector< std::string > StringArray
A std::vector of std::strings.
Definition: gstrings.h:31