// 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;
    }
}

