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