/*
 * ====================================================================
 * Copyright (c) 2004-2012 TMate Software Ltd.  All rights reserved.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution.  The terms
 * are also available at http://svnkit.com/license.html
 * If newer versions of this license are posted there, you may use a
 * newer version instead, at your option.
 * ====================================================================
 */
package org.tmatesoft.svn.core.internal.io.dav.http;

import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;

import org.tmatesoft.svn.core.SVNErrorCode;
import org.tmatesoft.svn.core.SVNErrorMessage;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.internal.util.SVNBase64;
import org.tmatesoft.svn.core.internal.util.SVNFormatUtil;
import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
import org.tmatesoft.svn.util.SVNLogType;

/**
 * @version 1.3
 * @author  TMate Software Ltd.
 */
class HTTPNTLMAuthentication extends HTTPAuthentication {

    private static final String NTLM_CASE_CONVERTION_PROPERTY = "svnkit.http.ntlm.uppercase";
    private static final String OLD_NTLM_CASE_CONVERTION_PROPERTY = "javasvn.http.ntlm.uppercase";
    
    private static final String DEFAULT_CHARSET = "ASCII";
    private static final String PROTOCOL_NAME = "NTLMSSP";
    private static final int LM_RESPONSE_LENGTH = 24;
    private static final int UNINITIATED = 0;
    protected static final int TYPE1 = 1;
    protected static final int TYPE3 = 3;
    private static byte[] ourMagicBytes = {
        (byte) 0x4B, (byte) 0x47, (byte) 0x53, (byte) 0x21, 
        (byte) 0x40, (byte) 0x23, (byte) 0x24, (byte) 0x25
    };
    
    private static final long NEGOTIATE_UNICODE = 0x00000001L;  
    private static final long NEGOTIATE_OEM = 0x00000002L;  
    private static final long REQUEST_TARGET = 0x00000004L;  
    private static final long NEGOTIATE_SIGN = 0x00000010L;  
    private static final long NEGOTIATE_SEAL = 0x00000020L;  
    private static final long NEGOTIATE_DATAGRAM_STYLE = 0x00000040L;  
    private static final long NEGOTIATE_LAN_MANAGER_KEY = 0x00000080L;  
    private static final long NEGOTIATE_NETWARE = 0x00000100L;  
    private static final long NEGOTIATE_NTLM = 0x00000200L;  
    private static final long NEGOTIATE_DOMAIN_SUPPLIED = 0x00001000L;  
    private static final long NEGOTIATE_WORKSTATION_SUPPLIED = 0x00002000L;  
    private static final long NEGOTIATE_LOCAL_CALL = 0x00004000L;  
    private static final long NEGOTIATE_ALWAYS_SIGN = 0x00008000L;  
    private static final long TARGET_TYPE_DOMAIN = 0x00010000L;  
    private static final long TARGET_TYPE_SERVER = 0x00020000L;  
    private static final long TARGET_TYPE_SHARE = 0x00040000L;  
    private static final long NEGOTIATE_NTLM2_KEY = 0x00080000L;  
    private static final long REQUEST_INIT_RESPONSE = 0x00100000L;  
    private static final long REQUEST_ACCEPT_RESPONSE = 0x00200000L;  
    private static final long REQUEST_NON_NT_SESSION_KEY = 0x00400000L;  
    private static final long NEGOTIATE_TARGET_INFO = 0x00800000L;  
    private static final long NEGOTIATE_128 = 0x20000000L;  
    private static final long NEGOTIATE_KEY_EXCHANGE = 0x40000000L;  
    private static final long NEGOTIATE_56 = 0x80000000L;  
    
