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