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