aboutsummaryrefslogtreecommitdiff
path: root/src/share/classes/sun/security/krb5/KrbAsReqBuilder.java
blob: 3ca7b6d71ac8a09d167aef71c9eb6125c11305e3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
/*
 * Copyright (c) 2010, 2019, 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.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * 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.
 */

package sun.security.krb5;

import java.io.IOException;
import java.util.Arrays;
import javax.security.auth.kerberos.KeyTab;
import sun.security.jgss.krb5.Krb5Util;
import sun.security.krb5.internal.HostAddresses;
import sun.security.krb5.internal.KDCOptions;
import sun.security.krb5.internal.KRBError;
import sun.security.krb5.internal.KerberosTime;
import sun.security.krb5.internal.Krb5;
import sun.security.krb5.internal.PAData;
import sun.security.krb5.internal.crypto.EType;

/**
 * A manager class for AS-REQ communications.
 *
 * This class does:
 * 1. Gather information to create AS-REQ
 * 2. Create and send AS-REQ
 * 3. Receive AS-REP and KRB-ERROR (-KRB_ERR_RESPONSE_TOO_BIG) and parse them
 * 4. Emit credentials and secret keys (for JAAS storeKey=true with password)
 *
 * This class does not:
 * 1. Deal with real communications (KdcComm does it, and TGS-REQ)
 *    a. Name of KDCs for a realm
 *    b. Server availability, timeout, UDP or TCP
 *    d. KRB_ERR_RESPONSE_TOO_BIG
 * 2. Stores its own copy of password, this means:
 *    a. Do not change/wipe it before Builder finish
 *    b. Builder will not wipe it for you
 *
 * With this class:
 * 1. KrbAsReq has only one constructor
 * 2. Krb5LoginModule and Kinit call a single builder
 * 3. Better handling of sensitive info
 *
 * @since 1.7
 */

public final class KrbAsReqBuilder {

    // Common data for AS-REQ fields
    private KDCOptions options;
    private PrincipalName cname;
    private PrincipalName sname;
    private KerberosTime from;
    private KerberosTime till;
    private KerberosTime rtime;
    private HostAddresses addresses;

    // Secret source: can't be changed once assigned, only one (of the two
    // sources) can be set to non-null
    private final char[] password;
    private final KeyTab ktab;

    // Used to create a ENC-TIMESTAMP in the 2nd AS-REQ
    private PAData[] paList;        // PA-DATA from both KRB-ERROR and AS-REP.
                                    // Used by getKeys() only.
                                    // Only AS-REP should be enough per RFC,
                                    // combined in case etypes are different.

    // The generated and received:
    private KrbAsReq req;
    private KrbAsRep rep;

    private static enum State {
        INIT,       // Initialized, can still add more initialization info
        REQ_OK,     // AS-REQ performed
        DESTROYED,  // Destroyed, not usable anymore
    }
    private State state;

    // Called by other constructors
    private void init(PrincipalName cname)
            throws KrbException {
        this.cname = cname;
        state = State.INIT;
    }

    /**
     * Creates a builder to be used by {@code cname} with existing keys.
     *
     * @param cname the client of the AS-REQ. Must not be null. Might have no
     * realm, where default realm will be used. This realm will be the target
     * realm for AS-REQ. I believe a client should only get initial TGT from
     * its own realm.
     * @param keys must not be null. if empty, might be quite useless.
     * This argument will neither be modified nor stored by the method.
     * @throws KrbException
     */
    public KrbAsReqBuilder(PrincipalName cname, KeyTab ktab)
            throws KrbException {
        init(cname);
        this.ktab = ktab;
        this.password = null;
    }

    /**
     * Creates a builder to be used by {@code cname} with a known password.
     *
     * @param cname the client of the AS-REQ. Must not be null. Might have no
     * realm, where default realm will be used. This realm will be the target
     * realm for AS-REQ. I believe a client should only get initial TGT from
     * its own realm.
     * @param pass must not be null. This argument will neither be modified
     * nor stored by the method.
     * @throws KrbException
     */
    public KrbAsReqBuilder(PrincipalName cname, char[] pass)
            throws KrbException {
        init(cname);
        this.password = pass.clone();
        this.ktab = null;
    }

