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