1 /* 2 * Copyright (c) 2010, 2012, 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) throws KrbException, IOException { 266 int[] eTypes; 267 if (password != null) { 268 eTypes = EType.getDefaults("default_tkt_enctypes"); 269 } else { 270 EncryptionKey[] ks = Krb5Util.keysFromJavaxKeyTab(ktab, cname); 271 eTypes = EType.getDefaults("default_tkt_enctypes", 272 ks); 273 for (EncryptionKey k: ks) k.destroy(); 274 } 275 return new KrbAsReq(key, 276 options, 277 cname, 278 sname, 279 from, 280 till, 281 rtime, 282 eTypes, 283 addresses); 284 } 285 286 /** 287 * Parses AS-REP, decrypts enc-part, retrieves ticket and session key 288 * @throws KrbException 289 * @throws Asn1Exception 290 * @throws IOException 291 */ 292 private KrbAsReqBuilder resolve() 293 throws KrbException, Asn1Exception, IOException { 294 if (ktab != null) { 295 rep.decryptUsingKeyTab(ktab, req, cname); 296 } else { 297 rep.decryptUsingPassword(password, req, cname); 298 } 299 if (rep.getPA() != null) { 300 if (paList == null || paList.length == 0) { 301 paList = rep.getPA(); 302 } else { 303 int extraLen = rep.getPA().length; 304 if (extraLen > 0) { 305 int oldLen = paList.length; 306 paList = Arrays.copyOf(paList, paList.length + extraLen); 307 System.arraycopy(rep.getPA(), 0, paList, oldLen, extraLen); 308 } 309 } 310 } 311 return this; 312 } 313 314 /** 315 * Communication until AS-REP or non preauth-related KRB-ERROR received 316 * @throws KrbException 317 * @throws IOException 318 */ 319 private KrbAsReqBuilder send() throws KrbException, IOException { 320 boolean preAuthFailedOnce = false; 321 KdcComm comm = new KdcComm(cname.getRealmAsString()); 322 EncryptionKey pakey = null; 323 while (true) { 324 try { 325 req = build(pakey); 326 rep = new KrbAsRep(comm.send(req.encoding())); 327 return this; 328 } catch (KrbException ke) { 329 if (!preAuthFailedOnce && ( 330 ke.returnCode() == Krb5.KDC_ERR_PREAUTH_FAILED || 331 ke.returnCode() == Krb5.KDC_ERR_PREAUTH_REQUIRED)) { 332 if (Krb5.DEBUG) { 333 System.out.println("KrbAsReqBuilder: " + 334 "PREAUTH FAILED/REQ, re-send AS-REQ"); 335 } 336 preAuthFailedOnce = true; 337 KRBError kerr = ke.getError(); 338 int paEType = PAData.getPreferredEType(kerr.getPA(), 339 EType.getDefaults("default_tkt_enctypes")[0]); 340 if (password == null) { 341 EncryptionKey[] ks = Krb5Util.keysFromJavaxKeyTab(ktab, cname); 342 pakey = EncryptionKey.findKey(paEType, ks); 343 if (pakey != null) pakey = (EncryptionKey)pakey.clone(); 344 for (EncryptionKey k: ks) k.destroy(); 345 } else { 346 pakey = EncryptionKey.acquireSecretKey(cname, 347 password, 348 paEType, 349 PAData.getSaltAndParams( 350 paEType, kerr.getPA())); 351 } 352 paList = kerr.getPA(); // Update current paList 353 } else { 354 throw ke; 355 } 356 } 357 } 358 } 359 360 /** 361 * Performs AS-REQ send and AS-REP receive. 362 * Maybe a state is needed here, to divide prepare process and getCreds. 363 * @throws KrbException 364 * @throws Asn1Exception 365 * @throws IOException 366 */ 367 public KrbAsReqBuilder action() 368 throws KrbException, Asn1Exception, IOException { 369 checkState(State.INIT, "Cannot call action"); 370 state = State.REQ_OK; 371 return send().resolve(); 372 } 373 374 /** 375 * Gets Credentials object after action 376 */ 377 public Credentials getCreds() { 378 checkState(State.REQ_OK, "Cannot retrieve creds"); 379 return rep.getCreds(); 380 } 381 382 /** 383 * Gets another type of Credentials after action 384 */ 385 public sun.security.krb5.internal.ccache.Credentials getCCreds() { 386 checkState(State.REQ_OK, "Cannot retrieve CCreds"); 387 return rep.getCCreds(); 388 } 389 390 /** 391 * Destroys the object and clears keys and password info. 392 */ 393 public void destroy() { 394 state = State.DESTROYED; 395 if (password != null) { 396 Arrays.fill(password, (char)0); 397 } 398 } 399 400 /** 401 * Checks if the current state is the specified one. 402 * @param st the expected state 403 * @param msg error message if state is not correct 404 * @throws IllegalStateException if state is not correct 405 */ 406 private void checkState(State st, String msg) { 407 if (state != st) { 408 throw new IllegalStateException(msg + " at " + st + " state"); 409 } 410 } 411 }