    /**
     * Retrieves an array of secret keys for the client. This is used when
     * the client supplies password but need keys to act as an acceptor. For
     * an initiator, it must be called after AS-REQ is performed (state is OK).
     * For an acceptor, it can be called when this KrbAsReqBuilder object is
     * constructed (state is INIT).
     * @param isInitiator if the caller is an initiator
     * @return generated keys from password. PA-DATA from server might be used.
     * All "default_tkt_enctypes" keys will be generated, Never null.
     * @throws IllegalStateException if not constructed from a password
     * @throws KrbException
     */
    public EncryptionKey[] getKeys(boolean isInitiator) throws KrbException {
        checkState(isInitiator?State.REQ_OK:State.INIT, "Cannot get keys");
        if (password != null) {
            int[] eTypes = EType.getDefaults("default_tkt_enctypes");
            EncryptionKey[] result = new EncryptionKey[eTypes.length];

            /*
             * Returns an array of keys. Before KrbAsReqBuilder, all etypes
             * use the same salt which is either the default one or a new salt
             * coming from PA-DATA. After KrbAsReqBuilder, each etype uses its
             * own new salt from PA-DATA. For an etype with no PA-DATA new salt
             * at all, what salt should it use?
             *
             * Commonly, the stored keys are only to be used by an acceptor to
             * decrypt service ticket in AP-REQ. Most impls only allow keys
             * from a keytab on acceptor, but unfortunately (?) Java supports
             * acceptor using password. In this case, if the service ticket is
             * encrypted using an etype which we don't have PA-DATA new salt,
             * using the default salt might be wrong (say, case-insensitive
             * user name). Instead, we would use the new salt of another etype.
             */

            String salt = null;     // the saved new salt
            try {
                for (int i=0; i<eTypes.length; i++) {
                    // First round, only calculate those have a PA entry
                    PAData.SaltAndParams snp =
                            PAData.getSaltAndParams(eTypes[i], paList);
                    if (snp != null) {
                        // Never uses a salt for rc4-hmac, it does not use
                        // a salt at all
                        if (eTypes[i] != EncryptedData.ETYPE_ARCFOUR_HMAC &&
                                snp.salt != null) {
                            salt = snp.salt;
                        }
                        result[i] = EncryptionKey.acquireSecretKey(cname,
                                password,
                                eTypes[i],
                                snp);
                    }
                }
                // No new salt from PA, maybe empty, maybe only rc4-hmac
                if (salt == null) salt = cname.getSalt();
                for (int i=0; i<eTypes.length; i++) {
                    // Second round, calculate those with no PA entry
                    if (result[i] == null) {
                        result[i] = EncryptionKey.acquireSecretKey(password,
                                salt,
                                eTypes[i],
                                null);
                    }
                }
            } catch (IOException ioe) {
                KrbException ke = new KrbException(Krb5.ASN1_PARSE_ERROR);
                ke.initCause(ioe);
                throw ke;
            }
            return result;
        } else {
            throw new IllegalStateException("Required password not provided");
        }
    }

    /**
     * Sets or clears options. If cleared, default options will be used
     * at creation time.
     * @param options
     */
    public void setOptions(KDCOptions options) {
        checkState(State.INIT, "Cannot specify options");
        this.options = options;
    }

    /**
     * Sets or clears target. If cleared, KrbAsReq might choose krbtgt
     * for cname realm
     * @param sname
     */
    public void setTarget(PrincipalName sname) {
        checkState(State.INIT, "Cannot specify target");
        this.sname = sname;
    }

    /**
     * Adds or clears addresses. KrbAsReq might add some if empty
     * field not allowed
     * @param addresses
     */
    public void setAddresses(HostAddresses addresses) {
        checkState(State.INIT, "Cannot specify addresses");
        this.addresses = addresses;
    }

    /**
     * Build a KrbAsReq object from all info fed above. Normally this method
     * will be called twice: initial AS-REQ and second with pakey
     * @param key null (initial AS-REQ) or pakey (with preauth)
     * @return the KrbAsReq object
     * @throws KrbException
     * @throws IOException
     */
    private KrbAsReq build(EncryptionKey key, ReferralsState referralsState)
            throws KrbException, IOException {
        PAData[] extraPAs = null;
        int[] eTypes;
        if (password != null) {
            eTypes = EType.getDefaults("default_tkt_enctypes");
        } else {
            EncryptionKey[] ks = Krb5Util.keysFromJavaxKeyTab(ktab, cname);
            eTypes = EType.getDefaults("default_tkt_enctypes",
                    ks);
            for (EncryptionKey k: ks) k.destroy();
        }
        options = (options == null) ? new KDCOptions() : options;
        if (referralsState.isEnabled()) {
            options.set(KDCOptions.CANONICALIZE, true);
            extraPAs = new PAData[]{ new PAData(Krb5.PA_REQ_ENC_PA_REP,
                    new byte[]{}) };
        } else {
            options.set(KDCOptions.CANONICALIZE, false);
        }
        return new KrbAsReq(key,
            options,
            cname,
            sname,
            from,
            till,
            rtime,
            eTypes,
            addresses,
            extraPAs);
    }

    /**
     * Parses AS-REP, decrypts enc-part, retrieves ticket and session key
     * @throws KrbException
     * @throws Asn1Exception
     * @throws IOException
     */
    private KrbAsReqBuilder resolve()
            throws KrbException, Asn1Exception, IOException {
        if (ktab != null) {
            rep.decryptUsingKeyTab(ktab, req, cname);
        } else {
            rep.decryptUsingPassword(password, req, cname);
        }
        if (rep.getPA() != null) {
            if (paList == null || paList.length == 0) {
                paList = rep.getPA();
            } else {
                int extraLen = rep.getPA().length;
                if (extraLen > 0) {
                    int oldLen = paList.length;
                    paList = Arrays.copyOf(paList, paList.length + extraLen);
                    System.arraycopy(rep.getPA(), 0, paList, oldLen, extraLen);
                }
            }
        }
        return this;
    }

