Merge branch 'master' of ssh://down/oryx/aox
[aox:aox.git] / sasl / digest-md5.cpp
1 // Copyright Oryx Mail Systems GmbH. All enquiries to info@oryx.com, please.
2
3 #include "digest-md5.h"
4
5 #include "configuration.h"
6 #include "allocator.h"
7 #include "entropy.h"
8 #include "estring.h"
9 #include "list.h"
10 #include "user.h"
11 #include "md5.h"
12
13 #include <time.h>
14
15
16 struct Nonce
17     : public Garbage
18 {
19     EString value;
20     EString count;
21     uint time;
22 };
23
24 static List<Nonce> * cache;
25
26
27 class DigestData
28     : public Garbage
29 {
30 public:
31     DigestData()
32         : stale( false ), cachedNonce( 0 )
33     {}
34
35     bool stale;
36     EString rspauth;
37     EString realm, nonce, qop;
38     EString cnonce, nc, response, uri;
39     Nonce *cachedNonce;
40 };
41
42
43 /*! \class DigestMD5 digest-md5.h
44     Implements SASL DIGEST-MD5 authentication (RFC 2831)
45
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.
50
51     We don't support a SASL initial response for this mechanism yet; nor
52     do we support its use for anything but authentication.
53 */
54
55
56 /*! Creates a digest-md5 SASL authentication object on behalf of \a c */
57
58 DigestMD5::DigestMD5( EventHandler *c )
59     : SaslMechanism( c, SaslMechanism::DigestMD5 ), d( new DigestData )
60 {
61     setState( AwaitingInitialResponse );
62     d->realm = Configuration::hostname();
63 }
64
65
66 EString DigestMD5::challenge()
67 {
68     EString r;
69
70     if ( d->rspauth.isEmpty() ) {
71         d->nonce = Entropy::asString( 48 ).e64();
72         d->qop = "auth";
73
74         r = "realm=\"" + d->realm + "\", " +
75             "nonce=\"" + d->nonce + "\", " +
76             "qop=\""   + d->qop   + "\", " +
77             "algorithm=md5-sess";
78
79         if ( d->stale )
80             r.append( ", stale=true" );
81     }
82     else {
83         r = "rspauth=" + d->rspauth;
84     }
85
86     return r;
87 }
88
89
90 void DigestMD5::parseResponse( const EString &r )
91 {
92     // Is this a response to our second challenge?
93     if ( !d->rspauth.isEmpty() ) {
94         if ( !r.isEmpty() ) {
95             setState( Failed );
96         }
97         else {
98             uint t = time(0);
99
100             // Update our nonce cache for successful authentication.
101             if ( d->cachedNonce ) {
102                 d->cachedNonce->count = d->nc;
103                 d->cachedNonce->time = t;
104             }
105             else {
106                 Nonce *cn = new Nonce;
107                 cn->value = d->nonce;
108                 cn->count = d->nc;
109                 cn->time = t;
110
111                 if ( !cache ) {
112                     cache = new List<Nonce>;
113                     Allocator::addEternal( cache, "Digest-MD5 nonce cache" );
114                 }
115                 cache->append( cn );
116                 if ( cache->count() > 128 )
117                     delete cache->shift();
118             }
119             setState( Succeeded );
120         }
121         execute();
122         return;
123     }
124
125     // Parse the first client response.
126     List< Variable > l;
127
128     bool ok = parse( r, l );
129
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" );
138
139     if ( !ok || l.isEmpty() ) {
140         log( "Empty/unparsable DIGEST-MD5 response: <<" + r + ">>",
141              Log::Error );
142         setState( Failed );
143         return;
144     }
145
146     require( user, "user" );
147     require( realm, "realm" );
148     require( nonce, "nonce" );
149     require( cnonce, "cnonce" );
150     require( uri, "uri" );
151
152     EString s;
153     if ( qop && ( !qop->unique() || qop->value() != "auth" ) ) {
154         s = "qop invalid in DIGEST-MD5 response: " + qop->value();
155         setState( Failed );
156     }
157     if ( !nc ) {
158         s = "nc not present in DIGEST-MD5 response";
159         setState( Failed );
160     }
161     else if ( !nc->unique() ) {
162         s = "nc not unique in DIGEST-MD5 response";
163         setState( Failed );
164     }
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";
168         setState( Failed );
169     }
170     if ( !resp ) {
171         s = "resp not present in DIGEST-MD5 response";
172         setState( Failed );
173     }
174     else if ( !resp->unique() ) {
175         s = "resp not unique in DIGEST-MD5 response";
176         setState( Failed );
177     }
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";
181         setState( Failed );
182     }
183     if ( state() == Failed ) {
184         log( "Full DIGEST-MD5 response was: <<" + r + ">>", Log::Debug );
185         log( s, Log::Error );
186         return;
187     }
188
189     uint n = nc->value().number( &ok, 16 );
190
191     d->cachedNonce = 0;
192     if ( ok && state() == AwaitingInitialResponse ) {
193         EString ncv = nonce->value().unquoted();
194         List< Nonce >::Iterator it( cache );
195         while ( it ) {
196             if ( it->value == ncv )
197                 break;
198             ++it;
199         }
200
201         if ( !it || n != it->count.number( 0, 16 )+1 ) {
202             setState( IssuingChallenge );
203             return;
204         }
205         else {
206             d->cachedNonce = it;
207             d->nonce = it->value;
208         }
209     }
210     else if ( !ok || nonce->value().unquoted() != d->nonce || n != 1 ) {
211         log( "DIGEST-MD5 response with bad nonce/nc.", Log::Error );
212         setState( Failed );
213         return;
214     }
215
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();
221     d->qop = "auth";
222     d->nc = nc->value();
223     execute();
224 }
225
226
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.
230 */
231
232 void DigestMD5::require( class Variable * v, const EString & n )
233 {
234     EString l;
235     if ( !v )
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";
241     if ( l.isEmpty() )
242         return;
243
244     log( l, Log::Debug );
245     setState( Failed );
246 }
247
248
249 void DigestMD5::verify()
250 {
251     EString R, A1, A2;
252
253     A1 = MD5::hash( login().utf8() +":"+ d->realm +":"+ storedSecret().utf8() )
254          +":"+ d->nonce +":"+ d->cnonce;
255     A2 = "AUTHENTICATE:" + d->uri;
256
257     R = MD5::hash(
258         MD5::hash( A1 ).hex() +":"+
259         d->nonce +":"+ d->nc +":"+ d->cnonce +":"+ d->qop +":"+
260         MD5::hash( A2 ).hex()
261     );
262
263     if ( R.hex() == d->response ||
264          ( Configuration::toggle( Configuration::AuthAnonymous ) &&
265            user() && user()->login() == "anonymous" ) ) {
266         setState( IssuingChallenge );
267
268         if ( d->cachedNonce &&
269              d->cachedNonce->time + 1800 < (uint)time( 0 ) )
270         {
271             d->stale = true;
272             return;
273         }
274
275         R = MD5::hash(
276             MD5::hash( A1 ).hex() +":"+
277             d->nonce +":"+ d->nc +":"+ d->cnonce +":"+ d->qop +":"+
278             MD5::hash( ":" + d->uri ).hex()
279         );
280         d->rspauth = R.hex();
281         execute();
282         return;
283     }
284
285     if ( d->cachedNonce )
286         setState( IssuingChallenge );
287     else
288         setState( Failed );
289 }
290
291
292 void DigestMD5::setChallenge( const EString &s )
293 {
294     List< Variable > l;
295     Variable *v;
296
297     if ( !parse( s, l ) )
298         return;
299
300     v = l.find( "realm" );
301     if ( v )
302         d->realm = v->value().unquoted();
303
304     v = l.find( "nonce" );
305     if ( v )
306         d->nonce = v->value().unquoted();
307
308     v = l.find( "qop" );
309     if ( v )
310         d->qop = v->value().unquoted();
311 }
312
313
314 static EString stripWSP( const EString & s )
315 {
316     if ( s.length() == 0 ) {
317         EString r;
318         return r;
319     }
320
321     uint i = 0;
322     while ( i < s.length() && ( s[i] == '\t' || s[i] == ' ' ) )
323         i++;
324
325     uint j = s.length() - 1;
326     while ( j > i && ( s[j] == '\t' || s[j] == ' ' ) )
327         j--;
328
329     if ( i > j ) {
330         EString r;
331         return r;
332     }
333     return s.mid( i, j - i + 1 );
334 }
335
336
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
339     linear white space:
340
341         ( *LWS expr *( *LWS "," *LWS expr ) )
342
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.
346
347     If a name occurs more than once in the string, its value is appended
348     to the instance already in \a l.
349 */
350
351 bool DigestMD5::parse( const EString &s, List< Variable > &l )
352 {
353     if ( stripWSP( s ).isEmpty() )
354         return true;
355
356     uint start = 0;
357     do {
358         // Find the beginning of the next element, skipping qdstr.
359         int i = start;
360         bool quoted = false;
361         while ( s[i] != '\0' ) {
362             if ( s[i] == '\\' )
363                 i++;
364             else if ( s[i] == '"' )
365                 quoted = !quoted;
366             else if ( !quoted && s[i] == ',' )
367                 break;
368             i++;
369         }
370
371         // There's one list element between s[ start..i ].
372         EString elem = stripWSP( s.mid( start, i-start ) );
373         start = i+1;
374
375         if ( !elem.isEmpty() ) {
376             int eq = elem.find( '=' );
377             if ( eq < 0 )
378                 return false;
379
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 );
384
385             if ( !v ) {
386                 v = new Variable;
387                 v->name = name;
388                 l.append( v );
389             }
390             v->values.append( new EString( value ) );
391         }
392     } while ( start < s.length() );
393
394     return true;
395 }