/* jcifs smb client library in Java
 * Copyright (C) 2002  "Michael B. Allen" <jcifs at samba dot org>
 *                  "Eric Glass" <jcifs at samba dot org>
 *
 * 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 jcifs.smb;

import java.io.UnsupportedEncodingException;
import java.io.Serializable;
import java.security.Principal;
import java.security.MessageDigest;
import java.security.GeneralSecurityException;
import java.util.Random;
import java.util.Arrays;
import jcifs.Config;
import jcifs.util.*;

/**
 * This class stores and encrypts NTLM user credentials. The default
 * credentials are retrieved from the <tt>jcifs.smb.client.domain</tt>,
 * <tt>jcifs.smb.client.username</tt>, and <tt>jcifs.smb.client.password</tt>
 * properties.
 * <p>
 * Read <a href="../../../authhandler.html">jCIFS Exceptions and
 * NtlmAuthenticator</a> for related information.
 */

public final class NtlmPasswordAuthentication implements Principal, Serializable {

    private static final int LM_COMPATIBILITY =
            Config.getInt("jcifs.smb.lmCompatibility", 3);

    private static final Random RANDOM = new Random();

    private static LogStream log = LogStream.getInstance();

    // KGS!@#$%
    private static final byte[] S8 = {
        (byte)0x4b, (byte)0x47, (byte)0x53, (byte)0x21,
        (byte)0x40, (byte)0x23, (byte)0x24, (byte)0x25
    };
    /* Accepts key multiple of 7
     * Returns enc multiple of 8
     * Multiple is the same like: 21 byte key gives 24 byte result
     */
    private static void E( byte[] key, byte[] data, byte[] e ) {
        byte[] key7 = new byte[7];
        byte[] e8 = new byte[8];

        for( int i = 0; i < key.length / 7; i++ ) {
            System.arraycopy( key, i * 7, key7, 0, 7 );
            DES des = new DES( key7 );
            des.encrypt( data, e8 );
            System.arraycopy( e8, 0, e, i * 8, 8 );
        }
    }

    static String DEFAULT_DOMAIN;
    static String DEFAULT_USERNAME;
    static String DEFAULT_PASSWORD;
    static final String BLANK = "";

    public static final NtlmPasswordAuthentication ANONYMOUS = new NtlmPasswordAuthentication("", "", "");

    static void initDefaults() {
        if (DEFAULT_DOMAIN != null) return;
        DEFAULT_DOMAIN = Config.getProperty("jcifs.smb.client.domain", "?");
        DEFAULT_USERNAME = Config.getProperty("jcifs.smb.client.username", "GUEST");
        DEFAULT_PASSWORD = Config.getProperty("jcifs.smb.client.password", BLANK);
    }

/**
 * Generate the ANSI DES hash for the password associated with these credentials.
 */
    static public byte[] getPreNTLMResponse( String password, byte[] challenge ) {
        byte[] p14 = new byte[14];
        byte[] p21 = new byte[21];
        byte[] p24 = new byte[24];
        byte[] passwordBytes;
        try {
            passwordBytes = password.toUpperCase().getBytes( ServerMessageBlock.OEM_ENCODING );
        } catch( UnsupportedEncodingException uee ) {
            throw new RuntimeException("Try setting jcifs.encoding=US-ASCII", uee);
        }
        int passwordLength = passwordBytes.length;

        // Only encrypt the first 14 bytes of the password for Pre 0.12 NT LM
        if( passwordLength > 14) {
            passwordLength = 14;
        }
        System.arraycopy( passwordBytes, 0, p14, 0, passwordLength );
        E( p14, S8, p21);
        E( p21, challenge, p24);
        return p24;
    }
/**
 * Generate the Unicode MD4 hash for the password associated with these credentials.
 */
    static public byte[] getNTLMResponse( String password, byte[] challenge ) {
        byte[] uni = null;
        byte[] p21 = new byte[21];
        byte[] p24 = new byte[24];

        try {
            uni = password.getBytes( SmbConstants.UNI_ENCODING );
        } catch( UnsupportedEncodingException uee ) {
            if( log.level > 0 )
                uee.printStackTrace( log );
        }
        MD4 md4 = new MD4();
        md4.update( uni );
        try {
            md4.digest(p21, 0, 16);
        } catch (Exception ex) {
            if( log.level > 0 )
                ex.printStackTrace( log );
        }
        E( p21, challenge, p24 );
        return p24;
    }