    /**
     * Communication until AS-REP or non preauth-related KRB-ERROR received
     * @throws KrbException
     * @throws IOException
     */
    private KrbAsReqBuilder send() throws KrbException, IOException {
        boolean preAuthFailedOnce = false;
        KdcComm comm = null;
        EncryptionKey pakey = null;
        ReferralsState referralsState = new ReferralsState();
        while (true) {
            if (referralsState.refreshComm()) {
                comm = new KdcComm(cname.getRealmAsString());
            }
            try {
                req = build(pakey, referralsState);
                rep = new KrbAsRep(comm.send(req.encoding()));
                return this;
            } catch (KrbException ke) {
                if (!preAuthFailedOnce && (
                        ke.returnCode() == Krb5.KDC_ERR_PREAUTH_FAILED ||
                        ke.returnCode() == Krb5.KDC_ERR_PREAUTH_REQUIRED)) {
                    if (Krb5.DEBUG) {
                        System.out.println("KrbAsReqBuilder: " +
                                "PREAUTH FAILED/REQ, re-send AS-REQ");
                    }
                    preAuthFailedOnce = true;
                    KRBError kerr = ke.getError();
                    int paEType = PAData.getPreferredEType(kerr.getPA(),
                            EType.getDefaults("default_tkt_enctypes")[0]);
                    if (password == null) {
                        EncryptionKey[] ks = Krb5Util.keysFromJavaxKeyTab(ktab, cname);
                        pakey = EncryptionKey.findKey(paEType, ks);
                        if (pakey != null) pakey = (EncryptionKey)pakey.clone();
                        for (EncryptionKey k: ks) k.destroy();
                    } else {
                        pakey = EncryptionKey.acquireSecretKey(cname,
                                password,
                                paEType,
                                PAData.getSaltAndParams(
                                    paEType, kerr.getPA()));
                    }
                    paList = kerr.getPA();  // Update current paList
                } else {
                    if (referralsState.handleError(ke)) {
                        continue;
                    }
                    throw ke;
                }
            }
        }
    }

    private final class ReferralsState {
        private boolean enabled;
        private int count;
        private boolean refreshComm;

        ReferralsState() throws KrbException {
            if (Config.DISABLE_REFERRALS) {
                if (cname.getNameType() == PrincipalName.KRB_NT_ENTERPRISE) {
                    throw new KrbException("NT-ENTERPRISE principals only allowed" +
                            " when referrals are enabled.");
                }
                enabled = false;
            } else {
                enabled = true;
            }
            refreshComm = true;
        }

        boolean handleError(KrbException ke) throws RealmException {
            if (enabled) {
                if (ke.returnCode() == Krb5.KRB_ERR_WRONG_REALM) {
                    Realm referredRealm = ke.getError().getClientRealm();
                    if (req.getMessage().reqBody.kdcOptions.get(KDCOptions.CANONICALIZE) &&
                            referredRealm != null && referredRealm.toString().length() > 0 &&
                            count < Config.MAX_REFERRALS) {
                        cname = new PrincipalName(cname.getNameType(),
                                cname.getNameStrings(), referredRealm);
                        refreshComm = true;
                        count++;
                        return true;
                    }
                }
                if (count < Config.MAX_REFERRALS &&
                        cname.getNameType() != PrincipalName.KRB_NT_ENTERPRISE) {
                    // Server may raise an error if CANONICALIZE is true.
                    // Try CANONICALIZE false.
                    enabled = false;
                    return true;
                }
            }
            return false;
        }

        boolean refreshComm() {
            boolean retRefreshComm = refreshComm;
            refreshComm = false;
            return retRefreshComm;
        }

        boolean isEnabled() {
            return enabled;
        }
    }

    /**
     * Performs AS-REQ send and AS-REP receive.
     * Maybe a state is needed here, to divide prepare process and getCreds.
     * @throws KrbException
     * @throws Asn1Exception
     * @throws IOException
     */
    public KrbAsReqBuilder action()
            throws KrbException, Asn1Exception, IOException {
        checkState(State.INIT, "Cannot call action");
        state = State.REQ_OK;
        return send().resolve();
    }

    /**
     * Gets Credentials object after action
     */
    public Credentials getCreds() {
        checkState(State.REQ_OK, "Cannot retrieve creds");
        return rep.getCreds();
    }

    /**
     * Gets another type of Credentials after action
     */
    public sun.security.krb5.internal.ccache.Credentials getCCreds() {
        checkState(State.REQ_OK, "Cannot retrieve CCreds");
        return rep.getCCreds();
    }

    /**
     * Destroys the object and clears keys and password info.
     */
    public void destroy() {
        state = State.DESTROYED;
        if (password != null) {
            Arrays.fill(password, (char)0);
        }
    }

    /**
     * Checks if the current state is the specified one.
     * @param st the expected state
     * @param msg error message if state is not correct
     * @throws IllegalStateException if state is not correct
     */
    private void checkState(State st, String msg) {
        if (state != st) {
            throw new IllegalStateException(msg + " at " + st + " state");
        }
    }
}