| 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 */  | 
 | 
 | 
 | 
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 { | 
 | 
 | 
 | 
      | 
 | 
    private KDCOptions options;  | 
 | 
    private PrincipalName cname;  | 
 | 
    private PrincipalName refCname;   | 
 | 
    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  | 
 | 
      | 
 | 
    private final char[] password;  | 
 | 
    private final KeyTab ktab;  | 
 | 
 | 
 | 
    // Used to create a ENC-TIMESTAMP in the 2nd AS-REQ  | 
 | 
    private PAData[] paList;          | 
 | 
                                    // Used by getKeys() only.  | 
 | 
                                    // Only AS-REP should be enough per RFC,  | 
 | 
                                    // combined in case etypes are different.  | 
 | 
 | 
 | 
      | 
 | 
    private KrbAsReq req;  | 
 | 
    private KrbAsRep rep;  | 
 | 
 | 
 | 
    private static enum State { | 
 | 
        INIT,         | 
 | 
        REQ_OK,       | 
 | 
        DESTROYED,    | 
 | 
    }  | 
 | 
    private State state;  | 
 | 
 | 
 | 
      | 
 | 
    private void init(PrincipalName cname)  | 
 | 
            throws KrbException { | 
 | 
        this.cname = cname;  | 
 | 
        this.refCname = cname;  | 
 | 
        state = State.INIT;  | 
 | 
    }  | 
 | 
 | 
 | 
      | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
     */  | 
 | 
    public KrbAsReqBuilder(PrincipalName cname, KeyTab ktab)  | 
 | 
            throws KrbException { | 
 | 
        init(cname);  | 
 | 
        this.ktab = ktab;  | 
 | 
        this.password = null;  | 
 | 
    }  | 
 | 
 | 
 | 
      | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
     */  | 
 | 
    public KrbAsReqBuilder(PrincipalName cname, char[] pass)  | 
 | 
            throws KrbException { | 
 | 
        init(cname);  | 
 | 
        this.password = pass.clone();  | 
 | 
        this.ktab = null;  | 
 | 
    }  | 
 | 
 | 
 | 
      | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
     */  | 
 | 
    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;       | 
 | 
            try { | 
 | 
                for (int i=0; i<eTypes.length; i++) { | 
 | 
                      | 
 | 
                    PAData.SaltAndParams snp =  | 
 | 
                            PAData.getSaltAndParams(eTypes[i], paList);  | 
 | 
                    if (snp != null) { | 
 | 
                        // Never uses a salt for rc4-hmac, it does not use  | 
 | 
                          | 
 | 
                        if (eTypes[i] != EncryptedData.ETYPE_ARCFOUR_HMAC &&  | 
 | 
                                snp.salt != null) { | 
 | 
                            salt = snp.salt;  | 
 | 
                        }  | 
 | 
                        result[i] = EncryptionKey.acquireSecretKey(cname,  | 
 | 
                                password,  | 
 | 
                                eTypes[i],  | 
 | 
                                snp);  | 
 | 
                    }  | 
 | 
                }  | 
 | 
                  | 
 | 
                if (salt == null) salt = cname.getSalt();  | 
 | 
                for (int i=0; i<eTypes.length; i++) { | 
 | 
                      | 
 | 
                    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"); | 
 | 
        }  | 
 | 
    }  | 
 | 
 | 
 | 
      | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
     */  | 
 | 
    public void setOptions(KDCOptions options) { | 
 | 
        checkState(State.INIT, "Cannot specify options");  | 
 | 
        this.options = options;  | 
 | 
    }  | 
 | 
 | 
 | 
    public void setTill(KerberosTime till) { | 
 | 
        checkState(State.INIT, "Cannot specify till");  | 
 | 
        this.till = till;  | 
 | 
    }  | 
 | 
 | 
 | 
    public void setRTime(KerberosTime rtime) { | 
 | 
        checkState(State.INIT, "Cannot specify rtime");  | 
 | 
        this.rtime = rtime;  | 
 | 
    }  | 
 | 
 | 
 | 
      | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
     */  | 
 | 
    public void setTarget(PrincipalName sname) { | 
 | 
        checkState(State.INIT, "Cannot specify target");  | 
 | 
        this.sname = sname;  | 
 | 
    }  | 
 | 
 | 
 | 
      | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
     */  | 
 | 
    public void setAddresses(HostAddresses addresses) { | 
 | 
        checkState(State.INIT, "Cannot specify addresses");  | 
 | 
        this.addresses = addresses;  | 
 | 
    }  | 
 | 
 | 
 | 
      | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
     */  | 
 | 
    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()) { | 
 | 
            if (referralsState.sendCanonicalize()) { | 
 | 
                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,  | 
 | 
            refCname,  | 
 | 
            sname,  | 
 | 
            from,  | 
 | 
            till,  | 
 | 
            rtime,  | 
 | 
            eTypes,  | 
 | 
            addresses,  | 
 | 
            extraPAs);  | 
 | 
    }  | 
 | 
 | 
 | 
      | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
     */  | 
 | 
    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;  | 
 | 
    }  | 
 | 
 | 
 | 
      | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
     */  | 
 | 
    private KrbAsReqBuilder send() throws KrbException, IOException { | 
 | 
        boolean preAuthFailedOnce = false;  | 
 | 
        KdcComm comm = null;  | 
 | 
        EncryptionKey pakey = null;  | 
 | 
        ReferralsState referralsState = new ReferralsState(this);  | 
 | 
        while (true) { | 
 | 
            if (referralsState.refreshComm()) { | 
 | 
                comm = new KdcComm(refCname.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();    | 
 | 
                } else { | 
 | 
                    if (referralsState.handleError(ke)) { | 
 | 
                        pakey = null;  | 
 | 
                        preAuthFailedOnce = false;  | 
 | 
                        continue;  | 
 | 
                    }  | 
 | 
                    throw ke;  | 
 | 
                }  | 
 | 
            }  | 
 | 
        }  | 
 | 
    }  | 
 | 
 | 
 | 
    static final class ReferralsState { | 
 | 
        private static boolean canonicalizeConfig;  | 
 | 
        private boolean enabled;  | 
 | 
        private boolean sendCanonicalize;  | 
 | 
        private boolean isEnterpriseCname;  | 
 | 
        private int count;  | 
 | 
        private boolean refreshComm;  | 
 | 
        private KrbAsReqBuilder reqBuilder;  | 
 | 
 | 
 | 
        static { | 
 | 
            initStatic();  | 
 | 
        }  | 
 | 
 | 
 | 
        // Config may be refreshed while running so the setting  | 
 | 
          | 
 | 
        static void initStatic() { | 
 | 
            canonicalizeConfig = false;  | 
 | 
            try { | 
 | 
                canonicalizeConfig = Config.getInstance()  | 
 | 
                        .getBooleanObject("libdefaults", "canonicalize") == | 
 | 
                        Boolean.TRUE;  | 
 | 
            } catch (KrbException e) { | 
 | 
                if (Krb5.DEBUG) { | 
 | 
                    System.out.println("Exception in getting canonicalize," + | 
 | 
                            " using default value " +  | 
 | 
                            Boolean.valueOf(canonicalizeConfig) + ": " +  | 
 | 
                            e.getMessage());  | 
 | 
                }  | 
 | 
            }  | 
 | 
        }  | 
 | 
 | 
 | 
        ReferralsState(KrbAsReqBuilder reqBuilder) throws KrbException { | 
 | 
            this.reqBuilder = reqBuilder;  | 
 | 
            sendCanonicalize = canonicalizeConfig;  | 
 | 
            isEnterpriseCname = reqBuilder.refCname.getNameType() ==  | 
 | 
                    PrincipalName.KRB_NT_ENTERPRISE;  | 
 | 
            updateStatus();  | 
 | 
            if (!enabled && isEnterpriseCname) { | 
 | 
                throw new KrbException("NT-ENTERPRISE principals only" + | 
 | 
                        " allowed when referrals are enabled.");  | 
 | 
            }  | 
 | 
            refreshComm = true;  | 
 | 
        }  | 
 | 
 | 
 | 
        private void updateStatus() { | 
 | 
            enabled = !Config.DISABLE_REFERRALS &&  | 
 | 
                    (isEnterpriseCname || sendCanonicalize);  | 
 | 
        }  | 
 | 
 | 
 | 
        boolean handleError(KrbException ke) throws RealmException { | 
 | 
            if (enabled) { | 
 | 
                if (ke.returnCode() == Krb5.KRB_ERR_WRONG_REALM) { | 
 | 
                    Realm referredRealm = ke.getError().getClientRealm();  | 
 | 
                    if (referredRealm != null &&  | 
 | 
                            !referredRealm.toString().isEmpty() &&  | 
 | 
                            count < Config.MAX_REFERRALS) { | 
 | 
                        // A valid referral was received while referrals  | 
 | 
                        // were enabled. Change the cname realm to the referred  | 
 | 
                          | 
 | 
                        reqBuilder.refCname = new PrincipalName(  | 
 | 
                                reqBuilder.refCname.getNameType(),  | 
 | 
                                reqBuilder.refCname.getNameStrings(),  | 
 | 
                                referredRealm);  | 
 | 
                        refreshComm = true;  | 
 | 
                        count++;  | 
 | 
                        return true;  | 
 | 
                    }  | 
 | 
                }  | 
 | 
                if (count < Config.MAX_REFERRALS && sendCanonicalize) { | 
 | 
                    if (Krb5.DEBUG) { | 
 | 
                        System.out.println("KrbAsReqBuilder: AS-REQ failed." + | 
 | 
                                " Retrying with CANONICALIZE false.");  | 
 | 
                    }  | 
 | 
 | 
 | 
                    // Server returned an unexpected error with  | 
 | 
                      | 
 | 
                    sendCanonicalize = false;  | 
 | 
 | 
 | 
                    // Setting CANONICALIZE to false may imply that referrals  | 
 | 
                      | 
 | 
                    updateStatus();  | 
 | 
 | 
 | 
                    return true;  | 
 | 
                }  | 
 | 
            }  | 
 | 
            return false;  | 
 | 
        }  | 
 | 
 | 
 | 
        boolean refreshComm() { | 
 | 
            boolean retRefreshComm = refreshComm;  | 
 | 
            refreshComm = false;  | 
 | 
            return retRefreshComm;  | 
 | 
        }  | 
 | 
 | 
 | 
        boolean isEnabled() { | 
 | 
            return enabled;  | 
 | 
        }  | 
 | 
 | 
 | 
        boolean sendCanonicalize() { | 
 | 
            return sendCanonicalize;  | 
 | 
        }  | 
 | 
    }  | 
 | 
 | 
 | 
      | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
     */  | 
 | 
    public KrbAsReqBuilder action()  | 
 | 
            throws KrbException, Asn1Exception, IOException { | 
 | 
        checkState(State.INIT, "Cannot call action");  | 
 | 
        state = State.REQ_OK;  | 
 | 
        return send().resolve();  | 
 | 
    }  | 
 | 
 | 
 | 
      | 
 | 
 | 
 | 
     */  | 
 | 
    public Credentials getCreds() { | 
 | 
        checkState(State.REQ_OK, "Cannot retrieve creds");  | 
 | 
        return rep.getCreds();  | 
 | 
    }  | 
 | 
 | 
 | 
      | 
 | 
 | 
 | 
     */  | 
 | 
    public sun.security.krb5.internal.ccache.Credentials getCCreds() { | 
 | 
        checkState(State.REQ_OK, "Cannot retrieve CCreds");  | 
 | 
        return rep.getCCreds();  | 
 | 
    }  | 
 | 
 | 
 | 
      | 
 | 
 | 
 | 
     */  | 
 | 
    public void destroy() { | 
 | 
        state = State.DESTROYED;  | 
 | 
        if (password != null) { | 
 | 
            Arrays.fill(password, (char)0);  | 
 | 
        }  | 
 | 
    }  | 
 | 
 | 
 | 
      | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
     */  | 
 | 
    private void checkState(State st, String msg) { | 
 | 
        if (state != st) { | 
 | 
            throw new IllegalStateException(msg + " at " + st + " state");  | 
 | 
        }  | 
 | 
    }  | 
 | 
}  |