1 /* 2 * Copyright (c) 2010, 2018, 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.getNameString().replaceAll( 404 PrincipalName.NAME_REALM_SEPARATOR + "", "\\\\" + 405 PrincipalName.NAME_REALM_SEPARATOR), cname.getNameType(), 406 referredRealm.toString()); 407 refreshComm = true; 408 count++; 409 return true; 410 } 411 } 412 if (count < Config.MAX_REFERRALS && 413 cname.getNameType() != PrincipalName.KRB_NT_ENTERPRISE) { 414 // Server may raise an error if CANONICALIZE is true. 415 // Try CANONICALIZE false. 416 enabled = false; 417 return true; 418 } 419 } 420 return false; 421 } 422 423 boolean refreshComm() { 424 boolean retRefreshComm = refreshComm; 425 refreshComm = false; 426 return retRefreshComm; 427 } 428 429 boolean isEnabled() { 430 return enabled; 431 } 432 } 433 434 /** 435 * Performs AS-REQ send and AS-REP receive. 436 * Maybe a state is needed here, to divide prepare process and getCreds. 437 * @throws KrbException 438 * @throws Asn1Exception 439 * @throws IOException 440 */ 441 public KrbAsReqBuilder action() 442 throws KrbException, Asn1Exception, IOException { 443 checkState(State.INIT, "Cannot call action"); 444 state = State.REQ_OK; 445 return send().resolve(); 446 } 447 448 /** 449 * Gets Credentials object after action 450 */ 451 public Credentials getCreds() { 452 checkState(State.REQ_OK, "Cannot retrieve creds"); 453 return rep.getCreds(); 454 } 455 456 /** 457 * Gets another type of Credentials after action 458 */ 459 public sun.security.krb5.internal.ccache.Credentials getCCreds() { 460 checkState(State.REQ_OK, "Cannot retrieve CCreds"); 461 return rep.getCCreds(); 462 } 463 464 /** 465 * Destroys the object and clears keys and password info. 466 */ 467 public void destroy() { 468 state = State.DESTROYED; 469 if (password != null) { 470 Arrays.fill(password, (char)0); 471 } 472 } 473 474 /** 475 * Checks if the current state is the specified one. 476 * @param st the expected state 477 * @param msg error message if state is not correct 478 * @throws IllegalStateException if state is not correct 479 */ 480 private void checkState(State st, String msg) { 481 if (state != st) { 482 throw new IllegalStateException(msg + " at " + st + " state"); 483 } 484 } 485 }