1 /*
   2  * Copyright (c) 2000, 2013, 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 /*
  27  *
  28  *  (C) Copyright IBM Corp. 1999 All Rights Reserved.
  29  *  Copyright 1997 The Open Group Research Institute.  All rights reserved.
  30  */
  31 
  32 package sun.security.krb5;
  33 
  34 import sun.security.krb5.internal.Krb5;
  35 import sun.security.util.*;
  36 import java.io.IOException;
  37 import java.util.*;
  38 
  39 import sun.security.krb5.internal.util.KerberosString;
  40 
  41 /**
  42  * Implements the ASN.1 Realm type.
  43  *
  44  * {@code Realm ::= GeneralString}
  45  *
  46  * This class is immutable.
  47  */
  48 public class Realm implements Cloneable {
  49     private final String realm; // not null nor empty
  50 
  51     public Realm(String name) throws RealmException {
  52         realm = parseRealm(name);
  53     }
  54 
  55     public static Realm getDefault() throws RealmException {
  56         try {
  57             return new Realm(Config.getInstance().getDefaultRealm());
  58         } catch (RealmException re) {
  59             throw re;
  60         } catch (KrbException ke) {
  61             throw new RealmException(ke);
  62         }
  63     }
  64 
  65     // Immutable class, no need to clone
  66     public Object clone() {
  67         return this;
  68     }
  69 
  70     public boolean equals(Object obj) {
  71         if (this == obj) {
  72             return true;
  73         }
  74 
  75         if (!(obj instanceof Realm)) {
  76             return false;
  77         }
  78 
  79         Realm that = (Realm)obj;
  80         return this.realm.equals(that.realm);
  81     }
  82 
  83     public int hashCode() {
  84         return realm.hashCode();
  85     }
  86 
  87     /**
  88      * Constructs a Realm object.
  89      * @param encoding a Der-encoded data.
  90      * @exception Asn1Exception if an error occurs while decoding an ASN1 encoded data.
  91      * @exception IOException if an I/O error occurs while reading encoded data.
  92      * @exception RealmException if an error occurs while parsing a Realm object.
  93      */
  94     public Realm(DerValue encoding)
  95         throws Asn1Exception, RealmException, IOException {
  96         if (encoding == null) {
  97             throw new IllegalArgumentException("encoding can not be null");
  98         }
  99         realm = new KerberosString(encoding).toString();
 100         if (realm == null || realm.length() == 0)
 101             throw new RealmException(Krb5.REALM_NULL);
 102         if (!isValidRealmString(realm))
 103             throw new RealmException(Krb5.REALM_ILLCHAR);
 104     }
 105 
 106     public String toString() {
 107         return realm;
 108     }
 109 
 110     // Extract realm from a string like dummy@REALM
 111     public static String parseRealmAtSeparator(String name)
 112         throws RealmException {
 113         if (name == null) {
 114             throw new IllegalArgumentException
 115                 ("null input name is not allowed");
 116         }
 117         String temp = new String(name);
 118         String result = null;
 119         int i = 0;
 120         while (i < temp.length()) {
 121             if (temp.charAt(i) == PrincipalName.NAME_REALM_SEPARATOR) {
 122                 if (i == 0 || temp.charAt(i - 1) != '\\') {
 123                     if (i + 1 < temp.length()) {
 124                         result = temp.substring(i + 1, temp.length());
 125                     } else {
 126                         throw new IllegalArgumentException
 127                                 ("empty realm part not allowed");
 128                     }
 129                     break;
 130                 }
 131             }
 132             i++;
 133         }
 134         if (result != null) {
 135             if (result.length() == 0)
 136                 throw new RealmException(Krb5.REALM_NULL);
 137             if (!isValidRealmString(result))
 138                 throw new RealmException(Krb5.REALM_ILLCHAR);
 139         }
 140         return result;
 141     }
 142 
 143     public static String parseRealmComponent(String name) {
 144         if (name == null) {
 145             throw new IllegalArgumentException
 146                 ("null input name is not allowed");
 147         }
 148         String temp = new String(name);
 149         String result = null;
 150         int i = 0;
 151         while (i < temp.length()) {
 152             if (temp.charAt(i) == PrincipalName.REALM_COMPONENT_SEPARATOR) {
 153                 if (i == 0 || temp.charAt(i - 1) != '\\') {
 154                     if (i + 1 < temp.length())
 155                         result = temp.substring(i + 1, temp.length());
 156                     break;
 157                 }
 158             }
 159             i++;
 160         }
 161         return result;
 162     }
 163 
 164     protected static String parseRealm(String name) throws RealmException {
 165         String result = parseRealmAtSeparator(name);
 166         if (result == null)
 167             result = name;
 168         if (result == null || result.length() == 0)
 169             throw new RealmException(Krb5.REALM_NULL);
 170         if (!isValidRealmString(result))
 171             throw new RealmException(Krb5.REALM_ILLCHAR);
 172         return result;
 173     }
 174 
 175     // This is protected because the definition of a realm
 176     // string is fixed
 177     protected static boolean isValidRealmString(String name) {
 178         if (name == null)
 179             return false;
 180         if (name.length() == 0)
 181             return false;
 182         for (int i = 0; i < name.length(); i++) {
 183             if (name.charAt(i) == '/' ||
 184                 name.charAt(i) == ':' ||
 185                 name.charAt(i) == '\0') {
 186                 return false;
 187             }
 188         }
 189         return true;
 190     }
 191 
 192     /**
 193      * Encodes a Realm object.
 194      * @return the byte array of encoded KrbCredInfo object.
 195      * @exception Asn1Exception if an error occurs while decoding an ASN1 encoded data.
 196      * @exception IOException if an I/O error occurs while reading encoded data.
 197      *
 198      */
 199     public byte[] asn1Encode() throws Asn1Exception, IOException {
 200         DerOutputStream out = new DerOutputStream();
 201         out.putDerValue(new KerberosString(this.realm).toDerValue());
 202         return out.toByteArray();
 203     }
 204 
 205 
 206     /**
 207      * Parse (unmarshal) a realm from a DER input stream.  This form
 208      * parsing might be used when expanding a value which is part of
 209      * a constructed sequence and uses explicitly tagged type.
 210      *
 211      * @exception Asn1Exception on error.
 212      * @param data the Der input stream value, which contains one or more marshaled value.
 213      * @param explicitTag tag number.
 214      * @param optional indicate if this data field is optional
 215      * @return an instance of Realm.
 216      *
 217      */
 218     public static Realm parse(DerInputStream data, byte explicitTag, boolean optional)
 219             throws Asn1Exception, IOException, RealmException {
 220         if ((optional) && (((byte)data.peekByte() & (byte)0x1F) != explicitTag)) {
 221             return null;
 222         }
 223         DerValue der = data.getDerValue();
 224         if (explicitTag != (der.getTag() & (byte)0x1F))  {
 225             throw new Asn1Exception(Krb5.ASN1_BAD_ID);
 226         } else {
 227             DerValue subDer = der.getData().getDerValue();
 228             return new Realm(subDer);
 229         }
 230     }
 231 
 232     /**
 233      * Returns an array of realms that may be traversed to obtain
 234      * a TGT from the initiating realm cRealm to the target realm
 235      * sRealm.
 236      * <br>
 237      * This method would read [capaths] to create a path, or generate a
 238      * hierarchical path if [capaths] does not contain a sub-stanza for cRealm
 239      * or the sub-stanza does not contain a tag for sRealm.
 240      * <br>
 241      * The returned list would never be null, and it always contains
 242      * cRealm as the head entry. sRealm is not included as the tail.
 243      *
 244      * @param cRealm the initiating realm, not null
 245      * @param sRealm the target realm, not null, not equals to cRealm
 246      * @return array of realms including at least cRealm as the first
 247      *         element
 248      */
 249     public static String[] getRealmsList(String cRealm, String sRealm) {
 250         try {
 251             // Try [capaths]
 252             return parseCapaths(cRealm, sRealm);
 253         } catch (KrbException ke) {
 254             // Now assume the realms are organized hierarchically.
 255             return parseHierarchy(cRealm, sRealm);
 256         }
 257     }
 258 
 259     /**
 260      * Parses the [capaths] stanza of the configuration file for a
 261      * list of realms to traverse to obtain credentials from the
 262      * initiating realm cRealm to the target realm sRealm.
 263      *
 264      * For a given client realm C there is a tag C in [capaths] whose
 265      * subtag S has a value which is a (possibly partial) path from C
 266      * to S. When the path is partial, it contains only the tail of the
 267      * full path. Values of other subtags will be used to build the full
 268      * path. The value "." means a direct path from C to S. If realm S
 269      * does not appear as a subtag, there is no path defined here.
 270      *
 271      * The implementation ignores all values which equals to C or S, or
 272      * a "." in multiple values, or any duplicated realm names.
 273      *
 274      * When a path value has more than two realms, they can be specified
 275      * with multiple key-value pairs each having a single value, but the
 276      * order must not change.
 277      *
 278      * For example:
 279      *
 280      * [capaths]
 281      *    TIVOLI.COM = {
 282      *        IBM.COM = IBM_LDAPCENTRAL.COM MOONLITE.ORG
 283      *        IBM_LDAPCENTRAL.COM = LDAPCENTRAL.NET
 284      *        LDAPCENTRAL.NET = .
 285      *    }
 286      *
 287      * TIVOLI.COM has a direct path to LDAPCENTRAL.NET, which has a direct
 288      * path to IBM_LDAPCENTRAL.COM. It also has a partial path to IBM.COM
 289      * being "IBM_LDAPCENTRAL.COM MOONLITE.ORG". Merging these info together,
 290      * a full path from TIVOLI.COM to IBM.COM will be
 291      *
 292      *   TIVOLI.COM -> LDAPCENTRAL.NET -> IBM_LDAPCENTRAL.COM
 293      *              -> IBM_LDAPCENTRAL.COM -> MOONLITE.ORG
 294      *
 295      * Please note the sRealm IBM.COM does not appear in the path.
 296      *
 297      * @param cRealm the initiating realm
 298      * @param sRealm the target realm, not the same as cRealm
 299      * @return array of realms including at least cRealm as the first
 300      *          element
 301      * @throws KrbException if the config does not contain a sub-stanza
 302      *          for cRealm in [capaths] or the sub-stanza does not contain
 303      *          sRealm as a tag
 304      */
 305     private static String[] parseCapaths(String cRealm, String sRealm)
 306             throws KrbException {
 307 
 308         // This line could throw a KrbException
 309         Config cfg = Config.getInstance();
 310 
 311         if (!cfg.exists("capaths", cRealm, sRealm)) {
 312             throw new KrbException("No conf");
 313         }
 314 
 315         LinkedList<String> path = new LinkedList<>();
 316 
 317         String head = sRealm;
 318         while (true) {
 319             String value = cfg.getAll("capaths", cRealm, head);
 320             if (value == null) {
 321                 break;
 322             }
 323             String[] more = value.split("\\s+");
 324             boolean changed = false;
 325             for (int i=more.length-1; i>=0; i--) {
 326                 if (path.contains(more[i])
 327                         || more[i].equals(".")
 328                         || more[i].equals(cRealm)
 329                         || more[i].equals(sRealm)
 330                         || more[i].equals(head)) {
 331                     // Ignore invalid values
 332                     continue;
 333                 }
 334                 changed = true;
 335                 path.addFirst(more[i]);
 336             }
 337             if (!changed) break;
 338             head = path.getFirst();
 339         }
 340         path.addFirst(cRealm);
 341         return path.toArray(new String[path.size()]);
 342    }
 343 
 344     /**
 345      * Build a list of realm that can be traversed
 346      * to obtain credentials from the initiating realm cRealm
 347      * for a service in the target realm sRealm.
 348      * @param cRealm the initiating realm
 349      * @param sRealm the target realm, not the same as cRealm
 350      * @return array of realms including cRealm as the first element
 351      */
 352     private static String[] parseHierarchy(String cRealm, String sRealm) {
 353 
 354         String[] cComponents = cRealm.split("\\.");
 355         String[] sComponents = sRealm.split("\\.");
 356 
 357         int cPos = cComponents.length;
 358         int sPos = sComponents.length;
 359 
 360         boolean hasCommon = false;
 361         for (sPos--, cPos--; sPos >=0 && cPos >= 0 &&
 362                 sComponents[sPos].equals(cComponents[cPos]);
 363                 sPos--, cPos--) {
 364             hasCommon = true;
 365         }
 366 
 367         // For those with common components:
 368         //                            length  pos
 369         // SITES1.SALES.EXAMPLE.COM   4       1
 370         //   EVERYWHERE.EXAMPLE.COM   3       0
 371 
 372         // For those without common components:
 373         //                     length  pos
 374         // DEVEL.EXAMPLE.COM   3       2
 375         // PROD.EXAMPLE.ORG    3       2
 376 
 377         LinkedList<String> path = new LinkedList<>();
 378 
 379         // Un-common ones for client side
 380         for (int i=0; i<=cPos; i++) {
 381             path.addLast(subStringFrom(cComponents, i));
 382         }
 383 
 384         // Common one
 385         if (hasCommon) {
 386             path.addLast(subStringFrom(cComponents, cPos+1));
 387         }
 388 
 389         // Un-common ones for server side
 390         for (int i=sPos; i>=0; i--) {
 391             path.addLast(subStringFrom(sComponents, i));
 392         }
 393 
 394         // Remove sRealm from path. Note that it might be added at last loop
 395         // or as a common component, if sRealm is a parent of cRealm
 396         path.removeLast();
 397 
 398         return path.toArray(new String[path.size()]);
 399     }
 400 
 401     /**
 402      * Creates a realm name using components from the given position.
 403      * For example, subStringFrom({"A", "B", "C"}, 1) is "B.C".
 404      */
 405     private static String subStringFrom(String[] components, int from) {
 406         StringBuilder sb = new StringBuilder();
 407         for (int i=from; i<components.length; i++) {
 408             if (sb.length() != 0) sb.append('.');
 409             sb.append(components[i]);
 410         }
 411         return sb.toString();
 412     }
 413 }