/*
 * openjavacard-tools: Development tools for JavaCard
 * Copyright (C) 2018 Ingo Albrecht <[email protected]>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3.0 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 */

package org.openjavacard.gp.keys;

import org.openjavacard.gp.crypto.GPCrypto;
import org.openjavacard.util.HexUtil;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

public class GPKey {

    /** GlobalPlatform default master secret */
    public static final byte[] GLOBALPLATFORM_MASTER_SECRET = {
            0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47,
            0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F
    };

    /** GlobalPlatform default master key */
    public static final GPKey GLOBALPLATFORM_MASTER = new GPKey(
            0, GPKeyUsage.MASTER, GPKeyCipher.GENERIC,
            GLOBALPLATFORM_MASTER_SECRET);

    /** Key id of this key */
    private final int mId;

    /** Usage type of this key */
    private final GPKeyUsage mUsage;

    /** Cipher that this key is intended for */
    private final GPKeyCipher mCipher;

    /** Secret corresponding to this key */
    private final byte[] mSecret;

    /**
     * Constructs a key object for the provided data
     *
     * @param keyId of the key
     * @param usage  of the key
     * @param cipher of the key
     * @param secret the key itself
     */
    public GPKey(int keyId, GPKeyUsage usage, GPKeyCipher cipher, byte[] secret) {
        GPKeyId.checkKeyId(keyId);
        mId = keyId;
        mUsage = usage;
        mCipher = cipher;
        mSecret = secret.clone();
        checkKeyLength();
    }

    /**
     * @return key id of this key
     */
    public int getId() {
        return mId;
    }

    /**
     * @return type of this key
     */
    public GPKeyUsage getUsage() {
        return mUsage;
    }

    /**
     * @return cipher of this key
     */
    public GPKeyCipher getCipher() {
        return mCipher;
    }

    /**
     * @return key bytes of this key
     */
    public byte[] getSecret() {
        return mSecret.clone();
    }

    /**
     * @return length of secret in bytes
     */
    public int getLength() {
        return mSecret.length;
    }

    /**
     * @return true if key is compatible with given cipher
     * @param cipher to check compatibility with
     */
    private boolean isCompatible(GPKeyCipher cipher) {
        return (mCipher == GPKeyCipher.GENERIC)
                || (mCipher == cipher)
                || (mCipher == GPKeyCipher.DES3 && cipher == GPKeyCipher.DES);
    }

    /**
     * Return the key check value (KCV) for the key
     * <p/>
     * The algorithm used depends on the cipher of the key.
     * <p/>
     * @param cipher to use
     * @return the key check value
     */
    public byte[] getCheckValue(GPKeyCipher cipher) {
        if(!isCompatible(cipher)) {
            throw new UnsupportedOperationException("Cannot use " + mCipher + " key with cipher " + cipher);
        }
        switch(cipher) {
            case DES3:
                return GPCrypto.kcv_3des(this);
            case AES:
                return GPCrypto.kcv_aes(this);
            default:
                throw new UnsupportedOperationException("Cannot generate KCV for cipher " + cipher);
        }
    }

    /**
     * Return a SecretKey for a specific cipher
     * <p/>
     * Will coerce the key if required, such as for GENERIC keys.
     * <p/>
     * @param cipher for the new key
     * @return the secret key
     */
    public SecretKey getSecretKey(GPKeyCipher cipher) {
        if(!isCompatible(cipher)) {
            throw new UnsupportedOperationException("Cannot use " + mCipher + " key with cipher " + cipher);
        }
        switch (cipher) {
            case DES:
                return new SecretKeySpec(enlarge(mSecret, 8), "DES");
            case DES3:
                return new SecretKeySpec(enlarge(mSecret, 24), "DESede");
            case AES:
                return new SecretKeySpec(mSecret, "AES");
            default:
                throw new IllegalArgumentException("Cannot make secret key for cipher " + cipher);
        }
    }

    private byte[] enlarge(byte[] key, int length) {
        int secretLen = key.length;
        if(length == secretLen) {
            return key;
        }
        if(length == 8) {
            byte[] key8 = new byte[8];
            switch(secretLen) {
                case 8:
                case 16:
                case 24:
                    System.arraycopy(key, 0, key8, 0, 8);
                    return key8;
            }
        }
        if(length == 24) {
            byte[] key24 = new byte[24];
            switch(secretLen) {
                case 8:
                    System.arraycopy(key, 0, key24,  0, 8);
                    System.arraycopy(key, 0, key24,  8, 8);
                    System.arraycopy(key, 0, key24, 16, 8);
                    return key24;
                case 16:
                    System.arraycopy(key, 0, key24,  0, 16);
                    System.arraycopy(key, 0, key24, 16,  8);
                    return key24;
                case 24:
                    System.arraycopy(key, 0, key24, 0, 24);
                    return key24;
            }
        }
        throw new Error("Do not know how to coerce DES key from length " + secretLen + " to length " + length);
    }

    /**
     * Internal: check that key length is appropriate
     */
    private void checkKeyLength() {
        int length = mSecret.length;
        switch (mCipher) {
            case GENERIC:
                if(length % 8 != 0) {
                    throw new IllegalArgumentException("Bad key length");
                }
                if(length > 32) {
                    throw new IllegalArgumentException("Key to long");
                }
                break;
            case DES:
                if(length != 8) {
                    throw new IllegalArgumentException("DES keys must be 8 bytes long");
                }
                break;
            case DES3:
                if(length != 8 && length != 16 && length != 24) {
                    throw new IllegalArgumentException("3DES keys must be [8,16,24] bytes long");
                }
                break;
            case AES:
                if(length != 16) {
                    throw new IllegalArgumentException("AES keys must be 16 bytes long");
                }
                break;
        }
    }

    public String toString() {
        return "key id " + (mId==0?"any":mId)
                + " usage " + mUsage
                + " cipher " + mCipher
                + " secret " + HexUtil.bytesToHex(mSecret);
    }

}