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