/*
 * Copyright (c) 2000, 2024, 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.
 */

/*
 *
 *  (C) Copyright IBM Corp. 1999 All Rights Reserved.
 *  Copyright 1997 The Open Group Research Institute.  All rights reserved.
 */

package sun.security.krb5;

import sun.security.krb5.internal.*;
import sun.security.util.*;
import java.net.*;
import java.util.ArrayList;
import java.util.Locale;
import java.io.IOException;
import java.math.BigInteger;
import java.util.Arrays;
import sun.security.krb5.internal.ccache.CCacheOutputStream;
import sun.security.krb5.internal.util.KerberosString;


/**
 * Implements the ASN.1 PrincipalName type and its realm in a single class.
 * <pre>{@code
 *    Realm           ::= KerberosString
 *
 *    PrincipalName   ::= SEQUENCE {
 *            name-type       [0] Int32,
 *            name-string     [1] SEQUENCE OF KerberosString
 *    }
 * }</pre>
 * This class is immutable.
 * @see Realm
 */
public class PrincipalName implements Cloneable {

    //name types

    /**
     * Name type not known
     */
    public static final int KRB_NT_UNKNOWN =   0;

    /**
     * Just the name of the principal as in DCE, or for users
     */
    public static final int KRB_NT_PRINCIPAL = 1;

    /**
     * Service and other unique instance (krbtgt)
     */
    public static final int KRB_NT_SRV_INST =  2;

    /**
     * Service with host name as instance (telnet, rcommands)
     */
    public static final int KRB_NT_SRV_HST =   3;

    /**
     * Service with host as remaining components
     */
    public static final int KRB_NT_SRV_XHST =  4;

    /**
     * Unique ID
     */
    public static final int KRB_NT_UID = 5;

    /**
     * Enterprise name (alias)
     */
    public static final int KRB_NT_ENTERPRISE = 10;

    /**
     * TGS Name
     */
    public static final String TGS_DEFAULT_SRV_NAME = "krbtgt";

    public static final char NAME_COMPONENT_SEPARATOR = '/';
    public static final char NAME_REALM_SEPARATOR = '@';
    public static final char REALM_COMPONENT_SEPARATOR = '.';

    public static final String NAME_COMPONENT_SEPARATOR_STR = "/";
    public static final String NAME_REALM_SEPARATOR_STR = "@";

    private static final boolean NAME_CASE_SENSITIVE_IN_MATCH
            = "true".equalsIgnoreCase(
                    SecurityProperties.getOverridableProperty(
                            "jdk.security.krb5.name.case.sensitive"));


    // Instance fields.

    /**
     * The name type, from PrincipalName's name-type field.
     */
    private final int nameType;

    /**
     * The name strings, from PrincipalName's name-strings field. This field
     * must be neither null nor empty. Each entry of it must also be neither
     * null nor empty. Make sure to clone the field when it's passed in or out.
     */
    private final String[] nameStrings;

    /**
     * The realm this principal belongs to.
     */
    private final Realm nameRealm;      // not null


    // cached default salt, not used in clone
    private transient String salt = null;

    // There are 3 basic constructors. All other constructors must call them.
    // All basic constructors must call validateNameStrings.
    // 1. From name components
    // 2. From name
    // 3. From DER encoding

    /**
     * Creates a PrincipalName.
     */
    public PrincipalName(int nameType, String[] nameStrings, Realm nameRealm) {
        if (nameRealm == null) {
            throw new IllegalArgumentException("Null realm not allowed");
        }
        validateNameStrings(nameStrings);
        this.nameType = nameType;
        this.nameStrings = nameStrings.clone();
        this.nameRealm = nameRealm;
    }

    // Warning: called by NativeCreds.c
    public PrincipalName(String[] nameParts, String realm) throws RealmException {
        this(KRB_NT_UNKNOWN, nameParts, new Realm(realm));
    }

    // Validate a nameStrings argument
    private static void validateNameStrings(String[] ns) {
        if (ns == null) {
            throw new IllegalArgumentException("Null nameStrings not allowed");
        }
        if (ns.length == 0) {
            throw new IllegalArgumentException("Empty nameStrings not allowed");
        }
        for (String s: ns) {
            if (s == null) {
                throw new IllegalArgumentException("Null nameString not allowed");
            }
            if (s.isEmpty()) {
                throw new IllegalArgumentException("Empty nameString not allowed");
            }
        }
    }

