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.core.kerberos; 021 022 023import java.nio.ByteBuffer; 024import java.util.ArrayList; 025import java.util.List; 026import java.util.Map; 027 028import org.apache.directory.api.asn1.EncoderException; 029import org.apache.directory.api.ldap.model.constants.Loggers; 030import org.apache.directory.api.ldap.model.constants.SchemaConstants; 031import org.apache.directory.api.ldap.model.entry.Attribute; 032import org.apache.directory.api.ldap.model.entry.DefaultAttribute; 033import org.apache.directory.api.ldap.model.entry.DefaultModification; 034import org.apache.directory.api.ldap.model.entry.Entry; 035import org.apache.directory.api.ldap.model.entry.Modification; 036import org.apache.directory.api.ldap.model.entry.ModificationOperation; 037import org.apache.directory.api.ldap.model.entry.Value; 038import org.apache.directory.api.ldap.model.exception.LdapAuthenticationException; 039import org.apache.directory.api.ldap.model.exception.LdapException; 040import org.apache.directory.api.ldap.model.name.Dn; 041import org.apache.directory.api.ldap.model.schema.AttributeType; 042import org.apache.directory.api.util.Strings; 043import org.apache.directory.server.core.api.DirectoryService; 044import org.apache.directory.server.core.api.entry.ClonedServerEntry; 045import org.apache.directory.server.core.api.interceptor.BaseInterceptor; 046import org.apache.directory.server.core.api.interceptor.Interceptor; 047import org.apache.directory.server.core.api.interceptor.context.AddOperationContext; 048import org.apache.directory.server.core.api.interceptor.context.LookupOperationContext; 049import org.apache.directory.server.core.api.interceptor.context.ModifyOperationContext; 050import org.apache.directory.server.i18n.I18n; 051import org.apache.directory.server.kerberos.shared.crypto.encryption.KerberosKeyFactory; 052import org.apache.directory.server.kerberos.shared.crypto.encryption.RandomKeyFactory; 053import org.apache.directory.shared.kerberos.KerberosAttribute; 054import org.apache.directory.shared.kerberos.codec.types.EncryptionType; 055import org.apache.directory.shared.kerberos.components.EncryptionKey; 056import org.apache.directory.shared.kerberos.exceptions.KerberosException; 057import org.slf4j.Logger; 058import org.slf4j.LoggerFactory; 059 060 061/** 062 * An {@link Interceptor} that creates symmetric Kerberos keys for users. When a 063 * 'userPassword' is added or modified, the 'userPassword' and 'krb5PrincipalName' 064 * are used to derive Kerberos keys. If the 'userPassword' is the special keyword 065 * 'randomKey', a random key is generated and used as the Kerberos key. 066 * 067 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a> 068 */ 069public class KeyDerivationInterceptor extends BaseInterceptor 070{ 071 /** The log for this class. */ 072 private static final Logger LOG = LoggerFactory.getLogger( KeyDerivationInterceptor.class ); 073 private static final Logger LOG_KRB = LoggerFactory.getLogger( Loggers.KERBEROS_LOG.getName() ); 074 075 /** The service name. */ 076 private static final String NAME = "keyDerivationService"; 077 078 /** The krb5Key attribute type */ 079 private AttributeType krb5KeyAT; 080 081 /** The krb5PrincipalName attribute type */ 082 private AttributeType krb5PrincipalNameAT; 083 084 /** The krb5KeyVersionNumber attribute type */ 085 private AttributeType krb5KeyVersionNumberAT; 086 087 /** The userPassword attribute tType */ 088 private AttributeType userPasswordAT; 089 090 091 /** 092 * Creates an instance of a KeyDerivationInterceptor. 093 */ 094 public KeyDerivationInterceptor() 095 { 096 super( NAME ); 097 } 098 099 100 /** 101 * {@inheritDoc} 102 */ 103 @Override 104 public void init( DirectoryService directoryService ) throws LdapException 105 { 106 super.init( directoryService ); 107 108 krb5KeyAT = schemaManager.lookupAttributeTypeRegistry( KerberosAttribute.KRB5_KEY_AT ); 109 krb5PrincipalNameAT = schemaManager.lookupAttributeTypeRegistry( KerberosAttribute.KRB5_PRINCIPAL_NAME_AT ); 110 krb5KeyVersionNumberAT = schemaManager 111 .lookupAttributeTypeRegistry( KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT ); 112 userPasswordAT = schemaManager 113 .lookupAttributeTypeRegistry( SchemaConstants.USER_PASSWORD_AT ); 114 115 LOG_KRB.info( "KeyDerivation Interceptor initialized" ); 116 } 117 118 119 /** 120 * Intercepts the addition of the 'userPassword' and 'krb5PrincipalName' attributes. 121 * Uses the 'userPassword' and 'krb5PrincipalName' attributes to derive Kerberos keys 122 * for the principal. If the 'userPassword' is the special keyword 'randomKey', set 123 * random keys for the principal. Set the key version number (kvno) to '0'. 124 */ 125 @Override 126 public void add( AddOperationContext addContext ) throws LdapException 127 { 128 // Bypass the replication events 129 if ( addContext.isReplEvent() ) 130 { 131 next( addContext ); 132 133 return; 134 } 135 136 Dn normName = addContext.getDn(); 137 138 Entry entry = addContext.getEntry(); 139 140 if ( ( entry.get( userPasswordAT ) != null ) && ( entry.get( krb5PrincipalNameAT ) != null ) ) 141 { 142 LOG.debug( "Adding the entry '{}' for Dn '{}'.", entry, normName.getName() ); 143 144 // Get the entry's password. We will use the first one. 145 Value userPassword = entry.get( userPasswordAT ).get(); 146 String strUserPassword = Strings.utf8ToString( userPassword.getBytes() ); 147 148 String principalName = entry.get( krb5PrincipalNameAT ).getString(); 149 150 if ( LOG.isDebugEnabled() ) 151 { 152 LOG.debug( "Got principal '{}'.", principalName ); 153 } 154 155 if ( LOG_KRB.isDebugEnabled() ) 156 { 157 LOG_KRB.debug( "Got principal '{}'", principalName ); 158 } 159 160 Map<EncryptionType, EncryptionKey> keys = generateKeys( principalName, strUserPassword ); 161 162 // Set the KVNO to 0 as it's a new entry 163 entry.put( krb5KeyVersionNumberAT, "0" ); 164 165 Attribute keyAttribute = getKeyAttribute( keys ); 166 entry.put( keyAttribute ); 167 168 if ( LOG.isDebugEnabled() ) 169 { 170 LOG.debug( "Adding modified entry '{}' for Dn '{}'.", entry, normName 171 .getName() ); 172 } 173 174 if ( LOG_KRB.isDebugEnabled() ) 175 { 176 LOG_KRB.debug( "Adding modified entry '{}' for Dn '{}'.", entry, normName 177 .getName() ); 178 } 179 } 180 181 next( addContext ); 182 } 183 184 185 /** 186 * Intercept the modification of the 'userPassword' attribute. Perform a lookup to check for an 187 * existing principal name and key version number (kvno). If a 'krb5PrincipalName' is not in 188 * the modify request, attempt to use an existing 'krb5PrincipalName' attribute. If a kvno 189 * exists, increment the kvno; otherwise, set the kvno to '0'. 190 * 191 * If both a 'userPassword' and 'krb5PrincipalName' can be found, use the 'userPassword' and 192 * 'krb5PrincipalName' attributes to derive Kerberos keys for the principal. 193 * 194 * If the 'userPassword' is the special keyword 'randomKey', set random keys for the principal. 195 */ 196 @Override 197 public void modify( ModifyOperationContext modContext ) throws LdapException 198 { 199 // bypass replication events 200 if ( modContext.isReplEvent() ) 201 { 202 next( modContext ); 203 return; 204 } 205 206 ModifySubContext subContext = new ModifySubContext(); 207 208 detectPasswordModification( modContext, subContext ); 209 210 if ( subContext.getUserPassword() != null ) 211 { 212 lookupPrincipalAttributes( modContext, subContext ); 213 } 214 215 if ( subContext.isPrincipal() && subContext.hasValues() ) 216 { 217 deriveKeys( modContext, subContext ); 218 } 219 220 next( modContext ); 221 } 222 223 224 /** 225 * Detect password modification by checking the modify request for the 'userPassword'. Additionally, 226 * check to see if a 'krb5PrincipalName' was provided. 227 * 228 * @param modContext The original ModifyContext 229 * @param subContext The modification container 230 * @throws LdapException If we get an exception 231 */ 232 private void detectPasswordModification( ModifyOperationContext modContext, ModifySubContext subContext ) 233 throws LdapException 234 { 235 List<Modification> mods = modContext.getModItems(); 236 237 String operation = null; 238 239 // Loop over attributes being modified to pick out 'userPassword' and 'krb5PrincipalName'. 240 for ( Modification mod : mods ) 241 { 242 if ( LOG.isDebugEnabled() ) 243 { 244 switch ( mod.getOperation() ) 245 { 246 case ADD_ATTRIBUTE: 247 operation = "Adding"; 248 break; 249 250 case REMOVE_ATTRIBUTE: 251 operation = "Removing"; 252 break; 253 254 case REPLACE_ATTRIBUTE: 255 operation = "Replacing"; 256 break; 257 258 default: 259 throw new IllegalArgumentException( "Unexpected modify operation " + mod.getOperation() ); 260 } 261 } 262 263 Attribute attr = mod.getAttribute(); 264 265 if ( userPasswordAT.equals( attr.getAttributeType() ) ) 266 { 267 Value firstValue = attr.get(); 268 String password = null; 269 270 if ( firstValue.isHumanReadable() ) 271 { 272 password = firstValue.getString(); 273 LOG.debug( "{} Attribute id : 'userPassword', Values : [ '{}' ]", operation, password ); 274 LOG_KRB.debug( "{} Attribute id : 'userPassword', Values : [ '{}' ]", operation, password ); 275 } 276 else 277 { 278 password = Strings.utf8ToString( firstValue.getBytes() ); 279 } 280 281 subContext.setUserPassword( password ); 282 } 283 284 if ( krb5PrincipalNameAT.equals( attr.getAttributeType() ) ) 285 { 286 subContext.setPrincipalName( attr.getString() ); 287 LOG.debug( "Got principal '{}'.", subContext.getPrincipalName() ); 288 LOG_KRB.debug( "Got principal '{}'.", subContext.getPrincipalName() ); 289 } 290 } 291 } 292 293 294 /** 295 * Lookup the principal's attributes that are relevant to executing key derivation. 296 * 297 * @param modContext The original ModifyContext 298 * @param subContext The modification container 299 * @throws LdapException If we get an exception 300 */ 301 private void lookupPrincipalAttributes( ModifyOperationContext modContext, ModifySubContext subContext ) 302 throws LdapException 303 { 304 Dn principalDn = modContext.getDn(); 305 306 LookupOperationContext lookupContext = modContext.newLookupContext( principalDn, 307 SchemaConstants.OBJECT_CLASS_AT, 308 KerberosAttribute.KRB5_PRINCIPAL_NAME_AT, 309 KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT ); 310 lookupContext.setPartition( modContext.getPartition() ); 311 lookupContext.setTransaction( modContext.getTransaction() ); 312 313 Entry userEntry = directoryService.getPartitionNexus().lookup( lookupContext ); 314 315 if ( userEntry == null ) 316 { 317 throw new LdapAuthenticationException( I18n.err( I18n.ERR_512, principalDn ) ); 318 } 319 320 if ( !( ( ClonedServerEntry ) userEntry ).getOriginalEntry().contains( 321 directoryService.getAtProvider().getObjectClass(), SchemaConstants.KRB5_PRINCIPAL_OC ) ) 322 { 323 return; 324 } 325 else 326 { 327 subContext.isPrincipal( true ); 328 LOG.debug( "Dn {} is a Kerberos principal. Will attempt key derivation.", principalDn.getName() ); 329 LOG_KRB.debug( "Dn {} is a Kerberos principal. Will attempt key derivation.", principalDn.getName() ); 330 } 331 332 if ( subContext.getPrincipalName() == null ) 333 { 334 Attribute principalAttribute = ( ( ClonedServerEntry ) userEntry ).getOriginalEntry().get( 335 krb5PrincipalNameAT ); 336 String principalName = principalAttribute.getString(); 337 subContext.setPrincipalName( principalName ); 338 LOG.debug( "Found principal '{}' from lookup.", principalName ); 339 LOG_KRB.debug( "Found principal '{}' from lookup.", principalName ); 340 } 341 342 Attribute keyVersionNumberAttr = ( ( ClonedServerEntry ) userEntry ).getOriginalEntry().get( 343 krb5KeyVersionNumberAT ); 344 345 // Set the KVNO to 0 if it's a password creation, 346 // otherwise increment it. 347 if ( keyVersionNumberAttr == null ) 348 { 349 subContext.setNewKeyVersionNumber( 0 ); 350 LOG.debug( "Key version number was null, setting to 0." ); 351 LOG_KRB.debug( "Key version number was null, setting to 0." ); 352 } 353 else 354 { 355 int oldKeyVersionNumber = Integer.parseInt( keyVersionNumberAttr.getString() ); 356 int newKeyVersionNumber = oldKeyVersionNumber + 1; 357 subContext.setNewKeyVersionNumber( newKeyVersionNumber ); 358 LOG.debug( "Found key version number '{}', setting to '{}'.", oldKeyVersionNumber, newKeyVersionNumber ); 359 LOG_KRB.debug( "Found key version number '{}', setting to '{}'.", oldKeyVersionNumber, newKeyVersionNumber ); 360 } 361 } 362 363 364 /** 365 * Use the 'userPassword' and 'krb5PrincipalName' attributes to derive Kerberos keys for the principal. 366 * 367 * If the 'userPassword' is the special keyword 'randomKey', set random keys for the principal. 368 * 369 * @param modContext The original ModifyContext 370 * @param subContext The modification container 371 */ 372 void deriveKeys( ModifyOperationContext modContext, ModifySubContext subContext ) throws LdapException 373 { 374 List<Modification> mods = modContext.getModItems(); 375 376 String principalName = subContext.getPrincipalName(); 377 String userPassword = subContext.getUserPassword(); 378 int kvno = subContext.getNewKeyVersionNumber(); 379 380 LOG.debug( "Got principal '{}' with userPassword '{}'.", principalName, userPassword ); 381 LOG_KRB.debug( "Got principal '{}' with userPassword '{}'.", principalName, userPassword ); 382 383 Map<EncryptionType, EncryptionKey> keys = generateKeys( principalName, userPassword ); 384 385 List<Modification> newModsList = new ArrayList<>(); 386 387 // Make sure we preserve any other modification items. 388 for ( Modification mod : mods ) 389 { 390 newModsList.add( mod ); 391 } 392 393 // Add our modification items. 394 Modification krb5PrincipalName = 395 new DefaultModification( 396 ModificationOperation.REPLACE_ATTRIBUTE, 397 new DefaultAttribute( 398 krb5PrincipalNameAT, 399 principalName ) ); 400 newModsList.add( krb5PrincipalName ); 401 402 Modification krb5KeyVersionNumber = 403 new DefaultModification( 404 ModificationOperation.REPLACE_ATTRIBUTE, 405 new DefaultAttribute( 406 krb5KeyVersionNumberAT, 407 Integer.toString( kvno ) ) ); 408 409 newModsList.add( krb5KeyVersionNumber ); 410 411 Attribute attribute = getKeyAttribute( keys ); 412 newModsList.add( new DefaultModification( ModificationOperation.REPLACE_ATTRIBUTE, attribute ) ); 413 414 LOG.debug( "Added two modifications to the current request : {} and {}", krb5PrincipalName, 415 krb5KeyVersionNumber ); 416 LOG_KRB.debug( "Added two modifications to the current request : {} and {}", krb5PrincipalName, 417 krb5KeyVersionNumber ); 418 419 modContext.setModItems( newModsList ); 420 } 421 422 423 /** 424 * Create the KRB5_KEY attribute with all the associated keys. 425 * 426 * @param keys The keys to inject in the attribute 427 * @return The create attribute 428 * @throws LdapException If we had an error while adding a key in the attribute 429 */ 430 private Attribute getKeyAttribute( Map<EncryptionType, EncryptionKey> keys ) 431 throws LdapException 432 { 433 Attribute keyAttribute = new DefaultAttribute( krb5KeyAT ); 434 435 for ( EncryptionKey encryptionKey : keys.values() ) 436 { 437 try 438 { 439 ByteBuffer buffer = ByteBuffer.allocate( encryptionKey.computeLength() ); 440 encryptionKey.encode( buffer ); 441 keyAttribute.add( buffer.array() ); 442 } 443 catch ( EncoderException ioe ) 444 { 445 LOG.error( I18n.err( I18n.ERR_122 ), ioe ); 446 LOG_KRB.error( I18n.err( I18n.ERR_122 ), ioe ); 447 } 448 } 449 450 return keyAttribute; 451 } 452 453 454 /** 455 * Generate the keys. 456 * 457 * @param principalName The Principal 458 * @param userPassword Its password 459 * @return A Map of keys 460 */ 461 private Map<EncryptionType, EncryptionKey> generateKeys( String principalName, String userPassword ) 462 { 463 if ( userPassword.equalsIgnoreCase( "randomKey" ) ) 464 { 465 // Generate random key. 466 try 467 { 468 return RandomKeyFactory.getRandomKeys(); 469 } 470 catch ( KerberosException ke ) 471 { 472 LOG.debug( ke.getLocalizedMessage(), ke ); 473 LOG_KRB.debug( ke.getLocalizedMessage(), ke ); 474 475 return null; 476 } 477 } 478 else 479 { 480 // Derive key based on password and principal name. 481 return KerberosKeyFactory.getKerberosKeys( principalName, userPassword ); 482 } 483 } 484 485 /** 486 * A ModifyContext used to store the changes made to the original context. This 487 * is used while processing a ModifyOperation and will be injected in the 488 * original ModifyContext. 489 */ 490 static class ModifySubContext 491 { 492 /** Tells if this is a principal */ 493 private boolean isPrincipal = false; 494 495 /** The Principal name */ 496 private String principalName; 497 498 /** The User password */ 499 private String userPassword; 500 501 /** The Key version */ 502 private int newKeyVersionNumber = -1; 503 504 505 boolean isPrincipal() 506 { 507 return isPrincipal; 508 } 509 510 511 void isPrincipal( boolean isPrincipal ) 512 { 513 this.isPrincipal = isPrincipal; 514 } 515 516 517 String getPrincipalName() 518 { 519 return principalName; 520 } 521 522 523 void setPrincipalName( String principalName ) 524 { 525 this.principalName = principalName; 526 } 527 528 529 String getUserPassword() 530 { 531 return userPassword; 532 } 533 534 535 void setUserPassword( String userPassword ) 536 { 537 this.userPassword = userPassword; 538 } 539 540 541 int getNewKeyVersionNumber() 542 { 543 return newKeyVersionNumber; 544 } 545 546 547 void setNewKeyVersionNumber( int newKeyVersionNumber ) 548 { 549 this.newKeyVersionNumber = newKeyVersionNumber; 550 } 551 552 553 boolean hasValues() 554 { 555 return ( userPassword != null ) && ( principalName != null ) && ( newKeyVersionNumber > -1 ); 556 } 557 } 558}