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