1 // Copyright 2009 The Archiveopteryx Developers <info@aox.org>
3 #include "digest-md5.h"
5 #include "configuration.h"
24 static List<Nonce> * cache;
32 : stale( false ), cachedNonce( 0 )
37 EString realm, nonce, qop;
38 EString cnonce, nc, response, uri;
43 /*! \class DigestMD5 digest-md5.h
44 Implements SASL DIGEST-MD5 authentication (RFC 2831)
46 The server sends a challenge containing various parameters which the
47 client uses to compute a response. The server validates the response
48 based on the stored secret, and responds with another challenge, to
49 which the client must send an empty response.
51 We don't support a SASL initial response for this mechanism yet; nor
52 do we support its use for anything but authentication.
56 /*! Creates a digest-md5 SASL authentication object on behalf of \a c */
58 DigestMD5::DigestMD5( EventHandler *c )
59 : SaslMechanism( c, SaslMechanism::DigestMD5 ), d( new DigestData )
61 setState( AwaitingInitialResponse );
62 d->realm = Configuration::hostname();
66 EString DigestMD5::challenge()
70 if ( d->rspauth.isEmpty() ) {
71 d->nonce = Entropy::asString( 48 ).e64();
74 r = "realm=\"" + d->realm + "\", " +
75 "nonce=\"" + d->nonce + "\", " +
76 "qop=\"" + d->qop + "\", " +
80 r.append( ", stale=true" );
83 r = "rspauth=" + d->rspauth;
90 void DigestMD5::parseResponse( const EString &r )
92 // Is this a response to our second challenge?
93 if ( !d->rspauth.isEmpty() ) {
100 // Update our nonce cache for successful authentication.
101 if ( d->cachedNonce ) {
102 d->cachedNonce->count = d->nc;
103 d->cachedNonce->time = t;
106 Nonce *cn = new Nonce;
107 cn->value = d->nonce;
112 cache = new List<Nonce>;
113 Allocator::addEternal( cache, "Digest-MD5 nonce cache" );
116 if ( cache->count() > 128 )
117 delete cache->shift();
119 setState( Succeeded );
125 // Parse the first client response.
128 bool ok = parse( r, l );
130 Variable *user = l.find( "username" );
131 Variable *realm = l.find( "realm" );
132 Variable *nonce = l.find( "nonce" );
133 Variable *cnonce = l.find( "cnonce" );
134 Variable *resp = l.find( "response" );
135 Variable *qop = l.find( "qop" );
136 Variable *uri = l.find( "digest-uri" );
137 Variable *nc = l.find( "nc" );
139 if ( !ok || l.isEmpty() ) {
140 log( "Empty/unparsable DIGEST-MD5 response: <<" + r + ">>",
146 require( user, "user" );
147 require( realm, "realm" );
148 require( nonce, "nonce" );
149 require( cnonce, "cnonce" );
150 require( uri, "uri" );
153 if ( qop && ( !qop->unique() || qop->value() != "auth" ) ) {
154 s = "qop invalid in DIGEST-MD5 response: " + qop->value();
158 s = "nc not present in DIGEST-MD5 response";
161 else if ( !nc->unique() ) {
162 s = "nc not unique in DIGEST-MD5 response";
165 else if ( nc->value().length() != 8 ) {
166 s = "nc <<" + nc->value() + ">> has length " +
167 fn( nc->value().length() ) + " (not 8) in DIGEST-MD5 response";
171 s = "resp not present in DIGEST-MD5 response";
174 else if ( !resp->unique() ) {
175 s = "resp not unique in DIGEST-MD5 response";
178 else if ( resp->value().length() != 32 ) {
179 s = "resp <<" + resp->value() + ">> has length " +
180 fn( resp->value().length() ) + " (not 32) in DIGEST-MD5 response";
183 if ( state() == Failed ) {
184 log( "Full DIGEST-MD5 response was: <<" + r + ">>", Log::Debug );
185 log( s, Log::Error );
189 uint n = nc->value().number( &ok, 16 );
192 if ( ok && state() == AwaitingInitialResponse ) {
193 EString ncv = nonce->value().unquoted();
194 List< Nonce >::Iterator it( cache );
196 if ( it->value == ncv )
201 if ( !it || n != it->count.number( 0, 16 )+1 ) {
202 setState( IssuingChallenge );
207 d->nonce = it->value;
210 else if ( !ok || nonce->value().unquoted() != d->nonce || n != 1 ) {
211 log( "DIGEST-MD5 response with bad nonce/nc.", Log::Error );
216 setState( Authenticating );
217 setLogin( user->value().unquoted() );
218 d->cnonce = cnonce->value().unquoted();
219 d->response = resp->value();
220 d->uri = uri->value().unquoted();
227 /*! This private helpers checks that \a v is present, is unique and
228 quoted. If either breaks, it logs an appropriate debug message
229 (naming \a v \a n) and sets the state to Failed.
232 void DigestMD5::require( class Variable * v, const EString & n )
236 l = n + " is not present in DIGEST-MD5 response";
237 else if ( !v->unique() )
238 l = n + " is not unique in DIGEST-MD5 response";
239 else if ( !v->value().isQuoted() )
240 l = n + " is not quoted in DIGEST-MD5 response";
244 log( l, Log::Debug );
249 void DigestMD5::verify()
253 A1 = MD5::hash( login().utf8() +":"+ d->realm +":"+ storedSecret().utf8() )
254 +":"+ d->nonce +":"+ d->cnonce;
255 A2 = "AUTHENTICATE:" + d->uri;
258 MD5::hash( A1 ).hex() +":"+
259 d->nonce +":"+ d->nc +":"+ d->cnonce +":"+ d->qop +":"+
260 MD5::hash( A2 ).hex()
263 if ( R.hex() == d->response ||
264 ( Configuration::toggle( Configuration::AuthAnonymous ) &&
265 user() && user()->login() == "anonymous" ) ) {
266 setState( IssuingChallenge );
268 if ( d->cachedNonce &&
269 d->cachedNonce->time + 1800 < (uint)time( 0 ) )
276 MD5::hash( A1 ).hex() +":"+
277 d->nonce +":"+ d->nc +":"+ d->cnonce +":"+ d->qop +":"+
278 MD5::hash( ":" + d->uri ).hex()
280 d->rspauth = R.hex();
285 if ( d->cachedNonce )
286 setState( IssuingChallenge );
292 void DigestMD5::setChallenge( const EString &s )
297 if ( !parse( s, l ) )
300 v = l.find( "realm" );
302 d->realm = v->value().unquoted();
304 v = l.find( "nonce" );
306 d->nonce = v->value().unquoted();
310 d->qop = v->value().unquoted();
314 static EString stripWSP( const EString & s )
316 if ( s.length() == 0 ) {
322 while ( i < s.length() && ( s[i] == '\t' || s[i] == ' ' ) )
325 uint j = s.length() - 1;
326 while ( j > i && ( s[j] == '\t' || s[j] == ' ' ) )
333 return s.mid( i, j - i + 1 );
337 /*! RFC 2831 defines "n#m( expr )" as a list containing at least n, and
338 at most m repetitions of expr, separated by commas, and optional
341 ( *LWS expr *( *LWS "," *LWS expr ) )
343 This function tries to parse \a s as #( name=["]value["] ), and adds
344 each element to the list \a l. It returns true if the entire string
345 could be parsed without error, and false otherwise.
347 If a name occurs more than once in the string, its value is appended
348 to the instance already in \a l.
351 bool DigestMD5::parse( const EString &s, List< Variable > &l )
353 if ( stripWSP( s ).isEmpty() )
358 // Find the beginning of the next element, skipping qdstr.
361 while ( s[i] != '\0' ) {
364 else if ( s[i] == '"' )
366 else if ( !quoted && s[i] == ',' )
371 // There's one list element between s[ start..i ].
372 EString elem = stripWSP( s.mid( start, i-start ) );
375 if ( !elem.isEmpty() ) {
376 int eq = elem.find( '=' );
380 // We should validate name and value.
381 EString name = stripWSP( elem.mid( 0, eq ) ).lower();
382 EString value = stripWSP( elem.mid( eq+1, elem.length()-eq ) );
383 Variable *v = l.find( name );
390 v->values.append( new EString( value ) );
392 } while ( start < s.length() );