    public Object clone() {
        try {
            PrincipalName pName = (PrincipalName) super.clone();
            UNSAFE.putReference(this, NAME_STRINGS_OFFSET, nameStrings.clone());
            return pName;
        } catch (CloneNotSupportedException ex) {
            throw new AssertionError("Should never happen");
        }
    }

    private static final long NAME_STRINGS_OFFSET;
    private static final jdk.internal.misc.Unsafe UNSAFE;
    static {
        try {
            jdk.internal.misc.Unsafe unsafe = jdk.internal.misc.Unsafe.getUnsafe();
            NAME_STRINGS_OFFSET = unsafe.objectFieldOffset(
                    PrincipalName.class.getDeclaredField("nameStrings"));
            UNSAFE = unsafe;
        } catch (ReflectiveOperationException e) {
            throw new Error(e);
        }
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        return o instanceof PrincipalName other
                && nameRealm.equals(other.nameRealm)
                && Arrays.equals(nameStrings, other.nameStrings);
    }

    /**
     * Returns the ASN.1 encoding of the
     * <pre>{@code
     * PrincipalName    ::= SEQUENCE {
     *          name-type       [0] Int32,
     *          name-string     [1] SEQUENCE OF KerberosString
     * }
     *
     * KerberosString   ::= GeneralString (IA5String)
     * }</pre>
     *
     * <p>
     * This definition reflects the Network Working Group RFC 4120
     * specification available at
     * <a href="http://www.ietf.org/rfc/rfc4120.txt">
     * http://www.ietf.org/rfc/rfc4120.txt</a>.
     *
     * @param encoding DER-encoded PrincipalName (without Realm)
     * @param realm the realm for this name
     * @exception Asn1Exception if an error occurs while decoding
     * an ASN1 encoded data.
     * @exception Asn1Exception if there is an ASN1 encoding error
     * @exception IOException if an I/O error occurs
     * @exception IllegalArgumentException if encoding is null
     * reading encoded data.
     */
    public PrincipalName(DerValue encoding, Realm realm)
            throws Asn1Exception, IOException {
        if (realm == null) {
            throw new IllegalArgumentException("Null realm not allowed");
        }
        nameRealm = realm;
        DerValue der;
        if (encoding == null) {
            throw new IllegalArgumentException("Null encoding not allowed");
        }
        if (encoding.getTag() != DerValue.tag_Sequence) {
            throw new Asn1Exception(Krb5.ASN1_BAD_ID);
        }
        der = encoding.getData().getDerValue();
        if ((der.getTag() & 0x1F) == 0x00) {
            BigInteger bint = der.getData().getBigInteger();
            nameType = bint.intValue();
        } else {
            throw new Asn1Exception(Krb5.ASN1_BAD_ID);
        }
        der = encoding.getData().getDerValue();
        if ((der.getTag() & 0x01F) == 0x01) {
            DerValue subDer = der.getData().getDerValue();
            if (subDer.getTag() != DerValue.tag_SequenceOf) {
                throw new Asn1Exception(Krb5.ASN1_BAD_ID);
            }
            ArrayList<String> v = new ArrayList<>();
            DerValue subSubDer;
            while(subDer.getData().available() > 0) {
                subSubDer = subDer.getData().getDerValue();
                String namePart = new KerberosString(subSubDer).toString();
                v.add(namePart);
            }
            nameStrings = v.toArray(new String[0]);
            validateNameStrings(nameStrings);
        } else  {
            throw new Asn1Exception(Krb5.ASN1_BAD_ID);
        }
    }

    /**
     * Parse (unmarshal) a <code>PrincipalName</code> from a DER
     * input stream.  This form
     * parsing might be used when expanding a value which is part of
     * a constructed sequence and uses explicitly tagged type.
     *
     * @exception Asn1Exception on error.
     * @param data the Der input stream value, which contains one or
     * more marshaled value.
     * @param explicitTag tag number.
     * @param optional indicate if this data field is optional
     * @param realm the realm for the name
     * @return an instance of <code>PrincipalName</code>, or null if the
     * field is optional and missing.
     */
    public static PrincipalName parse(DerInputStream data,
                                      byte explicitTag, boolean
                                      optional,
                                      Realm realm)
        throws Asn1Exception, IOException, RealmException {

        if ((optional) && (((byte)data.peekByte() & (byte)0x1F) !=
                           explicitTag))
            return null;
        DerValue der = data.getDerValue();
        if (explicitTag != (der.getTag() & (byte)0x1F)) {
            throw new Asn1Exception(Krb5.ASN1_BAD_ID);
        } else {
            DerValue subDer = der.getData().getDerValue();
            if (realm == null) {
                realm = Realm.getDefault();
            }
            return new PrincipalName(subDer, realm);
        }
    }


