1 /*
   2  * Copyright (c) 2010, 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 package sun.security.krb5;
  27 
  28 import java.io.IOException;
  29 import java.util.Arrays;
  30 import javax.security.auth.kerberos.KeyTab;
  31 import sun.security.jgss.krb5.Krb5Util;
  32 import sun.security.krb5.internal.HostAddresses;
  33 import sun.security.krb5.internal.KDCOptions;
  34 import sun.security.krb5.internal.KRBError;
  35 import sun.security.krb5.internal.KerberosTime;
  36 import sun.security.krb5.internal.Krb5;
  37 import sun.security.krb5.internal.PAData;
  38 import sun.security.krb5.internal.crypto.EType;
  39 
  40 /**
  41  * A manager class for AS-REQ communications.
  42  *
  43  * This class does:
  44  * 1. Gather information to create AS-REQ
  45  * 2. Create and send AS-REQ
  46  * 3. Receive AS-REP and KRB-ERROR (-KRB_ERR_RESPONSE_TOO_BIG) and parse them
  47  * 4. Emit credentials and secret keys (for JAAS storeKey=true with password)
  48  *
  49  * This class does not:
  50  * 1. Deal with real communications (KdcComm does it, and TGS-REQ)
  51  *    a. Name of KDCs for a realm
  52  *    b. Server availability, timeout, UDP or TCP
  53  *    d. KRB_ERR_RESPONSE_TOO_BIG
  54  * 2. Stores its own copy of password, this means:
  55  *    a. Do not change/wipe it before Builder finish
  56  *    b. Builder will not wipe it for you
  57  *
  58  * With this class:
  59  * 1. KrbAsReq has only one constructor
  60  * 2. Krb5LoginModule and Kinit call a single builder
  61  * 3. Better handling of sensitive info
  62  *
  63  * @since 1.7
  64  */
  65 
  66 public final class KrbAsReqBuilder {
  67 
  68     // Common data for AS-REQ fields
  69     private KDCOptions options;
  70     private PrincipalName cname;
  71     private PrincipalName sname;
  72     private KerberosTime from;
  73     private KerberosTime till;
  74     private KerberosTime rtime;
  75     private HostAddresses addresses;
  76 
  77     // Secret source: can't be changed once assigned, only one (of the two
  78     // sources) can be set to non-null
  79     private final char[] password;
  80     private final KeyTab ktab;
  81 
  82     // Used to create a ENC-TIMESTAMP in the 2nd AS-REQ
  83     private PAData[] paList;        // PA-DATA from both KRB-ERROR and AS-REP.
  84                                     // Used by getKeys() only.
  85                                     // Only AS-REP should be enough per RFC,
  86                                     // combined in case etypes are different.
  87 
  88     // The generated and received:
  89     private KrbAsReq req;
  90     private KrbAsRep rep;
  91 
  92     private static enum State {
  93         INIT,       // Initialized, can still add more initialization info
  94         REQ_OK,     // AS-REQ performed
  95         DESTROYED,  // Destroyed, not usable anymore
  96     }
  97     private State state;
  98 
  99     // Called by other constructors
 100     private void init(PrincipalName cname)
 101             throws KrbException {
 102         this.cname = cname;
 103         state = State.INIT;
 104     }
 105 
 106     /**
 107      * Creates a builder to be used by {@code cname} with existing keys.
 108      *
 109      * @param cname the client of the AS-REQ. Must not be null. Might have no
 110      * realm, where default realm will be used. This realm will be the target
 111      * realm for AS-REQ. I believe a client should only get initial TGT from
 112      * its own realm.
 113      * @param ktab must not be null. If empty, might be quite useless.
 114      * This argument will neither be modified nor stored by the method.
 115      * @throws KrbException
 116      */
 117     public KrbAsReqBuilder(PrincipalName cname, KeyTab ktab)
 118             throws KrbException {
 119         init(cname);
 120         this.ktab = ktab;
 121         this.password = null;
 122     }
 123 
 124     /**
 125      * Creates a builder to be used by {@code cname} with a known password.
 126      *
 127      * @param cname the client of the AS-REQ. Must not be null. Might have no
 128      * realm, where default realm will be used. This realm will be the target
 129      * realm for AS-REQ. I believe a client should only get initial TGT from
 130      * its own realm.
 131      * @param pass must not be null. This argument will neither be modified
 132      * nor stored by the method.
 133      * @throws KrbException
 134      */
 135     public KrbAsReqBuilder(PrincipalName cname, char[] pass)
 136             throws KrbException {
 137         init(cname);
 138         this.password = pass.clone();
 139         this.ktab = null;
 140     }
 141 
 142     /**
 143      * Retrieves an array of secret keys for the client. This is used when
 144      * the client supplies password but need keys to act as an acceptor. For
 145      * an initiator, it must be called after AS-REQ is performed (state is OK).
 146      * For an acceptor, it can be called when this KrbAsReqBuilder object is
 147      * constructed (state is INIT).
 148      * @param isInitiator if the caller is an initiator
 149      * @return generated keys from password. PA-DATA from server might be used.
 150      * All "default_tkt_enctypes" keys will be generated, Never null.
 151      * @throws IllegalStateException if not constructed from a password
 152      * @throws KrbException
 153      */
 154     public EncryptionKey[] getKeys(boolean isInitiator) throws KrbException {
 155         checkState(isInitiator?State.REQ_OK:State.INIT, "Cannot get keys");
 156         if (password != null) {
 157             int[] eTypes = EType.getDefaults("default_tkt_enctypes");
 158             EncryptionKey[] result = new EncryptionKey[eTypes.length];
 159 
 160             /*
 161              * Returns an array of keys. Before KrbAsReqBuilder, all etypes
 162              * use the same salt which is either the default one or a new salt
 163              * coming from PA-DATA. After KrbAsReqBuilder, each etype uses its
 164              * own new salt from PA-DATA. For an etype with no PA-DATA new salt
 165              * at all, what salt should it use?
 166              *
 167              * Commonly, the stored keys are only to be used by an acceptor to
 168              * decrypt service ticket in AP-REQ. Most impls only allow keys
 169              * from a keytab on acceptor, but unfortunately (?) Java supports
 170              * acceptor using password. In this case, if the service ticket is
 171              * encrypted using an etype which we don't have PA-DATA new salt,
 172              * using the default salt might be wrong (say, case-insensitive
 173              * user name). Instead, we would use the new salt of another etype.
 174              */
 175 
 176             String salt = null;     // the saved new salt
 177             try {
 178                 for (int i=0; i<eTypes.length; i++) {
 179                     // First round, only calculate those have a PA entry
 180                     PAData.SaltAndParams snp =
 181                             PAData.getSaltAndParams(eTypes[i], paList);
 182                     if (snp != null) {
 183                         // Never uses a salt for rc4-hmac, it does not use
 184                         // a salt at all
 185                         if (eTypes[i] != EncryptedData.ETYPE_ARCFOUR_HMAC &&
 186                                 snp.salt != null) {
 187                             salt = snp.salt;
 188                         }
 189                         result[i] = EncryptionKey.acquireSecretKey(cname,
 190                                 password,
 191                                 eTypes[i],
 192                                 snp);
 193                     }
 194                 }
 195                 // No new salt from PA, maybe empty, maybe only rc4-hmac
 196                 if (salt == null) salt = cname.getSalt();
 197                 for (int i=0; i<eTypes.length; i++) {
 198                     // Second round, calculate those with no PA entry
 199                     if (result[i] == null) {
 200                         result[i] = EncryptionKey.acquireSecretKey(password,
 201                                 salt,
 202                                 eTypes[i],
 203                                 null);
 204                     }
 205                 }
 206             } catch (IOException ioe) {
 207                 KrbException ke = new KrbException(Krb5.ASN1_PARSE_ERROR);
 208                 ke.initCause(ioe);
 209                 throw ke;
 210             }
 211             return result;
 212         } else {
 213             throw new IllegalStateException("Required password not provided");
 214         }
 215     }
 216 
 217     /**
 218      * Sets or clears options. If cleared, default options will be used
 219      * at creation time.
 220      * @param options
 221      */
 222     public void setOptions(KDCOptions options) {
 223         checkState(State.INIT, "Cannot specify options");
 224         this.options = options;
 225     }
 226 
 227     public void setTill(KerberosTime till) {
 228         checkState(State.INIT, "Cannot specify till");
 229         this.till = till;
 230     }
 231 
 232     public void setRTime(KerberosTime rtime) {
 233         checkState(State.INIT, "Cannot specify rtime");
 234         this.rtime = rtime;
 235     }
 236 
 237     /**
 238      * Sets or clears target. If cleared, KrbAsReq might choose krbtgt
 239      * for cname realm
 240      * @param sname
 241      */
 242     public void setTarget(PrincipalName sname) {
 243         checkState(State.INIT, "Cannot specify target");
 244         this.sname = sname;
 245     }
 246 
 247     /**
 248      * Adds or clears addresses. KrbAsReq might add some if empty
 249      * field not allowed
 250      * @param addresses
 251      */
 252     public void setAddresses(HostAddresses addresses) {
 253         checkState(State.INIT, "Cannot specify addresses");
 254         this.addresses = addresses;
 255     }
 256 
 257     /**
 258      * Build a KrbAsReq object from all info fed above. Normally this method
 259      * will be called twice: initial AS-REQ and second with pakey
 260      * @param key null (initial AS-REQ) or pakey (with preauth)
 261      * @return the KrbAsReq object
 262      * @throws KrbException
 263      * @throws IOException
 264      */
 265     private KrbAsReq build(EncryptionKey key) throws KrbException, IOException {
 266         int[] eTypes;
 267         if (password != null) {
 268             eTypes = EType.getDefaults("default_tkt_enctypes");
 269         } else {
 270             EncryptionKey[] ks = Krb5Util.keysFromJavaxKeyTab(ktab, cname);
 271             eTypes = EType.getDefaults("default_tkt_enctypes",
 272                     ks);
 273             for (EncryptionKey k: ks) k.destroy();
 274         }
 275         return new KrbAsReq(key,
 276             options,
 277             cname,
 278             sname,
 279             from,
 280             till,
 281             rtime,
 282             eTypes,
 283             addresses);
 284     }
 285 
 286     /**
 287      * Parses AS-REP, decrypts enc-part, retrieves ticket and session key
 288      * @throws KrbException
 289      * @throws Asn1Exception
 290      * @throws IOException
 291      */
 292     private KrbAsReqBuilder resolve()
 293             throws KrbException, Asn1Exception, IOException {
 294         if (ktab != null) {
 295             rep.decryptUsingKeyTab(ktab, req, cname);
 296         } else {
 297             rep.decryptUsingPassword(password, req, cname);
 298         }
 299         if (rep.getPA() != null) {
 300             if (paList == null || paList.length == 0) {
 301                 paList = rep.getPA();
 302             } else {
 303                 int extraLen = rep.getPA().length;
 304                 if (extraLen > 0) {
 305                     int oldLen = paList.length;
 306                     paList = Arrays.copyOf(paList, paList.length + extraLen);
 307                     System.arraycopy(rep.getPA(), 0, paList, oldLen, extraLen);
 308                 }
 309             }
 310         }
 311         return this;
 312     }
 313 
 314     /**
 315      * Communication until AS-REP or non preauth-related KRB-ERROR received
 316      * @throws KrbException
 317      * @throws IOException
 318      */
 319     private KrbAsReqBuilder send() throws KrbException, IOException {
 320         boolean preAuthFailedOnce = false;
 321         KdcComm comm = new KdcComm(cname.getRealmAsString());
 322         EncryptionKey pakey = null;
 323         while (true) {
 324             try {
 325                 req = build(pakey);
 326                 rep = new KrbAsRep(comm.send(req.encoding()));
 327                 return this;
 328             } catch (KrbException ke) {
 329                 if (!preAuthFailedOnce && (
 330                         ke.returnCode() == Krb5.KDC_ERR_PREAUTH_FAILED ||
 331                         ke.returnCode() == Krb5.KDC_ERR_PREAUTH_REQUIRED)) {
 332                     if (Krb5.DEBUG) {
 333                         System.out.println("KrbAsReqBuilder: " +
 334                                 "PREAUTH FAILED/REQ, re-send AS-REQ");
 335                     }
 336                     preAuthFailedOnce = true;
 337                     KRBError kerr = ke.getError();
 338                     int paEType = PAData.getPreferredEType(kerr.getPA(),
 339                             EType.getDefaults("default_tkt_enctypes")[0]);
 340                     if (password == null) {
 341                         EncryptionKey[] ks = Krb5Util.keysFromJavaxKeyTab(ktab, cname);
 342                         pakey = EncryptionKey.findKey(paEType, ks);
 343                         if (pakey != null) pakey = (EncryptionKey)pakey.clone();
 344                         for (EncryptionKey k: ks) k.destroy();
 345                     } else {
 346                         pakey = EncryptionKey.acquireSecretKey(cname,
 347                                 password,
 348                                 paEType,
 349                                 PAData.getSaltAndParams(
 350                                     paEType, kerr.getPA()));
 351                     }
 352                     paList = kerr.getPA();  // Update current paList
 353                 } else {
 354                     throw ke;
 355                 }
 356             }
 357         }
 358     }
 359 
 360     /**
 361      * Performs AS-REQ send and AS-REP receive.
 362      * Maybe a state is needed here, to divide prepare process and getCreds.
 363      * @throws KrbException
 364      * @throws Asn1Exception
 365      * @throws IOException
 366      */
 367     public KrbAsReqBuilder action()
 368             throws KrbException, Asn1Exception, IOException {
 369         checkState(State.INIT, "Cannot call action");
 370         state = State.REQ_OK;
 371         return send().resolve();
 372     }
 373 
 374     /**
 375      * Gets Credentials object after action
 376      */
 377     public Credentials getCreds() {
 378         checkState(State.REQ_OK, "Cannot retrieve creds");
 379         return rep.getCreds();
 380     }
 381 
 382     /**
 383      * Gets another type of Credentials after action
 384      */
 385     public sun.security.krb5.internal.ccache.Credentials getCCreds() {
 386         checkState(State.REQ_OK, "Cannot retrieve CCreds");
 387         return rep.getCCreds();
 388     }
 389 
 390     /**
 391      * Destroys the object and clears keys and password info.
 392      */
 393     public void destroy() {
 394         state = State.DESTROYED;
 395         if (password != null) {
 396             Arrays.fill(password, (char)0);
 397         }
 398     }
 399 
 400     /**
 401      * Checks if the current state is the specified one.
 402      * @param st the expected state
 403      * @param msg error message if state is not correct
 404      * @throws IllegalStateException if state is not correct
 405      */
 406     private void checkState(State st, String msg) {
 407         if (state != st) {
 408             throw new IllegalStateException(msg + " at " + st + " state");
 409         }
 410     }
 411 }