1 /*
2  * Copyright (c) 2000, 2013, 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      */
KeyTab(String filename)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      */
getInstance0(String s)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      */
getInstance(String s)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      */
getInstance(File file)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      */
getInstance()166     public static KeyTab getInstance() {
167         return getInstance(getDefaultTabName());
168     }
169 
isMissing()170     public boolean isMissing() {
171         return isMissing;
172     }
173 
isValid()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      */
getDefaultTabName()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
normalize(String name)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 
load(KeyTabInputStream kis)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      */
getOneName()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      */
readServiceKeys(PrincipalName service)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      */
findServiceEntry(PrincipalName service)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 
tabName()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      */
addEntry(PrincipalName service, char[] psswd, int kvno, boolean append)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
addEntry(PrincipalName service, String salt, char[] psswd, int kvno, boolean append)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      */
getEntries()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      */
create()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      */
create(String name)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      */
save()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      */
deleteEntries(PrincipalName service, int etype, int kvno)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      */
createVersion(File file)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 }
525