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.core.kerberos; 021 022 023 import java.io.IOException; 024 import java.util.ArrayList; 025 import java.util.Collection; 026 import java.util.Collections; 027 import java.util.HashSet; 028 import java.util.Iterator; 029 import java.util.List; 030 import java.util.Map; 031 import java.util.Set; 032 033 import javax.naming.NamingException; 034 035 import org.apache.directory.server.core.authn.AuthenticationInterceptor; 036 import org.apache.directory.server.core.authz.AciAuthorizationInterceptor; 037 import org.apache.directory.server.core.authz.DefaultAuthorizationInterceptor; 038 import org.apache.directory.server.core.collective.CollectiveAttributeInterceptor; 039 import org.apache.directory.server.core.entry.ClonedServerEntry; 040 import org.apache.directory.server.core.event.EventInterceptor; 041 import org.apache.directory.server.core.exception.ExceptionInterceptor; 042 import org.apache.directory.server.core.interceptor.BaseInterceptor; 043 import org.apache.directory.server.core.interceptor.Interceptor; 044 import org.apache.directory.server.core.interceptor.NextInterceptor; 045 import org.apache.directory.server.core.interceptor.context.AddOperationContext; 046 import org.apache.directory.server.core.interceptor.context.LookupOperationContext; 047 import org.apache.directory.server.core.interceptor.context.ModifyOperationContext; 048 import org.apache.directory.server.core.normalization.NormalizationInterceptor; 049 import org.apache.directory.server.core.operational.OperationalAttributeInterceptor; 050 import org.apache.directory.server.core.referral.ReferralInterceptor; 051 import org.apache.directory.server.core.schema.SchemaInterceptor; 052 import org.apache.directory.server.core.subtree.SubentryInterceptor; 053 import org.apache.directory.server.core.trigger.TriggerInterceptor; 054 import org.apache.directory.server.i18n.I18n; 055 import org.apache.directory.server.kerberos.shared.crypto.encryption.EncryptionType; 056 import org.apache.directory.server.kerberos.shared.crypto.encryption.KerberosKeyFactory; 057 import org.apache.directory.server.kerberos.shared.crypto.encryption.RandomKeyFactory; 058 import org.apache.directory.server.kerberos.shared.exceptions.KerberosException; 059 import org.apache.directory.server.kerberos.shared.io.encoder.EncryptionKeyEncoder; 060 import org.apache.directory.server.kerberos.shared.messages.value.EncryptionKey; 061 import org.apache.directory.server.kerberos.shared.store.KerberosAttribute; 062 import org.apache.directory.shared.ldap.constants.SchemaConstants; 063 import org.apache.directory.shared.ldap.entry.BinaryValue; 064 import org.apache.directory.shared.ldap.entry.StringValue; 065 import org.apache.directory.shared.ldap.entry.DefaultServerAttribute; 066 import org.apache.directory.shared.ldap.entry.EntryAttribute; 067 import org.apache.directory.shared.ldap.entry.Modification; 068 import org.apache.directory.shared.ldap.entry.ModificationOperation; 069 import org.apache.directory.shared.ldap.entry.ServerEntry; 070 import org.apache.directory.shared.ldap.entry.ServerModification; 071 import org.apache.directory.shared.ldap.entry.Value; 072 import org.apache.directory.shared.ldap.exception.LdapAuthenticationException; 073 import org.apache.directory.shared.ldap.name.DN; 074 import org.apache.directory.shared.ldap.schema.SchemaManager; 075 import org.apache.directory.shared.ldap.util.StringTools; 076 import org.slf4j.Logger; 077 import org.slf4j.LoggerFactory; 078 079 080 /** 081 * An {@link Interceptor} that creates symmetric Kerberos keys for users. When a 082 * 'userPassword' is added or modified, the 'userPassword' and 'krb5PrincipalName' 083 * are used to derive Kerberos keys. If the 'userPassword' is the special keyword 084 * 'randomKey', a random key is generated and used as the Kerberos key. 085 * 086 * @org.apache.xbean.XBean 087 * 088 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a> 089 * @version $Rev$, $Date$ 090 */ 091 public class KeyDerivationInterceptor extends BaseInterceptor 092 { 093 /** The log for this class. */ 094 private static final Logger log = LoggerFactory.getLogger( KeyDerivationInterceptor.class ); 095 096 /** The service name. */ 097 public static final String NAME = "keyDerivationService"; 098 099 /** 100 * Define the interceptors to bypass upon user lookup. 101 */ 102 private static final Collection<String> USERLOOKUP_BYPASS; 103 static 104 { 105 Set<String> c = new HashSet<String>(); 106 c.add( NormalizationInterceptor.class.getName() ); 107 c.add( AuthenticationInterceptor.class.getName() ); 108 c.add( ReferralInterceptor.class.getName() ); 109 c.add( AciAuthorizationInterceptor.class.getName() ); 110 c.add( DefaultAuthorizationInterceptor.class.getName() ); 111 c.add( ExceptionInterceptor.class.getName() ); 112 c.add( OperationalAttributeInterceptor.class.getName() ); 113 c.add( SchemaInterceptor.class.getName() ); 114 c.add( SubentryInterceptor.class.getName() ); 115 c.add( CollectiveAttributeInterceptor.class.getName() ); 116 c.add( EventInterceptor.class.getName() ); 117 c.add( TriggerInterceptor.class.getName() ); 118 USERLOOKUP_BYPASS = Collections.unmodifiableCollection( c ); 119 } 120 121 122 /** 123 * Intercept the addition of the 'userPassword' and 'krb5PrincipalName' attributes. Use the 'userPassword' 124 * and 'krb5PrincipalName' attributes to derive Kerberos keys for the principal. If the 'userPassword' is 125 * the special keyword 'randomKey', set random keys for the principal. Set the key version number (kvno) 126 * to '0'. 127 */ 128 public void add( NextInterceptor next, AddOperationContext addContext ) throws Exception 129 { 130 DN normName = addContext.getDn(); 131 132 ServerEntry entry = addContext.getEntry(); 133 134 if ( ( entry.get( SchemaConstants.USER_PASSWORD_AT ) != null ) && 135 ( entry.get( KerberosAttribute.KRB5_PRINCIPAL_NAME_AT ) != null ) ) 136 { 137 log.debug( "Adding the entry '{}' for DN '{}'.", entry, normName.getName() ); 138 139 BinaryValue userPassword = (BinaryValue)entry.get( SchemaConstants.USER_PASSWORD_AT ).get(); 140 String strUserPassword = userPassword.getString(); 141 142 if ( log.isDebugEnabled() ) 143 { 144 StringBuffer sb = new StringBuffer(); 145 sb.append( "'" + strUserPassword + "' ( " ); 146 sb.append( userPassword ); 147 sb.append( " )" ); 148 log.debug( "Adding Attribute id : 'userPassword', Values : [ {} ]", sb.toString() ); 149 } 150 151 Value<?> principalNameValue = entry.get( KerberosAttribute.KRB5_PRINCIPAL_NAME_AT ).get(); 152 153 String principalName = principalNameValue.getString(); 154 155 log.debug( "Got principal '{}' with userPassword '{}'.", principalName, strUserPassword ); 156 157 Map<EncryptionType, EncryptionKey> keys = generateKeys( principalName, strUserPassword ); 158 159 entry.put( KerberosAttribute.KRB5_PRINCIPAL_NAME_AT, principalName ); 160 entry.put( KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT, "0" ); 161 162 entry.put( getKeyAttribute( addContext.getSession().getDirectoryService().getSchemaManager(), keys ) ); 163 164 log.debug( "Adding modified entry '{}' for DN '{}'.", entry, normName 165 .getName() ); 166 } 167 168 next.add( addContext ); 169 } 170 171 172 /** 173 * Intercept the modification of the 'userPassword' attribute. Perform a lookup to check for an 174 * existing principal name and key version number (kvno). If a 'krb5PrincipalName' is not in 175 * the modify request, attempt to use an existing 'krb5PrincipalName' attribute. If a kvno 176 * exists, increment the kvno; otherwise, set the kvno to '0'. 177 * 178 * If both a 'userPassword' and 'krb5PrincipalName' can be found, use the 'userPassword' and 179 * 'krb5PrincipalName' attributes to derive Kerberos keys for the principal. 180 * 181 * If the 'userPassword' is the special keyword 'randomKey', set random keys for the principal. 182 */ 183 public void modify( NextInterceptor next, ModifyOperationContext modContext ) throws Exception 184 { 185 ModifySubContext subContext = new ModifySubContext(); 186 187 detectPasswordModification( modContext, subContext ); 188 189 if ( subContext.getUserPassword() != null ) 190 { 191 lookupPrincipalAttributes( modContext, subContext ); 192 } 193 194 if ( subContext.isPrincipal() && subContext.hasValues() ) 195 { 196 deriveKeys( modContext, subContext ); 197 } 198 199 next.modify( modContext ); 200 } 201 202 203 /** 204 * Detect password modification by checking the modify request for the 'userPassword'. Additionally, 205 * check to see if a 'krb5PrincipalName' was provided. 206 * 207 * @param modContext 208 * @param subContext 209 * @throws NamingException 210 */ 211 void detectPasswordModification( ModifyOperationContext modContext, ModifySubContext subContext ) 212 throws Exception 213 { 214 List<Modification> mods = modContext.getModItems(); 215 216 String operation = null; 217 218 // Loop over attributes being modified to pick out 'userPassword' and 'krb5PrincipalName'. 219 for ( Modification mod:mods ) 220 { 221 if ( log.isDebugEnabled() ) 222 { 223 switch ( mod.getOperation() ) 224 { 225 case ADD_ATTRIBUTE: 226 operation = "Adding"; 227 break; 228 229 case REMOVE_ATTRIBUTE: 230 operation = "Removing"; 231 break; 232 233 case REPLACE_ATTRIBUTE: 234 operation = "Replacing"; 235 break; 236 } 237 } 238 239 EntryAttribute attr = mod.getAttribute(); 240 241 if ( attr.instanceOf( SchemaConstants.USER_PASSWORD_AT ) ) 242 { 243 Object firstValue = attr.get(); 244 String password = null; 245 246 if ( firstValue instanceof StringValue ) 247 { 248 password = ((StringValue)firstValue).getString(); 249 log.debug( "{} Attribute id : 'userPassword', Values : [ '{}' ]", operation, password ); 250 } 251 else if ( firstValue instanceof BinaryValue ) 252 { 253 password = ((BinaryValue)firstValue).getString(); 254 255 if ( log.isDebugEnabled() ) 256 { 257 StringBuffer sb = new StringBuffer(); 258 sb.append( "'" + password + "' ( " ); 259 sb.append( StringTools.dumpBytes( ((BinaryValue)firstValue).getBytes() ).trim() ); 260 sb.append( " )" ); 261 log.debug( "{} Attribute id : 'userPassword', Values : [ {} ]", operation, sb.toString() ); 262 } 263 } 264 265 subContext.setUserPassword( password ); 266 log.debug( "Got userPassword '{}'.", subContext.getUserPassword() ); 267 } 268 269 if ( attr.instanceOf( KerberosAttribute.KRB5_PRINCIPAL_NAME_AT ) ) 270 { 271 subContext.setPrincipalName( attr.getString() ); 272 log.debug( "Got principal '{}'.", subContext.getPrincipalName() ); 273 } 274 } 275 } 276 277 278 /** 279 * Lookup the principal's attributes that are relevant to executing key derivation. 280 * 281 * @param modContext 282 * @param subContext 283 * @throws NamingException 284 */ 285 void lookupPrincipalAttributes( ModifyOperationContext modContext, ModifySubContext subContext ) 286 throws Exception 287 { 288 DN principalDn = modContext.getDn(); 289 290 LookupOperationContext lookupContext = modContext.newLookupContext( principalDn ); 291 lookupContext.setByPassed( USERLOOKUP_BYPASS ); 292 lookupContext.setAttrsId( new String[] 293 { 294 SchemaConstants.OBJECT_CLASS_AT, 295 KerberosAttribute.KRB5_PRINCIPAL_NAME_AT, 296 KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT 297 } ); 298 299 ClonedServerEntry userEntry = modContext.lookup( lookupContext ); 300 301 if ( userEntry == null ) 302 { 303 throw new LdapAuthenticationException( I18n.err( I18n.ERR_512, principalDn ) ); 304 } 305 306 EntryAttribute objectClass = userEntry.getOriginalEntry().get( SchemaConstants.OBJECT_CLASS_AT ); 307 308 if ( !objectClass.contains( SchemaConstants.KRB5_PRINCIPAL_OC ) ) 309 { 310 return; 311 } 312 else 313 { 314 subContext.isPrincipal( true ); 315 log.debug( "DN {} is a Kerberos principal. Will attempt key derivation.", principalDn.getName() ); 316 } 317 318 if ( subContext.getPrincipalName() == null ) 319 { 320 EntryAttribute principalAttribute = userEntry.getOriginalEntry().get( KerberosAttribute.KRB5_PRINCIPAL_NAME_AT ); 321 String principalName = principalAttribute.getString(); 322 subContext.setPrincipalName( principalName ); 323 log.debug( "Found principal '{}' from lookup.", principalName ); 324 } 325 326 EntryAttribute keyVersionNumberAttr = userEntry.getOriginalEntry().get( KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT ); 327 328 if ( keyVersionNumberAttr == null ) 329 { 330 subContext.setNewKeyVersionNumber( 0 ); 331 log.debug( "Key version number was null, setting to 0." ); 332 } 333 else 334 { 335 int oldKeyVersionNumber = Integer.valueOf( keyVersionNumberAttr.getString() ); 336 int newKeyVersionNumber = oldKeyVersionNumber + 1; 337 subContext.setNewKeyVersionNumber( newKeyVersionNumber ); 338 log.debug( "Found key version number '{}', setting to '{}'.", oldKeyVersionNumber, newKeyVersionNumber ); 339 } 340 } 341 342 343 /** 344 * Use the 'userPassword' and 'krb5PrincipalName' attributes to derive Kerberos keys for the principal. 345 * 346 * If the 'userPassword' is the special keyword 'randomKey', set random keys for the principal. 347 * 348 * @param modContext 349 * @param subContext 350 */ 351 void deriveKeys( ModifyOperationContext modContext, ModifySubContext subContext ) throws Exception 352 { 353 List<Modification> mods = modContext.getModItems(); 354 355 String principalName = subContext.getPrincipalName(); 356 String userPassword = subContext.getUserPassword(); 357 int kvno = subContext.getNewKeyVersionNumber(); 358 359 log.debug( "Got principal '{}' with userPassword '{}'.", principalName, userPassword ); 360 361 Map<EncryptionType, EncryptionKey> keys = generateKeys( principalName, userPassword ); 362 363 List<Modification> newModsList = new ArrayList<Modification>(); 364 365 // Make sure we preserve any other modification items. 366 for ( Modification mod:mods ) 367 { 368 newModsList.add( mod ); 369 } 370 371 SchemaManager schemaManager = modContext.getSession() 372 .getDirectoryService().getSchemaManager(); 373 374 // Add our modification items. 375 newModsList.add( 376 new ServerModification( 377 ModificationOperation.REPLACE_ATTRIBUTE, 378 new DefaultServerAttribute( 379 KerberosAttribute.KRB5_PRINCIPAL_NAME_AT, 380 schemaManager.lookupAttributeTypeRegistry( KerberosAttribute.KRB5_PRINCIPAL_NAME_AT ), 381 principalName ) ) ); 382 newModsList.add( 383 new ServerModification( 384 ModificationOperation.REPLACE_ATTRIBUTE, 385 new DefaultServerAttribute( 386 KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT, 387 schemaManager.lookupAttributeTypeRegistry( KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT ), 388 Integer.toString( kvno ) ) ) ); 389 390 EntryAttribute attribute = getKeyAttribute( modContext.getSession() 391 .getDirectoryService().getSchemaManager(), keys ); 392 newModsList.add( new ServerModification( ModificationOperation.REPLACE_ATTRIBUTE, attribute ) ); 393 394 modContext.setModItems( newModsList ); 395 } 396 397 398 private EntryAttribute getKeyAttribute( SchemaManager schemaManager, Map<EncryptionType, EncryptionKey> keys ) throws Exception 399 { 400 EntryAttribute keyAttribute = 401 new DefaultServerAttribute( KerberosAttribute.KRB5_KEY_AT, 402 schemaManager.lookupAttributeTypeRegistry( KerberosAttribute.KRB5_KEY_AT ) ); 403 404 Iterator<EncryptionKey> it = keys.values().iterator(); 405 406 while ( it.hasNext() ) 407 { 408 try 409 { 410 keyAttribute.add( EncryptionKeyEncoder.encode( it.next() ) ); 411 } 412 catch ( IOException ioe ) 413 { 414 log.error( I18n.err( I18n.ERR_122 ), ioe ); 415 } 416 } 417 418 return keyAttribute; 419 } 420 421 422 private Map<EncryptionType, EncryptionKey> generateKeys( String principalName, String userPassword ) 423 { 424 if ( userPassword.equalsIgnoreCase( "randomKey" ) ) 425 { 426 // Generate random key. 427 try 428 { 429 return RandomKeyFactory.getRandomKeys(); 430 } 431 catch ( KerberosException ke ) 432 { 433 log.debug( ke.getLocalizedMessage(), ke ); 434 return null; 435 } 436 } 437 else 438 { 439 // Derive key based on password and principal name. 440 return KerberosKeyFactory.getKerberosKeys( principalName, userPassword ); 441 } 442 } 443 444 class ModifySubContext 445 { 446 private boolean isPrincipal = false; 447 private String principalName; 448 private String userPassword; 449 private int newKeyVersionNumber = -1; 450 451 452 boolean isPrincipal() 453 { 454 return isPrincipal; 455 } 456 457 458 void isPrincipal( boolean isPrincipal ) 459 { 460 this.isPrincipal = isPrincipal; 461 } 462 463 464 String getPrincipalName() 465 { 466 return principalName; 467 } 468 469 470 void setPrincipalName( String principalName ) 471 { 472 this.principalName = principalName; 473 } 474 475 476 String getUserPassword() 477 { 478 return userPassword; 479 } 480 481 482 void setUserPassword( String userPassword ) 483 { 484 this.userPassword = userPassword; 485 } 486 487 488 int getNewKeyVersionNumber() 489 { 490 return newKeyVersionNumber; 491 } 492 493 494 void setNewKeyVersionNumber( int newKeyVersionNumber ) 495 { 496 this.newKeyVersionNumber = newKeyVersionNumber; 497 } 498 499 500 boolean hasValues() 501 { 502 return userPassword != null && principalName != null && newKeyVersionNumber > -1; 503 } 504 } 505 }