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. 283 * @param service the PrincipalName of the requested service 284 * @return an array containing all the service keys, never null 285 */ 286 public EncryptionKey[] readServiceKeys(PrincipalName service) { 287 KeyTabEntry entry; 288 EncryptionKey key; 289 int size = entries.size(); 290 ArrayList<EncryptionKey> keys = new ArrayList<>(size); 291 if (DEBUG) { 292 System.out.println("Looking for keys for: " + service); 293 } 294 for (int i = size-1; i >= 0; i--) { 295 entry = entries.elementAt(i); 296 if (entry.service.match(service)) { 297 if (EType.isSupported(entry.keyType)) { 298 key = new EncryptionKey(entry.keyblock, 299 entry.keyType, 300 new Integer(entry.keyVersion)); 301 keys.add(key); 302 if (DEBUG) { 303 System.out.println("Added key: " + entry.keyType + 304 "version: " + entry.keyVersion); 305 } 306 } else if (DEBUG) { 307 System.out.println("Found unsupported keytype (" + 308 entry.keyType + ") for " + service); 309 } 310 } 311 } 312 size = keys.size(); 313 EncryptionKey[] retVal = keys.toArray(new EncryptionKey[size]); 314 315 // Sort the keys by kvno. Sometimes we must choose a single key (say, 316 // generate encrypted timestamp in AS-REQ). A key with a higher KVNO 317 // sounds like a newer one. 318 Arrays.sort(retVal, new Comparator<EncryptionKey>() { 319 @Override 320 public int compare(EncryptionKey o1, EncryptionKey o2) { 321 return o2.getKeyVersionNumber().intValue() 322 - o1.getKeyVersionNumber().intValue(); 323 } 324 }); 325 326 return retVal; 327 } 328 329 330 331 /** 332 * Searches for the service entry in the keytab file. 333 * The etype of the key must be one that has been configured 334 * to be used. 335 * @param service the PrincipalName of the requested service. 336 * @return true if the entry is found, otherwise, return false. 337 */ 338 public boolean findServiceEntry(PrincipalName service) { 339 KeyTabEntry entry; 340 for (int i = 0; i < entries.size(); i++) { 341 entry = entries.elementAt(i); 342 if (entry.service.match(service)) { 343 if (EType.isSupported(entry.keyType)) { 344 return true; 345 } else if (DEBUG) { 346 System.out.println("Found unsupported keytype (" + 347 entry.keyType + ") for " + service); 348 } 349 } 350 } 351 return false; 352 } 353 354 public String tabName() { 355 return tabName; 356 } 357 358 /////////////////// THE WRITE SIDE /////////////////////// 359 /////////////// only used by ktab tool ////////////////// 360 361 /** 362 * Adds a new entry in the key table. 363 * @param service the service which will have a new entry in the key table. 364 * @param psswd the password which generates the key. 365 * @param kvno the kvno to use, -1 means automatic increasing 366 * @param append false if entries with old kvno would be removed. 367 * Note: if kvno is not -1, entries with the same kvno are always removed 368 */ 369 public void addEntry(PrincipalName service, char[] psswd, 370 int kvno, boolean append) throws KrbException { 371 addEntry(service, service.getSalt(), psswd, kvno, append); 372 } 373 374 // Called by KDC test 375 public void addEntry(PrincipalName service, String salt, char[] psswd, 376 int kvno, boolean append) throws KrbException { 377 378 EncryptionKey[] encKeys = EncryptionKey.acquireSecretKeys( 379 psswd, salt); 380 381 // There should be only one maximum KVNO value for all etypes, so that 382 // all added keys can have the same KVNO. 383 384 int maxKvno = 0; // only useful when kvno == -1 385 for (int i = entries.size()-1; i >= 0; i--) { 386 KeyTabEntry e = entries.get(i); 387 if (e.service.match(service)) { 388 if (e.keyVersion > maxKvno) { 389 maxKvno = e.keyVersion; 390 } 391 if (!append || e.keyVersion == kvno) { 392 entries.removeElementAt(i); 393 } 394 } 395 } 396 if (kvno == -1) { 397 kvno = maxKvno + 1; 398 } 399 400 for (int i = 0; encKeys != null && i < encKeys.length; i++) { 401 int keyType = encKeys[i].getEType(); 402 byte[] keyValue = encKeys[i].getBytes(); 403 404 KeyTabEntry newEntry = new KeyTabEntry(service, 405 service.getRealm(), 406 new KerberosTime(System.currentTimeMillis()), 407 kvno, keyType, keyValue); 408 entries.addElement(newEntry); 409 } 410 } 411 412 /** 413 * Gets the list of service entries in key table. 414 * @return array of <code>KeyTabEntry</code>. 415 */ 416 public KeyTabEntry[] getEntries() { 417 KeyTabEntry[] kentries = new KeyTabEntry[entries.size()]; 418 for (int i = 0; i < kentries.length; i++) { 419 kentries[i] = entries.elementAt(i); 420 } 421 return kentries; 422 } 423 424 /** 425 * Creates a new default key table. 426 */ 427 public synchronized static KeyTab create() 428 throws IOException, RealmException { 429 String dname = getDefaultTabName(); 430 return create(dname); 431 } 432 433 /** 434 * Creates a new default key table. 435 */ 436 public synchronized static KeyTab create(String name) 437 throws IOException, RealmException { 438 439 try (KeyTabOutputStream kos = 440 new KeyTabOutputStream(new FileOutputStream(name))) { 441 kos.writeVersion(KRB5_KT_VNO); 442 } 443 return new KeyTab(name); 444 } 445 446 /** 447 * Saves the file at the directory. 448 */ 449 public synchronized void save() throws IOException { 450 try (KeyTabOutputStream kos = 451 new KeyTabOutputStream(new FileOutputStream(tabName))) { 452 kos.writeVersion(kt_vno); 453 for (int i = 0; i < entries.size(); i++) { 454 kos.writeEntry(entries.elementAt(i)); 455 } 456 } 457 } 458 459 /** 460 * Removes entries from the key table. 461 * @param service the service <code>PrincipalName</code>. 462 * @param etype the etype to match, remove all if -1 463 * @param kvno what kvno to remove, -1 for all, -2 for old 464 * @return the number of entries deleted 465 */ 466 public int deleteEntries(PrincipalName service, int etype, int kvno) { 467 int count = 0; 468 469 // Remember the highest KVNO for each etype. Used for kvno == -2 470 Map<Integer,Integer> highest = new HashMap<>(); 471 472 for (int i = entries.size()-1; i >= 0; i--) { 473 KeyTabEntry e = entries.get(i); 474 if (service.match(e.getService())) { 475 if (etype == -1 || e.keyType == etype) { 476 if (kvno == -2) { 477 // Two rounds for kvno == -2. In the first round (here), 478 // only find out highest KVNO for each etype 479 if (highest.containsKey(e.keyType)) { 480 int n = highest.get(e.keyType); 481 if (e.keyVersion > n) { 482 highest.put(e.keyType, e.keyVersion); 483 } 484 } else { 485 highest.put(e.keyType, e.keyVersion); 486 } 487 } else if (kvno == -1 || e.keyVersion == kvno) { 488 entries.removeElementAt(i); 489 count++; 490 } 491 } 492 } 493 } 494 495 // Second round for kvno == -2, remove old entries 496 if (kvno == -2) { 497 for (int i = entries.size()-1; i >= 0; i--) { 498 KeyTabEntry e = entries.get(i); 499 if (service.match(e.getService())) { 500 if (etype == -1 || e.keyType == etype) { 501 int n = highest.get(e.keyType); 502 if (e.keyVersion != n) { 503 entries.removeElementAt(i); 504 count++; 505 } 506 } 507 } 508 } 509 } 510 return count; 511 } 512 513 /** 514 * Creates key table file version. 515 * @param file the key table file. 516 * @exception IOException. 517 */ 518 public synchronized void createVersion(File file) throws IOException { 519 try (KeyTabOutputStream kos = 520 new KeyTabOutputStream(new FileOutputStream(file))) { 521 kos.write16(KRB5_KT_VNO); 522 } 523 } 524 }