E-MailRelay
gsaslclient.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 gsaslclient.cpp
19///
20
21#include "gdef.h"
22#include "gsaslclient.h"
23#include "gstringview.h"
24#include "gstringlist.h"
25#include "gssl.h"
26#include "gmd5.h"
27#include "ghash.h"
28#include "gcram.h"
29#include "gbase64.h"
30#include "gstr.h"
31#include "gassert.h"
32#include "glog.h"
33#include <algorithm>
34
35namespace GAuth
36{
37 namespace SaslClientStrings
38 {
39 static constexpr std::string_view login_challenge_1 {"Username:",9U} ;
40 static constexpr std::string_view login_challenge_2 {"Password:",9U} ;
41 }
42}
43
44//| \class GAuth::SaslClientImp
45/// A private pimple-pattern implementation class used by GAuth::SaslClient.
46///
47class GAuth::SaslClientImp
48{
49public:
50 using Response = SaslClient::Response ;
51 SaslClientImp( const SaslClientSecrets & , const std::string & ) ;
52 bool validSelector( std::string_view ) const ;
53 bool mustAuthenticate( std::string_view ) const ;
54 std::string mechanism( const G::StringArray & , std::string_view ) const ;
55 Response initialResponse( std::string_view selector , std::size_t ) const ;
56 Response response( std::string_view mechanism , std::string_view challenge , std::string_view selector ) const ;
57 bool next() ;
58 std::string mechanism() const ;
59 std::string id() const ;
60 std::string info() const ;
61 static bool match( const G::StringArray & mechanisms , const std::string & ) ;
62
63public:
64 ~SaslClientImp() = default ;
65 SaslClientImp( const SaslClientImp & ) = delete ;
66 SaslClientImp( SaslClientImp && ) = delete ;
67 SaslClientImp & operator=( const SaslClientImp & ) = delete ;
68 SaslClientImp & operator=( SaslClientImp && ) = delete ;
69
70private:
71 const SaslClientSecrets & m_secrets ;
72 std::string m_config ;
73 mutable G::StringArray m_mechanisms ;
74 mutable std::string m_info ;
75 mutable std::string m_id ;
76 std::string PLAIN ;
77 std::string LOGIN ;
78} ;
79
80// ===
81
82GAuth::SaslClientImp::SaslClientImp( const SaslClientSecrets & secrets ,
83 const std::string & sasl_client_config ) :
84 m_secrets(secrets) ,
85 m_config(sasl_client_config) ,
86 PLAIN("PLAIN") ,
87 LOGIN("LOGIN")
88{
89}
90
91std::string GAuth::SaslClientImp::mechanism( const G::StringArray & server_mechanisms , std::string_view selector ) const
92{
93 // if we have a plaintext password then we can use any cram
94 // mechanism for which we have a hash function -- otherwise
95 // we can use cram mechanisms where we have a hashed password
96 // of the correct type and the hash function is capable of
97 // initialisation with an intermediate state
98 //
99 G::StringArray our_list ;
100 if( m_secrets.clientSecret("plain",selector).valid() )
101 {
102 our_list = Cram::hashTypes( "CRAM-" , false ) ;
103 }
104 else
105 {
106 our_list = Cram::hashTypes( "CRAM-" , true ) ;
107 for( auto p = our_list.begin() ; p != our_list.end() ; )
108 {
109 std::string type = (*p).substr( 5U ) ;
110 if( m_secrets.clientSecret(type,selector).valid() )
111 ++p ;
112 else
113 p = our_list.erase( p ) ;
114 }
115 }
116 if( m_secrets.clientSecret("oauth",selector).valid() )
117 {
118 our_list.emplace_back( "XOAUTH2" ) ;
119 }
120 if( m_secrets.clientSecret("plain",selector).valid() )
121 {
122 our_list.push_back( PLAIN ) ;
123 our_list.push_back( LOGIN ) ;
124 }
125
126 // use the configuration string as a mechanism whitelist and/or blocklist
127 if( !m_config.empty() )
128 {
129 bool simple = G::StringList::imatch( our_list , m_config ) ; // eg. allow "plain" as well as "m:plain"
130 G::StringArray list = G::Str::splitIntoTokens( G::Str::upper(m_config) , ";" ) ;
131 G::StringArray whitelist = G::Str::splitIntoTokens( simple ? G::Str::upper(m_config) : G::StringList::headMatchResidue( list , "M:" ) , "," ) ;
133 G::StringList::keepMatch( our_list , whitelist , G::StringList::Ignore::Case ) ;
134 G::StringList::removeMatch( our_list , blocklist , G::StringList::Ignore::Case ) ;
135 }
136
137 // build the list of mechanisms that we can use with the server
138 m_mechanisms.clear() ;
139 for( auto & our_mechanism : our_list )
140 {
141 if( match(server_mechanisms,our_mechanism) )
142 {
143 m_mechanisms.push_back( our_mechanism ) ;
144 }
145 }
146
147 G_DEBUG( "GAuth::SaslClientImp::mechanism: server mechanisms: [" << G::Str::join(",",server_mechanisms) << "]" ) ;
148 G_DEBUG( "GAuth::SaslClientImp::mechanism: our mechanisms: [" << G::Str::join(",",our_list) << "]" ) ;
149 G_DEBUG( "GAuth::SaslClientImp::mechanism: usable mechanisms: [" << G::Str::join(",",m_mechanisms) << "]" ) ;
150
151 return m_mechanisms.empty() ? std::string() : m_mechanisms.at(0U) ;
152}
153
154bool GAuth::SaslClientImp::next()
155{
156 if( !m_mechanisms.empty() )
157 m_mechanisms.erase( m_mechanisms.begin() ) ;
158 return !m_mechanisms.empty() ;
159}
160
161std::string GAuth::SaslClientImp::mechanism() const
162{
163 return m_mechanisms.empty() ? std::string() : m_mechanisms.at(0U) ;
164}
165
166GAuth::SaslClient::Response GAuth::SaslClientImp::initialResponse( std::string_view selector , std::size_t limit ) const
167{
168 // (the implementation of response() is stateless because it can derive
169 // state from the challege, so we doesn't need to worry here about
170 // side-effects between initialResponse() and response())
171
172 if( m_mechanisms.empty() || m_mechanisms[0U].find("CRAM-") == 0U )
173 return {} ;
174
175 const std::string & m = m_mechanisms[0] ;
176 Response rsp = response( m , m == "LOGIN" ? SaslClientStrings::login_challenge_1 : std::string_view() , selector ) ;
177 if( rsp.error || rsp.data.size() > limit )
178 return {} ;
179 else
180 return rsp ;
181}
182
183GAuth::SaslClient::Response GAuth::SaslClientImp::response( std::string_view mechanism ,
184 std::string_view challenge , std::string_view selector ) const
185{
186 Response rsp ;
187 rsp.sensitive = true ;
188 rsp.error = true ;
189 rsp.final = false ;
190
191 Secret secret = Secret::none() ;
192 if( mechanism.find("CRAM-") == 0U )
193 {
194 std::string_view hash_type = mechanism.substr( 5U ) ;
195 secret = m_secrets.clientSecret( hash_type , selector ) ;
196 if( !secret.valid() )
197 secret = m_secrets.clientSecret( "plain" , selector ) ;
198 rsp.data = Cram::response( hash_type , true , secret , challenge , secret.id() ) ;
199 rsp.error = rsp.data.empty() ;
200 rsp.final = true ;
201 }
202 else if( mechanism == "APOP"_sv )
203 {
204 secret = m_secrets.clientSecret( "MD5"_sv , selector ) ;
205 rsp.data = Cram::response( "MD5"_sv , false , secret , challenge , secret.id() ) ;
206 rsp.error = rsp.data.empty() ;
207 rsp.final = true ;
208 }
209 else if( mechanism == PLAIN )
210 {
211 secret = m_secrets.clientSecret( "plain"_sv , selector ) ;
212 rsp.data = std::string(1U,'\0').append(secret.id()).append(1U,'\0').append(secret.secret()) ;
213 rsp.error = !secret.valid() ;
214 rsp.final = true ;
215 }
216 else if( mechanism == LOGIN && challenge == SaslClientStrings::login_challenge_1 )
217 {
218 secret = m_secrets.clientSecret( "plain"_sv , selector ) ;
219 rsp.data = secret.id() ;
220 rsp.error = !secret.valid() ;
221 rsp.final = false ;
222 rsp.sensitive = false ; // userid
223 }
224 else if( mechanism == LOGIN && challenge == SaslClientStrings::login_challenge_2 )
225 {
226 secret = m_secrets.clientSecret( "plain"_sv , selector ) ;
227 rsp.data = secret.secret() ;
228 rsp.error = !secret.valid() ;
229 rsp.final = true ;
230 }
231 else if( mechanism == "XOAUTH2"_sv && challenge.empty() )
232 {
233 secret = m_secrets.clientSecret( "oauth"_sv , selector ) ;
234 rsp.data = secret.secret() ;
235 rsp.error = !secret.valid() ;
236 rsp.final = true ; // not always -- may get an informational challenge
237 }
238 else if( mechanism == "XOAUTH2"_sv )
239 {
240 secret = m_secrets.clientSecret( "oauth"_sv , selector ) ;
241 rsp.data.clear() ; // information-only challenge gets an empty response
242 rsp.error = false ;
243 rsp.final = true ;
244 rsp.sensitive = false ; // information-only
245 }
246
247 if( rsp.final )
248 {
249 m_info
250 .assign("using mechanism [")
251 .append(G::Str::lower(G::sv_to_string(mechanism)))
252 .append("] and ",6U)
253 .append(secret.info()) ;
254 m_id = secret.id() ;
255 }
256
257 return rsp ;
258}
259
260std::string GAuth::SaslClientImp::id() const
261{
262 return m_id ;
263}
264
265std::string GAuth::SaslClientImp::info() const
266{
267 return m_info ;
268}
269
270bool GAuth::SaslClientImp::validSelector( std::string_view selector ) const
271{
272 return m_secrets.validSelector( selector ) ;
273}
274
275bool GAuth::SaslClientImp::mustAuthenticate( std::string_view selector ) const
276{
277 return m_secrets.mustAuthenticate( selector ) ;
278}
279
280bool GAuth::SaslClientImp::match( const G::StringArray & mechanisms , const std::string & mechanism )
281{
282 return std::find(mechanisms.begin(),mechanisms.end(),mechanism) != mechanisms.end() ;
283}
284
285// ===
286
287GAuth::SaslClient::SaslClient( const SaslClientSecrets & secrets , const std::string & config ) :
288 m_imp(std::make_unique<SaslClientImp>(secrets,config))
289{
290}
291
293= default ;
294
295bool GAuth::SaslClient::validSelector( std::string_view selector ) const
296{
297 return m_imp->validSelector( selector ) ;
298}
299
300bool GAuth::SaslClient::mustAuthenticate( std::string_view selector ) const
301{
302 return m_imp->mustAuthenticate( selector ) ;
303}
304
306 std::string_view challenge , std::string_view selector ) const
307{
308 return m_imp->response( mechanism , challenge , selector ) ;
309}
310
311GAuth::SaslClient::Response GAuth::SaslClient::initialResponse( std::string_view selector , std::size_t limit ) const
312{
313 return m_imp->initialResponse( selector , limit ) ;
314}
315
316std::string GAuth::SaslClient::mechanism( const G::StringArray & server_mechanisms , std::string_view selector ) const
317{
318 return m_imp->mechanism( server_mechanisms , selector ) ;
319}
320
322{
323 return m_imp->next() ;
324}
325
326#ifndef G_LIB_SMALL
327std::string GAuth::SaslClient::next( const std::string & s )
328{
329 if( s.empty() ) return s ;
330 return m_imp->next() ? mechanism() : std::string() ;
331}
332#endif
333
335{
336 return m_imp->mechanism() ;
337}
338
339std::string GAuth::SaslClient::id() const
340{
341 return m_imp->id() ;
342}
343
344std::string GAuth::SaslClient::info() const
345{
346 return m_imp->info() ;
347}
348
An interface used by GAuth::SaslClient to obtain a client id and its authentication secret.
std::string id() const
Returns the authentication id, valid after the last response().
~SaslClient()
Destructor.
SaslClient(const SaslClientSecrets &secrets, const std::string &config)
Constructor. The secrets reference is kept.
Response response(std::string_view mechanism, std::string_view challenge, std::string_view selector) const
Returns a response to the given challenge.
bool validSelector(std::string_view selector) const
Returns true if the selector is valid.
bool next()
Moves to the next preferred mechanism.
std::string info() const
Returns logging and diagnostic information, valid after the last response().
std::string mechanism() const
Returns the name of the current mechanism once next() has returned true.
Response initialResponse(std::string_view selector, std::size_t limit=0U) const
Returns an optional initial response.
bool mustAuthenticate(std::string_view selector) const
Returns true if authentication is required.
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 std::string join(std::string_view sep, const StringArray &strings)
Concatenates an array of strings with separators.
Definition: gstr.cpp:1221
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
An interface to an underlying TLS library.
SASL authentication classes.
Definition: gcram.cpp:38
void keepMatch(StringArray &list, const StringArray &allow_list, Ignore=Ignore::Nothing)
Removes items in the list that do not match any entry in the allow list.
Definition: gstringlist.cpp:59
bool imatch(const StringArray &, const std::string &)
Returns true if any string in the array matches the given string, ignoring case.
void removeMatch(StringArray &list, const StringArray &deny_list, Ignore=Ignore::Nothing)
Removes items in the list that match an entry in the deny list.
Definition: gstringlist.cpp:69
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.
Result structure returned from GAuth::SaslClient::response.
Definition: gsaslclient.h:45