      1 /*
      2  * Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved.
      3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
      4  *
      5  * This code is free software; you can redistribute it and/or modify it
      6  * under the terms of the GNU General Public License version 2 only, as
      7  * published by the Free Software Foundation.  Oracle designates this
      8  * particular file as subject to the "Classpath" exception as provided
      9  * by Oracle in the LICENSE file that accompanied this code.
     10  *
     11  * This code is distributed in the hope that it will be useful, but WITHOUT
     12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
     13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
     14  * version 2 for more details (a copy is included in the LICENSE file that
     15  * accompanied this code).
     16  *
     17  * You should have received a copy of the GNU General Public License version
     18  * 2 along with this work; if not, write to the Free Software Foundation,
     19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
     20  *
     21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
     22  * or visit www.oracle.com if you need additional information or have any
     23  * questions.
     24  */
     25
     26 package javax.security.auth.kerberos;
     27
     28 import java.io.*;
     29 import java.util.Date;
     30 import java.util.Arrays;
     31 import java.net.InetAddress;
     32 import java.util.Objects;
     33 import javax.crypto.SecretKey;
     34 import javax.security.auth.Refreshable;
     35 import javax.security.auth.Destroyable;
     36 import javax.security.auth.RefreshFailedException;
     37 import javax.security.auth.DestroyFailedException;
     38
     39 import sun.security.util.HexDumpEncoder;
     40
     41 /**
     42  * This class encapsulates a Kerberos ticket and associated
     43  * information as viewed from the client's point of view. It captures all
     44  * information that the Key Distribution Center (KDC) sends to the client
     45  * in the reply message KDC-REP defined in the Kerberos Protocol
     46  * Specification (<a href=http://www.ietf.org/rfc/rfc4120.txt>RFC 4120</a>).
     47  * <p>
     48  * All Kerberos JAAS login modules that authenticate a user to a KDC should
     49  * use this class. Where available, the login module might even read this
     50  * information from a ticket cache in the operating system instead of
     51  * directly communicating with the KDC. During the commit phase of the JAAS
     52  * authentication process, the JAAS login module should instantiate this
     53  * class and store the instance in the private credential set of a
     54  * {@link javax.security.auth.Subject Subject}.<p>
     55  *
     56  * It might be necessary for the application to be granted a
     57  * {@link javax.security.auth.PrivateCredentialPermission
     58  * PrivateCredentialPermission} if it needs to access a {@code KerberosTicket}
     59  * instance from a {@code Subject}. This permission is not needed when the
     60  * application depends on the default JGSS Kerberos mechanism to access the
     61  * {@code KerberosTicket}. In that case, however, the application will need an
     62  * appropriate
     63  * {@link javax.security.auth.kerberos.ServicePermission ServicePermission}.
     64  * <p>
     65  * Note that this class is applicable to both ticket granting tickets and
     66  * other regular service tickets. A ticket granting ticket is just a
     67  * special case of a more generalized service ticket.
     68  *
     69  * @implNote The JAAS login module in the JDK reference implementation destroys
     70  * all tickets after logout.
     71  *
     72  * @see javax.security.auth.Subject
     73  * @see javax.security.auth.PrivateCredentialPermission
     74  * @see javax.security.auth.login.LoginContext
     75  * @see org.ietf.jgss.GSSCredential
     76  * @see org.ietf.jgss.GSSManager
     77  *
     78  * @author Mayank Upadhyay
     79  * @since 1.4
     80  */
     81 public class KerberosTicket implements Destroyable, Refreshable,
     82          java.io.Serializable {
     83
     84     private static final long serialVersionUID = 7395334370157380539L;
     85
     86     // XXX Make these flag indices public
     87     private static final int FORWARDABLE_TICKET_FLAG = 1;
     88     private static final int FORWARDED_TICKET_FLAG   = 2;
     89     private static final int PROXIABLE_TICKET_FLAG   = 3;
     90     private static final int PROXY_TICKET_FLAG       = 4;
     91     private static final int POSTDATED_TICKET_FLAG   = 6;
     92     private static final int RENEWABLE_TICKET_FLAG   = 8;
     93     private static final int INITIAL_TICKET_FLAG     = 9;
     94
     95     private static final int NUM_FLAGS = 32;
     96
     97     /**
     98      *
     99      * ASN.1 DER Encoding of the Ticket as defined in the
    100      * Kerberos Protocol Specification RFC4120.
    101      *
    102      * @serial
    103      */
    104
    105     private byte[] asn1Encoding;
    106
    107     /**
    108      *{@code KeyImpl} is serialized by writing out the ASN1 Encoded bytes
    109      * of the encryption key. The ASN1 encoding is defined in RFC4120 and as
    110      * follows:
    111      * <pre>
    112      * EncryptionKey   ::= SEQUENCE {
    113      *          keytype    [0] Int32 -- actually encryption type --,
    114      *          keyvalue   [1] OCTET STRING
    115      * }
    116      * </pre>
    117      *
    118      * @serial
    119      */
    120
    121     private KeyImpl sessionKey;
    122
    123     /**
    124      *
    125      * Ticket Flags as defined in the Kerberos Protocol Specification RFC4120.
    126      *
    127      * @serial
    128      */
    129
    130     private boolean[] flags;
    131
    132     /**
    133      *
    134      * Time of initial authentication
    135      *
    136      * @serial
    137      */
    138
    139     private Date authTime;
    140
    141     /**
    142      *
    143      * Time after which the ticket is valid.
    144      * @serial
    145      */
    146     private Date startTime;
    147
    148     /**
    149      *
    150      * Time after which the ticket will not be honored. (its expiration time).
    151      *
    152      * @serial
    153      */
    154
    155     private Date endTime;
    156
    157     /**
    158      *
    159      * For renewable Tickets it indicates the maximum endtime that may be
    160      * included in a renewal. It can be thought of as the absolute expiration
    161      * time for the ticket, including all renewals. This field may be null
    162      * for tickets that are not renewable.
    163      *
    164      * @serial
    165      */
    166
    167     private Date renewTill;
    168
    169     /**
    170      *
    171      * Client that owns the service ticket
    172      *
    173      * @serial
    174      */
    175
    176     private KerberosPrincipal client;
    177
    178     /**
    179      *
    180      * The service for which the ticket was issued.
    181      *
    182      * @serial
    183      */
    184
    185     private KerberosPrincipal server;
    186
    187     /**
    188      *
    189      * The addresses from where the ticket may be used by the client.
    190      * This field may be null when the ticket is usable from any address.
    191      *
    192      * @serial
    193      */
    194
    195     private InetAddress[] clientAddresses;
    196
    197     /**
    198      * Evidence ticket if proxy_impersonator. This field can be accessed
    199      * by KerberosSecrets. It's serialized.
    200      */
    201     KerberosTicket proxy = null;
    202
    203     private transient boolean destroyed = false;
    204
    205     transient KerberosPrincipal clientAlias = null;
    206
    207     transient KerberosPrincipal serverAlias = null;
    208
    209     /**
    210      * Constructs a {@code KerberosTicket} using credentials information that a
    211      * client either receives from a KDC or reads from a cache.
    212      *
    213      * @param asn1Encoding the ASN.1 encoding of the ticket as defined by
    214      * the Kerberos protocol specification.
    215      * @param client the client that owns this service
    216      * ticket
    217      * @param server the service that this ticket is for
    218      * @param sessionKey the raw bytes for the session key that must be
    219      * used to encrypt the authenticator that will be sent to the server
    220      * @param keyType the key type for the session key as defined by the
    221      * Kerberos protocol specification.
    222      * @param flags the ticket flags. Each element in this array indicates
    223      * the value for the corresponding bit in the ASN.1 BitString that
    224      * represents the ticket flags. If the number of elements in this array
    225      * is less than the number of flags used by the Kerberos protocol,
    226      * then the missing flags will be filled in with false.
    227      * @param authTime the time of initial authentication for the client
    228      * @param startTime the time after which the ticket will be valid. This
    229      * may be null in which case the value of authTime is treated as the
    230      * startTime.
    231      * @param endTime the time after which the ticket will no longer be
    232      * valid
    233      * @param renewTill an absolute expiration time for the ticket,
    234      * including all renewal that might be possible. This field may be null
    235      * for tickets that are not renewable.
    236      * @param clientAddresses the addresses from where the ticket may be
    237      * used by the client. This field may be null when the ticket is usable
    238      * from any address.
    239      */
    240     public KerberosTicket(byte[] asn1Encoding,
    241                          KerberosPrincipal client,
    242                          KerberosPrincipal server,
    243                          byte[] sessionKey,
    244                          int keyType,
    245                          boolean[] flags,
    246                          Date authTime,
    247                          Date startTime,
    248                          Date endTime,
    249                          Date renewTill,
    250                          InetAddress[] clientAddresses) {
    251
    252         init(asn1Encoding, client, server, sessionKey, keyType, flags,
    253             authTime, startTime, endTime, renewTill, clientAddresses);
    254     }
    255
    256     private void init(byte[] asn1Encoding,
    257                          KerberosPrincipal client,
    258                          KerberosPrincipal server,
    259                          byte[] sessionKey,
    260                          int keyType,
    261                          boolean[] flags,
    262                          Date authTime,
    263                          Date startTime,
    264                          Date endTime,
    265                          Date renewTill,
    266                          InetAddress[] clientAddresses) {
    267         if (sessionKey == null) {
    268             throw new IllegalArgumentException("Session key for ticket"
    269                     + " cannot be null");
    270         }
    271         init(asn1Encoding, client, server,
    272              new KeyImpl(sessionKey, keyType), flags, authTime,
    273              startTime, endTime, renewTill, clientAddresses);
    274     }
    275
    276     private void init(byte[] asn1Encoding,
    277                          KerberosPrincipal client,
    278                          KerberosPrincipal server,
    279                          KeyImpl sessionKey,
    280                          boolean[] flags,
    281                          Date authTime,
    282                          Date startTime,
    283                          Date endTime,
    284                          Date renewTill,
    285                          InetAddress[] clientAddresses) {
    286         if (asn1Encoding == null) {
    287             throw new IllegalArgumentException("ASN.1 encoding of ticket"
    288                     + " cannot be null");
    289         }
    290         this.asn1Encoding = asn1Encoding.clone();
    291
    292         if (client == null) {
    293             throw new IllegalArgumentException("Client name in ticket"
    294                     + " cannot be null");
    295         }
    296         this.client = client;
    297
    298         if (server == null) {
    299             throw new IllegalArgumentException("Server name in ticket"
    300                     + " cannot be null");
    301         }
    302         this.server = server;
    303
    304         // Caller needs to make sure `sessionKey` will not be null
    305         this.sessionKey = sessionKey;
    306
    307         if (flags != null) {
    308            if (flags.length >= NUM_FLAGS) {
    309                this.flags = flags.clone();
    310            } else {
    311                 this.flags = new boolean[NUM_FLAGS];
    312                 // Fill in whatever we have
    313                 for (int i = 0; i < flags.length; i++) {
    314                     this.flags[i] = flags[i];
    315                 }
    316            }
    317         } else {
    318             this.flags = new boolean[NUM_FLAGS];
    319         }
    320
    321         if (this.flags[RENEWABLE_TICKET_FLAG] && renewTill != null) {
    322            this.renewTill = new Date(renewTill.getTime());
    323         }
    324
    325         if (authTime != null) {
    326             this.authTime = new Date(authTime.getTime());
    327         }
    328         if (startTime != null) {
    329             this.startTime = new Date(startTime.getTime());
    330         } else {
    331             this.startTime = this.authTime;
    332         }
    333
    334         if (endTime == null) {
    335             throw new IllegalArgumentException("End time for ticket validity"
    336                     + " cannot be null");
    337         }
    338         this.endTime = new Date(endTime.getTime());
    339
    340         if (clientAddresses != null) {
    341             this.clientAddresses = clientAddresses.clone();
    342         }
    343     }
    344
    345     /**
    346      * Returns the client principal associated with this ticket.
    347      *
    348      * @return the client principal, or {@code null} if destroyed.
    349      */
    350     public final KerberosPrincipal getClient() {
    351         return client;
    352     }
    353
    354     /**
    355      * Returns the service principal associated with this ticket.
    356      *
    357      * @return the service principal, or {@code null} if destroyed.
    358      */
    359     public final KerberosPrincipal getServer() {
    360         return server;
    361     }
    362
    363     /**
    364      * Returns the session key associated with this ticket. The return value
    365      * is always a {@link EncryptionKey} object.
    366      *
    367      * @return the session key.
    368      * @throws IllegalStateException if this ticket is destroyed
    369      */
    370     public final SecretKey getSessionKey() {
    371         if (destroyed) {
    372             throw new IllegalStateException("This ticket is no longer valid");
    373         }
    374         return new EncryptionKey(
    375                 sessionKey.getEncoded(), sessionKey.getKeyType());
    376     }
    377
    378     /**
    379      * Returns the key type of the session key associated with this
    380      * ticket as defined by the Kerberos Protocol Specification.
    381      *
    382      * @return the key type of the session key associated with this
    383      * ticket.
    384      * @throws IllegalStateException if this ticket is destroyed
    385      *
    386      * @see #getSessionKey()
    387      */
    388     public final int getSessionKeyType() {
    389         if (destroyed) {
    390             throw new IllegalStateException("This ticket is no longer valid");
    391         }
    392         return sessionKey.getKeyType();
    393     }
    394
    395     /**
    396      * Determines if this ticket is forwardable.
    397      *
    398      * @return true if this ticket is forwardable, or false if not forwardable
    399      * or destroyed.
    400      */
    401     public final boolean isForwardable() {
    402         return flags == null? false: flags[FORWARDABLE_TICKET_FLAG];
    403     }
    404
    405     /**
    406      * Determines if this ticket had been forwarded or was issued based on
    407      * authentication involving a forwarded ticket-granting ticket.
    408      *
    409      * @return true if this ticket had been forwarded or was issued based on
    410      * authentication involving a forwarded ticket-granting ticket,
    411      * or false otherwise or destroyed.
    412      */
    413     public final boolean isForwarded() {
    414         return flags == null? false: flags[FORWARDED_TICKET_FLAG];
    415     }
    416
    417     /**
    418      * Determines if this ticket is proxiable.
    419      *
    420      * @return true if this ticket is proxiable, or false if not proxiable
    421      * or destroyed.
    422      */
    423     public final boolean isProxiable() {
    424         return flags == null? false: flags[PROXIABLE_TICKET_FLAG];
    425     }
    426
    427     /**
    428      * Determines is this ticket is a proxy-ticket.
    429      *
    430      * @return true if this ticket is a proxy-ticket, or false if not
    431      * a proxy-ticket or destroyed.
    432      */
    433     public final boolean isProxy() {
    434         return flags == null? false: flags[PROXY_TICKET_FLAG];
    435     }
    436
    437
    438     /**
    439      * Determines is this ticket is post-dated.
    440      *
    441      * @return true if this ticket is post-dated, or false if not post-dated
    442      * or destroyed.
    443      */
    444     public final boolean isPostdated() {
    445         return flags == null? false: flags[POSTDATED_TICKET_FLAG];
    446     }
    447
    448     /**
    449      * Determines is this ticket is renewable. If so, the {@link #refresh()
    450      * refresh} method can be called, assuming the validity period for
    451      * renewing is not already over.
    452      *
    453      * @return true if this ticket is renewable, or false if not renewable
    454      * or destroyed.
    455      */
    456     public final boolean isRenewable() {
    457         return flags == null? false: flags[RENEWABLE_TICKET_FLAG];
    458     }
    459
    460     /**
    461      * Determines if this ticket was issued using the Kerberos AS-Exchange
    462      * protocol, and not issued based on some ticket-granting ticket.
    463      *
    464      * @return true if this ticket was issued using the Kerberos AS-Exchange
    465      * protocol, or false if not issued this way or destroyed.
    466      */
    467     public final boolean isInitial() {
    468         return flags == null? false: flags[INITIAL_TICKET_FLAG];
    469     }
    470
    471     /**
    472      * Returns the flags associated with this ticket. Each element in the
    473      * returned array indicates the value for the corresponding bit in the
    474      * ASN.1 BitString that represents the ticket flags.
    475      *
    476      * @return the flags associated with this ticket, or {@code null}
    477      * if destroyed.
    478      */
    479     public final boolean[]  getFlags() {
    480         return (flags == null? null: flags.clone());
    481     }
    482
    483     /**
    484      * Returns the time that the client was authenticated.
    485      *
    486      * @return the time that the client was authenticated
    487      *         or {@code null} if the field is not set or
    488      *         this ticket is destroyed.
    489      */
    490     public final java.util.Date getAuthTime() {
    491         return (authTime == null) ? null : (Date)authTime.clone();
    492     }
    493
    494     /**
    495      * Returns the start time for this ticket's validity period.
    496      *
    497      * @return the start time for this ticket's validity period
    498      *         or {@code null} if the field is not set or
    499      *         this ticket is destroyed.
    500      */
    501     public final java.util.Date getStartTime() {
    502         return (startTime == null) ? null : (Date)startTime.clone();
    503     }
    504
    505     /**
    506      * Returns the expiration time for this ticket's validity period.
    507      *
    508      * @return the expiration time for this ticket's validity period,
    509      * or {@code null} if destroyed.
    510      */
    511     public final java.util.Date getEndTime() {
    512         return (endTime == null) ? null : (Date) endTime.clone();
    513     }
    514
    515     /**
    516      * Returns the latest expiration time for this ticket, including all
    517      * renewals. This will return a null value for non-renewable tickets.
    518      *
    519      * @return the latest expiration time for this ticket, or {@code null}
    520      * if destroyed.
    521      */
    522     public final java.util.Date getRenewTill() {
    523         return (renewTill == null) ? null: (Date)renewTill.clone();
    524     }
    525
    526     /**
    527      * Returns a list of addresses from where the ticket can be used.
    528      *
    529      * @return the list of addresses, or {@code null} if the field was not
    530      * provided or this ticket is destroyed.
    531      */
    532     public final java.net.InetAddress[] getClientAddresses() {
    533         return (clientAddresses == null) ? null: clientAddresses.clone();
    534     }
    535
    536     /**
    537      * Returns an ASN.1 encoding of the entire ticket.
    538      *
    539      * @return an ASN.1 encoding of the entire ticket. A new byte
    540      * array is returned each time this method is called.
    541      * @throws IllegalStateException if this ticket is destroyed
    542      */
    543     public final byte[] getEncoded() {
    544         if (destroyed) {
    545             throw new IllegalStateException("This ticket is no longer valid");
    546         }
    547         return asn1Encoding.clone();
    548     }
    549
    550     /**
    551      * Determines if this ticket is still current.
    552      *
    553      * @return true if this ticket is still current, or false if not current
    554      * or destroyed.
    555      */
    556     public boolean isCurrent() {
    557         return endTime == null? false: (System.currentTimeMillis() <= endTime.getTime());
    558     }
    559
    560     /**
    561      * Extends the validity period of this ticket. The ticket will contain
    562      * a new session key if the refresh operation succeeds. The refresh
    563      * operation will fail if the ticket is not renewable or the latest
    564      * allowable renew time has passed. Any other error returned by the
    565      * KDC will also cause this method to fail.
    566      *
    567      * Note: This method is not synchronized with the accessor
    568      * methods of this object. Hence callers need to be aware of multiple
    569      * threads that might access this and try to renew it at the same
    570      * time.
    571      *
    572      * @throws IllegalStateException if this ticket is destroyed
    573      * @throws RefreshFailedException if the ticket is not renewable, or
    574      * the latest allowable renew time has passed, or the KDC returns some
    575      * error.
    576      *
    577      * @see #isRenewable()
    578      * @see #getRenewTill()
    579      */
    580     public void refresh() throws RefreshFailedException {
    581
    582         if (destroyed) {
    583             throw new RefreshFailedException("A destroyed ticket "
    584                     + "cannot be renewd.");
    585         }
    586         if (!isRenewable()) {
    587             throw new RefreshFailedException("This ticket is not renewable");
    588         }
    589
    590         if (getRenewTill() == null) {
    591             // Renewable ticket without renew-till. Illegal and ignored.
    592             return;
    593         }
    594
    595         if (System.currentTimeMillis() > getRenewTill().getTime()) {
    596             throw new RefreshFailedException("This ticket is past "
    597                                            + "its last renewal time.");
    598         }
    599         Throwable e = null;
    600         sun.security.krb5.Credentials krb5Creds = null;
    601
    602         try {
    603             krb5Creds = new sun.security.krb5.Credentials(asn1Encoding,
    604                                                     client.getName(),
    605                                                     (clientAlias != null ?
    606                                                             clientAlias.getName() : null),
    607                                                     server.getName(),
    608                                                     (serverAlias != null ?
    609                                                             serverAlias.getName() : null),
    610                                                     sessionKey.getEncoded(),
    611                                                     sessionKey.getKeyType(),
    612                                                     flags,
    613                                                     authTime,
    614                                                     startTime,
    615                                                     endTime,
    616                                                     renewTill,
    617                                                     clientAddresses);
    618             krb5Creds = krb5Creds.renew();
    619         } catch (sun.security.krb5.KrbException krbException) {
    620             e = krbException;
    621         } catch (java.io.IOException ioException) {
    622             e = ioException;
    623         }
    624
    625         if (e != null) {
    626             RefreshFailedException rfException
    627                 = new RefreshFailedException("Failed to renew Kerberos Ticket "
    628                                              + "for client " + client
    629                                              + " and server " + server
    630                                              + " - " + e.getMessage());
    631             rfException.initCause(e);
    632             throw rfException;
    633         }
    634
    635         /*
    636          * In case multiple threads try to refresh it at the same time.
    637          */
    638         synchronized (this) {
    639             try {
    640                 this.destroy();
    641             } catch (DestroyFailedException dfException) {
    642                 // Squelch it since we don't care about the old ticket.
    643             }
    644             init(krb5Creds.getEncoded(),
    645                  new KerberosPrincipal(krb5Creds.getClient().getName()),
    646                  new KerberosPrincipal(krb5Creds.getServer().getName(),
    647                                         KerberosPrincipal.KRB_NT_SRV_INST),
    648                  krb5Creds.getSessionKey().getBytes(),
    649                  krb5Creds.getSessionKey().getEType(),
    650                  krb5Creds.getFlags(),
    651                  krb5Creds.getAuthTime(),
    652                  krb5Creds.getStartTime(),
    653                  krb5Creds.getEndTime(),
    654                  krb5Creds.getRenewTill(),
    655                  krb5Creds.getClientAddresses());
    656             destroyed = false;
    657         }
    658     }
    659
    660     /**
    661      * Destroys the ticket and destroys any sensitive information stored in
    662      * it.
    663      */
    664     public void destroy() throws DestroyFailedException {
    665         if (!destroyed) {
    666             Arrays.fill(asn1Encoding, (byte) 0);
    667             client = null;
    668             server = null;
    669             sessionKey.destroy();
    670             flags = null;
    671             authTime = null;
    672             startTime = null;
    673             endTime = null;
    674             renewTill = null;
    675             clientAddresses = null;
    676             destroyed = true;
    677         }
    678     }
    679
    680     /**
    681      * Determines if this ticket has been destroyed.
    682      */
    683     public boolean isDestroyed() {
    684         return destroyed;
    685     }
    686
    687     /**
    688      * Returns an informative textual representation of this {@code KerberosTicket}.
    689      *
    690      * @return an informative textual representation of this {@code KerberosTicket}.
    691      */
    692     public String toString() {
    693         if (destroyed) {
    694             return "Destroyed KerberosTicket";
    695         }
    696         StringBuilder caddrString = new StringBuilder();
    697         if (clientAddresses != null) {
    698             for (int i = 0; i < clientAddresses.length; i++) {
    699                 caddrString.append("clientAddresses[" + i + "] = " +
    700                         clientAddresses[i].toString());
    701             }
    702         }
    703         return ("Ticket (hex) = " + "\n" +
    704                  (new HexDumpEncoder()).encodeBuffer(asn1Encoding) + "\n" +
    705                 "Client Principal = " + client.toString() + "\n" +
    706                 "Server Principal = " + server.toString() + "\n" +
    707                 "Session Key = " + sessionKey.toString() + "\n" +
    708                 "Forwardable Ticket " + flags[FORWARDABLE_TICKET_FLAG] + "\n" +
    709                 "Forwarded Ticket " + flags[FORWARDED_TICKET_FLAG] + "\n" +
    710                 "Proxiable Ticket " + flags[PROXIABLE_TICKET_FLAG] + "\n" +
    711                 "Proxy Ticket " + flags[PROXY_TICKET_FLAG] + "\n" +
    712                 "Postdated Ticket " + flags[POSTDATED_TICKET_FLAG] + "\n" +
    713                 "Renewable Ticket " + flags[RENEWABLE_TICKET_FLAG] + "\n" +
    714                 "Initial Ticket " + flags[INITIAL_TICKET_FLAG] + "\n" +
    715                 "Auth Time = " + String.valueOf(authTime) + "\n" +
    716                 "Start Time = " + String.valueOf(startTime) + "\n" +
    717                 "End Time = " + endTime.toString() + "\n" +
    718                 "Renew Till = " + String.valueOf(renewTill) + "\n" +
    719                 "Client Addresses " +
    720                 (clientAddresses == null ? " Null " : caddrString.toString() +
    721                 (proxy == null ? "" : "\nwith a proxy ticket") +
    722                 "\n"));
    723     }
    724
    725     /**
    726      * Returns a hash code for this {@code KerberosTicket}.
    727      *
    728      * @return a hash code for this {@code KerberosTicket}.
    729      * @since 1.6
    730      */
    731     public int hashCode() {
    732         int result = 17;
    733         if (isDestroyed()) {
    734             return result;
    735         }
    736         result = result * 37 + Arrays.hashCode(getEncoded());
    737         result = result * 37 + endTime.hashCode();
    738         result = result * 37 + client.hashCode();
    739         result = result * 37 + server.hashCode();
    740         result = result * 37 + sessionKey.hashCode();
    741
    742         // authTime may be null
    743         if (authTime != null) {
    744             result = result * 37 + authTime.hashCode();
    745         }
    746
    747         // startTime may be null
    748         if (startTime != null) {
    749             result = result * 37 + startTime.hashCode();
    750         }
    751
    752         // renewTill may be null
    753         if (renewTill != null) {
    754             result = result * 37 + renewTill.hashCode();
    755         }
    756
    757         // clientAddress may be null, the array's hashCode is 0
    758         result = result * 37 + Arrays.hashCode(clientAddresses);
    759
    760         if (proxy != null) {
    761             result = result * 37 + proxy.hashCode();
    762         }
    763         return result * 37 + Arrays.hashCode(flags);
    764     }
    765
    766     /**
    767      * Compares the specified object with this {@code KerberosTicket} for equality.
    768      * Returns true if the given object is also a
    769      * {@code KerberosTicket} and the two
    770      * {@code KerberosTicket} instances are equivalent.
    771      * A destroyed {@code KerberosTicket} object is only equal to itself.
    772      *
    773      * @param other the object to compare to
    774      * @return true if the specified object is equal to this {@code KerberosTicket},
    775      * false otherwise.
    776      * @since 1.6
    777      */
    778     public boolean equals(Object other) {
    779
    780         if (other == this) {
    781             return true;
    782         }
    783
    784         if (! (other instanceof KerberosTicket)) {
    785             return false;
    786         }
    787
    788         KerberosTicket otherTicket = ((KerberosTicket) other);
    789         if (isDestroyed() || otherTicket.isDestroyed()) {
    790             return false;
    791         }
    792
    793         if (!Arrays.equals(getEncoded(), otherTicket.getEncoded()) ||
    794                 !endTime.equals(otherTicket.getEndTime()) ||
    795                 !server.equals(otherTicket.getServer()) ||
    796                 !client.equals(otherTicket.getClient()) ||
    797                 !sessionKey.equals(otherTicket.sessionKey) ||
    798                 !Arrays.equals(clientAddresses, otherTicket.getClientAddresses()) ||
    799                 !Arrays.equals(flags, otherTicket.getFlags())) {
    800             return false;
    801         }
    802
    803         // authTime may be null
    804         if (authTime == null) {
    805             if (otherTicket.getAuthTime() != null) {
    806                 return false;
    807             }
    808         } else {
    809             if (!authTime.equals(otherTicket.getAuthTime())) {
    810                 return false;
    811             }
    812         }
    813
    814         // startTime may be null
    815         if (startTime == null) {
    816             if (otherTicket.getStartTime() != null) {
    817                 return false;
    818             }
    819         } else {
    820             if (!startTime.equals(otherTicket.getStartTime())) {
    821                 return false;
    822             }
    823         }
    824
    825         if (renewTill == null) {
    826             if (otherTicket.getRenewTill() != null) {
    827                 return false;
    828             }
    829         } else {
    830             if (!renewTill.equals(otherTicket.getRenewTill())) {
    831                 return false;
    832             }
    833         }
    834
    835         if (!Objects.equals(proxy, otherTicket.proxy)) {
    836             return false;
    837         }
    838
    839         return true;
    840     }
    841

    842     private void readObject(ObjectInputStream s)
    843             throws IOException, ClassNotFoundException {
    844         s.defaultReadObject();
    845         if (sessionKey == null) {
    846            throw new InvalidObjectException("Session key cannot be null");
    847         }
    848         try {
    849             init(asn1Encoding, client, server, sessionKey,
    850                  flags, authTime, startTime, endTime,
    851                  renewTill, clientAddresses);
    852         } catch (IllegalArgumentException iae) {
    853             throw (InvalidObjectException)
    854                 new InvalidObjectException(iae.getMessage()).initCause(iae);
    855         }
    856     }
    857 }
    --- EOF ---