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