org.apache.http.impl.auth.NTLMEngineImpl Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of httpclient Show documentation
Show all versions of httpclient Show documentation
Apache HttpComponents Client
The newest version!
/*
* ====================================================================
* 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
* .
*
*/
package org.apache.http.impl.auth;
import java.nio.charset.Charset;
import java.security.Key;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.util.Arrays;
import java.util.Locale;
import java.util.Random;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;
import org.apache.http.Consts;
/**
* Provides an implementation for NTLMv1, NTLMv2, and NTLM2 Session forms of the NTLM
* authentication protocol.
*
* @since 4.1
*/
final class NTLMEngineImpl implements NTLMEngine {
/** Unicode encoding */
private static final Charset UNICODE_LITTLE_UNMARKED = Charset.forName("UnicodeLittleUnmarked");
/** Character encoding */
private static final Charset DEFAULT_CHARSET = Consts.ASCII;
// Flags we use; descriptions according to:
// http://davenport.sourceforge.net/ntlm.html
// and
// http://msdn.microsoft.com/en-us/library/cc236650%28v=prot.20%29.aspx
// [MS-NLMP] section 2.2.2.5
static final int FLAG_REQUEST_UNICODE_ENCODING = 0x00000001; // Unicode string encoding requested
static final int FLAG_REQUEST_OEM_ENCODING = 0x00000002; // OEM string encoding requested
static final int FLAG_REQUEST_TARGET = 0x00000004; // Requests target field
static final int FLAG_REQUEST_SIGN = 0x00000010; // Requests all messages have a signature attached, in NEGOTIATE message.
static final int FLAG_REQUEST_SEAL = 0x00000020; // Request key exchange for message confidentiality in NEGOTIATE message. MUST be used in conjunction with 56BIT.
static final int FLAG_REQUEST_LAN_MANAGER_KEY = 0x00000080; // Request Lan Manager key instead of user session key
static final int FLAG_REQUEST_NTLMv1 = 0x00000200; // Request NTLMv1 security. MUST be set in NEGOTIATE and CHALLENGE both
static final int FLAG_DOMAIN_PRESENT = 0x00001000; // Domain is present in message
static final int FLAG_WORKSTATION_PRESENT = 0x00002000; // Workstation is present in message
static final int FLAG_REQUEST_ALWAYS_SIGN = 0x00008000; // Requests a signature block on all messages. Overridden by REQUEST_SIGN and REQUEST_SEAL.
static final int FLAG_REQUEST_NTLM2_SESSION = 0x00080000; // From server in challenge, requesting NTLM2 session security
static final int FLAG_REQUEST_VERSION = 0x02000000; // Request protocol version
static final int FLAG_TARGETINFO_PRESENT = 0x00800000; // From server in challenge message, indicating targetinfo is present
static final int FLAG_REQUEST_128BIT_KEY_EXCH = 0x20000000; // Request explicit 128-bit key exchange
static final int FLAG_REQUEST_EXPLICIT_KEY_EXCH = 0x40000000; // Request explicit key exchange
static final int FLAG_REQUEST_56BIT_ENCRYPTION = 0x80000000; // Must be used in conjunction with SEAL
// Attribute-value identifiers (AvId)
// according to [MS-NLMP] section 2.2.2.1
static final int MSV_AV_EOL = 0x0000; // Indicates that this is the last AV_PAIR in the list.
static final int MSV_AV_NB_COMPUTER_NAME = 0x0001; // The server's NetBIOS computer name.
static final int MSV_AV_NB_DOMAIN_NAME = 0x0002; // The server's NetBIOS domain name.
static final int MSV_AV_DNS_COMPUTER_NAME = 0x0003; // The fully qualified domain name (FQDN) of the computer.
static final int MSV_AV_DNS_DOMAIN_NAME = 0x0004; // The FQDN of the domain.
static final int MSV_AV_DNS_TREE_NAME = 0x0005; // The FQDN of the forest.
static final int MSV_AV_FLAGS = 0x0006; // A 32-bit value indicating server or client configuration.
static final int MSV_AV_TIMESTAMP = 0x0007; // server local time
static final int MSV_AV_SINGLE_HOST = 0x0008; // A Single_Host_Data structure.
static final int MSV_AV_TARGET_NAME = 0x0009; // The SPN of the target server.
static final int MSV_AV_CHANNEL_BINDINGS = 0x000A; // A channel bindings hash.
static final int MSV_AV_FLAGS_ACCOUNT_AUTH_CONSTAINED = 0x00000001; // Indicates to the client that the account authentication is constrained.
static final int MSV_AV_FLAGS_MIC = 0x00000002; // Indicates that the client is providing message integrity in the MIC field in the AUTHENTICATE_MESSAGE.
static final int MSV_AV_FLAGS_UNTRUSTED_TARGET_SPN = 0x00000004; // Indicates that the client is providing a target SPN generated from an untrusted source.
/** 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 (final Exception ignore) {
}
RND_GEN = rnd;
}
/** The signature string as bytes in the default encoding */
private static final byte[] SIGNATURE = getNullTerminatedAsciiString("NTLMSSP");
// Key derivation magic strings for the SIGNKEY algorithm defined in
// [MS-NLMP] section 3.4.5.2
private static final byte[] SIGN_MAGIC_SERVER = getNullTerminatedAsciiString(
"session key to server-to-client signing key magic constant");
private static final byte[] SIGN_MAGIC_CLIENT = getNullTerminatedAsciiString(
"session key to client-to-server signing key magic constant");
private static final byte[] SEAL_MAGIC_SERVER = getNullTerminatedAsciiString(
"session key to server-to-client sealing key magic constant");
private static final byte[] SEAL_MAGIC_CLIENT = getNullTerminatedAsciiString(
"session key to client-to-server sealing key magic constant");
// prefix for GSS API channel binding
private static final byte[] MAGIC_TLS_SERVER_ENDPOINT = "tls-server-end-point:".getBytes(Consts.ASCII);
private static byte[] getNullTerminatedAsciiString( final String source )
{
final byte[] bytesWithoutNull = source.getBytes(Consts.ASCII);
final byte[] target = new byte[bytesWithoutNull.length + 1];
System.arraycopy(bytesWithoutNull, 0, target, 0, bytesWithoutNull.length);
target[bytesWithoutNull.length] = (byte) 0x00;
return target;
}
private static final String TYPE_1_MESSAGE = new Type1Message().getResponse();
NTLMEngineImpl() {
}
/**
* 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.
*/
static String getType1Message(final String host, final String domain) {
// For compatibility reason do not include domain and host in type 1 message
//return new Type1Message(domain, host).getResponse();
return TYPE_1_MESSAGE;
}
/**
* 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 {@link #Type3Message
* (String, String, String, String, byte[], int, String, byte[])} fails.
*/
static String getType3Message(final String user, final String password, final String host, final String domain,
final byte[] nonce, final int type2Flags, final String target, final byte[] targetInformation)
throws NTLMEngineException {
return new Type3Message(domain, host, user, password, nonce, type2Flags, target,
targetInformation).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 {@link #Type3Message
* (String, String, String, String, byte[], int, String, byte[], Certificate, byte[], byte[])} fails.
*/
static String getType3Message(final String user, final String password, final String host, final String domain,
final byte[] nonce, final int type2Flags, final String target, final byte[] targetInformation,
final Certificate peerServerCertificate, final byte[] type1Message, final byte[] type2Message)
throws NTLMEngineException {
return new Type3Message(domain, host, user, password, nonce, type2Flags, target,
targetInformation, peerServerCertificate, type1Message, type2Message).getResponse();
}
private static int readULong(final byte[] src, final int index) {
if (src.length < index + 4) {
return 0;
}
return (src[index] & 0xff) | ((src[index + 1] & 0xff) << 8)
| ((src[index + 2] & 0xff) << 16) | ((src[index + 3] & 0xff) << 24);
}
private static int readUShort(final byte[] src, final int index) {
if (src.length < index + 2) {
return 0;
}
return (src[index] & 0xff) | ((src[index + 1] & 0xff) << 8);
}
private static byte[] readSecurityBuffer(final byte[] src, final int index) {
final int length = readUShort(src, index);
final int offset = readULong(src, index + 4);
if (src.length < offset + length) {
return new byte[length];
}
final byte[] buffer = new byte[length];
System.arraycopy(src, offset, buffer, 0, length);
return buffer;
}
/** Calculate a challenge block */
private static byte[] makeRandomChallenge(final Random random) {
final byte[] rval = new byte[8];
synchronized (random) {
random.nextBytes(rval);
}
return rval;
}
/** Calculate a 16-byte secondary key */
private static byte[] makeSecondaryKey(final Random random) {
final byte[] rval = new byte[16];
synchronized (random) {
random.nextBytes(rval);
}
return rval;
}
protected static class CipherGen {
protected final Random random;
protected final long currentTime;
protected final String domain;
protected final String user;
protected final String password;
protected final byte[] challenge;
protected final String target;
protected final byte[] targetInformation;
// Information we can generate but may be passed in (for testing)
protected byte[] clientChallenge;
protected byte[] clientChallenge2;
protected byte[] secondaryKey;
protected byte[] timestamp;
// Stuff we always generate
protected byte[] lmHash = null;
protected byte[] lmResponse = null;
protected byte[] ntlmHash = null;
protected byte[] ntlmResponse = null;
protected byte[] ntlmv2Hash = null;
protected byte[] lmv2Hash = null;
protected byte[] lmv2Response = null;
protected byte[] ntlmv2Blob = null;
protected byte[] ntlmv2Response = null;
protected byte[] ntlm2SessionResponse = null;
protected byte[] lm2SessionResponse = null;
protected byte[] lmUserSessionKey = null;
protected byte[] ntlmUserSessionKey = null;
protected byte[] ntlmv2UserSessionKey = null;
protected byte[] ntlm2SessionResponseUserSessionKey = null;
protected byte[] lanManagerSessionKey = null;
/**
* @deprecated Use {@link CipherGen#CipherGen(Random, long, String, String, String, byte[], String, byte[], byte[], byte[], byte[], byte[])}
*/
@Deprecated
public CipherGen(final String domain, final String user, final String password,
final byte[] challenge, final String target, final byte[] targetInformation,
final byte[] clientChallenge, final byte[] clientChallenge2,
final byte[] secondaryKey, final byte[] timestamp) {
this(RND_GEN, System.currentTimeMillis(),
domain, user, password, challenge, target, targetInformation,
clientChallenge, clientChallenge2,
secondaryKey, timestamp);
}
public CipherGen(final Random random, final long currentTime,
final String domain, final String user, final String password,
final byte[] challenge, final String target, final byte[] targetInformation,
final byte[] clientChallenge, final byte[] clientChallenge2,
final byte[] secondaryKey, final byte[] timestamp) {
this.random = random;
this.currentTime = currentTime;
this.domain = domain;
this.target = target;
this.user = user;
this.password = password;
this.challenge = challenge;
this.targetInformation = targetInformation;
this.clientChallenge = clientChallenge;
this.clientChallenge2 = clientChallenge2;
this.secondaryKey = secondaryKey;
this.timestamp = timestamp;
}
/**
* @deprecated Use {@link CipherGen#CipherGen(Random, long, String, String, String, byte[], String, byte[], byte[], byte[], byte[], byte[])}
*/
@Deprecated
public CipherGen(final String domain,
final String user,
final String password,
final byte[] challenge,
final String target,
final byte[] targetInformation) {
this(RND_GEN, System.currentTimeMillis(), domain, user, password, challenge, target, targetInformation);
}
public CipherGen(final Random random, final long currentTime,
final String domain,
final String user,
final String password,
final byte[] challenge,
final String target,
final byte[] targetInformation) {
this(random, currentTime, domain, user, password, challenge, target, targetInformation, null, null, null, null);
}
/** Calculate and return client challenge */
public byte[] getClientChallenge()
throws NTLMEngineException {
if (clientChallenge == null) {
clientChallenge = makeRandomChallenge(random);
}
return clientChallenge;
}
/** Calculate and return second client challenge */
public byte[] getClientChallenge2()
throws NTLMEngineException {
if (clientChallenge2 == null) {
clientChallenge2 = makeRandomChallenge(random);
}
return clientChallenge2;
}
/** Calculate and return random secondary key */
public byte[] getSecondaryKey()
throws NTLMEngineException {
if (secondaryKey == null) {
secondaryKey = makeSecondaryKey(random);
}
return secondaryKey;
}
/** Calculate and return the LMHash */
public byte[] getLMHash()
throws NTLMEngineException {
if (lmHash == null) {
lmHash = lmHash(password);
}
return lmHash;
}
/** Calculate and return the LMResponse */
public byte[] getLMResponse()
throws NTLMEngineException {
if (lmResponse == null) {
lmResponse = lmResponse(getLMHash(),challenge);
}
return lmResponse;
}
/** Calculate and return the NTLMHash */
public byte[] getNTLMHash()
throws NTLMEngineException {
if (ntlmHash == null) {
ntlmHash = ntlmHash(password);
}
return ntlmHash;
}
/** Calculate and return the NTLMResponse */
public byte[] getNTLMResponse()
throws NTLMEngineException {
if (ntlmResponse == null) {
ntlmResponse = lmResponse(getNTLMHash(),challenge);
}
return ntlmResponse;
}
/** Calculate the LMv2 hash */
public byte[] getLMv2Hash()
throws NTLMEngineException {
if (lmv2Hash == null) {
lmv2Hash = lmv2Hash(domain, user, getNTLMHash());
}
return lmv2Hash;
}
/** Calculate the NTLMv2 hash */
public byte[] getNTLMv2Hash()
throws NTLMEngineException {
if (ntlmv2Hash == null) {
ntlmv2Hash = ntlmv2Hash(domain, user, getNTLMHash());
}
return ntlmv2Hash;
}
/** Calculate a timestamp */
public byte[] getTimestamp() {
if (timestamp == null) {
long time = this.currentTime;
time += 11644473600000L; // milliseconds from January 1, 1601 -> epoch.
time *= 10000; // tenths of a microsecond.
// convert to little-endian byte array.
timestamp = new byte[8];
for (int i = 0; i < 8; i++) {
timestamp[i] = (byte) time;
time >>>= 8;
}
}
return timestamp;
}
/** Calculate the NTLMv2Blob */
public byte[] getNTLMv2Blob()
throws NTLMEngineException {
if (ntlmv2Blob == null) {
ntlmv2Blob = createBlob(getClientChallenge2(), targetInformation, getTimestamp());
}
return ntlmv2Blob;
}
/** Calculate the NTLMv2Response */
public byte[] getNTLMv2Response()
throws NTLMEngineException {
if (ntlmv2Response == null) {
ntlmv2Response = lmv2Response(getNTLMv2Hash(),challenge,getNTLMv2Blob());
}
return ntlmv2Response;
}
/** Calculate the LMv2Response */
public byte[] getLMv2Response()
throws NTLMEngineException {
if (lmv2Response == null) {
lmv2Response = lmv2Response(getLMv2Hash(),challenge,getClientChallenge());
}
return lmv2Response;
}
/** Get NTLM2SessionResponse */
public byte[] getNTLM2SessionResponse()
throws NTLMEngineException {
if (ntlm2SessionResponse == null) {
ntlm2SessionResponse = ntlm2SessionResponse(getNTLMHash(),challenge,getClientChallenge());
}
return ntlm2SessionResponse;
}
/** Calculate and return LM2 session response */
public byte[] getLM2SessionResponse()
throws NTLMEngineException {
if (lm2SessionResponse == null) {
final byte[] clntChallenge = getClientChallenge();
lm2SessionResponse = new byte[24];
System.arraycopy(clntChallenge, 0, lm2SessionResponse, 0, clntChallenge.length);
Arrays.fill(lm2SessionResponse, clntChallenge.length, lm2SessionResponse.length, (byte) 0x00);
}
return lm2SessionResponse;
}
/** Get LMUserSessionKey */
public byte[] getLMUserSessionKey()
throws NTLMEngineException {
if (lmUserSessionKey == null) {
lmUserSessionKey = new byte[16];
System.arraycopy(getLMHash(), 0, lmUserSessionKey, 0, 8);
Arrays.fill(lmUserSessionKey, 8, 16, (byte) 0x00);
}
return lmUserSessionKey;
}
/** Get NTLMUserSessionKey */
public byte[] getNTLMUserSessionKey()
throws NTLMEngineException {
if (ntlmUserSessionKey == null) {
final MD4 md4 = new MD4();
md4.update(getNTLMHash());
ntlmUserSessionKey = md4.getOutput();
}
return ntlmUserSessionKey;
}
/** GetNTLMv2UserSessionKey */
public byte[] getNTLMv2UserSessionKey()
throws NTLMEngineException {
if (ntlmv2UserSessionKey == null) {
final byte[] ntlmv2hash = getNTLMv2Hash();
final byte[] truncatedResponse = new byte[16];
System.arraycopy(getNTLMv2Response(), 0, truncatedResponse, 0, 16);
ntlmv2UserSessionKey = hmacMD5(truncatedResponse, ntlmv2hash);
}
return ntlmv2UserSessionKey;
}
/** Get NTLM2SessionResponseUserSessionKey */
public byte[] getNTLM2SessionResponseUserSessionKey()
throws NTLMEngineException {
if (ntlm2SessionResponseUserSessionKey == null) {
final byte[] ntlm2SessionResponseNonce = getLM2SessionResponse();
final byte[] sessionNonce = new byte[challenge.length + ntlm2SessionResponseNonce.length];
System.arraycopy(challenge, 0, sessionNonce, 0, challenge.length);
System.arraycopy(ntlm2SessionResponseNonce, 0, sessionNonce, challenge.length, ntlm2SessionResponseNonce.length);
ntlm2SessionResponseUserSessionKey = hmacMD5(sessionNonce,getNTLMUserSessionKey());
}
return ntlm2SessionResponseUserSessionKey;
}
/** Get LAN Manager session key */
public byte[] getLanManagerSessionKey()
throws NTLMEngineException {
if (lanManagerSessionKey == null) {
try {
final byte[] keyBytes = new byte[14];
System.arraycopy(getLMHash(), 0, keyBytes, 0, 8);
Arrays.fill(keyBytes, 8, keyBytes.length, (byte)0xbd);
final Key lowKey = createDESKey(keyBytes, 0);
final Key highKey = createDESKey(keyBytes, 7);
final byte[] truncatedResponse = new byte[8];
System.arraycopy(getLMResponse(), 0, truncatedResponse, 0, truncatedResponse.length);
Cipher des = Cipher.getInstance("DES/ECB/NoPadding");
des.init(Cipher.ENCRYPT_MODE, lowKey);
final byte[] lowPart = des.doFinal(truncatedResponse);
des = Cipher.getInstance("DES/ECB/NoPadding");
des.init(Cipher.ENCRYPT_MODE, highKey);
final byte[] highPart = des.doFinal(truncatedResponse);
lanManagerSessionKey = new byte[16];
System.arraycopy(lowPart, 0, lanManagerSessionKey, 0, lowPart.length);
System.arraycopy(highPart, 0, lanManagerSessionKey, lowPart.length, highPart.length);
} catch (final Exception e) {
throw new NTLMEngineException(e.getMessage(), e);
}
}
return lanManagerSessionKey;
}
}
/** Calculates HMAC-MD5 */
static byte[] hmacMD5(final byte[] value, final byte[] key)
throws NTLMEngineException {
final HMACMD5 hmacMD5 = new HMACMD5(key);
hmacMD5.update(value);
return hmacMD5.getOutput();
}
/** Calculates RC4 */
static byte[] RC4(final byte[] value, final byte[] key)
throws NTLMEngineException {
try {
final Cipher rc4 = Cipher.getInstance("RC4");
rc4.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "RC4"));
return rc4.doFinal(value);
} catch (final Exception e) {
throw new NTLMEngineException(e.getMessage(), e);
}
}
/**
* Calculates the NTLM2 Session Response for the given challenge, using the
* specified password and 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[] ntlm2SessionResponse(final byte[] ntlmHash, final byte[] challenge,
final byte[] clientChallenge) throws NTLMEngineException {
try {
final MessageDigest md5 = getMD5();
md5.update(challenge);
md5.update(clientChallenge);
final byte[] digest = md5.digest();
final byte[] sessionHash = new byte[8];
System.arraycopy(digest, 0, sessionHash, 0, 8);
return lmResponse(ntlmHash, sessionHash);
} catch (final 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(final String password) throws NTLMEngineException {
try {
final byte[] oemPassword = password.toUpperCase(Locale.ROOT).getBytes(Consts.ASCII);
final int length = Math.min(oemPassword.length, 14);
final byte[] keyBytes = new byte[14];
System.arraycopy(oemPassword, 0, keyBytes, 0, length);
final Key lowKey = createDESKey(keyBytes, 0);
final Key highKey = createDESKey(keyBytes, 7);
final byte[] magicConstant = "KGS!@#$%".getBytes(Consts.ASCII);
final Cipher des = Cipher.getInstance("DES/ECB/NoPadding");
des.init(Cipher.ENCRYPT_MODE, lowKey);
final byte[] lowHash = des.doFinal(magicConstant);
des.init(Cipher.ENCRYPT_MODE, highKey);
final byte[] highHash = des.doFinal(magicConstant);
final byte[] lmHash = new byte[16];
System.arraycopy(lowHash, 0, lmHash, 0, 8);
System.arraycopy(highHash, 0, lmHash, 8, 8);
return lmHash;
} catch (final 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(final String password) throws NTLMEngineException {
if (UNICODE_LITTLE_UNMARKED == null) {
throw new NTLMEngineException("Unicode not supported");
}
final byte[] unicodePassword = password.getBytes(UNICODE_LITTLE_UNMARKED);
final MD4 md4 = new MD4();
md4.update(unicodePassword);
return md4.getOutput();
}
/**
* Creates the LMv2 Hash of the user's password.
*
* @return The LMv2 Hash, used in the calculation of the NTLMv2 and LMv2
* Responses.
*/
private static byte[] lmv2Hash(final String domain, final String user, final byte[] ntlmHash)
throws NTLMEngineException {
if (UNICODE_LITTLE_UNMARKED == null) {
throw new NTLMEngineException("Unicode not supported");
}
final HMACMD5 hmacMD5 = new HMACMD5(ntlmHash);
// Upper case username, upper case domain!
hmacMD5.update(user.toUpperCase(Locale.ROOT).getBytes(UNICODE_LITTLE_UNMARKED));
if (domain != null) {
hmacMD5.update(domain.toUpperCase(Locale.ROOT).getBytes(UNICODE_LITTLE_UNMARKED));
}
return hmacMD5.getOutput();
}
/**
* Creates the NTLMv2 Hash of the user's password.
*
* @return The NTLMv2 Hash, used in the calculation of the NTLMv2 and LMv2
* Responses.
*/
private static byte[] ntlmv2Hash(final String domain, final String user, final byte[] ntlmHash)
throws NTLMEngineException {
if (UNICODE_LITTLE_UNMARKED == null) {
throw new NTLMEngineException("Unicode not supported");
}
final HMACMD5 hmacMD5 = new HMACMD5(ntlmHash);
// Upper case username, mixed case target!!
hmacMD5.update(user.toUpperCase(Locale.ROOT).getBytes(UNICODE_LITTLE_UNMARKED));
if (domain != null) {
hmacMD5.update(domain.getBytes(UNICODE_LITTLE_UNMARKED));
}
return hmacMD5.getOutput();
}
/**
* 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(final byte[] hash, final byte[] challenge) throws NTLMEngineException {
try {
final byte[] keyBytes = new byte[21];
System.arraycopy(hash, 0, keyBytes, 0, 16);
final Key lowKey = createDESKey(keyBytes, 0);
final Key middleKey = createDESKey(keyBytes, 7);
final Key highKey = createDESKey(keyBytes, 14);
final Cipher des = Cipher.getInstance("DES/ECB/NoPadding");
des.init(Cipher.ENCRYPT_MODE, lowKey);
final byte[] lowResponse = des.doFinal(challenge);
des.init(Cipher.ENCRYPT_MODE, middleKey);
final byte[] middleResponse = des.doFinal(challenge);
des.init(Cipher.ENCRYPT_MODE, highKey);
final byte[] highResponse = des.doFinal(challenge);
final 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 (final 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(final byte[] hash, final byte[] challenge, final byte[] clientData) {
final HMACMD5 hmacMD5 = new HMACMD5(hash);
hmacMD5.update(challenge);
hmacMD5.update(clientData);
final byte[] mac = hmacMD5.getOutput();
final 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;
}
enum Mode
{
CLIENT, SERVER;
}
static class Handle
{
final private byte[] exportedSessionKey;
private byte[] signingKey;
private byte[] sealingKey;
private final Cipher rc4;
final Mode mode;
final private boolean isConnection;
int sequenceNumber = 0;
Handle( final byte[] exportedSessionKey, final Mode mode, final boolean isConnection )
throws NTLMEngineException
{
this.exportedSessionKey = exportedSessionKey;
this.isConnection = isConnection;
this.mode = mode;
try
{
final MessageDigest signMd5 = getMD5();
final MessageDigest sealMd5 = getMD5();
signMd5.update( exportedSessionKey );
sealMd5.update( exportedSessionKey );
if ( mode == Mode.CLIENT )
{
signMd5.update( SIGN_MAGIC_CLIENT );
sealMd5.update( SEAL_MAGIC_CLIENT );
}
else
{
signMd5.update( SIGN_MAGIC_SERVER );
sealMd5.update( SEAL_MAGIC_SERVER );
}
signingKey = signMd5.digest();
sealingKey = sealMd5.digest();
}
catch ( final Exception e )
{
throw new NTLMEngineException( e.getMessage(), e );
}
rc4 = initCipher();
}
public byte[] getSigningKey()
{
return signingKey;
}
public byte[] getSealingKey()
{
return sealingKey;
}
private Cipher initCipher() throws NTLMEngineException
{
final Cipher cipher;
try
{
cipher = Cipher.getInstance( "RC4" );
if ( mode == Mode.CLIENT )
{
cipher.init( Cipher.ENCRYPT_MODE, new SecretKeySpec( sealingKey, "RC4" ) );
}
else
{
cipher.init( Cipher.DECRYPT_MODE, new SecretKeySpec( sealingKey, "RC4" ) );
}
}
catch ( final Exception e )
{
throw new NTLMEngineException( e.getMessage(), e );
}
return cipher;
}
private void advanceMessageSequence() throws NTLMEngineException
{
if ( !isConnection )
{
final MessageDigest sealMd5 = getMD5();
sealMd5.update( sealingKey );
final byte[] seqNumBytes = new byte[4];
writeULong( seqNumBytes, sequenceNumber, 0 );
sealMd5.update( seqNumBytes );
sealingKey = sealMd5.digest();
initCipher();
}
sequenceNumber++;
}
private byte[] encrypt( final byte[] data )
{
return rc4.update( data );
}
private byte[] decrypt( final byte[] data )
{
return rc4.update( data );
}
private byte[] computeSignature( final byte[] message )
{
final byte[] sig = new byte[16];
// version
sig[0] = 0x01;
sig[1] = 0x00;
sig[2] = 0x00;
sig[3] = 0x00;
// HMAC (first 8 bytes)
final HMACMD5 hmacMD5 = new HMACMD5( signingKey );
hmacMD5.update( encodeLong( sequenceNumber ) );
hmacMD5.update( message );
final byte[] hmac = hmacMD5.getOutput();
final byte[] trimmedHmac = new byte[8];
System.arraycopy( hmac, 0, trimmedHmac, 0, 8 );
final byte[] encryptedHmac = encrypt( trimmedHmac );
System.arraycopy( encryptedHmac, 0, sig, 4, 8 );
// sequence number
encodeLong( sig, 12, sequenceNumber );
return sig;
}
private boolean validateSignature( final byte[] signature, final byte message[] )
{
final byte[] computedSignature = computeSignature( message );
// log.info( "SSSSS validateSignature("+seqNumber+")\n"
// + " received: " + DebugUtil.dump( signature ) + "\n"
// + " computed: " + DebugUtil.dump( computedSignature ) );
return Arrays.equals( signature, computedSignature );
}
public byte[] signAndEncryptMessage( final byte[] cleartextMessage ) throws NTLMEngineException
{
final byte[] encryptedMessage = encrypt( cleartextMessage );
final byte[] signature = computeSignature( cleartextMessage );
final byte[] outMessage = new byte[signature.length + encryptedMessage.length];
System.arraycopy( signature, 0, outMessage, 0, signature.length );
System.arraycopy( encryptedMessage, 0, outMessage, signature.length, encryptedMessage.length );
advanceMessageSequence();
return outMessage;
}
public byte[] decryptAndVerifySignedMessage( final byte[] inMessage ) throws NTLMEngineException
{
final byte[] signature = new byte[16];
System.arraycopy( inMessage, 0, signature, 0, signature.length );
final byte[] encryptedMessage = new byte[inMessage.length - 16];
System.arraycopy( inMessage, 16, encryptedMessage, 0, encryptedMessage.length );
final byte[] cleartextMessage = decrypt( encryptedMessage );
if ( !validateSignature( signature, cleartextMessage ) )
{
throw new NTLMEngineException( "Wrong signature" );
}
advanceMessageSequence();
return cleartextMessage;
}
}
private static byte[] encodeLong( final int value )
{
final byte[] enc = new byte[4];
encodeLong( enc, 0, value );
return enc;
}
private static void encodeLong( final byte[] buf, final int offset, final int value )
{
buf[offset + 0] = ( byte ) ( value & 0xff );
buf[offset + 1] = ( byte ) ( value >> 8 & 0xff );
buf[offset + 2] = ( byte ) ( value >> 16 & 0xff );
buf[offset + 3] = ( byte ) ( value >> 24 & 0xff );
}
/**
* 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(final byte[] clientChallenge, final byte[] targetInformation, final byte[] timestamp) {
final byte[] blobSignature = new byte[] { (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x00 };
final byte[] reserved = new byte[] { (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00 };
final byte[] unknown1 = new byte[] { (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00 };
final byte[] unknown2 = new byte[] { (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00 };
final byte[] blob = new byte[blobSignature.length + reserved.length + timestamp.length + 8
+ unknown1.length + targetInformation.length + unknown2.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);
offset += targetInformation.length;
System.arraycopy(unknown2, 0, blob, offset, unknown2.length);
offset += unknown2.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(final byte[] bytes, final int offset) {
final byte[] keyBytes = new byte[7];
System.arraycopy(bytes, offset, keyBytes, 0, 7);
final 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(final byte[] bytes) {
for (int i = 0; i < bytes.length; i++) {
final byte b = bytes[i];
final 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;
}
}
}
/**
* Find the character set based on the flags.
* @param flags is the flags.
* @return the character set.
*/
private static Charset getCharset(final int flags) throws NTLMEngineException
{
if ((flags & FLAG_REQUEST_UNICODE_ENCODING) == 0) {
return DEFAULT_CHARSET;
}
if (UNICODE_LITTLE_UNMARKED == null) {
throw new NTLMEngineException( "Unicode not supported" );
}
return UNICODE_LITTLE_UNMARKED;
}
/** Strip dot suffix from a name */
private static String stripDotSuffix(final String value) {
if (value == null) {
return null;
}
final int index = value.indexOf('.');
if (index != -1) {
return value.substring(0, index);
}
return value;
}
/** Convert host to standard form */
private static String convertHost(final String host) {
return stripDotSuffix(host);
}
/** Convert domain to standard form */
private static String convertDomain(final String domain) {
return stripDotSuffix(domain);
}
/** NTLM message generation, base class */
static class NTLMMessage {
/** The current response */
protected byte[] messageContents = null;
/** The current output position */
protected int currentOutputPosition = 0;
/** Constructor to use when message contents are not yet known */
NTLMMessage() {
}
/** Constructor taking a string */
NTLMMessage(final String messageBody, final int expectedType) throws NTLMEngineException {
this(Base64.decodeBase64(messageBody.getBytes(DEFAULT_CHARSET)), expectedType);
}
/** Constructor to use when message bytes are known */
NTLMMessage(final byte[] message, final int expectedType) throws NTLMEngineException {
messageContents = message;
// 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
final 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(final 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(final byte[] buffer, final 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(final int position) throws NTLMEngineException {
return NTLMEngineImpl.readUShort(messageContents, position);
}
/** Read a ulong from a position within the message buffer */
protected int readULong(final int position) throws NTLMEngineException {
return NTLMEngineImpl.readULong(messageContents, position);
}
/** Read a security buffer from a position within the message buffer */
protected byte[] readSecurityBuffer(final int position) throws NTLMEngineException {
return NTLMEngineImpl.readSecurityBuffer(messageContents, position);
}
/**
* Prepares the object to create a response of the given length.
*
* @param maxlength
* the maximum length of the response to prepare,
* including the type and the signature (which this method
* adds).
*/
protected void prepareResponse(final int maxlength, final 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(final byte b) {
messageContents[currentOutputPosition] = b;
currentOutputPosition++;
}
/**
* Adds the given bytes to the response.
*
* @param bytes
* the bytes to add.
*/
protected void addBytes(final byte[] bytes) {
if (bytes == null) {
return;
}
for (final byte b : bytes) {
messageContents[currentOutputPosition] = b;
currentOutputPosition++;
}
}
/** Adds a USHORT to the response */
protected void addUShort(final int value) {
addByte((byte) (value & 0xff));
addByte((byte) (value >> 8 & 0xff));
}
/** Adds a ULong to the response */
protected void addULong(final 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.
*/
public String getResponse() {
return new String(Base64.encodeBase64(getBytes()), Consts.ASCII);
}
public byte[] getBytes() {
if (messageContents == null) {
buildMessage();
}
final byte[] resp;
if ( messageContents.length > currentOutputPosition ) {
final byte[] tmp = new byte[currentOutputPosition];
System.arraycopy( messageContents, 0, tmp, 0, currentOutputPosition );
messageContents = tmp;
}
return messageContents;
}
protected void buildMessage() {
throw new RuntimeException("Message builder not implemented for "+getClass().getName());
}
}
/** Type 1 message assembly class */
static class Type1Message extends NTLMMessage {
private final byte[] hostBytes;
private final byte[] domainBytes;
private final int flags;
Type1Message(final String domain, final String host) throws NTLMEngineException {
this(domain, host, null);
}
Type1Message(final String domain, final String host, final Integer flags) throws NTLMEngineException {
super();
this.flags = ((flags == null)?getDefaultFlags():flags);
// Strip off domain name from the host!
final String unqualifiedHost = convertHost(host);
// Use only the base domain name!
final String unqualifiedDomain = convertDomain(domain);
hostBytes = unqualifiedHost != null ?
unqualifiedHost.getBytes(UNICODE_LITTLE_UNMARKED) : null;
domainBytes = unqualifiedDomain != null ?
unqualifiedDomain.toUpperCase(Locale.ROOT).getBytes(UNICODE_LITTLE_UNMARKED) : null;
}
Type1Message() {
super();
hostBytes = null;
domainBytes = null;
flags = getDefaultFlags();
}
private int getDefaultFlags() {
return
//FLAG_WORKSTATION_PRESENT |
//FLAG_DOMAIN_PRESENT |
// Required flags
//FLAG_REQUEST_LAN_MANAGER_KEY |
FLAG_REQUEST_NTLMv1 |
FLAG_REQUEST_NTLM2_SESSION |
// Protocol version request
FLAG_REQUEST_VERSION |
// Recommended privacy settings
FLAG_REQUEST_ALWAYS_SIGN |
//FLAG_REQUEST_SEAL |
//FLAG_REQUEST_SIGN |
// These must be set according to documentation, based on use of SEAL above
FLAG_REQUEST_128BIT_KEY_EXCH |
FLAG_REQUEST_56BIT_ENCRYPTION |
//FLAG_REQUEST_EXPLICIT_KEY_EXCH |
FLAG_REQUEST_UNICODE_ENCODING;
}
/**
* Getting the response involves building the message before returning
* it
*/
@Override
protected void buildMessage() {
int domainBytesLength = 0;
if ( domainBytes != null ) {
domainBytesLength = domainBytes.length;
}
int hostBytesLength = 0;
if ( hostBytes != null ) {
hostBytesLength = hostBytes.length;
}
// Now, build the message. Calculate its length first, including
// signature or type.
final int finalLength = 32 + 8 + hostBytesLength + domainBytesLength;
// 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(flags);
// Domain length (two times).
addUShort(domainBytesLength);
addUShort(domainBytesLength);
// Domain offset.
addULong(hostBytesLength + 32 + 8);
// Host length (two times).
addUShort(hostBytesLength);
addUShort(hostBytesLength);
// Host offset (always 32 + 8).
addULong(32 + 8);
// Version
addUShort(0x0105);
// Build
addULong(2600);
// NTLM revision
addUShort(0x0f00);
// Host (workstation) String.
if (hostBytes != null) {
addBytes(hostBytes);
}
// Domain String.
if (domainBytes != null) {
addBytes(domainBytes);
}
}
}
/** Type 2 message class */
static class Type2Message extends NTLMMessage {
protected final byte[] challenge;
protected String target;
protected byte[] targetInfo;
protected final int flags;
Type2Message(final String messageBody) throws NTLMEngineException {
this(Base64.decodeBase64(messageBody.getBytes(DEFAULT_CHARSET)));
}
Type2Message(final byte[] message) throws NTLMEngineException {
super(message, 2);
// Type 2 message is laid out as follows:
// First 8 bytes: NTLMSSP[0]
// Next 4 bytes: Ulong, value 2
// Next 8 bytes, starting at offset 12: target field (2 ushort lengths, 1 ulong offset)
// Next 4 bytes, starting at offset 20: Flags, e.g. 0x22890235
// Next 8 bytes, starting at offset 24: Challenge
// Next 8 bytes, starting at offset 32: ??? (8 bytes of zeros)
// Next 8 bytes, starting at offset 40: targetinfo field (2 ushort lengths, 1 ulong offset)
// Next 2 bytes, major/minor version number (e.g. 0x05 0x02)
// Next 8 bytes, build number
// Next 2 bytes, protocol version number (e.g. 0x00 0x0f)
// Next, various text fields, and a ushort of value 0 at the end
// 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);
// 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) {
final byte[] bytes = readSecurityBuffer(12);
if (bytes.length != 0) {
target = new String(bytes, getCharset(flags));
}
}
// Do the target info!
targetInfo = null;
// TARGET_DESIRED flag cannot be relied on, so use packet length
if (getMessageLength() >= 40 + 8) {
final 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 {
// For mic computation
protected final byte[] type1Message;
protected final byte[] type2Message;
// Response flags from the type2 message
protected final int type2Flags;
protected final byte[] domainBytes;
protected final byte[] hostBytes;
protected final byte[] userBytes;
protected byte[] lmResp;
protected byte[] ntResp;
protected final byte[] sessionKey;
protected final byte[] exportedSessionKey;
protected final boolean computeMic;
/** More primitive constructor: don't include cert or previous messages.
*/
Type3Message(final String domain,
final String host,
final String user,
final String password,
final byte[] nonce,
final int type2Flags,
final String target,
final byte[] targetInformation)
throws NTLMEngineException {
this(domain, host, user, password, nonce, type2Flags, target, targetInformation, null, null, null);
}
/** More primitive constructor: don't include cert or previous messages.
*/
Type3Message(final Random random, final long currentTime,
final String domain,
final String host,
final String user,
final String password,
final byte[] nonce,
final int type2Flags,
final String target,
final byte[] targetInformation)
throws NTLMEngineException {
this(random, currentTime, domain, host, user, password, nonce, type2Flags, target, targetInformation, null, null, null);
}
/** Constructor. Pass the arguments we will need */
Type3Message(final String domain,
final String host,
final String user,
final String password,
final byte[] nonce,
final int type2Flags,
final String target,
final byte[] targetInformation,
final Certificate peerServerCertificate,
final byte[] type1Message,
final byte[] type2Message)
throws NTLMEngineException {
this(RND_GEN, System.currentTimeMillis(), domain, host, user, password, nonce, type2Flags, target, targetInformation, peerServerCertificate, type1Message, type2Message);
}
/** Constructor. Pass the arguments we will need */
Type3Message(final Random random, final long currentTime,
final String domain,
final String host,
final String user,
final String password,
final byte[] nonce,
final int type2Flags,
final String target,
final byte[] targetInformation,
final Certificate peerServerCertificate,
final byte[] type1Message,
final byte[] type2Message)
throws NTLMEngineException {
if (random == null) {
throw new NTLMEngineException("Random generator not available");
}
// Save the flags
this.type2Flags = type2Flags;
this.type1Message = type1Message;
this.type2Message = type2Message;
// Strip off domain name from the host!
final String unqualifiedHost = convertHost(host);
// Use only the base domain name!
final String unqualifiedDomain = convertDomain(domain);
byte[] responseTargetInformation = targetInformation;
if (peerServerCertificate != null) {
responseTargetInformation = addGssMicAvsToTargetInfo(targetInformation, peerServerCertificate);
computeMic = true;
} else {
computeMic = false;
}
// Create a cipher generator class. Use domain BEFORE it gets modified!
final CipherGen gen = new CipherGen(random, currentTime,
unqualifiedDomain,
user,
password,
nonce,
target,
responseTargetInformation);
// Use the new code to calculate the responses, including v2 if that
// seems warranted.
byte[] userSessionKey;
try {
// This conditional may not work on Windows Server 2008 R2 and above, where it has not yet
// been tested
if (((type2Flags & FLAG_TARGETINFO_PRESENT) != 0) &&
targetInformation != null && target != null) {
// NTLMv2
ntResp = gen.getNTLMv2Response();
lmResp = gen.getLMv2Response();
if ((type2Flags & FLAG_REQUEST_LAN_MANAGER_KEY) != 0) {
userSessionKey = gen.getLanManagerSessionKey();
} else {
userSessionKey = gen.getNTLMv2UserSessionKey();
}
} else {
// NTLMv1
if ((type2Flags & FLAG_REQUEST_NTLM2_SESSION) != 0) {
// NTLM2 session stuff is requested
ntResp = gen.getNTLM2SessionResponse();
lmResp = gen.getLM2SessionResponse();
if ((type2Flags & FLAG_REQUEST_LAN_MANAGER_KEY) != 0) {
userSessionKey = gen.getLanManagerSessionKey();
} else {
userSessionKey = gen.getNTLM2SessionResponseUserSessionKey();
}
} else {
ntResp = gen.getNTLMResponse();
lmResp = gen.getLMResponse();
if ((type2Flags & FLAG_REQUEST_LAN_MANAGER_KEY) != 0) {
userSessionKey = gen.getLanManagerSessionKey();
} else {
userSessionKey = gen.getNTLMUserSessionKey();
}
}
}
} catch (final NTLMEngineException e) {
// This likely means we couldn't find the MD4 hash algorithm -
// fail back to just using LM
ntResp = new byte[0];
lmResp = gen.getLMResponse();
if ((type2Flags & FLAG_REQUEST_LAN_MANAGER_KEY) != 0) {
userSessionKey = gen.getLanManagerSessionKey();
} else {
userSessionKey = gen.getLMUserSessionKey();
}
}
if ((type2Flags & FLAG_REQUEST_SIGN) != 0) {
if ((type2Flags & FLAG_REQUEST_EXPLICIT_KEY_EXCH) != 0) {
exportedSessionKey = gen.getSecondaryKey();
sessionKey = RC4(exportedSessionKey, userSessionKey);
} else {
sessionKey = userSessionKey;
exportedSessionKey = sessionKey;
}
} else {
if (computeMic) {
throw new NTLMEngineException("Cannot sign/seal: no exported session key");
}
sessionKey = null;
exportedSessionKey = null;
}
final Charset charset = getCharset(type2Flags);
hostBytes = unqualifiedHost != null ? unqualifiedHost.getBytes(charset) : null;
domainBytes = unqualifiedDomain != null ? unqualifiedDomain
.toUpperCase(Locale.ROOT).getBytes(charset) : null;
userBytes = user.getBytes(charset);
}
public byte[] getEncryptedRandomSessionKey() {
return sessionKey;
}
public byte[] getExportedSessionKey() {
return exportedSessionKey;
}
/** Assemble the response */
@Override
protected void buildMessage() {
final int ntRespLen = ntResp.length;
final int lmRespLen = lmResp.length;
final int domainLen = domainBytes != null ? domainBytes.length : 0;
final int hostLen = hostBytes != null ? hostBytes.length: 0;
final int userLen = userBytes.length;
final int sessionKeyLen;
if (sessionKey != null) {
sessionKeyLen = sessionKey.length;
} else {
sessionKeyLen = 0;
}
// Calculate the layout within the packet
final int lmRespOffset = 72 + // allocate space for the version
( computeMic ? 16 : 0 ); // and MIC
final int ntRespOffset = lmRespOffset + lmRespLen;
final int domainOffset = ntRespOffset + ntRespLen;
final int userOffset = domainOffset + domainLen;
final int hostOffset = userOffset + userLen;
final int sessionKeyOffset = hostOffset + hostLen;
final int finalLength = sessionKeyOffset + sessionKeyLen;
// 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);
// Session key length (twice)
addUShort(sessionKeyLen);
addUShort(sessionKeyLen);
// Session key offset
addULong(sessionKeyOffset);
// Flags.
addULong(
/*
//FLAG_WORKSTATION_PRESENT |
//FLAG_DOMAIN_PRESENT |
// Required flags
(type2Flags & FLAG_REQUEST_LAN_MANAGER_KEY) |
(type2Flags & FLAG_REQUEST_NTLMv1) |
(type2Flags & FLAG_REQUEST_NTLM2_SESSION) |
// Protocol version request
FLAG_REQUEST_VERSION |
// Recommended privacy settings
(type2Flags & FLAG_REQUEST_ALWAYS_SIGN) |
(type2Flags & FLAG_REQUEST_SEAL) |
(type2Flags & FLAG_REQUEST_SIGN) |
// These must be set according to documentation, based on use of SEAL above
(type2Flags & FLAG_REQUEST_128BIT_KEY_EXCH) |
(type2Flags & FLAG_REQUEST_56BIT_ENCRYPTION) |
(type2Flags & FLAG_REQUEST_EXPLICIT_KEY_EXCH) |
(type2Flags & FLAG_TARGETINFO_PRESENT) |
(type2Flags & FLAG_REQUEST_UNICODE_ENCODING) |
(type2Flags & FLAG_REQUEST_TARGET)
*/
type2Flags
);
// Version
addUShort(0x0105);
// Build
addULong(2600);
// NTLM revision
addUShort(0x0f00);
int micPosition = -1;
if ( computeMic ) {
micPosition = currentOutputPosition;
currentOutputPosition += 16;
}
// Add the actual data
addBytes(lmResp);
addBytes(ntResp);
addBytes(domainBytes);
addBytes(userBytes);
addBytes(hostBytes);
if (sessionKey != null) {
addBytes(sessionKey);
}
// Write the mic back into its slot in the message
if (computeMic) {
// Computation of message integrity code (MIC) as specified in [MS-NLMP] section 3.2.5.1.2.
final HMACMD5 hmacMD5 = new HMACMD5( exportedSessionKey );
hmacMD5.update( type1Message );
hmacMD5.update( type2Message );
hmacMD5.update( messageContents );
final byte[] mic = hmacMD5.getOutput();
System.arraycopy( mic, 0, messageContents, micPosition, mic.length );
}
}
/**
* Add GSS channel binding hash and MIC flag to the targetInfo.
* Looks like this is needed if we want to use exported session key for GSS wrapping.
*/
private byte[] addGssMicAvsToTargetInfo( final byte[] originalTargetInfo,
final Certificate peerServerCertificate ) throws NTLMEngineException
{
final byte[] newTargetInfo = new byte[originalTargetInfo.length + 8 + 20];
final int appendLength = originalTargetInfo.length - 4; // last tag is MSV_AV_EOL, do not copy that
System.arraycopy( originalTargetInfo, 0, newTargetInfo, 0, appendLength );
writeUShort( newTargetInfo, MSV_AV_FLAGS, appendLength );
writeUShort( newTargetInfo, 4, appendLength + 2 );
writeULong( newTargetInfo, MSV_AV_FLAGS_MIC, appendLength + 4 );
writeUShort( newTargetInfo, MSV_AV_CHANNEL_BINDINGS, appendLength + 8 );
writeUShort( newTargetInfo, 16, appendLength + 10 );
final byte[] channelBindingsHash;
try
{
final byte[] certBytes = peerServerCertificate.getEncoded();
final MessageDigest sha256 = MessageDigest.getInstance( "SHA-256" );
final byte[] certHashBytes = sha256.digest( certBytes );
final byte[] channelBindingStruct = new byte[16 + 4 + MAGIC_TLS_SERVER_ENDPOINT.length
+ certHashBytes.length];
writeULong( channelBindingStruct, 0x00000035, 16 );
System.arraycopy( MAGIC_TLS_SERVER_ENDPOINT, 0, channelBindingStruct, 20,
MAGIC_TLS_SERVER_ENDPOINT.length );
System.arraycopy( certHashBytes, 0, channelBindingStruct, 20 + MAGIC_TLS_SERVER_ENDPOINT.length,
certHashBytes.length );
final MessageDigest md5 = getMD5();
channelBindingsHash = md5.digest( channelBindingStruct );
}
catch ( final CertificateEncodingException e )
{
throw new NTLMEngineException( e.getMessage(), e );
}
catch ( final NoSuchAlgorithmException e )
{
throw new NTLMEngineException( e.getMessage(), e );
}
System.arraycopy( channelBindingsHash, 0, newTargetInfo, appendLength + 12, 16 );
return newTargetInfo;
}
}
static void writeUShort(final byte[] buffer, final int value, final int offset) {
buffer[offset] = ( byte ) ( value & 0xff );
buffer[offset + 1] = ( byte ) ( value >> 8 & 0xff );
}
static void writeULong(final byte[] buffer, final int value, final 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(final int x, final int y, final int z) {
return ((x & y) | (~x & z));
}
static int G(final int x, final int y, final int z) {
return ((x & y) | (x & z) | (y & z));
}
static int H(final int x, final int y, final int z) {
return (x ^ y ^ z);
}
static int rotintlft(final int val, final int numbits) {
return ((val << numbits) | (val >>> (32 - numbits)));
}
static MessageDigest getMD5() {
try {
return MessageDigest.getInstance("MD5");
} catch (final NoSuchAlgorithmException ex) {
throw new RuntimeException("MD5 message digest doesn't seem to exist - fatal error: "+ex.getMessage(), ex);
}
}
/**
* 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 ([email protected]).
*/
static class MD4 {
protected int A = 0x67452301;
protected int B = 0xefcdab89;
protected int C = 0x98badcfe;
protected int D = 0x10325476;
protected long count = 0L;
protected final byte[] dataBuffer = new byte[64];
MD4() {
}
void update(final 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
final 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) {
final 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.
final int bufferIndex = (int) (count & 63L);
final int padLen = (bufferIndex < 56) ? (56 - bufferIndex) : (120 - bufferIndex);
final 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
final 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
final 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
final int AA = A;
final int BB = B;
final int CC = C;
final int DD = D;
round1(d);
round2(d);
round3(d);
A += AA;
B += BB;
C += CC;
D += DD;
}
protected void round1(final 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(final 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(final 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 final byte[] ipad;
protected final byte[] opad;
protected final MessageDigest md5;
HMACMD5(final byte[] input) {
byte[] key = input;
md5 = getMD5();
// 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: processChallenge the digest with the ipad buffer
md5.reset();
md5.update(ipad);
}
/** Grab the current digest. This is the "answer". */
byte[] getOutput() {
final byte[] digest = md5.digest();
md5.update(opad);
return md5.digest(digest);
}
/** Update by adding a complete array */
void update(final byte[] input) {
md5.update(input);
}
/** Update the algorithm */
void update(final byte[] input, final int offset, final int length) {
md5.update(input, offset, length);
}
}
@Override
public String generateType1Msg(
final String domain,
final String workstation) throws NTLMEngineException {
return getType1Message(workstation, domain);
}
@Override
public String generateType3Msg(
final String username,
final String password,
final String domain,
final String workstation,
final String challenge) throws NTLMEngineException {
final Type2Message t2m = new Type2Message(challenge);
return getType3Message(
username,
password,
workstation,
domain,
t2m.getChallenge(),
t2m.getFlags(),
t2m.getTarget(),
t2m.getTargetInfo());
}
}