1 /*
   2  * Copyright (c) 2008, 2017, 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.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 import java.lang.reflect.Constructor;
  25 import java.lang.reflect.Field;
  26 import java.lang.reflect.InvocationTargetException;
  27 import java.net.*;
  28 import java.io.*;
  29 import java.lang.reflect.Method;
  30 import java.security.SecureRandom;
  31 import java.util.*;
  32 import java.util.concurrent.*;
  33 
  34 import sun.net.spi.nameservice.NameService;
  35 import sun.net.spi.nameservice.NameServiceDescriptor;
  36 import sun.security.krb5.*;
  37 import sun.security.krb5.internal.*;
  38 import sun.security.krb5.internal.ccache.CredentialsCache;
  39 import sun.security.krb5.internal.crypto.KeyUsage;
  40 import sun.security.krb5.internal.ktab.KeyTab;
  41 import sun.security.util.DerInputStream;
  42 import sun.security.util.DerOutputStream;
  43 import sun.security.util.DerValue;
  44 
  45 /**
  46  * A KDC server.
  47  * <p>
  48  * Features:
  49  * <ol>
  50  * <li> Supports TCP and UDP
  51  * <li> Supports AS-REQ and TGS-REQ
  52  * <li> Principal db and other settings hard coded in application
  53  * <li> Options, say, request preauth or not
  54  * </ol>
  55  * Side effects:
  56  * <ol>
  57  * <li> The Sun-internal class <code>sun.security.krb5.Config</code> is a
  58  * singleton and initialized according to Kerberos settings (krb5.conf and
  59  * java.security.krb5.* system properties). This means once it's initialized
  60  * it will not automatically notice any changes to these settings (or file
  61  * changes of krb5.conf). The KDC class normally does not touch these
  62  * settings (except for the <code>writeKtab()</code> method). However, to make
  63  * sure nothing ever goes wrong, if you want to make any changes to these
  64  * settings after calling a KDC method, call <code>Config.refresh()</code> to
  65  * make sure your changes are reflected in the <code>Config</code> object.
  66  * </ol>
  67  * System properties recognized:
  68  * <ul>
  69  * <li>test.kdc.save.ccache
  70  * </ul>
  71  * Issues and TODOs:
  72  * <ol>
  73  * <li> Generates krb5.conf to be used on another machine, currently the kdc is
  74  * always localhost
  75  * <li> More options to KDC, say, error output, say, response nonce !=
  76  * request nonce
  77  * </ol>
  78  * Note: This program uses internal krb5 classes (including reflection to
  79  * access private fields and methods).
  80  * <p>
  81  * Usages:
  82  * <p>
  83  * 1. Init and start the KDC:
  84  * <pre>
  85  * KDC kdc = KDC.create("REALM.NAME", port, isDaemon);
  86  * KDC kdc = KDC.create("REALM.NAME");
  87  * </pre>
  88  * Here, <code>port</code> is the UDP and TCP port number the KDC server
  89  * listens on. If zero, a random port is chosen, which you can use getPort()
  90  * later to retrieve the value.
  91  * <p>
  92  * If <code>isDaemon</code> is true, the KDC worker threads will be daemons.
  93  * <p>
  94  * The shortcut <code>KDC.create("REALM.NAME")</code> has port=0 and
  95  * isDaemon=false, and is commonly used in an embedded KDC.
  96  * <p>
  97  * 2. Adding users:
  98  * <pre>
  99  * kdc.addPrincipal(String principal_name, char[] password);
 100  * kdc.addPrincipalRandKey(String principal_name);
 101  * </pre>
 102  * A service principal's name should look like "host/f.q.d.n". The second form
 103  * generates a random key. To expose this key, call <code>writeKtab()</code> to
 104  * save the keys into a keytab file.
 105  * <p>
 106  * Note that you need to add the principal name krbtgt/REALM.NAME yourself.
 107  * <p>
 108  * Note that you can safely add a principal at any time after the KDC is
 109  * started and before a user requests info on this principal.
 110  * <p>
 111  * 3. Other public methods:
 112  * <ul>
 113  * <li> <code>getPort</code>: Returns the port number the KDC uses
 114  * <li> <code>getRealm</code>: Returns the realm name
 115  * <li> <code>writeKtab</code>: Writes all principals' keys into a keytab file
 116  * <li> <code>saveConfig</code>: Saves a krb5.conf file to access this KDC
 117  * <li> <code>setOption</code>: Sets various options
 118  * </ul>
 119  * Read the javadoc for details. Lazy developer can use <code>OneKDC</code>
 120  * directly.
 121  */
 122 public class KDC {
 123 
 124     // Under the hood.
 125 
 126     // The random generator to generate random keys (including session keys)
 127     private static SecureRandom secureRandom = new SecureRandom();
 128 
 129     // Principal db. principal -> pass. A case-insensitive TreeMap is used
 130     // so that even if the client provides a name with different case, the KDC
 131     // can still locate the principal and give back correct salt.
 132     private TreeMap<String,char[]> passwords = new TreeMap<>
 133             (String.CASE_INSENSITIVE_ORDER);
 134 
 135     // Realm name
 136     private String realm;
 137     // KDC
 138     private String kdc;
 139     // Service port number
 140     private int port;
 141     // The request/response job queue
 142     private BlockingQueue<Job> q = new ArrayBlockingQueue<>(100);
 143     // Options
 144     private Map<Option,Object> options = new HashMap<>();
 145     // Realm-specific krb5.conf settings
 146     private List<String> conf = new ArrayList<>();
 147 
 148     private Thread thread1, thread2, thread3;
 149     private volatile boolean udpConsumerReady = false;
 150     private volatile boolean tcpConsumerReady = false;
 151     private volatile boolean dispatcherReady = false;
 152     DatagramSocket u1 = null;
 153     ServerSocket t1 = null;
 154 
 155     public static enum KtabMode { APPEND, EXISTING };
 156 
 157     /**
 158      * Option names, to be expanded forever.
 159      */
 160     public static enum Option {
 161         /**
 162          * Whether pre-authentication is required. Default Boolean.TRUE
 163          */
 164         PREAUTH_REQUIRED,
 165         /**
 166          * Only issue TGT in RC4
 167          */
 168         ONLY_RC4_TGT,
 169         /**
 170          * Use RC4 as the first in preauth
 171          */
 172         RC4_FIRST_PREAUTH,
 173         /**
 174          * Use only one preauth, so that some keys are not easy to generate
 175          */
 176         ONLY_ONE_PREAUTH,
 177         /**
 178          * Set all name-type to a value in response
 179          */
 180         RESP_NT,
 181         /**
 182          * Multiple ETYPE-INFO-ENTRY with same etype but different salt
 183          */
 184         DUP_ETYPE,
 185         /**
 186          * What backend server can be delegated to
 187          */
 188         OK_AS_DELEGATE,
 189         /**
 190          * Allow S4U2self, List<String> of middle servers.
 191          * If not set, means KDC does not understand S4U2self at all, therefore
 192          * would ignore any PA-FOR-USER request and send a ticket using the
 193          * cname of teh requestor. If set, it returns FORWARDABLE tickets to
 194          * a server with its name in the list
 195          */
 196         ALLOW_S4U2SELF,
 197         /**
 198          * Allow S4U2proxy, Map<String,List<String>> of middle servers to
 199          * backends. If not set or a backend not in a server's list,
 200          * Krb5.KDC_ERR_POLICY will be send for S4U2proxy request.
 201          */
 202         ALLOW_S4U2PROXY,
 203         /**
 204          * Sensitive accounts can never be delegated.
 205          */
 206         SENSITIVE_ACCOUNTS,
 207     };
 208 
 209     //static {
 210     //    System.setProperty("sun.net.spi.nameservice.provider.1", "ns,mock");
 211     //}
 212 
 213     /**
 214      * A standalone KDC server.
 215      */
 216     public static void main(String[] args) throws Exception {
 217         KDC kdc = create("RABBIT.HOLE", "kdc.rabbit.hole", 0, false);
 218         kdc.addPrincipal("dummy", "bogus".toCharArray());
 219         kdc.addPrincipal("foo", "bar".toCharArray());
 220         kdc.addPrincipalRandKey("krbtgt/RABBIT.HOLE");
 221         kdc.addPrincipalRandKey("server/host.rabbit.hole");
 222         kdc.addPrincipalRandKey("backend/host.rabbit.hole");
 223         KDC.saveConfig("krb5.conf", kdc, "forwardable = true");
 224     }
 225 
 226     /**
 227      * Creates and starts a KDC running as a daemon on a random port.
 228      * @param realm the realm name
 229      * @return the running KDC instance
 230      * @throws java.io.IOException for any socket creation error
 231      */
 232     public static KDC create(String realm) throws IOException {
 233         return create(realm, "kdc." + realm.toLowerCase(), 0, true);
 234     }
 235 
 236     public static KDC existing(String realm, String kdc, int port) {
 237         KDC k = new KDC(realm, kdc);
 238         k.port = port;
 239         return k;
 240     }
 241 
 242     /**
 243      * Creates and starts a KDC server.
 244      * @param realm the realm name
 245      * @param port the TCP and UDP port to listen to. A random port will to
 246      *        chosen if zero.
 247      * @param asDaemon if true, KDC threads will be daemons. Otherwise, not.
 248      * @return the running KDC instance
 249      * @throws java.io.IOException for any socket creation error
 250      */
 251     public static KDC create(String realm, String kdc, int port, boolean asDaemon) throws IOException {
 252         return new KDC(realm, kdc, port, asDaemon);
 253     }
 254 
 255     /**
 256      * Sets an option
 257      * @param key the option name
 258      * @param value the value
 259      */
 260     public void setOption(Option key, Object value) {
 261         if (value == null) {
 262             options.remove(key);
 263         } else {
 264             options.put(key, value);
 265         }
 266     }
 267 
 268     /**
 269      * Writes or appends keys into a keytab.
 270      * <p>
 271      * Attention: This is the most basic one of a series of methods below on
 272      * keytab creation or modification. All these methods reference krb5.conf
 273      * settings. If you need to modify krb5.conf or switch to another krb5.conf
 274      * later, please call <code>Config.refresh()</code> again. For example:
 275      * <pre>
 276      * kdc.writeKtab("/etc/kdc/ktab", true);  // Config is initialized,
 277      * System.setProperty("java.security.krb5.conf", "/home/mykrb5.conf");
 278      * Config.refresh();
 279      * </pre>
 280      * Inside this method there are 2 places krb5.conf is used:
 281      * <ol>
 282      * <li> (Fatal) Generating keys: EncryptionKey.acquireSecretKeys
 283      * <li> (Has workaround) Creating PrincipalName
 284      * </ol>
 285      * @param tab the keytab file name
 286      * @param append true if append, otherwise, overwrite.
 287      * @param names the names to write into, write all if names is empty
 288      */
 289     public void writeKtab(String tab, boolean append, String... names)
 290             throws IOException, KrbException {
 291         KeyTab ktab = append ? KeyTab.getInstance(tab) : KeyTab.create(tab);
 292         Iterable<String> entries =
 293                 (names.length != 0) ? Arrays.asList(names): passwords.keySet();
 294         for (String name : entries) {
 295             char[] pass = passwords.get(name);
 296             int kvno = 0;
 297             if (Character.isDigit(pass[pass.length-1])) {
 298                 kvno = pass[pass.length-1] - '0';
 299             }
 300             PrincipalName pn = new PrincipalName(name,
 301                         name.indexOf('/') < 0 ?
 302                             PrincipalName.KRB_NT_UNKNOWN :
 303                             PrincipalName.KRB_NT_SRV_HST);
 304             ktab.addEntry(pn,
 305                         getSalt(pn),
 306                         pass,
 307                         kvno,
 308                         true);
 309         }
 310         ktab.save();
 311     }
 312 
 313     /**
 314      * Writes all principals' keys from multiple KDCs into one keytab file.
 315      * @throws java.io.IOException for any file output error
 316      * @throws sun.security.krb5.KrbException for any realm and/or principal
 317      *         name error.
 318      */
 319     public static void writeMultiKtab(String tab, KDC... kdcs)
 320             throws IOException, KrbException {
 321         KeyTab.create(tab).save();      // Empty the old keytab
 322         appendMultiKtab(tab, kdcs);
 323     }
 324 
 325     /**
 326      * Appends all principals' keys from multiple KDCs to one keytab file.
 327      */
 328     public static void appendMultiKtab(String tab, KDC... kdcs)
 329             throws IOException, KrbException {
 330         for (KDC kdc: kdcs) {
 331             kdc.writeKtab(tab, true);
 332         }
 333     }
 334 
 335     /**
 336      * Write a ktab for this KDC.
 337      */
 338     public void writeKtab(String tab) throws IOException, KrbException {
 339         writeKtab(tab, false);
 340     }
 341 
 342     /**
 343      * Appends keys in this KDC to a ktab.
 344      */
 345     public void appendKtab(String tab) throws IOException, KrbException {
 346         writeKtab(tab, true);
 347     }
 348 
 349     /**
 350      * Adds a new principal to this realm with a given password.
 351      * @param user the principal's name. For a service principal, use the
 352      *        form of host/f.q.d.n
 353      * @param pass the password for the principal
 354      */
 355     public void addPrincipal(String user, char[] pass) {
 356         if (user.indexOf('@') < 0) {
 357             user = user + "@" + realm;
 358         }
 359         passwords.put(user, pass);
 360     }
 361 
 362     /**
 363      * Adds a new principal to this realm with a random password
 364      * @param user the principal's name. For a service principal, use the
 365      *        form of host/f.q.d.n
 366      */
 367     public void addPrincipalRandKey(String user) {
 368         addPrincipal(user, randomPassword());
 369     }
 370 
 371     /**
 372      * Returns the name of this realm
 373      * @return the name of this realm
 374      */
 375     public String getRealm() {
 376         return realm;
 377     }
 378 
 379     /**
 380      * Returns the name of kdc
 381      * @return the name of kdc
 382      */
 383     public String getKDC() {
 384         return kdc;
 385     }
 386 
 387     /**
 388      * Add realm-specific krb5.conf setting
 389      */
 390     public void addConf(String s) {
 391         conf.add(s);
 392     }
 393 
 394     /**
 395      * Writes a krb5.conf for one or more KDC that includes KDC locations for
 396      * each realm and the default realm name. You can also add extra strings
 397      * into the file. The method should be called like:
 398      * <pre>
 399      *   KDC.saveConfig("krb5.conf", kdc1, kdc2, ..., line1, line2, ...);
 400      * </pre>
 401      * Here you can provide one or more kdc# and zero or more line# arguments.
 402      * The line# will be put after [libdefaults] and before [realms]. Therefore
 403      * you can append new lines into [libdefaults] and/or create your new
 404      * stanzas as well. Note that a newline character will be appended to
 405      * each line# argument.
 406      * <p>
 407      * For example:
 408      * <pre>
 409      * KDC.saveConfig("krb5.conf", this);
 410      * </pre>
 411      * generates:
 412      * <pre>
 413      * [libdefaults]
 414      * default_realm = REALM.NAME
 415      *
 416      * [realms]
 417      *   REALM.NAME = {
 418      *     kdc = host:port_number
 419      *     # realm-specific settings
 420      *   }
 421      * </pre>
 422      *
 423      * Another example:
 424      * <pre>
 425      * KDC.saveConfig("krb5.conf", kdc1, kdc2, "forwardable = true", "",
 426      *         "[domain_realm]",
 427      *         ".kdc1.com = KDC1.NAME");
 428      * </pre>
 429      * generates:
 430      * <pre>
 431      * [libdefaults]
 432      * default_realm = KDC1.NAME
 433      * forwardable = true
 434      *
 435      * [domain_realm]
 436      * .kdc1.com = KDC1.NAME
 437      *
 438      * [realms]
 439      *   KDC1.NAME = {
 440      *     kdc = host:port1
 441      *   }
 442      *   KDC2.NAME = {
 443      *     kdc = host:port2
 444      *   }
 445      * </pre>
 446      * @param file the name of the file to write into
 447      * @param kdc the first (and default) KDC
 448      * @param more more KDCs or extra lines (in their appearing order) to
 449      * insert into the krb5.conf file. This method reads each argument's type
 450      * to determine what it's for. This argument can be empty.
 451      * @throws java.io.IOException for any file output error
 452      */
 453     public static void saveConfig(String file, KDC kdc, Object... more)
 454             throws IOException {
 455         File f = new File(file);
 456         StringBuffer sb = new StringBuffer();
 457         sb.append("[libdefaults]\ndefault_realm = ");
 458         sb.append(kdc.realm);
 459         sb.append("\n");
 460         for (Object o: more) {
 461             if (o instanceof String) {
 462                 sb.append(o);
 463                 sb.append("\n");
 464             }
 465         }
 466         sb.append("\n[realms]\n");
 467         sb.append(kdc.realmLine());
 468         for (Object o: more) {
 469             if (o instanceof KDC) {
 470                 sb.append(((KDC)o).realmLine());
 471             }
 472         }
 473         FileOutputStream fos = new FileOutputStream(f);
 474         fos.write(sb.toString().getBytes());
 475         fos.close();
 476     }
 477 
 478     /**
 479      * Returns the service port of the KDC server.
 480      * @return the KDC service port
 481      */
 482     public int getPort() {
 483         return port;
 484     }
 485 
 486     // Private helper methods
 487 
 488     /**
 489      * Private constructor, cannot be called outside.
 490      * @param realm
 491      */
 492     private KDC(String realm, String kdc) {
 493         this.realm = realm;
 494         this.kdc = kdc;
 495     }
 496 
 497     /**
 498      * A constructor that starts the KDC service also.
 499      */
 500     protected KDC(String realm, String kdc, int port, boolean asDaemon)
 501             throws IOException {
 502         this(realm, kdc);
 503         startServer(port, asDaemon);
 504     }
 505     /**
 506      * Generates a 32-char random password
 507      * @return the password
 508      */
 509     private static char[] randomPassword() {
 510         char[] pass = new char[32];
 511         for (int i=0; i<31; i++)
 512             pass[i] = (char)secureRandom.nextInt();
 513         // The last char cannot be a number, otherwise, keyForUser()
 514         // believes it's a sign of kvno
 515         pass[31] = 'Z';
 516         return pass;
 517     }
 518 
 519     /**
 520      * Generates a random key for the given encryption type.
 521      * @param eType the encryption type
 522      * @return the generated key
 523      * @throws sun.security.krb5.KrbException for unknown/unsupported etype
 524      */
 525     private static EncryptionKey generateRandomKey(int eType)
 526             throws KrbException  {
 527         // Is 32 enough for AES256? I should have generated the keys directly
 528         // but different cryptos have different rules on what keys are valid.
 529         char[] pass = randomPassword();
 530         String algo;
 531         switch (eType) {
 532             case EncryptedData.ETYPE_DES_CBC_MD5: algo = "DES"; break;
 533             case EncryptedData.ETYPE_DES3_CBC_HMAC_SHA1_KD: algo = "DESede"; break;
 534             case EncryptedData.ETYPE_AES128_CTS_HMAC_SHA1_96: algo = "AES128"; break;
 535             case EncryptedData.ETYPE_ARCFOUR_HMAC: algo = "ArcFourHMAC"; break;
 536             case EncryptedData.ETYPE_AES256_CTS_HMAC_SHA1_96: algo = "AES256"; break;
 537             default: algo = "DES"; break;
 538         }
 539         return new EncryptionKey(pass, "NOTHING", algo);    // Silly
 540     }
 541 
 542     /**
 543      * Returns the password for a given principal
 544      * @param p principal
 545      * @return the password
 546      * @throws sun.security.krb5.KrbException when the principal is not inside
 547      *         the database.
 548      */
 549     private char[] getPassword(PrincipalName p, boolean server)
 550             throws KrbException {
 551         String pn = p.toString();
 552         if (p.getRealmString() == null) {
 553             pn = pn + "@" + getRealm();
 554         }
 555         char[] pass = passwords.get(pn);
 556         if (pass == null) {
 557             throw new KrbException(server?
 558                 Krb5.KDC_ERR_S_PRINCIPAL_UNKNOWN:
 559                 Krb5.KDC_ERR_C_PRINCIPAL_UNKNOWN, pn.toString());
 560         }
 561         return pass;
 562     }
 563 
 564     /**
 565      * Returns the salt string for the principal.
 566      * @param p principal
 567      * @return the salt
 568      */
 569     protected String getSalt(PrincipalName p) {
 570         String pn = p.toString();
 571         if (p.getRealmString() == null) {
 572             pn = pn + "@" + getRealm();
 573         }
 574         if (passwords.containsKey(pn)) {
 575             try {
 576                 // Find the principal name with correct case.
 577                 p = new PrincipalName(passwords.ceilingEntry(pn).getKey());
 578             } catch (RealmException re) {
 579                 // Won't happen
 580             }
 581         }
 582         String s = p.getRealmString();
 583         if (s == null) s = getRealm();
 584         for (String n: p.getNameStrings()) {
 585             s += n;
 586         }
 587         return s;
 588     }
 589 
 590     /**
 591      * Returns the key for a given principal of the given encryption type
 592      * @param p the principal
 593      * @param etype the encryption type
 594      * @param server looking for a server principal?
 595      * @return the key
 596      * @throws sun.security.krb5.KrbException for unknown/unsupported etype
 597      */
 598     private EncryptionKey keyForUser(PrincipalName p, int etype, boolean server)
 599             throws KrbException {
 600         try {
 601             // Do not call EncryptionKey.acquireSecretKeys(), otherwise
 602             // the krb5.conf config file would be loaded.
 603             Integer kvno = null;
 604             // For service whose password ending with a number, use it as kvno.
 605             // Kvno must be postive.
 606             if (p.toString().indexOf('/') > 0) {
 607                 char[] pass = getPassword(p, server);
 608                 if (Character.isDigit(pass[pass.length-1])) {
 609                     kvno = pass[pass.length-1] - '0';
 610                 }
 611             }
 612             return new EncryptionKey(EncryptionKeyDotStringToKey(
 613                     getPassword(p, server), getSalt(p), null, etype),
 614                     etype, kvno);
 615         } catch (KrbException ke) {
 616             throw ke;
 617         } catch (Exception e) {
 618             throw new RuntimeException(e);  // should not happen
 619         }
 620     }
 621 
 622     /**
 623      * Processes an incoming request and generates a response.
 624      * @param in the request
 625      * @return the response
 626      * @throws java.lang.Exception for various errors
 627      */
 628     protected byte[] processMessage(byte[] in) throws Exception {
 629         if ((in[0] & 0x1f) == Krb5.KRB_AS_REQ)
 630             return processAsReq(in);
 631         else
 632             return processTgsReq(in);
 633     }
 634 
 635     /**
 636      * Processes a TGS_REQ and generates a TGS_REP (or KRB_ERROR)
 637      * @param in the request
 638      * @return the response
 639      * @throws java.lang.Exception for various errors
 640      */
 641     protected byte[] processTgsReq(byte[] in) throws Exception {
 642         TGSReq tgsReq = new TGSReq(in);
 643         PrincipalName service = tgsReq.reqBody.sname;
 644         if (options.containsKey(KDC.Option.RESP_NT)) {
 645             service = new PrincipalName((int)options.get(KDC.Option.RESP_NT),
 646                     service.getNameStrings(), service.getRealm());
 647         }
 648         try {
 649             System.out.println(realm + "> " + tgsReq.reqBody.cname +
 650                     " sends TGS-REQ for " +
 651                     service + ", " + tgsReq.reqBody.kdcOptions);
 652             KDCReqBody body = tgsReq.reqBody;
 653             int[] eTypes = KDCReqBodyDotEType(body);
 654             int e2 = eTypes[0];     // etype for outgoing session key
 655             int e3 = eTypes[0];     // etype for outgoing ticket
 656 
 657             PAData[] pas = KDCReqDotPAData(tgsReq);
 658 
 659             Ticket tkt = null;
 660             EncTicketPart etp = null;
 661 
 662             PrincipalName cname = null;
 663             boolean allowForwardable = true;
 664 
 665             if (pas == null || pas.length == 0) {
 666                 throw new KrbException(Krb5.KDC_ERR_PADATA_TYPE_NOSUPP);
 667             } else {
 668                 PrincipalName forUserCName = null;
 669                 for (PAData pa: pas) {
 670                     if (pa.getType() == Krb5.PA_TGS_REQ) {
 671                         APReq apReq = new APReq(pa.getValue());
 672                         EncryptedData ed = apReq.authenticator;
 673                         tkt = apReq.ticket;
 674                         int te = tkt.encPart.getEType();
 675                         EncryptionKey kkey = keyForUser(tkt.sname, te, true);
 676                         byte[] bb = tkt.encPart.decrypt(kkey, KeyUsage.KU_TICKET);
 677                         DerInputStream derIn = new DerInputStream(bb);
 678                         DerValue der = derIn.getDerValue();
 679                         etp = new EncTicketPart(der.toByteArray());
 680                         // Finally, cname will be overwritten by PA-FOR-USER
 681                         // if it exists.
 682                         cname = etp.cname;
 683                         System.out.println(realm + "> presenting a ticket of "
 684                                 + etp.cname + " to " + tkt.sname);
 685                     } else if (pa.getType() == Krb5.PA_FOR_USER) {
 686                         if (options.containsKey(Option.ALLOW_S4U2SELF)) {
 687                             PAForUserEnc p4u = new PAForUserEnc(
 688                                     new DerValue(pa.getValue()), null);
 689                             forUserCName = p4u.name;
 690                             System.out.println(realm + "> presenting a PA_FOR_USER "
 691                                     + " in the name of " + p4u.name);
 692                         }
 693                     }
 694                 }
 695                 if (forUserCName != null) {
 696                     List<String> names = (List<String>)options.get(Option.ALLOW_S4U2SELF);
 697                     if (!names.contains(cname.toString())) {
 698                         // Mimic the normal KDC behavior. When a server is not
 699                         // allowed to send S4U2self, do not send an error.
 700                         // Instead, send a ticket which is useless later.
 701                         allowForwardable = false;
 702                     }
 703                     cname = forUserCName;
 704                 }
 705                 if (tkt == null) {
 706                     throw new KrbException(Krb5.KDC_ERR_PADATA_TYPE_NOSUPP);
 707                 }
 708             }
 709 
 710             // Session key for original ticket, TGT
 711             EncryptionKey ckey = etp.key;
 712 
 713             // Session key for session with the service
 714             EncryptionKey key = generateRandomKey(e2);
 715 
 716             // Check time, TODO
 717             KerberosTime till = body.till;
 718             if (till == null) {
 719                 throw new KrbException(Krb5.KDC_ERR_NEVER_VALID); // TODO
 720             } else if (till.isZero()) {
 721                 till = new KerberosTime(new Date().getTime() + 1000 * 3600 * 11);
 722             }
 723 
 724             boolean[] bFlags = new boolean[Krb5.TKT_OPTS_MAX+1];
 725             if (body.kdcOptions.get(KDCOptions.FORWARDABLE)
 726                     && allowForwardable) {
 727                 List<String> sensitives = (List<String>)
 728                         options.get(Option.SENSITIVE_ACCOUNTS);
 729                 if (sensitives != null && sensitives.contains(cname.toString())) {
 730                     // Cannot make FORWARDABLE
 731                 } else {
 732                     bFlags[Krb5.TKT_OPTS_FORWARDABLE] = true;
 733                 }
 734             }
 735             if (body.kdcOptions.get(KDCOptions.FORWARDED) ||
 736                     etp.flags.get(Krb5.TKT_OPTS_FORWARDED)) {
 737                 bFlags[Krb5.TKT_OPTS_FORWARDED] = true;
 738             }
 739             if (body.kdcOptions.get(KDCOptions.RENEWABLE)) {
 740                 bFlags[Krb5.TKT_OPTS_RENEWABLE] = true;
 741                 //renew = new KerberosTime(new Date().getTime() + 1000 * 3600 * 24 * 7);
 742             }
 743             if (body.kdcOptions.get(KDCOptions.PROXIABLE)) {
 744                 bFlags[Krb5.TKT_OPTS_PROXIABLE] = true;
 745             }
 746             if (body.kdcOptions.get(KDCOptions.POSTDATED)) {
 747                 bFlags[Krb5.TKT_OPTS_POSTDATED] = true;
 748             }
 749             if (body.kdcOptions.get(KDCOptions.ALLOW_POSTDATE)) {
 750                 bFlags[Krb5.TKT_OPTS_MAY_POSTDATE] = true;
 751             }
 752             if (body.kdcOptions.get(KDCOptions.CNAME_IN_ADDL_TKT)) {
 753                 if (!options.containsKey(Option.ALLOW_S4U2PROXY)) {
 754                     // Don't understand CNAME_IN_ADDL_TKT
 755                     throw new KrbException(Krb5.KDC_ERR_BADOPTION);
 756                 } else {
 757                     Map<String,List<String>> map = (Map<String,List<String>>)
 758                             options.get(Option.ALLOW_S4U2PROXY);
 759                     Ticket second = KDCReqBodyDotFirstAdditionalTicket(body);
 760                     EncryptionKey key2 = keyForUser(second.sname, second.encPart.getEType(), true);
 761                     byte[] bb = second.encPart.decrypt(key2, KeyUsage.KU_TICKET);
 762                     DerInputStream derIn = new DerInputStream(bb);
 763                     DerValue der = derIn.getDerValue();
 764                     EncTicketPart tktEncPart = new EncTicketPart(der.toByteArray());
 765                     if (!tktEncPart.flags.get(Krb5.TKT_OPTS_FORWARDABLE)) {
 766                         //throw new KrbException(Krb5.KDC_ERR_BADOPTION);
 767                     }
 768                     PrincipalName client = tktEncPart.cname;
 769                     System.out.println(realm + "> and an additional ticket of "
 770                             + client + " to " + second.sname);
 771                     if (map.containsKey(cname.toString())) {
 772                         if (map.get(cname.toString()).contains(service.toString())) {
 773                             System.out.println(realm + "> S4U2proxy OK");
 774                         } else {
 775                             throw new KrbException(Krb5.KDC_ERR_BADOPTION);
 776                         }
 777                     } else {
 778                         throw new KrbException(Krb5.KDC_ERR_BADOPTION);
 779                     }
 780                     cname = client;
 781                 }
 782             }
 783 
 784             String okAsDelegate = (String)options.get(Option.OK_AS_DELEGATE);
 785             if (okAsDelegate != null && (
 786                     okAsDelegate.isEmpty() ||
 787                     okAsDelegate.contains(service.getNameString()))) {
 788                 bFlags[Krb5.TKT_OPTS_DELEGATE] = true;
 789             }
 790             bFlags[Krb5.TKT_OPTS_INITIAL] = true;
 791 
 792             TicketFlags tFlags = new TicketFlags(bFlags);
 793             EncTicketPart enc = new EncTicketPart(
 794                     tFlags,
 795                     key,
 796                     cname,
 797                     new TransitedEncoding(1, new byte[0]),  // TODO
 798                     new KerberosTime(new Date()),
 799                     body.from,
 800                     till, body.rtime,
 801                     body.addresses != null  // always set caddr
 802                             ? body.addresses
 803                             : new HostAddresses(
 804                                 new InetAddress[]{InetAddress.getLocalHost()}),
 805                     null);
 806             EncryptionKey skey = keyForUser(service, e3, true);
 807             if (skey == null) {
 808                 throw new KrbException(Krb5.KDC_ERR_SUMTYPE_NOSUPP); // TODO
 809             }
 810             Ticket t = new Ticket(
 811                     System.getProperty("test.kdc.diff.sname") != null ?
 812                         new PrincipalName("xx" + service.toString()) :
 813                         service,
 814                     new EncryptedData(skey, enc.asn1Encode(), KeyUsage.KU_TICKET)
 815             );
 816             EncTGSRepPart enc_part = new EncTGSRepPart(
 817                     key,
 818                     new LastReq(new LastReqEntry[]{
 819                         new LastReqEntry(0, new KerberosTime(new Date().getTime() - 10000))
 820                     }),
 821                     body.getNonce(),    // TODO: detect replay
 822                     new KerberosTime(new Date().getTime() + 1000 * 3600 * 24),
 823                     // Next 5 and last MUST be same with ticket
 824                     tFlags,
 825                     new KerberosTime(new Date()),
 826                     body.from,
 827                     till, body.rtime,
 828                     service,
 829                     body.addresses != null  // always set caddr
 830                             ? body.addresses
 831                             : new HostAddresses(
 832                                 new InetAddress[]{InetAddress.getLocalHost()})
 833                     );
 834             EncryptedData edata = new EncryptedData(ckey, enc_part.asn1Encode(), KeyUsage.KU_ENC_TGS_REP_PART_SESSKEY);
 835             TGSRep tgsRep = new TGSRep(null,
 836                     cname,
 837                     t,
 838                     edata);
 839             System.out.println("     Return " + tgsRep.cname
 840                     + " ticket for " + tgsRep.ticket.sname + ", flags "
 841                     + tFlags);
 842 
 843             DerOutputStream out = new DerOutputStream();
 844             out.write(DerValue.createTag(DerValue.TAG_APPLICATION,
 845                     true, (byte)Krb5.KRB_TGS_REP), tgsRep.asn1Encode());
 846             return out.toByteArray();
 847         } catch (KrbException ke) {
 848             ke.printStackTrace(System.out);
 849             KRBError kerr = ke.getError();
 850             KDCReqBody body = tgsReq.reqBody;
 851             System.out.println("     Error " + ke.returnCode()
 852                     + " " +ke.returnCodeMessage());
 853             if (kerr == null) {
 854                 kerr = new KRBError(null, null, null,
 855                         new KerberosTime(new Date()),
 856                         0,
 857                         ke.returnCode(),
 858                         body.cname,
 859                         service,
 860                         KrbException.errorMessage(ke.returnCode()),
 861                         null);
 862             }
 863             return kerr.asn1Encode();
 864         }
 865     }
 866 
 867     /**
 868      * Processes a AS_REQ and generates a AS_REP (or KRB_ERROR)
 869      * @param in the request
 870      * @return the response
 871      * @throws java.lang.Exception for various errors
 872      */
 873     protected byte[] processAsReq(byte[] in) throws Exception {
 874         ASReq asReq = new ASReq(in);
 875         int[] eTypes = null;
 876         List<PAData> outPAs = new ArrayList<>();
 877 
 878         PrincipalName service = asReq.reqBody.sname;
 879         if (options.containsKey(KDC.Option.RESP_NT)) {
 880             service = new PrincipalName((int)options.get(KDC.Option.RESP_NT),
 881                     service.getNameStrings(),
 882                     Realm.getDefault());
 883         }
 884         try {
 885             System.out.println(realm + "> " + asReq.reqBody.cname +
 886                     " sends AS-REQ for " +
 887                     service + ", " + asReq.reqBody.kdcOptions);
 888 
 889             KDCReqBody body = asReq.reqBody;
 890 
 891             eTypes = KDCReqBodyDotEType(body);
 892             int eType = eTypes[0];
 893 
 894             EncryptionKey ckey = keyForUser(body.cname, eType, false);
 895             EncryptionKey skey = keyForUser(service, eType, true);
 896 
 897             if (options.containsKey(KDC.Option.ONLY_RC4_TGT)) {
 898                 int tgtEType = EncryptedData.ETYPE_ARCFOUR_HMAC;
 899                 boolean found = false;
 900                 for (int i=0; i<eTypes.length; i++) {
 901                     if (eTypes[i] == tgtEType) {
 902                         found = true;
 903                         break;
 904                     }
 905                 }
 906                 if (!found) {
 907                     throw new KrbException(Krb5.KDC_ERR_ETYPE_NOSUPP);
 908                 }
 909                 skey = keyForUser(service, tgtEType, true);
 910             }
 911             if (ckey == null) {
 912                 throw new KrbException(Krb5.KDC_ERR_ETYPE_NOSUPP);
 913             }
 914             if (skey == null) {
 915                 throw new KrbException(Krb5.KDC_ERR_SUMTYPE_NOSUPP); // TODO
 916             }
 917 
 918             // Session key
 919             EncryptionKey key = generateRandomKey(eType);
 920             // Check time, TODO
 921             KerberosTime till = body.till;
 922             if (till == null) {
 923                 throw new KrbException(Krb5.KDC_ERR_NEVER_VALID); // TODO
 924             } else if (till.isZero()) {
 925                 till = new KerberosTime(new Date().getTime() + 1000 * 3600 * 11);
 926             }
 927             //body.from
 928             boolean[] bFlags = new boolean[Krb5.TKT_OPTS_MAX+1];
 929             if (body.kdcOptions.get(KDCOptions.FORWARDABLE)) {
 930                 List<String> sensitives = (List<String>)
 931                         options.get(Option.SENSITIVE_ACCOUNTS);
 932                 if (sensitives != null && sensitives.contains(body.cname.toString())) {
 933                     // Cannot make FORWARDABLE
 934                 } else {
 935                     bFlags[Krb5.TKT_OPTS_FORWARDABLE] = true;
 936                 }
 937             }
 938             if (body.kdcOptions.get(KDCOptions.RENEWABLE)) {
 939                 bFlags[Krb5.TKT_OPTS_RENEWABLE] = true;
 940                 //renew = new KerberosTime(new Date().getTime() + 1000 * 3600 * 24 * 7);
 941             }
 942             if (body.kdcOptions.get(KDCOptions.PROXIABLE)) {
 943                 bFlags[Krb5.TKT_OPTS_PROXIABLE] = true;
 944             }
 945             if (body.kdcOptions.get(KDCOptions.POSTDATED)) {
 946                 bFlags[Krb5.TKT_OPTS_POSTDATED] = true;
 947             }
 948             if (body.kdcOptions.get(KDCOptions.ALLOW_POSTDATE)) {
 949                 bFlags[Krb5.TKT_OPTS_MAY_POSTDATE] = true;
 950             }
 951             bFlags[Krb5.TKT_OPTS_INITIAL] = true;
 952 
 953             // Creating PA-DATA
 954             DerValue[] pas2 = null, pas = null;
 955             if (options.containsKey(KDC.Option.DUP_ETYPE)) {
 956                 int n = (Integer)options.get(KDC.Option.DUP_ETYPE);
 957                 switch (n) {
 958                     case 1:     // customer's case in 7067974
 959                         pas2 = new DerValue[] {
 960                             new DerValue(new ETypeInfo2(1, null, null).asn1Encode()),
 961                             new DerValue(new ETypeInfo2(1, "", null).asn1Encode()),
 962                             new DerValue(new ETypeInfo2(1, realm, new byte[]{1}).asn1Encode()),
 963                         };
 964                         pas = new DerValue[] {
 965                             new DerValue(new ETypeInfo(1, null).asn1Encode()),
 966                             new DerValue(new ETypeInfo(1, "").asn1Encode()),
 967                             new DerValue(new ETypeInfo(1, realm).asn1Encode()),
 968                         };
 969                         break;
 970                     case 2:     // we still reject non-null s2kparams and prefer E2 over E
 971                         pas2 = new DerValue[] {
 972                             new DerValue(new ETypeInfo2(1, realm, new byte[]{1}).asn1Encode()),
 973                             new DerValue(new ETypeInfo2(1, null, null).asn1Encode()),
 974                             new DerValue(new ETypeInfo2(1, "", null).asn1Encode()),
 975                         };
 976                         pas = new DerValue[] {
 977                             new DerValue(new ETypeInfo(1, realm).asn1Encode()),
 978                             new DerValue(new ETypeInfo(1, null).asn1Encode()),
 979                             new DerValue(new ETypeInfo(1, "").asn1Encode()),
 980                         };
 981                         break;
 982                     case 3:     // but only E is wrong
 983                         pas = new DerValue[] {
 984                             new DerValue(new ETypeInfo(1, realm).asn1Encode()),
 985                             new DerValue(new ETypeInfo(1, null).asn1Encode()),
 986                             new DerValue(new ETypeInfo(1, "").asn1Encode()),
 987                         };
 988                         break;
 989                     case 4:     // we also ignore rc4-hmac
 990                         pas = new DerValue[] {
 991                             new DerValue(new ETypeInfo(23, "ANYTHING").asn1Encode()),
 992                             new DerValue(new ETypeInfo(1, null).asn1Encode()),
 993                             new DerValue(new ETypeInfo(1, "").asn1Encode()),
 994                         };
 995                         break;
 996                     case 5:     // "" should be wrong, but we accept it now
 997                                 // See s.s.k.internal.PAData$SaltAndParams
 998                         pas = new DerValue[] {
 999                             new DerValue(new ETypeInfo(1, "").asn1Encode()),
1000                             new DerValue(new ETypeInfo(1, null).asn1Encode()),
1001                         };
1002                         break;
1003                 }
1004             } else {
1005                 int[] epas = eTypes;
1006                 if (options.containsKey(KDC.Option.RC4_FIRST_PREAUTH)) {
1007                     for (int i=1; i<epas.length; i++) {
1008                         if (epas[i] == EncryptedData.ETYPE_ARCFOUR_HMAC) {
1009                             epas[i] = epas[0];
1010                             epas[0] = EncryptedData.ETYPE_ARCFOUR_HMAC;
1011                             break;
1012                         }
1013                     };
1014                 } else if (options.containsKey(KDC.Option.ONLY_ONE_PREAUTH)) {
1015                     epas = new int[] { eTypes[0] };
1016                 }
1017                 pas2 = new DerValue[epas.length];
1018                 for (int i=0; i<epas.length; i++) {
1019                     pas2[i] = new DerValue(new ETypeInfo2(
1020                             epas[i],
1021                             epas[i] == EncryptedData.ETYPE_ARCFOUR_HMAC ?
1022                                 null : getSalt(body.cname),
1023                             null).asn1Encode());
1024                 }
1025                 boolean allOld = true;
1026                 for (int i: eTypes) {
1027                     if (i == EncryptedData.ETYPE_AES128_CTS_HMAC_SHA1_96 ||
1028                             i == EncryptedData.ETYPE_AES256_CTS_HMAC_SHA1_96) {
1029                         allOld = false;
1030                         break;
1031                     }
1032                 }
1033                 if (allOld) {
1034                     pas = new DerValue[epas.length];
1035                     for (int i=0; i<epas.length; i++) {
1036                         pas[i] = new DerValue(new ETypeInfo(
1037                                 epas[i],
1038                                 epas[i] == EncryptedData.ETYPE_ARCFOUR_HMAC ?
1039                                     null : getSalt(body.cname)
1040                                 ).asn1Encode());
1041                     }
1042                 }
1043             }
1044 
1045             DerOutputStream eid;
1046             if (pas2 != null) {
1047                 eid = new DerOutputStream();
1048                 eid.putSequence(pas2);
1049                 outPAs.add(new PAData(Krb5.PA_ETYPE_INFO2, eid.toByteArray()));
1050             }
1051             if (pas != null) {
1052                 eid = new DerOutputStream();
1053                 eid.putSequence(pas);
1054                 outPAs.add(new PAData(Krb5.PA_ETYPE_INFO, eid.toByteArray()));
1055             }
1056 
1057             PAData[] inPAs = KDCReqDotPAData(asReq);
1058             if (inPAs == null || inPAs.length == 0) {
1059                 Object preauth = options.get(Option.PREAUTH_REQUIRED);
1060                 if (preauth == null || preauth.equals(Boolean.TRUE)) {
1061                     throw new KrbException(Krb5.KDC_ERR_PREAUTH_REQUIRED);
1062                 }
1063             } else {
1064                 try {
1065                     EncryptedData data = newEncryptedData(new DerValue(inPAs[0].getValue()));
1066                     EncryptionKey pakey = keyForUser(body.cname, data.getEType(), false);
1067                     data.decrypt(pakey, KeyUsage.KU_PA_ENC_TS);
1068                 } catch (Exception e) {
1069                     throw new KrbException(Krb5.KDC_ERR_PREAUTH_FAILED);
1070                 }
1071                 bFlags[Krb5.TKT_OPTS_PRE_AUTHENT] = true;
1072             }
1073 
1074             TicketFlags tFlags = new TicketFlags(bFlags);
1075             EncTicketPart enc = new EncTicketPart(
1076                     tFlags,
1077                     key,
1078                     body.cname,
1079                     new TransitedEncoding(1, new byte[0]),
1080                     new KerberosTime(new Date()),
1081                     body.from,
1082                     till, body.rtime,
1083                     body.addresses,
1084                     null);
1085             Ticket t = new Ticket(
1086                     service,
1087                     new EncryptedData(skey, enc.asn1Encode(), KeyUsage.KU_TICKET)
1088             );
1089             EncASRepPart enc_part = new EncASRepPart(
1090                     key,
1091                     new LastReq(new LastReqEntry[]{
1092                         new LastReqEntry(0, new KerberosTime(new Date().getTime() - 10000))
1093                     }),
1094                     body.getNonce(),    // TODO: detect replay?
1095                     new KerberosTime(new Date().getTime() + 1000 * 3600 * 24),
1096                     // Next 5 and last MUST be same with ticket
1097                     tFlags,
1098                     new KerberosTime(new Date()),
1099                     body.from,
1100                     till, body.rtime,
1101                     service,
1102                     body.addresses
1103                     );
1104             EncryptedData edata = new EncryptedData(ckey, enc_part.asn1Encode(), KeyUsage.KU_ENC_AS_REP_PART);
1105             ASRep asRep = new ASRep(
1106                     outPAs.toArray(new PAData[outPAs.size()]),
1107                     body.cname,
1108                     t,
1109                     edata);
1110 
1111             System.out.println("     Return " + asRep.cname
1112                     + " ticket for " + asRep.ticket.sname + ", flags "
1113                     + tFlags);
1114 
1115             DerOutputStream out = new DerOutputStream();
1116             out.write(DerValue.createTag(DerValue.TAG_APPLICATION,
1117                     true, (byte)Krb5.KRB_AS_REP), asRep.asn1Encode());
1118             byte[] result = out.toByteArray();
1119 
1120             // Added feature:
1121             // Write the current issuing TGT into a ccache file specified
1122             // by the system property below.
1123             String ccache = System.getProperty("test.kdc.save.ccache");
1124             if (ccache != null) {
1125                 asRep.encKDCRepPart = enc_part;
1126                 sun.security.krb5.internal.ccache.Credentials credentials =
1127                     new sun.security.krb5.internal.ccache.Credentials(asRep);
1128                 CredentialsCache cache =
1129                     CredentialsCache.create(asReq.reqBody.cname, ccache);
1130                 if (cache == null) {
1131                    throw new IOException("Unable to create the cache file " +
1132                                          ccache);
1133                 }
1134                 cache.update(credentials);
1135                 cache.save();
1136             }
1137 
1138             return result;
1139         } catch (KrbException ke) {
1140             ke.printStackTrace(System.out);
1141             KRBError kerr = ke.getError();
1142             KDCReqBody body = asReq.reqBody;
1143             System.out.println("     Error " + ke.returnCode()
1144                     + " " +ke.returnCodeMessage());
1145             byte[] eData = null;
1146             if (kerr == null) {
1147                 if (ke.returnCode() == Krb5.KDC_ERR_PREAUTH_REQUIRED ||
1148                         ke.returnCode() == Krb5.KDC_ERR_PREAUTH_FAILED) {
1149                     DerOutputStream bytes = new DerOutputStream();
1150                     bytes.write(new PAData(Krb5.PA_ENC_TIMESTAMP, new byte[0]).asn1Encode());
1151                     for (PAData p: outPAs) {
1152                         bytes.write(p.asn1Encode());
1153                     }
1154                     DerOutputStream temp = new DerOutputStream();
1155                     temp.write(DerValue.tag_Sequence, bytes);
1156                     eData = temp.toByteArray();
1157                 }
1158                 kerr = new KRBError(null, null, null,
1159                         new KerberosTime(new Date()),
1160                         0,
1161                         ke.returnCode(),
1162                         body.cname,
1163                         service,
1164                         KrbException.errorMessage(ke.returnCode()),
1165                         eData);
1166             }
1167             return kerr.asn1Encode();
1168         }
1169     }
1170 
1171     /**
1172      * Generates a line for a KDC to put inside [realms] of krb5.conf
1173      * @return REALM.NAME = { kdc = host:port etc }
1174      */
1175     private String realmLine() {
1176         StringBuilder sb = new StringBuilder();
1177         sb.append(realm).append(" = {\n    kdc = ")
1178                 .append(kdc).append(':').append(port).append('\n');
1179         for (String s: conf) {
1180             sb.append("    ").append(s).append('\n');
1181         }
1182         return sb.append("}\n").toString();
1183     }
1184 
1185     /**
1186      * Start the KDC service. This server listens on both UDP and TCP using
1187      * the same port number. It uses three threads to deal with requests.
1188      * They can be set to daemon threads if requested.
1189      * @param port the port number to listen to. If zero, a random available
1190      *  port no less than 8000 will be chosen and used.
1191      * @param asDaemon true if the KDC threads should be daemons
1192      * @throws java.io.IOException for any communication error
1193      */
1194     protected void startServer(int port, boolean asDaemon) throws IOException {
1195         if (port > 0) {
1196             u1 = new DatagramSocket(port, InetAddress.getByName("127.0.0.1"));
1197             t1 = new ServerSocket(port);
1198         } else {
1199             while (true) {
1200                 // Try to find a port number that's both TCP and UDP free
1201                 try {
1202                     port = 8000 + new java.util.Random().nextInt(10000);
1203                     u1 = null;
1204                     u1 = new DatagramSocket(port, InetAddress.getByName("127.0.0.1"));
1205                     t1 = new ServerSocket(port);
1206                     break;
1207                 } catch (Exception e) {
1208                     if (u1 != null) u1.close();
1209                 }
1210             }
1211         }
1212         final DatagramSocket udp = u1;
1213         final ServerSocket tcp = t1;
1214         System.out.println("Start KDC on " + port);
1215 
1216         this.port = port;
1217 
1218         // The UDP consumer
1219         thread1 = new Thread() {
1220             public void run() {
1221                 udpConsumerReady = true;
1222                 while (true) {
1223                     try {
1224                         byte[] inbuf = new byte[8192];
1225                         DatagramPacket p = new DatagramPacket(inbuf, inbuf.length);
1226                         udp.receive(p);
1227                         System.out.println("-----------------------------------------------");
1228                         System.out.println(">>>>> UDP packet received");
1229                         q.put(new Job(processMessage(Arrays.copyOf(inbuf, p.getLength())), udp, p));
1230                     } catch (Exception e) {
1231                         e.printStackTrace();
1232                     }
1233                 }
1234             }
1235         };
1236         thread1.setDaemon(asDaemon);
1237         thread1.start();
1238 
1239         // The TCP consumer
1240         thread2 = new Thread() {
1241             public void run() {
1242                 tcpConsumerReady = true;
1243                 while (true) {
1244                     try {
1245                         Socket socket = tcp.accept();
1246                         System.out.println("-----------------------------------------------");
1247                         System.out.println(">>>>> TCP connection established");
1248                         DataInputStream in = new DataInputStream(socket.getInputStream());
1249                         DataOutputStream out = new DataOutputStream(socket.getOutputStream());
1250                         byte[] token = new byte[in.readInt()];
1251                         in.readFully(token);
1252                         q.put(new Job(processMessage(token), socket, out));
1253                     } catch (Exception e) {
1254                         e.printStackTrace();
1255                     }
1256                 }
1257             }
1258         };
1259         thread2.setDaemon(asDaemon);
1260         thread2.start();
1261 
1262         // The dispatcher
1263         thread3 = new Thread() {
1264             public void run() {
1265                 dispatcherReady = true;
1266                 while (true) {
1267                     try {
1268                         q.take().send();
1269                     } catch (Exception e) {
1270                     }
1271                 }
1272             }
1273         };
1274         thread3.setDaemon(true);
1275         thread3.start();
1276 
1277         // wait for the KDC is ready
1278         try {
1279             while (!isReady()) {
1280                 Thread.sleep(100);
1281             }
1282         } catch(InterruptedException e) {
1283             throw new IOException(e);
1284         }
1285     }
1286 
1287     boolean isReady() {
1288         return udpConsumerReady && tcpConsumerReady && dispatcherReady;
1289     }
1290 
1291     public void terminate() {
1292         try {
1293             thread1.stop();
1294             thread2.stop();
1295             thread3.stop();
1296             u1.close();
1297             t1.close();
1298         } catch (Exception e) {
1299             // OK
1300         }
1301     }
1302 
1303     public static KDC startKDC(final String host, final String krbConfFileName,
1304             final String realm, final Map<String, String> principals,
1305             final String ktab, final KtabMode mode) {
1306 
1307         KDC kdc;
1308         try {
1309             kdc = KDC.create(realm, host, 0, true);
1310             kdc.setOption(KDC.Option.PREAUTH_REQUIRED, Boolean.FALSE);
1311             if (krbConfFileName != null) {
1312                 KDC.saveConfig(krbConfFileName, kdc);
1313             }
1314 
1315             // Add principals
1316             if (principals != null) {
1317                 principals.forEach((name, password) -> {
1318                     if (password == null || password.isEmpty()) {
1319                         System.out.println(String.format(
1320                                 "KDC:add a principal '%s' with a random " +
1321                                         "password", name));
1322                         kdc.addPrincipalRandKey(name);
1323                     } else {
1324                         System.out.println(String.format(
1325                                 "KDC:add a principal '%s' with '%s' password",
1326                                 name, password));
1327                         kdc.addPrincipal(name, password.toCharArray());
1328                     }
1329                 });
1330             }
1331 
1332             // Create or append keys to existing keytab file
1333             if (ktab != null) {
1334                 File ktabFile = new File(ktab);
1335                 switch(mode) {
1336                     case APPEND:
1337                         if (ktabFile.exists()) {
1338                             System.out.println(String.format(
1339                                     "KDC:append keys to an exising keytab "
1340                                     + "file %s", ktab));
1341                             kdc.appendKtab(ktab);
1342                         } else {
1343                             System.out.println(String.format(
1344                                     "KDC:create a new keytab file %s", ktab));
1345                             kdc.writeKtab(ktab);
1346                         }
1347                         break;
1348                     case EXISTING:
1349                         System.out.println(String.format(
1350                                 "KDC:use an existing keytab file %s", ktab));
1351                         break;
1352                     default:
1353                         throw new RuntimeException(String.format(
1354                                 "KDC:unsupported keytab mode: %s", mode));
1355                 }
1356             }
1357 
1358             System.out.println(String.format(
1359                     "KDC: started on %s:%s with '%s' realm",
1360                     host, kdc.getPort(), realm));
1361         } catch (Exception e) {
1362             throw new RuntimeException("KDC: unexpected exception", e);
1363         }
1364 
1365         return kdc;
1366     }
1367 
1368     /**
1369      * Helper class to encapsulate a job in a KDC.
1370      */
1371     private static class Job {
1372         byte[] token;           // The received request at creation time and
1373                                 // the response at send time
1374         Socket s;               // The TCP socket from where the request comes
1375         DataOutputStream out;   // The OutputStream of the TCP socket
1376         DatagramSocket s2;      // The UDP socket from where the request comes
1377         DatagramPacket dp;      // The incoming UDP datagram packet
1378         boolean useTCP;         // Whether TCP or UDP is used
1379 
1380         // Creates a job object for TCP
1381         Job(byte[] token, Socket s, DataOutputStream out) {
1382             useTCP = true;
1383             this.token = token;
1384             this.s = s;
1385             this.out = out;
1386         }
1387 
1388         // Creates a job object for UDP
1389         Job(byte[] token, DatagramSocket s2, DatagramPacket dp) {
1390             useTCP = false;
1391             this.token = token;
1392             this.s2 = s2;
1393             this.dp = dp;
1394         }
1395 
1396         // Sends the output back to the client
1397         void send() {
1398             try {
1399                 if (useTCP) {
1400                     System.out.println(">>>>> TCP request honored");
1401                     out.writeInt(token.length);
1402                     out.write(token);
1403                     s.close();
1404                 } else {
1405                     System.out.println(">>>>> UDP request honored");
1406                     s2.send(new DatagramPacket(token, token.length, dp.getAddress(), dp.getPort()));
1407                 }
1408             } catch (Exception e) {
1409                 e.printStackTrace();
1410             }
1411         }
1412     }
1413 
1414     public static class KDCNameService implements NameServiceDescriptor {
1415 
1416         public static String NOT_EXISTING_HOST = "not.existing.host";
1417 
1418         @Override
1419         public NameService createNameService() throws Exception {
1420             NameService ns = new NameService() {
1421                 @Override
1422                 public InetAddress[] lookupAllHostAddr(String host)
1423                         throws UnknownHostException {
1424                     // Everything is localhost except NOT_EXISTING_HOST
1425                     if (NOT_EXISTING_HOST.equals(host)) {
1426                         throw new UnknownHostException("Unknown host name: "
1427                                 + NOT_EXISTING_HOST);
1428                     }
1429                     return new InetAddress[]{
1430                         InetAddress.getByAddress(host, new byte[]{127,0,0,1})
1431                     };
1432                 }
1433                 @Override
1434                 public String getHostByAddr(byte[] addr)
1435                         throws UnknownHostException {
1436                     // No reverse lookup, PrincipalName use original string
1437                     throw new UnknownHostException();
1438                 }
1439             };
1440             return ns;
1441         }
1442 
1443         @Override
1444         public String getProviderName() {
1445             return "mock";
1446         }
1447 
1448         @Override
1449         public String getType() {
1450             return "ns";
1451         }
1452     }
1453 
1454     // Calling private methods thru reflections
1455     private static final Field getPADataField;
1456     private static final Field getEType;
1457     private static final Constructor<EncryptedData> ctorEncryptedData;
1458     private static final Method stringToKey;
1459     private static final Field getAddlTkt;
1460 
1461     static {
1462         try {
1463             ctorEncryptedData = EncryptedData.class.getDeclaredConstructor(DerValue.class);
1464             ctorEncryptedData.setAccessible(true);
1465             getPADataField = KDCReq.class.getDeclaredField("pAData");
1466             getPADataField.setAccessible(true);
1467             getEType = KDCReqBody.class.getDeclaredField("eType");
1468             getEType.setAccessible(true);
1469             stringToKey = EncryptionKey.class.getDeclaredMethod(
1470                     "stringToKey",
1471                     char[].class, String.class, byte[].class, Integer.TYPE);
1472             stringToKey.setAccessible(true);
1473             getAddlTkt = KDCReqBody.class.getDeclaredField("additionalTickets");
1474             getAddlTkt.setAccessible(true);
1475         } catch (NoSuchFieldException nsfe) {
1476             throw new AssertionError(nsfe);
1477         } catch (NoSuchMethodException nsme) {
1478             throw new AssertionError(nsme);
1479         }
1480     }
1481     private EncryptedData newEncryptedData(DerValue der) {
1482         try {
1483             return ctorEncryptedData.newInstance(der);
1484         } catch (Exception e) {
1485             throw new AssertionError(e);
1486         }
1487     }
1488     private static PAData[] KDCReqDotPAData(KDCReq req) {
1489         try {
1490             return (PAData[])getPADataField.get(req);
1491         } catch (Exception e) {
1492             throw new AssertionError(e);
1493         }
1494     }
1495     private static int[] KDCReqBodyDotEType(KDCReqBody body) {
1496         try {
1497             return (int[]) getEType.get(body);
1498         } catch (Exception e) {
1499             throw new AssertionError(e);
1500         }
1501     }
1502     private static byte[] EncryptionKeyDotStringToKey(char[] password, String salt,
1503             byte[] s2kparams, int keyType) throws KrbCryptoException {
1504         try {
1505             return (byte[])stringToKey.invoke(
1506                     null, password, salt, s2kparams, keyType);
1507         } catch (InvocationTargetException ex) {
1508             throw (KrbCryptoException)ex.getCause();
1509         } catch (Exception e) {
1510             throw new AssertionError(e);
1511         }
1512     }
1513     private static Ticket KDCReqBodyDotFirstAdditionalTicket(KDCReqBody body) {
1514         try {
1515             return ((Ticket[])getAddlTkt.get(body))[0];
1516         } catch (Exception e) {
1517             throw new AssertionError(e);
1518         }
1519     }
1520 }