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