    // XXX Error checkin consistent with MIT krb5_parse_name
    // Code repetition, realm parsed again by class Realm
    private static String[] parseName(String name) {

        ArrayList<String> tempStrings = new ArrayList<>();
        String temp = name;
        int i = 0;
        int componentStart = 0;
        String component;

        while (i < temp.length()) {
            if (temp.charAt(i) == NAME_COMPONENT_SEPARATOR) {
                /*
                 * If this separator is escaped then don't treat it
                 * as a separator
                 */
                if (i > 0 && temp.charAt(i - 1) == '\\') {
                    temp = temp.substring(0, i - 1) +
                        temp.substring(i, temp.length());
                    continue;
                }
                else {
                    if (componentStart <= i) {
                        component = temp.substring(componentStart, i);
                        tempStrings.add(component);
                    }
                    componentStart = i + 1;
                }
            } else {
                if (temp.charAt(i) == NAME_REALM_SEPARATOR) {
                    /*
                     * If this separator is escaped then don't treat it
                     * as a separator
                     */
                    if (i > 0 && temp.charAt(i - 1) == '\\') {
                        temp = temp.substring(0, i - 1) +
                            temp.substring(i, temp.length());
                        continue;
                    } else {
                        if (componentStart < i) {
                            component = temp.substring(componentStart, i);
                            tempStrings.add(component);
                        }
                        componentStart = i + 1;
                        break;
                    }
                }
            }
            i++;
        }

        if (i == temp.length()) {
            component = temp.substring(componentStart, i);
            tempStrings.add(component);
        }

        String[] result = tempStrings.toArray(new String[0]);
        return result;
    }

    /**
     * Constructs a PrincipalName from a string.
     * @param name the name
     * @param type the type
     * @param realm the realm, null if not known. Note that when realm is not
     * null, it will be always used even if there is a realm part in name. When
     * realm is null, will read realm part from name, or try to map a realm
     * (for KRB_NT_SRV_HST), or use the default realm, or fail
     * @throws RealmException
     */
    public PrincipalName(String name, int type, String realm)
            throws RealmException {
        if (name == null) {
            throw new IllegalArgumentException("Null name not allowed");
        }
        String[] nameParts = parseName(name);
        validateNameStrings(nameParts);
        if (realm == null) {
            realm = Realm.parseRealmAtSeparator(name);
        }

        switch (type) {
        case KRB_NT_SRV_HST:
            if (nameParts.length >= 2) {
                String hostName = nameParts[1];
                Boolean option;
                try {
                    // If true, try canonicalizing and accept it if it starts
                    // with the short name. Otherwise, never. Default true.
                    option = Config.getInstance().getBooleanObject(
                            "libdefaults", "dns_canonicalize_hostname");
                } catch (KrbException e) {
                    option = null;
                }
                if (option != Boolean.FALSE) {
                    try {
                        // RFC4120 does not recommend canonicalizing a hostname.
                        // However, for compatibility reason, we will try
                        // canonicalizing it and see if the output looks better.

                        String canonicalized = (InetAddress.getByName(hostName)).
                                getCanonicalHostName();

                        // Looks if canonicalized is a longer format of hostName,
                        // we accept cases like
                        //     bunny -> bunny.rabbit.hole
                        if (canonicalized.toLowerCase(Locale.ENGLISH).startsWith(
                                hostName.toLowerCase(Locale.ENGLISH) + ".")) {
                            hostName = canonicalized;
                        }
                    } catch (UnknownHostException e) {
                        // not canonicalized, use old
                    }
                    if (hostName.endsWith(".")) {
                        hostName = hostName.substring(0, hostName.length() - 1);
                    }
                }
                nameParts[1] = hostName.toLowerCase(Locale.ENGLISH);
            }
            nameStrings = nameParts;
            nameType = type;

            if (realm != null) {
                nameRealm = new Realm(realm);
            } else {
                // We will try to get realm name from the mapping in
                // the configuration. If it is not specified
                // we will use the default realm. This nametype does
                // not allow a realm to be specified. The name string must of
                // the form service@host and this is internally changed into
                // service/host by Kerberos
                String mapRealm =  mapHostToRealm(nameParts[1]);
                if (mapRealm != null) {
                    nameRealm = new Realm(mapRealm);
                } else {
                    nameRealm = Realm.getDefault();
                }
            }
            break;
        case KRB_NT_UNKNOWN:
        case KRB_NT_PRINCIPAL:
        case KRB_NT_SRV_INST:
        case KRB_NT_SRV_XHST:
        case KRB_NT_UID:
        case KRB_NT_ENTERPRISE:
            nameStrings = nameParts;
            nameType = type;
            if (realm != null) {
                nameRealm = new Realm(realm);
            } else {
                nameRealm = Realm.getDefault();
            }
            break;
        default:
            throw new IllegalArgumentException("Illegal name type");
        }
    }