    /**
     * Creates the LMv2 response for the supplied information.
     *
     * @param domain The domain in which the username exists.
     * @param user The username.
     * @param password The user's password.
     * @param challenge The server challenge.
     * @param clientChallenge The client challenge (nonce). 
     */ 
    public static byte[] getLMv2Response(String domain, String user,
            String password, byte[] challenge, byte[] clientChallenge) {
        try {
            byte[] hash = new byte[16];
            byte[] response = new byte[24];
// The next 2-1/2 lines of this should be placed with nTOWFv1 in place of password
            MD4 md4 = new MD4();
            md4.update(password.getBytes(SmbConstants.UNI_ENCODING));
            HMACT64 hmac = new HMACT64(md4.digest());
            hmac.update(user.toUpperCase().getBytes(SmbConstants.UNI_ENCODING));
            hmac.update(domain.toUpperCase().getBytes(SmbConstants.UNI_ENCODING));
            hmac = new HMACT64(hmac.digest());
            hmac.update(challenge);
            hmac.update(clientChallenge);
            hmac.digest(response, 0, 16);
            System.arraycopy(clientChallenge, 0, response, 16, 8);
            return response;
        } catch (Exception ex) {
            if( log.level > 0 )
                ex.printStackTrace( log );
            return null;
        }
    }
    public static byte[] getNTLM2Response(byte[] nTOWFv1,
                    byte[] serverChallenge,
                    byte[] clientChallenge)
    {
        byte[] sessionHash = new byte[8];

        try {
            MessageDigest md5;
            md5 = MessageDigest.getInstance("MD5");
            md5.update(serverChallenge);
            md5.update(clientChallenge, 0, 8);
            System.arraycopy(md5.digest(), 0, sessionHash, 0, 8);
        } catch (GeneralSecurityException gse) {
            if (log.level > 0)
                gse.printStackTrace(log);
            throw new RuntimeException("MD5", gse);
        }

        byte[] key = new byte[21];
        System.arraycopy(nTOWFv1, 0, key, 0, 16);
        byte[] ntResponse = new byte[24];
        E(key, sessionHash, ntResponse);

        return ntResponse;
    }
    public static byte[] nTOWFv1(String password)
    {
        if (password == null)
            throw new RuntimeException("Password parameter is required");
        try {
            MD4 md4 = new MD4();
            md4.update(password.getBytes(SmbConstants.UNI_ENCODING));
            return md4.digest();
        } catch (UnsupportedEncodingException uee) {
            throw new RuntimeException(uee.getMessage());
        }
    }
    public static byte[] nTOWFv2(String domain, String username, String password)
    {
        try {
            MD4 md4 = new MD4();
            md4.update(password.getBytes(SmbConstants.UNI_ENCODING));
            HMACT64 hmac = new HMACT64(md4.digest());
            hmac.update(username.toUpperCase().getBytes(SmbConstants.UNI_ENCODING));
            hmac.update(domain.getBytes(SmbConstants.UNI_ENCODING));
            return hmac.digest();
        } catch (UnsupportedEncodingException uee) {
            throw new RuntimeException(uee.getMessage());
        }
    }
    static byte[] computeResponse(byte[] responseKey,
                byte[] serverChallenge,
                byte[] clientData,
                int offset,
                int length)
    {
        HMACT64 hmac = new HMACT64(responseKey);
        hmac.update(serverChallenge);
        hmac.update(clientData, offset, length);
        byte[] mac = hmac.digest();
        byte[] ret = new byte[mac.length + clientData.length];
        System.arraycopy(mac, 0, ret, 0, mac.length);
        System.arraycopy(clientData, 0, ret, mac.length, clientData.length);
        return ret;
    }
    public static byte[] getLMv2Response(
            byte[] responseKeyLM,
            byte[] serverChallenge,
            byte[] clientChallenge)
    {
        return NtlmPasswordAuthentication.computeResponse(responseKeyLM,
                    serverChallenge,
                    clientChallenge,
                    0,
                    clientChallenge.length);
    }
    public static byte[] getNTLMv2Response(
            byte[] responseKeyNT,
            byte[] serverChallenge,
            byte[] clientChallenge,
            long nanos1601,
            byte[] targetInfo)
    {
        int targetInfoLength = targetInfo != null ? targetInfo.length : 0;
        byte[] temp = new byte[28 + targetInfoLength + 4];

        Encdec.enc_uint32le(0x00000101, temp, 0); // Header
        Encdec.enc_uint32le(0x00000000, temp, 4); // Reserved
        Encdec.enc_uint64le(nanos1601, temp, 8);
        System.arraycopy(clientChallenge, 0, temp, 16, 8);
        Encdec.enc_uint32le(0x00000000, temp, 24); // Unknown
        if (targetInfo != null)
            System.arraycopy(targetInfo, 0, temp, 28, targetInfoLength);
        Encdec.enc_uint32le(0x00000000, temp, 28 + targetInfoLength); // mystery bytes!

        return NtlmPasswordAuthentication.computeResponse(responseKeyNT,
                        serverChallenge,
                        temp,
                        0,
                        temp.length);
    }

