E-MailRelay
gcram.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 gcram.cpp
19///
20
21#include "gdef.h"
22#include "gcram.h"
23#include "ghash.h"
24#include "ghashstate.h"
25#include "gmd5.h"
26#include "gstr.h"
27#include "gstringlist.h"
28#include "gssl.h"
29#include "gbase64.h"
30#include "glocal.h"
31#include "gexception.h"
32#include "gtest.h"
33#include "glog.h"
34#include <algorithm>
35
36namespace GAuth
37{
38 namespace CramImp
39 {
40 GSsl::Library & lib()
41 {
43 if( p == nullptr ) throw Cram::NoTls() ;
44 return *p ;
45 }
46 struct DigesterAdaptor /// Used by GAuth::Cram to use GSsl::Digester.
47 {
48 explicit DigesterAdaptor( G::string_view name ) :
49 m_name(G::sv_to_string(name))
50 {
51 GSsl::Digester d( CramImp::lib().digester(m_name) ) ;
52 m_blocksize = d.blocksize() ;
53 }
54 std::string operator()( G::string_view data_1 , G::string_view data_2 ) const
55 {
56 GSsl::Digester d( CramImp::lib().digester(m_name) ) ;
57 d.add( data_1 ) ;
58 d.add( data_2 ) ;
59 return d.value() ;
60 }
61 std::size_t blocksize() const
62 {
63 return m_blocksize ;
64 }
65 std::string m_name ;
66 std::size_t m_blocksize ;
67 } ;
68 struct PostDigesterAdaptor /// Used by GAuth::Cram to use GSsl::Digester.
69 {
70 explicit PostDigesterAdaptor( G::string_view name ) :
71 m_name(G::sv_to_string(name))
72 {
73 GSsl::Digester d( CramImp::lib().digester(m_name,std::string(),true) ) ;
74 if( d.statesize() == 0U )
75 throw Cram::NoState( m_name ) ;
76 m_valuesize = d.valuesize() ;
77 m_blocksize = d.blocksize() ;
78 }
79 std::string operator()( const std::string & state_pair , const std::string & data ) const
80 {
81 if( state_pair.size() != (2U*m_valuesize) ) throw Cram::InvalidState( m_name ) ;
82 std::string state_i = state_pair.substr( 0U , state_pair.size()/2U ) + G::HashStateImp::extension(m_blocksize) ;
83 std::string state_o = state_pair.substr( state_pair.size()/2U ) + G::HashStateImp::extension(m_blocksize) ;
84 GSsl::Digester xi( CramImp::lib().digester( m_name , state_i ) ) ;
85 xi.add( data ) ;
86 GSsl::Digester xo( CramImp::lib().digester( m_name , state_o ) ) ;
87 xo.add( xi.value() ) ;
88 return xo.value() ;
89 }
90 std::string m_name ;
91 std::size_t m_valuesize ;
92 std::size_t m_blocksize ;
93 } ;
94 }
95}
96
97std::string GAuth::Cram::response( G::string_view hash_type , bool as_hmac ,
98 const Secret & secret , G::string_view challenge , G::string_view id_prefix )
99{
100 try
101 {
102 G_DEBUG( "GAuth::Cram::response: [" << hash_type << "]"
103 << "[" << as_hmac << "]"
104 << "[" << G::Str::printable(secret.secret()) << "]"
105 << "[" << secret.maskHashFunction() << "][" << challenge << "]"
106 << "[" << G::Str::printable(id_prefix) << "]"
107 << "[" << responseImp(hash_type,as_hmac,secret,challenge) << "]" ) ;
108
109 return G::sv_to_string(id_prefix).append(1U,' ').append(responseImp(hash_type,as_hmac,secret,challenge)) ;
110 }
111 catch( std::exception & e )
112 {
113 G_WARNING( "GAuth::Cram::response: challenge-response failure: " << e.what() ) ;
114 return std::string() ;
115 }
116}
117
118bool GAuth::Cram::validate( G::string_view hash_type , bool as_hmac ,
119 const Secret & secret , G::string_view challenge ,
120 G::string_view response_in )
121{
122 try
123 {
124 G_DEBUG( "GAuth::Cram::validate: [" << hash_type << "]"
125 << "[" << as_hmac << "]"
126 << "[" << G::Str::printable(secret.secret()) << "]"
127 << "[" << secret.maskHashFunction() << "]"
128 << "[" << challenge << "]"
129 << "[" << response_in << "]"
130 << "[" << responseImp(hash_type,as_hmac,secret,challenge) << "]" ) ;
131
132 std::string expectation = G::Str::tail( response_in , response_in.rfind(' ') ) ;
133 return !expectation.empty() && responseImp(hash_type,as_hmac,secret,challenge) == expectation ;
134 }
135 catch( std::exception & e )
136 {
137 G_WARNING( "GAuth::Cram::validate: challenge-response failure: " << e.what() ) ;
138 return false ;
139 }
140}
141
142std::string GAuth::Cram::id( G::string_view response )
143{
144 // the response is "<id> <hexchars>" but also allow for ids with spaces
145 std::size_t pos = response.rfind( ' ' ) ;
146 return G::Str::head( response , pos ) ;
147}
148
149std::string GAuth::Cram::responseImp( G::string_view mechanism_hash_type , bool as_hmac ,
150 const Secret & secret , G::string_view challenge )
151{
152 G_DEBUG( "GAuth::Cram::responseImp: mechanism-hash=[" << mechanism_hash_type << "] "
153 << "secret-hash=[" << secret.maskHashFunction() << "] "
154 << "as-hmac=" << as_hmac ) ;
155
156 if( !as_hmac )
157 {
158 if( secret.masked() )
159 throw BadType( secret.maskHashFunction() ) ;
160
161 if( G::Str::imatch( mechanism_hash_type , "MD5"_sv ) )
162 {
163 return G::Hash::printable( G::Md5::digest(challenge,secret.secret()) ) ;
164 }
165 else
166 {
167 CramImp::DigesterAdaptor digest( mechanism_hash_type ) ;
168 return G::Hash::printable( digest(challenge,secret.secret()) ) ;
169 }
170 }
171 else if( secret.masked() )
172 {
173 if( ! G::Str::imatch(secret.maskHashFunction(),mechanism_hash_type) )
174 throw Mismatch( secret.maskHashFunction() , G::sv_to_string(mechanism_hash_type) ) ;
175
176 if( G::Str::imatch( mechanism_hash_type , "MD5"_sv ) )
177 {
178 return G::Hash::printable( G::Hash::hmac(G::Md5::postdigest,secret.secret(),G::sv_to_string(challenge),G::Hash::Masked()) ) ;
179 }
180 else
181 {
182 CramImp::PostDigesterAdaptor postdigest( mechanism_hash_type ) ;
183 return G::Hash::printable( G::Hash::hmac(postdigest,secret.secret(),G::sv_to_string(challenge),G::Hash::Masked()) ) ;
184 }
185 }
186 else
187 {
188 if( G::Str::imatch( mechanism_hash_type , "MD5"_sv ) )
189 {
190 return G::Hash::printable( G::Hash::hmac(G::Md5::digest2,G::Md5::blocksize(),secret.secret(),G::sv_to_string(challenge)) ) ;
191 }
192 else
193 {
194 CramImp::DigesterAdaptor digest( mechanism_hash_type ) ;
195 return G::Hash::printable( G::Hash::hmac(digest,digest.blocksize(),secret.secret(),G::sv_to_string(challenge)) ) ;
196 }
197 }
198}
199
201{
202 // we can do CRAM-X for all hash functions (X) provided by the TLS library
203 // but if we only have masked passwords (ie. require_state) then we only
204 // want hash functions that are capable of initialision with intermediate state
205 //
206 G::StringArray result = GSsl::Library::digesters( require_state ) ; // strongest first
207 if( G::Test::enabled("cram-fake-hash") )
208 result.push_back( "FAKE" ) ;
209
210 G_DEBUG( "GAuth::Cram::hashTypes: tls library [" << GSsl::Library::ids() << "]" ) ;
211 G_DEBUG( "GAuth::Cram::hashTypes: tls library hash types: [" << G::Str::join(",",result) << "] "
212 << "(" << (require_state?1:0) << ")" ) ;
213
214 // always include MD5 since we use G::Md5 code
215 if( !G::StringList::match( result , "MD5" ) )
216 result.push_back( "MD5" ) ;
217
218 if( !prefix.empty() )
219 {
220 for( auto & hashtype : result )
221 hashtype.insert( 0U , prefix.data() , prefix.size() ) ;
222 }
223 return result ;
224}
225
226std::string GAuth::Cram::challenge( unsigned int random , const std::string & challenge_domain_in )
227{
228 std::string challenge_domain = challenge_domain_in.empty() ? GNet::Local::canonicalName() : challenge_domain_in ;
229 return std::string(1U,'<')
230 .append(std::to_string(random)).append(1U,'.')
231 .append(std::to_string(G::SystemTime::now().s())).append(1U,'@')
232 .append(challenge_domain).append(1U,'>') ;
233}
234
static G::StringArray hashTypes(G::string_view prefix={}, bool require_state=false)
Returns a list of supported hash types, such as "MD5" and "SHA1", ordered with the strongest first.
Definition: gcram.cpp:200
static bool validate(G::string_view hash_type, bool hmac, const Secret &secret, G::string_view challenge, G::string_view response)
Validates the response with respect to the original challenge.
Definition: gcram.cpp:118
static std::string challenge(unsigned int random, const std::string &challenge_domain)
Returns a challenge string that incorporates the given random number and the current time.
Definition: gcram.cpp:226
static std::string id(G::string_view response)
Returns the leading id part of the response.
Definition: gcram.cpp:142
static std::string response(G::string_view hash_type, bool hmac, const Secret &secret, G::string_view challenge, G::string_view response_prefix)
Constructs a response to a challenge comprising the response-prefix, space, and digest-or-hmac of sec...
Definition: gcram.cpp:97
Encapsulates a userid/shared-secret/hash-function tuple from the secrets file.
Definition: gsecret.h:43
std::string secret() const
Returns the secret shared key. Throws if not valid().
Definition: gsecret.cpp:85
bool masked() const
Returns true if a non-empty hash function was passed to the ctor.
Definition: gsecret.cpp:91
std::string maskHashFunction() const
Returns the masking function name as passed to the ctor, such as "md5", or the empty string if not ma...
Definition: gsecret.cpp:102
static std::string canonicalName()
Returns the canonical network name assiciated with hostname().
Definition: glocal.cpp:34
A class for objects that can perform a cryptographic hash.
Definition: gssl.h:215
std::string value()
Returns the hash value.
Definition: gssl.cpp:228
void add(G::string_view)
Adds data of arbitrary size.
Definition: gssl.cpp:223
std::size_t statesize() const noexcept
Returns the size of the state() string in bytes, or zero if state() is not implemented.
Definition: gssl.cpp:248
std::size_t blocksize() const noexcept
Returns the hash function's block size in bytes.
Definition: gssl.cpp:238
std::size_t valuesize() const noexcept
Returns the hash function's value size in bytes.
Definition: gssl.cpp:243
A singleton class for initialising the underlying TLS library.
Definition: gssl.h:255
static Library * instance()
Returns a pointer to a library object, if any.
Definition: gssl.cpp:58
static std::string ids()
Returns a concatenation of all available TLS library names and versions.
Definition: gssl_none.cpp:90
static G::StringArray digesters(bool need_state=false)
Returns a list of hash function names (such as "MD5") that the TLS library can do,...
Definition: gssl.cpp:133
static std::string hmac(Fn2 digest, std::size_t blocksize, const std::string &key, const std::string &input)
Computes a Hashed Message Authentication Code using the given hash function.
Definition: ghash.h:120
static std::string printable(const std::string &input)
Converts a binary string into a printable form, using a lowercase hexadecimal encoding.
Definition: ghash.cpp:53
static std::string postdigest(const std::string &state_pair, const std::string &message)
A convenience function that returns the value() from an outer digest that is initialised with the sec...
Definition: gmd5.cpp:610
static std::size_t blocksize()
Returns the block size in bytes (64).
Definition: gmd5.cpp:628
static std::string digest2(const std::string &input_1, const std::string &input_2)
A non-overloaded name for the digest() overload taking two parameters.
Definition: gmd5.cpp:597
static std::string digest(const std::string &input)
A convenience function that returns a digest from one input.
Definition: gmd5.cpp:566
static bool imatch(char, char) noexcept
Returns true if the two characters are the same, ignoring seven-bit case.
Definition: gstr.cpp:1418
static std::string join(string_view sep, const StringArray &strings)
Concatenates an array of strings with separators.
Definition: gstr.cpp:1224
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:916
static std::string tail(string_view in, std::size_t pos, string_view default_={})
Returns the last part of the string after the given position.
Definition: gstr.cpp:1325
static std::string head(string_view in, std::size_t pos, string_view default_={})
Returns the first part of the string up to just before the given position.
Definition: gstr.cpp:1297
static SystemTime now()
Factory function for the current time.
Definition: gdatetime.cpp:328
static bool enabled() noexcept
Returns true if test features are enabled.
Definition: gtest.cpp:79
A class like c++17's std::string_view.
Definition: gstringview.h:51
An interface to an underlying TLS library.
SASL authentication classes.
Definition: gcram.cpp:37
bool match(const StringArray &, const std::string &)
Returns true if any string in the array matches the given string.
std::vector< std::string > StringArray
A std::vector of std::strings.
Definition: gstringarray.h:30
An overload discriminator for G::Hash::hmac()
Definition: ghash.h:41