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///
37 class GAuth::SaslClientImp
38{
39 public:
40 using Response = SaslClient::Response ;
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
52 private:
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
64 const char * const GAuth::SaslClientImp::login_challenge_1 = "Username:" ;
65 const 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
143 bool 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
172 GAuth::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
258 bool GAuth::SaslClientImp::active() const
259{
260 return m_secrets.valid() ;
261}
262
263 bool 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
270 GAuth::SaslClient::SaslClient( const SaslClientSecrets & secrets , const std::string & config ) :
271 m_imp(std::make_unique<SaslClientImp>(secrets,config))
272{
273}
274
275 GAuth::SaslClient::~SaslClient()
276= default;
277
278 bool GAuth::SaslClient::active() const
279{
280 return m_imp->active() ;
281}
282
283 GAuth::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
298 bool GAuth::SaslClient::next()
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
309std::string GAuth::SaslClient::mechanism() const
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
GAuth::Cram::response
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
GAuth::Cram::hashTypes
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
GAuth::SaslClientImp
A private pimple-pattern implementation class used by GAuth::SaslClient.
Definition: gsaslclient.cpp:38
GAuth::SaslClientSecrets
An interface used by GAuth::SaslClient to obtain a client id and its authentication secret.
Definition: gsaslclientsecrets.h:39
GAuth::SaslClient::response
Response response(const std::string &mechanism, const std::string &challenge) const
Returns a response to the given challenge.
Definition: gsaslclient.cpp:283
GAuth::SaslClient::id
std::string id() const
Returns the authentication id, valid after the last response().
Definition: gsaslclient.cpp:314
GAuth::SaslClient::initialResponse
std::string initialResponse(std::size_t limit=0U) const
Returns an optional initial response.
Definition: gsaslclient.cpp:288
GAuth::SaslClient::~SaslClient
~SaslClient()
Destructor.
GAuth::SaslClient::SaslClient
SaslClient(const SaslClientSecrets &secrets, const std::string &config)
Constructor. The secrets reference is kept.
Definition: gsaslclient.cpp:270
GAuth::SaslClient::next
bool next()
Moves to the next preferred mechanism.
Definition: gsaslclient.cpp:298
GAuth::SaslClient::info
std::string info() const
Returns logging and diagnostic information, valid after the last response().
Definition: gsaslclient.cpp:319
GAuth::SaslClient::mechanism
std::string mechanism() const
Returns the name of the current mechanism once next() has returned true.
Definition: gsaslclient.cpp:309
GAuth::SaslClient::active
bool active() const
Returns true if the constructor's secrets object is valid.
Definition: gsaslclient.cpp:278
GAuth::Secret::none
static Secret none()
Factory function that returns a secret that is not valid() and has an empty id().
Definition: gsecret.cpp:94
G::Str::join
static std::string join(const std::string &sep, const StringArray &strings)
Concatenates an array of strings with separators.
Definition: gstr.cpp:1195
G::Str::imatch
static bool imatch(char, char)
Returns true if the two characters are the same, ignoring Latin-1 case.
Definition: gstr.cpp:1414
G::Str::splitIntoTokens
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
G::Str::removeMatch
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
G::Str::upper
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
G::Str::headMatchResidue
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
G::Str::keepMatch
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
G::Str::lower
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
gassert.h
gbase64.h
gcram.h
ghash.h
glog.h
gmd5.h
gsaslclient.h
gssl.h
An interface to an underlying TLS library.
gstr.h
G::StringArray
std::vector< std::string > StringArray
A std::vector of std::strings.
Definition: gstrings.h:31
GAuth::SaslClient::Response
Result structure returned from GAuth::SaslClient::response.
Definition: gsaslclient.h:44