E-MailRelay
gsaslclient.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 gsaslclient.cpp
19///
20
21#include "gdef.h"
22#include "gsaslclient.h"
23#include "gssl.h"
24#include "gmd5.h"
25#include "ghash.h"
26#include "gcram.h"
27#include "gbase64.h"
28#include "gstr.h"
29#include "gassert.h"
30#include "glog.h"
31#include <algorithm>
32#include <sstream>
33
34//| \class GAuth::SaslClientImp
35/// A private pimple-pattern implementation class used by GAuth::SaslClient.
36///
38{
39public:
41 SaslClientImp( const SaslClientSecrets & , const std::string & ) ;
42 bool active() const ;
43 std::string mechanism( const G::StringArray & ) const ;
44 std::string initialResponse( std::size_t limit ) const ;
45 Response response( const std::string & mechanism , const std::string & challenge ) const ;
46 bool next() ;
47 std::string mechanism() const ;
48 std::string id() const ;
49 std::string info() const ;
50 static bool match( const G::StringArray & mechanisms , const std::string & ) ;
51
52private:
53 const SaslClientSecrets & m_secrets ;
54 std::string m_config ;
55 mutable G::StringArray m_mechanisms ;
56 mutable std::string m_info ;
57 mutable std::string m_id ;
58 std::string PLAIN ;
59 std::string LOGIN ;
60 static const char * const login_challenge_1 ;
61 static const char * const login_challenge_2 ;
62} ;
63
64const char * const GAuth::SaslClientImp::login_challenge_1 = "Username:" ;
65const char * const GAuth::SaslClientImp::login_challenge_2 = "Password:" ;
66
67// ===
68
69GAuth::SaslClientImp::SaslClientImp( const SaslClientSecrets & secrets , const std::string & sasl_client_config ) :
70 m_secrets(secrets) ,
71 m_config(sasl_client_config) ,
72 PLAIN("PLAIN") ,
73 LOGIN("LOGIN")
74{
75}
76
77std::string GAuth::SaslClientImp::mechanism( const G::StringArray & server_mechanisms ) const
78{
79 // short-circuit if no secrets file
80 if( !active() )
81 return std::string() ;
82
83 // if we have a plaintext password then we can use any cram
84 // mechanism for which we have a hash function -- otherwise
85 // we can use cram mechanisms where we have a hashed password
86 // of the correct type and the hash function is capable of
87 // initialisation with an intermediate state
88 //
89 G::StringArray our_list ;
90 if( m_secrets.clientSecret("plain").valid() )
91 {
92 our_list = Cram::hashTypes( "CRAM-" , false ) ;
93 }
94 else
95 {
96 our_list = Cram::hashTypes( "CRAM-" , true ) ;
97 for( auto p = our_list.begin() ; p != our_list.end() ; )
98 {
99 if( m_secrets.clientSecret((*p).substr(5U)).valid() )
100 ++p ;
101 else
102 p = our_list.erase( p ) ;
103 }
104 }
105 if( m_secrets.clientSecret("oauth").valid() )
106 {
107 our_list.push_back( "XOAUTH2" ) ;
108 }
109 if( m_secrets.clientSecret("plain").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::Str::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::Str::headMatchResidue( list , "M:" ) , "," ) ;
121 G::StringArray blocklist = G::Str::splitIntoTokens( G::Str::headMatchResidue( list , "X:" ) , "," ) ;
122 our_list.erase( G::Str::keepMatch( our_list.begin() , our_list.end() , whitelist , true ) , our_list.end() ) ;
123 our_list.erase( G::Str::removeMatch( our_list.begin() , our_list.end() , blocklist , true ) , our_list.end() ) ;
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
155std::string GAuth::SaslClientImp::initialResponse( 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 const std::string m = mechanism() ;
162 if( m.empty() || m.find("CRAM-") == 0U )
163 return std::string() ;
164
165 Response rsp = response( m , m=="LOGIN"?std::string(login_challenge_1):std::string() ) ;
166 if( rsp.error || rsp.data.size() > limit )
167 return std::string() ;
168 else
169 return rsp.data ;
170}
171
172GAuth::SaslClient::Response GAuth::SaslClientImp::response( const std::string & mechanism ,
173 const std::string & challenge ) 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 std::string hash_type = mechanism.substr( 5U ) ;
184 secret = m_secrets.clientSecret( hash_type ) ;
185 if( !secret.valid() )
186 secret = m_secrets.clientSecret( "plain" ) ;
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" )
192 {
193 secret = m_secrets.clientSecret( "MD5" ) ;
194 rsp.data = Cram::response( "MD5" , 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" ) ;
201 const std::string sep( 1U , '\0' ) ;
202 rsp.data = sep + secret.id() + sep + secret.key() ;
203 rsp.error = !secret.valid() ;
204 rsp.final = true ;
205 }
206 else if( mechanism == LOGIN && challenge == login_challenge_1 )
207 {
208 secret = m_secrets.clientSecret( "plain" ) ;
209 rsp.data = secret.id() ;
210 rsp.error = !secret.valid() ;
211 rsp.final = false ;
212 rsp.sensitive = false ; // userid
213 }
214 else if( mechanism == LOGIN && challenge == login_challenge_2 )
215 {
216 secret = m_secrets.clientSecret( "plain" ) ;
217 rsp.data = secret.key() ;
218 rsp.error = !secret.valid() ;
219 rsp.final = true ;
220 }
221 else if( mechanism == "XOAUTH2" && challenge.empty() )
222 {
223 secret = m_secrets.clientSecret( "oauth" ) ;
224 rsp.data = secret.key() ;
225 rsp.error = !secret.valid() ;
226 rsp.final = true ; // not always -- may get an informational challenge
227 }
228 else if( mechanism == "XOAUTH2" )
229 {
230 secret = m_secrets.clientSecret( "oauth" ) ;
231 rsp.data.clear() ; // information-only challenge gets an empty response
232 rsp.error = false ;
233 rsp.final = true ;
234 rsp.sensitive = false ; // information-only
235 }
236
237 if( rsp.final )
238 {
239 std::ostringstream ss ;
240 ss << "using mechanism [" << G::Str::lower(mechanism) << "] and " << secret.info() ;
241 m_info = ss.str() ;
242 m_id = secret.id() ;
243 }
244
245 return rsp ;
246}
247
248std::string GAuth::SaslClientImp::id() const
249{
250 return m_id ;
251}
252
253std::string GAuth::SaslClientImp::info() const
254{
255 return m_info ;
256}
257
258bool GAuth::SaslClientImp::active() const
259{
260 return m_secrets.valid() ;
261}
262
263bool GAuth::SaslClientImp::match( const G::StringArray & mechanisms , const std::string & mechanism )
264{
265 return std::find(mechanisms.begin(),mechanisms.end(),mechanism) != mechanisms.end() ;
266}
267
268// ===
269
270GAuth::SaslClient::SaslClient( const SaslClientSecrets & secrets , const std::string & config ) :
271 m_imp(std::make_unique<SaslClientImp>(secrets,config))
272{
273}
274
276= default;
277
279{
280 return m_imp->active() ;
281}
282
283GAuth::SaslClient::Response GAuth::SaslClient::response( const std::string & mechanism , const std::string & challenge ) const
284{
285 return m_imp->response( mechanism , challenge ) ;
286}
287
288std::string GAuth::SaslClient::initialResponse( std::size_t limit ) const
289{
290 return m_imp->initialResponse( limit ) ;
291}
292
293std::string GAuth::SaslClient::mechanism( const G::StringArray & server_mechanisms ) const
294{
295 return m_imp->mechanism( server_mechanisms ) ;
296}
297
299{
300 return m_imp->next() ;
301}
302
303std::string GAuth::SaslClient::next( const std::string & s )
304{
305 if( s.empty() ) return s ;
306 return m_imp->next() ? mechanism() : std::string() ;
307}
308
310{
311 return m_imp->mechanism() ;
312}
313
314std::string GAuth::SaslClient::id() const
315{
316 return m_imp->id() ;
317}
318
319std::string GAuth::SaslClient::info() const
320{
321 return m_imp->info() ;
322}
323
static std::string response(const std::string &hash_type, bool hmac, const Secret &secret, const std::string &challenge, const std::string &response_prefix)
Constructs a response to a challenge comprising the response-prefix, space, and digest-or-hmac of sec...
Definition: gcram.cpp:96
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::SaslClient.
Definition: gsaslclient.cpp:38
An interface used by GAuth::SaslClient to obtain a client id and its authentication secret.
Response response(const std::string &mechanism, const std::string &challenge) const
Returns a response to the given challenge.
std::string id() const
Returns the authentication id, valid after the last response().
std::string initialResponse(std::size_t limit=0U) const
Returns an optional initial response.
~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 active() const
Returns true if the constructor's secrets object is valid.
static Secret none()
Factory function that returns a secret that is not valid() and has an empty id().
Definition: gsecret.cpp:94
static std::string join(const std::string &sep, const StringArray &strings)
Concatenates an array of strings with separators.
Definition: gstr.cpp:1195
static bool imatch(char, char)
Returns true if the two characters are the same, ignoring Latin-1 case.
Definition: gstr.cpp:1414
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 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 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
An interface to an underlying TLS library.
std::vector< std::string > StringArray
A std::vector of std::strings.
Definition: gstrings.h:31
Result structure returned from GAuth::SaslClient::response.
Definition: gsaslclient.h:44