| /* |
| * 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. |
| */ |
| |
| package org.apache.ahc.auth; |
| |
| import java.security.NoSuchAlgorithmException; |
| import java.security.InvalidKeyException; |
| import javax.crypto.Cipher; |
| import javax.crypto.NoSuchPaddingException; |
| import javax.crypto.BadPaddingException; |
| import javax.crypto.IllegalBlockSizeException; |
| import javax.crypto.spec.SecretKeySpec; |
| |
| import org.apache.commons.codec.binary.Base64; |
| import org.apache.ahc.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:adrian@ephox.com">Adrian Sutton</a> |
| * @author <a href="mailto:jsdever@apache.org">Jeff Dever</a> |
| * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a> |
| */ |
| 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 AuthenticationException 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().isEmpty()) { |
| 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[] key) throws 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] >> 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; |
| } |
| |
| /** |
| * Encrypt the data. |
| * @param key The key. |
| * @param bytes The data |
| * @return byte[] The encrypted data |
| * @throws AuthenticationException If {@link javax.crypto.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((byte) 0); |
| |
| // Type |
| addByte((byte) 1); |
| addByte((byte) 0); |
| addByte((byte) 0); |
| addByte((byte) 0); |
| |
| // Flags |
| addByte((byte) 6); |
| addByte((byte) 82); |
| addByte((byte) 0); |
| addByte((byte) 0); |
| |
| // 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((byte) 0); |
| addByte((byte) 0); |
| |
| // 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((byte) 0); |
| addByte((byte) 0); |
| |
| // 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((byte) 0); |
| addByte((byte) 3); |
| addByte((byte) 0); |
| addByte((byte) 0); |
| addByte((byte) 0); |
| |
| // LM Resp Length (twice) |
| addBytes(convertShort(24)); |
| addBytes(convertShort(24)); |
| |
| // LM Resp Offset |
| addBytes(convertShort(finalLength - 24)); |
| addByte((byte) 0); |
| addByte((byte) 0); |
| |
| // NT Resp Length (twice) |
| addBytes(convertShort(0)); |
| addBytes(convertShort(0)); |
| |
| // NT Resp Offset |
| addBytes(convertShort(finalLength)); |
| addByte((byte) 0); |
| addByte((byte) 0); |
| |
| // Domain length (twice) |
| addBytes(convertShort(domainLen)); |
| addBytes(convertShort(domainLen)); |
| |
| // Domain offset. |
| addBytes(convertShort(64)); |
| addByte((byte) 0); |
| addByte((byte) 0); |
| |
| // User Length (twice) |
| addBytes(convertShort(userLen)); |
| addBytes(convertShort(userLen)); |
| |
| // User offset |
| addBytes(convertShort(64 + domainLen)); |
| addByte((byte) 0); |
| addByte((byte) 0); |
| |
| // Host length (twice) |
| addBytes(convertShort(hostLen)); |
| addBytes(convertShort(hostLen)); |
| |
| // Host offset |
| addBytes(convertShort(64 + domainLen + userLen)); |
| |
| for (int i = 0; i < 6; i++) { |
| addByte((byte) 0); |
| } |
| |
| // Message length |
| addBytes(convertShort(finalLength)); |
| addByte((byte) 0); |
| addByte((byte) 0); |
| |
| // Flags |
| addByte((byte) 6); |
| addByte((byte) 82); |
| addByte((byte) 0); |
| addByte((byte) 0); |
| |
| 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 AuthenticationException 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] = (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; |
| } |
| |
| // Create LanManager hashed Password |
| byte[] magic = { |
| (byte) 0x4B, (byte) 0x47, (byte) 0x53, (byte) 0x21, |
| (byte) 0x40, (byte) 0x23, (byte) 0x24, (byte) 0x25 |
| }; |
| |
| 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] = (byte) 0; |
| } |
| |
| // 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(2, 4); |
| String high = hex.substring(0, 2); |
| |
| val[0] = (byte) Integer.parseInt(low, 16); |
| val[1] = (byte) Integer.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; |
| } |
| |
| } |