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 */ 020package org.apache.directory.server.kerberos.changepwd.service; 021 022import java.net.InetAddress; 023import java.net.UnknownHostException; 024import java.nio.ByteBuffer; 025 026import javax.security.auth.kerberos.KerberosPrincipal; 027 028import org.apache.directory.api.asn1.ber.Asn1Decoder; 029import org.apache.directory.api.util.Network; 030import org.apache.directory.api.util.Strings; 031import org.apache.directory.server.i18n.I18n; 032import org.apache.directory.server.kerberos.ChangePasswordConfig; 033import org.apache.directory.server.kerberos.changepwd.exceptions.ChangePasswdErrorType; 034import org.apache.directory.server.kerberos.changepwd.exceptions.ChangePasswordException; 035import org.apache.directory.server.kerberos.changepwd.messages.AbstractPasswordMessage; 036import org.apache.directory.server.kerberos.changepwd.messages.ChangePasswordReply; 037import org.apache.directory.server.kerberos.changepwd.messages.ChangePasswordRequest; 038import org.apache.directory.server.kerberos.shared.crypto.encryption.CipherTextHandler; 039import org.apache.directory.server.kerberos.shared.crypto.encryption.KeyUsage; 040import org.apache.directory.server.kerberos.shared.replay.ReplayCache; 041import org.apache.directory.server.kerberos.shared.store.PrincipalStore; 042import org.apache.directory.server.kerberos.shared.store.PrincipalStoreEntry; 043import org.apache.directory.shared.kerberos.KerberosUtils; 044import org.apache.directory.shared.kerberos.codec.KerberosDecoder; 045import org.apache.directory.shared.kerberos.codec.changePwdData.ChangePasswdDataContainer; 046import org.apache.directory.shared.kerberos.codec.types.EncryptionType; 047import org.apache.directory.shared.kerberos.codec.types.PrincipalNameType; 048import org.apache.directory.shared.kerberos.components.EncKrbPrivPart; 049import org.apache.directory.shared.kerberos.components.EncryptedData; 050import org.apache.directory.shared.kerberos.components.EncryptionKey; 051import org.apache.directory.shared.kerberos.components.HostAddress; 052import org.apache.directory.shared.kerberos.components.HostAddresses; 053import org.apache.directory.shared.kerberos.components.PrincipalName; 054import org.apache.directory.shared.kerberos.exceptions.ErrorType; 055import org.apache.directory.shared.kerberos.exceptions.KerberosException; 056import org.apache.directory.shared.kerberos.messages.ApRep; 057import org.apache.directory.shared.kerberos.messages.ApReq; 058import org.apache.directory.shared.kerberos.messages.Authenticator; 059import org.apache.directory.shared.kerberos.messages.ChangePasswdData; 060import org.apache.directory.shared.kerberos.messages.EncApRepPart; 061import org.apache.directory.shared.kerberos.messages.KrbPriv; 062import org.apache.directory.shared.kerberos.messages.Ticket; 063import org.apache.mina.core.session.IoSession; 064import org.slf4j.Logger; 065import org.slf4j.LoggerFactory; 066 067public final class ChangePasswordService 068{ 069 /** the logger for this class */ 070 private static final Logger LOG = LoggerFactory.getLogger( ChangePasswordService.class ); 071 072 private static final CipherTextHandler CIPHER_TEXT_HANDLER = new CipherTextHandler(); 073 074 075 private ChangePasswordService() 076 { 077 } 078 079 080 public static void execute( IoSession session, ChangePasswordContext changepwContext ) throws Exception 081 { 082 if ( LOG.isDebugEnabled() ) 083 { 084 monitorRequest( changepwContext ); 085 } 086 087 configureChangePassword( changepwContext ); 088 getAuthHeader( changepwContext ); 089 verifyServiceTicket( changepwContext ); 090 getServerEntry( changepwContext ); 091 verifyServiceTicketAuthHeader( changepwContext ); 092 extractPassword( changepwContext ); 093 094 if ( LOG.isDebugEnabled() ) 095 { 096 monitorContext( changepwContext ); 097 } 098 099 processPasswordChange( changepwContext ); 100 buildReply( changepwContext ); 101 102 if ( LOG.isDebugEnabled() ) 103 { 104 monitorReply( changepwContext ); 105 } 106 } 107 108 109 private static void processPasswordChange( ChangePasswordContext changepwContext ) throws KerberosException 110 { 111 PrincipalStore store = changepwContext.getStore(); 112 Authenticator authenticator = changepwContext.getAuthenticator(); 113 String newPassword = Strings.utf8ToString( changepwContext.getPasswordData().getNewPasswd() ); 114 KerberosPrincipal byPrincipal = KerberosUtils.getKerberosPrincipal( 115 authenticator.getCName(), 116 authenticator.getCRealm() ); 117 118 KerberosPrincipal targetPrincipal = null; 119 120 PrincipalName targName = changepwContext.getPasswordData().getTargName(); 121 122 if ( targName != null ) 123 { 124 targetPrincipal = new KerberosPrincipal( targName.getNameString(), PrincipalNameType.KRB_NT_PRINCIPAL.getValue() ); 125 } 126 else 127 { 128 targetPrincipal = byPrincipal; 129 } 130 131 // usec and seq-number must be present per MS but aren't in legacy kpasswd 132 // seq-number must have same value as authenticator 133 // ignore r-address 134 135 store.changePassword( byPrincipal, targetPrincipal, newPassword, changepwContext.getTicket().getEncTicketPart().getFlags().isInitial() ); 136 LOG.debug( "Successfully modified password for {} BY {}.", targetPrincipal, byPrincipal ); 137 } 138 139 140 private static void monitorRequest( ChangePasswordContext changepwContext ) throws KerberosException 141 { 142 try 143 { 144 ChangePasswordRequest request = ( ChangePasswordRequest ) changepwContext.getRequest(); 145 short versionNumber = request.getVersionNumber(); 146 147 StringBuffer sb = new StringBuffer(); 148 sb.append( "Responding to change password request:" ); 149 sb.append( "\n\t" + "versionNumber " + versionNumber ); 150 151 LOG.debug( sb.toString() ); 152 } 153 catch ( Exception e ) 154 { 155 // This is a monitor. No exceptions should bubble up. 156 LOG.error( I18n.err( I18n.ERR_152 ), e ); 157 } 158 } 159 160 161 private static void configureChangePassword( ChangePasswordContext changepwContext ) 162 { 163 changepwContext.setCipherTextHandler( CIPHER_TEXT_HANDLER ); 164 } 165 166 167 private static void getAuthHeader( ChangePasswordContext changepwContext ) throws KerberosException 168 { 169 ChangePasswordRequest request = ( ChangePasswordRequest ) changepwContext.getRequest(); 170 171 short pvno = request.getVersionNumber(); 172 173 if ( ( pvno != AbstractPasswordMessage.PVNO ) && ( pvno != AbstractPasswordMessage.OLD_PVNO ) ) 174 { 175 throw new ChangePasswordException( ChangePasswdErrorType.KRB5_KPASSWD_BAD_VERSION ); 176 } 177 178 if ( request.getAuthHeader() == null || request.getAuthHeader().getTicket() == null ) 179 { 180 throw new ChangePasswordException( ChangePasswdErrorType.KRB5_KPASSWD_AUTHERROR ); 181 } 182 183 ApReq authHeader = request.getAuthHeader(); 184 Ticket ticket = authHeader.getTicket(); 185 186 changepwContext.setAuthHeader( authHeader ); 187 changepwContext.setTicket( ticket ); 188 } 189 190 191 private static void verifyServiceTicket( ChangePasswordContext changepwContext ) throws KerberosException 192 { 193 ChangePasswordConfig config = changepwContext.getConfig(); 194 Ticket ticket = changepwContext.getTicket(); 195 String primaryRealm = config.getPrimaryRealm(); 196 KerberosPrincipal changepwPrincipal = config.getServicePrincipal(); 197 KerberosPrincipal serverPrincipal = KerberosUtils.getKerberosPrincipal( ticket.getSName(), ticket.getRealm() ); 198 199 // for some reason kpassword is setting the pricnipaltype value as 1 for ticket.getSName() 200 // hence changing to string based comparison for server and changepw principals 201 // instead of serverPrincipal.equals( changepwPrincipal ) 202 if ( !ticket.getRealm().equals( primaryRealm ) || !serverPrincipal.getName().equals( changepwPrincipal.getName() ) ) 203 { 204 throw new KerberosException( org.apache.directory.shared.kerberos.exceptions.ErrorType.KRB_AP_ERR_NOT_US ); 205 } 206 } 207 208 209 private static void getServerEntry( ChangePasswordContext changepwContext ) throws KerberosException 210 { 211 Ticket ticket = changepwContext.getTicket(); 212 KerberosPrincipal principal = KerberosUtils.getKerberosPrincipal( ticket.getSName(), ticket.getRealm() ); 213 PrincipalStore store = changepwContext.getStore(); 214 215 changepwContext.setServerEntry( KerberosUtils.getEntry( principal, store, ErrorType.KDC_ERR_S_PRINCIPAL_UNKNOWN ) ); 216 } 217 218 219 private static void verifyServiceTicketAuthHeader( ChangePasswordContext changepwContext ) throws KerberosException 220 { 221 ApReq authHeader = changepwContext.getAuthHeader(); 222 Ticket ticket = changepwContext.getTicket(); 223 224 EncryptionType encryptionType = ticket.getEncPart().getEType(); 225 EncryptionKey serverKey = changepwContext.getServerEntry().getKeyMap().get( encryptionType ); 226 227 long clockSkew = changepwContext.getConfig().getAllowableClockSkew(); 228 ReplayCache replayCache = changepwContext.getReplayCache(); 229 boolean emptyAddressesAllowed = changepwContext.getConfig().isEmptyAddressesAllowed(); 230 InetAddress clientAddress = changepwContext.getClientAddress(); 231 CipherTextHandler cipherTextHandler = changepwContext.getCipherTextHandler(); 232 233 Authenticator authenticator = KerberosUtils.verifyAuthHeader( authHeader, ticket, serverKey, clockSkew, replayCache, 234 emptyAddressesAllowed, clientAddress, cipherTextHandler, KeyUsage.AP_REQ_AUTHNT_SESS_KEY, false ); 235 236 changepwContext.setAuthenticator( authenticator ); 237 } 238 239 240 private static void extractPassword( ChangePasswordContext changepwContext ) throws Exception 241 { 242 ChangePasswordRequest request = ( ChangePasswordRequest ) changepwContext.getRequest(); 243 Authenticator authenticator = changepwContext.getAuthenticator(); 244 CipherTextHandler cipherTextHandler = changepwContext.getCipherTextHandler(); 245 246 // get the subsession key from the Authenticator 247 EncryptionKey subSessionKey = authenticator.getSubKey(); 248 249 // decrypt the request's private message with the subsession key 250 EncryptedData encReqPrivPart = request.getPrivateMessage().getEncPart(); 251 252 ChangePasswdData passwordData = null; 253 254 try 255 { 256 byte[] decryptedData = cipherTextHandler.decrypt( subSessionKey, encReqPrivPart, KeyUsage.KRB_PRIV_ENC_PART_CHOSEN_KEY ); 257 EncKrbPrivPart privatePart = KerberosDecoder.decodeEncKrbPrivPart( decryptedData ); 258 259 if ( authenticator.getSeqNumber() != privatePart.getSeqNumber() ) 260 { 261 throw new ChangePasswordException( ChangePasswdErrorType.KRB5_KPASSWD_MALFORMED ); 262 } 263 264 if ( request.getVersionNumber() == AbstractPasswordMessage.OLD_PVNO ) 265 { 266 passwordData = new ChangePasswdData(); 267 passwordData.setNewPasswd( privatePart.getUserData() ); 268 } 269 else 270 { 271 Asn1Decoder passwordDecoder = new Asn1Decoder(); 272 ByteBuffer stream = ByteBuffer.wrap( privatePart.getUserData() ); 273 ChangePasswdDataContainer container = new ChangePasswdDataContainer( stream ); 274 passwordDecoder.decode( stream, container ); 275 passwordData = container.getChngPwdData(); 276 } 277 } 278 catch ( KerberosException ke ) 279 { 280 throw new ChangePasswordException( ChangePasswdErrorType.KRB5_KPASSWD_SOFTERROR, ke ); 281 } 282 283 changepwContext.setChngPwdData( passwordData ); 284 } 285 286 287 private static void monitorContext( ChangePasswordContext changepwContext ) throws KerberosException 288 { 289 try 290 { 291 PrincipalStore store = changepwContext.getStore(); 292 ApReq authHeader = changepwContext.getAuthHeader(); 293 Ticket ticket = changepwContext.getTicket(); 294 ReplayCache replayCache = changepwContext.getReplayCache(); 295 long clockSkew = changepwContext.getConfig().getAllowableClockSkew(); 296 297 Authenticator authenticator = changepwContext.getAuthenticator(); 298 KerberosPrincipal clientPrincipal = KerberosUtils.getKerberosPrincipal( 299 authenticator.getCName(), authenticator.getCRealm() ); 300 301 InetAddress clientAddress = changepwContext.getClientAddress(); 302 HostAddresses clientAddresses = ticket.getEncTicketPart().getClientAddresses(); 303 304 boolean caddrContainsSender = false; 305 306 if ( ticket.getEncTicketPart().getClientAddresses() != null ) 307 { 308 caddrContainsSender = ticket.getEncTicketPart().getClientAddresses().contains( new HostAddress( clientAddress ) ); 309 } 310 311 StringBuffer sb = new StringBuffer(); 312 sb.append( "Monitoring context:" ); 313 sb.append( "\n\t" + "store " + store ); 314 sb.append( "\n\t" + "authHeader " + authHeader ); 315 sb.append( "\n\t" + "ticket " + ticket ); 316 sb.append( "\n\t" + "replayCache " + replayCache ); 317 sb.append( "\n\t" + "clockSkew " + clockSkew ); 318 sb.append( "\n\t" + "clientPrincipal " + clientPrincipal ); 319 sb.append( "\n\t" + "ChangePasswdData " + changepwContext.getPasswordData() ); 320 sb.append( "\n\t" + "clientAddress " + clientAddress ); 321 sb.append( "\n\t" + "clientAddresses " + clientAddresses ); 322 sb.append( "\n\t" + "caddr contains sender " + caddrContainsSender ); 323 sb.append( "\n\t" + "Ticket principal " + ticket.getSName() ); 324 325 PrincipalStoreEntry ticketPrincipal = changepwContext.getServerEntry(); 326 327 sb.append( "\n\t" + "cn " + ticketPrincipal.getCommonName() ); 328 sb.append( "\n\t" + "realm " + ticketPrincipal.getRealmName() ); 329 sb.append( "\n\t" + "Service principal " + ticketPrincipal.getPrincipal() ); 330 sb.append( "\n\t" + "SAM type " + ticketPrincipal.getSamType() ); 331 332 EncryptionType encryptionType = ticket.getEncPart().getEType(); 333 int keyVersion = ticketPrincipal.getKeyMap().get( encryptionType ).getKeyVersion(); 334 sb.append( "\n\t" + "Ticket key type " + encryptionType ); 335 sb.append( "\n\t" + "Service key version " + keyVersion ); 336 337 LOG.debug( sb.toString() ); 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, UnknownHostException 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 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 KrbPriv privateMessage = new KrbPriv(); 382 privateMessage.setEncPart( encPrivPart ); 383 384 // Begin AP_REP generation 385 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 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 ) throws KerberosException 416 { 417 try 418 { 419 ChangePasswordReply reply = ( ChangePasswordReply ) changepwContext.getReply(); 420 ApRep appReply = reply.getApplicationReply(); 421 KrbPriv 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}