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 refCname; // May be changed by referrals
72     private PrincipalName sname;
73     private KerberosTime from;
74     private KerberosTime till;
75     private KerberosTime rtime;
76     private HostAddresses addresses;
77 
78     // Secret source: can't be changed once assigned, only one (of the two
79     // sources) can be set to non-null
80     private final char[] password;
81     private final KeyTab ktab;
82 
83     // Used to create a ENC-TIMESTAMP in the 2nd AS-REQ
84     private PAData[] paList;        // PA-DATA from both KRB-ERROR and AS-REP.
85                                     // Used by getKeys() only.
86                                     // Only AS-REP should be enough per RFC,
87                                     // combined in case etypes are different.
88 
89     // The generated and received:
90     private KrbAsReq req;
91     private KrbAsRep rep;
92 
93     private static enum State {
94         INIT,       // Initialized, can still add more initialization info
95         REQ_OK,     // AS-REQ performed
96         DESTROYED,  // Destroyed, not usable anymore
97     }
98     private State state;
99 
100     // Called by other constructors
init(PrincipalName cname)101     private void init(PrincipalName cname)
102             throws KrbException {
103         this.cname = cname;
104         this.refCname = cname;
105         state = State.INIT;
106     }
107 
108     /**
109      * Creates a builder to be used by {@code cname} with existing keys.
110      *
111      * @param cname the client of the AS-REQ. Must not be null. Might have no
112      * realm, where default realm will be used. This realm will be the target
113      * realm for AS-REQ. I believe a client should only get initial TGT from
114      * its own realm.
115      * @param ktab must not be null. If empty, might be quite useless.
116      * This argument will neither be modified nor stored by the method.
117      * @throws KrbException
118      */
KrbAsReqBuilder(PrincipalName cname, KeyTab ktab)119     public KrbAsReqBuilder(PrincipalName cname, KeyTab ktab)
120             throws KrbException {
121         init(cname);
122         this.ktab = ktab;
123         this.password = null;
124     }
125 
126     /**
127      * Creates a builder to be used by {@code cname} with a known password.
128      *
129      * @param cname the client of the AS-REQ. Must not be null. Might have no
130      * realm, where default realm will be used. This realm will be the target
131      * realm for AS-REQ. I believe a client should only get initial TGT from
132      * its own realm.
133      * @param pass must not be null. This argument will neither be modified
134      * nor stored by the method.
135      * @throws KrbException
136      */
KrbAsReqBuilder(PrincipalName cname, char[] pass)137     public KrbAsReqBuilder(PrincipalName cname, char[] pass)
138             throws KrbException {
139         init(cname);
140         this.password = pass.clone();
141         this.ktab = null;
142     }
143 
144     /**
145      * Retrieves an array of secret keys for the client. This is used when
146      * the client supplies password but need keys to act as an acceptor. For
147      * an initiator, it must be called after AS-REQ is performed (state is OK).
148      * For an acceptor, it can be called when this KrbAsReqBuilder object is
149      * constructed (state is INIT).
150      * @param isInitiator if the caller is an initiator
151      * @return generated keys from password. PA-DATA from server might be used.
152      * All "default_tkt_enctypes" keys will be generated, Never null.
153      * @throws IllegalStateException if not constructed from a password
154      * @throws KrbException
155      */
getKeys(boolean isInitiator)156     public EncryptionKey[] getKeys(boolean isInitiator) throws KrbException {
157         checkState(isInitiator?State.REQ_OK:State.INIT, "Cannot get keys");
158         if (password != null) {
159             int[] eTypes = EType.getDefaults("default_tkt_enctypes");
160             EncryptionKey[] result = new EncryptionKey[eTypes.length];
161 
162             /*
163              * Returns an array of keys. Before KrbAsReqBuilder, all etypes
164              * use the same salt which is either the default one or a new salt
165              * coming from PA-DATA. After KrbAsReqBuilder, each etype uses its
166              * own new salt from PA-DATA. For an etype with no PA-DATA new salt
167              * at all, what salt should it use?
168              *
169              * Commonly, the stored keys are only to be used by an acceptor to
170              * decrypt service ticket in AP-REQ. Most impls only allow keys
171              * from a keytab on acceptor, but unfortunately (?) Java supports
172              * acceptor using password. In this case, if the service ticket is
173              * encrypted using an etype which we don't have PA-DATA new salt,
174              * using the default salt might be wrong (say, case-insensitive
175              * user name). Instead, we would use the new salt of another etype.
176              */
177 
178             String salt = null;     // the saved new salt
179             try {
180                 for (int i=0; i<eTypes.length; i++) {
181                     // First round, only calculate those have a PA entry
182                     PAData.SaltAndParams snp =
183                             PAData.getSaltAndParams(eTypes[i], paList);
184                     if (snp != null) {
185                         // Never uses a salt for rc4-hmac, it does not use
186                         // a salt at all
187                         if (eTypes[i] != EncryptedData.ETYPE_ARCFOUR_HMAC &&
188                                 snp.salt != null) {
189                             salt = snp.salt;
190                         }
191                         result[i] = EncryptionKey.acquireSecretKey(cname,
192                                 password,
193                                 eTypes[i],
194                                 snp);
195                     }
196                 }
197                 // No new salt from PA, maybe empty, maybe only rc4-hmac
198                 if (salt == null) salt = cname.getSalt();
199                 for (int i=0; i<eTypes.length; i++) {
200                     // Second round, calculate those with no PA entry
201                     if (result[i] == null) {
202                         result[i] = EncryptionKey.acquireSecretKey(password,
203                                 salt,
204                                 eTypes[i],
205                                 null);
206                     }
207                 }
208             } catch (IOException ioe) {
209                 KrbException ke = new KrbException(Krb5.ASN1_PARSE_ERROR);
210                 ke.initCause(ioe);
211                 throw ke;
212             }
213             return result;
214         } else {
215             throw new IllegalStateException("Required password not provided");
216         }
217     }
218 
219     /**
220      * Sets or clears options. If cleared, default options will be used
221      * at creation time.
222      * @param options
223      */
setOptions(KDCOptions options)224     public void setOptions(KDCOptions options) {
225         checkState(State.INIT, "Cannot specify options");
226         this.options = options;
227     }
228 
setTill(KerberosTime till)229     public void setTill(KerberosTime till) {
230         checkState(State.INIT, "Cannot specify till");
231         this.till = till;
232     }
233 
setRTime(KerberosTime rtime)234     public void setRTime(KerberosTime rtime) {
235         checkState(State.INIT, "Cannot specify rtime");
236         this.rtime = rtime;
237     }
238 
239     /**
240      * Sets or clears target. If cleared, KrbAsReq might choose krbtgt
241      * for cname realm
242      * @param sname
243      */
setTarget(PrincipalName sname)244     public void setTarget(PrincipalName sname) {
245         checkState(State.INIT, "Cannot specify target");
246         this.sname = sname;
247     }
248 
249     /**
250      * Adds or clears addresses. KrbAsReq might add some if empty
251      * field not allowed
252      * @param addresses
253      */
setAddresses(HostAddresses addresses)254     public void setAddresses(HostAddresses addresses) {
255         checkState(State.INIT, "Cannot specify addresses");
256         this.addresses = addresses;
257     }
258 
259     /**
260      * Build a KrbAsReq object from all info fed above. Normally this method
261      * will be called twice: initial AS-REQ and second with pakey
262      * @param key null (initial AS-REQ) or pakey (with preauth)
263      * @return the KrbAsReq object
264      * @throws KrbException
265      * @throws IOException
266      */
build(EncryptionKey key, ReferralsState referralsState)267     private KrbAsReq build(EncryptionKey key, ReferralsState referralsState)
268             throws KrbException, IOException {
269         PAData[] extraPAs = null;
270         int[] eTypes;
271         if (password != null) {
272             eTypes = EType.getDefaults("default_tkt_enctypes");
273         } else {
274             EncryptionKey[] ks = Krb5Util.keysFromJavaxKeyTab(ktab, cname);
275             eTypes = EType.getDefaults("default_tkt_enctypes",
276                     ks);
277             for (EncryptionKey k: ks) k.destroy();
278         }
279         options = (options == null) ? new KDCOptions() : options;
280         if (referralsState.isEnabled()) {
281             options.set(KDCOptions.CANONICALIZE, true);
282             extraPAs = new PAData[]{ new PAData(Krb5.PA_REQ_ENC_PA_REP,
283                     new byte[]{}) };
284         } else {
285             options.set(KDCOptions.CANONICALIZE, false);
286         }
287         return new KrbAsReq(key,
288             options,
289             refCname,
290             sname,
291             from,
292             till,
293             rtime,
294             eTypes,
295             addresses,
296             extraPAs);
297     }
298 
299     /**
300      * Parses AS-REP, decrypts enc-part, retrieves ticket and session key
301      * @throws KrbException
302      * @throws Asn1Exception
303      * @throws IOException
304      */
resolve()305     private KrbAsReqBuilder resolve()
306             throws KrbException, Asn1Exception, IOException {
307         if (ktab != null) {
308             rep.decryptUsingKeyTab(ktab, req, cname);
309         } else {
310             rep.decryptUsingPassword(password, req, cname);
311         }
312         if (rep.getPA() != null) {
313             if (paList == null || paList.length == 0) {
314                 paList = rep.getPA();
315             } else {
316                 int extraLen = rep.getPA().length;
317                 if (extraLen > 0) {
318                     int oldLen = paList.length;
319                     paList = Arrays.copyOf(paList, paList.length + extraLen);
320                     System.arraycopy(rep.getPA(), 0, paList, oldLen, extraLen);
321                 }
322             }
323         }
324         return this;
325     }
326 
327     /**
328      * Communication until AS-REP or non preauth-related KRB-ERROR received
329      * @throws KrbException
330      * @throws IOException
331      */
send()332     private KrbAsReqBuilder send() throws KrbException, IOException {
333         boolean preAuthFailedOnce = false;
334         KdcComm comm = null;
335         EncryptionKey pakey = null;
336         ReferralsState referralsState = new ReferralsState();
337         while (true) {
338             if (referralsState.refreshComm()) {
339                 comm = new KdcComm(refCname.getRealmAsString());
340             }
341             try {
342                 req = build(pakey, referralsState);
343                 rep = new KrbAsRep(comm.send(req.encoding()));
344                 return this;
345             } catch (KrbException ke) {
346                 if (!preAuthFailedOnce && (
347                         ke.returnCode() == Krb5.KDC_ERR_PREAUTH_FAILED ||
348                         ke.returnCode() == Krb5.KDC_ERR_PREAUTH_REQUIRED)) {
349                     if (Krb5.DEBUG) {
350                         System.out.println("KrbAsReqBuilder: " +
351                                 "PREAUTH FAILED/REQ, re-send AS-REQ");
352                     }
353                     preAuthFailedOnce = true;
354                     KRBError kerr = ke.getError();
355                     int paEType = PAData.getPreferredEType(kerr.getPA(),
356                             EType.getDefaults("default_tkt_enctypes")[0]);
357                     if (password == null) {
358                         EncryptionKey[] ks = Krb5Util.keysFromJavaxKeyTab(ktab, cname);
359                         pakey = EncryptionKey.findKey(paEType, ks);
360                         if (pakey != null) pakey = (EncryptionKey)pakey.clone();
361                         for (EncryptionKey k: ks) k.destroy();
362                     } else {
363                         pakey = EncryptionKey.acquireSecretKey(cname,
364                                 password,
365                                 paEType,
366                                 PAData.getSaltAndParams(
367                                     paEType, kerr.getPA()));
368                     }
369                     paList = kerr.getPA();  // Update current paList
370                 } else {
371                     if (referralsState.handleError(ke)) {
372                         pakey = null;
373                         preAuthFailedOnce = false;
374                         continue;
375                     }
376                     throw ke;
377                 }
378             }
379         }
380     }
381 
382     private final class ReferralsState {
383         private boolean enabled;
384         private int count;
385         private boolean refreshComm;
386 
ReferralsState()387         ReferralsState() throws KrbException {
388             if (Config.DISABLE_REFERRALS) {
389                 if (refCname.getNameType() == PrincipalName.KRB_NT_ENTERPRISE) {
390                     throw new KrbException("NT-ENTERPRISE principals only allowed" +
391                             " when referrals are enabled.");
392                 }
393                 enabled = false;
394             } else {
395                 enabled = true;
396             }
397             refreshComm = true;
398         }
399 
handleError(KrbException ke)400         boolean handleError(KrbException ke) throws RealmException {
401             if (enabled) {
402                 if (ke.returnCode() == Krb5.KRB_ERR_WRONG_REALM) {
403                     Realm referredRealm = ke.getError().getClientRealm();
404                     if (req.getMessage().reqBody.kdcOptions.get(KDCOptions.CANONICALIZE) &&
405                             referredRealm != null && referredRealm.toString().length() > 0 &&
406                             count < Config.MAX_REFERRALS) {
407                         refCname = new PrincipalName(refCname.getNameType(),
408                                 refCname.getNameStrings(), referredRealm);
409                         refreshComm = true;
410                         count++;
411                         return true;
412                     }
413                 }
414                 if (count < Config.MAX_REFERRALS &&
415                         refCname.getNameType() != PrincipalName.KRB_NT_ENTERPRISE) {
416                     // Server may raise an error if CANONICALIZE is true.
417                     // Try CANONICALIZE false.
418                     enabled = false;
419                     return true;
420                 }
421             }
422             return false;
423         }
424 
refreshComm()425         boolean refreshComm() {
426             boolean retRefreshComm = refreshComm;
427             refreshComm = false;
428             return retRefreshComm;
429         }
430 
isEnabled()431         boolean isEnabled() {
432             return enabled;
433         }
434     }
435 
436     /**
437      * Performs AS-REQ send and AS-REP receive.
438      * Maybe a state is needed here, to divide prepare process and getCreds.
439      * @throws KrbException
440      * @throws Asn1Exception
441      * @throws IOException
442      */
action()443     public KrbAsReqBuilder action()
444             throws KrbException, Asn1Exception, IOException {
445         checkState(State.INIT, "Cannot call action");
446         state = State.REQ_OK;
447         return send().resolve();
448     }
449 
450     /**
451      * Gets Credentials object after action
452      */
getCreds()453     public Credentials getCreds() {
454         checkState(State.REQ_OK, "Cannot retrieve creds");
455         return rep.getCreds();
456     }
457 
458     /**
459      * Gets another type of Credentials after action
460      */
getCCreds()461     public sun.security.krb5.internal.ccache.Credentials getCCreds() {
462         checkState(State.REQ_OK, "Cannot retrieve CCreds");
463         return rep.getCCreds();
464     }
465 
466     /**
467      * Destroys the object and clears keys and password info.
468      */
destroy()469     public void destroy() {
470         state = State.DESTROYED;
471         if (password != null) {
472             Arrays.fill(password, (char)0);
473         }
474     }
475 
476     /**
477      * Checks if the current state is the specified one.
478      * @param st the expected state
479      * @param msg error message if state is not correct
480      * @throws IllegalStateException if state is not correct
481      */
checkState(State st, String msg)482     private void checkState(State st, String msg) {
483         if (state != st) {
484             throw new IllegalStateException(msg + " at " + st + " state");
485         }
486     }
487 }
488