    private static Map<Long, String> ourFlags = new TreeMap<Long, String>();
    static {
        ourFlags.put(new Long(NEGOTIATE_UNICODE), "0x00000001 (Negotiate Unicode)");
        ourFlags.put(new Long(NEGOTIATE_OEM), "0x00000002 (Negotiate OEM)");
        ourFlags.put(new Long(REQUEST_TARGET), "0x00000004 (Request Target)");
        ourFlags.put(new Long(0x00000008L), "0x00000008 (Unknown)");
        ourFlags.put(new Long(NEGOTIATE_SIGN), "0x00000010 (Negotiate Sign)");
        ourFlags.put(new Long(NEGOTIATE_SEAL), "0x00000020 (Negotiate Seal)");
        ourFlags.put(new Long(NEGOTIATE_DATAGRAM_STYLE), "0x00000040 (Negotiate Datagram Style)");
        ourFlags.put(new Long(NEGOTIATE_LAN_MANAGER_KEY), "0x00000080 (Negotiate Lan Manager Key)");
        ourFlags.put(new Long(NEGOTIATE_NETWARE), "0x00000100 (Negotiate Netware)");
        ourFlags.put(new Long(NEGOTIATE_NTLM), "0x00000200 (Negotiate NTLM)");
        ourFlags.put(new Long(0x00000400L), "0x00000400 (Unknown)");
        ourFlags.put(new Long(0x00000800L), "0x00000800 (Unknown)");
        ourFlags.put(new Long(NEGOTIATE_DOMAIN_SUPPLIED), "0x00001000 (Negotiate Domain Supplied)");
        ourFlags.put(new Long(NEGOTIATE_WORKSTATION_SUPPLIED), "0x00002000 (Negotiate Workstation Supplied)");
        ourFlags.put(new Long(NEGOTIATE_LOCAL_CALL), "0x00004000 (Negotiate Local Call)");
        ourFlags.put(new Long(NEGOTIATE_ALWAYS_SIGN), "0x00008000 (Negotiate Always Sign)");
        ourFlags.put(new Long(TARGET_TYPE_DOMAIN), "0x00010000 (Target Type Domain)");
        ourFlags.put(new Long(TARGET_TYPE_SERVER), "0x00020000 (Target Type Server)");
        ourFlags.put(new Long(TARGET_TYPE_SHARE), "0x00040000 (Target Type Share)");
        ourFlags.put(new Long(NEGOTIATE_NTLM2_KEY), "0x00080000 (Negotiate NTLM2 Key)");
        ourFlags.put(new Long(REQUEST_INIT_RESPONSE), "0x00100000 (Request Init Response)");
        ourFlags.put(new Long(REQUEST_ACCEPT_RESPONSE), "0x00200000 (Request Accept Response)");
        ourFlags.put(new Long(REQUEST_NON_NT_SESSION_KEY), "0x00400000 (Request Non-NT Session Key)");
        ourFlags.put(new Long(NEGOTIATE_TARGET_INFO), "0x00800000 (Negotiate Target Info)");
        ourFlags.put(new Long(0x01000000L), "0x01000000 (Unknown)");
        ourFlags.put(new Long(0x02000000L), "0x02000000 (Unknown)");
        ourFlags.put(new Long(0x04000000L), "0x04000000 (Unknown)");
        ourFlags.put(new Long(0x08000000L), "0x08000000 (Unknown)");
        ourFlags.put(new Long(0x10000000L), "0x10000000 (Unknown)");
        ourFlags.put(new Long(NEGOTIATE_128), "0x20000000 (Negotiate 128)");
        ourFlags.put(new Long(NEGOTIATE_KEY_EXCHANGE), "0x40000000 (Negotiate Key Exchange)");
        ourFlags.put(new Long(NEGOTIATE_56), "0x80000000 (Negotiate 56)");
    }

    private static Map<Integer, String> ourTargetInfoTypes = new TreeMap<Integer, String>();
    static {
        ourTargetInfoTypes.put(new Integer(1), "Server Name");
        ourTargetInfoTypes.put(new Integer(2), "Domain Name");
        ourTargetInfoTypes.put(new Integer(3), "DNS Host Name");
        ourTargetInfoTypes.put(new Integer(4), "DNS Domain Name");
    }
    
    protected int myState;
    private String myCharset;
    private byte[] myResponse;
    private int myPosition; 
    private byte[] myNonce;
    private boolean myIsNegotiateLocalCall;
    
    protected HTTPNTLMAuthentication (String charset) {
        myState = UNINITIATED;
        myIsNegotiateLocalCall = false;
        myCharset = charset;
        if (myCharset == null) {
            myCharset = "US-ASCII";
        }
    }
    
    public void setType1State(){
        myState = TYPE1;
    }

    public void setType3State(){
        myState = TYPE3;
    }

    public boolean isInType3State(){
        return myState == TYPE3;
    }
    
    private void initResponse(int bufferSize){
        myResponse = new byte[bufferSize];
        myPosition = 0;
    }
    
    private void addByte(byte b){
        myResponse[myPosition++] = b;
    }

