1   // ========================================================================
2   // Copyright 2002-2005 Mort Bay Consulting Pty. Ltd.
3   // ------------------------------------------------------------------------
4   // Licensed under the Apache License, Version 2.0 (the "License");
5   // you may not use this file except in compliance with the License.
6   // You may obtain a copy of the License at 
7   // http://www.apache.org/licenses/LICENSE-2.0
8   // Unless required by applicable law or agreed to in writing, software
9   // distributed under the License is distributed on an "AS IS" BASIS,
10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11  // See the License for the specific language governing permissions and
12  // limitations under the License.
13  // ========================================================================
14  
15  package org.mortbay.jetty.security;
16  
17  import java.io.IOException;
18  import java.security.MessageDigest;
19  import java.security.Principal;
20  
21  import javax.servlet.http.HttpServletResponse;
22  
23  import org.mortbay.jetty.Authenticator;
24  import org.mortbay.jetty.HttpHeaders;
25  import org.mortbay.jetty.Request;
26  import org.mortbay.jetty.Response;
27  import org.mortbay.jetty.UserRealm;
28  import org.mortbay.log.Log;
29  import org.mortbay.util.QuotedStringTokenizer;
30  import org.mortbay.util.StringUtil;
31  import org.mortbay.util.TypeUtil;
32  
33  /* ------------------------------------------------------------ */
34  /** DIGEST authentication.
35   *
36   * @author Greg Wilkins (gregw)
37   */
38  public class DigestAuthenticator implements Authenticator
39  {
40      protected long maxNonceAge=0;
41      protected long nonceSecret=this.hashCode() ^ System.currentTimeMillis();
42      protected boolean useStale=false;
43      
44      
45      /* ------------------------------------------------------------ */
46      /** 
47       * @return UserPrinciple if authenticated or null if not. If
48       * Authentication fails, then the authenticator may have committed
49       * the response as an auth challenge or redirect.
50       * @exception IOException 
51       */
52      public Principal authenticate(UserRealm realm,
53                                             String pathInContext,
54                                             Request request,
55                                             Response response)
56          throws IOException
57      {
58          // Get the user if we can
59          boolean stale=false;
60          Principal user=null;
61          String credentials = request.getHeader(HttpHeaders.AUTHORIZATION);
62          
63          if (credentials!=null )
64          {
65              if(Log.isDebugEnabled())Log.debug("Credentials: "+credentials);
66              QuotedStringTokenizer tokenizer = new QuotedStringTokenizer(credentials,
67                                                                          "=, ",
68                                                                          true,
69                                                                          false);
70              Digest digest=new Digest(request.getMethod());
71              String last=null;
72              String name=null;
73  
74            loop:
75              while (tokenizer.hasMoreTokens())
76              {
77                  String tok = tokenizer.nextToken();
78                  char c=(tok.length()==1)?tok.charAt(0):'\0';
79  
80                  switch (c)
81                  {
82                    case '=':
83                        name=last;
84                        last=tok;
85                        break;
86                    case ',':
87                        name=null;
88                    case ' ':
89                        break;
90  
91                    default:
92                        last=tok;
93                        if (name!=null)
94                        {
95                            if ("username".equalsIgnoreCase(name))
96                                digest.username=tok;
97                            else if ("realm".equalsIgnoreCase(name))
98                                digest.realm=tok;
99                            else if ("nonce".equalsIgnoreCase(name))
100                               digest.nonce=tok;
101                           else if ("nc".equalsIgnoreCase(name))
102                               digest.nc=tok;
103                           else if ("cnonce".equalsIgnoreCase(name))
104                               digest.cnonce=tok;
105                           else if ("qop".equalsIgnoreCase(name))
106                               digest.qop=tok;
107                           else if ("uri".equalsIgnoreCase(name))
108                               digest.uri=tok;
109                           else if ("response".equalsIgnoreCase(name))
110                               digest.response=tok;
111                           break;
112                       }
113                 }
114             }            
115 
116             int n=checkNonce(digest.nonce,request);
117             if (n>0)
118                 user = realm.authenticate(digest.username,digest,request);
119             else if (n==0)
120                 stale = true;
121             
122             if (user==null)
123                 Log.warn("AUTH FAILURE: user "+StringUtil.printable(digest.username));
124             else    
125             {
126                 request.setAuthType(Constraint.__DIGEST_AUTH);
127                 request.setUserPrincipal(user);                
128             }
129         }
130 
131         // Challenge if we have no user
132         if (user==null && response!=null)
133             sendChallenge(realm,request,response,stale);
134         
135         return user;
136     }
137     
138     /* ------------------------------------------------------------ */
139     public String getAuthMethod()
140     {
141         return Constraint.__DIGEST_AUTH;
142     }
143     
144     /* ------------------------------------------------------------ */
145     public void sendChallenge(UserRealm realm,
146                               Request request,
147                               Response response,
148                               boolean stale)
149         throws IOException
150     {
151         String domain=request.getContextPath();
152         if (domain==null)
153             domain="/";
154         response.setHeader(HttpHeaders.WWW_AUTHENTICATE,
155 			    "Digest realm=\""+realm.getName()+
156 			    "\", domain=\""+domain +
157 			    "\", nonce=\""+newNonce(request)+
158 			    "\", algorithm=MD5, qop=\"auth\"" + (useStale?(" stale="+stale):"")
159                           );
160         response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
161     }
162 
163     /* ------------------------------------------------------------ */
164     public String newNonce(Request request)
165     {
166         long ts=request.getTimeStamp();
167         long sk=nonceSecret;
168         
169         byte[] nounce = new byte[24];
170         for (int i=0;i<8;i++)
171         {
172             nounce[i]=(byte)(ts&0xff);
173             ts=ts>>8;
174             nounce[8+i]=(byte)(sk&0xff);
175             sk=sk>>8;
176         }
177         
178         byte[] hash=null;
179         try
180         {
181             MessageDigest md = MessageDigest.getInstance("MD5");
182             md.reset();
183             md.update(nounce,0,16);
184             hash = md.digest();
185         }
186         catch(Exception e)
187         {
188             Log.warn(e);
189         }
190         
191         for (int i=0;i<hash.length;i++)
192         {
193             nounce[8+i]=hash[i];
194             if (i==23)
195                 break;
196         }
197         
198         return new String(B64Code.encode(nounce));
199     }
200 
201     /**
202      * @param nonce
203      * @param request
204      * @return -1 for a bad nonce, 0 for a stale none, 1 for a good nonce
205      */
206     /* ------------------------------------------------------------ */
207     public int checkNonce(String nonce, Request request)
208     {
209         try
210         {
211             byte[] n = B64Code.decode(nonce.toCharArray());
212             if (n.length!=24)
213                 return -1;
214             
215             long ts=0;
216             long sk=nonceSecret;
217             byte[] n2 = new byte[16];
218             System.arraycopy(n, 0, n2, 0, 8);
219             for (int i=0;i<8;i++)
220             {
221                 n2[8+i]=(byte)(sk&0xff);
222                 sk=sk>>8;
223                 ts=(ts<<8)+(0xff&(long)n[7-i]);
224             }
225             
226             long age=request.getTimeStamp()-ts;
227             if (Log.isDebugEnabled()) Log.debug("age="+age);
228             
229             byte[] hash=null;
230             try
231             {
232                 MessageDigest md = MessageDigest.getInstance("MD5");
233                 md.reset();
234                 md.update(n2,0,16);
235                 hash = md.digest();
236             }
237             catch(Exception e)
238             {
239                 Log.warn(e);
240             }
241             
242             for (int i=0;i<16;i++)
243                 if (n[i+8]!=hash[i])
244                     return -1;
245                 
246             if(maxNonceAge>0 && (age<0 || age>maxNonceAge))
247                 return 0; // stale
248             
249             return 1;
250         }
251         catch(Exception e)
252         {
253             Log.ignore(e);
254         }
255         return -1;
256     }
257 
258     /* ------------------------------------------------------------ */
259     /* ------------------------------------------------------------ */
260     /* ------------------------------------------------------------ */
261     private static class Digest extends Credential
262     {
263         String method=null;
264         String username = null;
265         String realm = null;
266         String nonce = null;
267         String nc = null;
268         String cnonce = null;
269         String qop = null;
270         String uri = null;
271         String response=null;
272         
273         /* ------------------------------------------------------------ */
274         Digest(String m)
275         {
276             method=m;
277         }
278         
279         /* ------------------------------------------------------------ */
280         public boolean check(Object credentials)
281         {
282             String password=(credentials instanceof String)
283                 ?(String)credentials
284                 :credentials.toString();
285             
286             try{
287                 MessageDigest md = MessageDigest.getInstance("MD5");
288                 byte[] ha1;
289                 if(credentials instanceof Credential.MD5)
290                 {
291                     // Credentials are already a MD5 digest - assume it's in
292                     // form user:realm:password (we have no way to know since 
293                     // it's a digest, alright?)
294                     ha1 = ((Credential.MD5)credentials).getDigest();
295                 }
296                 else
297                 {
298                     // calc A1 digest
299                     md.update(username.getBytes(StringUtil.__ISO_8859_1));
300                     md.update((byte)':');
301                     md.update(realm.getBytes(StringUtil.__ISO_8859_1));
302                     md.update((byte)':');
303                     md.update(password.getBytes(StringUtil.__ISO_8859_1));
304                     ha1=md.digest();
305                 }
306                 // calc A2 digest
307                 md.reset();
308                 md.update(method.getBytes(StringUtil.__ISO_8859_1));
309                 md.update((byte)':');
310                 md.update(uri.getBytes(StringUtil.__ISO_8859_1));
311                 byte[] ha2=md.digest();
312                 
313                 
314                 
315                 
316                 
317                 // calc digest
318                 // request-digest  = <"> < KD ( H(A1), unq(nonce-value) ":" nc-value ":" unq(cnonce-value) ":" unq(qop-value) ":" H(A2) ) <">
319                 // request-digest  = <"> < KD ( H(A1), unq(nonce-value) ":" H(A2) ) > <">
320 
321                 
322                 
323                 md.update(TypeUtil.toString(ha1,16).getBytes(StringUtil.__ISO_8859_1));
324                 md.update((byte)':');
325                 md.update(nonce.getBytes(StringUtil.__ISO_8859_1));
326                 md.update((byte)':');
327                 md.update(nc.getBytes(StringUtil.__ISO_8859_1));
328                 md.update((byte)':');
329                 md.update(cnonce.getBytes(StringUtil.__ISO_8859_1));
330                 md.update((byte)':');
331                 md.update(qop.getBytes(StringUtil.__ISO_8859_1));
332                 md.update((byte)':');
333                 md.update(TypeUtil.toString(ha2,16).getBytes(StringUtil.__ISO_8859_1));
334                 byte[] digest=md.digest();
335                 
336                 // check digest
337                 return (TypeUtil.toString(digest,16).equalsIgnoreCase(response));
338             }
339             catch (Exception e)
340             {Log.warn(e);}
341 
342             return false;
343         }
344 
345         public String toString()
346         {
347             return username+","+response;
348         }
349         
350     }
351     /**
352      * @return Returns the maxNonceAge.
353      */
354     public long getMaxNonceAge()
355     {
356         return maxNonceAge;
357     }
358     /**
359      * @param maxNonceAge The maxNonceAge to set.
360      */
361     public void setMaxNonceAge(long maxNonceAge)
362     {
363         this.maxNonceAge = maxNonceAge;
364     }
365     /**
366      * @return Returns the nonceSecret.
367      */
368     public long getNonceSecret()
369     {
370         return nonceSecret;
371     }
372     /**
373      * @param nonceSecret The nonceSecret to set.
374      */
375     public void setNonceSecret(long nonceSecret)
376     {
377         this.nonceSecret = nonceSecret;
378     }
379 
380     public void setUseStale(boolean us)
381     {
382 	this.useStale=us;
383     }
384 
385     public boolean getUseStale()
386     {
387 	return useStale;
388     }
389 }
390