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