/*
* Copyright (c) 2016 Dell EMC Software
* All Rights Reserved
*/
package com.iwave.ext.windows.winrm.ntlm;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Provider;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.EndianUtils;
import org.apache.commons.lang3.CharEncoding;
import org.apache.http.Header;
import org.apache.http.HeaderElement;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.auth.AUTH;
import org.apache.http.message.BasicHeader;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A collection of useful methods and constants for use by the NTLM protocol.
*/
public final class NTLMUtils {
/**
* Bouncy castle provider, required for MD4.
*/
private static final Provider PROVIDER = new BouncyCastleProvider();
/** Constant for HMAC-MD5. */
private static final String HMAC_MD5_NAME = "HmacMD5";
/** Constant for the negotitate that needs to be prepended to the header. */
private static final String NEGOTIATE_WITH_SPACE = "Negotiate ";
/** The length of the negotiate message. */
private static final int NEGOTIATE_WITH_SPACE_LENGTH = NEGOTIATE_WITH_SPACE.length();
/** Default charset to use for NTLM communication. */
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
/**
* The logger for this class.
*/
private static final Logger LOG = LoggerFactory.getLogger(NTLMUtils.class);
/** Indicates that Unicode strings are supported for use in security buffer data. */
public static final int NEGOTIATE_UNICODE = 1;
/** Indicates that OEM strings are supported for use in security buffer data. */
public static final int NEGOTIATE_OEM = 1 << 1;
/** Requests that the server's authentication realm be included in the Type 2 message. */
public static final int REQUEST_TARGET = 1 << 2;
/** This flag's usage has not been identified. */
public static final int UNKNOWN_1 = 1 << 3;
/**
* Specifies that authenticated communication between the client and server should carry a digital signature (message
* integrity).
*/
public static final int NEGOTIATE_SIGN = 1 << 4;
/**
* Specifies that authenticated communication between the client and server should be encrypted (message
* confidentiality).
*/
public static final int NEGOTIATE_SEAL = 1 << 5;
/** Indicates that datagram authentication is being used. */
public static final int NEGOTIATE_DATAGRAM_STYLE = 1 << 6;
/** Indicates that the Lan Manager Session Key should be used for signing and sealing authenticated communications. */
public static final int NEGOTIATE_LAN_MANAGER_KEY = 1 << 7;
/** This flag's usage has not been identified. */
public static final int NEGOTIATE_NETWARE = 1 << 8;
/** Indicates that NTLM authentication is being used. */
public static final int NEGOTIATE_NTLM = 1 << 9;
/** This flag's usage has not been identified. */
public static final int UNKNOWN_2 = 1 << 10;
/**
* Sent by the client in the Type 3 message to indicate that an anonymous context has been established. This also affects
* the response fields (as detailed in the "Anonymous Response" section).
*/
public static final int NEGOTIATE_ANONYMOUS = 1 << 11;
/**
* Sent by the client in the Type 1 message to indicate that the name of the domain in which the client workstation has
* membership is included in the message. This is used by the server to determine whether the client is eligible for
* local authentication.
*/
public static final int NEGOTIATE_DOMAIN_SUPPLIED = 1 << 12;
/**
* Indicates that 56-bit encryption is supported. Supplied Sent by the client in the Type 1 message to indicate that the
* client workstation's name is included in the message. This is used by the server to determine whether the client is
* eligible for local authentication.
*/
public static final int NEGOTIATE_WORKSTATION_SUPPLIED = 1 << 13;
/**
* Sent by the server to indicate that the server and client are on the same machine. Implies that the client may use the
* established local credentials for authentication instead of calculating a response to the challenge.
*/
public static final int NEGOTIATE_LOCAL_CALL = 1 << 14;
/**
* Indicates that authenticated communication between the client and server should be signed with a "dummy" signature.
*/
public static final int NEGOTIATE_ALWAYS_SIGN = 1 << 15;
/** Sent by the server in the Type 2 message to indicate that the target authentication realm is a domain. */
public static final int TARGET_TYPE_DOMAIN = 1 << 16;
/** Sent by the server in the Type 2 message to indicate that the target authentication realm is a server. */
public static final int TARGET_TYPE_SERVER = 1 << 17;
/**
* Sent by the server in the Type 2 message to indicate that the target authentication realm is a share. Presumably, this
* is for share-level authentication. Usage is unclear.
*/
public static final int TARGET_TYPE_SHARE = 1 << 18;
/**
* Indicates that the NTLM2 signing and sealing scheme should be used for protecting authenticated communications. Note
* that this refers to a particular session security scheme, and is not related to the use of NTLMv2 authentication. This
* flag can, however, have an effect on the response calculations (as detailed in the "NTLM2 Session Response" section).
*/
public static final int NEGOTIATE_NTLM2_KEY = 1 << 19;
/** This flag's usage has not been identified. */
public static final int REQUEST_INIT_RESPONSE = 1 << 20;
/** This flag's usage has not been identified. */
public static final int REQUEST_ACCEPT_RESPONSE = 1 << 21;
/** This flag's usage has not been identified. */
public static final int REQUEST_NON_NT_SESSION_KEY = 1 << 22;
/**
* Sent by the server in the Type 2 message to indicate that it is including a Target Information block in the message.
* The Target Information block is used in the calculation of the NTLMv2 response.
*/
public static final int NEGOTIATE_TARGET_INFO = 1 << 23;
/** This flag's usage has not been identified. */
public static final int UNKNOWN_3 = 1 << 24;
/** This flag's usage has not been identified. */
public static final int UNKNOWN_4 = 1 << 25;
/** This flag's usage has not been identified. */
public static final int UNKNOWN_5 = 1 << 26;
/** This flag's usage has not been identified. */
public static final int UNKNOWN_6 = 1 << 27;
/** This flag's usage has not been identified. */
public static final int UNKNOWN_7 = 1 << 28;
/** Indicates that 128-bit encryption is supported. */
public static final int NEGOTIATE_128 = 1 << 29;
/** Indicates that the client will provide an encrypted master key in the "Session Key" field of the Type 3 message. */
public static final int NEGOTIATE_KEY_EXCHANGE = 1 << 30;
/** Indicates that 56-bit encryption is supported. */
public static final int NEGOTIATE_56 = 1 << 31;
/**
* Returns the contents of an NTLM security buffer. The buffer works in the following way.
*
* <pre>
* Description Content Example
* 0 Length 2 byte number 0xd204 (1234 bytes)
* 2 Allocated Space 2 byte number 0xd204 (1234 bytes)
* 4 Offset 4 byte number 0xe1100000 (4321 bytes)
*
* Length is the length of the content.
* Allocated Space is the total size of the content.
* Offset is the starting position of the content from the beginning of the message.
* </pre>
*
* @param start
* the starting position of the security buffer
* @param bytes
* the total message
* @return the contents of the security buffer
*/
public static byte[] getSecurityBuffer(int start, byte[] bytes) {
short length = getShort(start, bytes);
// Not really used, as it just defines the allocated size of the byte buffer... actually could probably cause a
// security issue if length > allocatedSpace
// short allocatedSpace = getShort(start + 2, bytes);
int offset = getInt(start + 4, bytes);
return Arrays.copyOfRange(bytes, offset, offset + length);
}
/**
* Gets a short out of the byte array.
*
* @param start
* the start of the short
* @param bytes
* the byte array
* @return the short
*/
public static short getShort(int start, byte[] bytes) {
return EndianUtils.readSwappedShort(bytes, start);
}
/**
* Gets an int out of the byte array.
*
* @param start
* the start of the int
* @param bytes
* the byte array
* @return the int
*/
public static int getInt(int start, byte[] bytes) {
return EndianUtils.readSwappedInteger(bytes, start);
}
/**
* Gets a long out of the byte array.
*
* @param start
* the start of the long
* @param bytes
* the byte array
* @return the long
*/
public static long getLong(int start, byte[] bytes) {
return EndianUtils.readSwappedLong(bytes, start);
}
/**
* Converts a long into a byte array.
*
* @param num
* the long to convert
* @return the byte array
*/
public static byte[] convertLong(long num) {
byte[] toReturn = new byte[8];
EndianUtils.writeSwappedLong(toReturn, 0, num);
return toReturn;
}
/**
* Converts an int into a byte array.
*
* @param num
* the int to convert
* @return the byte array
*/
public static byte[] convertInt(int num) {
byte[] toReturn = new byte[4];
EndianUtils.writeSwappedInteger(toReturn, 0, num);
return toReturn;
}
/**
* Converts a short into a byte array.
*
* @param num
* the short to convert
* @return the byte array
*/
public static byte[] convertShort(short num) {
byte[] toReturn = new byte[2];
EndianUtils.writeSwappedShort(toReturn, 0, num);
return toReturn;
}
/**
* Creates a security buffer using the given bytes as the content.
*
* @param length
* the length of the security buffer
* @param offset
* the offset of where the content will be in the created message
* @return the security buffer
*/
public static byte[] createSecurityBuffer(short length, int offset) {
byte[] toReturn = new byte[8];
EndianUtils.writeSwappedShort(toReturn, 0, length);
EndianUtils.writeSwappedShort(toReturn, 2, length);
EndianUtils.writeSwappedInteger(toReturn, 4, offset);
return toReturn;
}
/**
* Concatenates any number of byte arrays into a single byte array.
*
* @param args
* the arrays to concatenate
* @return the concatenated array
*/
public static byte[] concat(byte[]... args) {
int length = 0;
for (int i = 0; i < args.length; i++) {
byte[] bytes = args[i];
length += bytes.length;
}
byte[] data = new byte[length];
int offset = 0;
for (byte[] bytes : args) {
System.arraycopy(bytes, 0, data, offset, bytes.length);
offset += bytes.length;
}
return data;
}
/**
* Performs an rc4 operation on the provided input.
*
* @param input
* the input to cipher
* @param key
* the key to cipher with
* @return the ciphered bytes
* @throws Exception
* if something went wrong
*/
public static byte[] rc4(byte[] input, byte[] key) throws Exception {
final Cipher rc4 = Cipher.getInstance("RC4");
SecretKeySpec sKey = new SecretKeySpec(key, "RC4");
// The mode doesn't actually matter for RC4, as it's a xor operation regardless of whether it's encrypt or decrypt
rc4.init(Cipher.ENCRYPT_MODE, sKey);
return rc4.doFinal(input);
}
/**
* Calculates the HMAC-MD5 hash.
*
* @param key
* the key to use
* @param data
* the data to hash
* @return the hashed value
*/
public static byte[] calculateHmacMD5(byte[] key, byte[] data) {
Mac hmacMD5 = createHmacMD5(key);
hmacMD5.update(data);
return hmacMD5.doFinal();
}
/**
* Instantiates an HMAC-MD5 hash.
*
* @param key
* the key to instantiate with
* @return the HMAC
*/
private static Mac createHmacMD5(byte[] key) {
try {
// Create a MAC object using HMAC-MD5 and initialize with key
Mac hmacMD5 = Mac.getInstance(HMAC_MD5_NAME);
hmacMD5.init(new SecretKeySpec(key, HMAC_MD5_NAME));
return hmacMD5;
} catch (Exception e) {
throw new IllegalArgumentException("Invalid key", e);
}
}
/**
* Performs md4 hashing on the passed data.
*
* @param data
* the data to hash
* @return the hash
* @throws NoSuchAlgorithmException
* Thrown if MD4 cannot be found
*/
public static byte[] md4(byte[] data) throws NoSuchAlgorithmException {
// The md4 instance needs to be re-created every time because the digest method is not thread safe
MessageDigest md4 = MessageDigest.getInstance("MD4", PROVIDER);
return md4.digest(data);
}
/**
* Generate the key uses for signing and sealing.
*
* @param challenge
* the challenge extracted from the type 2 message
* @param nonce
* the nonce extracted from the type 3 message
* @param sessionkey
* the session key extracted from the type 3 message
* @param password
* the users passsword
* @throws Exception
* if something goes wrong
* @return the v1 key
*/
public static byte[] calculateV1Key(byte[] challenge, byte[] nonce, byte[] sessionkey, String password)
throws Exception {
byte[] ntlmHash = NTLMUtils.md4(password.getBytes(CharEncoding.UTF_16LE));
byte[] ntlmUserSessionKey = NTLMUtils.md4(ntlmHash);
byte[] ntlm2SessionResponseUserSessionKey = NTLMUtils.calculateHmacMD5(ntlmUserSessionKey,
NTLMUtils.concat(challenge, nonce));
return NTLMUtils.rc4(sessionkey, ntlm2SessionResponseUserSessionKey);
}
/**
* Performs md5 hasing on the passed data.
*
* @param data
* the data to hash
* @return the hash
*/
public static byte[] md5(byte[]... data) {
MessageDigest md5 = createMD5();
for (byte[] b : data) {
md5.update(b);
}
return md5.digest();
}
/**
* Creates an instance of the MD5 message digest.
*
* @return the created digest
*/
private static MessageDigest createMD5() {
try {
return MessageDigest.getInstance("MD5");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Calculates the ntlm2 session response. The procedure is taken from
* http://davenport.sourceforge.net/ntlm.html#theNtlm2SessionResponse.
*
* @param password
* the password to use
* @param challenge
* the server challenge
* @param nonce
* the client nonce
* @return the ntlm2 session response
* @throws Exception
* if something goes wrong
*/
public static byte[] getNTLM2SessionResponse(String password, byte[] challenge, byte[] nonce) throws Exception {
byte[] ntlmHash = NTLMUtils.md4(password.getBytes(CharEncoding.UTF_16LE));
byte[] sessionNonce = NTLMUtils.md5(challenge, nonce);
byte[] ntlm2SessionHash = new byte[8];
System.arraycopy(sessionNonce, 0, ntlm2SessionHash, 0, 8);
byte[] nullPaddedNtlmHash = new byte[21];
System.arraycopy(ntlmHash, 0, nullPaddedNtlmHash, 0, 16);
return encryptHashWithDES(ntlm2SessionHash, nullPaddedNtlmHash);
}
/**
* Encrypts the ntlm2 session hash with DES using the method described at
* http://davenport.sourceforge.net/ntlm.html#theNtlm2SessionResponse.
*
* @param ntlm2SessionHash
* the ntlm2 session hash to encrypt
* @param ntlmHash
* the 21 byte long ntlm hash
* @return the encrypted session hash
* @throws Exception
* if something goes wrong
*/
private static byte[] encryptHashWithDES(byte[] ntlm2SessionHash, byte[] ntlmHash) throws Exception {
byte[] toReturn = new byte[24];
for (int i = 0; i < 3; i++) {
byte[] key = new byte[7];
System.arraycopy(ntlmHash, i * 7, key, 0, 7);
key = adjustOddParityForDES(key);
DESKeySpec desKeySpec = new DESKeySpec(key);
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
SecretKey secretKey = keyFactory.generateSecret(desKeySpec);
Cipher desCipher = Cipher.getInstance("DES/ECB/PKCS5Padding");
desCipher.init(Cipher.ENCRYPT_MODE, secretKey);
System.arraycopy(desCipher.doFinal(ntlm2SessionHash), 0, toReturn, i * 8, 8);
}
return toReturn;
}
/**
* Adjusts the DES key to have odd parity according to the procedure described at
* http://davenport.sourceforge.net/ntlm.html#theLmResponse.
*
* @param key
* the key to adjust
* @return the adjusted key
*/
private static byte[] adjustOddParityForDES(byte[] key) {
final byte[] toReturn = new byte[8];
// We take out 7 (8-bit) byte array and convert it into an 8 (7-bit) byte array with the trailing bit being 0
toReturn[0] = key[0];
int len = toReturn.length;
for (int i = 1; i < len - 1; i++) {
toReturn[i] = (byte) (key[i - 1] << (len - i) | (key[i] & 0xff) >> i);
}
toReturn[7] = (byte) (key[6] << 1);
// We apply an odd parity to each byte of the byte array, changing the trailing 0 to a 1 if necesary to ensure an odd
// number of bits in the key
for (int i = 0; i < toReturn.length; i++) {
int parity = 0;
for (int j = 0; j < 8; j++) {
parity += (toReturn[i] >> j) % 2;
}
if (parity % 2 == 0 && toReturn[i] % 2 == 0) {
toReturn[i] |= 1;
}
}
return toReturn;
}
/**
* Builds a NTLMMessage from the header so that we can interract with it.
*
* @param header
* the header to read
* @return the NTLMMessage
*/
private static NTLMMessage parseNTLMHeader(Header header) {
if (header != null) {
try {
// Strip NEGOTIATE from the beginning of the message, and then use that to build the NTLMMessage
return NTLMMessage.parse(getRaw(header));
} catch (Exception e) {
LOG.error("Error extracting NTLM message from: " + header.getValue() + "Ignoring and proceeding to process it as a non-NTLM message");
LOG.error("-############-" + "\n" + e);
}
}
return null;
}
/**
* Retrieves the raw bytes from the header.
*
* @param header
* the header to retreive the bytes from
* @return the bytes of the header
*/
private static byte[] getRaw(Header header) {
return Base64.decodeBase64(header.getValue().substring(NEGOTIATE_WITH_SPACE_LENGTH));
}
/**
* Extracts an NTLM Message from an HttpRequest.
*
* @param request
* the request to extract the NTLM Message from
* @return the NTLM Message
*/
public static NTLMMessage getNTLMMessage(HttpRequest request) {
return parseNTLMHeader(request.getFirstHeader(AUTH.WWW_AUTH_RESP));
}
/**
* Extracts the byte array from the NTLM Message from an HttpRequest.
*
* @param request
* the request to extract the byte array from
* @return the byte array
*/
public static byte[] getRawNTLMMessage(HttpRequest request) {
return getRaw(request.getFirstHeader(AUTH.WWW_AUTH_RESP));
}
/**
* Extracts an NTLM Message from an HttpResponse.
*
* @param response
* the response to extract the NTLM Message from
* @return the NTLM Message
*/
public static NTLMMessage getNTLMMessage(HttpResponse response) {
return parseNTLMHeader(response.getFirstHeader(AUTH.WWW_AUTH));
}
/**
* Extracts the byte array from the NTLM Message from an HttpResponse.
*
* @param response
* the response to extract the byte array from
* @return the byte array
*/
public static byte[] getRawNTLMMessage(HttpResponse response) {
return getRaw(response.getFirstHeader(AUTH.WWW_AUTH));
}
/**
* Retrieves the index of a subarray in an array starting from a given point.
*
* @param subSequence
* the subarray to find
* @param sequence
* the total array
* @param index
* where in the total array to start looking
* @return the index of the subarray or -1 if not found
*/
public static int indexOf(byte[] subSequence, byte[] sequence, int index) {
if (subSequence.length == 0) {
return index;
}
for (int i = index; i < sequence.length - subSequence.length + 1; i++) {
boolean found = true;
for (int j = 0; j < subSequence.length; j++) {
if (subSequence[j] != sequence[i + j]) {
found = false;
break;
}
}
if (found) {
return i;
}
}
return -1;
}
/**
* Finds the index of a subarray in an array.
*
* @param subSequence
* the subarray to look for
* @param sequence
* the array to traverse
* @return the index of the subarray, or -1 if not found
*/
public static int indexOf(byte[] subSequence, byte[] sequence) {
return indexOf(subSequence, sequence, 0);
}
/**
* Splits a byte array on the specified subsequence.
*
* @param subSequence
* the subsequence to split on
* @param sequence
* the sequence to split
* @return a list of byte arrays
*/
public static List<byte[]> split(byte[] subSequence, byte[] sequence) {
List<byte[]> toReturn = new ArrayList<byte[]>();
int index = 0;
while (index < sequence.length) {
int nextIndex = indexOf(subSequence, sequence, index);
if (nextIndex == -1) {
// Just set the next index to the end of the array so that we copy all of the sequence
nextIndex = sequence.length;
}
toReturn.add(Arrays.copyOfRange(sequence, index, nextIndex));
index = nextIndex + subSequence.length;
}
return toReturn;
}
/**
* Take the NTLMMessage and turn it into a header.
*
* @param msg
* the NTLMMessag to turn into a header.
* @return the Header object
*/
public static Header buildNtlmHeader(NTLMMessage msg) {
return new BasicHeader(AUTH.WWW_AUTH_RESP, NEGOTIATE_WITH_SPACE + Base64.encodeBase64String(msg.toHeaderBytes()));
}
/**
* Retrieves the boundary from an HttpResponse.
*
* @param response
* the response to retrieve the boundary from.
* @return the boundary, or null if not found
*/
public static String getBoundary(HttpResponse response) {
Header[] headers = response.getHeaders(HttpHeaders.CONTENT_TYPE);
for (Header h : headers) {
for (HeaderElement he : h.getElements()) {
for (NameValuePair nvp : he.getParameters()) {
if (nvp.getName().equals(NTLMConstants.BOUNDARY)) {
return nvp.getValue();
}
}
}
}
return null;
}
/** Difference between microsoft time (1601-01-01) and epoch time (1970-01-01). */
private static final long EPOCH_TIME_MS_TIME_DIFF = 11644473600000L;
/**
* Retrieve a timestamp from a byte array that is in microsoft filetime format.
*
* @param bytes
* the bytes of the timestamp
* @return a long corresponding to the epoch time
*/
public static long fromMicrosoftTimestamp(byte[] bytes) {
long filetime = getLong(0, bytes);
long mstime = filetime / (1000 * 10);
long javatime = mstime - EPOCH_TIME_MS_TIME_DIFF;
return javatime;
}
/**
* Converts a timestamp to a byte array that is in microsoft filetime format.
*
* @param javatime
* the timestamp
* @return a byte array corresponding to the filetime of the timestamp
*/
public static byte[] toMicrosoftTimestamp(long javatime) {
final long mstime = javatime + EPOCH_TIME_MS_TIME_DIFF;
return convertLong(mstime * 1000 * 10);
}
/**
* Utility class.
*/
private NTLMUtils() {
}
}