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 /*
  27  *
  28  *  (C) Copyright IBM Corp. 1999 All Rights Reserved.
  29  *  Copyright 1997 The Open Group Research Institute.  All rights reserved.
  30  */
  31 package sun.security.krb5;
  32 
  33 import java.io.File;
  34 import java.io.FilePermission;
  35 import java.nio.file.DirectoryStream;
  36 import java.nio.file.Files;
  37 import java.nio.file.Paths;
  38 import java.nio.file.Path;
  39 import java.security.PrivilegedAction;
  40 import java.util.*;
  41 import java.io.IOException;
  42 import java.net.InetAddress;
  43 import java.net.UnknownHostException;
  44 import java.security.AccessController;
  45 import java.security.PrivilegedExceptionAction;
  46 
  47 import sun.net.dns.ResolverConfiguration;
  48 import sun.security.krb5.internal.crypto.EType;
  49 import sun.security.krb5.internal.Krb5;
  50 
  51 /**
  52  * This class maintains key-value pairs of Kerberos configurable constants
  53  * from configuration file or from user specified system properties.
  54  */
  55 
  56 public class Config {
  57 
  58     /*
  59      * Only allow a single instance of Config.
  60      */
  61     private static Config singleton = null;
  62 
  63     /*
  64      * Hashtable used to store configuration information.
  65      */
  66     private Hashtable<String,Object> stanzaTable = new Hashtable<>();
  67 
  68     private static boolean DEBUG = sun.security.krb5.internal.Krb5.DEBUG;
  69 
  70     // these are used for hexdecimal calculation.
  71     private static final int BASE16_0 = 1;
  72     private static final int BASE16_1 = 16;
  73     private static final int BASE16_2 = 16 * 16;
  74     private static final int BASE16_3 = 16 * 16 * 16;
  75 
  76     /**
  77      * Specified by system properties. Must be both null or non-null.
  78      */
  79     private final String defaultRealm;
  80     private final String defaultKDC;
  81 
  82     // used for native interface
  83     private static native String getWindowsDirectory(boolean isSystem);
  84 
  85 
  86     /**
  87      * Gets an instance of Config class. One and only one instance (the
  88      * singleton) is returned.
  89      *
  90      * @exception KrbException if error occurs when constructing a Config
  91      * instance. Possible causes would be either of java.security.krb5.realm or
  92      * java.security.krb5.kdc not specified, error reading configuration file.
  93      */
  94     public static synchronized Config getInstance() throws KrbException {
  95         if (singleton == null) {
  96             singleton = new Config();
  97         }
  98         return singleton;
  99     }
 100 
 101     /**
 102      * Refresh and reload the Configuration. This could involve,
 103      * for example reading the Configuration file again or getting
 104      * the java.security.krb5.* system properties again. This method
 105      * also tries its best to update static fields in other classes
 106      * that depend on the configuration.
 107      *
 108      * @exception KrbException if error occurs when constructing a Config
 109      * instance. Possible causes would be either of java.security.krb5.realm or
 110      * java.security.krb5.kdc not specified, error reading configuration file.
 111      */
 112 
 113     public static synchronized void refresh() throws KrbException {
 114         singleton = new Config();
 115         KdcComm.initStatic();
 116         EType.initStatic();
 117         Checksum.initStatic();
 118     }
 119 
 120 
 121     private static boolean isMacosLionOrBetter() {
 122         // split the "10.x.y" version number
 123         String osname = getProperty("os.name");
 124         if (!osname.contains("OS X")) {
 125             return false;
 126         }
 127 
 128         String osVersion = getProperty("os.version");
 129         String[] fragments = osVersion.split("\\.");
 130 
 131         // sanity check the "10." part of the version
 132         if (!fragments[0].equals("10")) return false;
 133         if (fragments.length < 2) return false;
 134 
 135         // check if Mac OS X 10.7(.y)
 136         try {
 137             int minorVers = Integer.parseInt(fragments[1]);
 138             if (minorVers >= 7) return true;
 139         } catch (NumberFormatException e) {
 140             // was not an integer
 141         }
 142 
 143         return false;
 144     }
 145 
 146     /**
 147      * Private constructor - can not be instantiated externally.
 148      */
 149     private Config() throws KrbException {
 150         /*
 151          * If either one system property is specified, we throw exception.
 152          */
 153         String tmp = getProperty("java.security.krb5.kdc");
 154         if (tmp != null) {
 155             // The user can specify a list of kdc hosts separated by ":"
 156             defaultKDC = tmp.replace(':', ' ');
 157         } else {
 158             defaultKDC = null;
 159         }
 160         defaultRealm = getProperty("java.security.krb5.realm");
 161         if ((defaultKDC == null && defaultRealm != null) ||
 162             (defaultRealm == null && defaultKDC != null)) {
 163             throw new KrbException
 164                 ("System property java.security.krb5.kdc and " +
 165                  "java.security.krb5.realm both must be set or " +
 166                  "neither must be set.");
 167         }
 168 
 169         // Always read the Kerberos configuration file
 170         try {
 171             List<String> configFile;
 172             String fileName = getJavaFileName();
 173             if (fileName != null) {
 174                 configFile = loadConfigFile(fileName);
 175                 stanzaTable = parseStanzaTable(configFile);
 176                 if (DEBUG) {
 177                     System.out.println("Loaded from Java config");
 178                 }
 179             } else {
 180                 boolean found = false;
 181                 if (isMacosLionOrBetter()) {
 182                     try {
 183                         stanzaTable = SCDynamicStoreConfig.getConfig();
 184                         if (DEBUG) {
 185                             System.out.println("Loaded from SCDynamicStoreConfig");
 186                         }
 187                         found = true;
 188                     } catch (IOException ioe) {
 189                         // OK. Will go on with file
 190                     }
 191                 }
 192                 if (!found) {
 193                     fileName = getNativeFileName();
 194                     configFile = loadConfigFile(fileName);
 195                     stanzaTable = parseStanzaTable(configFile);
 196                     if (DEBUG) {
 197                         System.out.println("Loaded from native config");
 198                     }
 199                 }
 200             }
 201         } catch (IOException ioe) {
 202             if (DEBUG) {
 203                 System.out.println("Exception thrown in loading config:");
 204                 ioe.printStackTrace(System.out);
 205             }
 206             throw new KrbException("krb5.conf loading failed");
 207         }
 208     }
 209 
 210     /**
 211      * Gets the last-defined string value for the specified keys.
 212      * @param keys the keys, as an array from section name, sub-section names
 213      * (if any), to value name.
 214      * @return the value. When there are multiple values for the same key,
 215      * returns the first one. {@code null} is returned if not all the keys are
 216      * defined. For example, {@code get("libdefaults", "forwardable")} will
 217      * return null if "forwardable" is not defined in [libdefaults], and
 218      * {@code get("realms", "R", "kdc")} will return null if "R" is not
 219      * defined in [realms] or "kdc" is not defined for "R".
 220      * @throws IllegalArgumentException if any of the keys is illegal, either
 221      * because a key not the last one is not a (sub)section name or the last
 222      * key is still a section name. For example, {@code get("libdefaults")}
 223      * throws this exception because [libdefaults] is a section name instead of
 224      * a value name, and {@code get("libdefaults", "forwardable", "tail")}
 225      * also throws this exception because "forwardable" is already a value name
 226      * and has no sub-key at all (given "forwardable" is defined, otherwise,
 227      * this method has no knowledge if it's a value name or a section name),
 228      */
 229     public String get(String... keys) {
 230         Vector<String> v = getString0(keys);
 231         if (v == null) return null;
 232         return v.firstElement();
 233     }
 234 
 235     /**
 236      * Gets the boolean value for the specified keys. Returns TRUE if the
 237      * string value is "yes", or "true", FALSE if "no", or "false", or null
 238      * if otherwise or not defined. The comparision is case-insensitive.
 239      *
 240      * @param keys the keys, see {@link #get(String...)}
 241      * @return the boolean value, or null if there is no value defined or the
 242      * value does not look like a boolean value.
 243      * @throws IllegalArgumentException see {@link #get(String...)}
 244      */
 245     public Boolean getBooleanObject(String... keys) {
 246         String s = get(keys);
 247         if (s == null) {
 248             return null;
 249         }
 250         switch (s.toLowerCase(Locale.US)) {
 251             case "yes": case "true":
 252                 return Boolean.TRUE;
 253             case "no": case "false":
 254                 return Boolean.FALSE;
 255             default:
 256                 return null;
 257         }
 258     }
 259 
 260     /**
 261      * Gets all values for the specified keys.
 262      * @throws IllegalArgumentException if any of the keys is illegal
 263      *         (See {@link #get})
 264      */
 265     public String getAll(String... keys) {
 266         Vector<String> v = getString0(keys);
 267         if (v == null) return null;
 268         StringBuilder sb = new StringBuilder();
 269         boolean first = true;
 270         for (String s: v) {
 271             if (first) {
 272                 sb.append(s);
 273                 first = false;
 274             } else {
 275                 sb.append(' ').append(s);
 276             }
 277         }
 278         return sb.toString();
 279     }
 280 
 281     /**
 282      * Returns true if keys exists, can be final string(s) or a sub-section
 283      * @throws IllegalArgumentException if any of the keys is illegal
 284      *         (See {@link #get})
 285      */
 286     public boolean exists(String... keys) {
 287         return get0(keys) != null;
 288     }
 289 
 290     // Returns final string value(s) for given keys.
 291     @SuppressWarnings("unchecked")
 292     private Vector<String> getString0(String... keys) {
 293         try {
 294             return (Vector<String>)get0(keys);
 295         } catch (ClassCastException cce) {
 296             throw new IllegalArgumentException(cce);
 297         }
 298     }
 299 
 300     // Internal method. Returns the value for keys, which can be a sub-section
 301     // (as a Hashtable) or final string value(s) (as a Vector). This is the
 302     // only method (except for toString) that reads stanzaTable directly.
 303     @SuppressWarnings("unchecked")
 304     private Object get0(String... keys) {
 305         Object current = stanzaTable;
 306         try {
 307             for (String key: keys) {
 308                 current = ((Hashtable<String,Object>)current).get(key);
 309                 if (current == null) return null;
 310             }
 311             return current;
 312         } catch (ClassCastException cce) {
 313             throw new IllegalArgumentException(cce);
 314         }
 315     }
 316 
 317     /**
 318      * Gets the int value for the specified keys.
 319      * @param keys the keys
 320      * @return the int value, Integer.MIN_VALUE is returned if it cannot be
 321      * found or the value is not a legal integer.
 322      * @throw IllegalArgumentException if any of the keys is illegal
 323      * @see #get(java.lang.String[])
 324      */
 325     public int getIntValue(String... keys) {
 326         String result = get(keys);
 327         int value = Integer.MIN_VALUE;
 328         if (result != null) {
 329             try {
 330                 value = parseIntValue(result);
 331             } catch (NumberFormatException e) {
 332                 if (DEBUG) {
 333                     System.out.println("Exception in getting value of " +
 334                                        Arrays.toString(keys) + " " +
 335                                        e.getMessage());
 336                     System.out.println("Setting " + Arrays.toString(keys) +
 337                                        " to minimum value");
 338                 }
 339                 value = Integer.MIN_VALUE;
 340             }
 341         }
 342         return value;
 343     }
 344 
 345     /**
 346      * Parses a string to an integer. The convertible strings include the
 347      * string representations of positive integers, negative integers, and
 348      * hex decimal integers.  Valid inputs are, e.g., -1234, +1234,
 349      * 0x40000.
 350      *
 351      * @param input the String to be converted to an Integer.
 352      * @return an numeric value represented by the string
 353      * @exception NumberFormatException if the String does not contain a
 354      * parsable integer.
 355      */
 356     private int parseIntValue(String input) throws NumberFormatException {
 357         int value = 0;
 358         if (input.startsWith("+")) {
 359             String temp = input.substring(1);
 360             return Integer.parseInt(temp);
 361         } else if (input.startsWith("0x")) {
 362             String temp = input.substring(2);
 363             char[] chars = temp.toCharArray();
 364             if (chars.length > 8) {
 365                 throw new NumberFormatException();
 366             } else {
 367                 for (int i = 0; i < chars.length; i++) {
 368                     int index = chars.length - i - 1;
 369                     switch (chars[i]) {
 370                     case '0':
 371                         value += 0;
 372                         break;
 373                     case '1':
 374                         value += 1 * getBase(index);
 375                         break;
 376                     case '2':
 377                         value += 2 * getBase(index);
 378                         break;
 379                     case '3':
 380                         value += 3 * getBase(index);
 381                         break;
 382                     case '4':
 383                         value += 4 * getBase(index);
 384                         break;
 385                     case '5':
 386                         value += 5 * getBase(index);
 387                         break;
 388                     case '6':
 389                         value += 6 * getBase(index);
 390                         break;
 391                     case '7':
 392                         value += 7 * getBase(index);
 393                         break;
 394                     case '8':
 395                         value += 8 * getBase(index);
 396                         break;
 397                     case '9':
 398                         value += 9 * getBase(index);
 399                         break;
 400                     case 'a':
 401                     case 'A':
 402                         value += 10 * getBase(index);
 403                         break;
 404                     case 'b':
 405                     case 'B':
 406                         value += 11 * getBase(index);
 407                         break;
 408                     case 'c':
 409                     case 'C':
 410                         value += 12 * getBase(index);
 411                         break;
 412                     case 'd':
 413                     case 'D':
 414                         value += 13 * getBase(index);
 415                         break;
 416                     case 'e':
 417                     case 'E':
 418                         value += 14 * getBase(index);
 419                         break;
 420                     case 'f':
 421                     case 'F':
 422                         value += 15 * getBase(index);
 423                         break;
 424                     default:
 425                         throw new NumberFormatException("Invalid numerical format");
 426                     }
 427                 }
 428             }
 429             if (value < 0) {
 430                 throw new NumberFormatException("Data overflow.");
 431             }
 432         } else {
 433             value = Integer.parseInt(input);
 434         }
 435         return value;
 436     }
 437 
 438     private int getBase(int i) {
 439         int result = 16;
 440         switch (i) {
 441         case 0:
 442             result = BASE16_0;
 443             break;
 444         case 1:
 445             result = BASE16_1;
 446             break;
 447         case 2:
 448             result = BASE16_2;
 449             break;
 450         case 3:
 451             result = BASE16_3;
 452             break;
 453         default:
 454             for (int j = 1; j < i; j++) {
 455                 result *= 16;
 456             }
 457         }
 458         return result;
 459     }
 460 
 461     /**
 462      * Reads the lines of the configuration file. All include and includedir
 463      * directives are resolved by calling this method recursively.
 464      *
 465      * @param file the krb5.conf file, must be absolute
 466      * @param content the lines. Comment and empty lines are removed,
 467      *                all lines trimmed, include and includedir
 468      *                directives resolved, unknown directives ignored
 469      * @param dups a set of Paths to check for possible infinite loop
 470      * @throws IOException if there is an I/O error
 471      */
 472     private static Void readConfigFileLines(
 473             Path file, List<String> content, Set<Path> dups)
 474             throws IOException {
 475 
 476         if (DEBUG) {
 477             System.out.println("Loading krb5 profile at " + file);
 478         }
 479         if (!file.isAbsolute()) {
 480             throw new IOException("Profile path not absolute");
 481         }
 482 
 483         if (!dups.add(file)) {
 484             throw new IOException("Profile path included more than once");
 485         }
 486 
 487         List<String> lines = Files.readAllLines(file);
 488 
 489         boolean inDirectives = true;
 490         for (String line: lines) {
 491             line = line.trim();
 492             if (line.isEmpty() || line.startsWith("#")) {
 493                 continue;
 494             }
 495             if (inDirectives) {
 496                 if (line.charAt(0) == '[') {
 497                     inDirectives = false;
 498                     content.add(line);
 499                 } else if (line.startsWith("includedir ")) {
 500                     Path dir = Paths.get(
 501                             line.substring("includedir ".length()).trim());
 502                     try (DirectoryStream<Path> files =
 503                                  Files.newDirectoryStream(dir)) {
 504                         for (Path p: files) {
 505                             if (Files.isDirectory(p)) continue;
 506                             String name = p.getFileName().toString();
 507                             if (name.matches("[a-zA-Z0-9_-]+")) {
 508                                 // if dir is absolute, so is p
 509                                 readConfigFileLines(p, content, dups);
 510                             }
 511                         }
 512                     }
 513                 } else if (line.startsWith("include ")) {
 514                     readConfigFileLines(
 515                             Paths.get(line.substring("include ".length()).trim()),
 516                             content, dups);
 517                 } else {
 518                     // Unsupported directives
 519                     if (DEBUG) {
 520                         System.out.println("Unknown directive: " + line);
 521                     }
 522                 }
 523             } else {
 524                 content.add(line);
 525             }
 526         }
 527         return null;
 528     }
 529 
 530     /**
 531      * Reads the configuration file and return normalized lines.
 532      * If the original file is:
 533      *
 534      *     [realms]
 535      *     EXAMPLE.COM =
 536      *     {
 537      *         kdc = kerberos.example.com
 538      *         ...
 539      *     }
 540      *     ...
 541      *
 542      * The result will be (no indentations):
 543      *
 544      *     {
 545      *         realms = {
 546      *             EXAMPLE.COM = {
 547      *                 kdc = kerberos.example.com
 548      *                 ...
 549      *             }
 550      *         }
 551      *         ...
 552      *     }
 553      *
 554      * @param fileName the configuration file
 555      * @return normalized lines
 556      */
 557     private List<String> loadConfigFile(final String fileName)
 558             throws IOException, KrbException {
 559 
 560         List<String> result = new ArrayList<>();
 561         List<String> raw = new ArrayList<>();
 562         Set<Path> dupsCheck = new HashSet<>();
 563 
 564         try {
 565             Path fullp = AccessController.doPrivileged((PrivilegedAction<Path>)
 566                         () -> Paths.get(fileName).toAbsolutePath(),
 567                     null,
 568                     new PropertyPermission("user.dir", "read"));
 569             AccessController.doPrivileged(
 570                     new PrivilegedExceptionAction<Void>() {
 571                         @Override
 572                         public Void run() throws IOException {
 573                             Path path = Paths.get(fileName);
 574                             if (!Files.exists(path)) {
 575                                 // This is OK. There are other ways to get
 576                                 // Kerberos 5 settings
 577                                 return null;
 578                             } else {
 579                                 return readConfigFileLines(
 580                                         fullp, raw, dupsCheck);
 581                             }
 582                         }
 583                     },
 584                     null,
 585                     // include/includedir can go anywhere
 586                     new FilePermission("<<ALL FILES>>", "read"));
 587         } catch (java.security.PrivilegedActionException pe) {
 588             throw (IOException)pe.getException();
 589         }
 590         String previous = null;
 591         for (String line: raw) {
 592             if (line.startsWith("[")) {
 593                 if (!line.endsWith("]")) {
 594                     throw new KrbException("Illegal config content:"
 595                             + line);
 596                 }
 597                 if (previous != null) {
 598                     result.add(previous);
 599                     result.add("}");
 600                 }
 601                 String title = line.substring(
 602                         1, line.length()-1).trim();
 603                 if (title.isEmpty()) {
 604                     throw new KrbException("Illegal config content:"
 605                             + line);
 606                 }
 607                 previous = title + " = {";
 608             } else if (line.startsWith("{")) {
 609                 if (previous == null) {
 610                     throw new KrbException(
 611                         "Config file should not start with \"{\"");
 612                 }
 613                 previous += " {";
 614                 if (line.length() > 1) {
 615                     // { and content on the same line
 616                     result.add(previous);
 617                     previous = line.substring(1).trim();
 618                 }
 619             } else {
 620                 if (previous == null) {
 621                     // This won't happen, because before a section
 622                     // all directives have been resolved
 623                     throw new KrbException(
 624                         "Config file must starts with a section");
 625                 }
 626                 result.add(previous);
 627                 previous = line;
 628             }
 629         }
 630         if (previous != null) {
 631             result.add(previous);
 632             result.add("}");
 633         }
 634         return result;
 635     }
 636 
 637     /**
 638      * Parses the input lines to a hashtable. The key would be section names
 639      * (libdefaults, realms, domain_realms, etc), and the value would be
 640      * another hashtable which contains the key-value pairs inside the section.
 641      * The value of this sub-hashtable can be another hashtable containing
 642      * another sub-sub-section or a non-empty vector of strings for final values
 643      * (even if there is only one value defined).
 644      * <p>
 645      * For top-level sections with duplicates names, their contents are merged.
 646      * For sub-sections the former overwrites the latter. For final values,
 647      * they are stored in a vector in their appearing order. Please note these
 648      * values must appear in the same sub-section. Otherwise, the sub-section
 649      * appears first should have already overridden the others.
 650      * <p>
 651      * As a corner case, if the same name is used as both a section name and a
 652      * value name, the first appearance decides the type. That is to say, if the
 653      * first one is for a section, all latter appearances are ignored. If it's
 654      * a value, latter appearances as sections are ignored, but those as values
 655      * are added to the vector.
 656      * <p>
 657      * The behavior described above is compatible to other krb5 implementations
 658      * but it's not decumented publicly anywhere. the best practice is not to
 659      * assume any kind of override functionality and only specify values for
 660      * a particular key in one place.
 661      *
 662      * @param v the normalized input as return by loadConfigFile
 663      * @throws KrbException if there is a file format error
 664      */
 665     @SuppressWarnings("unchecked")
 666     private Hashtable<String,Object> parseStanzaTable(List<String> v)
 667             throws KrbException {
 668         Hashtable<String,Object> current = stanzaTable;
 669         for (String line: v) {
 670             // There are only 3 kinds of lines
 671             // 1. a = b
 672             // 2. a = {
 673             // 3. }
 674             if (line.equals("}")) {
 675                 // Go back to parent, see below
 676                 current = (Hashtable<String,Object>)current.remove(" PARENT ");
 677                 if (current == null) {
 678                     throw new KrbException("Unmatched close brace");
 679                 }
 680             } else {
 681                 int pos = line.indexOf('=');
 682                 if (pos < 0) {
 683                     throw new KrbException("Illegal config content:" + line);
 684                 }
 685                 String key = line.substring(0, pos).trim();
 686                 String value = unquote(line.substring(pos + 1));
 687                 if (value.equals("{")) {
 688                     Hashtable<String,Object> subTable;
 689                     if (current == stanzaTable) {
 690                         key = key.toLowerCase(Locale.US);
 691                     }
 692                     // When there are dup names for sections
 693                     if (current.containsKey(key)) {
 694                         if (current == stanzaTable) {   // top-level, merge
 695                             // The value at top-level must be another Hashtable
 696                             subTable = (Hashtable<String,Object>)current.get(key);
 697                         } else {                        // otherwise, ignored
 698                             // read and ignore it (do not put into current)
 699                             subTable = new Hashtable<>();
 700                         }
 701                     } else {
 702                         subTable = new Hashtable<>();
 703                         current.put(key, subTable);
 704                     }
 705                     // A special entry for its parent. Put whitespaces around,
 706                     // so will never be confused with a normal key
 707                     subTable.put(" PARENT ", current);
 708                     current = subTable;
 709                 } else {
 710                     Vector<String> values;
 711                     if (current.containsKey(key)) {
 712                         Object obj = current.get(key);
 713                         if (obj instanceof Vector) {
 714                             // String values are merged
 715                             values = (Vector<String>)obj;
 716                             values.add(value);
 717                         } else {
 718                             // If a key shows as section first and then a value,
 719                             // ignore the value.
 720                         }
 721                     } else {
 722                         values = new Vector<String>();
 723                         values.add(value);
 724                         current.put(key, values);
 725                     }
 726                 }
 727             }
 728         }
 729         if (current != stanzaTable) {
 730             throw new KrbException("Not closed");
 731         }
 732         return current;
 733     }
 734 
 735     /**
 736      * Gets the default Java configuration file name.
 737      *
 738      * If the system property "java.security.krb5.conf" is defined, we'll
 739      * use its value, no matter if the file exists or not. Otherwise, we
 740      * will look at $JAVA_HOME/lib/security directory with "krb5.conf" name,
 741      * and return it if the file exists.
 742      *
 743      * The method returns null if it cannot find a Java config file.
 744      */
 745     private String getJavaFileName() {
 746         String name = getProperty("java.security.krb5.conf");
 747         if (name == null) {
 748             name = getProperty("java.home") + File.separator +
 749                                 "lib" + File.separator + "security" +
 750                                 File.separator + "krb5.conf";
 751             if (!fileExists(name)) {
 752                 name = null;
 753             }
 754         }
 755         if (DEBUG) {
 756             System.out.println("Java config name: " + name);
 757         }
 758         return name;
 759     }
 760 
 761     /**
 762      * Gets the default native configuration file name.
 763      *
 764      * Depending on the OS type, the method returns the default native
 765      * kerberos config file name, which is at windows directory with
 766      * the name of "krb5.ini" for Windows, /etc/krb5/krb5.conf for Solaris,
 767      * /etc/krb5.conf otherwise. Mac OSX X has a different file name.
 768      *
 769      * Note: When the Terminal Service is started in Windows (from 2003),
 770      * there are two kinds of Windows directories: A system one (say,
 771      * C:\Windows), and a user-private one (say, C:\Users\Me\Windows).
 772      * We will first look for krb5.ini in the user-private one. If not
 773      * found, try the system one instead.
 774      *
 775      * This method will always return a non-null non-empty file name,
 776      * even if that file does not exist.
 777      */
 778     private String getNativeFileName() {
 779         String name = null;
 780         String osname = getProperty("os.name");
 781         if (osname.startsWith("Windows")) {
 782             try {
 783                 Credentials.ensureLoaded();
 784             } catch (Exception e) {
 785                 // ignore exceptions
 786             }
 787             if (Credentials.alreadyLoaded) {
 788                 String path = getWindowsDirectory(false);
 789                 if (path != null) {
 790                     if (path.endsWith("\\")) {
 791                         path = path + "krb5.ini";
 792                     } else {
 793                         path = path + "\\krb5.ini";
 794                     }
 795                     if (fileExists(path)) {
 796                         name = path;
 797                     }
 798                 }
 799                 if (name == null) {
 800                     path = getWindowsDirectory(true);
 801                     if (path != null) {
 802                         if (path.endsWith("\\")) {
 803                             path = path + "krb5.ini";
 804                         } else {
 805                             path = path + "\\krb5.ini";
 806                         }
 807                         name = path;
 808                     }
 809                 }
 810             }
 811             if (name == null) {
 812                 name = "c:\\winnt\\krb5.ini";
 813             }
 814         } else if (osname.startsWith("SunOS")) {
 815             name =  "/etc/krb5/krb5.conf";
 816         } else if (osname.contains("OS X")) {
 817             name = findMacosConfigFile();
 818         } else {
 819             name =  "/etc/krb5.conf";
 820         }
 821         if (DEBUG) {
 822             System.out.println("Native config name: " + name);
 823         }
 824         return name;
 825     }
 826 
 827     private static String getProperty(String property) {
 828         return java.security.AccessController.doPrivileged(
 829                 new sun.security.action.GetPropertyAction(property));
 830     }
 831 
 832     private String findMacosConfigFile() {
 833         String userHome = getProperty("user.home");
 834         final String PREF_FILE = "/Library/Preferences/edu.mit.Kerberos";
 835         String userPrefs = userHome + PREF_FILE;
 836 
 837         if (fileExists(userPrefs)) {
 838             return userPrefs;
 839         }
 840 
 841         if (fileExists(PREF_FILE)) {
 842             return PREF_FILE;
 843         }
 844 
 845         return "/etc/krb5.conf";
 846     }
 847 
 848     private static String unquote(String s) {
 849         s = s.trim();
 850         if (s.isEmpty()) return s;
 851         if (s.charAt(0) == '"' && s.charAt(s.length()-1) == '"' ||
 852                 s.charAt(0) == '\'' && s.charAt(s.length()-1) == '\'') {
 853             s = s.substring(1, s.length()-1).trim();
 854         }
 855         return s;
 856     }
 857 
 858     /**
 859      * For testing purpose. This method lists all information being parsed from
 860      * the configuration file to the hashtable.
 861      */
 862     public void listTable() {
 863         System.out.println(this);
 864     }
 865 
 866     /**
 867      * Returns all etypes specified in krb5.conf for the given configName,
 868      * or all the builtin defaults. This result is always non-empty.
 869      * If no etypes are found, an exception is thrown.
 870      */
 871     public int[] defaultEtype(String configName) throws KrbException {
 872         String default_enctypes;
 873         default_enctypes = get("libdefaults", configName);
 874         int[] etype;
 875         if (default_enctypes == null) {
 876             if (DEBUG) {
 877                 System.out.println("Using builtin default etypes for " +
 878                     configName);
 879             }
 880             etype = EType.getBuiltInDefaults();
 881         } else {
 882             String delim = " ";
 883             StringTokenizer st;
 884             for (int j = 0; j < default_enctypes.length(); j++) {
 885                 if (default_enctypes.substring(j, j + 1).equals(",")) {
 886                     // only two delimiters are allowed to use
 887                     // according to Kerberos DCE doc.
 888                     delim = ",";
 889                     break;
 890                 }
 891             }
 892             st = new StringTokenizer(default_enctypes, delim);
 893             int len = st.countTokens();
 894             ArrayList<Integer> ls = new ArrayList<>(len);
 895             int type;
 896             for (int i = 0; i < len; i++) {
 897                 type = Config.getType(st.nextToken());
 898                 if (type != -1 && EType.isSupported(type)) {
 899                     ls.add(type);
 900                 }
 901             }
 902             if (ls.isEmpty()) {
 903                 throw new KrbException("no supported default etypes for "
 904                         + configName);
 905             } else {
 906                 etype = new int[ls.size()];
 907                 for (int i = 0; i < etype.length; i++) {
 908                     etype[i] = ls.get(i);
 909                 }
 910             }
 911         }
 912 
 913         if (DEBUG) {
 914             System.out.print("default etypes for " + configName + ":");
 915             for (int i = 0; i < etype.length; i++) {
 916                 System.out.print(" " + etype[i]);
 917             }
 918             System.out.println(".");
 919         }
 920         return etype;
 921     }
 922 
 923 
 924     /**
 925      * Get the etype and checksum value for the specified encryption and
 926      * checksum type.
 927      *
 928      */
 929     /*
 930      * This method converts the string representation of encryption type and
 931      * checksum type to int value that can be later used by EType and
 932      * Checksum classes.
 933      */
 934     public static int getType(String input) {
 935         int result = -1;
 936         if (input == null) {
 937             return result;
 938         }
 939         if (input.startsWith("d") || (input.startsWith("D"))) {
 940             if (input.equalsIgnoreCase("des-cbc-crc")) {
 941                 result = EncryptedData.ETYPE_DES_CBC_CRC;
 942             } else if (input.equalsIgnoreCase("des-cbc-md5")) {
 943                 result = EncryptedData.ETYPE_DES_CBC_MD5;
 944             } else if (input.equalsIgnoreCase("des-mac")) {
 945                 result = Checksum.CKSUMTYPE_DES_MAC;
 946             } else if (input.equalsIgnoreCase("des-mac-k")) {
 947                 result = Checksum.CKSUMTYPE_DES_MAC_K;
 948             } else if (input.equalsIgnoreCase("des-cbc-md4")) {
 949                 result = EncryptedData.ETYPE_DES_CBC_MD4;
 950             } else if (input.equalsIgnoreCase("des3-cbc-sha1") ||
 951                 input.equalsIgnoreCase("des3-hmac-sha1") ||
 952                 input.equalsIgnoreCase("des3-cbc-sha1-kd") ||
 953                 input.equalsIgnoreCase("des3-cbc-hmac-sha1-kd")) {
 954                 result = EncryptedData.ETYPE_DES3_CBC_HMAC_SHA1_KD;
 955             }
 956         } else if (input.startsWith("a") || (input.startsWith("A"))) {
 957             // AES
 958             if (input.equalsIgnoreCase("aes128-cts") ||
 959                 input.equalsIgnoreCase("aes128-cts-hmac-sha1-96")) {
 960                 result = EncryptedData.ETYPE_AES128_CTS_HMAC_SHA1_96;
 961             } else if (input.equalsIgnoreCase("aes256-cts") ||
 962                 input.equalsIgnoreCase("aes256-cts-hmac-sha1-96")) {
 963                 result = EncryptedData.ETYPE_AES256_CTS_HMAC_SHA1_96;
 964             // ARCFOUR-HMAC
 965             } else if (input.equalsIgnoreCase("arcfour-hmac") ||
 966                    input.equalsIgnoreCase("arcfour-hmac-md5")) {
 967                 result = EncryptedData.ETYPE_ARCFOUR_HMAC;
 968             }
 969         // RC4-HMAC
 970         } else if (input.equalsIgnoreCase("rc4-hmac")) {
 971             result = EncryptedData.ETYPE_ARCFOUR_HMAC;
 972         } else if (input.equalsIgnoreCase("CRC32")) {
 973             result = Checksum.CKSUMTYPE_CRC32;
 974         } else if (input.startsWith("r") || (input.startsWith("R"))) {
 975             if (input.equalsIgnoreCase("rsa-md5")) {
 976                 result = Checksum.CKSUMTYPE_RSA_MD5;
 977             } else if (input.equalsIgnoreCase("rsa-md5-des")) {
 978                 result = Checksum.CKSUMTYPE_RSA_MD5_DES;
 979             }
 980         } else if (input.equalsIgnoreCase("hmac-sha1-des3-kd")) {
 981             result = Checksum.CKSUMTYPE_HMAC_SHA1_DES3_KD;
 982         } else if (input.equalsIgnoreCase("hmac-sha1-96-aes128")) {
 983             result = Checksum.CKSUMTYPE_HMAC_SHA1_96_AES128;
 984         } else if (input.equalsIgnoreCase("hmac-sha1-96-aes256")) {
 985             result = Checksum.CKSUMTYPE_HMAC_SHA1_96_AES256;
 986         } else if (input.equalsIgnoreCase("hmac-md5-rc4") ||
 987                 input.equalsIgnoreCase("hmac-md5-arcfour") ||
 988                 input.equalsIgnoreCase("hmac-md5-enc")) {
 989             result = Checksum.CKSUMTYPE_HMAC_MD5_ARCFOUR;
 990         } else if (input.equalsIgnoreCase("NULL")) {
 991             result = EncryptedData.ETYPE_NULL;
 992         }
 993 
 994         return result;
 995     }
 996 
 997     /**
 998      * Resets the default kdc realm.
 999      * We do not need to synchronize these methods since assignments are atomic
1000      *
1001      * This method was useless. Kept here in case some class still calls it.
1002      */
1003     public void resetDefaultRealm(String realm) {
1004         if (DEBUG) {
1005             System.out.println(">>> Config try resetting default kdc " + realm);
1006         }
1007     }
1008 
1009     /**
1010      * Check to use addresses in tickets
1011      * use addresses if "no_addresses" or "noaddresses" is set to false
1012      */
1013     public boolean useAddresses() {
1014         return getBooleanObject("libdefaults", "no_addresses") == Boolean.FALSE ||
1015                 getBooleanObject("libdefaults", "noaddresses") == Boolean.FALSE;
1016     }
1017 
1018     /**
1019      * Check if need to use DNS to locate Kerberos services for name. If not
1020      * defined, check dns_fallback, whose default value is true.
1021      */
1022     private boolean useDNS(String name) {
1023         Boolean value = getBooleanObject("libdefaults", name);
1024         if (value != null) {
1025             return value.booleanValue();
1026         } else {
1027             return getBooleanObject("libdefaults", "dns_fallback") != Boolean.FALSE;
1028         }
1029     }
1030 
1031     /**
1032      * Check if need to use DNS to locate the KDC
1033      */
1034     private boolean useDNS_KDC() {
1035         return useDNS("dns_lookup_kdc");
1036     }
1037 
1038     /*
1039      * Check if need to use DNS to locate the Realm
1040      */
1041     private boolean useDNS_Realm() {
1042         return useDNS("dns_lookup_realm");
1043     }
1044 
1045     /**
1046      * Gets default realm.
1047      * @throws KrbException where no realm can be located
1048      * @return the default realm, always non null
1049      */
1050     public String getDefaultRealm() throws KrbException {
1051         if (defaultRealm != null) {
1052             return defaultRealm;
1053         }
1054         Exception cause = null;
1055         String realm = get("libdefaults", "default_realm");
1056         if ((realm == null) && useDNS_Realm()) {
1057             // use DNS to locate Kerberos realm
1058             try {
1059                 realm = getRealmFromDNS();
1060             } catch (KrbException ke) {
1061                 cause = ke;
1062             }
1063         }
1064         if (realm == null) {
1065             realm = java.security.AccessController.doPrivileged(
1066                     new java.security.PrivilegedAction<String>() {
1067                 @Override
1068                 public String run() {
1069                     String osname = System.getProperty("os.name");
1070                     if (osname.startsWith("Windows")) {
1071                         return System.getenv("USERDNSDOMAIN");
1072                     }
1073                     return null;
1074                 }
1075             });
1076         }
1077         if (realm == null) {
1078             KrbException ke = new KrbException("Cannot locate default realm");
1079             if (cause != null) {
1080                 ke.initCause(cause);
1081             }
1082             throw ke;
1083         }
1084         return realm;
1085     }
1086 
1087     /**
1088      * Returns a list of KDC's with each KDC separated by a space
1089      *
1090      * @param realm the realm for which the KDC list is desired
1091      * @throws KrbException if there's no way to find KDC for the realm
1092      * @return the list of KDCs separated by a space, always non null
1093      */
1094     public String getKDCList(String realm) throws KrbException {
1095         if (realm == null) {
1096             realm = getDefaultRealm();
1097         }
1098         if (realm.equalsIgnoreCase(defaultRealm)) {
1099             return defaultKDC;
1100         }
1101         Exception cause = null;
1102         String kdcs = getAll("realms", realm, "kdc");
1103         if ((kdcs == null) && useDNS_KDC()) {
1104             // use DNS to locate KDC
1105             try {
1106                 kdcs = getKDCFromDNS(realm);
1107             } catch (KrbException ke) {
1108                 cause = ke;
1109             }
1110         }
1111         if (kdcs == null) {
1112             kdcs = java.security.AccessController.doPrivileged(
1113                     new java.security.PrivilegedAction<String>() {
1114                 @Override
1115                 public String run() {
1116                     String osname = System.getProperty("os.name");
1117                     if (osname.startsWith("Windows")) {
1118                         String logonServer = System.getenv("LOGONSERVER");
1119                         if (logonServer != null
1120                                 && logonServer.startsWith("\\\\")) {
1121                             logonServer = logonServer.substring(2);
1122                         }
1123                         return logonServer;
1124                     }
1125                     return null;
1126                 }
1127             });
1128         }
1129         if (kdcs == null) {
1130             if (defaultKDC != null) {
1131                 return defaultKDC;
1132             }
1133             KrbException ke = new KrbException("Cannot locate KDC");
1134             if (cause != null) {
1135                 ke.initCause(cause);
1136             }
1137             throw ke;
1138         }
1139         return kdcs;
1140     }
1141 
1142     /**
1143      * Locate Kerberos realm using DNS
1144      *
1145      * @return the Kerberos realm
1146      */
1147     private String getRealmFromDNS() throws KrbException {
1148         // use DNS to locate Kerberos realm
1149         String realm = null;
1150         String hostName = null;
1151         try {
1152             hostName = InetAddress.getLocalHost().getCanonicalHostName();
1153         } catch (UnknownHostException e) {
1154             KrbException ke = new KrbException(Krb5.KRB_ERR_GENERIC,
1155                 "Unable to locate Kerberos realm: " + e.getMessage());
1156             ke.initCause(e);
1157             throw (ke);
1158         }
1159         // get the domain realm mapping from the configuration
1160         String mapRealm = PrincipalName.mapHostToRealm(hostName);
1161         if (mapRealm == null) {
1162             // No match. Try search and/or domain in /etc/resolv.conf
1163             List<String> srchlist = ResolverConfiguration.open().searchlist();
1164             for (String domain: srchlist) {
1165                 realm = checkRealm(domain);
1166                 if (realm != null) {
1167                     break;
1168                 }
1169             }
1170         } else {
1171             realm = checkRealm(mapRealm);
1172         }
1173         if (realm == null) {
1174             throw new KrbException(Krb5.KRB_ERR_GENERIC,
1175                                 "Unable to locate Kerberos realm");
1176         }
1177         return realm;
1178     }
1179 
1180     /**
1181      * Check if the provided realm is the correct realm
1182      * @return the realm if correct, or null otherwise
1183      */
1184     private static String checkRealm(String mapRealm) {
1185         if (DEBUG) {
1186             System.out.println("getRealmFromDNS: trying " + mapRealm);
1187         }
1188         String[] records = null;
1189         String newRealm = mapRealm;
1190         while ((records == null) && (newRealm != null)) {
1191             // locate DNS TXT record
1192             records = KrbServiceLocator.getKerberosService(newRealm);
1193             newRealm = Realm.parseRealmComponent(newRealm);
1194             // if no DNS TXT records found, try again using sub-realm
1195         }
1196         if (records != null) {
1197             for (int i = 0; i < records.length; i++) {
1198                 if (records[i].equalsIgnoreCase(mapRealm)) {
1199                     return records[i];
1200                 }
1201             }
1202         }
1203         return null;
1204     }
1205 
1206     /**
1207      * Locate KDC using DNS
1208      *
1209      * @param realm the realm for which the master KDC is desired
1210      * @return the KDC
1211      */
1212     private String getKDCFromDNS(String realm) throws KrbException {
1213         // use DNS to locate KDC
1214         String kdcs = "";
1215         String[] srvs = null;
1216         // locate DNS SRV record using UDP
1217         if (DEBUG) {
1218             System.out.println("getKDCFromDNS using UDP");
1219         }
1220         srvs = KrbServiceLocator.getKerberosService(realm, "_udp");
1221         if (srvs == null) {
1222             // locate DNS SRV record using TCP
1223             if (DEBUG) {
1224                 System.out.println("getKDCFromDNS using TCP");
1225             }
1226             srvs = KrbServiceLocator.getKerberosService(realm, "_tcp");
1227         }
1228         if (srvs == null) {
1229             // no DNS SRV records
1230             throw new KrbException(Krb5.KRB_ERR_GENERIC,
1231                 "Unable to locate KDC for realm " + realm);
1232         }
1233         if (srvs.length == 0) {
1234             return null;
1235         }
1236         for (int i = 0; i < srvs.length; i++) {
1237             kdcs += srvs[i].trim() + " ";
1238         }
1239         kdcs = kdcs.trim();
1240         if (kdcs.equals("")) {
1241             return null;
1242         }
1243         return kdcs;
1244     }
1245 
1246     private boolean fileExists(String name) {
1247         return java.security.AccessController.doPrivileged(
1248                                 new FileExistsAction(name));
1249     }
1250 
1251     static class FileExistsAction
1252         implements java.security.PrivilegedAction<Boolean> {
1253 
1254         private String fileName;
1255 
1256         public FileExistsAction(String fileName) {
1257             this.fileName = fileName;
1258         }
1259 
1260         public Boolean run() {
1261             return new File(fileName).exists();
1262         }
1263     }
1264 
1265     // Shows the content of the Config object for debug purpose.
1266     //
1267     // {
1268     //      libdefaults = {
1269     //          default_realm = R
1270     //      }
1271     //      realms = {
1272     //          R = {
1273     //              kdc = [k1,k2]
1274     //          }
1275     //      }
1276     // }
1277 
1278     @Override
1279     public String toString() {
1280         StringBuffer sb = new StringBuffer();
1281         toStringInternal("", stanzaTable, sb);
1282         return sb.toString();
1283     }
1284     private static void toStringInternal(String prefix, Object obj,
1285             StringBuffer sb) {
1286         if (obj instanceof String) {
1287             // A string value, just print it
1288             sb.append(obj).append('\n');
1289         } else if (obj instanceof Hashtable) {
1290             // A table, start a new sub-section...
1291             Hashtable<?, ?> tab = (Hashtable<?, ?>)obj;
1292             sb.append("{\n");
1293             for (Object o: tab.keySet()) {
1294                 // ...indent, print "key = ", and
1295                 sb.append(prefix).append("    ").append(o).append(" = ");
1296                 // ...go recursively into value
1297                 toStringInternal(prefix + "    ", tab.get(o), sb);
1298             }
1299             sb.append(prefix).append("}\n");
1300         } else if (obj instanceof Vector) {
1301             // A vector of strings, print them inside [ and ]
1302             Vector<?> v = (Vector<?>)obj;
1303             sb.append("[");
1304             boolean first = true;
1305             for (Object o: v.toArray()) {
1306                 if (!first) sb.append(",");
1307                 sb.append(o);
1308                 first = false;
1309             }
1310             sb.append("]\n");
1311         }
1312     }
1313 }