Open Source Repository

Home /commons-httpclient/commons-httpclient-3.1 | Repository Home



org/apache/commons/httpclient/auth/NTLM.java
/*
 * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//httpclient/src/java/org/apache/commons/httpclient/auth/NTLM.java,v 1.11 2004/05/13 04:02:00 mbecke Exp $
 * $Revision: 480424 $
 * $Date: 2006-11-29 06:56:49 +0100 (Wed, 29 Nov 2006) $
 *
 * ====================================================================
 *
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 *
 */

package org.apache.commons.httpclient.auth;

import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

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

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.httpclient.util.EncodingUtil;

/**
 * Provides an implementation of the NTLM authentication protocol.
 <p>
 * This class provides methods for generating authentication
 * challenge responses for the NTLM authentication protocol.  The NTLM
 * protocol is a proprietary Microsoft protocol and as such no RFC
 * exists for it.  This class is based upon the reverse engineering
 * efforts of a wide range of people.</p>
 *
 <p>Please note that an implementation of JCE must be correctly installed and configured when
 * using NTLM support.</p>
 *
 <p>This class should not be used externally to HttpClient as it's API is specifically
 * designed to work with HttpClient's use case, in particular it's connection management.</p>
 *
 @author <a href="mailto:[email protected]">Adrian Sutton</a>
 @author <a href="mailto:[email protected]">Jeff Dever</a>
 @author <a href="mailto:[email protected]">Mike Bowler</a>
 *
 @version $Revision: 480424 $ $Date: 2006-11-29 06:56:49 +0100 (Wed, 29 Nov 2006) $
 @since 3.0
 */
final class NTLM {

    /** Character encoding */
    public static final String DEFAULT_CHARSET = "ASCII";

    /** The current response */
    private byte[] currentResponse;

    /** The current position */
    private int currentPosition = 0;

    /** The character set to use for encoding the credentials */
    private String credentialCharset = DEFAULT_CHARSET;
    
