E-MailRelay

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