| /* |
| * Copyright (c) 2008, 2011, Oracle and/or its affiliates. All rights reserved. |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| * |
| * This code is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License version 2 only, as |
| * published by the Free Software Foundation. |
| * |
| * This code is distributed in the hope that it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| * version 2 for more details (a copy is included in the LICENSE file that |
| * accompanied this code). |
| * |
| * You should have received a copy of the GNU General Public License version |
| * 2 along with this work; if not, write to the Free Software Foundation, |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| * |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| * or visit www.oracle.com if you need additional information or have any |
| * questions. |
| */ |
| |
| import java.lang.reflect.Constructor; |
| import java.lang.reflect.Field; |
| import java.lang.reflect.InvocationTargetException; |
| import java.net.*; |
| import java.io.*; |
| import java.lang.reflect.Method; |
| import java.security.SecureRandom; |
| import java.util.*; |
| import java.util.concurrent.*; |
| import sun.net.spi.nameservice.NameService; |
| import sun.net.spi.nameservice.NameServiceDescriptor; |
| import sun.security.krb5.*; |
| import sun.security.krb5.internal.*; |
| import sun.security.krb5.internal.ccache.CredentialsCache; |
| import sun.security.krb5.internal.crypto.KeyUsage; |
| import sun.security.krb5.internal.ktab.KeyTab; |
| import sun.security.util.DerInputStream; |
| import sun.security.util.DerOutputStream; |
| import sun.security.util.DerValue; |
| |
| /** |
| * A KDC server. |
| * <p> |
| * Features: |
| * <ol> |
| * <li> Supports TCP and UDP |
| * <li> Supports AS-REQ and TGS-REQ |
| * <li> Principal db and other settings hard coded in application |
| * <li> Options, say, request preauth or not |
| * </ol> |
| * Side effects: |
| * <ol> |
| * <li> The Sun-internal class <code>sun.security.krb5.Config</code> is a |
| * singleton and initialized according to Kerberos settings (krb5.conf and |
| * java.security.krb5.* system properties). This means once it's initialized |
| * it will not automatically notice any changes to these settings (or file |
| * changes of krb5.conf). The KDC class normally does not touch these |
| * settings (except for the <code>writeKtab()</code> method). However, to make |
| * sure nothing ever goes wrong, if you want to make any changes to these |
| * settings after calling a KDC method, call <code>Config.refresh()</code> to |
| * make sure your changes are reflected in the <code>Config</code> object. |
| * </ol> |
| * System properties recognized: |
| * <ul> |
| * <li>test.kdc.save.ccache |
| * </ul> |
| * Support policies: |
| * <ul> |
| * <li>ok-as-delegate |
| * </ul> |
| * Issues and TODOs: |
| * <ol> |
| * <li> Generates krb5.conf to be used on another machine, currently the kdc is |
| * always localhost |
| * <li> More options to KDC, say, error output, say, response nonce != |
| * request nonce |
| * </ol> |
| * Note: This program uses internal krb5 classes (including reflection to |
| * access private fields and methods). |
| * <p> |
| * Usages: |
| * <p> |
| * 1. Init and start the KDC: |
| * <pre> |
| * KDC kdc = KDC.create("REALM.NAME", port, isDaemon); |
| * KDC kdc = KDC.create("REALM.NAME"); |
| * </pre> |
| * Here, <code>port</code> is the UDP and TCP port number the KDC server |
| * listens on. If zero, a random port is chosen, which you can use getPort() |
| * later to retrieve the value. |
| * <p> |
| * If <code>isDaemon</code> is true, the KDC worker threads will be daemons. |
| * <p> |
| * The shortcut <code>KDC.create("REALM.NAME")</code> has port=0 and |
| * isDaemon=false, and is commonly used in an embedded KDC. |
| * <p> |
| * 2. Adding users: |
| * <pre> |
| * kdc.addPrincipal(String principal_name, char[] password); |
| * kdc.addPrincipalRandKey(String principal_name); |
| * </pre> |
| * A service principal's name should look like "host/f.q.d.n". The second form |
| * generates a random key. To expose this key, call <code>writeKtab()</code> to |
| * save the keys into a keytab file. |
| * <p> |
| * Note that you need to add the principal name krbtgt/REALM.NAME yourself. |
| * <p> |
| * Note that you can safely add a principal at any time after the KDC is |
| * started and before a user requests info on this principal. |
| * <p> |
| * 3. Other public methods: |
| * <ul> |
| * <li> <code>getPort</code>: Returns the port number the KDC uses |
| * <li> <code>getRealm</code>: Returns the realm name |
| * <li> <code>writeKtab</code>: Writes all principals' keys into a keytab file |
| * <li> <code>saveConfig</code>: Saves a krb5.conf file to access this KDC |
| * <li> <code>setOption</code>: Sets various options |
| * </ul> |
| * Read the javadoc for details. Lazy developer can use <code>OneKDC</code> |
| * directly. |
| */ |
| public class KDC { |
| |
| // Under the hood. |
| |
| // The random generator to generate random keys (including session keys) |
| private static SecureRandom secureRandom = new SecureRandom(); |
| |
| // Principal db. principal -> pass. A case-insensitive TreeMap is used |
| // so that even if the client provides a name with different case, the KDC |
| // can still locate the principal and give back correct salt. |
| private TreeMap<String,char[]> passwords = new TreeMap<> |
| (String.CASE_INSENSITIVE_ORDER); |
| |
| // Realm name |
| private String realm; |
| // KDC |
| private String kdc; |
| // Service port number |
| private int port; |
| // The request/response job queue |
| private BlockingQueue<Job> q = new ArrayBlockingQueue<>(100); |
| // Options |
| private Map<Option,Object> options = new HashMap<>(); |
| |
| private Thread thread1, thread2, thread3; |
| DatagramSocket u1 = null; |
| ServerSocket t1 = null; |
| |
| /** |
| * Option names, to be expanded forever. |
| */ |
| public static enum Option { |
| /** |
| * Whether pre-authentication is required. Default Boolean.TRUE |
| */ |
| PREAUTH_REQUIRED, |
| /** |
| * Only issue TGT in RC4 |
| */ |
| ONLY_RC4_TGT, |
| /** |
| * Use RC4 as the first in preauth |
| */ |
| RC4_FIRST_PREAUTH, |
| /** |
| * Use only one preauth, so that some keys are not easy to generate |
| */ |
| ONLY_ONE_PREAUTH, |
| /** |
| * Set all name-type to a value in response |
| */ |
| RESP_NT, |
| /** |
| * Multiple ETYPE-INFO-ENTRY with same etype but different salt |
| */ |
| DUP_ETYPE, |
| }; |
| |
| static { |
| System.setProperty("sun.net.spi.nameservice.provider.1", "ns,mock"); |
| } |
| |
| /** |
| * A standalone KDC server. |
| */ |
| public static void main(String[] args) throws Exception { |
| KDC kdc = create("RABBIT.HOLE", "kdc.rabbit.hole", 0, false); |
| kdc.addPrincipal("dummy", "bogus".toCharArray()); |
| kdc.addPrincipal("foo", "bar".toCharArray()); |
| kdc.addPrincipalRandKey("krbtgt/RABBIT.HOLE"); |
| kdc.addPrincipalRandKey("server/host.rabbit.hole"); |
| kdc.addPrincipalRandKey("backend/host.rabbit.hole"); |
| KDC.saveConfig("krb5.conf", kdc, "forwardable = true"); |
| } |
| |
| /** |
| * Creates and starts a KDC running as a daemon on a random port. |
| * @param realm the realm name |
| * @return the running KDC instance |
| * @throws java.io.IOException for any socket creation error |
| */ |
| public static KDC create(String realm) throws IOException { |
| return create(realm, "kdc." + realm.toLowerCase(), 0, true); |
| } |
| |
| public static KDC existing(String realm, String kdc, int port) { |
| KDC k = new KDC(realm, kdc); |
| k.port = port; |
| return k; |
| } |
| |
| /** |
| * Creates and starts a KDC server. |
| * @param realm the realm name |
| * @param port the TCP and UDP port to listen to. A random port will to |
| * chosen if zero. |
| * @param asDaemon if true, KDC threads will be daemons. Otherwise, not. |
| * @return the running KDC instance |
| * @throws java.io.IOException for any socket creation error |
| */ |
| public static KDC create(String realm, String kdc, int port, boolean asDaemon) throws IOException { |
| return new KDC(realm, kdc, port, asDaemon); |
| } |
| |
| /** |
| * Sets an option |
| * @param key the option name |
| * @param obj the value |
| */ |
| public void setOption(Option key, Object value) { |
| options.put(key, value); |
| } |
| |
| /** |
| * Writes or appends KDC keys into a keytab. See doc for writeMultiKtab. |
| * @param append true if append, otherwise, overwrite. |
| */ |
| private static void writeKtab0(String tab, boolean append, KDC... kdcs) |
| throws IOException, KrbException { |
| KeyTab ktab = append ? KeyTab.getInstance(tab) : KeyTab.create(tab); |
| for (KDC kdc: kdcs) { |
| for (String name : kdc.passwords.keySet()) { |
| char[] pass = kdc.passwords.get(name); |
| int kvno = 0; |
| if (Character.isDigit(pass[pass.length-1])) { |
| kvno = pass[pass.length-1] - '0'; |
| } |
| ktab.addEntry(new PrincipalName(name, |
| name.indexOf('/') < 0 ? |
| PrincipalName.KRB_NT_UNKNOWN : |
| PrincipalName.KRB_NT_SRV_HST), |
| pass, |
| kvno, |
| true); |
| } |
| } |
| ktab.save(); |
| } |
| |
| /** |
| * Writes all principals' keys from multiple KDCs into one keytab file. |
| * Note that the keys for the krbtgt principals will not be written. |
| * <p> |
| * Attention: This method references krb5.conf settings. If you need to |
| * setup krb5.conf later, please call <code>Config.refresh()</code> after |
| * the new setting. For example: |
| * <pre> |
| * KDC.writeKtab("/etc/kdc/ktab", kdc); // Config is initialized, |
| * System.setProperty("java.security.krb5.conf", "/home/mykrb5.conf"); |
| * Config.refresh(); |
| * </pre> |
| * |
| * Inside this method there are 2 places krb5.conf is used: |
| * <ol> |
| * <li> (Fatal) Generating keys: EncryptionKey.acquireSecretKeys |
| * <li> (Has workaround) Creating PrincipalName |
| * </ol> |
| * @param tab The keytab filename to write to. |
| * @throws java.io.IOException for any file output error |
| * @throws sun.security.krb5.KrbException for any realm and/or principal |
| * name error. |
| */ |
| public static void writeMultiKtab(String tab, KDC... kdcs) |
| throws IOException, KrbException { |
| writeKtab0(tab, false, kdcs); |
| } |
| |
| /** |
| * Appends all principals' keys from multiple KDCs to one keytab file. |
| * See writeMultiKtab for details. |
| */ |
| public static void appendMultiKtab(String tab, KDC... kdcs) |
| throws IOException, KrbException { |
| writeKtab0(tab, true, kdcs); |
| } |
| |
| /** |
| * Write a ktab for this KDC. |
| */ |
| public void writeKtab(String tab) throws IOException, KrbException { |
| KDC.writeMultiKtab(tab, this); |
| } |
| |
| /** |
| * Appends keys in this KDC to a ktab. |
| */ |
| public void appendKtab(String tab) throws IOException, KrbException { |
| KDC.appendMultiKtab(tab, this); |
| } |
| |
| /** |
| * Adds a new principal to this realm with a given password. |
| * @param user the principal's name. For a service principal, use the |
| * form of host/f.q.d.n |
| * @param pass the password for the principal |
| */ |
| public void addPrincipal(String user, char[] pass) { |
| if (user.indexOf('@') < 0) { |
| user = user + "@" + realm; |
| } |
| passwords.put(user, pass); |
| } |
| |
| /** |
| * Adds a new principal to this realm with a random password |
| * @param user the principal's name. For a service principal, use the |
| * form of host/f.q.d.n |
| */ |
| public void addPrincipalRandKey(String user) { |
| addPrincipal(user, randomPassword()); |
| } |
| |
| /** |
| * Returns the name of this realm |
| * @return the name of this realm |
| */ |
| public String getRealm() { |
| return realm; |
| } |
| |
| /** |
| * Returns the name of kdc |
| * @return the name of kdc |
| */ |
| public String getKDC() { |
| return kdc; |
| } |
| |
| /** |
| * Writes a krb5.conf for one or more KDC that includes KDC locations for |
| * each realm and the default realm name. You can also add extra strings |
| * into the file. The method should be called like: |
| * <pre> |
| * KDC.saveConfig("krb5.conf", kdc1, kdc2, ..., line1, line2, ...); |
| * </pre> |
| * Here you can provide one or more kdc# and zero or more line# arguments. |
| * The line# will be put after [libdefaults] and before [realms]. Therefore |
| * you can append new lines into [libdefaults] and/or create your new |
| * stanzas as well. Note that a newline character will be appended to |
| * each line# argument. |
| * <p> |
| * For example: |
| * <pre> |
| * KDC.saveConfig("krb5.conf", this); |
| * </pre> |
| * generates: |
| * <pre> |
| * [libdefaults] |
| * default_realm = REALM.NAME |
| * |
| * [realms] |
| * REALM.NAME = { |
| * kdc = host:port_number |
| * } |
| * </pre> |
| * |
| * Another example: |
| * <pre> |
| * KDC.saveConfig("krb5.conf", kdc1, kdc2, "forwardable = true", "", |
| * "[domain_realm]", |
| * ".kdc1.com = KDC1.NAME"); |
| * </pre> |
| * generates: |
| * <pre> |
| * [libdefaults] |
| * default_realm = KDC1.NAME |
| * forwardable = true |
| * |
| * [domain_realm] |
| * .kdc1.com = KDC1.NAME |
| * |
| * [realms] |
| * KDC1.NAME = { |
| * kdc = host:port1 |
| * } |
| * KDC2.NAME = { |
| * kdc = host:port2 |
| * } |
| * </pre> |
| * @param file the name of the file to write into |
| * @param kdc the first (and default) KDC |
| * @param more more KDCs or extra lines (in their appearing order) to |
| * insert into the krb5.conf file. This method reads each argument's type |
| * to determine what it's for. This argument can be empty. |
| * @throws java.io.IOException for any file output error |
| */ |
| public static void saveConfig(String file, KDC kdc, Object... more) |
| throws IOException { |
| File f = new File(file); |
| StringBuffer sb = new StringBuffer(); |
| sb.append("[libdefaults]\ndefault_realm = "); |
| sb.append(kdc.realm); |
| sb.append("\n"); |
| for (Object o: more) { |
| if (o instanceof String) { |
| sb.append(o); |
| sb.append("\n"); |
| } |
| } |
| sb.append("\n[realms]\n"); |
| sb.append(realmLineForKDC(kdc)); |
| for (Object o: more) { |
| if (o instanceof KDC) { |
| sb.append(realmLineForKDC((KDC)o)); |
| } |
| } |
| FileOutputStream fos = new FileOutputStream(f); |
| fos.write(sb.toString().getBytes()); |
| fos.close(); |
| } |
| |
| /** |
| * Returns the service port of the KDC server. |
| * @return the KDC service port |
| */ |
| public int getPort() { |
| return port; |
| } |
| |
| // Private helper methods |
| |
| /** |
| * Private constructor, cannot be called outside. |
| * @param realm |
| */ |
| private KDC(String realm, String kdc) { |
| this.realm = realm; |
| this.kdc = kdc; |
| } |
| |
| /** |
| * A constructor that starts the KDC service also. |
| */ |
| protected KDC(String realm, String kdc, int port, boolean asDaemon) |
| throws IOException { |
| this(realm, kdc); |
| startServer(port, asDaemon); |
| } |
| /** |
| * Generates a 32-char random password |
| * @return the password |
| */ |
| private static char[] randomPassword() { |
| char[] pass = new char[32]; |
| for (int i=0; i<31; i++) |
| pass[i] = (char)secureRandom.nextInt(); |
| // The last char cannot be a number, otherwise, keyForUser() |
| // believes it's a sign of kvno |
| pass[31] = 'Z'; |
| return pass; |
| } |
| |
| /** |
| * Generates a random key for the given encryption type. |
| * @param eType the encryption type |
| * @return the generated key |
| * @throws sun.security.krb5.KrbException for unknown/unsupported etype |
| */ |
| private static EncryptionKey generateRandomKey(int eType) |
| throws KrbException { |
| // Is 32 enough for AES256? I should have generated the keys directly |
| // but different cryptos have different rules on what keys are valid. |
| char[] pass = randomPassword(); |
| String algo; |
| switch (eType) { |
| case EncryptedData.ETYPE_DES_CBC_MD5: algo = "DES"; break; |
| case EncryptedData.ETYPE_DES3_CBC_HMAC_SHA1_KD: algo = "DESede"; break; |
| case EncryptedData.ETYPE_AES128_CTS_HMAC_SHA1_96: algo = "AES128"; break; |
| case EncryptedData.ETYPE_ARCFOUR_HMAC: algo = "ArcFourHMAC"; break; |
| case EncryptedData.ETYPE_AES256_CTS_HMAC_SHA1_96: algo = "AES256"; break; |
| default: algo = "DES"; break; |
| } |
| return new EncryptionKey(pass, "NOTHING", algo); // Silly |
| } |
| |
| /** |
| * Returns the password for a given principal |
| * @param p principal |
| * @return the password |
| * @throws sun.security.krb5.KrbException when the principal is not inside |
| * the database. |
| */ |
| private char[] getPassword(PrincipalName p, boolean server) |
| throws KrbException { |
| String pn = p.toString(); |
| if (p.getRealmString() == null) { |
| pn = pn + "@" + getRealm(); |
| } |
| char[] pass = passwords.get(pn); |
| if (pass == null) { |
| throw new KrbException(server? |
| Krb5.KDC_ERR_S_PRINCIPAL_UNKNOWN: |
| Krb5.KDC_ERR_C_PRINCIPAL_UNKNOWN); |
| } |
| return pass; |
| } |
| |
| /** |
| * Returns the salt string for the principal. |
| * @param p principal |
| * @return the salt |
| */ |
| private String getSalt(PrincipalName p) { |
| String pn = p.toString(); |
| if (p.getRealmString() == null) { |
| pn = pn + "@" + getRealm(); |
| } |
| if (passwords.containsKey(pn)) { |
| try { |
| // Find the principal name with correct case. |
| p = new PrincipalName(passwords.ceilingEntry(pn).getKey()); |
| } catch (RealmException re) { |
| // Won't happen |
| } |
| } |
| String s = p.getRealmString(); |
| if (s == null) s = getRealm(); |
| for (String n: p.getNameStrings()) { |
| s += n; |
| } |
| return s; |
| } |
| |
| /** |
| * Returns the key for a given principal of the given encryption type |
| * @param p the principal |
| * @param etype the encryption type |
| * @param server looking for a server principal? |
| * @return the key |
| * @throws sun.security.krb5.KrbException for unknown/unsupported etype |
| */ |
| private EncryptionKey keyForUser(PrincipalName p, int etype, boolean server) |
| throws KrbException { |
| try { |
| // Do not call EncryptionKey.acquireSecretKeys(), otherwise |
| // the krb5.conf config file would be loaded. |
| Integer kvno = null; |
| // For service whose password ending with a number, use it as kvno. |
| // Kvno must be postive. |
| if (p.toString().indexOf('/') > 0) { |
| char[] pass = getPassword(p, server); |
| if (Character.isDigit(pass[pass.length-1])) { |
| kvno = pass[pass.length-1] - '0'; |
| } |
| } |
| return new EncryptionKey(EncryptionKeyDotStringToKey( |
| getPassword(p, server), getSalt(p), null, etype), |
| etype, kvno); |
| } catch (KrbException ke) { |
| throw ke; |
| } catch (Exception e) { |
| throw new RuntimeException(e); // should not happen |
| } |
| } |
| |
| private Map<String,String> policies = new HashMap<>(); |
| |
| public void setPolicy(String rule, String value) { |
| if (value == null) { |
| policies.remove(rule); |
| } else { |
| policies.put(rule, value); |
| } |
| } |
| /** |
| * If the provided client/server pair matches a rule |
| * |
| * A system property named test.kdc.policy.RULE will be consulted. |
| * If it's unset, returns false. If its value is "", any pair is |
| * matched. Otherwise, it should contains the server name matched. |
| * |
| * TODO: client name is not used currently. |
| * |
| * @param c client name |
| * @param s server name |
| * @param rule rule name |
| * @return if a match is found |
| */ |
| private boolean configMatch(String c, String s, String rule) { |
| String policy = policies.get(rule); |
| boolean result = false; |
| if (policy == null) { |
| result = false; |
| } else if (policy.length() == 0) { |
| result = true; |
| } else { |
| String[] names = policy.split("\\s+"); |
| for (String name: names) { |
| if (name.equals(s)) { |
| result = true; |
| break; |
| } |
| } |
| } |
| if (result) { |
| System.out.printf(">>>> Policy match result (%s vs %s on %s) %b\n", |
| c, s, rule, result); |
| } |
| return result; |
| } |
| |
| |
| /** |
| * Processes an incoming request and generates a response. |
| * @param in the request |
| * @return the response |
| * @throws java.lang.Exception for various errors |
| */ |
| private byte[] processMessage(byte[] in) throws Exception { |
| if ((in[0] & 0x1f) == Krb5.KRB_AS_REQ) |
| return processAsReq(in); |
| else |
| return processTgsReq(in); |
| } |
| |
| /** |
| * Processes a TGS_REQ and generates a TGS_REP (or KRB_ERROR) |
| * @param in the request |
| * @return the response |
| * @throws java.lang.Exception for various errors |
| */ |
| private byte[] processTgsReq(byte[] in) throws Exception { |
| TGSReq tgsReq = new TGSReq(in); |
| PrincipalName service = tgsReq.reqBody.sname; |
| if (options.containsKey(KDC.Option.RESP_NT)) { |
| service = new PrincipalName(service.getNameStrings(), |
| (int)options.get(KDC.Option.RESP_NT)); |
| service.setRealm(service.getRealm()); |
| } |
| try { |
| System.out.println(realm + "> " + tgsReq.reqBody.cname + |
| " sends TGS-REQ for " + |
| service); |
| KDCReqBody body = tgsReq.reqBody; |
| int[] eTypes = KDCReqBodyDotEType(body); |
| int e2 = eTypes[0]; // etype for outgoing session key |
| int e3 = eTypes[0]; // etype for outgoing ticket |
| |
| PAData[] pas = kDCReqDotPAData(tgsReq); |
| |
| Ticket tkt = null; |
| EncTicketPart etp = null; |
| if (pas == null || pas.length == 0) { |
| throw new KrbException(Krb5.KDC_ERR_PADATA_TYPE_NOSUPP); |
| } else { |
| for (PAData pa: pas) { |
| if (pa.getType() == Krb5.PA_TGS_REQ) { |
| APReq apReq = new APReq(pa.getValue()); |
| EncryptedData ed = apReq.authenticator; |
| tkt = apReq.ticket; |
| int te = tkt.encPart.getEType(); |
| tkt.sname.setRealm(tkt.realm); |
| EncryptionKey kkey = keyForUser(tkt.sname, te, true); |
| byte[] bb = tkt.encPart.decrypt(kkey, KeyUsage.KU_TICKET); |
| DerInputStream derIn = new DerInputStream(bb); |
| DerValue der = derIn.getDerValue(); |
| etp = new EncTicketPart(der.toByteArray()); |
| } |
| } |
| if (tkt == null) { |
| throw new KrbException(Krb5.KDC_ERR_PADATA_TYPE_NOSUPP); |
| } |
| } |
| |
| // Session key for original ticket, TGT |
| EncryptionKey ckey = etp.key; |
| |
| // Session key for session with the service |
| EncryptionKey key = generateRandomKey(e2); |
| |
| // Check time, TODO |
| KerberosTime till = body.till; |
| if (till == null) { |
| throw new KrbException(Krb5.KDC_ERR_NEVER_VALID); // TODO |
| } else if (till.isZero()) { |
| till = new KerberosTime(new Date().getTime() + 1000 * 3600 * 11); |
| } |
| |
| boolean[] bFlags = new boolean[Krb5.TKT_OPTS_MAX+1]; |
| if (body.kdcOptions.get(KDCOptions.FORWARDABLE)) { |
| bFlags[Krb5.TKT_OPTS_FORWARDABLE] = true; |
| } |
| if (body.kdcOptions.get(KDCOptions.FORWARDED) || |
| etp.flags.get(Krb5.TKT_OPTS_FORWARDED)) { |
| bFlags[Krb5.TKT_OPTS_FORWARDED] = true; |
| } |
| if (body.kdcOptions.get(KDCOptions.RENEWABLE)) { |
| bFlags[Krb5.TKT_OPTS_RENEWABLE] = true; |
| //renew = new KerberosTime(new Date().getTime() + 1000 * 3600 * 24 * 7); |
| } |
| if (body.kdcOptions.get(KDCOptions.PROXIABLE)) { |
| bFlags[Krb5.TKT_OPTS_PROXIABLE] = true; |
| } |
| if (body.kdcOptions.get(KDCOptions.POSTDATED)) { |
| bFlags[Krb5.TKT_OPTS_POSTDATED] = true; |
| } |
| if (body.kdcOptions.get(KDCOptions.ALLOW_POSTDATE)) { |
| bFlags[Krb5.TKT_OPTS_MAY_POSTDATE] = true; |
| } |
| |
| if (configMatch("", service.getNameString(), "ok-as-delegate")) { |
| bFlags[Krb5.TKT_OPTS_DELEGATE] = true; |
| } |
| bFlags[Krb5.TKT_OPTS_INITIAL] = true; |
| |
| TicketFlags tFlags = new TicketFlags(bFlags); |
| EncTicketPart enc = new EncTicketPart( |
| tFlags, |
| key, |
| etp.crealm, |
| etp.cname, |
| new TransitedEncoding(1, new byte[0]), // TODO |
| new KerberosTime(new Date()), |
| body.from, |
| till, body.rtime, |
| body.addresses != null // always set caddr |
| ? body.addresses |
| : new HostAddresses( |
| new InetAddress[]{InetAddress.getLocalHost()}), |
| null); |
| EncryptionKey skey = keyForUser(service, e3, true); |
| if (skey == null) { |
| throw new KrbException(Krb5.KDC_ERR_SUMTYPE_NOSUPP); // TODO |
| } |
| Ticket t = new Ticket( |
| body.crealm, |
| service, |
| new EncryptedData(skey, enc.asn1Encode(), KeyUsage.KU_TICKET) |
| ); |
| EncTGSRepPart enc_part = new EncTGSRepPart( |
| key, |
| new LastReq(new LastReqEntry[]{ |
| new LastReqEntry(0, new KerberosTime(new Date().getTime() - 10000)) |
| }), |
| body.getNonce(), // TODO: detect replay |
| new KerberosTime(new Date().getTime() + 1000 * 3600 * 24), |
| // Next 5 and last MUST be same with ticket |
| tFlags, |
| new KerberosTime(new Date()), |
| body.from, |
| till, body.rtime, |
| body.crealm, |
| service, |
| body.addresses != null // always set caddr |
| ? body.addresses |
| : new HostAddresses( |
| new InetAddress[]{InetAddress.getLocalHost()}) |
| ); |
| EncryptedData edata = new EncryptedData(ckey, enc_part.asn1Encode(), KeyUsage.KU_ENC_TGS_REP_PART_SESSKEY); |
| TGSRep tgsRep = new TGSRep(null, |
| etp.crealm, |
| etp.cname, |
| t, |
| edata); |
| System.out.println(" Return " + tgsRep.cname |
| + " ticket for " + tgsRep.ticket.sname); |
| |
| DerOutputStream out = new DerOutputStream(); |
| out.write(DerValue.createTag(DerValue.TAG_APPLICATION, |
| true, (byte)Krb5.KRB_TGS_REP), tgsRep.asn1Encode()); |
| return out.toByteArray(); |
| } catch (KrbException ke) { |
| ke.printStackTrace(System.out); |
| KRBError kerr = ke.getError(); |
| KDCReqBody body = tgsReq.reqBody; |
| System.out.println(" Error " + ke.returnCode() |
| + " " +ke.returnCodeMessage()); |
| if (kerr == null) { |
| kerr = new KRBError(null, null, null, |
| new KerberosTime(new Date()), |
| 0, |
| ke.returnCode(), |
| body.crealm, body.cname, |
| new Realm(getRealm()), service, |
| KrbException.errorMessage(ke.returnCode()), |
| null); |
| } |
| return kerr.asn1Encode(); |
| } |
| } |
| |
| /** |
| * Processes a AS_REQ and generates a AS_REP (or KRB_ERROR) |
| * @param in the request |
| * @return the response |
| * @throws java.lang.Exception for various errors |
| */ |
| private byte[] processAsReq(byte[] in) throws Exception { |
| ASReq asReq = new ASReq(in); |
| int[] eTypes = null; |
| List<PAData> outPAs = new ArrayList<>(); |
| |
| PrincipalName service = asReq.reqBody.sname; |
| if (options.containsKey(KDC.Option.RESP_NT)) { |
| service = new PrincipalName(service.getNameStrings(), |
| (int)options.get(KDC.Option.RESP_NT)); |
| service.setRealm(service.getRealm()); |
| } |
| try { |
| System.out.println(realm + "> " + asReq.reqBody.cname + |
| " sends AS-REQ for " + |
| service); |
| |
| KDCReqBody body = asReq.reqBody; |
| body.cname.setRealm(getRealm()); |
| |
| eTypes = KDCReqBodyDotEType(body); |
| int eType = eTypes[0]; |
| |
| EncryptionKey ckey = keyForUser(body.cname, eType, false); |
| EncryptionKey skey = keyForUser(service, eType, true); |
| |
| if (options.containsKey(KDC.Option.ONLY_RC4_TGT)) { |
| int tgtEType = EncryptedData.ETYPE_ARCFOUR_HMAC; |
| boolean found = false; |
| for (int i=0; i<eTypes.length; i++) { |
| if (eTypes[i] == tgtEType) { |
| found = true; |
| break; |
| } |
| } |
| if (!found) { |
| throw new KrbException(Krb5.KDC_ERR_ETYPE_NOSUPP); |
| } |
| skey = keyForUser(service, tgtEType, true); |
| } |
| if (ckey == null) { |
| throw new KrbException(Krb5.KDC_ERR_ETYPE_NOSUPP); |
| } |
| if (skey == null) { |
| throw new KrbException(Krb5.KDC_ERR_SUMTYPE_NOSUPP); // TODO |
| } |
| |
| // Session key |
| EncryptionKey key = generateRandomKey(eType); |
| // Check time, TODO |
| KerberosTime till = body.till; |
| if (till == null) { |
| throw new KrbException(Krb5.KDC_ERR_NEVER_VALID); // TODO |
| } else if (till.isZero()) { |
| till = new KerberosTime(new Date().getTime() + 1000 * 3600 * 11); |
| } |
| //body.from |
| boolean[] bFlags = new boolean[Krb5.TKT_OPTS_MAX+1]; |
| if (body.kdcOptions.get(KDCOptions.FORWARDABLE)) { |
| bFlags[Krb5.TKT_OPTS_FORWARDABLE] = true; |
| } |
| if (body.kdcOptions.get(KDCOptions.RENEWABLE)) { |
| bFlags[Krb5.TKT_OPTS_RENEWABLE] = true; |
| //renew = new KerberosTime(new Date().getTime() + 1000 * 3600 * 24 * 7); |
| } |
| if (body.kdcOptions.get(KDCOptions.PROXIABLE)) { |
| bFlags[Krb5.TKT_OPTS_PROXIABLE] = true; |
| } |
| if (body.kdcOptions.get(KDCOptions.POSTDATED)) { |
| bFlags[Krb5.TKT_OPTS_POSTDATED] = true; |
| } |
| if (body.kdcOptions.get(KDCOptions.ALLOW_POSTDATE)) { |
| bFlags[Krb5.TKT_OPTS_MAY_POSTDATE] = true; |
| } |
| bFlags[Krb5.TKT_OPTS_INITIAL] = true; |
| |
| // Creating PA-DATA |
| DerValue[] pas2 = null, pas = null; |
| if (options.containsKey(KDC.Option.DUP_ETYPE)) { |
| int n = (Integer)options.get(KDC.Option.DUP_ETYPE); |
| switch (n) { |
| case 1: // customer's case in 7067974 |
| pas2 = new DerValue[] { |
| new DerValue(new ETypeInfo2(1, null, null).asn1Encode()), |
| new DerValue(new ETypeInfo2(1, "", null).asn1Encode()), |
| new DerValue(new ETypeInfo2(1, OneKDC.REALM, new byte[]{1}).asn1Encode()), |
| }; |
| pas = new DerValue[] { |
| new DerValue(new ETypeInfo(1, null).asn1Encode()), |
| new DerValue(new ETypeInfo(1, "").asn1Encode()), |
| new DerValue(new ETypeInfo(1, OneKDC.REALM).asn1Encode()), |
| }; |
| break; |
| case 2: // we still reject non-null s2kparams and prefer E2 over E |
| pas2 = new DerValue[] { |
| new DerValue(new ETypeInfo2(1, OneKDC.REALM, new byte[]{1}).asn1Encode()), |
| new DerValue(new ETypeInfo2(1, null, null).asn1Encode()), |
| new DerValue(new ETypeInfo2(1, "", null).asn1Encode()), |
| }; |
| pas = new DerValue[] { |
| new DerValue(new ETypeInfo(1, OneKDC.REALM).asn1Encode()), |
| new DerValue(new ETypeInfo(1, null).asn1Encode()), |
| new DerValue(new ETypeInfo(1, "").asn1Encode()), |
| }; |
| break; |
| case 3: // but only E is wrong |
| pas = new DerValue[] { |
| new DerValue(new ETypeInfo(1, OneKDC.REALM).asn1Encode()), |
| new DerValue(new ETypeInfo(1, null).asn1Encode()), |
| new DerValue(new ETypeInfo(1, "").asn1Encode()), |
| }; |
| break; |
| case 4: // we also ignore rc4-hmac |
| pas = new DerValue[] { |
| new DerValue(new ETypeInfo(23, "ANYTHING").asn1Encode()), |
| new DerValue(new ETypeInfo(1, null).asn1Encode()), |
| new DerValue(new ETypeInfo(1, "").asn1Encode()), |
| }; |
| break; |
| case 5: // "" should be wrong, but we accept it now |
| // See s.s.k.internal.PAData$SaltAndParams |
| pas = new DerValue[] { |
| new DerValue(new ETypeInfo(1, "").asn1Encode()), |
| new DerValue(new ETypeInfo(1, null).asn1Encode()), |
| }; |
| break; |
| } |
| } else { |
| int[] epas = eTypes; |
| if (options.containsKey(KDC.Option.RC4_FIRST_PREAUTH)) { |
| for (int i=1; i<epas.length; i++) { |
| if (epas[i] == EncryptedData.ETYPE_ARCFOUR_HMAC) { |
| epas[i] = epas[0]; |
| epas[0] = EncryptedData.ETYPE_ARCFOUR_HMAC; |
| break; |
| } |
| }; |
| } else if (options.containsKey(KDC.Option.ONLY_ONE_PREAUTH)) { |
| epas = new int[] { eTypes[0] }; |
| } |
| pas2 = new DerValue[epas.length]; |
| for (int i=0; i<epas.length; i++) { |
| pas2[i] = new DerValue(new ETypeInfo2( |
| epas[i], |
| epas[i] == EncryptedData.ETYPE_ARCFOUR_HMAC ? |
| null : getSalt(body.cname), |
| null).asn1Encode()); |
| } |
| boolean allOld = true; |
| for (int i: eTypes) { |
| if (i == EncryptedData.ETYPE_AES128_CTS_HMAC_SHA1_96 || |
| i == EncryptedData.ETYPE_AES256_CTS_HMAC_SHA1_96) { |
| allOld = false; |
| break; |
| } |
| } |
| if (allOld) { |
| pas = new DerValue[epas.length]; |
| for (int i=0; i<epas.length; i++) { |
| pas[i] = new DerValue(new ETypeInfo( |
| epas[i], |
| epas[i] == EncryptedData.ETYPE_ARCFOUR_HMAC ? |
| null : getSalt(body.cname) |
| ).asn1Encode()); |
| } |
| } |
| } |
| |
| DerOutputStream eid; |
| if (pas2 != null) { |
| eid = new DerOutputStream(); |
| eid.putSequence(pas2); |
| outPAs.add(new PAData(Krb5.PA_ETYPE_INFO2, eid.toByteArray())); |
| } |
| if (pas != null) { |
| eid = new DerOutputStream(); |
| eid.putSequence(pas); |
| outPAs.add(new PAData(Krb5.PA_ETYPE_INFO, eid.toByteArray())); |
| } |
| |
| PAData[] inPAs = kDCReqDotPAData(asReq); |
| if (inPAs == null || inPAs.length == 0) { |
| Object preauth = options.get(Option.PREAUTH_REQUIRED); |
| if (preauth == null || preauth.equals(Boolean.TRUE)) { |
| throw new KrbException(Krb5.KDC_ERR_PREAUTH_REQUIRED); |
| } |
| } else { |
| try { |
| EncryptedData data = newEncryptedData(new DerValue(inPAs[0].getValue())); |
| EncryptionKey pakey = keyForUser(body.cname, data.getEType(), false); |
| data.decrypt(pakey, KeyUsage.KU_PA_ENC_TS); |
| } catch (Exception e) { |
| throw new KrbException(Krb5.KDC_ERR_PREAUTH_FAILED); |
| } |
| bFlags[Krb5.TKT_OPTS_PRE_AUTHENT] = true; |
| } |
| |
| TicketFlags tFlags = new TicketFlags(bFlags); |
| EncTicketPart enc = new EncTicketPart( |
| tFlags, |
| key, |
| body.crealm, |
| body.cname, |
| new TransitedEncoding(1, new byte[0]), |
| new KerberosTime(new Date()), |
| body.from, |
| till, body.rtime, |
| body.addresses, |
| null); |
| Ticket t = new Ticket( |
| body.crealm, |
| service, |
| new EncryptedData(skey, enc.asn1Encode(), KeyUsage.KU_TICKET) |
| ); |
| EncASRepPart enc_part = new EncASRepPart( |
| key, |
| new LastReq(new LastReqEntry[]{ |
| new LastReqEntry(0, new KerberosTime(new Date().getTime() - 10000)) |
| }), |
| body.getNonce(), // TODO: detect replay? |
| new KerberosTime(new Date().getTime() + 1000 * 3600 * 24), |
| // Next 5 and last MUST be same with ticket |
| tFlags, |
| new KerberosTime(new Date()), |
| body.from, |
| till, body.rtime, |
| body.crealm, |
| service, |
| body.addresses |
| ); |
| EncryptedData edata = new EncryptedData(ckey, enc_part.asn1Encode(), KeyUsage.KU_ENC_AS_REP_PART); |
| ASRep asRep = new ASRep( |
| outPAs.toArray(new PAData[outPAs.size()]), |
| body.crealm, |
| body.cname, |
| t, |
| edata); |
| |
| System.out.println(" Return " + asRep.cname |
| + " ticket for " + asRep.ticket.sname); |
| |
| DerOutputStream out = new DerOutputStream(); |
| out.write(DerValue.createTag(DerValue.TAG_APPLICATION, |
| true, (byte)Krb5.KRB_AS_REP), asRep.asn1Encode()); |
| byte[] result = out.toByteArray(); |
| |
| // Added feature: |
| // Write the current issuing TGT into a ccache file specified |
| // by the system property below. |
| String ccache = System.getProperty("test.kdc.save.ccache"); |
| if (ccache != null) { |
| asRep.encKDCRepPart = enc_part; |
| sun.security.krb5.internal.ccache.Credentials credentials = |
| new sun.security.krb5.internal.ccache.Credentials(asRep); |
| asReq.reqBody.cname.setRealm(getRealm()); |
| CredentialsCache cache = |
| CredentialsCache.create(asReq.reqBody.cname, ccache); |
| if (cache == null) { |
| throw new IOException("Unable to create the cache file " + |
| ccache); |
| } |
| cache.update(credentials); |
| cache.save(); |
| } |
| |
| return result; |
| } catch (KrbException ke) { |
| ke.printStackTrace(System.out); |
| KRBError kerr = ke.getError(); |
| KDCReqBody body = asReq.reqBody; |
| System.out.println(" Error " + ke.returnCode() |
| + " " +ke.returnCodeMessage()); |
| byte[] eData = null; |
| if (kerr == null) { |
| if (ke.returnCode() == Krb5.KDC_ERR_PREAUTH_REQUIRED || |
| ke.returnCode() == Krb5.KDC_ERR_PREAUTH_FAILED) { |
| DerOutputStream bytes = new DerOutputStream(); |
| bytes.write(new PAData(Krb5.PA_ENC_TIMESTAMP, new byte[0]).asn1Encode()); |
| for (PAData p: outPAs) { |
| bytes.write(p.asn1Encode()); |
| } |
| DerOutputStream temp = new DerOutputStream(); |
| temp.write(DerValue.tag_Sequence, bytes); |
| eData = temp.toByteArray(); |
| } |
| kerr = new KRBError(null, null, null, |
| new KerberosTime(new Date()), |
| 0, |
| ke.returnCode(), |
| body.crealm, body.cname, |
| new Realm(getRealm()), service, |
| KrbException.errorMessage(ke.returnCode()), |
| eData); |
| } |
| return kerr.asn1Encode(); |
| } |
| } |
| |
| /** |
| * Generates a line for a KDC to put inside [realms] of krb5.conf |
| * @param kdc the KDC |
| * @return REALM.NAME = { kdc = host:port } |
| */ |
| private static String realmLineForKDC(KDC kdc) { |
| return String.format(" %s = {\n kdc = %s:%d\n }\n", |
| kdc.realm, |
| kdc.kdc, |
| kdc.port); |
| } |
| |
| /** |
| * Start the KDC service. This server listens on both UDP and TCP using |
| * the same port number. It uses three threads to deal with requests. |
| * They can be set to daemon threads if requested. |
| * @param port the port number to listen to. If zero, a random available |
| * port no less than 8000 will be chosen and used. |
| * @param asDaemon true if the KDC threads should be daemons |
| * @throws java.io.IOException for any communication error |
| */ |
| protected void startServer(int port, boolean asDaemon) throws IOException { |
| if (port > 0) { |
| u1 = new DatagramSocket(port, InetAddress.getByName("127.0.0.1")); |
| t1 = new ServerSocket(port); |
| } else { |
| while (true) { |
| // Try to find a port number that's both TCP and UDP free |
| try { |
| port = 8000 + new java.util.Random().nextInt(10000); |
| u1 = null; |
| u1 = new DatagramSocket(port, InetAddress.getByName("127.0.0.1")); |
| t1 = new ServerSocket(port); |
| break; |
| } catch (Exception e) { |
| if (u1 != null) u1.close(); |
| } |
| } |
| } |
| final DatagramSocket udp = u1; |
| final ServerSocket tcp = t1; |
| System.out.println("Start KDC on " + port); |
| |
| this.port = port; |
| |
| // The UDP consumer |
| thread1 = new Thread() { |
| public void run() { |
| while (true) { |
| try { |
| byte[] inbuf = new byte[8192]; |
| DatagramPacket p = new DatagramPacket(inbuf, inbuf.length); |
| udp.receive(p); |
| System.out.println("-----------------------------------------------"); |
| System.out.println(">>>>> UDP packet received"); |
| q.put(new Job(processMessage(Arrays.copyOf(inbuf, p.getLength())), udp, p)); |
| } catch (Exception e) { |
| e.printStackTrace(); |
| } |
| } |
| } |
| }; |
| thread1.setDaemon(asDaemon); |
| thread1.start(); |
| |
| // The TCP consumer |
| thread2 = new Thread() { |
| public void run() { |
| while (true) { |
| try { |
| Socket socket = tcp.accept(); |
| System.out.println("-----------------------------------------------"); |
| System.out.println(">>>>> TCP connection established"); |
| DataInputStream in = new DataInputStream(socket.getInputStream()); |
| DataOutputStream out = new DataOutputStream(socket.getOutputStream()); |
| byte[] token = new byte[in.readInt()]; |
| in.readFully(token); |
| q.put(new Job(processMessage(token), socket, out)); |
| } catch (Exception e) { |
| e.printStackTrace(); |
| } |
| } |
| } |
| }; |
| thread2.setDaemon(asDaemon); |
| thread2.start(); |
| |
| // The dispatcher |
| thread3 = new Thread() { |
| public void run() { |
| while (true) { |
| try { |
| q.take().send(); |
| } catch (Exception e) { |
| } |
| } |
| } |
| }; |
| thread3.setDaemon(true); |
| thread3.start(); |
| } |
| |
| public void terminate() { |
| try { |
| thread1.stop(); |
| thread2.stop(); |
| thread3.stop(); |
| u1.close(); |
| t1.close(); |
| } catch (Exception e) { |
| // OK |
| } |
| } |
| /** |
| * Helper class to encapsulate a job in a KDC. |
| */ |
| private static class Job { |
| byte[] token; // The received request at creation time and |
| // the response at send time |
| Socket s; // The TCP socket from where the request comes |
| DataOutputStream out; // The OutputStream of the TCP socket |
| DatagramSocket s2; // The UDP socket from where the request comes |
| DatagramPacket dp; // The incoming UDP datagram packet |
| boolean useTCP; // Whether TCP or UDP is used |
| |
| // Creates a job object for TCP |
| Job(byte[] token, Socket s, DataOutputStream out) { |
| useTCP = true; |
| this.token = token; |
| this.s = s; |
| this.out = out; |
| } |
| |
| // Creates a job object for UDP |
| Job(byte[] token, DatagramSocket s2, DatagramPacket dp) { |
| useTCP = false; |
| this.token = token; |
| this.s2 = s2; |
| this.dp = dp; |
| } |
| |
| // Sends the output back to the client |
| void send() { |
| try { |
| if (useTCP) { |
| System.out.println(">>>>> TCP request honored"); |
| out.writeInt(token.length); |
| out.write(token); |
| s.close(); |
| } else { |
| System.out.println(">>>>> UDP request honored"); |
| s2.send(new DatagramPacket(token, token.length, dp.getAddress(), dp.getPort())); |
| } |
| } catch (Exception e) { |
| e.printStackTrace(); |
| } |
| } |
| } |
| |
| public static class KDCNameService implements NameServiceDescriptor { |
| @Override |
| public NameService createNameService() throws Exception { |
| NameService ns = new NameService() { |
| @Override |
| public InetAddress[] lookupAllHostAddr(String host) |
| throws UnknownHostException { |
| // Everything is localhost |
| return new InetAddress[]{ |
| InetAddress.getByAddress(host, new byte[]{127,0,0,1}) |
| }; |
| } |
| @Override |
| public String getHostByAddr(byte[] addr) |
| throws UnknownHostException { |
| // No reverse lookup, PrincipalName use original string |
| throw new UnknownHostException(); |
| } |
| }; |
| return ns; |
| } |
| |
| @Override |
| public String getProviderName() { |
| return "mock"; |
| } |
| |
| @Override |
| public String getType() { |
| return "ns"; |
| } |
| } |
| |
| // Calling private methods thru reflections |
| private static final Field getPADataField; |
| private static final Field getEType; |
| private static final Constructor<EncryptedData> ctorEncryptedData; |
| private static final Method stringToKey; |
| |
| static { |
| try { |
| ctorEncryptedData = EncryptedData.class.getDeclaredConstructor(DerValue.class); |
| ctorEncryptedData.setAccessible(true); |
| getPADataField = KDCReq.class.getDeclaredField("pAData"); |
| getPADataField.setAccessible(true); |
| getEType = KDCReqBody.class.getDeclaredField("eType"); |
| getEType.setAccessible(true); |
| stringToKey = EncryptionKey.class.getDeclaredMethod( |
| "stringToKey", |
| char[].class, String.class, byte[].class, Integer.TYPE); |
| stringToKey.setAccessible(true); |
| } catch (NoSuchFieldException nsfe) { |
| throw new AssertionError(nsfe); |
| } catch (NoSuchMethodException nsme) { |
| throw new AssertionError(nsme); |
| } |
| } |
| private EncryptedData newEncryptedData(DerValue der) { |
| try { |
| return ctorEncryptedData.newInstance(der); |
| } catch (Exception e) { |
| throw new AssertionError(e); |
| } |
| } |
| private static PAData[] kDCReqDotPAData(KDCReq req) { |
| try { |
| return (PAData[])getPADataField.get(req); |
| } catch (Exception e) { |
| throw new AssertionError(e); |
| } |
| } |
| private static int[] KDCReqBodyDotEType(KDCReqBody body) { |
| try { |
| return (int[]) getEType.get(body); |
| } catch (Exception e) { |
| throw new AssertionError(e); |
| } |
| } |
| private static byte[] EncryptionKeyDotStringToKey(char[] password, String salt, |
| byte[] s2kparams, int keyType) throws KrbCryptoException { |
| try { |
| return (byte[])stringToKey.invoke( |
| null, password, salt, s2kparams, keyType); |
| } catch (InvocationTargetException ex) { |
| throw (KrbCryptoException)ex.getCause(); |
| } catch (Exception e) { |
| throw new AssertionError(e); |
| } |
| } |
| } |