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}