    static final NtlmPasswordAuthentication NULL =
                new NtlmPasswordAuthentication( "", "", "" );
    static final NtlmPasswordAuthentication GUEST =
                new NtlmPasswordAuthentication( "?", "GUEST", "" );
    static final NtlmPasswordAuthentication DEFAULT =
                new NtlmPasswordAuthentication( null );

    String domain;
    String username;
    String password;
    byte[] ansiHash;
    byte[] unicodeHash;
    boolean hashesExternal = false;
    byte[] clientChallenge = null;
    byte[] challenge = null;

/**
 * Create an <tt>NtlmPasswordAuthentication</tt> object from the userinfo
 * component of an SMB URL like "<tt>domain;user:pass</tt>". This constructor
 * is used internally be jCIFS when parsing SMB URLs.
 */

    public NtlmPasswordAuthentication( String userInfo ) {
        domain = username = password = null;

        if( userInfo != null ) {
            try {
                userInfo = unescape( userInfo );
            } catch( UnsupportedEncodingException uee ) {
            }
            int i, u, end;
            char c;

            end = userInfo.length();
            for( i = 0, u = 0; i < end; i++ ) {
                c = userInfo.charAt( i );
                if( c == ';' ) {
                    domain = userInfo.substring( 0, i );
                    u = i + 1;
                } else if( c == ':' ) {
                    password = userInfo.substring( i + 1 );
                    break;
                }
            }
            username = userInfo.substring( u, i );
        }

        initDefaults();

        if( domain == null ) this.domain = DEFAULT_DOMAIN;
        if( username == null ) this.username = DEFAULT_USERNAME;
        if( password == null ) this.password = DEFAULT_PASSWORD;
    }
/**
 * Create an <tt>NtlmPasswordAuthentication</tt> object from a
 * domain, username, and password. Parameters that are <tt>null</tt>
 * will be substituted with <tt>jcifs.smb.client.domain</tt>,
 * <tt>jcifs.smb.client.username</tt>, <tt>jcifs.smb.client.password</tt>
 * property values.
 */
    public NtlmPasswordAuthentication( String domain, String username, String password ) {
        int ci;

        if (username != null) {
        ci = username.indexOf('@');
        if (ci > 0) {
            domain = username.substring(ci + 1);
            username = username.substring(0, ci);
        } else {
            ci = username.indexOf('\\');
            if (ci > 0) {
                domain = username.substring(0, ci);
                username = username.substring(ci + 1);
            }
        }
        }

        this.domain = domain;
        this.username = username;
        this.password = password;

        initDefaults();

        if( domain == null ) this.domain = DEFAULT_DOMAIN;
        if( username == null ) this.username = DEFAULT_USERNAME;
        if( password == null ) this.password = DEFAULT_PASSWORD;
    }
/**
 * Create an <tt>NtlmPasswordAuthentication</tt> object with raw password
 * hashes. This is used exclusively by the <tt>jcifs.http.NtlmSsp</tt>
 * class which is in turn used by NTLM HTTP authentication functionality.
 */
    public NtlmPasswordAuthentication( String domain, String username,
                    byte[] challenge, byte[] ansiHash, byte[] unicodeHash ) {
        if( domain == null || username == null ||
                                    ansiHash == null || unicodeHash == null ) {
            throw new IllegalArgumentException( "External credentials cannot be null" );
        }
        this.domain = domain;
        this.username = username;
        this.password = null;
        this.challenge = challenge;
        this.ansiHash = ansiHash;
        this.unicodeHash = unicodeHash;
        hashesExternal = true;
    }

/**
 * Returns the domain.
 */
    public String getDomain() {
        return domain;
    }

/**
 * Returns the username.
 */
    public String getUsername() {
        return username;
    }
/**
 * Returns the password in plain text or <tt>null</tt> if the raw password
 * hashes were used to construct this <tt>NtlmPasswordAuthentication</tt>
 * object which will be the case when NTLM HTTP Authentication is
 * used. There is no way to retrieve a users password in plain text unless
 * it is supplied by the user at runtime.
 */
    public String getPassword() {
        return password;
    }
/**
 * Return the domain and username in the format:
 * <tt>domain\\username</tt>. This is equivalent to <tt>toString()</tt>.
 */
    public String getName() {
        boolean d = domain.length() > 0 && domain.equals( "?" ) == false;
        return d ? domain + "\\" + username : username;
    }

/**
 * Computes the 24 byte ANSI password hash given the 8 byte server challenge.
 */
    public byte[] getAnsiHash( byte[] challenge ) {
        if( hashesExternal ) {
            return ansiHash;
        }
        switch (LM_COMPATIBILITY) {
        case 0:
        case 1:
            return getPreNTLMResponse( password, challenge );
        case 2:
            return getNTLMResponse( password, challenge );
        case 3:
        case 4:
        case 5:
            if( clientChallenge == null ) {
                clientChallenge = new byte[8];
                RANDOM.nextBytes( clientChallenge );
            }
            return getLMv2Response(domain, username, password, challenge,
                    clientChallenge);
        default:
            return getPreNTLMResponse( password, challenge );
        }
    }
/**
 * Computes the 24 byte Unicode password hash given the 8 byte server challenge.
 */
    public byte[] getUnicodeHash( byte[] challenge ) {
        if( hashesExternal ) {
            return unicodeHash;
        }
        switch (LM_COMPATIBILITY) {
        case 0:
        case 1:
        case 2:
            return getNTLMResponse( password, challenge );
        case 3:
        case 4:
        case 5:
            /*
            if( clientChallenge == null ) {
                clientChallenge = new byte[8];
                RANDOM.nextBytes( clientChallenge );
            }
            return getNTLMv2Response(domain, username, password, null,
                    challenge, clientChallenge);
            */
            return new byte[0];
        default:
            return getNTLMResponse( password, challenge );
        }
    }