    // Warning: called by nativeccache.c
    public PrincipalName(String name, int type) throws RealmException {
        this(name, type, (String)null);
    }

    public PrincipalName(String name) throws RealmException {
        this(name, KRB_NT_UNKNOWN);
    }

    public PrincipalName(String name, String realm) throws RealmException {
        this(name, KRB_NT_UNKNOWN, realm);
    }

    public static PrincipalName tgsService(String r1, String r2)
            throws KrbException {
        return new PrincipalName(PrincipalName.KRB_NT_SRV_INST,
                new String[] {PrincipalName.TGS_DEFAULT_SRV_NAME, r1},
                new Realm(r2));
    }

    public String getRealmAsString() {
        return getRealmString();
    }

    public String getPrincipalNameAsString() {
        StringBuilder temp = new StringBuilder(nameStrings[0]);
        for (int i = 1; i < nameStrings.length; i++)
            temp.append(nameStrings[i]);
        return temp.toString();
    }

    @Override
    public int hashCode() {
        return toString().hashCode();
    }

    public String getName() {
        return toString();
    }

    public int getNameType() {
        return nameType;
    }

    public String[] getNameStrings() {
        return nameStrings.clone();
    }

    public byte[][] toByteArray() {
        byte[][] result = new byte[nameStrings.length][];
        for (int i = 0; i < nameStrings.length; i++) {
            result[i] = nameStrings[i].getBytes();
        }
        return result;
    }

    public String getRealmString() {
        return nameRealm.toString();
    }

    public Realm getRealm() {
        return nameRealm;
    }

    public String getSalt() {
        if (salt == null) {
            StringBuilder salt = new StringBuilder();
            salt.append(nameRealm.toString());
            for (int i = 0; i < nameStrings.length; i++) {
                salt.append(nameStrings[i]);
            }
            return salt.toString();
        }
        return salt;
    }

    public String toString() {
        StringBuilder str = new StringBuilder();
        for (int i = 0; i < nameStrings.length; i++) {
            if (i > 0)
                str.append("/");
            String n = nameStrings[i];
            n = n.replace("@", "\\@");
            str.append(n);
        }
        str.append("@");
        str.append(nameRealm.toString());
        return str.toString();
    }

    public String getNameString() {
        StringBuilder str = new StringBuilder();
        for (int i = 0; i < nameStrings.length; i++) {
            if (i > 0)
                str.append("/");
            str.append(nameStrings[i]);
        }
        return str.toString();
    }

    /**
     * Encodes a <code>PrincipalName</code> object. Note that only the type and
     * names are encoded. To encode the realm, call getRealm().asn1Encode().
     * @return the byte array of the encoded PrncipalName object.
     * @exception Asn1Exception if an error occurs while decoding an ASN1 encoded data.
     * @exception IOException if an I/O error occurs while reading encoded data.
     *
     */
    public byte[] asn1Encode() throws Asn1Exception, IOException {
        DerOutputStream bytes = new DerOutputStream();
        DerOutputStream temp = new DerOutputStream();
        BigInteger bint = BigInteger.valueOf(this.nameType);
        temp.putInteger(bint);
        bytes.write(DerValue.createTag(DerValue.TAG_CONTEXT, true, (byte)0x00), temp);
        temp = new DerOutputStream();
        DerValue[] der = new DerValue[nameStrings.length];
        for (int i = 0; i < nameStrings.length; i++) {
            der[i] = new KerberosString(nameStrings[i]).toDerValue();
        }
        temp.putSequence(der);
        bytes.write(DerValue.createTag(DerValue.TAG_CONTEXT, true, (byte)0x01), temp);
        temp = new DerOutputStream();
        temp.write(DerValue.tag_Sequence, bytes);
        return temp.toByteArray();
    }


