2004-02-18 Sebastien Pouliot <sebastien@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                 static string [] keywords = { "realm", "opaque", "nonce", "algorithm", "qop" };
43                 static char [] endSeparator = new char[] { '"', ',' };
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                         // note: Apache doesn't use " in all case (like algorithm)
147                         if (pos + 1 >= length)
148                                 return false;
149                         if (header [pos] == '"')
150                                 pos++;
151
152                         int beginQ = pos;
153                         pos = header.IndexOf ('"', pos);
154                         if (pos == -1)
155                                 return false;
156
157                         value = header.Substring (beginQ, pos - beginQ);
158                         pos += 2;
159                         return true;
160                 }
161         }
162
163         class DigestSession
164         {
165                 static RandomNumberGenerator rng;
166                 
167                 static DigestSession () 
168                 {
169                         rng = RandomNumberGenerator.Create ();
170                 }
171
172                 private int _nc;
173                 private HashAlgorithm hash;
174                 private DigestHeaderParser parser;
175                 private string _cnonce;
176
177                 public DigestSession () 
178                 {
179                         _nc = 1;
180                 }
181
182                 public string Algorithm {
183                         get { return parser.Algorithm; }
184                 }
185
186                 public string Realm {
187                         get { return parser.Realm; }
188                 }
189
190                 public string Nonce {
191                         get { return parser.Nonce; }
192                 }
193
194                 public string Opaque {
195                         get { return parser.Opaque; }
196                 }
197
198                 public string QOP {
199                         get { return parser.QOP; }
200                 }
201
202                 public string CNonce {
203                         get { 
204                                 if (_cnonce == null) {
205                                         // 15 is a multiple of 3 which is better for base64 because it
206                                         // wont end with '=' and risk messing up the server parsing
207                                         byte[] bincnonce = new byte [15];
208                                         rng.GetBytes (bincnonce);
209                                         _cnonce = Convert.ToBase64String (bincnonce);
210                                         Array.Clear (bincnonce, 0, bincnonce.Length);
211                                 }
212                                 return _cnonce;
213                         }
214                 }
215
216                 public bool Parse (string challenge) 
217                 {
218                         parser = new DigestHeaderParser (challenge);
219                         if (!parser.Parse ()) {
220                                 Console.WriteLine ("Parser");
221                                 return false;
222                         }
223
224                         // build the hash object (only MD5 is defined in RFC2617)
225                         if ((parser.Algorithm == null) || (parser.Algorithm.ToUpper ().StartsWith ("MD5")))
226                                 hash = HashAlgorithm.Create ("MD5");
227
228                         return true;
229                 }
230
231                 private string HashToHexString (string toBeHashed) 
232                 {
233                         if (hash == null)
234                                 return null;
235
236                         hash.Initialize ();
237                         byte[] result = hash.ComputeHash (Encoding.ASCII.GetBytes (toBeHashed));
238
239                         StringBuilder sb = new StringBuilder ();
240                         foreach (byte b in result)
241                                 sb.Append (b.ToString ("x2"));
242                         return sb.ToString ();
243                 }
244
245                 private string HA1 (string username, string password) 
246                 {
247                         string ha1 = String.Format ("{0}:{1}:{2}", username, Realm, password);
248                         if (Algorithm != null && Algorithm.ToLower () == "md5-sess")
249                                 ha1 = String.Format ("{0}:{1}:{2}", HashToHexString (ha1), Nonce, CNonce);
250                         return HashToHexString (ha1);
251                 }
252
253                 private string HA2 (HttpWebRequest webRequest) 
254                 {
255                         string ha2 = String.Format ("{0}:{1}", webRequest.Method, webRequest.RequestUri.AbsolutePath);
256                         if (QOP == "auth-int") {
257                                 // TODO
258                                 // ha2 += String.Format (":{0}", hentity);
259                         }               
260                         return HashToHexString (ha2);
261                 }
262
263                 private string Response (string username, string password, HttpWebRequest webRequest) 
264                 {
265                         string response = String.Format ("{0}:{1}:", HA1 (username, password), Nonce);
266                         if (QOP != null)
267                                 response += String.Format ("{0}:{1}:{2}:", _nc.ToString ("x8"), CNonce, QOP);
268                         response += HA2 (webRequest);
269                         return HashToHexString (response);
270                 }
271
272                 public Authorization Authenticate (WebRequest webRequest, ICredentials credentials) 
273                 {
274                         if (parser == null)
275                                 throw new InvalidOperationException ();
276
277                         HttpWebRequest request = webRequest as HttpWebRequest;
278                         if (request == null)
279                                 return null;
280         
281                         NetworkCredential cred = credentials.GetCredential (request.RequestUri, "digest");
282                         string userName = cred.UserName;
283                         if (userName == null || userName == "")
284                                 return null;
285
286                         string password = cred.Password;
287         
288                         StringBuilder auth = new StringBuilder ();
289                         auth.AppendFormat ("Digest username=\"{0}\", ", userName);
290                         auth.AppendFormat ("realm=\"{0}\", ", Realm);
291                         auth.AppendFormat ("nonce=\"{0}\", ", Nonce);
292                         auth.AppendFormat ("uri=\"{0}\", ", request.Address.PathAndQuery);
293
294                         if (QOP != null) // quality of protection (server decision)
295                                 auth.AppendFormat ("qop=\"{0}\", ", QOP);
296
297                         if (Algorithm != null) // hash algorithm (only MD5 in RFC2617)
298                                 auth.AppendFormat ("algorithm=\"{0}\", ", Algorithm);
299
300                         lock (this) {
301                                 // _nc MUST NOT change from here...
302                                 // number of request using this nonce
303                                 if (QOP != null) {
304                                         auth.AppendFormat ("nc={0:X8}, ", _nc);
305                                         _nc++;
306                                 }
307                                 // until here, now _nc can change
308                         }
309
310                         if (QOP != null) // opaque value from the client
311                                 auth.AppendFormat ("cnonce=\"{0}\", ", CNonce);
312
313                         if (Opaque != null) // exact same opaque value as received from server
314                                 auth.AppendFormat ("opaque=\"{0}\", ", Opaque);
315
316                         auth.AppendFormat ("response=\"{0}\"", Response (userName, password, request));
317                         return new Authorization (auth.ToString ());
318                 }
319         }
320
321         class DigestClient : IAuthenticationModule
322         {
323
324                 static Hashtable cache;         // cache entries by nonce
325
326                 static DigestClient () 
327                 {
328                         cache = Hashtable.Synchronized (new Hashtable ());
329                 }
330         
331                 public DigestClient () {}
332         
333                 // IAuthenticationModule
334         
335                 public Authorization Authenticate (string challenge, WebRequest webRequest, ICredentials credentials) 
336                 {
337                         if (credentials == null || challenge == null)
338                                 return null;
339         
340                         string header = challenge.Trim ();
341                         if (header.ToLower ().IndexOf ("digest") == -1)
342                                 return null;
343
344                         HttpWebRequest request = webRequest as HttpWebRequest;
345                         if (request == null)
346                                 return null;
347
348                         DigestSession ds = (DigestSession) cache [request.Address];
349                         bool addDS = (ds == null);
350                         if (addDS)
351                                 ds = new DigestSession ();
352
353                         if (!ds.Parse (challenge))
354                                 return null;
355
356                         if (addDS)
357                                 cache.Add (request.Address, ds);
358
359                         return ds.Authenticate (webRequest, credentials);
360                 }
361
362                 public Authorization PreAuthenticate (WebRequest webRequest, ICredentials credentials) 
363                 {
364                         HttpWebRequest request = webRequest as HttpWebRequest;
365                         if (request == null)
366                                 return null;
367
368                         // check cache for URI
369                         DigestSession ds = (DigestSession) cache [request.Address];
370                         if (ds == null)
371                                 return null;
372
373                         return ds.Authenticate (webRequest, credentials);
374                 }
375         
376                 public string AuthenticationType { 
377                         get { return "Digest"; }
378                 }
379         
380                 public bool CanPreAuthenticate { 
381                         get { return true; }
382                 }
383         }
384 }
385