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.kerberos.shared;
021    
022    import java.net.InetAddress;
023    import java.text.ParseException;
024    import java.util.ArrayList;
025    import java.util.List;
026    import java.util.Set;
027    
028    import javax.security.auth.kerberos.KerberosPrincipal;
029    
030    import org.apache.directory.server.i18n.I18n;
031    import org.apache.directory.server.kerberos.shared.crypto.encryption.CipherTextHandler;
032    import org.apache.directory.server.kerberos.shared.crypto.encryption.EncryptionType;
033    import org.apache.directory.server.kerberos.shared.crypto.encryption.KeyUsage;
034    import org.apache.directory.server.kerberos.shared.exceptions.ErrorType;
035    import org.apache.directory.server.kerberos.shared.exceptions.KerberosException;
036    import org.apache.directory.server.kerberos.shared.messages.ApplicationRequest;
037    import org.apache.directory.server.kerberos.shared.messages.components.Authenticator;
038    import org.apache.directory.server.kerberos.shared.messages.components.EncTicketPart;
039    import org.apache.directory.server.kerberos.shared.messages.components.Ticket;
040    import org.apache.directory.server.kerberos.shared.messages.value.ApOptions;
041    import org.apache.directory.server.kerberos.shared.messages.value.EncryptionKey;
042    import org.apache.directory.server.kerberos.shared.messages.value.HostAddress;
043    import org.apache.directory.server.kerberos.shared.messages.value.KerberosTime;
044    import org.apache.directory.server.kerberos.shared.messages.value.PrincipalName;
045    import org.apache.directory.server.kerberos.shared.replay.ReplayCache;
046    import org.apache.directory.server.kerberos.shared.store.PrincipalStore;
047    import org.apache.directory.server.kerberos.shared.store.PrincipalStoreEntry;
048    import org.apache.directory.shared.ldap.util.StringTools;
049    
050    /**
051     * An utility class for Kerberos.
052     *
053     * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
054     */
055    public class KerberosUtils
056    {
057        /** A constant for integer optional values */
058        public static final int NULL = -1;
059    
060        /** An empty list of principal names */
061        public static final List<String> EMPTY_PRINCIPAL_NAME = new ArrayList<String>();
062        
063        /**
064         * Parse a KerberosPrincipal instance and return the names. The Principal name
065         * is described in RFC 1964 : <br/>
066         * <br/>
067         * This name type corresponds to the single-string representation of a<br/>
068         * Kerberos name.  (Within the MIT Kerberos V5 implementation, such<br/>
069         * names are parseable with the krb5_parse_name() function.)  The<br/>
070         * elements included within this name representation are as follows,<br/>
071         * proceeding from the beginning of the string:<br/>
072         * <br/>
073         *  (1) One or more principal name components; if more than one<br/>
074         *  principal name component is included, the components are<br/>
075         *  separated by `/`.  Arbitrary octets may be included within<br/>
076         *  principal name components, with the following constraints and<br/>
077         *  special considerations:<br/>
078         * <br/>
079         *     (1a) Any occurrence of the characters `@` or `/` within a<br/>
080         *     name component must be immediately preceded by the `\`<br/>
081         *     quoting character, to prevent interpretation as a component<br/>
082         *     or realm separator.<br/>
083         * <br/>
084         *     (1b) The ASCII newline, tab, backspace, and null characters<br/>
085         *     may occur directly within the component or may be<br/>
086         *     represented, respectively, by `\n`, `\t`, `\b`, or `\0`.<br/>
087         * <br/>
088         *     (1c) If the `\` quoting character occurs outside the contexts<br/>
089         *     described in (1a) and (1b) above, the following character is<br/>
090         *     interpreted literally.  As a special case, this allows the<br/>
091         *     doubled representation `\\` to represent a single occurrence<br/>
092         *     of the quoting character.<br/>
093         * <br/>
094         *     (1d) An occurrence of the `\` quoting character as the last<br/>
095         *     character of a component is illegal.<br/>
096         * <br/>
097         *  (2) Optionally, a `@` character, signifying that a realm name<br/>
098         *  immediately follows. If no realm name element is included, the<br/>
099         *  local realm name is assumed.  The `/` , `:`, and null characters<br/>
100         *  may not occur within a realm name; the `@`, newline, tab, and<br/>
101         *  backspace characters may be included using the quoting<br/>
102         *  conventions described in (1a), (1b), and (1c) above.<br/>
103         * 
104         * @param principal The principal to be parsed
105         * @return The names as a List of nameComponent
106         * 
107         * @throws ParseException if the name is not valid
108         */
109        public static List<String> getNames( KerberosPrincipal principal ) throws ParseException
110        {
111            if ( principal == null )
112            {
113                return EMPTY_PRINCIPAL_NAME;
114            }
115            
116            String names = principal.getName();
117            
118            if ( StringTools.isEmpty( names ) )
119            {
120                // Empty name...
121                return EMPTY_PRINCIPAL_NAME;
122            }
123            
124            return getNames( names );
125        }
126    
127        /**
128         * Parse a PrincipalName and return the names.
129         */
130        public static List<String> getNames( String principalNames ) throws ParseException
131        {
132            if ( principalNames == null )
133            {
134                return EMPTY_PRINCIPAL_NAME;
135            }
136            
137            List<String> nameComponents = new ArrayList<String>();
138            
139            // Start the parsing. Another State Machine :)
140            char[] chars = principalNames.toCharArray();
141            
142            boolean escaped = false;
143            boolean done = false;
144            int start = 0;
145            int pos = 0;
146            
147            for ( int i = 0; i < chars.length; i++ )
148            {
149                pos = i;
150                
151                switch ( chars[i] )
152                {
153                    case '\\' :
154                        escaped = !escaped;
155                        break;
156                        
157                    case '/'  :
158                        if ( escaped )
159                        {
160                            escaped = false;
161                        }
162                        else 
163                        {
164                            // We have a new name component
165                            if ( i - start > 0 )
166                            {
167                                String nameComponent = new String( chars, start, i - start );
168                                nameComponents.add( nameComponent );
169                                start = i + 1;
170                            }
171                            else
172                            {
173                                throw new ParseException( I18n.err( I18n.ERR_628 ), i );
174                            }
175                        }
176                        
177                        break;
178                        
179                    case '@'  :
180                        if ( escaped )
181                        {
182                            escaped = false;
183                        }
184                        else
185                        {
186                            // We have reached the realm : let's get out
187                            done = true;
188                            // We have a new name component
189    
190                            if ( i - start > 0 )
191                            {
192                                String nameComponent = new String( chars, start, i - start );
193                                nameComponents.add( nameComponent );
194                                start = i + 1;
195                            }
196                            else
197                            {
198                                throw new ParseException( I18n.err( I18n.ERR_628 ), i );
199                            }
200                        }
201                        
202                        break;
203                        
204                    default :
205                }
206                
207                if ( done )
208                {
209                    break;
210                }
211            } 
212            
213            if ( escaped )
214            {
215                throw new ParseException( I18n.err( I18n.ERR_629 ), pos );
216            }
217            
218            return nameComponents;
219        }
220        
221        
222        /**
223         * Constructs a KerberosPrincipal from a PrincipalName and an 
224         * optional realm
225         *
226         * @param principal The principal name and type
227         * @param realm The optional realm
228         * 
229         * @return A KerberosPrincipal
230         */
231        public static KerberosPrincipal getKerberosPrincipal( PrincipalName principal, String realm )
232        {
233            String name = principal.getNameString(); 
234            
235            if ( !StringTools.isEmpty( realm ) )
236            {
237                name += '@' + realm;
238            }
239            
240            return new KerberosPrincipal( name, principal.getNameType().getOrdinal() );
241        }
242    
243    
244        /**
245         * Get the matching encryption type from the configured types, searching
246         * into the requested types. We returns the first we find.
247         *
248         * @param requestedTypes The client encryption types
249         * @param configuredTypes The configured encryption types
250         * @return The first matching encryption type.
251         */
252        public static EncryptionType getBestEncryptionType( Set<EncryptionType> requestedTypes, Set<EncryptionType> configuredTypes )
253        {
254            for ( EncryptionType encryptionType:requestedTypes )
255            {
256                if ( configuredTypes.contains( encryptionType ) )
257                {
258                    return encryptionType;
259                }
260            }
261    
262            return null;
263        }
264        
265        
266        /**
267         * Build a list of encryptionTypes
268         *
269         * @param encryptionTypes The encryptionTypes
270         * @return A list comma separated of the encryptionTypes
271         */
272        public static String getEncryptionTypesString( Set<EncryptionType> encryptionTypes )
273        {
274            StringBuilder sb = new StringBuilder();
275            boolean isFirst = true;
276    
277            for ( EncryptionType etype:encryptionTypes )
278            {
279                if ( isFirst )
280                {
281                    isFirst = false;
282                }
283                else
284                {
285                    sb.append( ", " );
286                }
287                
288                sb.append( etype );
289            }
290    
291            return sb.toString();
292        }
293    
294    
295        /**
296         * Get a PrincipalStoreEntry given a principal.  The ErrorType is used to indicate
297         * whether any resulting error pertains to a server or client.
298         *
299         * @param principal
300         * @param store
301         * @param errorType
302         * @return The PrincipalStoreEntry
303         * @throws Exception
304         */
305        public static PrincipalStoreEntry getEntry( KerberosPrincipal principal, PrincipalStore store, ErrorType errorType )
306            throws KerberosException
307        {
308            PrincipalStoreEntry entry = null;
309    
310            try
311            {
312                entry = store.getPrincipal( principal );
313            }
314            catch ( Exception e )
315            {
316                throw new KerberosException( errorType, e );
317            }
318    
319            if ( entry == null )
320            {
321                throw new KerberosException( errorType );
322            }
323    
324            if ( entry.getKeyMap() == null || entry.getKeyMap().isEmpty() )
325            {
326                throw new KerberosException( ErrorType.KDC_ERR_NULL_KEY );
327            }
328    
329            return entry;
330        }
331    
332    
333        /**
334         * Verifies an AuthHeader using guidelines from RFC 1510 section A.10., "KRB_AP_REQ verification."
335         *
336         * @param authHeader
337         * @param ticket
338         * @param serverKey
339         * @param clockSkew
340         * @param replayCache
341         * @param emptyAddressesAllowed
342         * @param clientAddress
343         * @param lockBox
344         * @param authenticatorKeyUsage
345         * @param isValidate
346         * @return The authenticator.
347         * @throws KerberosException
348         */
349        public static Authenticator verifyAuthHeader( ApplicationRequest authHeader, Ticket ticket, EncryptionKey serverKey,
350            long clockSkew, ReplayCache replayCache, boolean emptyAddressesAllowed, InetAddress clientAddress,
351            CipherTextHandler lockBox, KeyUsage authenticatorKeyUsage, boolean isValidate ) throws KerberosException
352        {
353            if ( authHeader.getProtocolVersionNumber() != KerberosConstants.KERBEROS_V5 )
354            {
355                throw new KerberosException( ErrorType.KRB_AP_ERR_BADVERSION );
356            }
357    
358            if ( authHeader.getMessageType() != KerberosMessageType.AP_REQ )
359            {
360                throw new KerberosException( ErrorType.KRB_AP_ERR_MSG_TYPE );
361            }
362    
363            if ( authHeader.getTicket().getTktVno() != KerberosConstants.KERBEROS_V5 )
364            {
365                throw new KerberosException( ErrorType.KRB_AP_ERR_BADVERSION );
366            }
367    
368            EncryptionKey ticketKey = null;
369    
370            if ( authHeader.getOption( ApOptions.USE_SESSION_KEY ) )
371            {
372                ticketKey = authHeader.getTicket().getEncTicketPart().getSessionKey();
373            }
374            else
375            {
376                ticketKey = serverKey;
377            }
378    
379            if ( ticketKey == null )
380            {
381                // TODO - check server key version number, skvno; requires store
382                if ( false )
383                {
384                    throw new KerberosException( ErrorType.KRB_AP_ERR_BADKEYVER );
385                }
386    
387                throw new KerberosException( ErrorType.KRB_AP_ERR_NOKEY );
388            }
389    
390            EncTicketPart encPart = ( EncTicketPart ) lockBox.unseal( EncTicketPart.class, ticketKey, ticket.getEncPart(),
391                KeyUsage.NUMBER2 );
392            ticket.setEncTicketPart( encPart );
393    
394            Authenticator authenticator = ( Authenticator ) lockBox.unseal( Authenticator.class, ticket.getEncTicketPart().getSessionKey(),
395                authHeader.getEncPart(), authenticatorKeyUsage );
396    
397            if ( !authenticator.getClientPrincipal().getName().equals( ticket.getEncTicketPart().getClientPrincipal().getName() ) )
398            {
399                throw new KerberosException( ErrorType.KRB_AP_ERR_BADMATCH );
400            }
401    
402            if ( ticket.getEncTicketPart().getClientAddresses() != null )
403            {
404                if ( !ticket.getEncTicketPart().getClientAddresses().contains( new HostAddress( clientAddress ) ) )
405                {
406                    throw new KerberosException( ErrorType.KRB_AP_ERR_BADADDR );
407                }
408            }
409            else
410            {
411                if ( !emptyAddressesAllowed )
412                {
413                    throw new KerberosException( ErrorType.KRB_AP_ERR_BADADDR );
414                }
415            }
416    
417            KerberosPrincipal serverPrincipal = ticket.getServerPrincipal();
418            KerberosPrincipal clientPrincipal = authenticator.getClientPrincipal();
419            KerberosTime clientTime = authenticator.getClientTime();
420            int clientMicroSeconds = authenticator.getClientMicroSecond();
421    
422            if ( replayCache.isReplay( serverPrincipal, clientPrincipal, clientTime, clientMicroSeconds ) )
423            {
424                throw new KerberosException( ErrorType.KRB_AP_ERR_REPEAT );
425            }
426    
427            replayCache.save( serverPrincipal, clientPrincipal, clientTime, clientMicroSeconds );
428    
429            if ( !authenticator.getClientTime().isInClockSkew( clockSkew ) )
430            {
431                throw new KerberosException( ErrorType.KRB_AP_ERR_SKEW );
432            }
433    
434            /*
435             * "The server computes the age of the ticket: local (server) time minus
436             * the starttime inside the Ticket.  If the starttime is later than the
437             * current time by more than the allowable clock skew, or if the INVALID
438             * flag is set in the ticket, the KRB_AP_ERR_TKT_NYV error is returned."
439             */
440            KerberosTime startTime = ( ticket.getEncTicketPart().getStartTime() != null ) ? ticket.getEncTicketPart().getStartTime() : ticket.getEncTicketPart().getAuthTime();
441    
442            KerberosTime now = new KerberosTime();
443            boolean isValidStartTime = startTime.lessThan( now );
444    
445            if ( !isValidStartTime || ( ticket.getEncTicketPart().getFlags().isInvalid() && !isValidate ) )
446            {
447                // it hasn't yet become valid
448                throw new KerberosException( ErrorType.KRB_AP_ERR_TKT_NYV );
449            }
450    
451            // TODO - doesn't take into account skew
452            if ( !ticket.getEncTicketPart().getEndTime().greaterThan( now ) )
453            {
454                throw new KerberosException( ErrorType.KRB_AP_ERR_TKT_EXPIRED );
455            }
456    
457            authHeader.setOption( ApOptions.MUTUAL_REQUIRED );
458    
459            return authenticator;
460        }
461    }