gecko/mobile/android/base/httpclientandroidlib/impl/auth/NTLMEngineImpl.java

1320 lines
49 KiB
Java

/*
* ====================================================================
*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package ch.boye.httpclientandroidlib.impl.auth;
import java.security.Key;
import java.security.MessageDigest;
import java.util.Arrays;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import org.mozilla.apache.commons.codec.binary.Base64;
import ch.boye.httpclientandroidlib.util.EncodingUtils;
/**
* Provides an implementation for NTLMv1, NTLMv2, and NTLM2 Session forms of the NTLM
* authentication protocol.
*
* @since 4.1
*/
final class NTLMEngineImpl implements NTLMEngine {
// Flags we use
protected final static int FLAG_UNICODE_ENCODING = 0x00000001;
protected final static int FLAG_TARGET_DESIRED = 0x00000004;
protected final static int FLAG_NEGOTIATE_SIGN = 0x00000010;
protected final static int FLAG_NEGOTIATE_SEAL = 0x00000020;
protected final static int FLAG_NEGOTIATE_NTLM = 0x00000200;
protected final static int FLAG_NEGOTIATE_ALWAYS_SIGN = 0x00008000;
protected final static int FLAG_NEGOTIATE_NTLM2 = 0x00080000;
protected final static int FLAG_NEGOTIATE_128 = 0x20000000;
protected final static int FLAG_NEGOTIATE_KEY_EXCH = 0x40000000;
/** Secure random generator */
private static final java.security.SecureRandom RND_GEN;
static {
java.security.SecureRandom rnd = null;
try {
rnd = java.security.SecureRandom.getInstance("SHA1PRNG");
} catch (Exception e) {
}
RND_GEN = rnd;
}
/** Character encoding */
static final String DEFAULT_CHARSET = "ASCII";
/** The character set to use for encoding the credentials */
private String credentialCharset = DEFAULT_CHARSET;
/** The signature string as bytes in the default encoding */
private static byte[] SIGNATURE;
static {
byte[] bytesWithoutNull = EncodingUtils.getBytes("NTLMSSP", "ASCII");
SIGNATURE = new byte[bytesWithoutNull.length + 1];
System.arraycopy(bytesWithoutNull, 0, SIGNATURE, 0, bytesWithoutNull.length);
SIGNATURE[bytesWithoutNull.length] = (byte) 0x00;
}
/**
* Returns the response for the given message.
*
* @param message
* the message that was received from the server.
* @param username
* the username to authenticate with.
* @param password
* the password to authenticate with.
* @param host
* The host.
* @param domain
* the NT domain to authenticate in.
* @return The response.
* @throws HttpException
* If the messages cannot be retrieved.
*/
final String getResponseFor(String message, String username, String password,
String host, String domain) throws NTLMEngineException {
final String response;
if (message == null || message.trim().equals("")) {
response = getType1Message(host, domain);
} else {
Type2Message t2m = new Type2Message(message);
response = getType3Message(username, password, host, domain, t2m.getChallenge(), t2m
.getFlags(), t2m.getTarget(), t2m.getTargetInfo());
}
return response;
}
/**
* Creates the first message (type 1 message) in the NTLM authentication
* sequence. This message includes the user name, domain and host for the
* authentication session.
*
* @param host
* the computer name of the host requesting authentication.
* @param domain
* The domain to authenticate with.
* @return String the message to add to the HTTP request header.
*/
String getType1Message(String host, String domain) throws NTLMEngineException {
return new Type1Message(domain, host).getResponse();
}
/**
* Creates the type 3 message using the given server nonce. The type 3
* message includes all the information for authentication, host, domain,
* username and the result of encrypting the nonce sent by the server using
* the user's password as the key.
*
* @param user
* The user name. This should not include the domain name.
* @param password
* The password.
* @param host
* The host that is originating the authentication request.
* @param domain
* The domain to authenticate within.
* @param nonce
* the 8 byte array the server sent.
* @return The type 3 message.
* @throws NTLMEngineException
* If {@encrypt(byte[],byte[])} fails.
*/
String getType3Message(String user, String password, String host, String domain,
byte[] nonce, int type2Flags, String target, byte[] targetInformation)
throws NTLMEngineException {
return new Type3Message(domain, host, user, password, nonce, type2Flags, target,
targetInformation).getResponse();
}
/**
* @return Returns the credentialCharset.
*/
String getCredentialCharset() {
return credentialCharset;
}
/**
* @param credentialCharset
* The credentialCharset to set.
*/
void setCredentialCharset(String credentialCharset) {
this.credentialCharset = credentialCharset;
}
/** Strip dot suffix from a name */
private static String stripDotSuffix(String value) {
int index = value.indexOf(".");
if (index != -1)
return value.substring(0, index);
return value;
}
/** Convert host to standard form */
private static String convertHost(String host) {
return stripDotSuffix(host);
}
/** Convert domain to standard form */
private static String convertDomain(String domain) {
return stripDotSuffix(domain);
}
private static int readULong(byte[] src, int index) throws NTLMEngineException {
if (src.length < index + 4)
throw new NTLMEngineException("NTLM authentication - buffer too small for DWORD");
return (src[index] & 0xff) | ((src[index + 1] & 0xff) << 8)
| ((src[index + 2] & 0xff) << 16) | ((src[index + 3] & 0xff) << 24);
}
private static int readUShort(byte[] src, int index) throws NTLMEngineException {
if (src.length < index + 2)
throw new NTLMEngineException("NTLM authentication - buffer too small for WORD");
return (src[index] & 0xff) | ((src[index + 1] & 0xff) << 8);
}
private static byte[] readSecurityBuffer(byte[] src, int index) throws NTLMEngineException {
int length = readUShort(src, index);
int offset = readULong(src, index + 4);
if (src.length < offset + length)
throw new NTLMEngineException(
"NTLM authentication - buffer too small for data item");
byte[] buffer = new byte[length];
System.arraycopy(src, offset, buffer, 0, length);
return buffer;
}
/** Calculate a challenge block */
private static byte[] makeRandomChallenge() throws NTLMEngineException {
if (RND_GEN == null) {
throw new NTLMEngineException("Random generator not available");
}
byte[] rval = new byte[8];
synchronized (RND_GEN) {
RND_GEN.nextBytes(rval);
}
return rval;
}
/** Calculate an NTLM2 challenge block */
private static byte[] makeNTLM2RandomChallenge() throws NTLMEngineException {
if (RND_GEN == null) {
throw new NTLMEngineException("Random generator not available");
}
byte[] rval = new byte[24];
synchronized (RND_GEN) {
RND_GEN.nextBytes(rval);
}
// 8-byte challenge, padded with zeros to 24 bytes.
Arrays.fill(rval, 8, 24, (byte) 0x00);
return rval;
}
/**
* Calculates the LM Response for the given challenge, using the specified
* password.
*
* @param password
* The user's password.
* @param challenge
* The Type 2 challenge from the server.
*
* @return The LM Response.
*/
static byte[] getLMResponse(String password, byte[] challenge)
throws NTLMEngineException {
byte[] lmHash = lmHash(password);
return lmResponse(lmHash, challenge);
}
/**
* Calculates the NTLM Response for the given challenge, using the specified
* password.
*
* @param password
* The user's password.
* @param challenge
* The Type 2 challenge from the server.
*
* @return The NTLM Response.
*/
static byte[] getNTLMResponse(String password, byte[] challenge)
throws NTLMEngineException {
byte[] ntlmHash = ntlmHash(password);
return lmResponse(ntlmHash, challenge);
}
/**
* Calculates the NTLMv2 Response for the given challenge, using the
* specified authentication target, username, password, target information
* block, and client challenge.
*
* @param target
* The authentication target (i.e., domain).
* @param user
* The username.
* @param password
* The user's password.
* @param targetInformation
* The target information block from the Type 2 message.
* @param challenge
* The Type 2 challenge from the server.
* @param clientChallenge
* The random 8-byte client challenge.
*
* @return The NTLMv2 Response.
*/
static byte[] getNTLMv2Response(String target, String user, String password,
byte[] challenge, byte[] clientChallenge, byte[] targetInformation)
throws NTLMEngineException {
byte[] ntlmv2Hash = ntlmv2Hash(target, user, password);
byte[] blob = createBlob(clientChallenge, targetInformation);
return lmv2Response(ntlmv2Hash, challenge, blob);
}
/**
* Calculates the LMv2 Response for the given challenge, using the specified
* authentication target, username, password, and client challenge.
*
* @param target
* The authentication target (i.e., domain).
* @param user
* The username.
* @param password
* The user's password.
* @param challenge
* The Type 2 challenge from the server.
* @param clientChallenge
* The random 8-byte client challenge.
*
* @return The LMv2 Response.
*/
static byte[] getLMv2Response(String target, String user, String password,
byte[] challenge, byte[] clientChallenge) throws NTLMEngineException {
byte[] ntlmv2Hash = ntlmv2Hash(target, user, password);
return lmv2Response(ntlmv2Hash, challenge, clientChallenge);
}
/**
* Calculates the NTLM2 Session Response for the given challenge, using the
* specified password and client challenge.
*
* @param password
* The user's password.
* @param challenge
* The Type 2 challenge from the server.
* @param clientChallenge
* The random 8-byte client challenge.
*
* @return The NTLM2 Session Response. This is placed in the NTLM response
* field of the Type 3 message; the LM response field contains the
* client challenge, null-padded to 24 bytes.
*/
static byte[] getNTLM2SessionResponse(String password, byte[] challenge,
byte[] clientChallenge) throws NTLMEngineException {
try {
byte[] ntlmHash = ntlmHash(password);
// Look up MD5 algorithm (was necessary on jdk 1.4.2)
// This used to be needed, but java 1.5.0_07 includes the MD5
// algorithm (finally)
// Class x = Class.forName("gnu.crypto.hash.MD5");
// Method updateMethod = x.getMethod("update",new
// Class[]{byte[].class});
// Method digestMethod = x.getMethod("digest",new Class[0]);
// Object mdInstance = x.newInstance();
// updateMethod.invoke(mdInstance,new Object[]{challenge});
// updateMethod.invoke(mdInstance,new Object[]{clientChallenge});
// byte[] digest = (byte[])digestMethod.invoke(mdInstance,new
// Object[0]);
MessageDigest md5 = MessageDigest.getInstance("MD5");
md5.update(challenge);
md5.update(clientChallenge);
byte[] digest = md5.digest();
byte[] sessionHash = new byte[8];
System.arraycopy(digest, 0, sessionHash, 0, 8);
return lmResponse(ntlmHash, sessionHash);
} catch (Exception e) {
if (e instanceof NTLMEngineException)
throw (NTLMEngineException) e;
throw new NTLMEngineException(e.getMessage(), e);
}
}
/**
* Creates the LM Hash of the user's password.
*
* @param password
* The password.
*
* @return The LM Hash of the given password, used in the calculation of the
* LM Response.
*/
private static byte[] lmHash(String password) throws NTLMEngineException {
try {
byte[] oemPassword = password.toUpperCase().getBytes("US-ASCII");
int length = Math.min(oemPassword.length, 14);
byte[] keyBytes = new byte[14];
System.arraycopy(oemPassword, 0, keyBytes, 0, length);
Key lowKey = createDESKey(keyBytes, 0);
Key highKey = createDESKey(keyBytes, 7);
byte[] magicConstant = "KGS!@#$%".getBytes("US-ASCII");
Cipher des = Cipher.getInstance("DES/ECB/NoPadding");
des.init(Cipher.ENCRYPT_MODE, lowKey);
byte[] lowHash = des.doFinal(magicConstant);
des.init(Cipher.ENCRYPT_MODE, highKey);
byte[] highHash = des.doFinal(magicConstant);
byte[] lmHash = new byte[16];
System.arraycopy(lowHash, 0, lmHash, 0, 8);
System.arraycopy(highHash, 0, lmHash, 8, 8);
return lmHash;
} catch (Exception e) {
throw new NTLMEngineException(e.getMessage(), e);
}
}
/**
* Creates the NTLM Hash of the user's password.
*
* @param password
* The password.
*
* @return The NTLM Hash of the given password, used in the calculation of
* the NTLM Response and the NTLMv2 and LMv2 Hashes.
*/
private static byte[] ntlmHash(String password) throws NTLMEngineException {
try {
byte[] unicodePassword = password.getBytes("UnicodeLittleUnmarked");
MD4 md4 = new MD4();
md4.update(unicodePassword);
return md4.getOutput();
} catch (java.io.UnsupportedEncodingException e) {
throw new NTLMEngineException("Unicode not supported: " + e.getMessage(), e);
}
}
/**
* Creates the NTLMv2 Hash of the user's password.
*
* @param target
* The authentication target (i.e., domain).
* @param user
* The username.
* @param password
* The password.
*
* @return The NTLMv2 Hash, used in the calculation of the NTLMv2 and LMv2
* Responses.
*/
private static byte[] ntlmv2Hash(String target, String user, String password)
throws NTLMEngineException {
try {
byte[] ntlmHash = ntlmHash(password);
HMACMD5 hmacMD5 = new HMACMD5(ntlmHash);
// Upper case username, mixed case target!!
hmacMD5.update(user.toUpperCase().getBytes("UnicodeLittleUnmarked"));
hmacMD5.update(target.getBytes("UnicodeLittleUnmarked"));
return hmacMD5.getOutput();
} catch (java.io.UnsupportedEncodingException e) {
throw new NTLMEngineException("Unicode not supported! " + e.getMessage(), e);
}
}
/**
* Creates the LM Response from the given hash and Type 2 challenge.
*
* @param hash
* The LM or NTLM Hash.
* @param challenge
* The server challenge from the Type 2 message.
*
* @return The response (either LM or NTLM, depending on the provided hash).
*/
private static byte[] lmResponse(byte[] hash, byte[] challenge) throws NTLMEngineException {
try {
byte[] keyBytes = new byte[21];
System.arraycopy(hash, 0, keyBytes, 0, 16);
Key lowKey = createDESKey(keyBytes, 0);
Key middleKey = createDESKey(keyBytes, 7);
Key highKey = createDESKey(keyBytes, 14);
Cipher des = Cipher.getInstance("DES/ECB/NoPadding");
des.init(Cipher.ENCRYPT_MODE, lowKey);
byte[] lowResponse = des.doFinal(challenge);
des.init(Cipher.ENCRYPT_MODE, middleKey);
byte[] middleResponse = des.doFinal(challenge);
des.init(Cipher.ENCRYPT_MODE, highKey);
byte[] highResponse = des.doFinal(challenge);
byte[] lmResponse = new byte[24];
System.arraycopy(lowResponse, 0, lmResponse, 0, 8);
System.arraycopy(middleResponse, 0, lmResponse, 8, 8);
System.arraycopy(highResponse, 0, lmResponse, 16, 8);
return lmResponse;
} catch (Exception e) {
throw new NTLMEngineException(e.getMessage(), e);
}
}
/**
* Creates the LMv2 Response from the given hash, client data, and Type 2
* challenge.
*
* @param hash
* The NTLMv2 Hash.
* @param clientData
* The client data (blob or client challenge).
* @param challenge
* The server challenge from the Type 2 message.
*
* @return The response (either NTLMv2 or LMv2, depending on the client
* data).
*/
private static byte[] lmv2Response(byte[] hash, byte[] challenge, byte[] clientData)
throws NTLMEngineException {
HMACMD5 hmacMD5 = new HMACMD5(hash);
hmacMD5.update(challenge);
hmacMD5.update(clientData);
byte[] mac = hmacMD5.getOutput();
byte[] lmv2Response = new byte[mac.length + clientData.length];
System.arraycopy(mac, 0, lmv2Response, 0, mac.length);
System.arraycopy(clientData, 0, lmv2Response, mac.length, clientData.length);
return lmv2Response;
}
/**
* Creates the NTLMv2 blob from the given target information block and
* client challenge.
*
* @param targetInformation
* The target information block from the Type 2 message.
* @param clientChallenge
* The random 8-byte client challenge.
*
* @return The blob, used in the calculation of the NTLMv2 Response.
*/
private static byte[] createBlob(byte[] clientChallenge, byte[] targetInformation) {
byte[] blobSignature = new byte[] { (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x00 };
byte[] reserved = new byte[] { (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00 };
byte[] unknown1 = new byte[] { (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00 };
long time = System.currentTimeMillis();
time += 11644473600000l; // milliseconds from January 1, 1601 -> epoch.
time *= 10000; // tenths of a microsecond.
// convert to little-endian byte array.
byte[] timestamp = new byte[8];
for (int i = 0; i < 8; i++) {
timestamp[i] = (byte) time;
time >>>= 8;
}
byte[] blob = new byte[blobSignature.length + reserved.length + timestamp.length + 8
+ unknown1.length + targetInformation.length];
int offset = 0;
System.arraycopy(blobSignature, 0, blob, offset, blobSignature.length);
offset += blobSignature.length;
System.arraycopy(reserved, 0, blob, offset, reserved.length);
offset += reserved.length;
System.arraycopy(timestamp, 0, blob, offset, timestamp.length);
offset += timestamp.length;
System.arraycopy(clientChallenge, 0, blob, offset, 8);
offset += 8;
System.arraycopy(unknown1, 0, blob, offset, unknown1.length);
offset += unknown1.length;
System.arraycopy(targetInformation, 0, blob, offset, targetInformation.length);
return blob;
}
/**
* Creates a DES encryption key from the given key material.
*
* @param bytes
* A byte array containing the DES key material.
* @param offset
* The offset in the given byte array at which the 7-byte key
* material starts.
*
* @return A DES encryption key created from the key material starting at
* the specified offset in the given byte array.
*/
private static Key createDESKey(byte[] bytes, int offset) {
byte[] keyBytes = new byte[7];
System.arraycopy(bytes, offset, keyBytes, 0, 7);
byte[] material = new byte[8];
material[0] = keyBytes[0];
material[1] = (byte) (keyBytes[0] << 7 | (keyBytes[1] & 0xff) >>> 1);
material[2] = (byte) (keyBytes[1] << 6 | (keyBytes[2] & 0xff) >>> 2);
material[3] = (byte) (keyBytes[2] << 5 | (keyBytes[3] & 0xff) >>> 3);
material[4] = (byte) (keyBytes[3] << 4 | (keyBytes[4] & 0xff) >>> 4);
material[5] = (byte) (keyBytes[4] << 3 | (keyBytes[5] & 0xff) >>> 5);
material[6] = (byte) (keyBytes[5] << 2 | (keyBytes[6] & 0xff) >>> 6);
material[7] = (byte) (keyBytes[6] << 1);
oddParity(material);
return new SecretKeySpec(material, "DES");
}
/**
* Applies odd parity to the given byte array.
*
* @param bytes
* The data whose parity bits are to be adjusted for odd parity.
*/
private static void oddParity(byte[] bytes) {
for (int i = 0; i < bytes.length; i++) {
byte b = bytes[i];
boolean needsParity = (((b >>> 7) ^ (b >>> 6) ^ (b >>> 5) ^ (b >>> 4) ^ (b >>> 3)
^ (b >>> 2) ^ (b >>> 1)) & 0x01) == 0;
if (needsParity) {
bytes[i] |= (byte) 0x01;
} else {
bytes[i] &= (byte) 0xfe;
}
}
}
/** NTLM message generation, base class */
static class NTLMMessage {
/** The current response */
private byte[] messageContents = null;
/** The current output position */
private int currentOutputPosition = 0;
/** Constructor to use when message contents are not yet known */
NTLMMessage() {
}
/** Constructor to use when message contents are known */
NTLMMessage(String messageBody, int expectedType) throws NTLMEngineException {
messageContents = Base64.decodeBase64(EncodingUtils.getBytes(messageBody,
DEFAULT_CHARSET));
// Look for NTLM message
if (messageContents.length < SIGNATURE.length)
throw new NTLMEngineException("NTLM message decoding error - packet too short");
int i = 0;
while (i < SIGNATURE.length) {
if (messageContents[i] != SIGNATURE[i])
throw new NTLMEngineException(
"NTLM message expected - instead got unrecognized bytes");
i++;
}
// Check to be sure there's a type 2 message indicator next
int type = readULong(SIGNATURE.length);
if (type != expectedType)
throw new NTLMEngineException("NTLM type " + Integer.toString(expectedType)
+ " message expected - instead got type " + Integer.toString(type));
currentOutputPosition = messageContents.length;
}
/**
* Get the length of the signature and flags, so calculations can adjust
* offsets accordingly.
*/
protected int getPreambleLength() {
return SIGNATURE.length + 4;
}
/** Get the message length */
protected int getMessageLength() {
return currentOutputPosition;
}
/** Read a byte from a position within the message buffer */
protected byte readByte(int position) throws NTLMEngineException {
if (messageContents.length < position + 1)
throw new NTLMEngineException("NTLM: Message too short");
return messageContents[position];
}
/** Read a bunch of bytes from a position in the message buffer */
protected void readBytes(byte[] buffer, int position) throws NTLMEngineException {
if (messageContents.length < position + buffer.length)
throw new NTLMEngineException("NTLM: Message too short");
System.arraycopy(messageContents, position, buffer, 0, buffer.length);
}
/** Read a ushort from a position within the message buffer */
protected int readUShort(int position) throws NTLMEngineException {
return NTLMEngineImpl.readUShort(messageContents, position);
}
/** Read a ulong from a position within the message buffer */
protected int readULong(int position) throws NTLMEngineException {
return NTLMEngineImpl.readULong(messageContents, position);
}
/** Read a security buffer from a position within the message buffer */
protected byte[] readSecurityBuffer(int position) throws NTLMEngineException {
return NTLMEngineImpl.readSecurityBuffer(messageContents, position);
}
/**
* Prepares the object to create a response of the given length.
*
* @param length
* the maximum length of the response to prepare, not
* including the type and the signature (which this method
* adds).
*/
protected void prepareResponse(int maxlength, int messageType) {
messageContents = new byte[maxlength];
currentOutputPosition = 0;
addBytes(SIGNATURE);
addULong(messageType);
}
/**
* Adds the given byte to the response.
*
* @param b
* the byte to add.
*/
protected void addByte(byte b) {
messageContents[currentOutputPosition] = b;
currentOutputPosition++;
}
/**
* Adds the given bytes to the response.
*
* @param bytes
* the bytes to add.
*/
protected void addBytes(byte[] bytes) {
for (int i = 0; i < bytes.length; i++) {
messageContents[currentOutputPosition] = bytes[i];
currentOutputPosition++;
}
}
/** Adds a USHORT to the response */
protected void addUShort(int value) {
addByte((byte) (value & 0xff));
addByte((byte) (value >> 8 & 0xff));
}
/** Adds a ULong to the response */
protected void addULong(int value) {
addByte((byte) (value & 0xff));
addByte((byte) (value >> 8 & 0xff));
addByte((byte) (value >> 16 & 0xff));
addByte((byte) (value >> 24 & 0xff));
}
/**
* Returns the response that has been generated after shrinking the
* array if required and base64 encodes the response.
*
* @return The response as above.
*/
String getResponse() {
byte[] resp;
if (messageContents.length > currentOutputPosition) {
byte[] tmp = new byte[currentOutputPosition];
for (int i = 0; i < currentOutputPosition; i++) {
tmp[i] = messageContents[i];
}
resp = tmp;
} else {
resp = messageContents;
}
return EncodingUtils.getAsciiString(Base64.encodeBase64(resp));
}
}
/** Type 1 message assembly class */
static class Type1Message extends NTLMMessage {
protected byte[] hostBytes;
protected byte[] domainBytes;
/** Constructor. Include the arguments the message will need */
Type1Message(String domain, String host) throws NTLMEngineException {
super();
try {
// Strip off domain name from the host!
host = convertHost(host);
// Use only the base domain name!
domain = convertDomain(domain);
hostBytes = host.getBytes("UnicodeLittleUnmarked");
domainBytes = domain.toUpperCase().getBytes("UnicodeLittleUnmarked");
} catch (java.io.UnsupportedEncodingException e) {
throw new NTLMEngineException("Unicode unsupported: " + e.getMessage(), e);
}
}
/**
* Getting the response involves building the message before returning
* it
*/
@Override
String getResponse() {
// Now, build the message. Calculate its length first, including
// signature or type.
int finalLength = 32 + hostBytes.length + domainBytes.length;
// Set up the response. This will initialize the signature, message
// type, and flags.
prepareResponse(finalLength, 1);
// Flags. These are the complete set of flags we support.
addULong(FLAG_NEGOTIATE_NTLM | FLAG_NEGOTIATE_NTLM2 | FLAG_NEGOTIATE_SIGN
| FLAG_NEGOTIATE_SEAL |
/*
* FLAG_NEGOTIATE_ALWAYS_SIGN | FLAG_NEGOTIATE_KEY_EXCH |
*/
FLAG_UNICODE_ENCODING | FLAG_TARGET_DESIRED | FLAG_NEGOTIATE_128);
// Domain length (two times).
addUShort(domainBytes.length);
addUShort(domainBytes.length);
// Domain offset.
addULong(hostBytes.length + 32);
// Host length (two times).
addUShort(hostBytes.length);
addUShort(hostBytes.length);
// Host offset (always 32).
addULong(32);
// Host String.
addBytes(hostBytes);
// Domain String.
addBytes(domainBytes);
return super.getResponse();
}
}
/** Type 2 message class */
static class Type2Message extends NTLMMessage {
protected byte[] challenge;
protected String target;
protected byte[] targetInfo;
protected int flags;
Type2Message(String message) throws NTLMEngineException {
super(message, 2);
// Parse out the rest of the info we need from the message
// The nonce is the 8 bytes starting from the byte in position 24.
challenge = new byte[8];
readBytes(challenge, 24);
flags = readULong(20);
if ((flags & FLAG_UNICODE_ENCODING) == 0)
throw new NTLMEngineException(
"NTLM type 2 message has flags that make no sense: "
+ Integer.toString(flags));
// Do the target!
target = null;
// The TARGET_DESIRED flag is said to not have understood semantics
// in Type2 messages, so use the length of the packet to decide
// how to proceed instead
if (getMessageLength() >= 12 + 8) {
byte[] bytes = readSecurityBuffer(12);
if (bytes.length != 0) {
try {
target = new String(bytes, "UnicodeLittleUnmarked");
} catch (java.io.UnsupportedEncodingException e) {
throw new NTLMEngineException(e.getMessage(), e);
}
}
}
// Do the target info!
targetInfo = null;
// TARGET_DESIRED flag cannot be relied on, so use packet length
if (getMessageLength() >= 40 + 8) {
byte[] bytes = readSecurityBuffer(40);
if (bytes.length != 0) {
targetInfo = bytes;
}
}
}
/** Retrieve the challenge */
byte[] getChallenge() {
return challenge;
}
/** Retrieve the target */
String getTarget() {
return target;
}
/** Retrieve the target info */
byte[] getTargetInfo() {
return targetInfo;
}
/** Retrieve the response flags */
int getFlags() {
return flags;
}
}
/** Type 3 message assembly class */
static class Type3Message extends NTLMMessage {
// Response flags from the type2 message
protected int type2Flags;
protected byte[] domainBytes;
protected byte[] hostBytes;
protected byte[] userBytes;
protected byte[] lmResp;
protected byte[] ntResp;
/** Constructor. Pass the arguments we will need */
Type3Message(String domain, String host, String user, String password, byte[] nonce,
int type2Flags, String target, byte[] targetInformation)
throws NTLMEngineException {
// Save the flags
this.type2Flags = type2Flags;
// Strip off domain name from the host!
host = convertHost(host);
// Use only the base domain name!
domain = convertDomain(domain);
// Use the new code to calculate the responses, including v2 if that
// seems warranted.
try {
if (targetInformation != null && target != null) {
byte[] clientChallenge = makeRandomChallenge();
ntResp = getNTLMv2Response(target, user, password, nonce, clientChallenge,
targetInformation);
lmResp = getLMv2Response(target, user, password, nonce, clientChallenge);
} else {
if ((type2Flags & FLAG_NEGOTIATE_NTLM2) != 0) {
// NTLM2 session stuff is requested
byte[] clientChallenge = makeNTLM2RandomChallenge();
ntResp = getNTLM2SessionResponse(password, nonce, clientChallenge);
lmResp = clientChallenge;
// All the other flags we send (signing, sealing, key
// exchange) are supported, but they don't do anything
// at all in an
// NTLM2 context! So we're done at this point.
} else {
ntResp = getNTLMResponse(password, nonce);
lmResp = getLMResponse(password, nonce);
}
}
} catch (NTLMEngineException e) {
// This likely means we couldn't find the MD4 hash algorithm -
// fail back to just using LM
ntResp = new byte[0];
lmResp = getLMResponse(password, nonce);
}
try {
domainBytes = domain.toUpperCase().getBytes("UnicodeLittleUnmarked");
hostBytes = host.getBytes("UnicodeLittleUnmarked");
userBytes = user.getBytes("UnicodeLittleUnmarked");
} catch (java.io.UnsupportedEncodingException e) {
throw new NTLMEngineException("Unicode not supported: " + e.getMessage(), e);
}
}
/** Assemble the response */
@Override
String getResponse() {
int ntRespLen = ntResp.length;
int lmRespLen = lmResp.length;
int domainLen = domainBytes.length;
int hostLen = hostBytes.length;
int userLen = userBytes.length;
// Calculate the layout within the packet
int lmRespOffset = 64;
int ntRespOffset = lmRespOffset + lmRespLen;
int domainOffset = ntRespOffset + ntRespLen;
int userOffset = domainOffset + domainLen;
int hostOffset = userOffset + userLen;
int sessionKeyOffset = hostOffset + hostLen;
int finalLength = sessionKeyOffset + 0;
// Start the response. Length includes signature and type
prepareResponse(finalLength, 3);
// LM Resp Length (twice)
addUShort(lmRespLen);
addUShort(lmRespLen);
// LM Resp Offset
addULong(lmRespOffset);
// NT Resp Length (twice)
addUShort(ntRespLen);
addUShort(ntRespLen);
// NT Resp Offset
addULong(ntRespOffset);
// Domain length (twice)
addUShort(domainLen);
addUShort(domainLen);
// Domain offset.
addULong(domainOffset);
// User Length (twice)
addUShort(userLen);
addUShort(userLen);
// User offset
addULong(userOffset);
// Host length (twice)
addUShort(hostLen);
addUShort(hostLen);
// Host offset
addULong(hostOffset);
// 4 bytes of zeros - not sure what this is
addULong(0);
// Message length
addULong(finalLength);
// Flags. Currently: NEGOTIATE_NTLM + UNICODE_ENCODING +
// TARGET_DESIRED + NEGOTIATE_128
addULong(FLAG_NEGOTIATE_NTLM | FLAG_UNICODE_ENCODING | FLAG_TARGET_DESIRED
| FLAG_NEGOTIATE_128 | (type2Flags & FLAG_NEGOTIATE_NTLM2)
| (type2Flags & FLAG_NEGOTIATE_SIGN) | (type2Flags & FLAG_NEGOTIATE_SEAL)
| (type2Flags & FLAG_NEGOTIATE_KEY_EXCH)
| (type2Flags & FLAG_NEGOTIATE_ALWAYS_SIGN));
// Add the actual data
addBytes(lmResp);
addBytes(ntResp);
addBytes(domainBytes);
addBytes(userBytes);
addBytes(hostBytes);
return super.getResponse();
}
}
static void writeULong(byte[] buffer, int value, int offset) {
buffer[offset] = (byte) (value & 0xff);
buffer[offset + 1] = (byte) (value >> 8 & 0xff);
buffer[offset + 2] = (byte) (value >> 16 & 0xff);
buffer[offset + 3] = (byte) (value >> 24 & 0xff);
}
static int F(int x, int y, int z) {
return ((x & y) | (~x & z));
}
static int G(int x, int y, int z) {
return ((x & y) | (x & z) | (y & z));
}
static int H(int x, int y, int z) {
return (x ^ y ^ z);
}
static int rotintlft(int val, int numbits) {
return ((val << numbits) | (val >>> (32 - numbits)));
}
/**
* Cryptography support - MD4. The following class was based loosely on the
* RFC and on code found at http://www.cs.umd.edu/~harry/jotp/src/md.java.
* Code correctness was verified by looking at MD4.java from the jcifs
* library (http://jcifs.samba.org). It was massaged extensively to the
* final form found here by Karl Wright (kwright@metacarta.com).
*/
static class MD4 {
protected int A = 0x67452301;
protected int B = 0xefcdab89;
protected int C = 0x98badcfe;
protected int D = 0x10325476;
protected long count = 0L;
protected byte[] dataBuffer = new byte[64];
MD4() {
}
void update(byte[] input) {
// We always deal with 512 bits at a time. Correspondingly, there is
// a buffer 64 bytes long that we write data into until it gets
// full.
int curBufferPos = (int) (count & 63L);
int inputIndex = 0;
while (input.length - inputIndex + curBufferPos >= dataBuffer.length) {
// We have enough data to do the next step. Do a partial copy
// and a transform, updating inputIndex and curBufferPos
// accordingly
int transferAmt = dataBuffer.length - curBufferPos;
System.arraycopy(input, inputIndex, dataBuffer, curBufferPos, transferAmt);
count += transferAmt;
curBufferPos = 0;
inputIndex += transferAmt;
processBuffer();
}
// If there's anything left, copy it into the buffer and leave it.
// We know there's not enough left to process.
if (inputIndex < input.length) {
int transferAmt = input.length - inputIndex;
System.arraycopy(input, inputIndex, dataBuffer, curBufferPos, transferAmt);
count += transferAmt;
curBufferPos += transferAmt;
}
}
byte[] getOutput() {
// Feed pad/length data into engine. This must round out the input
// to a multiple of 512 bits.
int bufferIndex = (int) (count & 63L);
int padLen = (bufferIndex < 56) ? (56 - bufferIndex) : (120 - bufferIndex);
byte[] postBytes = new byte[padLen + 8];
// Leading 0x80, specified amount of zero padding, then length in
// bits.
postBytes[0] = (byte) 0x80;
// Fill out the last 8 bytes with the length
for (int i = 0; i < 8; i++) {
postBytes[padLen + i] = (byte) ((count * 8) >>> (8 * i));
}
// Update the engine
update(postBytes);
// Calculate final result
byte[] result = new byte[16];
writeULong(result, A, 0);
writeULong(result, B, 4);
writeULong(result, C, 8);
writeULong(result, D, 12);
return result;
}
protected void processBuffer() {
// Convert current buffer to 16 ulongs
int[] d = new int[16];
for (int i = 0; i < 16; i++) {
d[i] = (dataBuffer[i * 4] & 0xff) + ((dataBuffer[i * 4 + 1] & 0xff) << 8)
+ ((dataBuffer[i * 4 + 2] & 0xff) << 16)
+ ((dataBuffer[i * 4 + 3] & 0xff) << 24);
}
// Do a round of processing
int AA = A;
int BB = B;
int CC = C;
int DD = D;
round1(d);
round2(d);
round3(d);
A += AA;
B += BB;
C += CC;
D += DD;
}
protected void round1(int[] d) {
A = rotintlft((A + F(B, C, D) + d[0]), 3);
D = rotintlft((D + F(A, B, C) + d[1]), 7);
C = rotintlft((C + F(D, A, B) + d[2]), 11);
B = rotintlft((B + F(C, D, A) + d[3]), 19);
A = rotintlft((A + F(B, C, D) + d[4]), 3);
D = rotintlft((D + F(A, B, C) + d[5]), 7);
C = rotintlft((C + F(D, A, B) + d[6]), 11);
B = rotintlft((B + F(C, D, A) + d[7]), 19);
A = rotintlft((A + F(B, C, D) + d[8]), 3);
D = rotintlft((D + F(A, B, C) + d[9]), 7);
C = rotintlft((C + F(D, A, B) + d[10]), 11);
B = rotintlft((B + F(C, D, A) + d[11]), 19);
A = rotintlft((A + F(B, C, D) + d[12]), 3);
D = rotintlft((D + F(A, B, C) + d[13]), 7);
C = rotintlft((C + F(D, A, B) + d[14]), 11);
B = rotintlft((B + F(C, D, A) + d[15]), 19);
}
protected void round2(int[] d) {
A = rotintlft((A + G(B, C, D) + d[0] + 0x5a827999), 3);
D = rotintlft((D + G(A, B, C) + d[4] + 0x5a827999), 5);
C = rotintlft((C + G(D, A, B) + d[8] + 0x5a827999), 9);
B = rotintlft((B + G(C, D, A) + d[12] + 0x5a827999), 13);
A = rotintlft((A + G(B, C, D) + d[1] + 0x5a827999), 3);
D = rotintlft((D + G(A, B, C) + d[5] + 0x5a827999), 5);
C = rotintlft((C + G(D, A, B) + d[9] + 0x5a827999), 9);
B = rotintlft((B + G(C, D, A) + d[13] + 0x5a827999), 13);
A = rotintlft((A + G(B, C, D) + d[2] + 0x5a827999), 3);
D = rotintlft((D + G(A, B, C) + d[6] + 0x5a827999), 5);
C = rotintlft((C + G(D, A, B) + d[10] + 0x5a827999), 9);
B = rotintlft((B + G(C, D, A) + d[14] + 0x5a827999), 13);
A = rotintlft((A + G(B, C, D) + d[3] + 0x5a827999), 3);
D = rotintlft((D + G(A, B, C) + d[7] + 0x5a827999), 5);
C = rotintlft((C + G(D, A, B) + d[11] + 0x5a827999), 9);
B = rotintlft((B + G(C, D, A) + d[15] + 0x5a827999), 13);
}
protected void round3(int[] d) {
A = rotintlft((A + H(B, C, D) + d[0] + 0x6ed9eba1), 3);
D = rotintlft((D + H(A, B, C) + d[8] + 0x6ed9eba1), 9);
C = rotintlft((C + H(D, A, B) + d[4] + 0x6ed9eba1), 11);
B = rotintlft((B + H(C, D, A) + d[12] + 0x6ed9eba1), 15);
A = rotintlft((A + H(B, C, D) + d[2] + 0x6ed9eba1), 3);
D = rotintlft((D + H(A, B, C) + d[10] + 0x6ed9eba1), 9);
C = rotintlft((C + H(D, A, B) + d[6] + 0x6ed9eba1), 11);
B = rotintlft((B + H(C, D, A) + d[14] + 0x6ed9eba1), 15);
A = rotintlft((A + H(B, C, D) + d[1] + 0x6ed9eba1), 3);
D = rotintlft((D + H(A, B, C) + d[9] + 0x6ed9eba1), 9);
C = rotintlft((C + H(D, A, B) + d[5] + 0x6ed9eba1), 11);
B = rotintlft((B + H(C, D, A) + d[13] + 0x6ed9eba1), 15);
A = rotintlft((A + H(B, C, D) + d[3] + 0x6ed9eba1), 3);
D = rotintlft((D + H(A, B, C) + d[11] + 0x6ed9eba1), 9);
C = rotintlft((C + H(D, A, B) + d[7] + 0x6ed9eba1), 11);
B = rotintlft((B + H(C, D, A) + d[15] + 0x6ed9eba1), 15);
}
}
/**
* Cryptography support - HMACMD5 - algorithmically based on various web
* resources by Karl Wright
*/
static class HMACMD5 {
protected byte[] ipad;
protected byte[] opad;
protected MessageDigest md5;
HMACMD5(byte[] key) throws NTLMEngineException {
try {
md5 = MessageDigest.getInstance("MD5");
} catch (Exception ex) {
// Umm, the algorithm doesn't exist - throw an
// NTLMEngineException!
throw new NTLMEngineException(
"Error getting md5 message digest implementation: " + ex.getMessage(), ex);
}
// Initialize the pad buffers with the key
ipad = new byte[64];
opad = new byte[64];
int keyLength = key.length;
if (keyLength > 64) {
// Use MD5 of the key instead, as described in RFC 2104
md5.update(key);
key = md5.digest();
keyLength = key.length;
}
int i = 0;
while (i < keyLength) {
ipad[i] = (byte) (key[i] ^ (byte) 0x36);
opad[i] = (byte) (key[i] ^ (byte) 0x5c);
i++;
}
while (i < 64) {
ipad[i] = (byte) 0x36;
opad[i] = (byte) 0x5c;
i++;
}
// Very important: update the digest with the ipad buffer
md5.reset();
md5.update(ipad);
}
/** Grab the current digest. This is the "answer". */
byte[] getOutput() {
byte[] digest = md5.digest();
md5.update(opad);
return md5.digest(digest);
}
/** Update by adding a complete array */
void update(byte[] input) {
md5.update(input);
}
/** Update the algorithm */
void update(byte[] input, int offset, int length) {
md5.update(input, offset, length);
}
}
public String generateType1Msg(
final String domain,
final String workstation) throws NTLMEngineException {
return getType1Message(workstation, domain);
}
public String generateType3Msg(
final String username,
final String password,
final String domain,
final String workstation,
final String challenge) throws NTLMEngineException {
Type2Message t2m = new Type2Message(challenge);
return getType3Message(
username,
password,
workstation,
domain,
t2m.getChallenge(),
t2m.getFlags(),
t2m.getTarget(),
t2m.getTargetInfo());
}
}