1 /*
   2  * Copyright (c) 2010, 2019, 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, ReferralsState referralsState)
 266             throws KrbException, IOException {
 267         PAData[] extraPAs = null;
 268         int[] eTypes;
 269         if (password != null) {
 270             eTypes = EType.getDefaults("default_tkt_enctypes");
 271         } else {
 272             EncryptionKey[] ks = Krb5Util.keysFromJavaxKeyTab(ktab, cname);
 273             eTypes = EType.getDefaults("default_tkt_enctypes",
 274                     ks);
 275             for (EncryptionKey k: ks) k.destroy();
 276         }
 277         options = (options == null) ? new KDCOptions() : options;
 278         if (referralsState.isEnabled()) {
 279             options.set(KDCOptions.CANONICALIZE, true);
 280             extraPAs = new PAData[]{ new PAData(Krb5.PA_REQ_ENC_PA_REP,
 281                     new byte[]{}) };
 282         } else {
 283             options.set(KDCOptions.CANONICALIZE, false);
 284         }
 285         return new KrbAsReq(key,
 286             options,
 287             cname,
 288             sname,
 289             from,
 290             till,
 291             rtime,
 292             eTypes,
 293             addresses,
 294             extraPAs);
 295     }
 296 
 297     /**
 298      * Parses AS-REP, decrypts enc-part, retrieves ticket and session key
 299      * @throws KrbException
 300      * @throws Asn1Exception
 301      * @throws IOException
 302      */
 303     private KrbAsReqBuilder resolve()
 304             throws KrbException, Asn1Exception, IOException {
 305         if (ktab != null) {
 306             rep.decryptUsingKeyTab(ktab, req, cname);
 307         } else {
 308             rep.decryptUsingPassword(password, req, cname);
 309         }
 310         if (rep.getPA() != null) {
 311             if (paList == null || paList.length == 0) {
 312                 paList = rep.getPA();
 313             } else {
 314                 int extraLen = rep.getPA().length;
 315                 if (extraLen > 0) {
 316                     int oldLen = paList.length;
 317                     paList = Arrays.copyOf(paList, paList.length + extraLen);
 318                     System.arraycopy(rep.getPA(), 0, paList, oldLen, extraLen);
 319                 }
 320             }
 321         }
 322         return this;
 323     }
 324 
 325     /**
 326      * Communication until AS-REP or non preauth-related KRB-ERROR received
 327      * @throws KrbException
 328      * @throws IOException
 329      */
 330     private KrbAsReqBuilder send() throws KrbException, IOException {
 331         boolean preAuthFailedOnce = false;
 332         KdcComm comm = null;
 333         EncryptionKey pakey = null;
 334         ReferralsState referralsState = new ReferralsState();
 335         while (true) {
 336             if (referralsState.refreshComm()) {
 337                 comm = new KdcComm(cname.getRealmAsString());
 338             }
 339             try {
 340                 req = build(pakey, referralsState);
 341                 rep = new KrbAsRep(comm.send(req.encoding()));
 342                 return this;
 343             } catch (KrbException ke) {
 344                 if (!preAuthFailedOnce && (
 345                         ke.returnCode() == Krb5.KDC_ERR_PREAUTH_FAILED ||
 346                         ke.returnCode() == Krb5.KDC_ERR_PREAUTH_REQUIRED)) {
 347                     if (Krb5.DEBUG) {
 348                         System.out.println("KrbAsReqBuilder: " +
 349                                 "PREAUTH FAILED/REQ, re-send AS-REQ");
 350                     }
 351                     preAuthFailedOnce = true;
 352                     KRBError kerr = ke.getError();
 353                     int paEType = PAData.getPreferredEType(kerr.getPA(),
 354                             EType.getDefaults("default_tkt_enctypes")[0]);
 355                     if (password == null) {
 356                         EncryptionKey[] ks = Krb5Util.keysFromJavaxKeyTab(ktab, cname);
 357                         pakey = EncryptionKey.findKey(paEType, ks);
 358                         if (pakey != null) pakey = (EncryptionKey)pakey.clone();
 359                         for (EncryptionKey k: ks) k.destroy();
 360                     } else {
 361                         pakey = EncryptionKey.acquireSecretKey(cname,
 362                                 password,
 363                                 paEType,
 364                                 PAData.getSaltAndParams(
 365                                     paEType, kerr.getPA()));
 366                     }
 367                     paList = kerr.getPA();  // Update current paList
 368                 } else {
 369                     if (referralsState.handleError(ke)) {
 370                         continue;
 371                     }
 372                     throw ke;
 373                 }
 374             }
 375         }
 376     }
 377 
 378     private final class ReferralsState {
 379         private boolean enabled;
 380         private int count;
 381         private boolean refreshComm;
 382 
 383         ReferralsState() throws KrbException {
 384             if (Config.DISABLE_REFERRALS) {
 385                 if (cname.getNameType() == PrincipalName.KRB_NT_ENTERPRISE) {
 386                     throw new KrbException("NT-ENTERPRISE principals only allowed" +
 387                             " when referrals are enabled.");
 388                 }
 389                 enabled = false;
 390             } else {
 391                 enabled = true;
 392             }
 393             refreshComm = true;
 394         }
 395 
 396         boolean handleError(KrbException ke) throws RealmException {
 397             if (enabled) {
 398                 if (ke.returnCode() == Krb5.KRB_ERR_WRONG_REALM) {
 399                     Realm referredRealm = ke.getError().getClientRealm();
 400                     if (req.getMessage().reqBody.kdcOptions.get(KDCOptions.CANONICALIZE) &&
 401                             referredRealm != null && referredRealm.toString().length() > 0 &&
 402                             count < Config.MAX_REFERRALS) {
 403                         cname = new PrincipalName(cname.getNameType(),
 404                                 cname.getNameStrings(), referredRealm);
 405                         refreshComm = true;
 406                         count++;
 407                         return true;
 408                     }
 409                 }
 410                 if (count < Config.MAX_REFERRALS &&
 411                         cname.getNameType() != PrincipalName.KRB_NT_ENTERPRISE) {
 412                     // Server may raise an error if CANONICALIZE is true.
 413                     // Try CANONICALIZE false.
 414                     enabled = false;
 415                     return true;
 416                 }
 417             }
 418             return false;
 419         }
 420 
 421         boolean refreshComm() {
 422             boolean retRefreshComm = refreshComm;
 423             refreshComm = false;
 424             return retRefreshComm;
 425         }
 426 
 427         boolean isEnabled() {
 428             return enabled;
 429         }
 430     }
 431 
 432     /**
 433      * Performs AS-REQ send and AS-REP receive.
 434      * Maybe a state is needed here, to divide prepare process and getCreds.
 435      * @throws KrbException
 436      * @throws Asn1Exception
 437      * @throws IOException
 438      */
 439     public KrbAsReqBuilder action()
 440             throws KrbException, Asn1Exception, IOException {
 441         checkState(State.INIT, "Cannot call action");
 442         state = State.REQ_OK;
 443         return send().resolve();
 444     }
 445 
 446     /**
 447      * Gets Credentials object after action
 448      */
 449     public Credentials getCreds() {
 450         checkState(State.REQ_OK, "Cannot retrieve creds");
 451         return rep.getCreds();
 452     }
 453 
 454     /**
 455      * Gets another type of Credentials after action
 456      */
 457     public sun.security.krb5.internal.ccache.Credentials getCCreds() {
 458         checkState(State.REQ_OK, "Cannot retrieve CCreds");
 459         return rep.getCCreds();
 460     }
 461 
 462     /**
 463      * Destroys the object and clears keys and password info.
 464      */
 465     public void destroy() {
 466         state = State.DESTROYED;
 467         if (password != null) {
 468             Arrays.fill(password, (char)0);
 469         }
 470     }
 471 
 472     /**
 473      * Checks if the current state is the specified one.
 474      * @param st the expected state
 475      * @param msg error message if state is not correct
 476      * @throws IllegalStateException if state is not correct
 477      */
 478     private void checkState(State st, String msg) {
 479         if (state != st) {
 480             throw new IllegalStateException(msg + " at " + st + " state");
 481         }
 482     }
 483 }