1 /*
   2  * Copyright 2005-2008 Sun Microsystems, Inc.  All Rights Reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Sun designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Sun in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
  22  * CA 95054 USA or visit www.sun.com if you need additional information or
  23  * have any questions.
  24  */
  25 
  26 package sun.net.www.protocol.http.ntlm;
  27 
  28 import java.io.IOException;
  29 import java.io.UnsupportedEncodingException;
  30 import java.net.InetAddress;
  31 import java.net.PasswordAuthentication;
  32 import java.net.UnknownHostException;
  33 import java.net.URL;
  34 import java.security.GeneralSecurityException;
  35 import java.security.MessageDigest;
  36 import java.security.NoSuchAlgorithmException;
  37 import javax.crypto.Cipher;
  38 import javax.crypto.NoSuchPaddingException;
  39 import javax.crypto.SecretKey;
  40 import javax.crypto.SecretKeyFactory;
  41 import javax.crypto.spec.DESKeySpec;
  42 
  43 import sun.net.www.HeaderParser;
  44 import sun.net.www.protocol.http.AuthenticationInfo;
  45 import sun.net.www.protocol.http.AuthScheme;
  46 import sun.net.www.protocol.http.HttpURLConnection;
  47 
  48 /**
  49  * NTLMAuthentication:
  50  *
  51  * @author Michael McMahon
  52  */
  53 
  54 /*
  55  * NTLM authentication is nominally based on the framework defined in RFC2617,
  56  * but differs from the standard (Basic & Digest) schemes as follows:
  57  *
  58  * 1. A complete authentication requires three request/response transactions
  59  *    as shown below:
  60  *            REQ ------------------------------->
  61  *            <---- 401 (signalling NTLM) --------
  62  *
  63  *            REQ (with type1 NTLM msg) --------->
  64  *            <---- 401 (with type 2 NTLM msg) ---
  65  *
  66  *            REQ (with type3 NTLM msg) --------->
  67  *            <---- OK ---------------------------
  68  *
  69  * 2. The scope of the authentication is the TCP connection (which must be kept-alive)
  70  *    after the type2 response is received. This means that NTLM does not work end-to-end
  71  *    through a proxy, rather between client and proxy, or between client and server (with no proxy)
  72  */
  73 
  74 public class NTLMAuthentication extends AuthenticationInfo {
  75     private static final long serialVersionUID = -2403849171106437142L;
  76 
  77     private byte[] type1;
  78     private byte[] type3;
  79 
  80     private SecretKeyFactory fac;
  81     private Cipher cipher;
  82     private MessageDigest md4;
  83     private String hostname;
  84     private static String defaultDomain; /* Domain to use if not specified by user */
  85 
  86     static {
  87         defaultDomain = java.security.AccessController.doPrivileged(
  88             new sun.security.action.GetPropertyAction("http.auth.ntlm.domain",
  89                                                       "domain"));
  90     };
  91 
  92     public static boolean supportsTransparentAuth () {
  93         return false;
  94     }
  95 
  96     private void init0() {
  97         type1 = new byte[256];
  98         type3 = new byte[256];
  99         System.arraycopy (new byte[] {'N','T','L','M','S','S','P',0,1}, 0, type1, 0, 9);
 100         type1[12] = (byte) 3;
 101         type1[13] = (byte) 0xb2;
 102         type1[28] = (byte) 0x20;
 103         System.arraycopy (new byte[] {'N','T','L','M','S','S','P',0,3}, 0, type3, 0, 9);
 104         type3[12] = (byte) 0x18;
 105         type3[14] = (byte) 0x18;
 106         type3[20] = (byte) 0x18;
 107         type3[22] = (byte) 0x18;
 108         type3[32] = (byte) 0x40;
 109         type3[60] = (byte) 1;
 110         type3[61] = (byte) 0x82;
 111 
 112         try {
 113             hostname = java.security.AccessController.doPrivileged(
 114                 new java.security.PrivilegedAction<String>() {
 115                 public String run() {
 116                     String localhost;
 117                     try {
 118                         localhost = InetAddress.getLocalHost().getHostName().toUpperCase();
 119                     } catch (UnknownHostException e) {
 120                          localhost = "localhost";
 121                     }
 122                     return localhost;
 123                 }
 124             });
 125             int x = hostname.indexOf ('.');
 126             if (x != -1) {
 127                 hostname = hostname.substring (0, x);
 128             }
 129             fac = SecretKeyFactory.getInstance ("DES");
 130             cipher = Cipher.getInstance ("DES/ECB/NoPadding");
 131             md4 = sun.security.provider.MD4.getInstance();
 132         } catch (NoSuchPaddingException e) {
 133             assert false;
 134         } catch (NoSuchAlgorithmException e) {
 135             assert false;
 136         }
 137     };
 138 
 139     PasswordAuthentication pw;
 140     String username;
 141     String ntdomain;
 142     String password;
 143 
 144     /**
 145      * Create a NTLMAuthentication:
 146      * Username may be specified as domain<BACKSLASH>username in the application Authenticator.
 147      * If this notation is not used, then the domain will be taken
 148      * from a system property: "http.auth.ntlm.domain".
 149      */
 150     public NTLMAuthentication(boolean isProxy, URL url, PasswordAuthentication pw) {
 151         super(isProxy ? PROXY_AUTHENTICATION : SERVER_AUTHENTICATION,
 152                 AuthScheme.NTLM,
 153                 url,
 154                 "");
 155         init (pw);
 156     }
 157 
 158     private void init (PasswordAuthentication pw) {
 159         this.pw = pw;
 160         String s = pw.getUserName();
 161         int i = s.indexOf ('\\');
 162         if (i == -1) {
 163             username = s;
 164             ntdomain = defaultDomain;
 165         } else {
 166             ntdomain = s.substring (0, i).toUpperCase();
 167             username = s.substring (i+1);
 168         }
 169         password = new String (pw.getPassword());
 170         init0();
 171     }
 172 
 173    /**
 174     * Constructor used for proxy entries
 175     */
 176     public NTLMAuthentication(boolean isProxy, String host, int port,
 177                                 PasswordAuthentication pw) {
 178         super(isProxy ? PROXY_AUTHENTICATION : SERVER_AUTHENTICATION,
 179                 AuthScheme.NTLM,
 180                 host,
 181                 port,
 182                 "");
 183         init (pw);
 184     }
 185 
 186     /**
 187      * @return true if this authentication supports preemptive authorization
 188      */
 189     @Override
 190     public boolean supportsPreemptiveAuthorization() {
 191         return false;
 192     }
 193 
 194     /**
 195      * Not supported. Must use the setHeaders() method
 196      */
 197     @Override
 198     public String getHeaderValue(URL url, String method) {
 199         throw new RuntimeException ("getHeaderValue not supported");
 200     }
 201 
 202     /**
 203      * Check if the header indicates that the current auth. parameters are stale.
 204      * If so, then replace the relevant field with the new value
 205      * and return true. Otherwise return false.
 206      * returning true means the request can be retried with the same userid/password
 207      * returning false means we have to go back to the user to ask for a new
 208      * username password.
 209      */
 210     @Override
 211     public boolean isAuthorizationStale (String header) {
 212         return false; /* should not be called for ntlm */
 213     }
 214 
 215     /**
 216      * Set header(s) on the given connection.
 217      * @param conn The connection to apply the header(s) to
 218      * @param p A source of header values for this connection, not used because
 219      *          HeaderParser converts the fields to lower case, use raw instead
 220      * @param raw The raw header field.
 221      * @return true if all goes well, false if no headers were set.
 222      */
 223     @Override
 224     public synchronized boolean setHeaders(HttpURLConnection conn, HeaderParser p, String raw) {
 225 
 226         try {
 227             String response;
 228             if (raw.length() < 6) { /* NTLM<sp> */
 229                 response = buildType1Msg ();
 230             } else {
 231                 String msg = raw.substring (5); /* skip NTLM<sp> */
 232                 response = buildType3Msg (msg);
 233             }
 234             conn.setAuthenticationProperty(getHeaderName(), response);
 235             return true;
 236         } catch (IOException e) {
 237             return false;
 238         } catch (GeneralSecurityException e) {
 239             return false;
 240         }
 241     }
 242 
 243     private void copybytes (byte[] dest, int destpos, String src, String enc) {
 244         try {
 245             byte[] x = src.getBytes(enc);
 246             System.arraycopy (x, 0, dest, destpos, x.length);
 247         } catch (UnsupportedEncodingException e) {
 248             assert false;
 249         }
 250     }
 251 
 252     private String buildType1Msg () {
 253         int dlen = ntdomain.length();
 254         type1[16]= (byte) (dlen % 256);
 255         type1[17]= (byte) (dlen / 256);
 256         type1[18] = type1[16];
 257         type1[19] = type1[17];
 258 
 259         int hlen = hostname.length();
 260         type1[24]= (byte) (hlen % 256);
 261         type1[25]= (byte) (hlen / 256);
 262         type1[26] = type1[24];
 263         type1[27] = type1[25];
 264 
 265         copybytes (type1, 32, hostname, "ISO8859_1");
 266         copybytes (type1, hlen+32, ntdomain, "ISO8859_1");
 267         type1[20] = (byte) ((hlen+32) % 256);
 268         type1[21] = (byte) ((hlen+32) / 256);
 269 
 270         byte[] msg = new byte [32 + hlen + dlen];
 271         System.arraycopy (type1, 0, msg, 0, 32 + hlen + dlen);
 272         String result = "NTLM " + (new B64Encoder()).encode (msg);
 273         return result;
 274     }
 275 
 276 
 277     /* Convert a 7 byte array to an 8 byte array (for a des key with parity)
 278      * input starts at offset off
 279      */
 280     private byte[] makeDesKey (byte[] input, int off) {
 281         int[] in = new int [input.length];
 282         for (int i=0; i<in.length; i++ ) {
 283             in[i] = input[i]<0 ? input[i]+256: input[i];
 284         }
 285         byte[] out = new byte[8];
 286         out[0] = (byte)in[off+0];
 287         out[1] = (byte)(((in[off+0] << 7) & 0xFF) | (in[off+1] >> 1));
 288         out[2] = (byte)(((in[off+1] << 6) & 0xFF) | (in[off+2] >> 2));
 289         out[3] = (byte)(((in[off+2] << 5) & 0xFF) | (in[off+3] >> 3));
 290         out[4] = (byte)(((in[off+3] << 4) & 0xFF) | (in[off+4] >> 4));
 291         out[5] = (byte)(((in[off+4] << 3) & 0xFF) | (in[off+5] >> 5));
 292         out[6] = (byte)(((in[off+5] << 2) & 0xFF) | (in[off+6] >> 6));
 293         out[7] = (byte)((in[off+6] << 1) & 0xFF);
 294         return out;
 295     }
 296 
 297     private byte[] calcLMHash () throws GeneralSecurityException {
 298         byte[] magic = {0x4b, 0x47, 0x53, 0x21, 0x40, 0x23, 0x24, 0x25};
 299         byte[] pwb = password.toUpperCase ().getBytes();
 300         byte[] pwb1 = new byte [14];
 301         int len = password.length();
 302         if (len > 14)
 303             len = 14;
 304         System.arraycopy (pwb, 0, pwb1, 0, len); /* Zero padded */
 305 
 306         DESKeySpec dks1 = new DESKeySpec (makeDesKey (pwb1, 0));
 307         DESKeySpec dks2 = new DESKeySpec (makeDesKey (pwb1, 7));
 308 
 309         SecretKey key1 = fac.generateSecret (dks1);
 310         SecretKey key2 = fac.generateSecret (dks2);
 311         cipher.init (Cipher.ENCRYPT_MODE, key1);
 312         byte[] out1 = cipher.doFinal (magic, 0, 8);
 313         cipher.init (Cipher.ENCRYPT_MODE, key2);
 314         byte[] out2 = cipher.doFinal (magic, 0, 8);
 315 
 316         byte[] result = new byte [21];
 317         System.arraycopy (out1, 0, result, 0, 8);
 318         System.arraycopy (out2, 0, result, 8, 8);
 319         return result;
 320     }
 321 
 322     private byte[] calcNTHash () throws GeneralSecurityException {
 323         byte[] pw = null;
 324         try {
 325             pw = password.getBytes ("UnicodeLittleUnmarked");
 326         } catch (UnsupportedEncodingException e) {
 327             assert false;
 328         }
 329         byte[] out = md4.digest (pw);
 330         byte[] result = new byte [21];
 331         System.arraycopy (out, 0, result, 0, 16);
 332         return result;
 333     }
 334 
 335     /* key is a 21 byte array. Split it into 3 7 byte chunks,
 336      * Convert each to 8 byte DES keys, encrypt the text arg with
 337      * each key and return the three results in a sequential []
 338      */
 339     private byte[] calcResponse (byte[] key, byte[] text)
 340     throws GeneralSecurityException {
 341         assert key.length == 21;
 342         DESKeySpec dks1 = new DESKeySpec (makeDesKey (key, 0));
 343         DESKeySpec dks2 = new DESKeySpec (makeDesKey (key, 7));
 344         DESKeySpec dks3 = new DESKeySpec (makeDesKey (key, 14));
 345         SecretKey key1 = fac.generateSecret (dks1);
 346         SecretKey key2 = fac.generateSecret (dks2);
 347         SecretKey key3 = fac.generateSecret (dks3);
 348         cipher.init (Cipher.ENCRYPT_MODE, key1);
 349         byte[] out1 = cipher.doFinal (text, 0, 8);
 350         cipher.init (Cipher.ENCRYPT_MODE, key2);
 351         byte[] out2 = cipher.doFinal (text, 0, 8);
 352         cipher.init (Cipher.ENCRYPT_MODE, key3);
 353         byte[] out3 = cipher.doFinal (text, 0, 8);
 354         byte[] result = new byte [24];
 355         System.arraycopy (out1, 0, result, 0, 8);
 356         System.arraycopy (out2, 0, result, 8, 8);
 357         System.arraycopy (out3, 0, result, 16, 8);
 358         return result;
 359     }
 360 
 361     private String buildType3Msg (String challenge) throws GeneralSecurityException,
 362                                                            IOException  {
 363         /* First decode the type2 message to get the server nonce */
 364         /* nonce is located at type2[24] for 8 bytes */
 365 
 366         byte[] type2 = (new sun.misc.BASE64Decoder()).decodeBuffer (challenge);
 367         byte[] nonce = new byte [8];
 368         System.arraycopy (type2, 24, nonce, 0, 8);
 369 
 370         int ulen = username.length()*2;
 371         type3[36] = type3[38] = (byte) (ulen % 256);
 372         type3[37] = type3[39] = (byte) (ulen / 256);
 373         int dlen = ntdomain.length()*2;
 374         type3[28] = type3[30] = (byte) (dlen % 256);
 375         type3[29] = type3[31] = (byte) (dlen / 256);
 376         int hlen = hostname.length()*2;
 377         type3[44] = type3[46] = (byte) (hlen % 256);
 378         type3[45] = type3[47] = (byte) (hlen / 256);
 379 
 380         int l = 64;
 381         copybytes (type3, l, ntdomain, "UnicodeLittleUnmarked");
 382         type3[32] = (byte) (l % 256);
 383         type3[33] = (byte) (l / 256);
 384         l += dlen;
 385         copybytes (type3, l, username, "UnicodeLittleUnmarked");
 386         type3[40] = (byte) (l % 256);
 387         type3[41] = (byte) (l / 256);
 388         l += ulen;
 389         copybytes (type3, l, hostname, "UnicodeLittleUnmarked");
 390         type3[48] = (byte) (l % 256);
 391         type3[49] = (byte) (l / 256);
 392         l += hlen;
 393 
 394         byte[] lmhash = calcLMHash();
 395         byte[] lmresponse = calcResponse (lmhash, nonce);
 396         byte[] nthash = calcNTHash();
 397         byte[] ntresponse = calcResponse (nthash, nonce);
 398         System.arraycopy (lmresponse, 0, type3, l, 24);
 399         type3[16] = (byte) (l % 256);
 400         type3[17] = (byte) (l / 256);
 401         l += 24;
 402         System.arraycopy (ntresponse, 0, type3, l, 24);
 403         type3[24] = (byte) (l % 256);
 404         type3[25] = (byte) (l / 256);
 405         l += 24;
 406         type3[56] = (byte) (l % 256);
 407         type3[57] = (byte) (l / 256);
 408 
 409         byte[] msg = new byte [l];
 410         System.arraycopy (type3, 0, msg, 0, l);
 411         String result = "NTLM " + (new B64Encoder()).encode (msg);
 412         return result;
 413     }
 414 
 415 }
 416 
 417 
 418 class B64Encoder extends sun.misc.BASE64Encoder {
 419     /* to force it to to the entire encoding in one line */
 420     protected int bytesPerLine () {
 421         return 1024;
 422     }
 423 }