    /**
     * Checks if two <code>PrincipalName</code> objects have identical values
     * in their corresponding data fields.
     * <p>
     * If {@systemProperty jdk.security.krb5.name.case.sensitive} is set to true,
     * the name comparison is case-sensitive. Otherwise, it's case-insensitive.
     * <p>
     * It is used in {@link sun.security.krb5.internal.ccache.FileCredentialsCache}
     * and {@link sun.security.krb5.internal.ktab.KeyTab} to retrieve ccache
     * or keytab entry for a principal.
     *
     * @param pname the other <code>PrincipalName</code> object.
     * @return true if two have identical values, otherwise, return false.
     */
    public boolean match(PrincipalName pname) {
        // No need to check name type. It's just a hint, no two names can be
        // the same ignoring name type.
        if (NAME_CASE_SENSITIVE_IN_MATCH) {
            if (!(this.nameRealm.toString().equals(pname.nameRealm.toString()))) {
                return false;
            }
        } else {
            if (!(this.nameRealm.toString().equalsIgnoreCase(pname.nameRealm.toString()))) {
                return false;
            }
        }
        if (this.nameStrings.length != pname.nameStrings.length) {
            return false;
        } else {
            for (int i = 0; i < this.nameStrings.length; i++) {
                if (NAME_CASE_SENSITIVE_IN_MATCH) {
                    if (!(this.nameStrings[i].equals(pname.nameStrings[i]))) {
                        return false;
                    }
                } else {
                    if (!(this.nameStrings[i].equalsIgnoreCase(pname.nameStrings[i]))) {
                        return false;
                    }
                }
            }
        }
        return true;
    }

    /**
     * Writes data field values of <code>PrincipalName</code> in FCC format to an output stream.
     *
     * @param cos a <code>CCacheOutputStream</code> for writing data.
     * @exception IOException if an I/O exception occurs.
     * @see sun.security.krb5.internal.ccache.CCacheOutputStream
     */
    public void writePrincipal(CCacheOutputStream cos) throws IOException {
        cos.write32(nameType);
        cos.write32(nameStrings.length);
        byte[] realmBytes = null;
        realmBytes = nameRealm.toString().getBytes();
        cos.write32(realmBytes.length);
        cos.write(realmBytes, 0, realmBytes.length);
        byte[] bytes = null;
        for (int i = 0; i < nameStrings.length; i++) {
            bytes = nameStrings[i].getBytes();
            cos.write32(bytes.length);
            cos.write(bytes, 0, bytes.length);
        }
    }

    /**
     * Returns the instance component of a name.
     * In a multi-component name such as a KRB_NT_SRV_INST
     * name, the second component is returned.
     * Null is returned if there are not two or more
     * components in the name.
     *
     * @return instance component of a multi-component name.
     */
    public String getInstanceComponent()
    {
        if (nameStrings != null && nameStrings.length >= 2)
            {
                return new String(nameStrings[1]);
            }

        return null;
    }

    static String mapHostToRealm(String name) {
        String result = null;
        try {
            String subname = null;
            Config c = Config.getInstance();
            if ((result = c.get("domain_realm", name)) != null)
                return result;
            else {
                for (int i = 1; i < name.length(); i++) {
                    if ((name.charAt(i) == '.') && (i != name.length() - 1)) { //mapping could be .ibm.com = AUSTIN.IBM.COM
                        subname = name.substring(i);
                        result = c.get("domain_realm", subname);
                        if (result != null) {
                            break;
                        }
                        else {
                            subname = name.substring(i + 1);      //or mapping could be ibm.com = AUSTIN.IBM.COM
                            result = c.get("domain_realm", subname);
                            if (result != null) {
                                break;
                            }
                        }
                    }
                }
            }
        } catch (KrbException e) {
        }
        return result;
    }
}