    public byte[] getSigningKey(byte[] challenge) throws SmbException
    {
        switch (LM_COMPATIBILITY) {
            case 0:
            case 1:
            case 2:
                byte[] signingKey = new byte[40];
                getUserSessionKey(challenge, signingKey, 0);
                System.arraycopy(getUnicodeHash(challenge), 0, signingKey, 16, 24);
                return signingKey;
            case 3:
            case 4:
            case 5:
                /* This code is only called if extended security is not on. This will
                 * all be cleaned up an normalized in JCIFS 2.x.
                 */
                throw new SmbException("NTLMv2 requires extended security (jcifs.smb.client.useExtendedSecurity must be true if jcifs.smb.lmCompatibility >= 3)");
        }
        return null;
    }

    /**
     * Returns the effective user session key.
     * 
     * @param challenge The server challenge.
     * @return A <code>byte[]</code> containing the effective user session key,
     * used in SMB MAC signing and NTLMSSP signing and sealing.
     */
    public byte[] getUserSessionKey(byte[] challenge) {
        if (hashesExternal) return null;
        byte[] key = new byte[16];
        try {
            getUserSessionKey(challenge, key, 0); 
        } catch (Exception ex) {
            if( log.level > 0 )
                ex.printStackTrace( log );
        }
        return key; 
    }

