1 /* 2 * Copyright (c) 2000, 2012, 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 32 package sun.security.krb5.internal.ktab; 33 34 import sun.security.krb5.*; 35 import sun.security.krb5.internal.*; 36 import sun.security.krb5.internal.crypto.*; 37 import java.util.ArrayList; 38 import java.util.Arrays; 39 import java.io.IOException; 40 import java.io.FileInputStream; 41 import java.io.FileOutputStream; 42 import java.io.File; 43 import java.io.FileNotFoundException; 44 import java.util.Comparator; 45 import java.util.HashMap; 46 import java.util.Map; 47 import java.util.StringTokenizer; 48 import java.util.Vector; 49 import sun.security.jgss.krb5.ServiceCreds; 50 51 /** 52 * This class represents key table. The key table functions deal with storing 53 * and retrieving service keys for use in authentication exchanges. 54 * 55 * A KeyTab object is always constructed, if the file specified does not 56 * exist, it's still valid but empty. If there is an I/O error or file format 57 * error, it's invalid. 58 * 59 * The class is immutable on the read side (the write side is only used by 60 * the ktab tool). 61 * 62 * @author Yanni Zhang 63 */ 64 public class KeyTab implements KeyTabConstants { 65 66 private static final boolean DEBUG = Krb5.DEBUG; 67 private static String defaultTabName = null; 68 69 // Attention: Currently there is no way to remove a keytab from this map, 70 // this might lead to a memory leak. 71 private static Map<String,KeyTab> map = new HashMap<>(); 72 73 // KeyTab file does not exist. Note: a missing keytab is still valid 74 private boolean isMissing = false; 75 76 // KeyTab file is invalid, possibly an I/O error or a file format error. 77 private boolean isValid = true; 78 79 private final String tabName; 80 private long lastModified; 81 private int kt_vno = KRB5_KT_VNO; 82 83 private Vector<KeyTabEntry> entries = new Vector<>(); 84 85 /** 86 * Constructs a KeyTab object. 87 * 88 * If there is any I/O error or format errot during the loading, the 89 * isValid flag is set to false, and all half-read entries are dismissed. 90 * @param filename path name for the keytab file, must not be null 91 */ 92 private KeyTab(String filename) { 93 tabName = filename; 94 try { 95 lastModified = new File(tabName).lastModified(); 96 try (KeyTabInputStream kis = 97 new KeyTabInputStream(new FileInputStream(filename))) { 98 load(kis); 99 } 100 } catch (FileNotFoundException e) { 101 entries.clear(); 102 isMissing = true; 103 } catch (Exception ioe) { 104 entries.clear(); 105 isValid = false; 106 } 107 } 108 109 /** 110 * Read a keytab file. Returns a new object and save it into cache when 111 * new content (modified since last read) is available. If keytab file is 112 * invalid, the old object will be returned. This is a safeguard for 113 * partial-written keytab files or non-stable network. Please note that 114 * a missing keytab is valid, which is equivalent to an empty keytab. 115 * 116 * @param s file name of keytab, must not be null 117 * @return the keytab object, can be invalid, but never null. 118 */ 119 private synchronized static KeyTab getInstance0(String s) { 120 long lm = new File(s).lastModified(); 121 KeyTab old = map.get(s); 122 if (old != null && old.isValid() && old.lastModified == lm) { 123 return old; 124 } 125 KeyTab ktab = new KeyTab(s); 126 if (ktab.isValid()) { // A valid new keytab 127 map.put(s, ktab); 128 return ktab; 129 } else if (old != null) { // An existing old one 130 return old; 131 } else { 132 return ktab; // first read is invalid 133 } 134 } 135 136 /** 137 * Gets a KeyTab object. 138 * @param s the key tab file name. 139 * @return the KeyTab object, never null. 140 */ 141 public static KeyTab getInstance(String s) { 142 if (s == null) { 143 return getInstance(); 144 } else { 145 return getInstance0(normalize(s)); 146 } 147 } 148 149 /** 150 * Gets a KeyTab object. 151 * @param file the key tab file. 152 * @return the KeyTab object, never null. 153 */ 154 public static KeyTab getInstance(File file) { 155 if (file == null) { 156 return getInstance(); 157 } else { 158 return getInstance0(file.getPath()); 159 } 160 } 161 162 /** 163 * Gets the default KeyTab object. 164 * @return the KeyTab object, never null. 165 */ 166 public static KeyTab getInstance() { 167 return getInstance(getDefaultTabName()); 168 } 169 170 public boolean isMissing() { 171 return isMissing; 172 } 173 174 public boolean isValid() { 175 return isValid; 176 } 177 178 /** 179 * The location of keytab file will be read from the configuration file 180 * If it is not specified, consider user.home as the keytab file's 181 * default location. 182 * @return never null 183 */ 184 private static String getDefaultTabName() { 185 if (defaultTabName != null) { 186 return defaultTabName; 187 } else { 188 String kname = null; 189 try { 190 String keytab_names = Config.getInstance().get 191 ("libdefaults", "default_keytab_name"); 192 if (keytab_names != null) { 193 StringTokenizer st = new StringTokenizer(keytab_names, " "); 194 while (st.hasMoreTokens()) { 195 kname = normalize(st.nextToken()); 196 if (new File(kname).exists()) { 197 break; 198 } 199 } 200 } 201 } catch (KrbException e) { 202 kname = null; 203 } 204 205 if (kname == null) { 206 String user_home = 207 java.security.AccessController.doPrivileged( 208 new sun.security.action.GetPropertyAction("user.home")); 209 210 if (user_home == null) { 211 user_home = 212 java.security.AccessController.doPrivileged( 213 new sun.security.action.GetPropertyAction("user.dir")); 214 } 215 216 kname = user_home + File.separator + "krb5.keytab"; 217 } 218 defaultTabName = kname; 219 return kname; 220 } 221 } 222 223 /** 224 * Normalizes some common keytab name formats into the bare file name. 225 * For example, FILE:/etc/krb5.keytab to /etc/krb5.keytab 226 * @param name never null 227 * @return never null 228 */ 229 // This method is used in this class and Krb5LoginModule 230 public static String normalize(String name) { 231 String kname; 232 if ((name.length() >= 5) && 233 (name.substring(0, 5).equalsIgnoreCase("FILE:"))) { 234 kname = name.substring(5); 235 } else if ((name.length() >= 9) && 236 (name.substring(0, 9).equalsIgnoreCase("ANY:FILE:"))) { 237 // this format found in MIT's krb5.ini. 238 kname = name.substring(9); 239 } else if ((name.length() >= 7) && 240 (name.substring(0, 7).equalsIgnoreCase("SRVTAB:"))) { 241 // this format found in MIT's krb5.ini. 242 kname = name.substring(7); 243 } else 244 kname = name; 245 return kname; 246 } 247 248 private void load(KeyTabInputStream kis) 249 throws IOException, RealmException { 250 251 entries.clear(); 252 kt_vno = kis.readVersion(); 253 if (kt_vno == KRB5_KT_VNO_1) { 254 kis.setNativeByteOrder(); 255 } 256 int entryLength = 0; 257 KeyTabEntry entry; 258 while (kis.available() > 0) { 259 entryLength = kis.readEntryLength(); 260 entry = kis.readEntry(entryLength, kt_vno); 261 if (DEBUG) { 262 System.out.println(">>> KeyTab: load() entry length: " + 263 entryLength + "; type: " + 264 (entry != null? entry.keyType : 0)); 265 } 266 if (entry != null) 267 entries.addElement(entry); 268 } 269 } 270 271 /** 272 * Returns a principal name in this keytab. Used by 273 * {@link ServiceCreds#getKKeys()}. 274 */ 275 public PrincipalName getOneName() { 276 int size = entries.size(); 277 return size > 0 ? entries.elementAt(size-1).service : null; 278 } 279 280 /** 281 * Reads all keys for a service from the keytab file that have 282 * etypes that have been configured for use. If there are multiple 283 * keys with same etype, the one with the highest kvno is returned. 284 * @param service the PrincipalName of the requested service 285 * @return an array containing all the service keys, never null 286 */ 287 public EncryptionKey[] readServiceKeys(PrincipalName service) { 288 KeyTabEntry entry; 289 EncryptionKey key; 290 int size = entries.size(); 291 ArrayList<EncryptionKey> keys = new ArrayList<>(size); 292 if (DEBUG) { 293 System.out.println("Looking for keys for: " + service); 294 } 295 for (int i = size-1; i >= 0; i--) { 296 entry = entries.elementAt(i); 297 if (entry.service.match(service)) { 298 if (EType.isSupported(entry.keyType)) { 299 key = new EncryptionKey(entry.keyblock, 300 entry.keyType, 301 new Integer(entry.keyVersion)); 302 keys.add(key); 303 if (DEBUG) { 304 System.out.println("Added key: " + entry.keyType + 305 "version: " + entry.keyVersion); 306 } 307 } else if (DEBUG) { 308 System.out.println("Found unsupported keytype (" + 309 entry.keyType + ") for " + service); 310 } 311 } 312 } 313 size = keys.size(); 314 EncryptionKey[] retVal = keys.toArray(new EncryptionKey[size]); 315 316 // Sort keys according to default_tkt_enctypes 317 if (DEBUG) { 318 System.out.println("Ordering keys wrt default_tkt_enctypes list"); 319 } 320 321 final int[] etypes = EType.getDefaults("default_tkt_enctypes"); 322 323 // Sort the keys, k1 is preferred than k2 if: 324 // 1. k1's etype appears earlier in etypes than k2's 325 // 2. If same, k1's KVNO is higher 326 Arrays.sort(retVal, new Comparator<EncryptionKey>() { 327 @Override 328 public int compare(EncryptionKey o1, EncryptionKey o2) { 329 if (etypes != null) { 330 int o1EType = o1.getEType(); 331 int o2EType = o2.getEType(); 332 if (o1EType != o2EType) { 333 for (int i=0; i<etypes.length; i++) { 334 if (etypes[i] == o1EType) { 335 return -1; 336 } else if (etypes[i] == o2EType) { 337 return 1; 338 } 339 } 340 // Neither o1EType nor o2EType in default_tkt_enctypes, 341 // therefore won't be used in AS-REQ. We do not care 342 // about their order, use kvno is OK. 343 } 344 } 345 return o2.getKeyVersionNumber().intValue() 346 - o1.getKeyVersionNumber().intValue(); 347 } 348 }); 349 350 return retVal; 351 } 352 353 354 355 /** 356 * Searches for the service entry in the keytab file. 357 * The etype of the key must be one that has been configured 358 * to be used. 359 * @param service the PrincipalName of the requested service. 360 * @return true if the entry is found, otherwise, return false. 361 */ 362 public boolean findServiceEntry(PrincipalName service) { 363 KeyTabEntry entry; 364 for (int i = 0; i < entries.size(); i++) { 365 entry = entries.elementAt(i); 366 if (entry.service.match(service)) { 367 if (EType.isSupported(entry.keyType)) { 368 return true; 369 } else if (DEBUG) { 370 System.out.println("Found unsupported keytype (" + 371 entry.keyType + ") for " + service); 372 } 373 } 374 } 375 return false; 376 } 377 378 public String tabName() { 379 return tabName; 380 } 381 382 /////////////////// THE WRITE SIDE /////////////////////// 383 /////////////// only used by ktab tool ////////////////// 384 385 /** 386 * Adds a new entry in the key table. 387 * @param service the service which will have a new entry in the key table. 388 * @param psswd the password which generates the key. 389 * @param kvno the kvno to use, -1 means automatic increasing 390 * @param append false if entries with old kvno would be removed. 391 * Note: if kvno is not -1, entries with the same kvno are always removed 392 */ 393 public void addEntry(PrincipalName service, char[] psswd, 394 int kvno, boolean append) throws KrbException { 395 addEntry(service, service.getSalt(), psswd, kvno, append); 396 } 397 398 // Called by KDC test 399 public void addEntry(PrincipalName service, String salt, char[] psswd, 400 int kvno, boolean append) throws KrbException { 401 402 EncryptionKey[] encKeys = EncryptionKey.acquireSecretKeys( 403 psswd, salt); 404 405 // There should be only one maximum KVNO value for all etypes, so that 406 // all added keys can have the same KVNO. 407 408 int maxKvno = 0; // only useful when kvno == -1 409 for (int i = entries.size()-1; i >= 0; i--) { 410 KeyTabEntry e = entries.get(i); 411 if (e.service.match(service)) { 412 if (e.keyVersion > maxKvno) { 413 maxKvno = e.keyVersion; 414 } 415 if (!append || e.keyVersion == kvno) { 416 entries.removeElementAt(i); 417 } 418 } 419 } 420 if (kvno == -1) { 421 kvno = maxKvno + 1; 422 } 423 424 for (int i = 0; encKeys != null && i < encKeys.length; i++) { 425 int keyType = encKeys[i].getEType(); 426 byte[] keyValue = encKeys[i].getBytes(); 427 428 KeyTabEntry newEntry = new KeyTabEntry(service, 429 service.getRealm(), 430 new KerberosTime(System.currentTimeMillis()), 431 kvno, keyType, keyValue); 432 entries.addElement(newEntry); 433 } 434 } 435 436 /** 437 * Gets the list of service entries in key table. 438 * @return array of <code>KeyTabEntry</code>. 439 */ 440 public KeyTabEntry[] getEntries() { 441 KeyTabEntry[] kentries = new KeyTabEntry[entries.size()]; 442 for (int i = 0; i < kentries.length; i++) { 443 kentries[i] = entries.elementAt(i); 444 } 445 return kentries; 446 } 447 448 /** 449 * Creates a new default key table. 450 */ 451 public synchronized static KeyTab create() 452 throws IOException, RealmException { 453 String dname = getDefaultTabName(); 454 return create(dname); 455 } 456 457 /** 458 * Creates a new default key table. 459 */ 460 public synchronized static KeyTab create(String name) 461 throws IOException, RealmException { 462 463 try (KeyTabOutputStream kos = 464 new KeyTabOutputStream(new FileOutputStream(name))) { 465 kos.writeVersion(KRB5_KT_VNO); 466 } 467 return new KeyTab(name); 468 } 469 470 /** 471 * Saves the file at the directory. 472 */ 473 public synchronized void save() throws IOException { 474 try (KeyTabOutputStream kos = 475 new KeyTabOutputStream(new FileOutputStream(tabName))) { 476 kos.writeVersion(kt_vno); 477 for (int i = 0; i < entries.size(); i++) { 478 kos.writeEntry(entries.elementAt(i)); 479 } 480 } 481 } 482 483 /** 484 * Removes entries from the key table. 485 * @param service the service <code>PrincipalName</code>. 486 * @param etype the etype to match, remove all if -1 487 * @param kvno what kvno to remove, -1 for all, -2 for old 488 * @return the number of entries deleted 489 */ 490 public int deleteEntries(PrincipalName service, int etype, int kvno) { 491 int count = 0; 492 493 // Remember the highest KVNO for each etype. Used for kvno == -2 494 Map<Integer,Integer> highest = new HashMap<>(); 495 496 for (int i = entries.size()-1; i >= 0; i--) { 497 KeyTabEntry e = entries.get(i); 498 if (service.match(e.getService())) { 499 if (etype == -1 || e.keyType == etype) { 500 if (kvno == -2) { 501 // Two rounds for kvno == -2. In the first round (here), 502 // only find out highest KVNO for each etype 503 if (highest.containsKey(e.keyType)) { 504 int n = highest.get(e.keyType); 505 if (e.keyVersion > n) { 506 highest.put(e.keyType, e.keyVersion); 507 } 508 } else { 509 highest.put(e.keyType, e.keyVersion); 510 } 511 } else if (kvno == -1 || e.keyVersion == kvno) { 512 entries.removeElementAt(i); 513 count++; 514 } 515 } 516 } 517 } 518 519 // Second round for kvno == -2, remove old entries 520 if (kvno == -2) { 521 for (int i = entries.size()-1; i >= 0; i--) { 522 KeyTabEntry e = entries.get(i); 523 if (service.match(e.getService())) { 524 if (etype == -1 || e.keyType == etype) { 525 int n = highest.get(e.keyType); 526 if (e.keyVersion != n) { 527 entries.removeElementAt(i); 528 count++; 529 } 530 } 531 } 532 } 533 } 534 return count; 535 } 536 537 /** 538 * Creates key table file version. 539 * @param file the key table file. 540 * @exception IOException. 541 */ 542 public synchronized void createVersion(File file) throws IOException { 543 try (KeyTabOutputStream kos = 544 new KeyTabOutputStream(new FileOutputStream(file))) { 545 kos.write16(KRB5_KT_VNO); 546 } 547 } 548 }