blob: 5abd79ed9ef4b8501ca98f2628798c8b0c0a278d [file] [log] [blame]
/*
* Copyright 2005-2008 Sun Microsystems, Inc. All Rights Reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Sun designates this
* particular file as subject to the "Classpath" exception as provided
* by Sun in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
* CA 95054 USA or visit www.sun.com if you need additional information or
* have any questions.
*/
package sun.net.www.protocol.http;
import java.util.Arrays;
import java.util.StringTokenizer;
import java.util.Random;
import sun.net.www.HeaderParser;
import java.io.*;
import javax.crypto.*;
import javax.crypto.spec.*;
import java.security.*;
import java.net.*;
/**
* NTLMAuthentication:
*
* @author Michael McMahon
*/
/*
* NTLM authentication is nominally based on the framework defined in RFC2617,
* but differs from the standard (Basic & Digest) schemes as follows:
*
* 1. A complete authentication requires three request/response transactions
* as shown below:
* REQ ------------------------------->
* <---- 401 (signalling NTLM) --------
*
* REQ (with type1 NTLM msg) --------->
* <---- 401 (with type 2 NTLM msg) ---
*
* REQ (with type3 NTLM msg) --------->
* <---- OK ---------------------------
*
* 2. The scope of the authentication is the TCP connection (which must be kept-alive)
* after the type2 response is received. This means that NTLM does not work end-to-end
* through a proxy, rather between client and proxy, or between client and server (with no proxy)
*/
class NTLMAuthentication extends AuthenticationInfo {
private static final long serialVersionUID = -2403849171106437142L;
static char NTLM_AUTH = 'N';
private byte[] type1;
private byte[] type3;
private SecretKeyFactory fac;
private Cipher cipher;
private MessageDigest md4;
private String hostname;
private static String defaultDomain; /* Domain to use if not specified by user */
static {
defaultDomain = java.security.AccessController.doPrivileged(
new sun.security.action.GetPropertyAction("http.auth.ntlm.domain",
"domain"));
};
static boolean supportsTransparentAuth () {
return false;
}
private void init0() {
type1 = new byte[256];
type3 = new byte[256];
System.arraycopy (new byte[] {'N','T','L','M','S','S','P',0,1}, 0, type1, 0, 9);
type1[12] = (byte) 3;
type1[13] = (byte) 0xb2;
type1[28] = (byte) 0x20;
System.arraycopy (new byte[] {'N','T','L','M','S','S','P',0,3}, 0, type3, 0, 9);
type3[12] = (byte) 0x18;
type3[14] = (byte) 0x18;
type3[20] = (byte) 0x18;
type3[22] = (byte) 0x18;
type3[32] = (byte) 0x40;
type3[60] = (byte) 1;
type3[61] = (byte) 0x82;
try {
hostname = java.security.AccessController.doPrivileged(
new java.security.PrivilegedAction<String>() {
public String run() {
String localhost;
try {
localhost = InetAddress.getLocalHost().getHostName().toUpperCase();
} catch (UnknownHostException e) {
localhost = "localhost";
}
return localhost;
}
});
int x = hostname.indexOf ('.');
if (x != -1) {
hostname = hostname.substring (0, x);
}
fac = SecretKeyFactory.getInstance ("DES");
cipher = Cipher.getInstance ("DES/ECB/NoPadding");
md4 = sun.security.provider.MD4.getInstance();
} catch (NoSuchPaddingException e) {
assert false;
} catch (NoSuchAlgorithmException e) {
assert false;
}
};
PasswordAuthentication pw;
String username;
String ntdomain;
String password;
/**
* Create a NTLMAuthentication:
* Username may be specified as domain<BACKSLASH>username in the application Authenticator.
* If this notation is not used, then the domain will be taken
* from a system property: "http.auth.ntlm.domain".
*/
public NTLMAuthentication(boolean isProxy, URL url, PasswordAuthentication pw) {
super(isProxy?PROXY_AUTHENTICATION:SERVER_AUTHENTICATION, NTLM_AUTH, url, "");
init (pw);
}
private void init (PasswordAuthentication pw) {
this.pw = pw;
String s = pw.getUserName();
int i = s.indexOf ('\\');
if (i == -1) {
username = s;
ntdomain = defaultDomain;
} else {
ntdomain = s.substring (0, i).toUpperCase();
username = s.substring (i+1);
}
password = new String (pw.getPassword());
init0();
}
/**
* Constructor used for proxy entries
*/
public NTLMAuthentication(boolean isProxy, String host, int port,
PasswordAuthentication pw) {
super(isProxy?PROXY_AUTHENTICATION:SERVER_AUTHENTICATION, NTLM_AUTH,host, port, "");
init (pw);
}
/**
* @return true if this authentication supports preemptive authorization
*/
boolean supportsPreemptiveAuthorization() {
return false;
}
/**
* @return the name of the HTTP header this authentication wants set
*/
String getHeaderName() {
if (type == SERVER_AUTHENTICATION) {
return "Authorization";
} else {
return "Proxy-authorization";
}
}
/**
* Not supported. Must use the setHeaders() method
*/
String getHeaderValue(URL url, String method) {
throw new RuntimeException ("getHeaderValue not supported");
}
/**
* Check if the header indicates that the current auth. parameters are stale.
* If so, then replace the relevant field with the new value
* and return true. Otherwise return false.
* returning true means the request can be retried with the same userid/password
* returning false means we have to go back to the user to ask for a new
* username password.
*/
boolean isAuthorizationStale (String header) {
return false; /* should not be called for ntlm */
}
/**
* Set header(s) on the given connection.
* @param conn The connection to apply the header(s) to
* @param p A source of header values for this connection, not used because
* HeaderParser converts the fields to lower case, use raw instead
* @param raw The raw header field.
* @return true if all goes well, false if no headers were set.
*/
synchronized boolean setHeaders(HttpURLConnection conn, HeaderParser p, String raw) {
try {
String response;
if (raw.length() < 6) { /* NTLM<sp> */
response = buildType1Msg ();
} else {
String msg = raw.substring (5); /* skip NTLM<sp> */
response = buildType3Msg (msg);
}
conn.setAuthenticationProperty(getHeaderName(), response);
return true;
} catch (IOException e) {
return false;
} catch (GeneralSecurityException e) {
return false;
}
}
/* This is a no-op for NTLM, because there is no authentication information
* provided by the server to the client
*/
public void checkResponse (String header, String method, URL url) throws IOException {
}
private void copybytes (byte[] dest, int destpos, String src, String enc) {
try {
byte[] x = src.getBytes(enc);
System.arraycopy (x, 0, dest, destpos, x.length);
} catch (UnsupportedEncodingException e) {
assert false;
}
}
private String buildType1Msg () {
int dlen = ntdomain.length();
type1[16]= (byte) (dlen % 256);
type1[17]= (byte) (dlen / 256);
type1[18] = type1[16];
type1[19] = type1[17];
int hlen = hostname.length();
type1[24]= (byte) (hlen % 256);
type1[25]= (byte) (hlen / 256);
type1[26] = type1[24];
type1[27] = type1[25];
copybytes (type1, 32, hostname, "ISO8859_1");
copybytes (type1, hlen+32, ntdomain, "ISO8859_1");
type1[20] = (byte) ((hlen+32) % 256);
type1[21] = (byte) ((hlen+32) / 256);
byte[] msg = new byte [32 + hlen + dlen];
System.arraycopy (type1, 0, msg, 0, 32 + hlen + dlen);
String result = "NTLM " + (new B64Encoder()).encode (msg);
return result;
}
/* Convert a 7 byte array to an 8 byte array (for a des key with parity)
* input starts at offset off
*/
private byte[] makeDesKey (byte[] input, int off) {
int[] in = new int [input.length];
for (int i=0; i<in.length; i++ ) {
in[i] = input[i]<0 ? input[i]+256: input[i];
}
byte[] out = new byte[8];
out[0] = (byte)in[off+0];
out[1] = (byte)(((in[off+0] << 7) & 0xFF) | (in[off+1] >> 1));
out[2] = (byte)(((in[off+1] << 6) & 0xFF) | (in[off+2] >> 2));
out[3] = (byte)(((in[off+2] << 5) & 0xFF) | (in[off+3] >> 3));
out[4] = (byte)(((in[off+3] << 4) & 0xFF) | (in[off+4] >> 4));
out[5] = (byte)(((in[off+4] << 3) & 0xFF) | (in[off+5] >> 5));
out[6] = (byte)(((in[off+5] << 2) & 0xFF) | (in[off+6] >> 6));
out[7] = (byte)((in[off+6] << 1) & 0xFF);
return out;
}
private byte[] calcLMHash () throws GeneralSecurityException {
byte[] magic = {0x4b, 0x47, 0x53, 0x21, 0x40, 0x23, 0x24, 0x25};
byte[] pwb = password.toUpperCase ().getBytes();
byte[] pwb1 = new byte [14];
int len = password.length();
if (len > 14)
len = 14;
System.arraycopy (pwb, 0, pwb1, 0, len); /* Zero padded */
DESKeySpec dks1 = new DESKeySpec (makeDesKey (pwb1, 0));
DESKeySpec dks2 = new DESKeySpec (makeDesKey (pwb1, 7));
SecretKey key1 = fac.generateSecret (dks1);
SecretKey key2 = fac.generateSecret (dks2);
cipher.init (Cipher.ENCRYPT_MODE, key1);
byte[] out1 = cipher.doFinal (magic, 0, 8);
cipher.init (Cipher.ENCRYPT_MODE, key2);
byte[] out2 = cipher.doFinal (magic, 0, 8);
byte[] result = new byte [21];
System.arraycopy (out1, 0, result, 0, 8);
System.arraycopy (out2, 0, result, 8, 8);
return result;
}
private byte[] calcNTHash () throws GeneralSecurityException {
byte[] pw = null;
try {
pw = password.getBytes ("UnicodeLittleUnmarked");
} catch (UnsupportedEncodingException e) {
assert false;
}
byte[] out = md4.digest (pw);
byte[] result = new byte [21];
System.arraycopy (out, 0, result, 0, 16);
return result;
}
/* key is a 21 byte array. Split it into 3 7 byte chunks,
* Convert each to 8 byte DES keys, encrypt the text arg with
* each key and return the three results in a sequential []
*/
private byte[] calcResponse (byte[] key, byte[] text)
throws GeneralSecurityException {
assert key.length == 21;
DESKeySpec dks1 = new DESKeySpec (makeDesKey (key, 0));
DESKeySpec dks2 = new DESKeySpec (makeDesKey (key, 7));
DESKeySpec dks3 = new DESKeySpec (makeDesKey (key, 14));
SecretKey key1 = fac.generateSecret (dks1);
SecretKey key2 = fac.generateSecret (dks2);
SecretKey key3 = fac.generateSecret (dks3);
cipher.init (Cipher.ENCRYPT_MODE, key1);
byte[] out1 = cipher.doFinal (text, 0, 8);
cipher.init (Cipher.ENCRYPT_MODE, key2);
byte[] out2 = cipher.doFinal (text, 0, 8);
cipher.init (Cipher.ENCRYPT_MODE, key3);
byte[] out3 = cipher.doFinal (text, 0, 8);
byte[] result = new byte [24];
System.arraycopy (out1, 0, result, 0, 8);
System.arraycopy (out2, 0, result, 8, 8);
System.arraycopy (out3, 0, result, 16, 8);
return result;
}
private String buildType3Msg (String challenge) throws GeneralSecurityException,
IOException {
/* First decode the type2 message to get the server nonce */
/* nonce is located at type2[24] for 8 bytes */
byte[] type2 = (new sun.misc.BASE64Decoder()).decodeBuffer (challenge);
byte[] nonce = new byte [8];
System.arraycopy (type2, 24, nonce, 0, 8);
int ulen = username.length()*2;
type3[36] = type3[38] = (byte) (ulen % 256);
type3[37] = type3[39] = (byte) (ulen / 256);
int dlen = ntdomain.length()*2;
type3[28] = type3[30] = (byte) (dlen % 256);
type3[29] = type3[31] = (byte) (dlen / 256);
int hlen = hostname.length()*2;
type3[44] = type3[46] = (byte) (hlen % 256);
type3[45] = type3[47] = (byte) (hlen / 256);
int l = 64;
copybytes (type3, l, ntdomain, "UnicodeLittleUnmarked");
type3[32] = (byte) (l % 256);
type3[33] = (byte) (l / 256);
l += dlen;
copybytes (type3, l, username, "UnicodeLittleUnmarked");
type3[40] = (byte) (l % 256);
type3[41] = (byte) (l / 256);
l += ulen;
copybytes (type3, l, hostname, "UnicodeLittleUnmarked");
type3[48] = (byte) (l % 256);
type3[49] = (byte) (l / 256);
l += hlen;
byte[] lmhash = calcLMHash();
byte[] lmresponse = calcResponse (lmhash, nonce);
byte[] nthash = calcNTHash();
byte[] ntresponse = calcResponse (nthash, nonce);
System.arraycopy (lmresponse, 0, type3, l, 24);
type3[16] = (byte) (l % 256);
type3[17] = (byte) (l / 256);
l += 24;
System.arraycopy (ntresponse, 0, type3, l, 24);
type3[24] = (byte) (l % 256);
type3[25] = (byte) (l / 256);
l += 24;
type3[56] = (byte) (l % 256);
type3[57] = (byte) (l / 256);
byte[] msg = new byte [l];
System.arraycopy (type3, 0, msg, 0, l);
String result = "NTLM " + (new B64Encoder()).encode (msg);
return result;
}
}
class B64Encoder extends sun.misc.BASE64Encoder {
/* to force it to to the entire encoding in one line */
protected int bytesPerLine () {
return 1024;
}
}