    /**
     * Calculates the effective user session key.
     *
     * @param challenge The server challenge.
     * @param dest The destination array in which the user session key will be
     * placed.
     * @param offset The offset in the destination array at which the
     * session key will start.
     */
    void getUserSessionKey(byte[] challenge, byte[] dest, int offset) throws SmbException {
        if (hashesExternal) return;
        try {
            MD4 md4 = new MD4();
            md4.update(password.getBytes(SmbConstants.UNI_ENCODING)); 
            switch (LM_COMPATIBILITY) {
            case 0:
            case 1:
            case 2:
                md4.update(md4.digest()); 
                md4.digest(dest, offset, 16); 
                break; 
            case 3:
            case 4:
            case 5:
                if( clientChallenge == null ) {
                    clientChallenge = new byte[8];
                    RANDOM.nextBytes( clientChallenge );
                }

                HMACT64 hmac = new HMACT64(md4.digest());
                hmac.update(username.toUpperCase().getBytes(
                        SmbConstants.UNI_ENCODING));
                hmac.update(domain.toUpperCase().getBytes(
                        SmbConstants.UNI_ENCODING));
                byte[] ntlmv2Hash = hmac.digest();
                hmac = new HMACT64(ntlmv2Hash);
                hmac.update(challenge);
                hmac.update(clientChallenge); 
                HMACT64 userKey = new HMACT64(ntlmv2Hash); 
                userKey.update(hmac.digest()); 
                userKey.digest(dest, offset, 16); 
                break; 
            default: 
                md4.update(md4.digest()); 
                md4.digest(dest, offset, 16); 
                break; 
            } 
        } catch (Exception e) {
            throw new SmbException("", e);
        }
    } 

/**
 * Compares two <tt>NtlmPasswordAuthentication</tt> objects for
 * equality. Two <tt>NtlmPasswordAuthentication</tt> objects are equal if
 * their caseless domain and username fields are equal and either both hashes are external and they are equal or both internally supplied passwords are equal. If one <tt>NtlmPasswordAuthentication</tt> object has external hashes (meaning negotiated via NTLM HTTP Authentication) and the other does not they will not be equal. This is technically not correct however the server 8 byte challage would be required to compute and compare the password hashes but that it not available with this method.
 */
    public boolean equals( Object obj ) {
        if( obj instanceof NtlmPasswordAuthentication ) {
            NtlmPasswordAuthentication ntlm = (NtlmPasswordAuthentication)obj;
            if( ntlm.domain.toUpperCase().equals( domain.toUpperCase() ) &&
                        ntlm.username.toUpperCase().equals( username.toUpperCase() )) {
                if( hashesExternal && ntlm.hashesExternal ) {
                    return Arrays.equals( ansiHash, ntlm.ansiHash ) &&
                                Arrays.equals( unicodeHash, ntlm.unicodeHash );
                    /* This still isn't quite right. If one npa object does not have external
                     * hashes and the other does then they will not be considered equal even
                     * though they may be.
                     */
                } else if( !hashesExternal && password.equals( ntlm.password )) {
                    return true;
                }
            }
        }
        return false;
    }


/**
 * Return the upcased username hash code.
 */
    public int hashCode() {
        return getName().toUpperCase().hashCode();
    }
/**
 * Return the domain and username in the format:
 * <tt>domain\\username</tt>. This is equivalent to <tt>getName()</tt>.
 */
    public String toString() {
        return getName();
    }

    static String unescape( String str ) throws NumberFormatException, UnsupportedEncodingException {
        char ch;
        int i, j, state, len;
        char[] out;
        byte[] b = new byte[1];

        if( str == null ) {
            return null;
        }

        len = str.length();
        out = new char[len];
        state = 0;
        for( i = j = 0; i < len; i++ ) {
            switch( state ) {
                case 0:
                    ch = str.charAt( i );
                    if( ch == '%' ) {
                        state = 1;
                    } else {
                        out[j++] = ch;
                    }
                    break;
                case 1:
                    /* Get ASCII hex value and convert to platform dependant
                     * encoding like EBCDIC perhaps
                     */
                    b[0] = (byte)(Integer.parseInt( str.substring( i, i + 2 ), 16 ) & 0xFF);
                    out[j++] = (new String( b, 0, 1, "ASCII" )).charAt( 0 );
                    i++;
                    state = 0;
            }
        }

        return new String( out, 0, j );
    }

}

