/*
 * Copyright (c) 2000, 2013, 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.jgss.krb5.Krb5Util;
import sun.security.util.*;
import sun.security.krb5.internal.*;
import sun.security.krb5.internal.crypto.*;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.Arrays;
import sun.security.krb5.internal.ktab.KeyTab;
import sun.security.krb5.internal.ccache.CCacheOutputStream;
import javax.crypto.spec.DESKeySpec;
import javax.crypto.spec.DESedeKeySpec;

/**
 * This class encapsulates the concept of an EncryptionKey. An encryption
 * key is defined in RFC 4120 as:
 *
 * EncryptionKey   ::= SEQUENCE {
 *         keytype         [0] Int32 -- actually encryption type --,
 *         keyvalue        [1] OCTET STRING
 * }
 *
 * keytype
 *     This field specifies the encryption type of the encryption key
 *     that follows in the keyvalue field.  Although its name is
 *     "keytype", it actually specifies an encryption type.  Previously,
 *     multiple cryptosystems that performed encryption differently but
 *     were capable of using keys with the same characteristics were
 *     permitted to share an assigned number to designate the type of
 *     key; this usage is now deprecated.
 *
 * keyvalue
 *     This field contains the key itself, encoded as an octet string.
 */

public class EncryptionKey
    implements Cloneable {

    public static final EncryptionKey NULL_KEY =
        new EncryptionKey(new byte[] {}, EncryptedData.ETYPE_NULL, null);

    private int keyType;
    private byte[] keyValue;
    private Integer kvno; // not part of ASN1 encoding;

    private static final boolean DEBUG = Krb5.DEBUG;

    public synchronized int getEType() {
        return keyType;
    }

    public final Integer getKeyVersionNumber() {
        return kvno;
    }

    /**
     * Returns the raw key bytes, not in any ASN.1 encoding.
     */
    public final byte[] getBytes() {
        // This method cannot be called outside sun.security, hence no
        // cloning. getEncoded() calls this method.
        return keyValue;
    }

    public synchronized Object clone() {
        return new EncryptionKey(keyValue, keyType, kvno);
    }

    /**
     * Obtains all versions of the secret key of the principal from a
     * keytab.
     *
     * @param princ the principal whose secret key is desired
     * @param keytab the path to the keytab file. A value of null
     * will be accepted to indicate that the default path should be
     * searched.
     * @return an array of secret keys or null if none were found.
     */
    public static EncryptionKey[] acquireSecretKeys(PrincipalName princ,
                                                    String keytab) {

        if (princ == null)
            throw new IllegalArgumentException(
                "Cannot have null pricipal name to look in keytab.");

        // KeyTab getInstance(keytab) will call KeyTab.getInstance()
        // if keytab is null
        KeyTab ktab = KeyTab.getInstance(keytab);
        return ktab.readServiceKeys(princ);
    }

    /**
     * Obtains a key for a given etype of a principal with possible new salt
     * and s2kparams
     * @param cname NOT null
     * @param password NOT null
     * @param etype
     * @param snp can be NULL
     * @return never null
     */
    public static EncryptionKey acquireSecretKey(PrincipalName cname,
            char[] password, int etype, PAData.SaltAndParams snp)
            throws KrbException {
        String salt;
        byte[] s2kparams;
        if (snp != null) {
            salt = snp.salt != null ? snp.salt : cname.getSalt();
            s2kparams = snp.params;
        } else {
            salt = cname.getSalt();
            s2kparams = null;
        }
        return acquireSecretKey(password, salt, etype, s2kparams);
    }

    /**
     * Obtains a key for a given etype with salt and optional s2kparams
     * @param password NOT null
     * @param salt NOT null
     * @param etype
     * @param s2kparams can be NULL
     * @return never null
     */
    public static EncryptionKey acquireSecretKey(char[] password,
            String salt, int etype, byte[] s2kparams)
            throws KrbException {

        return new EncryptionKey(
                        stringToKey(password, salt, s2kparams, etype),
                        etype, null);
    }

    /**
     * Generate a list of keys using the given principal and password.
     * Construct a key for each configured etype.
     * Caller is responsible for clearing password.
     */
    /*
     * Usually, when keyType is decoded from ASN.1 it will contain a
     * value indicating what the algorithm to be used is. However, when
     * converting from a password to a key for the AS-EXCHANGE, this
     * keyType will not be available. Use builtin list of default etypes
     * as the default in that case. If default_tkt_enctypes was set in
     * the libdefaults of krb5.conf, then use that sequence.
     */
    public static EncryptionKey[] acquireSecretKeys(char[] password,
            String salt) throws KrbException {

        int[] etypes = EType.getDefaults("default_tkt_enctypes");

        EncryptionKey[] encKeys = new EncryptionKey[etypes.length];
        for (int i = 0; i < etypes.length; i++) {
            if (EType.isSupported(etypes[i])) {
                encKeys[i] = new EncryptionKey(
                        stringToKey(password, salt, null, etypes[i]),
                        etypes[i], null);
            } else {
                if (DEBUG) {
                    System.out.println("Encryption Type " +
                        EType.toString(etypes[i]) +
                        " is not supported/enabled");
                }
            }
        }
        return encKeys;
    }

    // Used in Krb5AcceptCredential, self
    public EncryptionKey(byte[] keyValue,
                         int keyType,
                         Integer kvno) {

        if (keyValue != null) {
            this.keyValue = new byte[keyValue.length];
            System.arraycopy(keyValue, 0, this.keyValue, 0, keyValue.length);
        } else {
            throw new IllegalArgumentException("EncryptionKey: " +
                                               "Key bytes cannot be null!");
        }
        this.keyType = keyType;
        this.kvno = kvno;
    }

    /**
     * Constructs an EncryptionKey by using the specified key type and key
     * value.  It is used to recover the key when retrieving data from
     * credential cache file.
     *
     */
     // Used in JSSE (KerberosWrapper), Credentials,
     // javax.security.auth.kerberos.KeyImpl
    public EncryptionKey(int keyType,
                         byte[] keyValue) {
        this(keyValue, keyType, null);
    }

    private static byte[] stringToKey(char[] password, String salt,
        byte[] s2kparams, int keyType) throws KrbCryptoException {

        char[] slt = salt.toCharArray();
        char[] pwsalt = new char[password.length + slt.length];
        System.arraycopy(password, 0, pwsalt, 0, password.length);
        System.arraycopy(slt, 0, pwsalt, password.length, slt.length);
        Arrays.fill(slt, '0');

        try {
            switch (keyType) {
                case EncryptedData.ETYPE_DES_CBC_CRC:
                case EncryptedData.ETYPE_DES_CBC_MD5:
                        return Des.string_to_key_bytes(pwsalt);

                case EncryptedData.ETYPE_DES3_CBC_HMAC_SHA1_KD:
                        return Des3.stringToKey(pwsalt);

                case EncryptedData.ETYPE_ARCFOUR_HMAC:
                        return ArcFourHmac.stringToKey(password);

                case EncryptedData.ETYPE_AES128_CTS_HMAC_SHA1_96:
                        return Aes128.stringToKey(password, salt, s2kparams);

                case EncryptedData.ETYPE_AES256_CTS_HMAC_SHA1_96:
                        return Aes256.stringToKey(password, salt, s2kparams);

                default:
                        throw new IllegalArgumentException("encryption type " +
                        EType.toString(keyType) + " not supported");
            }

        } catch (GeneralSecurityException e) {
            KrbCryptoException ke = new KrbCryptoException(e.getMessage());
            ke.initCause(e);
            throw ke;
        } finally {
            Arrays.fill(pwsalt, '0');
        }
    }

    // Used in javax.security.auth.kerberos.KeyImpl
    public EncryptionKey(char[] password,
                         String salt,
                         String algorithm) throws KrbCryptoException {

        if (algorithm == null || algorithm.equalsIgnoreCase("DES")) {
            keyType = EncryptedData.ETYPE_DES_CBC_MD5;
        } else if (algorithm.equalsIgnoreCase("DESede")) {
            keyType = EncryptedData.ETYPE_DES3_CBC_HMAC_SHA1_KD;
        } else if (algorithm.equalsIgnoreCase("AES128")) {
            keyType = EncryptedData.ETYPE_AES128_CTS_HMAC_SHA1_96;
        } else if (algorithm.equalsIgnoreCase("ArcFourHmac")) {
            keyType = EncryptedData.ETYPE_ARCFOUR_HMAC;
        } else if (algorithm.equalsIgnoreCase("AES256")) {
            keyType = EncryptedData.ETYPE_AES256_CTS_HMAC_SHA1_96;
            // validate if AES256 is enabled
            if (!EType.isSupported(keyType)) {
                throw new IllegalArgumentException("Algorithm " + algorithm +
                        " not enabled");
            }
        } else {
            throw new IllegalArgumentException("Algorithm " + algorithm +
                " not supported");
        }

        keyValue = stringToKey(password, salt, null, keyType);
        kvno = null;
    }

    /**
     * Generates a sub-sessionkey from a given session key.
     *
     * Used in AcceptSecContextToken and KrbApReq by acceptor- and initiator-
     * side respectively.
     */
    public EncryptionKey(EncryptionKey key) throws KrbCryptoException {
        // generate random sub-session key
        keyValue = Confounder.bytes(key.keyValue.length);
        for (int i = 0; i < keyValue.length; i++) {
          keyValue[i] ^= key.keyValue[i];
        }
        keyType = key.keyType;

        // check for key parity and weak keys
        try {
            // check for DES key
            if ((keyType == EncryptedData.ETYPE_DES_CBC_MD5) ||
                (keyType == EncryptedData.ETYPE_DES_CBC_CRC)) {
                // fix DES key parity
                if (!DESKeySpec.isParityAdjusted(keyValue, 0)) {
                    keyValue = Des.set_parity(keyValue);
                }
                // check for weak key
                if (DESKeySpec.isWeak(keyValue, 0)) {
                    keyValue[7] = (byte)(keyValue[7] ^ 0xF0);
                }
            }
            // check for 3DES key
            if (keyType == EncryptedData.ETYPE_DES3_CBC_HMAC_SHA1_KD) {
                // fix 3DES key parity
                if (!DESedeKeySpec.isParityAdjusted(keyValue, 0)) {
                    keyValue = Des3.parityFix(keyValue);
                }
                // check for weak keys
                byte[] oneKey = new byte[8];
                for (int i=0; i<keyValue.length; i+=8) {
                    System.arraycopy(keyValue, i, oneKey, 0, 8);
                    if (DESKeySpec.isWeak(oneKey, 0)) {
                        keyValue[i+7] = (byte)(keyValue[i+7] ^ 0xF0);
                    }
                }
            }
        } catch (GeneralSecurityException e) {
            KrbCryptoException ke = new KrbCryptoException(e.getMessage());
            ke.initCause(e);
            throw ke;
        }
    }

    /**
     * Constructs an instance of EncryptionKey type.
     * @param encoding a single DER-encoded value.
     * @exception Asn1Exception if an error occurs while decoding an ASN1
     * encoded data.
     * @exception IOException if an I/O error occurs while reading encoded
     * data.
     *
     *
     */
         // Used in javax.security.auth.kerberos.KeyImpl
    public EncryptionKey(DerValue encoding) throws Asn1Exception, IOException {
        DerValue der;
        if (encoding.getTag() != DerValue.tag_Sequence) {
            throw new Asn1Exception(Krb5.ASN1_BAD_ID);
        }
        der = encoding.getData().getDerValue();
        if ((der.getTag() & (byte)0x1F) == (byte)0x00) {
            keyType = der.getData().getBigInteger().intValue();
        }
        else
            throw new Asn1Exception(Krb5.ASN1_BAD_ID);
        der = encoding.getData().getDerValue();
        if ((der.getTag() & (byte)0x1F) == (byte)0x01) {
            keyValue = der.getData().getOctetString();
        }
        else
            throw new Asn1Exception(Krb5.ASN1_BAD_ID);
        if (der.getData().available() > 0) {
            throw new Asn1Exception(Krb5.ASN1_BAD_ID);
        }
    }

    /**
     * Returns the ASN.1 encoding of this EncryptionKey.
     *
     * <pre>{@code
     * EncryptionKey ::=   SEQUENCE {
     *                             keytype[0]    INTEGER,
     *                             keyvalue[1]   OCTET STRING }
     * }</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>.
     *
     * @return byte array of encoded EncryptionKey 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 synchronized byte[] asn1Encode() throws Asn1Exception, IOException {
        DerOutputStream bytes = new DerOutputStream();
        DerOutputStream temp = new DerOutputStream();
        temp.putInteger(keyType);
        bytes.write(DerValue.createTag(DerValue.TAG_CONTEXT, true,
                                       (byte)0x00), temp);
        temp = new DerOutputStream();
        temp.putOctetString(keyValue);
        bytes.write(DerValue.createTag(DerValue.TAG_CONTEXT, true,
                                       (byte)0x01), temp);
        temp = new DerOutputStream();
        temp.write(DerValue.tag_Sequence, bytes);
        return temp.toByteArray();
    }

    public synchronized void destroy() {
        if (keyValue != null)
            for (int i = 0; i < keyValue.length; i++)
                keyValue[i] = 0;
    }


    /**
     * Parse (unmarshal) an Encryption key 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.
     *
     * @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
     * @exception Asn1Exception if an error occurs while decoding an ASN1
     * encoded data.
     * @exception IOException if an I/O error occurs while reading encoded
     * data.
     * @return an instance of EncryptionKey.
     *
     */
    public static EncryptionKey parse(DerInputStream data, byte
                                      explicitTag, boolean optional) throws
                                      Asn1Exception, IOException {
        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();
            return new EncryptionKey(subDer);
        }
    }

    /**
     * Writes key value in FCC format to a <code>CCacheOutputStream</code>.
     *
     * @param cos a <code>CCacheOutputStream</code> to be written to.
     * @exception IOException if an I/O exception occurs.
     * @see sun.security.krb5.internal.ccache.CCacheOutputStream
     *
     */
    public synchronized void writeKey(CCacheOutputStream cos)
        throws IOException {

        cos.write16(keyType);
        // we use KRB5_FCC_FVNO_3
        cos.write16(keyType); // key type is recorded twice.
        cos.write32(keyValue.length);
        for (int i = 0; i < keyValue.length; i++) {
            cos.write8(keyValue[i]);
        }
    }

    public String toString() {
        return new String("EncryptionKey: keyType=" + keyType
                          + ", kvno=" + kvno + ", " + Krb5Util.keyInfo(keyValue));
    }

    /**
     * Find a key with given etype
     */
    public static EncryptionKey findKey(int etype, EncryptionKey[] keys)
            throws KrbException {
        return findKey(etype, null, keys);
    }

    /**
     * Determines if a kvno matches another kvno. Used in the method
     * findKey(type, kvno, keys). Always returns true if either input
     * is null or zero, in case any side does not have kvno info available.
     *
     * Note: zero is included because N/A is not a legal value for kvno
     * in javax.security.auth.kerberos.KerberosKey. Therefore, the info
     * that the kvno is N/A might be lost when converting between this
     * class and KerberosKey.
     */
    private static boolean versionMatches(Integer v1, Integer v2) {
        if (v1 == null || v1 == 0 || v2 == null || v2 == 0) {
            return true;
        }
        return v1.equals(v2);
    }

    /**
     * Find a key with given etype and kvno
     * @param kvno if null, return any (first?) key
     */
    public static EncryptionKey findKey(int etype, Integer kvno, EncryptionKey[] keys)
        throws KrbException {

        // check if encryption type is supported
        if (!EType.isSupported(etype)) {
            throw new KrbException("Encryption type " +
                EType.toString(etype) + " is not supported/enabled");
        }

        int ktype;
        boolean etypeFound = false;

        // When no matched kvno is found, returns tke key of the same
        // etype with the highest kvno
        int kvno_found = 0;
        EncryptionKey key_found = null;

        for (int i = 0; i < keys.length; i++) {
            ktype = keys[i].getEType();
            if (EType.isSupported(ktype)) {
                Integer kv = keys[i].getKeyVersionNumber();
                if (etype == ktype) {
                    etypeFound = true;
                    if (versionMatches(kvno, kv)) {
                        return keys[i];
                    } else if (kv > kvno_found) {
                        // kv is not null
                        key_found = keys[i];
                        kvno_found = kv;
                    }
                }
            }
        }

        // Key not found.
        // allow DES key to be used for the DES etypes
        if ((etype == EncryptedData.ETYPE_DES_CBC_CRC ||
            etype == EncryptedData.ETYPE_DES_CBC_MD5)) {
            for (int i = 0; i < keys.length; i++) {
                ktype = keys[i].getEType();
                if (ktype == EncryptedData.ETYPE_DES_CBC_CRC ||
                        ktype == EncryptedData.ETYPE_DES_CBC_MD5) {
                    Integer kv = keys[i].getKeyVersionNumber();
                    etypeFound = true;
                    if (versionMatches(kvno, kv)) {
                        return new EncryptionKey(etype, keys[i].getBytes());
                    } else if (kv > kvno_found) {
                        key_found = new EncryptionKey(etype, keys[i].getBytes());
                        kvno_found = kv;
                    }
                }
            }
        }
        if (etypeFound) {
            return key_found;
            // For compatibility, will not fail here.
            //throw new KrbException(Krb5.KRB_AP_ERR_BADKEYVER);
        }
        return null;
    }
}
