2003-12-02 Gonzalo Paniagua Javier <gonzalo@ximian.com>
[mono.git] / mcs / class / System / System.Net / DigestClient.cs
1 //
2 // System.Net.DigestClient.cs
3 //
4 // Authors:
5 //      Greg Reinacker (gregr@rassoc.com)
6 //      Sebastien Pouliot (spouliot@motus.com)
7 //      Gonzalo Paniagua Javier (gonzalo@ximian.com
8 //
9 // Copyright 2002-2003 Greg Reinacker, Reinacker & Associates, Inc. All rights reserved.
10 // Portions (C) 2003 Motus Technologies Inc. (http://www.motus.com)
11 // (c) 2003 Novell, Inc. (http://www.novell.com)
12 //
13 // Original (server-side) source code available at
14 // http://www.rassoc.com/gregr/weblog/stories/2002/07/09/webServicesSecurityHttpDigestAuthenticationWithoutActiveDirectory.html
15 //
16
17 using System;
18 using System.Collections;
19 using System.Collections.Specialized;
20 using System.IO;
21 using System.Net;
22 using System.Security.Cryptography;
23 using System.Text;
24
25 namespace System.Net
26 {
27         //
28         // This works with apache mod_digest
29         //TODO:
30         //      MD5-sess
31         //      qop (auth-int)
32         //
33         //      See RFC 2617 for details.
34         //
35
36
37         class DigestHeaderParser
38         {
39                 string header;
40                 int length;
41                 int pos;
42                 string realm, opaque, nonce, algorithm;
43                 static string [] keywords = { "realm", "opaque", "nonce", "algorithm", "qop" };
44                 string [] values = new string [keywords.Length];
45
46                 public DigestHeaderParser (string header)
47                 {
48                         this.header = header.Trim ();
49                 }
50
51                 public string Realm {
52                         get { return values [0]; }
53                 }
54
55                 public string Opaque {
56                         get { return values [1]; }
57                 }
58
59                 public string Nonce {
60                         get { return values [2]; }
61                 }
62                 
63                 public string Algorithm {
64                         get { return values [3]; }
65                 }
66                 
67                 public string QOP {
68                         get { return values [4]; }
69                 }
70                 
71                 public bool Parse ()
72                 {
73                         if (!header.ToLower ().StartsWith ("digest "))
74                                 return false;
75
76                         pos = 6;
77                         length = this.header.Length;
78                         while (pos < length) {
79                                 string key, value;
80                                 if (!GetKeywordAndValue (out key, out value))
81                                         return false;
82
83                                 SkipWhitespace ();
84                                 if (pos < length && header [pos] == ',')
85                                         pos++;
86
87                                 int idx = Array.IndexOf (keywords, (key));
88                                 if (idx == -1)
89                                         continue;
90
91                                 if (values [idx] != null)
92                                         return false;
93
94                                 values [idx] = value;
95                         }
96
97                         if (Realm == null || Nonce == null)
98                                 return false;
99
100                         return true;
101                 }
102
103                 void SkipWhitespace ()
104                 {
105                         char c = ' ';
106                         while (pos < length && (c == ' ' || c == '\t' || c == '\r' || c == '\n')) {
107                                 c = header [pos++];
108                         }
109                         pos--;
110                 }
111                 
112                 void SkipNonWhitespace ()
113                 {
114                         char c = 'a';
115                         while (pos < length && c != ' ' && c != '\t' && c != '\r' && c != '\n') {
116                                 c = header [pos++];
117                         }
118                         pos--;
119                 }
120                 
121                 string GetKey ()
122                 {
123                         SkipWhitespace ();
124                         int begin = pos;
125                         while (pos < length && header [pos] != '=') {
126                                 pos++;
127                         }
128                         
129                         string key = header.Substring (begin, pos - begin).Trim ().ToLower ();
130                         return key;
131                 }
132
133                 bool GetKeywordAndValue (out string key, out string value)
134                 {
135                         key = null;
136                         value = null;
137                         key = GetKey ();
138                         if (pos >= length)
139                                 return false;
140
141                         SkipWhitespace ();
142                         if (pos + 1 >= length || header [pos++] != '=')
143                                 return false;
144
145                         SkipWhitespace ();
146                         if (pos + 1 >= length || header [pos++] != '"')
147                                 return false;
148
149                         int beginQ = pos;
150                         pos = header.IndexOf ('"', pos);
151                         if (pos == -1)
152                                 return false;
153
154                         value = header.Substring (beginQ, pos - beginQ);
155                         pos += 2;
156                         return true;
157                 }
158         }
159
160         class DigestSession
161         {
162                 static RandomNumberGenerator rng;
163                 
164                 static DigestSession () 
165                 {
166                         rng = RandomNumberGenerator.Create ();
167                 }
168
169                 private int _nc;
170                 private HashAlgorithm hash;
171                 private DigestHeaderParser parser;
172                 private string _cnonce;
173
174                 public DigestSession () 
175                 {
176                         _nc = 1;
177                 }
178
179                 public string Algorithm {
180                         get { return parser.Algorithm; }
181                 }
182
183                 public string Realm {
184                         get { return parser.Realm; }
185                 }
186
187                 public string Nonce {
188                         get { return parser.Nonce; }
189                 }
190
191                 public string Opaque {
192                         get { return parser.Opaque; }
193                 }
194
195                 public string QOP {
196                         get { return parser.QOP; }
197                 }
198
199                 public string CNonce {
200                         get { 
201                                 if (_cnonce == null) {
202                                         // 15 is a multiple of 3 which is better for base64 because it
203                                         // wont end with '=' and risk messing up the server parsing
204                                         byte[] bincnonce = new byte [15];
205                                         rng.GetBytes (bincnonce);
206                                         _cnonce = Convert.ToBase64String (bincnonce);
207                                         Array.Clear (bincnonce, 0, bincnonce.Length);
208                                 }
209                                 return _cnonce;
210                         }
211                 }
212
213                 public bool Parse (string challenge) 
214                 {
215                         parser = new DigestHeaderParser (challenge);
216                         if (!parser.Parse ()) {
217                                 Console.WriteLine ("Parser");
218                                 return false;
219                         }
220
221                         // build the hash object (only MD5 is defined in RFC2617)
222                         if ((parser.Algorithm == null) || (parser.Algorithm.ToUpper ().StartsWith ("MD5")))
223                                 hash = HashAlgorithm.Create ("MD5");
224
225                         return true;
226                 }
227
228                 private string HashToHexString (string toBeHashed) 
229                 {
230                         if (hash == null)
231                                 return null;
232
233                         hash.Initialize ();
234                         byte[] result = hash.ComputeHash (Encoding.ASCII.GetBytes (toBeHashed));
235
236                         StringBuilder sb = new StringBuilder ();
237                         foreach (byte b in result)
238                                 sb.Append (b.ToString ("x2"));
239                         return sb.ToString ();
240                 }
241
242                 private string HA1 (string username, string password) 
243                 {
244                         string ha1 = String.Format ("{0}:{1}:{2}", username, Realm, password);
245                         if (Algorithm != null && Algorithm.ToLower () == "md5-sess")
246                                 ha1 = String.Format ("{0}:{1}:{2}", HashToHexString (ha1), Nonce, CNonce);
247                         return HashToHexString (ha1);
248                 }
249
250                 private string HA2 (HttpWebRequest webRequest) 
251                 {
252                         string ha2 = String.Format ("{0}:{1}", webRequest.Method, webRequest.RequestUri.AbsolutePath);
253                         if (QOP == "auth-int") {
254                                 // TODO
255                                 // ha2 += String.Format (":{0}", hentity);
256                         }               
257                         return HashToHexString (ha2);
258                 }
259
260                 private string Response (string username, string password, HttpWebRequest webRequest) 
261                 {
262                         string response = String.Format ("{0}:{1}:", HA1 (username, password), Nonce);
263                         if (QOP != null)
264                                 response += String.Format ("{0}:{1}:{2}:", _nc.ToString ("x8"), CNonce, QOP);
265                         response += HA2 (webRequest);
266                         return HashToHexString (response);
267                 }
268
269                 public Authorization Authenticate (WebRequest webRequest, ICredentials credentials) 
270                 {
271                         if (parser == null)
272                                 throw new InvalidOperationException ();
273
274                         HttpWebRequest request = webRequest as HttpWebRequest;
275                         if (request == null)
276                                 return null;
277         
278                         NetworkCredential cred = credentials.GetCredential (request.RequestUri, "digest");
279                         string userName = cred.UserName;
280                         if (userName == null || userName == "")
281                                 return null;
282
283                         string password = cred.Password;
284         
285                         StringBuilder auth = new StringBuilder ();
286                         auth.AppendFormat ("Digest username=\"{0}\", ", userName);
287                         auth.AppendFormat ("realm=\"{0}\", ", Realm);
288                         auth.AppendFormat ("nonce=\"{0}\", ", Nonce);
289                         auth.AppendFormat ("uri=\"{0}\", ", request.Address.PathAndQuery);
290
291                         if (QOP != null) // quality of protection (server decision)
292                                 auth.AppendFormat ("qop=\"{0}\", ", QOP);
293
294                         if (Algorithm != null) // hash algorithm (only MD5 in RFC2617)
295                                 auth.AppendFormat ("algorithm=\"{0}\", ", Algorithm);
296
297                         lock (this) {
298                                 // _nc MUST NOT change from here...
299                                 // number of request using this nonce
300                                 if (QOP != null) {
301                                         auth.AppendFormat ("nc={0:X8}, ", _nc);
302                                         _nc++;
303                                 }
304                                 // until here, now _nc can change
305                         }
306
307                         if (QOP != null) // opaque value from the client
308                                 auth.AppendFormat ("cnonce=\"{0}\", ", CNonce);
309
310                         if (Opaque != null) // exact same opaque value as received from server
311                                 auth.AppendFormat ("opaque=\"{0}\", ", Opaque);
312
313                         auth.AppendFormat ("response=\"{0}\"", Response (userName, password, request));
314                         return new Authorization (auth.ToString ());
315                 }
316         }
317
318         class DigestClient : IAuthenticationModule
319         {
320
321                 static Hashtable cache;         // cache entries by nonce
322
323                 static DigestClient () 
324                 {
325                         cache = Hashtable.Synchronized (new Hashtable ());
326                 }
327         
328                 public DigestClient () {}
329         
330                 // IAuthenticationModule
331         
332                 public Authorization Authenticate (string challenge, WebRequest webRequest, ICredentials credentials) 
333                 {
334                         if (credentials == null || challenge == null)
335                                 return null;
336         
337                         string header = challenge.Trim ();
338                         if (!header.ToLower ().StartsWith ("digest "))
339                                 return null;
340
341                         HttpWebRequest request = webRequest as HttpWebRequest;
342                         if (request == null)
343                                 return null;
344
345                         DigestSession ds = (DigestSession) cache [request.Address];
346                         bool addDS = (ds == null);
347                         if (addDS)
348                                 ds = new DigestSession ();
349
350                         if (!ds.Parse (challenge))
351                                 return null;
352
353                         if (addDS)
354                                 cache.Add (request.Address, ds);
355
356                         return ds.Authenticate (webRequest, credentials);
357                 }
358
359                 public Authorization PreAuthenticate (WebRequest webRequest, ICredentials credentials) 
360                 {
361                         HttpWebRequest request = webRequest as HttpWebRequest;
362                         if (request == null)
363                                 return null;
364
365                         // check cache for URI
366                         DigestSession ds = (DigestSession) cache [request.Address];
367                         if (ds == null)
368                                 return null;
369
370                         return ds.Authenticate (webRequest, credentials);
371                 }
372         
373                 public string AuthenticationType { 
374                         get { return "Digest"; }
375                 }
376         
377                 public bool CanPreAuthenticate { 
378                         get { return true; }
379                 }
380         }
381 }
382