// jTDS JDBC Driver for Microsoft SQL Server and Sybase // Copyright (C) 2004 The jTDS Project // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Lesser General Public // License as published by the Free Software Foundation; either // version 2.1 of the License, or (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA // package net.sourceforge.jtds.jdbc; import net.sourceforge.jtds.util.MD4Digest; import net.sourceforge.jtds.util.DESEngine; import net.sourceforge.jtds.util.MD5Digest; import java.io.UnsupportedEncodingException; import java.util.Arrays; /** * This class calculates the two "responses" to the nonce supplied by the server * as a part of NTLM authentication. * * Much gratitude to the authors of this page, esp. for NTLMv2 info: * http://davenport.sourceforge.net/ntlm.html * * @author Matt Brinkley * @version $Id: NtlmAuth.java,v 1.7 2006-06-23 18:00:56 matt_brinkley Exp $ */ public class NtlmAuth { //------------------------------------------------------------------------- // LM/NTLM - public interface //------------------------------------------------------------------------- public static byte[] answerNtChallenge(String password, byte[] nonce) throws UnsupportedEncodingException { return encryptNonce(ntHash(password), nonce); } public static byte[] answerLmChallenge(String pwd, byte[] nonce) throws UnsupportedEncodingException { byte[] password = convertPassword(pwd); DESEngine d1 = new DESEngine(true, makeDESkey(password, 0)); DESEngine d2 = new DESEngine(true, makeDESkey(password, 7)); byte[] encrypted = new byte[21]; Arrays.fill(encrypted, (byte)0); d1.processBlock(nonce, 0, encrypted, 0); d2.processBlock(nonce, 0, encrypted, 8); return encryptNonce(encrypted, nonce); } //------------------------------------------------------------------------- // LM/NTLM v2 - public interface //------------------------------------------------------------------------- public static byte[] answerNtlmv2Challenge( String domain, String user, String password, byte[] nonce, byte[] targetInfo, byte[] clientNonce) throws UnsupportedEncodingException { return answerNtlmv2Challenge( domain, user, password, nonce, targetInfo, clientNonce, System.currentTimeMillis()); } public static byte[] answerNtlmv2Challenge( String domain, String user, String password, byte[] nonce, byte[] targetInfo, byte[] clientNonce, byte[] timestamp) throws UnsupportedEncodingException { byte[] hash = ntv2Hash(domain, user, password); byte[] blob = createBlob(targetInfo, clientNonce, timestamp); return lmv2Response(hash,blob,nonce); } public static byte[] answerNtlmv2Challenge( String domain, String user, String password, byte[] nonce, byte[] targetInfo, byte[] clientNonce, long now) throws UnsupportedEncodingException { return answerNtlmv2Challenge( domain, user, password, nonce, targetInfo, clientNonce, createTimestamp(now)); } public static byte[] answerLmv2Challenge( String domain, String user, String password, byte[] nonce, byte[] clientNonce) throws UnsupportedEncodingException { byte[] hash = ntv2Hash(domain, user, password); return lmv2Response(hash, clientNonce, nonce); } //------------------------------------------------------------------------- // LMv2/NTLMv2 impl helpers //------------------------------------------------------------------------- private static byte[] ntv2Hash(String domain, String user, String password) throws UnsupportedEncodingException { byte[] hash = ntHash(password); String identity = user.toUpperCase() + domain.toUpperCase(); byte[] identityBytes = identity.getBytes("UnicodeLittleUnmarked"); return hmacMD5(identityBytes, hash); } /** * Creates the LMv2 Response from the given hash, client data, and * Type 2 challenge. * * @param hash The NTLMv2 Hash. * @param clientData The client data (blob or client challenge). * @param challenge The server challenge from the Type 2 message. * * @return The response (either NTLMv2 or LMv2, depending on the * client data). */ private static byte[] lmv2Response(byte[] hash, byte[] clientData, byte[] challenge) { byte[] data = new byte[challenge.length + clientData.length]; System.arraycopy(challenge, 0, data, 0, challenge.length); System.arraycopy(clientData, 0, data, challenge.length, clientData.length); byte[] mac = hmacMD5(data, hash); 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; } /** * Calculates the HMAC-MD5 hash of the given data using the specified * hashing key. * * @param data The data for which the hash will be calculated. * @param key The hashing key. * * @return The HMAC-MD5 hash of the given data. */ private static byte[] hmacMD5(byte[] data, byte[] key) { byte[] ipad = new byte[64]; byte[] opad = new byte[64]; for (int i = 0; i < 64; i++) { ipad[i] = (byte) 0x36; opad[i] = (byte) 0x5c; } for (int i = key.length - 1; i >= 0; i--) { ipad[i] ^= key[i]; opad[i] ^= key[i]; } byte[] content = new byte[data.length + 64]; System.arraycopy(ipad, 0, content, 0, 64); System.arraycopy(data, 0, content, 64, data.length); data = md5(content); content = new byte[data.length + 64]; System.arraycopy(opad, 0, content, 0, 64); System.arraycopy(data, 0, content, 64, data.length); return md5(content); } private static byte[] md5(byte[] data) { MD5Digest md5 = new MD5Digest(); md5.update(data, 0, data.length); byte[] hash = new byte[16]; md5.doFinal(hash, 0); return hash; } /** * Implementation of HMAC-MD5 that uses the JDK's crypto API * We don't use this because of JTDS's support of JDK 1.3. */ /* private static byte[] hmacMD5(byte[] data, byte[] key) throws NoSuchAlgorithmException, InvalidKeyException { SecretKey md5key = new SecretKeySpec( key, "HmacMD5" ); Mac mac = Mac.getInstance(md5key.getAlgorithm()); mac.init(md5key); return mac.doFinal(data); } */ /** * Creates a timestamp in the format used in NTLMv2 responses. * Public so it could be unit tested. * @param time current time, as returned from System.currentTimeMillis * @return little-endian byte array of number of tenths of microseconds since * Jan 1, 1601 */ public static byte[] createTimestamp(long time) { 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; } return timestamp; } /** * Creates the NTLMv2 blob from the given target information block and * client challenge. * * @param targetInformation The target information block from the Type 2 * message. * @param clientChallenge The random 8-byte client challenge. * * @return The blob, used in the calculation of the NTLMv2 Response. */ private static byte[] createBlob(byte[] targetInformation, byte[] clientChallenge, 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 }; byte[] unknown2 = new byte[] { (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00 }; byte[] blob = new byte[blobSignature.length + reserved.length + timestamp.length + clientChallenge.length + 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, clientChallenge.length); offset += clientChallenge.length; 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); return blob; } //------------------------------------------------------------------------- // LM/NTLM impl helpers //------------------------------------------------------------------------- private static byte[] encryptNonce(byte[] key, byte[] nonce) { byte[] out = new byte[24]; DESEngine d1 = new DESEngine(true, makeDESkey(key, 0)); DESEngine d2 = new DESEngine(true, makeDESkey(key, 7)); DESEngine d3 = new DESEngine(true, makeDESkey(key, 14)); d1.processBlock(nonce, 0, out, 0); d2.processBlock(nonce, 0, out, 8); d3.processBlock(nonce, 0, out, 16); return out; } /** * Creates the md4 hash of the unicode password. This is used as the DES * key when encrypting the nonce for NTLM challenge-response */ private static byte[] ntHash(String password) throws UnsupportedEncodingException { byte[] key = new byte[21]; Arrays.fill(key, (byte)0); byte[] pwd = password.getBytes("UnicodeLittleUnmarked"); // do the md4 hash of the unicode passphrase... MD4Digest md4 = new MD4Digest(); md4.update(pwd, 0, pwd.length); md4.doFinal(key, 0); return key; } /** * Used by answerNtlmChallenge. We need the password converted to caps, * narrowed and padded/truncated to 14 chars... */ private static byte[] convertPassword(String password) throws UnsupportedEncodingException { byte[] pwd = password.toUpperCase().getBytes("UTF8"); byte[] rtn = new byte[14]; Arrays.fill(rtn, (byte) 0); System.arraycopy( pwd, 0, // src rtn, 0, // dst pwd.length > 14 ? 14 : pwd.length); // length return rtn; } /** * Turns a 7-byte DES key into an 8-byte one by adding parity bits. All * implementations of DES seem to want an 8-byte key. */ private static byte[] makeDESkey(byte[] buf, int off) { byte[] ret = new byte[8]; ret[0] = (byte) ((buf[off+0] >> 1) & 0xff); ret[1] = (byte) ((((buf[off+0] & 0x01) << 6) | (((buf[off+1] & 0xff)>>2) & 0xff)) & 0xff); ret[2] = (byte) ((((buf[off+1] & 0x03) << 5) | (((buf[off+2] & 0xff)>>3) & 0xff)) & 0xff); ret[3] = (byte) ((((buf[off+2] & 0x07) << 4) | (((buf[off+3] & 0xff)>>4) & 0xff)) & 0xff); ret[4] = (byte) ((((buf[off+3] & 0x0F) << 3) | (((buf[off+4] & 0xff)>>5) & 0xff)) & 0xff); ret[5] = (byte) ((((buf[off+4] & 0x1F) << 2) | (((buf[off+5] & 0xff)>>6) & 0xff)) & 0xff); ret[6] = (byte) ((((buf[off+5] & 0x3F) << 1) | (((buf[off+6] & 0xff)>>7) & 0xff)) & 0xff); ret[7] = (byte) (buf[off+6] & 0x7F); for (int i = 0; i < 8; i++) { ret[i] = (byte) (ret[i] << 1); } return ret; } }