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.kerberos.changepwd.service;
21  
22  import java.net.InetAddress;
23  import java.nio.ByteBuffer;
24  
25  import javax.security.auth.kerberos.KerberosPrincipal;
26  
27  import org.apache.directory.api.asn1.ber.Asn1Decoder;
28  import org.apache.directory.api.util.Network;
29  import org.apache.directory.api.util.Strings;
30  import org.apache.directory.server.i18n.I18n;
31  import org.apache.directory.server.kerberos.ChangePasswordConfig;
32  import org.apache.directory.server.kerberos.changepwd.exceptions.ChangePasswdErrorType;
33  import org.apache.directory.server.kerberos.changepwd.exceptions.ChangePasswordException;
34  import org.apache.directory.server.kerberos.changepwd.messages.AbstractPasswordMessage;
35  import org.apache.directory.server.kerberos.changepwd.messages.ChangePasswordReply;
36  import org.apache.directory.server.kerberos.changepwd.messages.ChangePasswordRequest;
37  import org.apache.directory.server.kerberos.shared.crypto.encryption.CipherTextHandler;
38  import org.apache.directory.server.kerberos.shared.crypto.encryption.KeyUsage;
39  import org.apache.directory.server.kerberos.shared.replay.ReplayCache;
40  import org.apache.directory.server.kerberos.shared.store.PrincipalStore;
41  import org.apache.directory.server.kerberos.shared.store.PrincipalStoreEntry;
42  import org.apache.directory.shared.kerberos.KerberosUtils;
43  import org.apache.directory.shared.kerberos.codec.KerberosDecoder;
44  import org.apache.directory.shared.kerberos.codec.changePwdData.ChangePasswdDataContainer;
45  import org.apache.directory.shared.kerberos.codec.types.EncryptionType;
46  import org.apache.directory.shared.kerberos.codec.types.PrincipalNameType;
47  import org.apache.directory.shared.kerberos.components.EncKrbPrivPart;
48  import org.apache.directory.shared.kerberos.components.EncryptedData;
49  import org.apache.directory.shared.kerberos.components.EncryptionKey;
50  import org.apache.directory.shared.kerberos.components.HostAddress;
51  import org.apache.directory.shared.kerberos.components.HostAddresses;
52  import org.apache.directory.shared.kerberos.components.PrincipalName;
53  import org.apache.directory.shared.kerberos.exceptions.ErrorType;
54  import org.apache.directory.shared.kerberos.exceptions.KerberosException;
55  import org.apache.directory.shared.kerberos.messages.ApRep;
56  import org.apache.directory.shared.kerberos.messages.ApReq;
57  import org.apache.directory.shared.kerberos.messages.Authenticator;
58  import org.apache.directory.shared.kerberos.messages.ChangePasswdData;
59  import org.apache.directory.shared.kerberos.messages.EncApRepPart;
60  import org.apache.directory.shared.kerberos.messages.KrbPriv;
61  import org.apache.directory.shared.kerberos.messages.Ticket;
62  import org.apache.mina.core.session.IoSession;
63  import org.slf4j.Logger;
64  import org.slf4j.LoggerFactory;
65  
66  public final class ChangePasswordService
67  {
68      /** the logger for this class */
69      private static final Logger LOG = LoggerFactory.getLogger( ChangePasswordService.class );
70  
71      private static final CipherTextHandler/crypto/encryption/CipherTextHandler.html#CipherTextHandler">CipherTextHandler CIPHER_TEXT_HANDLER = new CipherTextHandler();
72  
73  
74      private ChangePasswordService()
75      {
76      }
77  
78  
79      public static void execute( IoSession session, ChangePasswordContext changepwContext ) throws Exception
80      {
81          if ( LOG.isDebugEnabled() )
82          {
83              monitorRequest( changepwContext );
84          }
85          
86          configureChangePassword( changepwContext );
87          getAuthHeader( changepwContext );
88          verifyServiceTicket( changepwContext );
89          getServerEntry( changepwContext );
90          verifyServiceTicketAuthHeader( changepwContext );
91          extractPassword( changepwContext );
92          
93          if ( LOG.isDebugEnabled() )
94          {
95              monitorContext( changepwContext );
96          }
97          
98          processPasswordChange( changepwContext );
99          buildReply( changepwContext );
100         
101         if ( LOG.isDebugEnabled() )
102         {
103             monitorReply( changepwContext );
104         }
105     }
106     
107     
108     private static void processPasswordChange( ChangePasswordContext changepwContext ) throws KerberosException
109     {
110         PrincipalStore store = changepwContext.getStore();
111         Authenticator authenticator = changepwContext.getAuthenticator();
112         String newPassword = Strings.utf8ToString( changepwContext.getPasswordData().getNewPasswd() );
113         KerberosPrincipal byPrincipal = KerberosUtils.getKerberosPrincipal( 
114             authenticator.getCName(),
115             authenticator.getCRealm() );
116 
117         KerberosPrincipal targetPrincipal = null;
118 
119         PrincipalName targName = changepwContext.getPasswordData().getTargName();
120         
121         if ( targName != null )
122         {
123             targetPrincipal = new KerberosPrincipal( targName.getNameString(), PrincipalNameType.KRB_NT_PRINCIPAL.getValue() );
124         }
125         else
126         {
127             targetPrincipal = byPrincipal;
128         }
129         
130         // usec and seq-number must be present per MS but aren't in legacy kpasswd
131         // seq-number must have same value as authenticator
132         // ignore r-address
133 
134         store.changePassword( byPrincipal, targetPrincipal, newPassword, changepwContext.getTicket().getEncTicketPart().getFlags().isInitial() );
135         LOG.debug( "Successfully modified password for {} BY {}.", targetPrincipal, byPrincipal );
136     }
137     
138     
139     private static void monitorRequest( ChangePasswordContext changepwContext )
140     {
141         try
142         {
143             ChangePasswordRequest/../org/apache/directory/server/kerberos/changepwd/messages/ChangePasswordRequest.html#ChangePasswordRequest">ChangePasswordRequest request = ( ChangePasswordRequest ) changepwContext.getRequest();
144             short versionNumber = request.getVersionNumber();
145 
146             if ( LOG.isDebugEnabled() )
147             {
148                 LOG.debug( "Responding to change password request:\\n\\tversionNumber    {}", versionNumber );
149             }
150         }
151         catch ( Exception e )
152         {
153             // This is a monitor.  No exceptions should bubble up.
154             LOG.error( I18n.err( I18n.ERR_152 ), e );
155         }
156     }
157     
158     
159     private static void configureChangePassword( ChangePasswordContext changepwContext )
160     {
161         changepwContext.setCipherTextHandler( CIPHER_TEXT_HANDLER );
162     }
163     
164     
165     private static void getAuthHeader( ChangePasswordContext changepwContext ) throws KerberosException
166     {
167         ChangePasswordRequest/../org/apache/directory/server/kerberos/changepwd/messages/ChangePasswordRequest.html#ChangePasswordRequest">ChangePasswordRequest request = ( ChangePasswordRequest ) changepwContext.getRequest();
168 
169         short pvno = request.getVersionNumber();
170         
171         if ( ( pvno != AbstractPasswordMessage.PVNO ) && ( pvno != AbstractPasswordMessage.OLD_PVNO ) )
172         {
173             throw new ChangePasswordException( ChangePasswdErrorType.KRB5_KPASSWD_BAD_VERSION );
174         }
175 
176         if ( request.getAuthHeader() == null || request.getAuthHeader().getTicket() == null )
177         {
178             throw new ChangePasswordException( ChangePasswdErrorType.KRB5_KPASSWD_AUTHERROR );
179         }
180 
181         ApReq authHeader = request.getAuthHeader();
182         Ticket ticket = authHeader.getTicket();
183 
184         changepwContext.setAuthHeader( authHeader );
185         changepwContext.setTicket( ticket );
186     }
187     
188     
189     private static void verifyServiceTicket( ChangePasswordContext changepwContext ) throws KerberosException
190     {
191         ChangePasswordConfig config = changepwContext.getConfig();
192         Ticket ticket = changepwContext.getTicket();
193         String primaryRealm = config.getPrimaryRealm();
194         KerberosPrincipal changepwPrincipal = config.getServicePrincipal();
195         KerberosPrincipal serverPrincipal = KerberosUtils.getKerberosPrincipal( ticket.getSName(), ticket.getRealm() ); 
196 
197         // for some reason kpassword is setting the pricnipaltype value as 1 for ticket.getSName()
198         // hence changing to string based comparison for server and changepw principals
199         // instead of serverPrincipal.equals( changepwPrincipal )
200         if ( !ticket.getRealm().equals( primaryRealm ) || !serverPrincipal.getName().equals( changepwPrincipal.getName() ) )
201         {
202             throw new KerberosException( org.apache.directory.shared.kerberos.exceptions.ErrorType.KRB_AP_ERR_NOT_US );
203         }
204     }
205     
206     
207     private static void getServerEntry( ChangePasswordContext changepwContext ) throws KerberosException
208     {
209         Ticket ticket = changepwContext.getTicket();
210         KerberosPrincipal principal =  KerberosUtils.getKerberosPrincipal( ticket.getSName(), ticket.getRealm() );
211         PrincipalStore store = changepwContext.getStore();
212 
213         changepwContext.setServerEntry( KerberosUtils.getEntry( principal, store, ErrorType.KDC_ERR_S_PRINCIPAL_UNKNOWN ) );
214     }
215     
216     
217     private static void verifyServiceTicketAuthHeader( ChangePasswordContext changepwContext ) throws KerberosException
218     {
219         ApReq authHeader = changepwContext.getAuthHeader();
220         Ticket ticket = changepwContext.getTicket();
221 
222         EncryptionType encryptionType = ticket.getEncPart().getEType();
223         EncryptionKey serverKey = changepwContext.getServerEntry().getKeyMap().get( encryptionType );
224 
225         long clockSkew = changepwContext.getConfig().getAllowableClockSkew();
226         ReplayCache replayCache = changepwContext.getReplayCache();
227         boolean emptyAddressesAllowed = changepwContext.getConfig().isEmptyAddressesAllowed();
228         InetAddress clientAddress = changepwContext.getClientAddress();
229         CipherTextHandler cipherTextHandler = changepwContext.getCipherTextHandler();
230 
231         Authenticator authenticator = KerberosUtils.verifyAuthHeader( authHeader, ticket, serverKey, clockSkew, replayCache,
232             emptyAddressesAllowed, clientAddress, cipherTextHandler, KeyUsage.AP_REQ_AUTHNT_SESS_KEY, false );
233 
234         changepwContext.setAuthenticator( authenticator );
235     }
236     
237     
238     private static void extractPassword( ChangePasswordContext changepwContext ) throws Exception
239     {
240         ChangePasswordRequest/../org/apache/directory/server/kerberos/changepwd/messages/ChangePasswordRequest.html#ChangePasswordRequest">ChangePasswordRequest request = ( ChangePasswordRequest ) changepwContext.getRequest();
241         Authenticator authenticator = changepwContext.getAuthenticator();
242         CipherTextHandler cipherTextHandler = changepwContext.getCipherTextHandler();
243 
244         // get the subsession key from the Authenticator
245         EncryptionKey subSessionKey = authenticator.getSubKey();
246 
247         // decrypt the request's private message with the subsession key
248         EncryptedData encReqPrivPart = request.getPrivateMessage().getEncPart();
249 
250         ChangePasswdData passwordData = null;
251         
252         try
253         {
254             byte[] decryptedData = cipherTextHandler.decrypt( subSessionKey, encReqPrivPart, KeyUsage.KRB_PRIV_ENC_PART_CHOSEN_KEY );
255             EncKrbPrivPart privatePart = KerberosDecoder.decodeEncKrbPrivPart( decryptedData );
256 
257             if ( ( authenticator.getSeqNumber() != null ) && ( authenticator.getSeqNumber() != privatePart.getSeqNumber() ) )
258             {
259                 throw new ChangePasswordException( ChangePasswdErrorType.KRB5_KPASSWD_MALFORMED );    
260             }
261             
262             if ( request.getVersionNumber() == AbstractPasswordMessage.OLD_PVNO )
263             {
264                 passwordData = new ChangePasswdData();
265                 passwordData.setNewPasswd( privatePart.getUserData() );
266             }
267             else
268             {
269                 ByteBuffer stream = ByteBuffer.wrap( privatePart.getUserData() );
270                 ChangePasswdDataContainerc/changePwdData/ChangePasswdDataContainer.html#ChangePasswdDataContainer">ChangePasswdDataContainer container = new ChangePasswdDataContainer( stream );
271                 Asn1Decoder.decode( stream, container );
272                 passwordData = container.getChngPwdData();
273             }
274         }
275         catch ( KerberosException ke )
276         {
277             throw new ChangePasswordException( ChangePasswdErrorType.KRB5_KPASSWD_SOFTERROR, ke );
278         }
279 
280         changepwContext.setChngPwdData( passwordData );
281     }
282 
283     
284     private static void monitorContext( ChangePasswordContext changepwContext )
285     {
286         try
287         {
288             PrincipalStore store = changepwContext.getStore();
289             ApReq authHeader = changepwContext.getAuthHeader();
290             Ticket ticket = changepwContext.getTicket();
291             ReplayCache replayCache = changepwContext.getReplayCache();
292             long clockSkew = changepwContext.getConfig().getAllowableClockSkew();
293 
294             Authenticator authenticator = changepwContext.getAuthenticator();
295             KerberosPrincipal clientPrincipal = KerberosUtils.getKerberosPrincipal( 
296                 authenticator.getCName(), authenticator.getCRealm() );
297 
298             InetAddress clientAddress = changepwContext.getClientAddress();
299             HostAddresses clientAddresses = ticket.getEncTicketPart().getClientAddresses();
300 
301             boolean caddrContainsSender = false;
302 
303             if ( ticket.getEncTicketPart().getClientAddresses() != null )
304             {
305                 caddrContainsSender = ticket.getEncTicketPart().getClientAddresses().contains( new HostAddress( clientAddress ) );
306             }
307 
308             if ( LOG.isDebugEnabled() )
309             {
310                 StringBuilder sb = new StringBuilder();
311                 sb.append( "Monitoring context:" );
312                 sb.append( "\n\tstore                  " ).append( store );
313                 sb.append( "\n\tauthHeader             " ).append( authHeader );
314                 sb.append( "\n\tticket                 " ).append( ticket );
315                 sb.append( "\n\treplayCache            " ).append( replayCache );
316                 sb.append( "\n\tclockSkew              " ).append( clockSkew );
317                 sb.append( "\n\tclientPrincipal        " ).append( clientPrincipal );
318                 sb.append( "\n\tChangePasswdData       " ).append( changepwContext.getPasswordData() );
319                 sb.append( "\n\tclientAddress          " ).append( clientAddress );
320                 sb.append( "\n\tclientAddresses        " ).append( clientAddresses );
321                 sb.append( "\n\tcaddr contains sender  " ).append( caddrContainsSender );
322                 sb.append( "\n\tTicket principal       " ).append( ticket.getSName() );
323     
324                 PrincipalStoreEntry ticketPrincipal = changepwContext.getServerEntry();
325                 
326                 sb.append( "\n\tcn                     " ).append( ticketPrincipal.getCommonName() );
327                 sb.append( "\n\trealm                  " ).append( ticketPrincipal.getRealmName() );
328                 sb.append( "\n\tService principal      " ).append( ticketPrincipal.getPrincipal() );
329                 sb.append( "\n\tSAM type               " ).append( ticketPrincipal.getSamType() );
330     
331                 EncryptionType encryptionType = ticket.getEncPart().getEType();
332                 int keyVersion = ticketPrincipal.getKeyMap().get( encryptionType ).getKeyVersion();
333                 sb.append( "\n\tTicket key type        " ).append( encryptionType );
334                 sb.append( "\n\tService key version    " ).append( keyVersion );
335 
336                 LOG.debug( sb.toString() );
337             }
338         }
339         catch ( Exception e )
340         {
341             // This is a monitor.  No exceptions should bubble up.
342             LOG.error( I18n.err( I18n.ERR_154 ), e );
343         }
344     }
345     
346     
347     private static void buildReply( ChangePasswordContext changepwContext ) throws KerberosException
348     {
349         Authenticator authenticator = changepwContext.getAuthenticator();
350         Ticket ticket = changepwContext.getTicket();
351         CipherTextHandler cipherTextHandler = changepwContext.getCipherTextHandler();
352 
353         // begin building reply
354 
355         // create priv message
356         // user-data component is short result code
357         EncKrbPrivParterberos/components/EncKrbPrivPart.html#EncKrbPrivPart">EncKrbPrivPart privPart = new EncKrbPrivPart();
358         // first two bytes are the result code, rest is the string 'Password Changed' followed by a null char
359         byte[] resultCode =
360             { ( byte ) 0x00, ( byte ) 0x00, ( byte ) 0x50, ( byte ) 0x61, ( byte ) 0x73, ( byte ) 0x73, ( byte ) 0x77,
361                 ( byte ) 0x6F, ( byte ) 0x72, ( byte ) 0x64, ( byte ) 0x20, ( byte ) 0x63, ( byte ) 0x68,
362                 ( byte ) 0x61, ( byte ) 0x6E, ( byte ) 0x67, ( byte ) 0x65, ( byte ) 0x64, ( byte ) 0x00 };
363         privPart.setUserData( resultCode );
364 
365         privPart.setSenderAddress( new HostAddress( Network.LOOPBACK ) );
366 
367         // get the subsession key from the Authenticator
368         EncryptionKey subSessionKey = authenticator.getSubKey();
369 
370         EncryptedData encPrivPart;
371 
372         try
373         {
374             encPrivPart = cipherTextHandler.seal( subSessionKey, privPart, KeyUsage.KRB_PRIV_ENC_PART_CHOSEN_KEY );
375         }
376         catch ( KerberosException ke )
377         {
378             throw new ChangePasswordException( ChangePasswdErrorType.KRB5_KPASSWD_SOFTERROR, ke );
379         }
380 
381         KrbPrivkerberos/messages/KrbPriv.html#KrbPriv">KrbPriv privateMessage = new KrbPriv();
382         privateMessage.setEncPart( encPrivPart );
383 
384         // Begin AP_REP generation
385         EncApRepPartd/kerberos/messages/EncApRepPart.html#EncApRepPart">EncApRepPart repPart = new EncApRepPart();
386         repPart.setCTime( authenticator.getCtime() );
387         repPart.setCusec( authenticator.getCusec() );
388         
389         if ( authenticator.getSeqNumber() != null )
390         {
391             repPart.setSeqNumber( authenticator.getSeqNumber() );
392         }
393         
394         repPart.setSubkey( subSessionKey );
395 
396         EncryptedData encRepPart;
397 
398         try
399         {
400             encRepPart = cipherTextHandler.seal( ticket.getEncTicketPart().getKey(), repPart, KeyUsage.AP_REP_ENC_PART_SESS_KEY );
401         }
402         catch ( KerberosException ke )
403         {
404             throw new ChangePasswordException( ChangePasswdErrorType.KRB5_KPASSWD_SOFTERROR, ke );
405         }
406 
407         ApRep/shared/kerberos/messages/ApRep.html#ApRep">ApRep appReply = new ApRep();
408         appReply.setEncPart( encRepPart );
409 
410         // return status message value object, the version number 
411         changepwContext.setReply( new ChangePasswordReply( AbstractPasswordMessage.OLD_PVNO, appReply, privateMessage ) );
412     }
413 
414     
415     private static void monitorReply( ChangePasswordContext changepwContext )
416     {
417         try
418         {
419             ChangePasswordReply./../../org/apache/directory/server/kerberos/changepwd/messages/ChangePasswordReply.html#ChangePasswordReply">ChangePasswordReply reply = ( ChangePasswordReply ) changepwContext.getReply();
420             ApRep appReply = reply.getApplicationReply();
421             KrbPriv priv = reply.getPrivateMessage();
422 
423             LOG.debug( "Responding with change password reply:\\n\\tappReply               {}\\n\\tpriv                   {}",
424                     appReply, priv );
425         }
426         catch ( Exception e )
427         {
428             // This is a monitor.  No exceptions should bubble up.
429             LOG.error( I18n.err( I18n.ERR_155 ), e );
430         }
431     }
432 }