com.microsoft.sqlserver.jdbc.NTLMAuthentication Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of mssql-jdbc Show documentation
Show all versions of mssql-jdbc Show documentation
Microsoft JDBC Driver for SQL Server.
/*
* Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made
* available under the terms of the MIT License. See the LICENSE file in the project root for more information.
*/
package com.microsoft.sqlserver.jdbc;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.MessageFormat;
import java.time.Instant;
import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import mssql.security.provider.MD4;
/**
* Provides an implementation of NTLMv2 authentication
*
* @see MS-NLMP:NTLMAuthentication
* Protocol
*/
final class NTLMAuthentication extends SSPIAuthentication {
private final java.util.logging.Logger logger = java.util.logging.Logger
.getLogger("com.microsoft.sqlserver.jdbc.internals.NTLMAuthentication");
/**
* Section 2.2.1.1 NEGOTIATE_MESSAGE
*
* Section 2.2.1.2 CHALLENGE_MESSAGE
*
* Section 2.2.1.3 AUTHENCIATE_MESSAGE
*
* NTLM signature for all 3 messages - "NTLMSSP\0"
*/
private static final byte[] NTLM_HEADER_SIGNATURE = {0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x00};
// NTLM messages type
private static final int NTLM_MESSAGE_TYPE_NEGOTIATE = 0x00000001;
private static final int NTLM_MESSAGE_TYPE_CHALLENGE = 0x00000002;
private static final int NTLM_MESSAGE_TYPE_AUTHENTICATE = 0x00000003;
/**
* Section 2.2.2.7 NTLM v2: NTLMv2_CLIENT_CHALLENGE
*
*
* RespType current version(aka Responseversion) of the challenge response type. This field MUST be 0x01
* HiRespType max supported version (aka HiResponserversion) of the client response type.
*
*/
private static final byte[] NTLM_CLIENT_CHALLENGE_RESPONSE_TYPE = {0x01, 0x01};
/**
* Section 2.2.2.7 NTLM v2: NTLMv2_CLIENT_CHALLENGE
*
*
* Reserved1 SHOULD be 0x0000
* Reserved2 SHOULD be 0x00000000
* Reserved3 SHOULD be 0x00000000
*
*/
private static final byte[] NTLM_CLIENT_CHALLENGE_RESERVED1 = {0x00, 0x00};
private static final byte[] NTLM_CLIENT_CHALLENGE_RESERVED2 = {0x00, 0x00, 0x00, 0x00};
private static final byte[] NTLM_CLIENT_CHALLENGE_RESERVED3 = {0x00, 0x00, 0x00, 0x00};
/**
* Section 3.1.5.1.1 Client Initiates the NEGOTIATE_MESSAGE
*
* If NTLM v2 authentication is used and the CHALLENGE_MESSAGE TargetInfo field (section 2.2.1.2) has an
* MsvAvTimestamp present, the client SHOULD NOT send the LmChallengeResponse and SHOULD send Z(24) instead.
*
*
* Section 2.2.2.1 AV_PAIR
* MsvAvTimestamp is always sent in the CHALLENGE_MESSAGE.
* Section 7 Appendix B: Product Behavior
*
*
* MsvAvTimestamp AV_PAIR type is not supported in Windows NT, Windows 2000, Windows XP, and Windows Server 2003.
*
* This defined here for completeness as it's necessary to 0 the correct number of bytes in the message.
*/
private static final byte[] NTLM_LMCHALLENAGERESPONSE = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
/**
* Section 2.2.2.10 VERSION
*
* used for debugging purposes only and its value does not affect NTLM message processing
*
* This defined here for completeness as it's necessary to 0 the correct number of bytes in the message.
*
*/
private static final byte[] NTLMSSP_VERSION = new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
/**
* Section 2.2.5 NEGOTIATE
*
*
* NTLM negotiate flags
* NTLMSSP_NEGOTIATE_UNICODE A bit requests unicode character set encoding.
* NTLMSSP_REQUEST_TARGET C bit TargetName field of the CHALLENGE_MESSAGE.
* NTLMSSP_NEGOTIATE_OEM_DOMAIN_SUPPLIED K bit indicates whether the domain name is provided.
* NTLMSSP_NEGOTIATE_OEM_WORKSTATION_SUPPLIED L bit indicates whether the Workstation field is present.
* NTLMSSP_NEGOTIATE_TARGET_INFO S bit indicates whether the TargetInfo fields are populated.
* NTLMSSP_NEGOTIATE_ALWAYS_SIGN M bit requests the presence of a signature block on all messages.
* NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY P bit requests usage of the NTLM v2 session security.
*
* Note - This is not specified in spec but NTLMSSP_NEGOTIATE_ALWAYS_SIGN is required for server to verify MIC!!
* If not set, server ignores MIC field even tho MSVAVFLAGS is set!!
*
*/
private static final long NTLMSSP_NEGOTIATE_UNICODE = 0x00000001;
private static final long NTLMSSP_REQUEST_TARGET = 0x00000004;
private static final long NTLMSSP_NEGOTIATE_OEM_DOMAIN_SUPPLIED = 0x00001000;
private static final long NTLMSSP_NEGOTIATE_OEM_WORKSTATION_SUPPLIED = 0x00002000;
private static final long NTLMSSP_NEGOTIATE_TARGET_INFO = 0x00800000;
private static final long NTLMSSP_NEGOTIATE_ALWAYS_SIGN = 0x00008000;
private static final long NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY = 0x00080000;
/**
* Section 2.2.2.1 AV_PAIR
*
*
* AAvId (2bytes)
* A 16-bit unsigned integer that defines the information type in the Value field. The contents of this field MUST
* be a value from the following table. The corresponding Value field in this AV_PAIR MUST contain the information
* specified in the description of that AvId
* NTLM_AVID_MSVAVEOL indicates that this is the last AV_PAIR in the list
* NTLM_AVID_MSVAVNBCOMPUTERNAME the server's NetBIOS computer name in unicode
* NTLM_AVID_MSVAVNBDOMAINNAME the server's NetBIOS domain name in unicode
* NTLM_AVID_MSVAVDNSCOMPUTERNAME the FQDN of the computer in unicode
* NTLM_AVID_MSVAVDNSDOMAINNAME the FQDN of the domain in unicode
* NTLM_AVID_MSVAVDNSTREENAME the FQDN of the forest in unicode (not currently used)
* NTLM_AVID_MSVAVFLAGS indicates server or client configuration (0x00000002 indicates MIC provided)
* NTLM_AVID_MSVAVTIMESTAMP FILETIME structure that contains the server local time (ALWAYS present in CHALLENGE_MESSAGE
* NTLM_AVID_MSVAVSINGLEHOST Single Host Data structure (not currently used)
* NTLM_AVID_MSVAVTARGETNAME SPN of the target server in unicode (not currently used)
*
*/
private static final short NTLM_AVID_MSVAVEOL = 0x0000;
private static final short NTLM_AVID_MSVAVNBCOMPUTERNAME = 0x0001;
private static final short NTLM_AVID_MSVAVNBDOMAINNAME = 0x0002;
private static final short NTLM_AVID_MSVAVDNSCOMPUTERNAME = 0x0003;
private static final short NTLM_AVID_MSVAVDNSDOMAINNAME = 0x0004;
private static final short NTLM_AVID_MSVAVDNSTREENAME = 0x0005;
private static final short NTLM_AVID_MSVAVFLAGS = 0x0006; // value in NTLM_AVID_VALUE_MIC
private static final short NTLM_AVID_MSVAVTIMESTAMP = 0x0007;
private static final short NTLM_AVID_MSVAVSINGLEHOST = 0x0008;
private static final short NTLM_AVID_MSVAVTARGETNAME = 0x0009;
/**
* Section 2.2.2.1 AV_PAIR
*
* value of NTLM_AVID_MSVAVFLAGS that indicates a MIC is provided
*
* Section 3.1.5.1.2 Client Receives a CHALLENGE_MESSAGE from the Server
*
* If the CHALLENGE_MESSAGE TargetInfo field has an MsvAvTimestamp present, the client SHOULD provide a MIC
*/
private static final int NTLM_AVID_LENGTH = 2; // length of an AvId
private static final int NTLM_AVLEN_LENGTH = 2; // length of an AVLen
private static final int NTLM_AVFLAG_VALUE_MIC = 0x00000002; // indicates MIC is provided
private static final int NTLM_MIC_LENGTH = 16; // length of MIC field
private static final int NTLM_AVID_MSVAVFLAGS_LEN = 4; // length of MSVAVFLAG
/**
* Section 2.2.1.1 NEGOTIATE_MESSAGE
*
*
* offset to payload in Negotiate message
* 8 (signature) +
* 4 (message type) +
* 8 (domain name) +
* 8 (workstation) +
* 4 (negotiate flags) +
* 0 (version)
*
*/
private static final int NTLM_NEGOTIATE_PAYLOAD_OFFSET = 32;
/**
* Section 2.2.1.3 AUTHENTICATE_MESSAGE
*
*
* offsets in Authenticate message
* 8 (signature) +
* 4 (message type) +
* 8 (lmChallengeResp) +
* 8 (ntChallengeResp) +
* 8 (domain name) +
* 8 (user name) +
* 8 (workstation) +
* 8 (encrypted random session key) +
* 4 (negotiate flags) +
* 8 (version) +
* 16 (MIC)
*
*/
private static final int NTLM_AUTHENTICATE_PAYLOAD_OFFSET = 88;
// client challenge length
private static final int NTLM_CLIENT_NONCE_LENGTH = 8;
// server challenge length
private static final int NTLM_SERVER_CHALLENGE_LENGTH = 8;
// Windows Filetime timestamp length
private static final int NTLM_TIMESTAMP_LENGTH = 8;
// Windows epoch time difference from Unix epoch time in secs
private static final long WINDOWS_EPOCH_DIFF = 11644473600L;
/**
* NTLM Client Context
*/
private class NTLMContext {
// domain name to authenticate to
private final String domainName;
// unicode bytes of domain name
private final byte[] domainUbytes;
// user credentials
private final String upperUserName;
private final byte[] userNameUbytes;
private final byte[] passwordHash;
// workstation
private String workstation;
// unicode bytes of server SPN
private final byte[] spnUbytes;
// message authentication code
private Mac mac = null;
// negotiate flags from Challenge msg
private long negotiateFlags = 0x00000000;
// session key calculated from password
private byte[] sessionBaseKey = null;
// Windows FileTime timestamp - number of 100 nanosecond ticks since Windows Epoch time
private byte[] timestamp = null;
// target info field from Challenge msg
private byte[] targetInfo = null;
// server challenge from Challenge msg
private byte[] serverChallenge = new byte[NTLM_SERVER_CHALLENGE_LENGTH];
/**
* Section 3.1.5.1.2 Client Receives a CHALLENGE_MESSAGE from the Server
*
* Save messages for calculating MIC
*/
private byte[] negotiateMsg = null;
private byte[] challengeMsg = null;
/**
* Creates an NTLM client context
*
* @param con
* connection to SQL server
* @param domainName
* domain name to authentication in using NTLM
* @param userName
* user name
* @param passwordHash
* password hash
* @param workstation
* hostname of the workstation
* @throws SQLServerException
* if error occurs
*/
NTLMContext(final SQLServerConnection con, final String domainName, final String userName,
final byte[] passwordHash, final String workstation) throws SQLServerException {
this.domainName = domainName.toUpperCase();
this.domainUbytes = unicode(this.domainName);
this.userNameUbytes = null != userName ? unicode(userName) : null;
this.upperUserName = null != userName ? userName.toUpperCase() : null;
this.passwordHash = passwordHash;
this.workstation = workstation;
String spn = null != con ? getSpn(con) : null;
this.spnUbytes = null != spn ? unicode(spn) : null;
if (logger.isLoggable(Level.FINEST)) {
logger.finest(toString() + " SPN detected: " + spn);
}
try {
mac = Mac.getInstance("HmacMD5");
} catch (NoSuchAlgorithmException e) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_ntlmHmacMD5Error"));
Object[] msgArgs = {domainName, e.getMessage()};
throw new SQLServerException(form.format(msgArgs), e);
}
}
};
// Handle to NTLM context
private NTLMContext context = null;
/**
* Creates an instance of the NTLM authentication
*
* @param con
* connection to SQL server
* @param domainName
* domain name used for NTLM authentication
* @param userName
* domain user
* @param passwordHash
* password hash
* @param workstation
* hostname of the workstation
* @throws SQLServerException
* if error occurs
*/
NTLMAuthentication(final SQLServerConnection con, final String domainName, final String userName,
final byte[] passwordHash, final String workstation) throws SQLServerException {
if (null == context) {
this.context = new NTLMContext(con, domainName, userName, passwordHash, workstation);
}
}
/**
* Generates the NTLM client context
*
* @param inToken
* SSPI input blob
* @param done
* indicates processing is done
* @return NTLM client context
* @throws SQLServerException
* if error occurs
*/
@Override
byte[] generateClientContext(final byte[] inToken, final boolean[] done) throws SQLServerException {
return initializeSecurityContext(inToken, done);
}
/**
* Releases the NTLM client context
*
* @throws SQLServerException
* if error occurs
*/
@Override
void releaseClientContext() {
context = null;
}
/**
* Parses the Type 2 NTLM Challenge message from server
*
* Section 2.2.1.2 CHALLENGE_MESSAGE
*
* @param inToken
* SSPI input blob
* @throws SQLServerException
* if error occurs
*/
private void parseNtlmChallenge(final byte[] inToken) throws SQLServerException {
// get token buffer
ByteBuffer token = ByteBuffer.wrap(inToken).order(ByteOrder.LITTLE_ENDIAN);
// verify signature
byte[] signature = new byte[NTLM_HEADER_SIGNATURE.length];
token.get(signature);
if (!Arrays.equals(signature, NTLM_HEADER_SIGNATURE)) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_ntlmSignatureError"));
Object[] msgArgs = {signature};
throw new SQLServerException(form.format(msgArgs), null);
}
// verify message type
int messageType = token.getInt();
if (messageType != NTLM_MESSAGE_TYPE_CHALLENGE) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_ntlmMessageTypeError"));
Object[] msgArgs = {messageType};
throw new SQLServerException(form.format(msgArgs), null);
}
// target name fields
int targetNameLen = token.getShort();
token.getShort(); // targetNameMaxLen
token.getInt(); // targetNameOffset
// negotiate flags
token.getInt();
// get server challenge
token.get(context.serverChallenge);
// 8 bytes reserved 32-40: all 0's
token.getLong();
// target info fields
int targetInfoLen = token.getShort();
token.getShort(); // targetInfoMaxLen
token.getInt(); // targetInfoOffset
token.getLong(); // version - not used, for debug only
// get requested target name
byte[] targetName = new byte[targetNameLen];
token.get(targetName);
// verify targetInfo - was requested so should always be sent
context.targetInfo = new byte[targetInfoLen];
token.get(context.targetInfo);
if (0 == context.targetInfo.length) {
throw new SQLServerException(SQLServerException.getErrString("R_ntlmNoTargetInfo"), null);
}
// parse target info AV pairs
ByteBuffer targetInfoBuf = ByteBuffer.wrap(context.targetInfo).order(ByteOrder.LITTLE_ENDIAN);
boolean done = false;
for (int i = 0; i < context.targetInfo.length && !done;) {
int id = targetInfoBuf.getShort();
byte[] value = new byte[targetInfoBuf.getShort()];
targetInfoBuf.get(value);
switch (id) {
case NTLM_AVID_MSVAVTIMESTAMP:
if (value.length > 0) {
context.timestamp = new byte[NTLM_TIMESTAMP_LENGTH];
System.arraycopy(value, 0, context.timestamp, 0, NTLM_TIMESTAMP_LENGTH);
}
break;
case NTLM_AVID_MSVAVEOL:
done = true;
break;
case NTLM_AVID_MSVAVDNSDOMAINNAME:
case NTLM_AVID_MSVAVDNSCOMPUTERNAME:
case NTLM_AVID_MSVAVFLAGS:
case NTLM_AVID_MSVAVNBCOMPUTERNAME:
case NTLM_AVID_MSVAVNBDOMAINNAME:
case NTLM_AVID_MSVAVDNSTREENAME:
case NTLM_AVID_MSVAVSINGLEHOST:
case NTLM_AVID_MSVAVTARGETNAME:
break;
default:
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_ntlmUnknownValue"));
Object[] msgArgs = {value};
throw new SQLServerException(form.format(msgArgs), null);
}
if (logger.isLoggable(Level.FINEST)) {
logger.finest(toString() + " NTLM Challenge Message target info: AvId " + id);
}
}
/**
* Section 2.2.2.1 AV_PAIR
*
* Section 7 Appendix B Product Behavior
*
* This structure should always be sent in the CHALLENGE_MESSAGE not supported in Windows NT, Windows 2000,
* Windows XP, and Windows Server 2003
*/
if (null == context.timestamp || 0 >= context.timestamp.length) {
// this SHOULD always be present but for some reason occasionally this had seen to be missing
if (logger.isLoggable(Level.WARNING)) {
logger.warning(toString() + " NTLM Challenge Message target info error: Missing timestamp.");
}
} else {
// save msg for calculating MIC in Authenticate msg
context.challengeMsg = new byte[inToken.length];
System.arraycopy(inToken, 0, context.challengeMsg, 0, inToken.length);
}
}
/**
* Initializes the NTLM client security context
*
* @param inToken
* SSPI input blob
* @param done
* indicates processing is done
* @return outbound security context
* @throws SQLServerException
* if error occurs
*/
private byte[] initializeSecurityContext(final byte[] inToken, final boolean[] done) throws SQLServerException {
if (null == inToken || 0 == inToken.length) {
return generateNtlmNegotiate();
} else {
// get challenge msg from server
parseNtlmChallenge(inToken);
// get authenticate msg
done[0] = true;
return generateNtlmAuthenticate();
}
}
/**
* Generates NTLMv2 Client Challenge blob
*
*
* Section 2.2.2.7 NTLM v2: NTLMv2_CLIENT_CHALLENGE:
*
* clientChallenge blob consists of:
* Responserversion,
* HiResponserversion, NTLM_CLIENT_CHALLENGE_RESPONSE_TYPE,
* Z(6), NTLM_CLIENT_CHALLENGE_RESERVED1, NTLM_CLIENT_CHALLENGE_RESERVED2
* Time, current timestamp
* ClientChallenge, clientNonce
* Z(4), NTLM_CLIENT_CHALLENGE_RESERVED2
* ServerName, CHALLENGE_MESSAGE.TargetInfo (from 3.1.5.1.2 Client Receives a CHALLENGE_MESSAGE from the Server)
* Z(4)) EOL
*
*
* @param clientNonce
* client challenge nonce
* @return client challenge blob
*/
private byte[] generateClientChallengeBlob(final byte[] clientNonce) {
// timestamp is number of 100 nanosecond ticks since Windows Epoch
ByteBuffer time = ByteBuffer.allocate(NTLM_TIMESTAMP_LENGTH).order(ByteOrder.LITTLE_ENDIAN);
time.putLong((TimeUnit.SECONDS.toNanos(Instant.now().getEpochSecond() + WINDOWS_EPOCH_DIFF)) / 100);
byte[] currentTime = time.array();
// allocate token buffer
ByteBuffer token = ByteBuffer
.allocate(NTLM_CLIENT_CHALLENGE_RESPONSE_TYPE.length + NTLM_CLIENT_CHALLENGE_RESERVED1.length
+ NTLM_CLIENT_CHALLENGE_RESERVED2.length + currentTime.length + NTLM_CLIENT_NONCE_LENGTH
+ NTLM_CLIENT_CHALLENGE_RESERVED3.length + context.targetInfo.length
+ /* add MIC */ NTLM_AVID_LENGTH + NTLM_AVLEN_LENGTH + NTLM_AVID_MSVAVFLAGS_LEN
+ /* add SPN */ NTLM_AVID_LENGTH + NTLM_AVLEN_LENGTH + context.spnUbytes.length)
.order(ByteOrder.LITTLE_ENDIAN);
token.put(NTLM_CLIENT_CHALLENGE_RESPONSE_TYPE);
token.put(NTLM_CLIENT_CHALLENGE_RESERVED1);
token.put(NTLM_CLIENT_CHALLENGE_RESERVED2);
token.put(currentTime, 0, NTLM_TIMESTAMP_LENGTH);
token.put(clientNonce, 0, NTLM_CLIENT_NONCE_LENGTH);
token.put(NTLM_CLIENT_CHALLENGE_RESERVED3);
/**
* Section 3.1.5.1.2 Client Receives a CHALLENGE_MESSAGE from the Server
*
* If the CHALLENGE_MESSAGE TargetInfo field has an MsvAvTimestamp present, the client SHOULD provide a MIC
*/
if (null == context.timestamp || 0 >= context.timestamp.length) {
token.put(context.targetInfo, 0, context.targetInfo.length);
if (logger.isLoggable(Level.WARNING)) {
logger.warning(toString()
+ " MsvAvTimestamp not recieved from SQL Server in Challenge Message. MIC field will not be set.");
}
} else {
// copy targetInfo up to NTLM_AVID_MSVAVEOL
token.put(context.targetInfo, 0, context.targetInfo.length - NTLM_AVID_LENGTH - NTLM_AVLEN_LENGTH);
// MIC
token.putShort(NTLM_AVID_MSVAVFLAGS);
token.putShort((short) NTLM_AVID_MSVAVFLAGS_LEN);
token.putInt((int) NTLM_AVFLAG_VALUE_MIC);
}
// SPN
token.putShort(NTLM_AVID_MSVAVTARGETNAME);
token.putShort((short) context.spnUbytes.length);
token.put(context.spnUbytes, 0, context.spnUbytes.length);
// EOL
token.putShort(NTLM_AVID_MSVAVEOL);
token.putShort((short) 0);
return token.array();
}
/**
* Gets the HMAC MD5 hash
*
* @see https://www.ietf.org/rfc/rfc2104.txt
*
* @param key
* key used for hash
* @param data
* input data
* @return HMAC MD5 hash
* @throws InvalidKeyException
* if key is invalid
*/
private byte[] hmacMD5(final byte[] key, final byte[] data) throws InvalidKeyException {
SecretKeySpec keySpec = new SecretKeySpec(key, "HmacMD5");
context.mac.init(keySpec);
return context.mac.doFinal(data);
}
/**
* Gets the MD4 hash of input string
*
* @param str
* input string
* @return MD4 hash
*/
private static byte[] md4(final byte[] str) {
MD4 md = new MD4();
md.reset();
md.update(str, 0, str.length);
byte[] hash = new byte[md.getDigestSize()];
md.doFinal(hash, 0);
return hash;
}
/**
* Gets the unicode of a string
*
* @param str
* string to convert to unicode
* @return unicode of string
*/
private static byte[] unicode(final String str) {
return (null != str) ? str.getBytes(java.nio.charset.StandardCharsets.UTF_16LE) : null;
}
/**
* Concatenates 2 byte arrays
*
* @param arr1
* array 1
* @param arr2
* array 2
* @return concatenated array of arr1 and and arr2
*/
private byte[] concat(final byte[] arr1, final byte[] arr2) {
if (null == arr1 || null == arr2) {
return null;
}
byte[] temp = new byte[arr1.length + arr2.length];
System.arraycopy(arr1, 0, temp, 0, arr1.length);
System.arraycopy(arr2, 0, temp, arr1.length, arr2.length);
return temp;
}
/**
* Get length of byte array
*
* @param arr
* array to get length of
* @return array length or 0 if null array
*/
private int getByteArrayLength(byte[] arr) {
return null == arr ? 0 : arr.length;
}
/**
* Generates a key from password to get NTLMv2 hash Section 3.3.2 NTLM v2 Authentication
*
* @return NT response key
* @throws InvalidKeyException
* if error getting hash due to invalid key
*/
private byte[] ntowfv2() throws InvalidKeyException {
return hmacMD5(context.passwordHash,
(null != context.upperUserName) ? unicode(context.upperUserName + context.domainName)
: unicode(context.domainName));
}
/**
* Computes the NT response and keys from the Challenge message
*
* Section 3.3.2 NTLM v2 Authentication
*
*
* Set temp to ConcatenationOf(Responserversion, HiResponserversion,
* Z(6), Time, ClientChallenge, Z(4), ServerName, Z(4))
* Set NTProofStr to HMAC_MD5(ResponseKeyNT,
* ConcatenationOf(CHALLENGE_MESSAGE.ServerChallenge,temp))
* Set NtChallengeResponse to ConcatenationOf(NTProofStr, temp)
* Set LmChallengeResponse to ConcatenationOf(HMAC_MD5(ResponseKeyLM,
* ConcatenationOf(CHALLENGE_MESSAGE.ServerChallenge, ClientChallenge)),
* ClientChallenge )
*
*
* @param responseKeyNT
* NT response hash key
* @return computed response
* @throws InvalidKeyException
* if error getting hash due to invalid key
*/
private byte[] computeResponse(final byte[] responseKeyNT) throws InvalidKeyException {
// get random client challenge nonce
byte[] clientNonce = new byte[NTLM_CLIENT_NONCE_LENGTH];
ThreadLocalRandom.current().nextBytes(clientNonce);
// get client challenge blob
byte[] temp = generateClientChallengeBlob(clientNonce);
byte[] ntProofStr = hmacMD5(responseKeyNT, concat(context.serverChallenge, temp));
context.sessionBaseKey = hmacMD5(responseKeyNT, ntProofStr);
return concat(ntProofStr, temp);
}
/**
* Generates the NT Challenge response to server Challenge
*
* Section 3.3.2 NTLM v2 Authentication
*
* @return NT challenge response
* @throws InvalidKeyException
* if error getting hash due to invalid key
*/
private byte[] getNtChallengeResp() throws InvalidKeyException {
byte[] responseKeyNT = ntowfv2();
return computeResponse(responseKeyNT);
}
/**
* Generates the Type 3 NTLM Authentication message
*
* Section 2.2.1.3 AUTHENTICATE_MESSAGE
*
* @return NTLM Authenticate message
* @throws SQLServerException
* if error occurs
*/
private byte[] generateNtlmAuthenticate() throws SQLServerException {
int domainNameLen = getByteArrayLength(context.domainUbytes);
int userNameLen = getByteArrayLength(context.userNameUbytes);
byte[] workstationBytes = unicode(context.workstation);
int workstationLen = getByteArrayLength(workstationBytes);
byte[] msg = null;
try {
// get NT challenge response
byte[] ntChallengeResp = getNtChallengeResp();
int ntChallengeLen = getByteArrayLength(ntChallengeResp);
// allocate token buffer
ByteBuffer token = ByteBuffer.allocate(NTLM_AUTHENTICATE_PAYLOAD_OFFSET + NTLM_LMCHALLENAGERESPONSE.length
+ ntChallengeLen + domainNameLen + userNameLen + workstationLen).order(ByteOrder.LITTLE_ENDIAN);
// set NTLM signature and message type
token.put(NTLM_HEADER_SIGNATURE, 0, NTLM_HEADER_SIGNATURE.length);
token.putInt(NTLM_MESSAGE_TYPE_AUTHENTICATE);
// start of payload data
int offset = NTLM_AUTHENTICATE_PAYLOAD_OFFSET;
// LM challenge response
token.putShort((short) 0);
token.putShort((short) 0);
token.putInt(offset);
offset += NTLM_LMCHALLENAGERESPONSE.length;
// NT challenge response
token.putShort((short) ntChallengeLen);
token.putShort((short) ntChallengeLen);
token.putInt(offset);
offset += ntChallengeLen;
// domain name fields
token.putShort((short) domainNameLen);
token.putShort((short) domainNameLen);
token.putInt(offset);
offset += domainNameLen;
// user name fields
token.putShort((short) userNameLen);
token.putShort((short) userNameLen);
token.putInt(offset);
offset += userNameLen;
// workstation fields
token.putShort((short) workstationLen);
token.putShort((short) workstationLen);
token.putInt(offset);
offset += workstationLen;
// not used = encrypted random session key fields
token.putShort((short) 0);
token.putShort((short) 0);
token.putInt(offset);
// same negotiate flags sent in negotiate message
token.putInt((int) context.negotiateFlags);
// version - not used but need to send blank to separate from MIC otherwise server confuses this with MIC
token.put(NTLMSSP_VERSION, 0, NTLMSSP_VERSION.length);
// 0 the MIC field first for calculation
byte[] mic = new byte[NTLM_MIC_LENGTH];
int micPosition = token.position(); // save position
token.put(mic, 0, NTLM_MIC_LENGTH);
// payload data
token.put(NTLM_LMCHALLENAGERESPONSE, 0, NTLM_LMCHALLENAGERESPONSE.length);
token.put(ntChallengeResp, 0, ntChallengeLen);
token.put(context.domainUbytes, 0, domainNameLen);
token.put(context.userNameUbytes, 0, userNameLen);
token.put(workstationBytes, 0, workstationLen);
msg = token.array();
/**
* Section 3.1.5.1.2 Client Receives a CHALLENGE_MESSAGE from the Server
*
* MIC is calculated by using a 0 MIC then hmacMD5 of session key and concat of the 3 msgs
*/
if (null != context.timestamp && 0 < context.timestamp.length) {
SecretKeySpec keySpec = new SecretKeySpec(context.sessionBaseKey, "HmacMD5");
context.mac.init(keySpec);
context.mac.update(context.negotiateMsg);
context.mac.update(context.challengeMsg);
mic = context.mac.doFinal(msg);
// put calculated MIC into Authenticate msg
System.arraycopy(mic, 0, msg, micPosition, NTLM_MIC_LENGTH);
}
} catch (InvalidKeyException e) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_ntlmAuthenticateError"));
Object[] msgArgs = {e.getMessage()};
throw new SQLServerException(form.format(msgArgs), e);
}
return msg;
}
/**
* Generates the Type 1 NTLM Negotiate message
*
* Section 2.2.1.1 NEGOTIATE_MESSAGE
*
* @return NTLM Negotiate message
*/
private byte[] generateNtlmNegotiate() {
int domainNameLen = getByteArrayLength(context.domainUbytes);
int workstationLen = getByteArrayLength(context.workstation.getBytes());
ByteBuffer token = null;
token = ByteBuffer.allocate(NTLM_NEGOTIATE_PAYLOAD_OFFSET + domainNameLen + workstationLen)
.order(ByteOrder.LITTLE_ENDIAN);
// signature and message type
token.put(NTLM_HEADER_SIGNATURE, 0, NTLM_HEADER_SIGNATURE.length);
token.putInt(NTLM_MESSAGE_TYPE_NEGOTIATE);
// NTLM negotiate flags - only NTLMV2 supported
context.negotiateFlags = NTLMSSP_NEGOTIATE_OEM_WORKSTATION_SUPPLIED | NTLMSSP_NEGOTIATE_OEM_DOMAIN_SUPPLIED
| NTLMSSP_REQUEST_TARGET | NTLMSSP_NEGOTIATE_TARGET_INFO | NTLMSSP_NEGOTIATE_UNICODE
| NTLMSSP_NEGOTIATE_ALWAYS_SIGN | NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY;
token.putInt((int) context.negotiateFlags);
int offset = NTLM_NEGOTIATE_PAYLOAD_OFFSET;
// domain name fields
token.putShort((short) domainNameLen);
token.putShort((short) domainNameLen);
token.putInt(offset);
offset += domainNameLen;
// workstation field
token.putShort((short) workstationLen);
token.putShort((short) workstationLen);
token.putInt(offset);
offset += workstationLen;
// version - not used, for debug only
// payload
token.put(context.domainUbytes, 0, domainNameLen);
token.put(context.workstation.getBytes(), 0, workstationLen);
// save msg for calculating MIC in Authenticate msg
byte[] msg = token.array();
context.negotiateMsg = new byte[msg.length];
System.arraycopy(msg, 0, context.negotiateMsg, 0, msg.length);
return msg;
}
public static byte[] getNtlmPasswordHash(String password) throws SQLServerException {
if (null == password) {
throw new SQLServerException(SQLServerException.getErrString("R_NtlmNoUserPasswordDomain"), null);
}
return md4(unicode(password));
}
}