View Javadoc

1   /*
2    *  Licensed to the Apache Software Foundation (ASF) under one
3    *  or more contributor license agreements.  See the NOTICE file
4    *  distributed with this work for additional information
5    *  regarding copyright ownership.  The ASF licenses this file
6    *  to you under the Apache License, Version 2.0 (the
7    *  "License"); you may not use this file except in compliance
8    *  with the License.  You may obtain a copy of the License at
9    *  
10   *    http://www.apache.org/licenses/LICENSE-2.0
11   *  
12   *  Unless required by applicable law or agreed to in writing,
13   *  software distributed under the License is distributed on an
14   *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   *  KIND, either express or implied.  See the License for the
16   *  specific language governing permissions and limitations
17   *  under the License. 
18   *  
19   */
20  package org.apache.directory.server.changepw.service;
21  
22  import java.io.IOException;
23  import java.io.UnsupportedEncodingException;
24  import java.net.InetAddress;
25  import java.net.UnknownHostException;
26  
27  import javax.naming.NamingException;
28  import javax.security.auth.kerberos.KerberosPrincipal;
29  
30  import org.apache.directory.server.changepw.ChangePasswordServer;
31  import org.apache.directory.server.changepw.exceptions.ChangePasswordException;
32  import org.apache.directory.server.changepw.exceptions.ErrorType;
33  import org.apache.directory.server.changepw.io.ChangePasswordDataDecoder;
34  import org.apache.directory.server.changepw.messages.ChangePasswordReply;
35  import org.apache.directory.server.changepw.messages.ChangePasswordReplyModifier;
36  import org.apache.directory.server.changepw.messages.ChangePasswordRequest;
37  import org.apache.directory.server.changepw.value.ChangePasswordData;
38  import org.apache.directory.server.changepw.value.ChangePasswordDataModifier;
39  import org.apache.directory.server.kerberos.shared.KerberosUtils;
40  import org.apache.directory.server.kerberos.shared.crypto.encryption.CipherTextHandler;
41  import org.apache.directory.server.kerberos.shared.crypto.encryption.EncryptionType;
42  import org.apache.directory.server.kerberos.shared.crypto.encryption.KeyUsage;
43  import org.apache.directory.server.kerberos.shared.exceptions.KerberosException;
44  //import org.apache.directory.server.kerberos.shared.exceptions.ErrorType;
45  import org.apache.directory.server.kerberos.shared.messages.ApplicationRequest;
46  import org.apache.directory.server.kerberos.shared.messages.application.ApplicationReply;
47  import org.apache.directory.server.kerberos.shared.messages.application.PrivateMessage;
48  import org.apache.directory.server.kerberos.shared.messages.components.Authenticator;
49  import org.apache.directory.server.kerberos.shared.messages.components.EncApRepPart;
50  import org.apache.directory.server.kerberos.shared.messages.components.EncApRepPartModifier;
51  import org.apache.directory.server.kerberos.shared.messages.components.EncKrbPrivPart;
52  import org.apache.directory.server.kerberos.shared.messages.components.EncKrbPrivPartModifier;
53  import org.apache.directory.server.kerberos.shared.messages.components.Ticket;
54  import org.apache.directory.server.kerberos.shared.messages.value.EncryptedData;
55  import org.apache.directory.server.kerberos.shared.messages.value.EncryptionKey;
56  import org.apache.directory.server.kerberos.shared.messages.value.HostAddress;
57  import org.apache.directory.server.kerberos.shared.messages.value.HostAddresses;
58  import org.apache.directory.server.kerberos.shared.replay.InMemoryReplayCache;
59  import org.apache.directory.server.kerberos.shared.replay.ReplayCache;
60  import org.apache.directory.server.kerberos.shared.store.PrincipalStore;
61  import org.apache.directory.server.kerberos.shared.store.PrincipalStoreEntry;
62  import org.apache.mina.common.IoSession;
63  import org.slf4j.Logger;
64  import org.slf4j.LoggerFactory;
65  
66  public class ChangePasswordService
67  {
68      /** the logger for this class */
69      private static final Logger LOG = LoggerFactory.getLogger( ChangePasswordService.class );
70  
71      private static final ReplayCache replayCache = new InMemoryReplayCache();
72      
73      private static final CipherTextHandler cipherTextHandler = new CipherTextHandler();
74  
75      
76      public static void execute( IoSession session, ChangePasswordContext changepwContext ) throws KerberosException, IOException
77      {
78          if ( LOG.isDebugEnabled() )
79          {
80              monitorRequest( changepwContext );
81          }
82          
83          configureChangePassword( changepwContext );
84          getAuthHeader( session, changepwContext );
85          verifyServiceTicket( changepwContext );
86          getServerEntry( changepwContext );
87          verifyServiceTicketAuthHeader( changepwContext );
88          extractPassword( changepwContext );
89          
90          if ( LOG.isDebugEnabled() )
91          {
92              monitorContext( changepwContext );
93          }
94          
95          processPasswordChange( changepwContext );
96          buildReply( changepwContext );
97          
98          if ( LOG.isDebugEnabled() )
99          {
100             monitorReply( changepwContext );
101         }
102     }
103     
104     
105     private static void processPasswordChange( ChangePasswordContext changepwContext ) throws KerberosException
106     {
107         PrincipalStore store = changepwContext.getStore();
108         Authenticator authenticator = changepwContext.getAuthenticator();
109         String newPassword = changepwContext.getPassword();
110         KerberosPrincipal clientPrincipal = authenticator.getClientPrincipal();
111 
112         // usec and seq-number must be present per MS but aren't in legacy kpasswd
113         // seq-number must have same value as authenticator
114         // ignore r-address
115 
116         try
117         {
118             String principalName = store.changePassword( clientPrincipal, newPassword );
119             LOG.debug( "Successfully modified principal {}.", principalName );
120         }
121         catch ( NamingException ne )
122         {
123             throw new ChangePasswordException( ErrorType.KRB5_KPASSWD_SOFTERROR, ne.getExplanation().getBytes(), ne );
124         }
125         catch ( Exception e )
126         {
127             throw new ChangePasswordException( ErrorType.KRB5_KPASSWD_HARDERROR, e );
128         }
129     }
130     
131     
132     private static void monitorRequest( ChangePasswordContext changepwContext ) throws KerberosException
133     {
134         try
135         {
136             ChangePasswordRequest request = ( ChangePasswordRequest ) changepwContext.getRequest();
137             short versionNumber = request.getVersionNumber();
138 
139             StringBuffer sb = new StringBuffer();
140             sb.append( "Responding to change password request:" );
141             sb.append( "\n\t" + "versionNumber    " + versionNumber );
142 
143             LOG.debug( sb.toString() );
144         }
145         catch ( Exception e )
146         {
147             // This is a monitor.  No exceptions should bubble up.
148             LOG.error( "Error in request monitor", e );
149         }
150     }
151     
152     
153     private static void configureChangePassword( ChangePasswordContext changepwContext )
154     {
155         changepwContext.setReplayCache( replayCache );
156         changepwContext.setCipherTextHandler( cipherTextHandler );
157     }
158     
159     
160     private static void getAuthHeader( IoSession session, ChangePasswordContext changepwContext ) throws KerberosException
161     {
162         ChangePasswordRequest request = ( ChangePasswordRequest ) changepwContext.getRequest();
163 
164         if ( request.getVersionNumber() != 1 )
165         {
166             throw new ChangePasswordException( ErrorType.KRB5_KPASSWD_BAD_VERSION );
167         }
168 
169         if ( request.getAuthHeader() == null || request.getAuthHeader().getTicket() == null )
170         {
171             throw new ChangePasswordException( ErrorType.KRB5_KPASSWD_AUTHERROR );
172         }
173 
174         ApplicationRequest authHeader = request.getAuthHeader();
175         Ticket ticket = authHeader.getTicket();
176 
177         changepwContext.setAuthHeader( authHeader );
178         changepwContext.setTicket( ticket );
179     }
180     
181     
182     private static void verifyServiceTicket( ChangePasswordContext changepwContext ) throws KerberosException
183     {
184         ChangePasswordServer config = changepwContext.getConfig();
185         Ticket ticket = changepwContext.getTicket();
186         String primaryRealm = config.getPrimaryRealm();
187         KerberosPrincipal changepwPrincipal = config.getServicePrincipal();
188         KerberosPrincipal serverPrincipal = ticket.getServerPrincipal(); 
189 
190         if ( !ticket.getRealm().equals( primaryRealm ) || !serverPrincipal.equals( changepwPrincipal ) )
191         {
192             throw new KerberosException( org.apache.directory.server.kerberos.shared.exceptions.ErrorType.KRB_AP_ERR_NOT_US );
193         }
194     }
195     
196     
197     private static void getServerEntry( ChangePasswordContext changepwContext ) throws KerberosException
198     {
199         KerberosPrincipal principal =  changepwContext.getTicket().getServerPrincipal();
200         PrincipalStore store = changepwContext.getStore();
201 
202         changepwContext.setServerEntry( KerberosUtils.getEntry( principal, store, org.apache.directory.server.kerberos.shared.exceptions.ErrorType.KDC_ERR_S_PRINCIPAL_UNKNOWN ) );
203     }
204     
205     
206     private static void verifyServiceTicketAuthHeader( ChangePasswordContext changepwContext ) throws KerberosException
207     {
208         ApplicationRequest authHeader = changepwContext.getAuthHeader();
209         Ticket ticket = changepwContext.getTicket();
210 
211         EncryptionType encryptionType = ticket.getEncPart().getEType();
212         EncryptionKey serverKey = changepwContext.getServerEntry().getKeyMap().get( encryptionType );
213 
214         long clockSkew = changepwContext.getConfig().getAllowableClockSkew();
215         ReplayCache replayCache = changepwContext.getReplayCache();
216         boolean emptyAddressesAllowed = changepwContext.getConfig().isEmptyAddressesAllowed();
217         InetAddress clientAddress = changepwContext.getClientAddress();
218         CipherTextHandler cipherTextHandler = changepwContext.getCipherTextHandler();
219 
220         Authenticator authenticator = KerberosUtils.verifyAuthHeader( authHeader, ticket, serverKey, clockSkew, replayCache,
221             emptyAddressesAllowed, clientAddress, cipherTextHandler, KeyUsage.NUMBER11, false );
222 
223         ChangePasswordRequest request = ( ChangePasswordRequest ) changepwContext.getRequest();
224 
225         if ( request.getVersionNumber() == 1 && !ticket.getEncTicketPart().getFlags().isInitial() )
226         {
227             throw new ChangePasswordException( ErrorType.KRB5_KPASSWD_INITIAL_FLAG_NEEDED );
228         }
229 
230         changepwContext.setAuthenticator( authenticator );
231     }
232     
233     
234     private static void extractPassword( ChangePasswordContext changepwContext ) throws KerberosException, IOException
235     {
236         ChangePasswordRequest request = ( ChangePasswordRequest ) changepwContext.getRequest();
237         Authenticator authenticator = changepwContext.getAuthenticator();
238         CipherTextHandler cipherTextHandler = changepwContext.getCipherTextHandler();
239 
240         // TODO - check ticket is for service authorized to change passwords
241         // ticket.getServerPrincipal().getName().equals(config.getChangepwPrincipal().getName()));
242 
243         // TODO - check client principal in ticket is authorized to change password
244 
245         // get the subsession key from the Authenticator
246         EncryptionKey subSessionKey = authenticator.getSubSessionKey();
247 
248         // decrypt the request's private message with the subsession key
249         EncryptedData encReqPrivPart = request.getPrivateMessage().getEncryptedPart();
250 
251         EncKrbPrivPart privatePart;
252 
253         try
254         {
255             privatePart = ( EncKrbPrivPart ) cipherTextHandler.unseal( EncKrbPrivPart.class, subSessionKey,
256                 encReqPrivPart, KeyUsage.NUMBER13 );
257         }
258         catch ( KerberosException ke )
259         {
260             throw new ChangePasswordException( ErrorType.KRB5_KPASSWD_SOFTERROR, ke );
261         }
262 
263         ChangePasswordData passwordData = null;
264 
265         if ( request.getVersionNumber() == ( short ) 1 )
266         {
267             // Use protocol version 0x0001, the legacy Kerberos change password protocol
268             ChangePasswordDataModifier modifier = new ChangePasswordDataModifier();
269             modifier.setNewPassword( privatePart.getUserData() );
270             passwordData = modifier.getChangePasswdData();
271         }
272         else
273         {
274             // Use protocol version 0xFF80, the backwards-compatible MS protocol
275             ChangePasswordDataDecoder passwordDecoder = new ChangePasswordDataDecoder();
276             passwordData = passwordDecoder.decodeChangePasswordData( privatePart.getUserData() );
277         }
278 
279         try
280         {
281             changepwContext.setPassword( new String( passwordData.getPassword(), "UTF-8" ) );
282         }
283         catch ( UnsupportedEncodingException uee )
284         {
285             throw new ChangePasswordException( ErrorType.KRB5_KPASSWD_SOFTERROR, uee );
286         }
287     }
288 
289     
290     private static void monitorContext( ChangePasswordContext changepwContext ) throws KerberosException
291     {
292         try
293         {
294             PrincipalStore store = changepwContext.getStore();
295             ApplicationRequest authHeader = changepwContext.getAuthHeader();
296             Ticket ticket = changepwContext.getTicket();
297             ReplayCache replayCache = changepwContext.getReplayCache();
298             long clockSkew = changepwContext.getConfig().getAllowableClockSkew();
299 
300             Authenticator authenticator = changepwContext.getAuthenticator();
301             KerberosPrincipal clientPrincipal = authenticator.getClientPrincipal();
302             String desiredPassword = changepwContext.getPassword();
303 
304             InetAddress clientAddress = changepwContext.getClientAddress();
305             HostAddresses clientAddresses = ticket.getEncTicketPart().getClientAddresses();
306 
307             boolean caddrContainsSender = false;
308 
309             if ( ticket.getEncTicketPart().getClientAddresses() != null )
310             {
311                 caddrContainsSender = ticket.getEncTicketPart().getClientAddresses().contains( new HostAddress( clientAddress ) );
312             }
313 
314             StringBuffer sb = new StringBuffer();
315             sb.append( "Monitoring context:" );
316             sb.append( "\n\t" + "store                  " + store );
317             sb.append( "\n\t" + "authHeader             " + authHeader );
318             sb.append( "\n\t" + "ticket                 " + ticket );
319             sb.append( "\n\t" + "replayCache            " + replayCache );
320             sb.append( "\n\t" + "clockSkew              " + clockSkew );
321             sb.append( "\n\t" + "clientPrincipal        " + clientPrincipal );
322             sb.append( "\n\t" + "desiredPassword        " + desiredPassword );
323             sb.append( "\n\t" + "clientAddress          " + clientAddress );
324             sb.append( "\n\t" + "clientAddresses        " + clientAddresses );
325             sb.append( "\n\t" + "caddr contains sender  " + caddrContainsSender );
326             sb.append( "\n\t" + "Ticket principal       " + ticket.getServerPrincipal() );
327 
328             PrincipalStoreEntry ticketPrincipal = changepwContext.getServerEntry();
329             
330             sb.append( "\n\t" + "cn                     " + ticketPrincipal.getCommonName() );
331             sb.append( "\n\t" + "realm                  " + ticketPrincipal.getRealmName() );
332             sb.append( "\n\t" + "Service principal      " + ticketPrincipal.getPrincipal() );
333             sb.append( "\n\t" + "SAM type               " + ticketPrincipal.getSamType() );
334 
335             EncryptionType encryptionType = ticket.getEncPart().getEType();
336             int keyVersion = ticketPrincipal.getKeyMap().get( encryptionType ).getKeyVersion();
337             sb.append( "\n\t" + "Ticket key type        " + encryptionType );
338             sb.append( "\n\t" + "Service key version    " + keyVersion );
339 
340             LOG.debug( sb.toString() );
341         }
342         catch ( Exception e )
343         {
344             // This is a monitor.  No exceptions should bubble up.
345             LOG.error( "Error in context monitor", e );
346         }
347     }
348     
349     
350     private static void buildReply( ChangePasswordContext changepwContext ) throws KerberosException, UnknownHostException
351     {
352         Authenticator authenticator = changepwContext.getAuthenticator();
353         Ticket ticket = changepwContext.getTicket();
354         CipherTextHandler cipherTextHandler = changepwContext.getCipherTextHandler();
355 
356         // begin building reply
357 
358         // create priv message
359         // user-data component is short result code
360         EncKrbPrivPartModifier modifier = new EncKrbPrivPartModifier();
361         byte[] resultCode =
362             { ( byte ) 0x00, ( byte ) 0x00 };
363         modifier.setUserData( resultCode );
364 
365         modifier.setSenderAddress( new HostAddress( InetAddress.getLocalHost() ) );
366         EncKrbPrivPart privPart = modifier.getEncKrbPrivPart();
367 
368         // get the subsession key from the Authenticator
369         EncryptionKey subSessionKey = authenticator.getSubSessionKey();
370 
371         EncryptedData encPrivPart;
372 
373         try
374         {
375             encPrivPart = cipherTextHandler.seal( subSessionKey, privPart, KeyUsage.NUMBER13 );
376         }
377         catch ( KerberosException ke )
378         {
379             throw new ChangePasswordException( ErrorType.KRB5_KPASSWD_SOFTERROR, ke );
380         }
381 
382         PrivateMessage privateMessage = new PrivateMessage( encPrivPart );
383 
384         // Begin AP_REP generation
385         EncApRepPartModifier encApModifier = new EncApRepPartModifier();
386         encApModifier.setClientTime( authenticator.getClientTime() );
387         encApModifier.setClientMicroSecond( authenticator.getClientMicroSecond() );
388         encApModifier.setSequenceNumber( new Integer( authenticator.getSequenceNumber() ) );
389         encApModifier.setSubSessionKey( authenticator.getSubSessionKey() );
390 
391         EncApRepPart repPart = encApModifier.getEncApRepPart();
392 
393         EncryptedData encRepPart;
394 
395         try
396         {
397             encRepPart = cipherTextHandler.seal( ticket.getEncTicketPart().getSessionKey(), repPart, KeyUsage.NUMBER12 );
398         }
399         catch ( KerberosException ke )
400         {
401             throw new ChangePasswordException( ErrorType.KRB5_KPASSWD_SOFTERROR, ke );
402         }
403 
404         ApplicationReply appReply = new ApplicationReply( encRepPart );
405 
406         // return status message value object
407         ChangePasswordReplyModifier replyModifier = new ChangePasswordReplyModifier();
408         replyModifier.setApplicationReply( appReply );
409         replyModifier.setPrivateMessage( privateMessage );
410 
411         changepwContext.setReply( replyModifier.getChangePasswordReply() );
412     }
413 
414     
415     private static void monitorReply( ChangePasswordContext changepwContext ) throws KerberosException
416     {
417         try
418         {
419             ChangePasswordReply reply = ( ChangePasswordReply ) changepwContext.getReply();
420             ApplicationReply appReply = reply.getApplicationReply();
421             PrivateMessage priv = reply.getPrivateMessage();
422 
423             StringBuilder sb = new StringBuilder();
424             sb.append( "Responding with change password reply:" );
425             sb.append( "\n\t" + "appReply               " + appReply );
426             sb.append( "\n\t" + "priv                   " + priv );
427 
428             LOG.debug( sb.toString() );
429         }
430         catch ( Exception e )
431         {
432             // This is a monitor.  No exceptions should bubble up.
433             LOG.error( "Error in reply monitor", e );
434         }
435     }
436 }