From 264d2038eaf5f1e91d0c7c030588c1fb7d65431d Mon Sep 17 00:00:00 2001 From: Oleg Kalnichevski Date: Wed, 5 Dec 2012 12:31:20 +0000 Subject: [PATCH] HTTPCLIENT-1266: NTLM engine refactoring and compatibility improvements. Contributed by Karl Wright git-svn-id: https://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk@1417384 13f79535-47bb-0310-9956-ffa450edef68 --- RELEASE_NOTES.txt | 3 + .../apache/http/impl/auth/NTLMEngineImpl.java | 578 +++++++++++++----- .../http/impl/auth/TestNTLMEngineImpl.java | 138 ++++- 3 files changed, 572 insertions(+), 147 deletions(-) diff --git a/RELEASE_NOTES.txt b/RELEASE_NOTES.txt index 8bc385a01..49b4ea774 100644 --- a/RELEASE_NOTES.txt +++ b/RELEASE_NOTES.txt @@ -1,6 +1,9 @@ Changes in trunk ------------------- +* [HTTPCLIENT-1266] NTLM engine refactoring and compatibility improvements. + Contributed by Karl Wright + * [HTTPCLIENT-1260] Javadoc incorrectly states StringBody(String)'s encoding uses the system default. Contributed by Tim and Gary Gregory diff --git a/httpclient/src/main/java/org/apache/http/impl/auth/NTLMEngineImpl.java b/httpclient/src/main/java/org/apache/http/impl/auth/NTLMEngineImpl.java index c0a4bd679..6f075a72f 100644 --- a/httpclient/src/main/java/org/apache/http/impl/auth/NTLMEngineImpl.java +++ b/httpclient/src/main/java/org/apache/http/impl/auth/NTLMEngineImpl.java @@ -32,6 +32,7 @@ import java.util.Arrays; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; +import javax.crypto.Mac; import org.apache.commons.codec.binary.Base64; import org.apache.http.util.EncodingUtils; @@ -44,16 +45,26 @@ import org.apache.http.util.EncodingUtils; */ 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; + // 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 + protected final static int FLAG_REQUEST_UNICODE_ENCODING = 0x00000001; // Unicode string encoding requested + protected final static int FLAG_REQUEST_TARGET = 0x00000004; // Requests target field + protected final static int FLAG_REQUEST_SIGN = 0x00000010; // Requests all messages have a signature attached, in NEGOTIATE message. + protected final static int FLAG_REQUEST_SEAL = 0x00000020; // Request key exchange for message confidentiality in NEGOTIATE message. MUST be used in conjunction with 56BIT. + protected final static int FLAG_REQUEST_LAN_MANAGER_KEY = 0x00000080; // Request Lan Manager key instead of user session key + protected final static int FLAG_REQUEST_NTLMv1 = 0x00000200; // Request NTLMv1 security. MUST be set in NEGOTIATE and CHALLENGE both + protected final static int FLAG_DOMAIN_PRESENT = 0x00001000; // Domain is present in message + protected final static int FLAG_WORKSTATION_PRESENT = 0x00002000; // Workstation is present in message + protected final static int FLAG_REQUEST_ALWAYS_SIGN = 0x00008000; // Requests a signature block on all messages. Overridden by REQUEST_SIGN and REQUEST_SEAL. + protected final static int FLAG_REQUEST_NTLM2_SESSION = 0x00080000; // From server in challenge, requesting NTLM2 session security + protected final static int FLAG_REQUEST_VERSION = 0x02000000; // Request protocol version + protected final static int FLAG_TARGETINFO_PRESENT = 0x00800000; // From server in challenge message, indicating targetinfo is present + protected final static int FLAG_REQUEST_128BIT_KEY_EXCH = 0x20000000; // Request explicit 128-bit key exchange + protected final static int FLAG_REQUEST_EXPLICIT_KEY_EXCH = 0x40000000; // Request explicit key exchange + protected final static int FLAG_REQUEST_56BIT_ENCRYPTION = 0x80000000; // Must be used in conjunction with SEAL + /** Secure random generator */ private static final java.security.SecureRandom RND_GEN; @@ -224,103 +235,285 @@ final class NTLMEngineImpl implements NTLMEngine { return rval; } - /** Calculate an NTLM2 challenge block */ - private static byte[] makeNTLM2RandomChallenge() throws NTLMEngineException { + /** Calculate a 16-byte secondary key */ + private static byte[] makeSecondaryKey() throws NTLMEngineException { if (RND_GEN == null) { throw new NTLMEngineException("Random generator not available"); } - byte[] rval = new byte[24]; + byte[] rval = new byte[16]; 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) + protected static class CipherGen { + + protected final String target; + protected final String user; + protected final String password; + protected final byte[] challenge; + protected final byte[] targetInformation; + + // Information we can generate but may be passed in (for testing) + protected byte[] clientChallenge; + 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[] 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; + + public CipherGen(String target, String user, String password, + byte[] challenge, byte[] targetInformation, + byte[] clientChallenge, byte[] secondaryKey, byte[] timestamp) { + this.target = target; + this.user = user; + this.password = password; + this.challenge = challenge; + this.targetInformation = targetInformation; + this.clientChallenge = clientChallenge; + this.secondaryKey = secondaryKey; + this.timestamp = timestamp; + } + + public CipherGen(String target, String user, String password, + byte[] challenge, byte[] targetInformation) { + this(target, user, password, challenge, targetInformation, null, null, null); + } + + /** Calculate and return client challenge */ + public byte[] getClientChallenge() throws NTLMEngineException { - byte[] lmHash = lmHash(password); - return lmResponse(lmHash, challenge); + if (clientChallenge == null) + clientChallenge = makeRandomChallenge(); + return clientChallenge; + } + + /** Calculate and return random secondary key */ + public byte[] getSecondaryKey() + throws NTLMEngineException { + if (secondaryKey == null) + secondaryKey = makeSecondaryKey(); + 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 NTLMv2 hash */ + public byte[] getNTLMv2Hash() + throws NTLMEngineException { + if (ntlmv2Hash == null) + ntlmv2Hash = ntlmv2Hash(target, user, password); + return ntlmv2Hash; + } + + /** Calculate a timestamp */ + public byte[] getTimestamp() { + if (timestamp == null) { + long time = System.currentTimeMillis(); + 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(getClientChallenge(), 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(getNTLMv2Hash(),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) { + byte[] clientChallenge = getClientChallenge(); + lm2SessionResponse = new byte[24]; + System.arraycopy(clientChallenge, 0, lm2SessionResponse, 0, clientChallenge.length); + Arrays.fill(lm2SessionResponse, clientChallenge.length, lm2SessionResponse.length, (byte) 0x00); + } + return lm2SessionResponse; + } + + /** Get LMUserSessionKey */ + public byte[] getLMUserSessionKey() + throws NTLMEngineException { + if (lmUserSessionKey == null) { + byte[] lmHash = getLMHash(); + lmUserSessionKey = new byte[16]; + System.arraycopy(lmHash, 0, lmUserSessionKey, 0, 8); + Arrays.fill(lmUserSessionKey, 8, 16, (byte) 0x00); + } + return lmUserSessionKey; + } + + /** Get NTLMUserSessionKey */ + public byte[] getNTLMUserSessionKey() + throws NTLMEngineException { + if (ntlmUserSessionKey == null) { + byte[] ntlmHash = getNTLMHash(); + MD4 md4 = new MD4(); + md4.update(ntlmHash); + ntlmUserSessionKey = md4.getOutput(); + } + return ntlmUserSessionKey; + } + + /** GetNTLMv2UserSessionKey */ + public byte[] getNTLMv2UserSessionKey() + throws NTLMEngineException { + if (ntlmv2UserSessionKey == null) { + byte[] ntlmv2Hash = getNTLMv2Hash(); + byte[] ntlmv2Blob = getNTLMv2Blob(); + byte[] temp = new byte[ntlmv2Blob.length + challenge.length]; + // "The challenge is concatenated with the blob" - check this (MHL) + System.arraycopy(challenge, 0, temp, 0, challenge.length); + System.arraycopy(ntlmv2Blob, 0, temp, challenge.length, ntlmv2Blob.length); + byte[] partial = hmacMD5(temp,ntlmv2Hash); + ntlmv2UserSessionKey = hmacMD5(partial,ntlmv2Hash); + } + return ntlmv2UserSessionKey; + } + + /** Get NTLM2SessionResponseUserSessionKey */ + public byte[] getNTLM2SessionResponseUserSessionKey() + throws NTLMEngineException { + if (ntlm2SessionResponseUserSessionKey == null) { + byte[] ntlmUserSessionKey = getNTLMUserSessionKey(); + byte[] ntlm2SessionResponseNonce = getLM2SessionResponse(); + 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,ntlmUserSessionKey); + } + return ntlm2SessionResponseUserSessionKey; + } + + /** Get LAN Manager session key */ + public byte[] getLanManagerSessionKey() + throws NTLMEngineException { + if (lanManagerSessionKey == null) { + byte[] lmHash = getLMHash(); + byte[] lmResponse = getLMResponse(); + try { + byte[] keyBytes = new byte[14]; + System.arraycopy(lmHash, 0, keyBytes, 0, lmHash.length); + Arrays.fill(keyBytes, lmHash.length, keyBytes.length, (byte)0xbd); + Key lowKey = createDESKey(keyBytes, 0); + Key highKey = createDESKey(keyBytes, 7); + byte[] truncatedResponse = new byte[8]; + System.arraycopy(lmResponse, 0, truncatedResponse, 0, truncatedResponse.length); + Cipher des = Cipher.getInstance("DES/ECB/NoPadding"); + des.init(Cipher.ENCRYPT_MODE, lowKey); + byte[] lowPart = des.doFinal(truncatedResponse); + des = Cipher.getInstance("DES/ECB/NoPadding"); + des.init(Cipher.ENCRYPT_MODE, highKey); + 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 (Exception e) { + throw new NTLMEngineException(e.getMessage(), e); + } + } + return lanManagerSessionKey; + } } - /** - * 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 HMAC-MD5 */ + static byte[] hmacMD5(byte[] value, byte[] key) + throws NTLMEngineException { + HMACMD5 hmacMD5 = new HMACMD5(key); + hmacMD5.update(value); + return hmacMD5.getOutput(); } - /** - * 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 RC4 */ + static byte[] RC4(byte[] value, byte[] key) + throws NTLMEngineException { + try { + Cipher rc4 = Cipher.getInstance("RC4"); + rc4.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "RC4")); + return rc4.doFinal(value); + } catch (Exception e) { + throw new NTLMEngineException(e.getMessage(), e); + } } /** @@ -338,11 +531,9 @@ final class NTLMEngineImpl implements NTLMEngine { * 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, + static byte[] ntlm2SessionResponse(byte[] ntlmHash, 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) @@ -521,21 +712,13 @@ final class NTLMEngineImpl implements NTLMEngine { * * @return The blob, used in the calculation of the NTLMv2 Response. */ - private static byte[] createBlob(byte[] clientChallenge, byte[] targetInformation) { + private static byte[] createBlob(byte[] clientChallenge, byte[] targetInformation, byte[] timestamp) { 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[] unknown2 = new byte[] { (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00 }; byte[] blob = new byte[blobSignature.length + reserved.length + timestamp.length + 8 - + unknown1.length + targetInformation.length]; + + unknown1.length + targetInformation.length + unknown2.length]; int offset = 0; System.arraycopy(blobSignature, 0, blob, offset, blobSignature.length); offset += blobSignature.length; @@ -548,6 +731,9 @@ final class NTLMEngineImpl implements NTLMEngine { 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; } @@ -780,40 +966,67 @@ final class NTLMEngineImpl implements NTLMEngine { String getResponse() { // Now, build the message. Calculate its length first, including // signature or type. - int finalLength = 32 + hostBytes.length + domainBytes.length; + int finalLength = 32 + 8 + 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); + addULong( + 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 | + FLAG_REQUEST_TARGET); // Domain length (two times). addUShort(domainBytes.length); addUShort(domainBytes.length); // Domain offset. - addULong(hostBytes.length + 32); + addULong(hostBytes.length + 32 + 8); // Host length (two times). addUShort(hostBytes.length); addUShort(hostBytes.length); - // Host offset (always 32). - addULong(32); + // Host offset (always 32 + 8). + addULong(32 + 8); - // Host String. + // Version + addUShort(0x0105); + // Build + addULong(2600); + // NTLM revision + addUShort(15); + + + // Host (workstation) String. addBytes(hostBytes); // Domain String. addBytes(domainBytes); + return super.getResponse(); } @@ -829,16 +1042,31 @@ final class NTLMEngineImpl implements NTLMEngine { Type2Message(String 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); - if ((flags & FLAG_UNICODE_ENCODING) == 0) + + if ((flags & FLAG_REQUEST_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 @@ -899,6 +1127,8 @@ final class NTLMEngineImpl implements NTLMEngine { protected byte[] lmResp; protected byte[] ntResp; + protected byte[] sessionKey; + /** Constructor. Pass the arguments we will need */ Type3Message(String domain, String host, String user, String password, byte[] nonce, @@ -912,38 +1142,62 @@ final class NTLMEngineImpl implements NTLMEngine { // Use only the base domain name! domain = convertDomain(domain); + // Create a cipher generator class + CipherGen gen = new CipherGen(target, user, password, nonce, targetInformation); + // Use the new code to calculate the responses, including v2 if that // seems warranted. + byte[] userSessionKey; try { - if (targetInformation != null && target != null) { - byte[] clientChallenge = makeRandomChallenge(); - ntResp = getNTLMv2Response(target, user, password, nonce, clientChallenge, - targetInformation); - lmResp = getLMv2Response(target, user, password, nonce, clientChallenge); + if (((type2Flags & FLAG_REQUEST_NTLM2_SESSION) == 0) && + ((type2Flags & FLAG_REQUEST_NTLMv1) == 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 { - if ((type2Flags & FLAG_NEGOTIATE_NTLM2) != 0) { + // NTLMv1 + if ((type2Flags & FLAG_REQUEST_NTLM2_SESSION) != 0) { // NTLM2 session stuff is requested - byte[] clientChallenge = makeNTLM2RandomChallenge(); - - ntResp = getNTLM2SessionResponse(password, nonce, clientChallenge); - lmResp = clientChallenge; - + ntResp = gen.getNTLM2SessionResponse(); + lmResp = gen.getLM2SessionResponse(); + if ((type2Flags & FLAG_REQUEST_LAN_MANAGER_KEY) != 0) + userSessionKey = gen.getLanManagerSessionKey(); + else + userSessionKey = gen.getNTLM2SessionResponseUserSessionKey(); // 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); + ntResp = gen.getNTLMResponse(); + lmResp = gen.getLMResponse(); + if ((type2Flags & FLAG_REQUEST_LAN_MANAGER_KEY) != 0) + userSessionKey = gen.getLanManagerSessionKey(); + else + userSessionKey = gen.getNTLMUserSessionKey(); } } } 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); + lmResp = gen.getLMResponse(); + if ((type2Flags & FLAG_REQUEST_LAN_MANAGER_KEY) != 0) + userSessionKey = gen.getLanManagerSessionKey(); + else + userSessionKey = gen.getLMUserSessionKey(); } + if ((type2Flags & FLAG_REQUEST_EXPLICIT_KEY_EXCH) != 0) + sessionKey = RC4(gen.getSecondaryKey(), userSessionKey); + else + sessionKey = null; + try { domainBytes = domain.toUpperCase().getBytes("UnicodeLittleUnmarked"); hostBytes = host.getBytes("UnicodeLittleUnmarked"); @@ -962,15 +1216,20 @@ final class NTLMEngineImpl implements NTLMEngine { int domainLen = domainBytes.length; int hostLen = hostBytes.length; int userLen = userBytes.length; + int sessionKeyLen; + if (sessionKey != null) + sessionKeyLen = sessionKey.length; + else + sessionKeyLen = 0; // Calculate the layout within the packet - int lmRespOffset = 64; + int lmRespOffset = 64 + 8; // allocate space for the version int ntRespOffset = lmRespOffset + lmRespLen; int domainOffset = ntRespOffset + ntRespLen; int userOffset = domainOffset + domainLen; int hostOffset = userOffset + userLen; int sessionKeyOffset = hostOffset + hostLen; - int finalLength = sessionKeyOffset + 0; + int finalLength = sessionKeyOffset + sessionKeyLen; // Start the response. Length includes signature and type prepareResponse(finalLength, 3); @@ -1010,19 +1269,50 @@ final class NTLMEngineImpl implements NTLMEngine { // Host offset addULong(hostOffset); - // 4 bytes of zeros - not sure what this is - addULong(0); + // Session key length (twice) + addUShort(sessionKeyLen); + addUShort(sessionKeyLen); + + // Session key offset + addULong(sessionKeyOffset); // Message length addULong(finalLength); - // Flags. Currently: NEGOTIATE_NTLM + UNICODE_ENCODING + + // Flags. Currently: WORKSTATION_PRESENT + DOMAIN_PRESENT + 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)); + 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) | + + FLAG_REQUEST_UNICODE_ENCODING | + FLAG_REQUEST_TARGET + ); + + // Version + addUShort(0x0105); + // Build + addULong(2600); + // NTLM revision + addUShort(15); // Add the actual data addBytes(lmResp); @@ -1030,6 +1320,8 @@ final class NTLMEngineImpl implements NTLMEngine { addBytes(domainBytes); addBytes(userBytes); addBytes(hostBytes); + if (sessionKey != null) + addBytes(sessionKey); return super.getResponse(); } diff --git a/httpclient/src/test/java/org/apache/http/impl/auth/TestNTLMEngineImpl.java b/httpclient/src/test/java/org/apache/http/impl/auth/TestNTLMEngineImpl.java index e8354f99f..d7fab2b39 100644 --- a/httpclient/src/test/java/org/apache/http/impl/auth/TestNTLMEngineImpl.java +++ b/httpclient/src/test/java/org/apache/http/impl/auth/TestNTLMEngineImpl.java @@ -27,6 +27,7 @@ package org.apache.http.impl.auth; import org.junit.Test; +import org.junit.Assert; public class TestNTLMEngineImpl { @@ -45,18 +46,18 @@ public class TestNTLMEngineImpl { } /* Test suite helper */ - static byte checkToNibble(char c) { + static byte toNibble(char c) { if (c >= 'a' && c <= 'f') return (byte) (c - 'a' + 0x0a); return (byte) (c - '0'); } /* Test suite helper */ - static byte[] checkToBytes(String hex) { + static byte[] toBytes(String hex) { byte[] rval = new byte[hex.length() / 2]; int i = 0; while (i < rval.length) { - rval[i] = (byte) ((checkToNibble(hex.charAt(i * 2)) << 4) | (checkToNibble(hex + rval[i] = (byte) ((toNibble(hex.charAt(i * 2)) << 4) | (toNibble(hex .charAt(i * 2 + 1)))); i++; } @@ -69,7 +70,7 @@ public class TestNTLMEngineImpl { md4 = new NTLMEngineImpl.MD4(); md4.update(input.getBytes("ASCII")); byte[] answer = md4.getOutput(); - byte[] correctAnswer = checkToBytes(hexOutput); + byte[] correctAnswer = toBytes(hexOutput); if (answer.length != correctAnswer.length) throw new Exception("Answer length disagrees for MD4('" + input + "')"); int i = 0; @@ -81,4 +82,133 @@ public class TestNTLMEngineImpl { } } + @Test + public void testLMResponse() throws Exception { + NTLMEngineImpl.CipherGen gen = new NTLMEngineImpl.CipherGen( + null, + null, + "SecREt01", + toBytes("0123456789abcdef"), + null, + null, + null, + null); + + checkArraysMatch(toBytes("c337cd5cbd44fc9782a667af6d427c6de67c20c2d3e77c56"), + gen.getLMResponse()); + } + + @Test + public void testNTLMResponse() throws Exception { + NTLMEngineImpl.CipherGen gen = new NTLMEngineImpl.CipherGen( + null, + null, + "SecREt01", + toBytes("0123456789abcdef"), + null, + null, + null, + null); + + checkArraysMatch(toBytes("25a98c1c31e81847466b29b2df4680f39958fb8c213a9cc6"), + gen.getNTLMResponse()); + } + + @Test + public void testLMv2Response() throws Exception { + NTLMEngineImpl.CipherGen gen = new NTLMEngineImpl.CipherGen( + "DOMAIN", + "user", + "SecREt01", + toBytes("0123456789abcdef"), + null, + toBytes("ffffff0011223344"), + null, + null); + + checkArraysMatch(toBytes("d6e6152ea25d03b7c6ba6629c2d6aaf0ffffff0011223344"), + gen.getLMv2Response()); + } + + @Test + public void testNTLMv2Response() throws Exception { + NTLMEngineImpl.CipherGen gen = new NTLMEngineImpl.CipherGen( + "DOMAIN", + "user", + "SecREt01", + toBytes("0123456789abcdef"), + toBytes("02000c0044004f004d00410049004e0001000c005300450052005600450052000400140064006f006d00610069006e002e0063006f006d00030022007300650072007600650072002e0064006f006d00610069006e002e0063006f006d0000000000"), + toBytes("ffffff0011223344"), + null, + toBytes("0090d336b734c301")); + + checkArraysMatch(toBytes("01010000000000000090d336b734c301ffffff00112233440000000002000c0044004f004d00410049004e0001000c005300450052005600450052000400140064006f006d00610069006e002e0063006f006d00030022007300650072007600650072002e0064006f006d00610069006e002e0063006f006d000000000000000000"), + gen.getNTLMv2Blob()); + checkArraysMatch(toBytes("cbabbca713eb795d04c97abc01ee498301010000000000000090d336b734c301ffffff00112233440000000002000c0044004f004d00410049004e0001000c005300450052005600450052000400140064006f006d00610069006e002e0063006f006d00030022007300650072007600650072002e0064006f006d00610069006e002e0063006f006d000000000000000000"), + gen.getNTLMv2Response()); + } + + @Test + public void testLM2SessionResponse() throws Exception { + NTLMEngineImpl.CipherGen gen = new NTLMEngineImpl.CipherGen( + "DOMAIN", + "user", + "SecREt01", + toBytes("0123456789abcdef"), + toBytes("02000c0044004f004d00410049004e0001000c005300450052005600450052000400140064006f006d00610069006e002e0063006f006d00030022007300650072007600650072002e0064006f006d00610069006e002e0063006f006d0000000000"), + toBytes("ffffff0011223344"), + null, + toBytes("0090d336b734c301")); + + checkArraysMatch(toBytes("ffffff001122334400000000000000000000000000000000"), + gen.getLM2SessionResponse()); + } + + @Test + public void testNTLM2SessionResponse() throws Exception { + NTLMEngineImpl.CipherGen gen = new NTLMEngineImpl.CipherGen( + "DOMAIN", + "user", + "SecREt01", + toBytes("0123456789abcdef"), + toBytes("02000c0044004f004d00410049004e0001000c005300450052005600450052000400140064006f006d00610069006e002e0063006f006d00030022007300650072007600650072002e0064006f006d00610069006e002e0063006f006d0000000000"), + toBytes("ffffff0011223344"), + null, + toBytes("0090d336b734c301")); + + checkArraysMatch(toBytes("10d550832d12b2ccb79d5ad1f4eed3df82aca4c3681dd455"), + gen.getNTLM2SessionResponse()); + } + + @Test + public void testNTLMUserSessionKey() throws Exception { + NTLMEngineImpl.CipherGen gen = new NTLMEngineImpl.CipherGen( + "DOMAIN", + "user", + "SecREt01", + toBytes("0123456789abcdef"), + toBytes("02000c0044004f004d00410049004e0001000c005300450052005600450052000400140064006f006d00610069006e002e0063006f006d00030022007300650072007600650072002e0064006f006d00610069006e002e0063006f006d0000000000"), + toBytes("ffffff0011223344"), + null, + toBytes("0090d336b734c301")); + + checkArraysMatch(toBytes("3f373ea8e4af954f14faa506f8eebdc4"), + gen.getNTLMUserSessionKey()); + } + + @Test + public void testRC4() throws Exception { + checkArraysMatch(toBytes("e37f97f2544f4d7e"), + NTLMEngineImpl.RC4(toBytes("0a003602317a759a"), + toBytes("2785f595293f3e2813439d73a223810d"))); + } + + /* Byte array check helper */ + static void checkArraysMatch(byte[] a1, byte[] a2) + throws Exception { + Assert.assertEquals(a1.length,a2.length); + for (int i = 0; i < a1.length; i++) { + Assert.assertEquals(a1[i],a2[i]); + } + } }