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