    /**
     * Returns the response for the given message.
     *
     @param message the message that was received from the server.
     @param username the username to authenticate with.
     @param password the password to authenticate with.
     @param host The host.
     @param domain the NT domain to authenticate in.
     @return The response.
     @throws HttpException If the messages cannot be retrieved.
     */
    public final String getResponseFor(String message,
            String username, String password, String host, String domain)
            throws AuthenticationException {
                
        final String response;
        if (message == null || message.trim().equals("")) {
            response = getType1Message(host, domain);
        else {
            response = getType3Message(username, password, host, domain,
                    parseType2Message(message));
        }
        return response;
    }

    /**
     * Return the cipher for the specified key.
     @param key The key.
     @return Cipher The cipher.
     @throws AuthenticationException If the cipher cannot be retrieved.
     */
    private Cipher getCipher(byte[] keythrows AuthenticationException {
        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) {
            throw new AuthenticationException("DES encryption is not available.", e);
        catch (InvalidKeyException e) {
            throw new AuthenticationException("Invalid key for DES encryption.", e);
        catch (NoSuchPaddingException e) {
            throw new AuthenticationException(
                "NoPadding option for DES is not available.", e);
        }
    }

    /** 
     * Adds parity bits to the key.
     @param key56 The key
     @return The modified key.
     */
    private byte[] setupKey(byte[] key56) {
        byte[] key = new byte[8];
        key[0(byte) ((key56[0>> 10xff);
        key[1(byte) ((((key56[00x01<< 6
            (((key56[10xff>> 20xff)) 0xff);
        key[2(byte) ((((key56[10x03<< 5
            (((key56[20xff>> 30xff)) 0xff);
        key[3(byte) ((((key56[20x07<< 4
            (((key56[30xff>> 40xff)) 0xff);
        key[4(byte) ((((key56[30x0f<< 3
            (((key56[40xff>> 50xff)) 0xff);
        key[5(byte) ((((key56[40x1f<< 2
            (((key56[50xff>> 60xff)) 0xff);
        key[6(byte) ((((key56[50x3f<< 1
            (((key56[60xff>> 70xff)) 0xff);
        key[7(byte) (key56[60x7f);
        
        for (int i = 0; i < key.length; i++) {
            key[i(byte) (key[i<< 1);
        }
        return key;
    }

    /**
     * Encrypt the data.
     @param key The key.
     @param bytes The data
     @return byte[] The encrypted data
     @throws HttpException If {@link Cipher.doFinal(byte[])} fails
     */
    private byte[] encrypt(byte[] key, byte[] bytes)
        throws AuthenticationException {
        Cipher ecipher = getCipher(key);
        try {
            byte[] enc = ecipher.doFinal(bytes);
            return enc;
        catch (IllegalBlockSizeException e) {
            throw new AuthenticationException("Invalid block size for DES encryption.", e);
        catch (BadPaddingException e) {
            throw new AuthenticationException("Data not padded correctly for DES encryption.", e);
        }
    }

    /** 
     * Prepares the object to create a response of the given length.
     @param length the length of the response to prepare.
     */
    private void prepareResponse(int length) {
        currentResponse = new byte[length];
        currentPosition = 0;
    }

    /** 
     * Adds the given byte to the response.
     @param b the byte to add.
     */
    private void addByte(byte b) {
        currentResponse[currentPosition= b;
        currentPosition++;
    }

    /** 
     * Adds the given bytes to the response.
     @param bytes the bytes to add.
     */
    private void addBytes(byte[] bytes) {
        for (int i = 0; i < bytes.length; i++) {
            currentResponse[currentPosition= bytes[i];
            currentPosition++;
        }
    }

    /** 
     * Returns the response that has been generated after shrinking the array if
     * required and base64 encodes the response.
     @return The response as above.
     */
    private String getResponse() {
        byte[] resp;
        if (currentResponse.length > currentPosition) {
            byte[] tmp = new byte[currentPosition];
            for (int i = 0; i < currentPosition; i++) {
                tmp[i= currentResponse[i];
            }
            resp = tmp;
        else {
            resp = currentResponse;
        }
        return EncodingUtil.getAsciiString(Base64.encodeBase64(resp));
    }
    
    /**
     * Creates the first message (type 1 message) in the NTLM authentication sequence.
     * This message includes the user name, domain and host for the authentication session.
     *
     @param host the computer name of the host requesting authentication.
     @param domain The domain to authenticate with.
     @return String the message to add to the HTTP request header.
     */
    public String getType1Message(String host, String domain) {
        host = host.toUpperCase();
        domain = domain.toUpperCase();
        byte[] hostBytes = EncodingUtil.getBytes(host, DEFAULT_CHARSET);
        byte[] domainBytes = EncodingUtil.getBytes(domain, DEFAULT_CHARSET);

        int finalLength = 32 + hostBytes.length + domainBytes.length;
        prepareResponse(finalLength);
        
        // The initial id string.
        byte[] protocol = EncodingUtil.getBytes("NTLMSSP", DEFAULT_CHARSET);
        addBytes(protocol);
        addByte((byte0);

        // Type
        addByte((byte1);
        addByte((byte0);
        addByte((byte0);
        addByte((byte0);

        // Flags
        addByte((byte6);
        addByte((byte82);
        addByte((byte0);
        addByte((byte0);

        // Domain length (first time).
        int iDomLen = domainBytes.length;
        byte[] domLen = convertShort(iDomLen);
        addByte(domLen[0]);
        addByte(domLen[1]);

        // Domain length (second time).
        addByte(domLen[0]);
        addByte(domLen[1]);

        // Domain offset.
        byte[] domOff = convertShort(hostBytes.length + 32);
        addByte(domOff[0]);
        addByte(domOff[1]);
        addByte((byte0);
        addByte((byte0);

        // Host length (first time).
        byte[] hostLen = convertShort(hostBytes.length);
        addByte(hostLen[0]);
        addByte(hostLen[1]);

        // Host length (second time).
        addByte(hostLen[0]);
        addByte(hostLen[1]);

        // Host offset (always 32).
        byte[] hostOff = convertShort(32);
        addByte(hostOff[0]);
        addByte(hostOff[1]);
        addByte((byte0);
        addByte((byte0);

        // Host String.
        addBytes(hostBytes);

        // Domain String.
        addBytes(domainBytes);

        return getResponse();
    }

    /** 
     * Extracts the server nonce out of the given message type 2.
     
     @param message the String containing the base64 encoded message.
     @return an array of 8 bytes that the server sent to be used when
     * hashing the password.
     */
    public byte[] parseType2Message(String message) {
        // Decode the message first.
        byte[] msg = Base64.decodeBase64(EncodingUtil.getBytes(message, DEFAULT_CHARSET));
        byte[] nonce = new byte[8];
        // The nonce is the 8 bytes starting from the byte in position 24.
        for (int i = 0; i < 8; i++) {
            nonce[i= msg[i + 24];
        }
        return nonce;
    }

    /** 
     * Creates the type 3 message using the given server nonce.  The type 3 message includes all the
     * information for authentication, host, domain, username and the result of encrypting the
     * nonce sent by the server using the user's password as the key.
     *
     @param user The user name.  This should not include the domain name.
     @param password The password.
     @param host The host that is originating the authentication request.
     @param domain The domain to authenticate within.
     @param nonce the 8 byte array the server sent.
     @return The type 3 message.
     @throws AuthenticationException If {@encrypt(byte[],byte[])} fails.
     */
    public String getType3Message(String user, String password,
            String host, String domain, byte[] nonce)
    throws AuthenticationException {

        int ntRespLen = 0;
        int lmRespLen = 24;
        domain = domain.toUpperCase();
        host = host.toUpperCase();
        user = user.toUpperCase();
        byte[] domainBytes = EncodingUtil.getBytes(domain, DEFAULT_CHARSET);
        byte[] hostBytes = EncodingUtil.getBytes(host, DEFAULT_CHARSET);
        byte[] userBytes = EncodingUtil.getBytes(user, credentialCharset);
        int domainLen = domainBytes.length;
        int hostLen = hostBytes.length;
        int userLen = userBytes.length;
        int finalLength = 64 + ntRespLen + lmRespLen + domainLen 
            + userLen + hostLen;
        prepareResponse(finalLength);
        byte[] ntlmssp = EncodingUtil.getBytes("NTLMSSP", DEFAULT_CHARSET);
        addBytes(ntlmssp);
        addByte((byte0);
        addByte((byte3);
        addByte((byte0);
        addByte((byte0);
        addByte((byte0);

        // LM Resp Length (twice)
        addBytes(convertShort(24));
        addBytes(convertShort(24));

        // LM Resp Offset
        addBytes(convertShort(finalLength - 24));
        addByte((byte0);
        addByte((byte0);

        // NT Resp Length (twice)
        addBytes(convertShort(0));
        addBytes(convertShort(0));

        // NT Resp Offset
        addBytes(convertShort(finalLength));
        addByte((byte0);
        addByte((byte0);

        // Domain length (twice)
        addBytes(convertShort(domainLen));
        addBytes(convertShort(domainLen));
        
        // Domain offset.
        addBytes(convertShort(64));
        addByte((byte0);
        addByte((byte0);

        // User Length (twice)
        addBytes(convertShort(userLen));
        addBytes(convertShort(userLen));

        // User offset
        addBytes(convertShort(64 + domainLen));
        addByte((byte0);
        addByte((byte0);

        // Host length (twice)
        addBytes(convertShort(hostLen));
        addBytes(convertShort(hostLen));

        // Host offset
        addBytes(convertShort(64 + domainLen + userLen));

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

        // Message length
        addBytes(convertShort(finalLength));
        addByte((byte0);
        addByte((byte0);

        // Flags
        addByte((byte6);
        addByte((byte82);
        addByte((byte0);
        addByte((byte0);

        addBytes(domainBytes);
        addBytes(userBytes);
        addBytes(hostBytes);
        addBytes(hashPassword(password, nonce));
        return getResponse();
    }

    /** 
     * Creates the LANManager and NT response for the given password using the
     * given nonce.
     @param password the password to create a hash for.
     @param nonce the nonce sent by the server.
     @return The response.
     @throws HttpException If {@link #encrypt(byte[],byte[])} fails.
     */
    private byte[] hashPassword(String password, byte[] nonce)
        throws AuthenticationException {
        byte[] passw = EncodingUtil.getBytes(password.toUpperCase(), credentialCharset);
        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(byte0;
        }

        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(byte0;
        }

        // Create LanManager hashed Password
        byte[] magic = {
            (byte0x4B(byte0x47(byte0x53(byte0x21
            (byte0x40(byte0x23(byte0x24(byte0x25
        };

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

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

        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(byte0;
        }

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

        return lmResp;
    }

    /** 
     * Takes a 21 byte array and treats it as 3 56-bit DES keys.  The 8 byte
     * plaintext is encrypted with each key and the resulting 24 bytes are
     * stored in the results array.
     
     @param keys The keys.
     @param plaintext The plain text to encrypt.
     @param results Where the results are stored.
     @throws AuthenticationException If {@link #encrypt(byte[],byte[])} fails.
     */
    private void calcResp(byte[] keys, byte[] plaintext, byte[] results)
        throws AuthenticationException {
        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, plaintext);

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

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

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

    /** 
     * Converts a given number to a two byte array in little endian order.
     @param num the number to convert.
     @return The byte representation of <i>num</i> in little endian order.
     */
    private byte[] convertShort(int num) {
        byte[] val = new byte[2];
        String hex = Integer.toString(num, 16);
        while (hex.length() 4) {
            hex = "0" + hex;
        }
        String low = hex.substring(24);
        String high = hex.substring(02);

        val[0(byteInteger.parseInt(low, 16);
        val[1(byteInteger.parseInt(high, 16);
        return val;
    }
    
    /**
     @return Returns the credentialCharset.
     */
    public String getCredentialCharset() {
        return credentialCharset;
    }

    /**
     @param credentialCharset The credentialCharset to set.
     */
    public void setCredentialCharset(String credentialCharset) {
        this.credentialCharset = credentialCharset;
    }

}