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.nio.ByteBuffer; 024 025import javax.security.auth.kerberos.KerberosPrincipal; 026 027import org.apache.directory.api.asn1.ber.Asn1Decoder; 028import org.apache.directory.api.util.Network; 029import org.apache.directory.api.util.Strings; 030import org.apache.directory.server.i18n.I18n; 031import org.apache.directory.server.kerberos.ChangePasswordConfig; 032import org.apache.directory.server.kerberos.changepwd.exceptions.ChangePasswdErrorType; 033import org.apache.directory.server.kerberos.changepwd.exceptions.ChangePasswordException; 034import org.apache.directory.server.kerberos.changepwd.messages.AbstractPasswordMessage; 035import org.apache.directory.server.kerberos.changepwd.messages.ChangePasswordReply; 036import org.apache.directory.server.kerberos.changepwd.messages.ChangePasswordRequest; 037import org.apache.directory.server.kerberos.shared.crypto.encryption.CipherTextHandler; 038import org.apache.directory.server.kerberos.shared.crypto.encryption.KeyUsage; 039import org.apache.directory.server.kerberos.shared.replay.ReplayCache; 040import org.apache.directory.server.kerberos.shared.store.PrincipalStore; 041import org.apache.directory.server.kerberos.shared.store.PrincipalStoreEntry; 042import org.apache.directory.shared.kerberos.KerberosUtils; 043import org.apache.directory.shared.kerberos.codec.KerberosDecoder; 044import org.apache.directory.shared.kerberos.codec.changePwdData.ChangePasswdDataContainer; 045import org.apache.directory.shared.kerberos.codec.types.EncryptionType; 046import org.apache.directory.shared.kerberos.codec.types.PrincipalNameType; 047import org.apache.directory.shared.kerberos.components.EncKrbPrivPart; 048import org.apache.directory.shared.kerberos.components.EncryptedData; 049import org.apache.directory.shared.kerberos.components.EncryptionKey; 050import org.apache.directory.shared.kerberos.components.HostAddress; 051import org.apache.directory.shared.kerberos.components.HostAddresses; 052import org.apache.directory.shared.kerberos.components.PrincipalName; 053import org.apache.directory.shared.kerberos.exceptions.ErrorType; 054import org.apache.directory.shared.kerberos.exceptions.KerberosException; 055import org.apache.directory.shared.kerberos.messages.ApRep; 056import org.apache.directory.shared.kerberos.messages.ApReq; 057import org.apache.directory.shared.kerberos.messages.Authenticator; 058import org.apache.directory.shared.kerberos.messages.ChangePasswdData; 059import org.apache.directory.shared.kerberos.messages.EncApRepPart; 060import org.apache.directory.shared.kerberos.messages.KrbPriv; 061import org.apache.directory.shared.kerberos.messages.Ticket; 062import org.apache.mina.core.session.IoSession; 063import org.slf4j.Logger; 064import org.slf4j.LoggerFactory; 065 066public final class ChangePasswordService 067{ 068 /** the logger for this class */ 069 private static final Logger LOG = LoggerFactory.getLogger( ChangePasswordService.class ); 070 071 private static final CipherTextHandler CIPHER_TEXT_HANDLER = new CipherTextHandler(); 072 073 074 private ChangePasswordService() 075 { 076 } 077 078 079 public static void execute( IoSession session, ChangePasswordContext changepwContext ) throws Exception 080 { 081 if ( LOG.isDebugEnabled() ) 082 { 083 monitorRequest( changepwContext ); 084 } 085 086 configureChangePassword( changepwContext ); 087 getAuthHeader( changepwContext ); 088 verifyServiceTicket( changepwContext ); 089 getServerEntry( changepwContext ); 090 verifyServiceTicketAuthHeader( changepwContext ); 091 extractPassword( changepwContext ); 092 093 if ( LOG.isDebugEnabled() ) 094 { 095 monitorContext( changepwContext ); 096 } 097 098 processPasswordChange( changepwContext ); 099 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 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 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 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 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 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 ) 416 { 417 try 418 { 419 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}