001 /* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 * 019 */ 020 package org.apache.directory.server.changepw.service; 021 022 import java.io.IOException; 023 import java.io.UnsupportedEncodingException; 024 import java.net.InetAddress; 025 import java.net.UnknownHostException; 026 027 import javax.naming.NamingException; 028 import javax.security.auth.kerberos.KerberosPrincipal; 029 030 import org.apache.directory.server.changepw.ChangePasswordServer; 031 import org.apache.directory.server.changepw.exceptions.ChangePasswordException; 032 import org.apache.directory.server.changepw.exceptions.ErrorType; 033 import org.apache.directory.server.changepw.io.ChangePasswordDataDecoder; 034 import org.apache.directory.server.changepw.messages.ChangePasswordReply; 035 import org.apache.directory.server.changepw.messages.ChangePasswordReplyModifier; 036 import org.apache.directory.server.changepw.messages.ChangePasswordRequest; 037 import org.apache.directory.server.changepw.value.ChangePasswordData; 038 import org.apache.directory.server.changepw.value.ChangePasswordDataModifier; 039 import org.apache.directory.server.i18n.I18n; 040 import org.apache.directory.server.kerberos.shared.KerberosUtils; 041 import org.apache.directory.server.kerberos.shared.crypto.encryption.CipherTextHandler; 042 import org.apache.directory.server.kerberos.shared.crypto.encryption.EncryptionType; 043 import org.apache.directory.server.kerberos.shared.crypto.encryption.KeyUsage; 044 import org.apache.directory.server.kerberos.shared.exceptions.KerberosException; 045 import org.apache.directory.server.kerberos.shared.messages.ApplicationRequest; 046 import org.apache.directory.server.kerberos.shared.messages.application.ApplicationReply; 047 import org.apache.directory.server.kerberos.shared.messages.application.PrivateMessage; 048 import org.apache.directory.server.kerberos.shared.messages.components.Authenticator; 049 import org.apache.directory.server.kerberos.shared.messages.components.EncApRepPart; 050 import org.apache.directory.server.kerberos.shared.messages.components.EncApRepPartModifier; 051 import org.apache.directory.server.kerberos.shared.messages.components.EncKrbPrivPart; 052 import org.apache.directory.server.kerberos.shared.messages.components.EncKrbPrivPartModifier; 053 import org.apache.directory.server.kerberos.shared.messages.components.Ticket; 054 import org.apache.directory.server.kerberos.shared.messages.value.EncryptedData; 055 import org.apache.directory.server.kerberos.shared.messages.value.EncryptionKey; 056 import org.apache.directory.server.kerberos.shared.messages.value.HostAddress; 057 import org.apache.directory.server.kerberos.shared.messages.value.HostAddresses; 058 import org.apache.directory.server.kerberos.shared.replay.InMemoryReplayCache; 059 import org.apache.directory.server.kerberos.shared.replay.ReplayCache; 060 import org.apache.directory.server.kerberos.shared.store.PrincipalStore; 061 import org.apache.directory.server.kerberos.shared.store.PrincipalStoreEntry; 062 import org.apache.mina.core.session.IoSession; 063 import org.slf4j.Logger; 064 import org.slf4j.LoggerFactory; 065 066 public class ChangePasswordService 067 { 068 /** the logger for this class */ 069 private static final Logger LOG = LoggerFactory.getLogger( ChangePasswordService.class ); 070 071 private static final ReplayCache replayCache = new InMemoryReplayCache(); 072 073 private static final CipherTextHandler cipherTextHandler = new CipherTextHandler(); 074 075 076 public static void execute( IoSession session, ChangePasswordContext changepwContext ) throws KerberosException, IOException 077 { 078 if ( LOG.isDebugEnabled() ) 079 { 080 monitorRequest( changepwContext ); 081 } 082 083 configureChangePassword( changepwContext ); 084 getAuthHeader( session, changepwContext ); 085 verifyServiceTicket( changepwContext ); 086 getServerEntry( changepwContext ); 087 verifyServiceTicketAuthHeader( changepwContext ); 088 extractPassword( changepwContext ); 089 090 if ( LOG.isDebugEnabled() ) 091 { 092 monitorContext( changepwContext ); 093 } 094 095 processPasswordChange( changepwContext ); 096 buildReply( changepwContext ); 097 098 if ( LOG.isDebugEnabled() ) 099 { 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( I18n.err( I18n.ERR_152 ), 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( I18n.err( I18n.ERR_154 ), 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( I18n.err( I18n.ERR_155 ), e ); 434 } 435 } 436 }