    private void addBytes(byte[] bytes) {
        for (int i = 0; i < bytes.length; i++) {
            myResponse[myPosition++] = bytes[i];
        }
    }

    private byte[] convertToShortValue(int num){
        byte[] val = new byte[2];
        val[0] = (byte)(num & 0xff);
        val[1] = (byte)((num >> 8) & 0xff);
        return val;
    }
    
    private String getResponse(){
        byte[] response;
        if (myResponse.length > myPosition) {
            response = new byte[myPosition];
            for (int i = 0; i < myPosition; i++) {
                response[i] = myResponse[i];
            }
        } else {
            response = myResponse;
        }
        
        return SVNBase64.byteArrayToBase64(response);
    }
    
    public void parseChallenge(String challenge) throws SVNException {
        if (challenge == null) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.RA_DAV_REQUEST_FAILED, 
                    "NTLM HTTP auth: expected challenge");
            SVNErrorManager.error(err, SVNLogType.NETWORK);
        }
        
        byte[] challengeBase64Bytes = HTTPAuthentication.getBytes(challenge, DEFAULT_CHARSET);
        byte[] resultBuffer = new byte[challengeBase64Bytes.length];
        int resultLength = 0;
        try {
            resultLength = SVNBase64.base64ToByteArray(new StringBuffer(new String(challengeBase64Bytes, myCharset)), 
                    resultBuffer);
        } catch (UnsupportedEncodingException e) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.RA_DAV_REQUEST_FAILED, 
                    "NTLM HTTP auth: " + e.getMessage());
            SVNErrorManager.error(err, SVNLogType.NETWORK);
        }
        
        String proto;
        try {
            proto = new String(resultBuffer, 0, 7, myCharset);
        } catch (UnsupportedEncodingException e) {
            proto = new String(resultBuffer, 0, 7);
        }
        byte[] typeBytes = new byte[4];
        for (int i = 0; i < 4; i++) {
            typeBytes[i] = resultBuffer[8 + i];
        }
        long type = toLong(typeBytes); 
        
        if (!PROTOCOL_NAME.equalsIgnoreCase(proto)) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.RA_DAV_REQUEST_FAILED, 
                    "NTLM HTTP auth: incorrect signature ''(0}''", proto);
            SVNErrorManager.error(err, SVNLogType.NETWORK);
        } else if (type != 2) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.RA_DAV_REQUEST_FAILED, 
                    "NTLM HTTP auth: expected type 2 message instead of ''(0, number, integer}''", 
                    new Long(type));
            SVNErrorManager.error(err, SVNLogType.NETWORK);
        }
        
        myNonce = new byte[8];
        for (int i = 0; i < 8; i++) {
            myNonce[i] = resultBuffer[i + 24];
        }
        
        byte[] flagBytes = new byte[4];
        for (int i = 0; i < 4; i++) {
            flagBytes[i] = resultBuffer[i + 20];
        }
        long flags = toLong(flagBytes);
        
        StringBuffer log = new StringBuffer();
        String base64DecodedMessage;
        try {
            base64DecodedMessage = new String(resultBuffer, 0, resultLength, myCharset);
        } catch (UnsupportedEncodingException e) {
            base64DecodedMessage = new String(resultBuffer, 0, resultLength);
        }
        log.append("NTLM auth message: " + base64DecodedMessage);
        log.append('\n');
        log.append("Length: " + base64DecodedMessage.length());
        log.append('\n');
        log.append("Signature: " + proto);
        log.append('\n');
        log.append("Type: " + type);
        log.append('\n');
        log.append("Flags: " + Long.toString(flags, 16));
        log.append('\n');
        for (Iterator<Long> flagsIter = ourFlags.keySet().iterator(); flagsIter.hasNext();) {
            final Long curFlag = flagsIter.next();
            if ((flags & curFlag.longValue()) != 0) {
                log.append(ourFlags.get(curFlag));
                log.append('\n');
            }
        }
        
        byte[] targetNameLengthBytes = new byte[2];
        for (int i = 0; i < 2; i++) {
            targetNameLengthBytes[i] = resultBuffer[12 + i];
        }
        int targetNameLength = toInt(targetNameLengthBytes);

        byte[] targetNameAllocatedBytes = new byte[2];
        for (int i = 0; i < 2; i++) {
            targetNameAllocatedBytes[i] = resultBuffer[14 + i];
        }
        int targetNameAllocated = toInt(targetNameAllocatedBytes);
        
        byte[] targetNameOffsetBytes = new byte[4];
        for (int i = 0; i < 4; i++) {
            targetNameOffsetBytes[i] = resultBuffer[16 + i];
        }
        long targetNameOffset = toLong(targetNameOffsetBytes); 
        
        if (targetNameLength > 0) {
            String targetName;
            try {
                targetName = new String(resultBuffer, (int)targetNameOffset, targetNameAllocated, myCharset);
            } catch (UnsupportedEncodingException e) {
                targetName = new String(resultBuffer, (int)targetNameOffset, targetNameAllocated);
            }
            log.append("Target Name: " + targetName);
            log.append('\n');
        }
        log.append("Challenge: ");
        for (int i = 0; i < myNonce.length; i++) {
            log.append(SVNFormatUtil.getHexNumberFromByte(myNonce[i]));
        }
        log.append('\n');
        
        //check for local call
        long contextH = -1;
        long contextL = -1;
        boolean containsContext = false;
        if (targetNameOffset != 32 && resultLength >= 40) {
            byte[] contextHBytes = new byte[4];
            byte[] contextLBytes = new byte[4];
            int i = 0;
            for (i = 0; i < 4; i++) {
                contextHBytes[i] = resultBuffer[i + 32];
            }
            for (;i < 8; i++) {
                contextLBytes[i - 4] = resultBuffer[i + 32];
            }

            contextH = toLong(contextHBytes);
            contextL = toLong(contextLBytes);
            if (contextL == 0) {
                containsContext = true;
                log.append("Context: ");
                log.append(Long.toString(contextH, 16) + " " + Long.toString(contextL, 16));
                log.append('\n');
            }
            
            if (contextH != 0 && (flags & NEGOTIATE_LOCAL_CALL) != 0) {
                myIsNegotiateLocalCall = true;
            } else {
                myIsNegotiateLocalCall = false;
            }
        } else {
            myIsNegotiateLocalCall = false;
        }
        System.out.println("negotiate local calls: " + myIsNegotiateLocalCall);

        if ((flags & NEGOTIATE_TARGET_INFO) != 0) {
            int tgtInfoSecurityBufferOffset = containsContext ? 40: 32;
            
            byte[] targetInfoLengthBytes = new byte[2];
            for (int i = 0; i < 2; i++) {
                targetInfoLengthBytes[i] = resultBuffer[tgtInfoSecurityBufferOffset + i];
            }
            int targetInfoLength = toInt(targetInfoLengthBytes);
            
            byte[] targetInfoAllocatedBytes = new byte[2];
            for (int i = 0; i < 2; i++) {
                targetInfoAllocatedBytes[i] = resultBuffer[tgtInfoSecurityBufferOffset + 2 + i];
            }
            int targetInfoAllocated = toInt(targetInfoAllocatedBytes);
            
            byte[] targetInfoOffsetBytes = new byte[4];
            for (int i = 0; i < 4; i++) {
                targetInfoOffsetBytes[i] = resultBuffer[tgtInfoSecurityBufferOffset + 4 + i];
            }
            long targetInfoOffset = toLong(targetInfoOffsetBytes);
            
            byte[] targetInfoTypeBytes = new byte[2];
            byte[] subblockLengthBytes = new byte[2];
            int read = 0;
            while (targetInfoLength > 0 && read <= targetInfoAllocated) {
                for (int i = 0; i < 2; i++) {
                    targetInfoTypeBytes[i] = resultBuffer[(int)targetInfoOffset + i];
                }
                read += 2;
                targetInfoOffset += 2;
                int targetInfoType = toInt(targetInfoTypeBytes);
                if (targetInfoType == 0){
                    break;
                }

                for (int i = 0; i < 2; i++) {
                    subblockLengthBytes[i] = resultBuffer[(int)targetInfoOffset + i];
                }
                read += 2;
                targetInfoOffset += 2;
                int subblockLength = toInt(subblockLengthBytes);

                String typeDescription = (String)ourTargetInfoTypes.get(new Integer(targetInfoType));
                if (typeDescription != null) {
                    String info;
                    try {
                        info = new String(resultBuffer, (int)targetInfoOffset, subblockLength, myCharset);
                    } catch (UnsupportedEncodingException e) {
                        info = new String(resultBuffer, (int)targetInfoOffset, subblockLength);
                    }
                    log.append(typeDescription + ": " + info);
                    log.append('\n');
                }
                read += subblockLength;
                targetInfoOffset += subblockLength;
            }
        }
        log.append('\n');
    }
    
    private static int toInt(byte[] num){
        int l = 0;
        for (int i = 0; i < 2 ; i++) {
            int b = num[i] & 0xff;
            b = b << i*8;
            l |=  b;
        }
        return l;
    }

    public String authenticate() throws SVNException {
        if (myState != TYPE1 && myState != TYPE3) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.RA_DAV_REQUEST_FAILED, 
                    "Unsupported message type in HTTP NTLM authentication");
            SVNErrorManager.error(err, SVNLogType.NETWORK);
        }
        
        final String username = getUserName();
        String domain = getDomain();
        if (domain == null) {
            domain = "";
        }
        
        String hostName = null;
        try {
            InetAddress localhost = InetAddress.getLocalHost();
            hostName = localhost.getHostName();
        } catch (UnknownHostException uhe) {
            hostName = "";
        }
        
        if (isUpperCase()) {
            domain = domain.toUpperCase();
            hostName = hostName.toUpperCase();
        }
        
        byte[] protocol = HTTPAuthentication.getBytes(PROTOCOL_NAME, DEFAULT_CHARSET);
        byte[] domainBytes = HTTPAuthentication.getBytes(domain, DEFAULT_CHARSET);
        byte[] hostNameBytes = HTTPAuthentication.getBytes(hostName, DEFAULT_CHARSET);
        byte[] domLen = convertToShortValue(domainBytes.length);
        byte[] hostLen = convertToShortValue(hostNameBytes.length);
        StringBuffer sublog = new StringBuffer();
        sublog.append("Signature: " + PROTOCOL_NAME);
        sublog.append('\n');

        long flags = NEGOTIATE_OEM | REQUEST_TARGET | NEGOTIATE_NTLM | NEGOTIATE_LOCAL_CALL;
        if (domain.length() > 0) {
            flags |= NEGOTIATE_DOMAIN_SUPPLIED;
        }
        
        if (myState == TYPE1) {
            int responseLength = 32 + domainBytes.length + hostNameBytes.length;
            
            initResponse(responseLength);

            //NTLMSSP\0 signature (8 bytes long)
            addBytes(protocol);
            addByte((byte) 0);

            // Type1 - Negotiate (4 bytes long)
            addByte((byte) 1);
            addByte((byte) 0);
            addByte((byte) 0);
            addByte((byte) 0);
            sublog.append("Type: " + 1);
            sublog.append('\n');

            // Flags (4 bytes long): 'Negotiate OEM', 'Request Target', 
            // 'Negotiate NTLM', 'Negotiate Always Sign'
            addByte((byte)(flags & 0xff));
            addByte((byte)((flags >> 8) & 0xff));
            addByte((byte)((flags >> 16) & 0xff));
            addByte((byte)((flags >> 24) & 0xff));
            sublog.append("Flags: " + Long.toString(flags, 16));
            sublog.append('\n');
            
            for (Iterator<Long> flagsIter = ourFlags.keySet().iterator(); flagsIter.hasNext();) {
                final Long curFlag = flagsIter.next();
                if ((flags & curFlag.longValue()) != 0) {
                    sublog.append(ourFlags.get(curFlag));
                    sublog.append('\n');
                }
            }

            // Domain name length (2 bytes short)
            addBytes(domLen);
            // Allocated space for the domain name (2 bytes short)
            addBytes(domLen);

            // Domain name offset (4 bytes long)
            byte[] domainOffset = convertToShortValue(hostNameBytes.length + 32);
            addBytes(domainOffset);
            addByte((byte) 0);
            addByte((byte) 0);

            // Host name length (2 bytes short).
            addBytes(hostLen);
            // Allocated space for the host name (2 bytes short)
            addBytes(hostLen);

            // Host name offset (always 32, 4 bytes long).
            byte[] hostOffset = convertToShortValue(32);
            addBytes(hostOffset);
            addByte((byte) 0);
            addByte((byte) 0);

            // Host name 
            addBytes(hostNameBytes);
            if (hostName.length() > 0) {
                sublog.append("Host Name: " + hostName);
                sublog.append('\n');
            }

            // Domain name
            addBytes(domainBytes);
            if (domain.length() > 0) {
                sublog.append("Domain: " + domain);
                sublog.append('\n');
            }
        } else if (myState == TYPE3) {
            byte[] userBytes = username.getBytes();
            sublog.append("Type: " + 3);
            sublog.append('\n');
            sublog.append("Flags: " + Long.toString(flags, 16));
            sublog.append('\n');
            for (Iterator<Long> flagsIter = ourFlags.keySet().iterator(); flagsIter.hasNext();) {
                final Long curFlag = flagsIter.next();
                if ((flags & curFlag.longValue()) != 0) {
                    sublog.append(ourFlags.get(curFlag));
                    sublog.append('\n');
                }
            }

            if (!myIsNegotiateLocalCall) {
                int responseLength = 64 + LM_RESPONSE_LENGTH + domainBytes.length + hostNameBytes.length + userBytes.length;
                
                initResponse(responseLength);
    
                addBytes(protocol);
                addByte((byte) 0);
    
                //Type3
                addByte((byte) 3);
                addByte((byte) 0);
                addByte((byte) 0);
                addByte((byte) 0);
    
                byte[] lmResponseLength = convertToShortValue(24); 
                // LM Response Length 
                addBytes(lmResponseLength);
                // LM Response allocated space
                addBytes(lmResponseLength);
    
                // LM Response Offset
                addBytes(convertToShortValue(responseLength - 24));
                addByte((byte) 0);
                addByte((byte) 0);
    
                byte[] ntlmResponseLength = convertToShortValue(0); 
                // NTLM Response Length 
                addBytes(ntlmResponseLength);
                // NTLM Response allocated space
                addBytes(ntlmResponseLength);
    
                byte[] responseLengthShortBytes = convertToShortValue(responseLength); 
                // NTLM Response Offset
                addBytes(responseLengthShortBytes);
                addByte((byte) 0);
                addByte((byte) 0);
    
                // Domain length
                addBytes(domLen);
                // Domain allocated space
                addBytes(domLen);
                
                // Domain Offset
                addBytes(convertToShortValue(64));
                addByte((byte) 0);
                addByte((byte) 0);
    
                byte[] usernameLength = convertToShortValue(userBytes.length); 
                // Username Length 
                addBytes(usernameLength);
                // Username allocated space
                addBytes(usernameLength);
    
                // User offset
                addBytes(convertToShortValue(64 + domainBytes.length));
                addByte((byte) 0);
                addByte((byte) 0);
    
                // Host name length
                addBytes(hostLen);
                // Host name allocated space
                addBytes(hostLen);
    
                // Host offset
                addBytes(convertToShortValue(64 + domainBytes.length + userBytes.length));
    
                for (int i = 0; i < 6; i++) {
                    addByte((byte) 0);
                }
    
                // Message length
                addBytes(responseLengthShortBytes);
                addByte((byte) 0);
                addByte((byte) 0);
    
                // Flags
                addByte((byte)(flags & 0xff));
                addByte((byte)((flags >> 8) & 0xff));
                addByte((byte)((flags >> 16) & 0xff));
                addByte((byte)((flags >> 24) & 0xff));

                addBytes(domainBytes);
                if (domain.length() > 0) {
                    sublog.append("Domain: " + domain);
                    sublog.append('\n');
                }

                addBytes(userBytes);
                if (username.length() > 0) {
                    sublog.append("User Name: " + username);
                    sublog.append('\n');
                }

                addBytes(hostNameBytes);
                if (hostName.length() > 0) {
                    sublog.append("Host Name: " + hostName);
                    sublog.append('\n');
                }

                String password = getPassword();
                byte[] hash = hashPassword(password != null ? password : ""); 
                addBytes(hash);

                sublog.append("Hash: " + new String(hash));
                sublog.append('\n');
                
            } else {
                int responseLength = 64;
                byte[] responseLengthShortBytes = convertToShortValue(responseLength); 
                initResponse(responseLength);

                addBytes(protocol);
                addByte((byte) 0);

                //Type3
                addByte((byte) 3);
                addByte((byte) 0);
                addByte((byte) 0);
                addByte((byte) 0);

                // LM Response Length 
                addByte((byte)0);
                addByte((byte)0);
                // LM Response allocated space
                addByte((byte)0);
                addByte((byte)0);

                // LM Response Offset
                addBytes(responseLengthShortBytes);
                addByte((byte) 0);
                addByte((byte) 0);

                // NTLM Response Length 
                addByte((byte)0);
                addByte((byte)0);
                // NTLM Response allocated space
                addByte((byte)0);
                addByte((byte)0);
                // NTLM Response Offset
                addBytes(responseLengthShortBytes);
                addByte((byte) 0);
                addByte((byte) 0);

                // Domain length
                addByte((byte)0);
                addByte((byte)0);
                // Domain allocated space
                addByte((byte)0);
                addByte((byte)0);
                // Domain Offset
                addBytes(responseLengthShortBytes);
                addByte((byte) 0);
                addByte((byte) 0);

                // Username Length 
                addByte((byte)0);
                addByte((byte)0);
                // Username allocated space
                addByte((byte)0);
                addByte((byte)0);
                // User offset
                addBytes(responseLengthShortBytes);
                addByte((byte) 0);
                addByte((byte) 0);

                // Host name length
                addByte((byte)0);
                addByte((byte)0);
                // Host name allocated space
                addByte((byte)0);
                addByte((byte)0);
                // Host offset
                addBytes(responseLengthShortBytes);

                for (int i = 0; i < 6; i++) {
                    addByte((byte) 0);
                }

                // Message length
                addBytes(responseLengthShortBytes);
                addByte((byte) 0);
                addByte((byte) 0);

                // Flags
                addByte((byte)(flags & 0xff));
                addByte((byte)((flags >> 8) & 0xff));
                addByte((byte)((flags >> 16) & 0xff));
                addByte((byte)((flags >> 24) & 0xff));
            }
            setType1State();
        }

        StringBuffer log = new StringBuffer();
        String message = null;
        try {
            message = new String(myResponse, 0, myPosition, myCharset);
        } catch (UnsupportedEncodingException e) {
            message = new String(myResponse, 0, myPosition);
        }
        log.append("NTLM auth message: " + message);
        log.append('\n');
        log.append("Length: " + message.length());
        log.append('\n');
        log.append(sublog);

        return "NTLM " + getResponse();
    }

    public String getAuthenticationScheme(){
        return "NTLM";
    }
    
    public boolean isNative() {
        return false;
    }
    
    public String getUserName() {
        String login = getRawUserName();
        String userName = null;
        int slashInd = login != null ? login.indexOf('\\') : -1; 
        if (slashInd != -1) {
            int lastInd = slashInd + 1;
            while (lastInd < login.length() && login.charAt(lastInd) == '\\') {
                lastInd++;
            }
            userName = login.substring(lastInd);
        } else {
            userName = login == null ? System.getProperty("user.name") : login;
        }
        return userName;
    }

    public String getDomain() {
        String login = getRawUserName();
        String domain = null;
        int slashInd = login != null ? login.indexOf('\\') : -1; 
        if (slashInd != -1) {
            domain = login.substring(0, slashInd);
        } 
        return domain;
    }
    
    private long toLong(byte[] num){
        long l = 0;
        for (int i = 0; i < 4 ; i++) {
            long b = num[i] & 0xff;
            b = b << i*8;
            l |=  b;
        }
        return l;
    }

    private boolean isUpperCase() {
        String upperCase = System.getProperty(NTLM_CASE_CONVERTION_PROPERTY, System.getProperty(OLD_NTLM_CASE_CONVERTION_PROPERTY, "true"));
        return Boolean.valueOf(upperCase).booleanValue();
    }

    private byte[] hashPassword(String password) throws SVNException {
        byte[] passw = isUpperCase() ? password.toUpperCase().getBytes() : password.getBytes();
        byte[] lmPw1 = new byte[7];
        byte[] lmPw2 = new byte[7];

        int len = passw.length;
        if (len > 7) {
            len = 7;
        }

        int idx;
        for (idx = 0; idx < len; idx++) {
            lmPw1[idx] = passw[idx];
        }
        
        for (; idx < 7; idx++) {
            lmPw1[idx] = (byte) 0;
        }

        len = passw.length;
        if (len > 14) {
            len = 14;
        }
        for (idx = 7; idx < len; idx++) {
            lmPw2[idx - 7] = passw[idx];
        }
        for (; idx < 14; idx++) {
            lmPw2[idx - 7] = (byte) 0;
        }

        byte[] lmHpw1;
        lmHpw1 = encrypt(lmPw1, ourMagicBytes);

        byte[] lmHpw2 = encrypt(lmPw2, ourMagicBytes);

        byte[] lmHpw = new byte[21];
        for (int i = 0; i < lmHpw1.length; i++) {
            lmHpw[i] = lmHpw1[i];
        }
        for (int i = 0; i < lmHpw2.length; i++) {
            lmHpw[i + 8] = lmHpw2[i];
        }
        for (int i = 0; i < 5; i++) {
            lmHpw[i + 16] = (byte) 0;
        }

        // Create the responses.
        byte[] lmResp = new byte[24];
        calcResp(lmHpw, lmResp);

        return lmResp;
    }
    
    private void calcResp(byte[] keys, byte[] results) throws SVNException {
        byte[] keys1 = new byte[7];
        byte[] keys2 = new byte[7];
        byte[] keys3 = new byte[7];
        
        for (int i = 0; i < 7; i++) {
            keys1[i] = keys[i];
        }

        for (int i = 0; i < 7; i++) {
            keys2[i] = keys[i + 7];
        }

        for (int i = 0; i < 7; i++) {
            keys3[i] = keys[i + 14];
        }
        
        byte[] results1 = encrypt(keys1, myNonce);

        byte[] results2 = encrypt(keys2, myNonce);

        byte[] results3 = encrypt(keys3, myNonce);

        for (int i = 0; i < 8; i++) {
            results[i] = results1[i];
        }
        
        for (int i = 0; i < 8; i++) {
            results[i + 8] = results2[i];
        }
        
        for (int i = 0; i < 8; i++) {
            results[i + 16] = results3[i];
        }
    }
    
    private byte[] encrypt(byte[] key, byte[] bytes) throws SVNException {
        Cipher ecipher = getCipher(key);
        try {
            byte[] enc = ecipher.doFinal(bytes);
            return enc;
        } catch (IllegalBlockSizeException e) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, "Invalid block size for DES encryption: {0}", e.getLocalizedMessage());
            SVNErrorManager.error(err, SVNLogType.NETWORK);
        } catch (BadPaddingException e) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, "Data not padded correctly for DES encryption: {0}", e.getLocalizedMessage());
            SVNErrorManager.error(err, SVNLogType.NETWORK);
        }
        return null;
    }
    
    private Cipher getCipher(byte[] key) throws SVNException {
        try {
            final Cipher ecipher = Cipher.getInstance("DES/ECB/NoPadding");
            key = setupKey(key);
            ecipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "DES"));
            return ecipher;
        } catch (NoSuchAlgorithmException e) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, "DES encryption is not available: {0}", e.getLocalizedMessage());
            SVNErrorManager.error(err, SVNLogType.NETWORK);
        } catch (InvalidKeyException e) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, "Invalid key for DES encryption: {0}", e.getLocalizedMessage());
            SVNErrorManager.error(err, SVNLogType.NETWORK);
        } catch (NoSuchPaddingException e) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, "NoPadding option for DES is not available: {0}", e.getLocalizedMessage());
            SVNErrorManager.error(err, SVNLogType.NETWORK);
        }
        return null;
    }
    
    private byte[] setupKey(byte[] key56) {
        byte[] key = new byte[8];
        key[0] = (byte) ((key56[0] >> 1) & 0xff);
        key[1] = (byte) ((((key56[0] & 0x01) << 6) 
            | (((key56[1] & 0xff) >> 2) & 0xff)) & 0xff);
        key[2] = (byte) ((((key56[1] & 0x03) << 5) 
            | (((key56[2] & 0xff) >> 3) & 0xff)) & 0xff);
        key[3] = (byte) ((((key56[2] & 0x07) << 4) 
            | (((key56[3] & 0xff) >> 4) & 0xff)) & 0xff);
        key[4] = (byte) ((((key56[3] & 0x0f) << 3) 
            | (((key56[4] & 0xff) >> 5) & 0xff)) & 0xff);
        key[5] = (byte) ((((key56[4] & 0x1f) << 2) 
            | (((key56[5] & 0xff) >> 6) & 0xff)) & 0xff);
        key[6] = (byte) ((((key56[5] & 0x3f) << 1) 
            | (((key56[6] & 0xff) >> 7) & 0xff)) & 0xff);
        key[7] = (byte) (key56[6] & 0x7f);
        
        for (int i = 0; i < key.length; i++) {
            key[i] = (byte) (key[i] << 1);
        }
        return key;
    }

    public boolean allowPropmtForCredentials() {
        return true;
    }

}
