/*
 * @(#)CipherFactory.java
 *
 * Copyright (c) 2001-2004 JScape
 * 1147 S. 53rd Pl., Mesa, Arizona, 85206, U.S.A.
 * All rights reserved.
 *
 * This software is the confidential and proprietary information of
 * JScape. ("Confidential Information").  You shall not disclose such
 * Confidential Information and shall use it only in accordance with
 * the terms of the license agreement you entered into with JScape.
 */
package com.jscape.inet.ssh.util.keyreader;

import com.jscape.util.Assert;

import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Locale;

/**
 * Cipher factory. Holds various cipher-related information.
 *
 * @author Alex Usun
 */
@SuppressWarnings("unchecked")
public class CipherFactory {

    private static CipherFactory instance;

    private final HashMap entries = new HashMap();

    /**
     * Returns global object instance.
     *
     * @return global object instance
     */
    public static synchronized CipherFactory getGlobalInstance() {
        if (instance == null) {
            instance = new CipherFactory();
        }

        return instance;
    }


    /**
     * Private constructor to avoid unnecessary instantiation.
     */
    public CipherFactory() {
        // empty
    }

    /**
     * Copy constructor.
     *
     * @param that a factory to copy.
     */
    public CipherFactory(CipherFactory that) {
        Assert.notNull(that);
        this.entries.putAll(that.entries);
    }

    /**
     * Adds a new cipher entry.
     *
     * @param name    entry name
     * @param jceName full JCE cipher name (with encryption type and padding)
     * @param keySize cipher key size
     *
     * @throws IllegalArgumentException if entry already exists
     */
    public void add(String name, String jceName, int keySize) {
        Assert.isTrue(!contains(name), "cipher already added");
        this.entries.put(name.toUpperCase(Locale.ENGLISH), new Entry(name, jceName, keySize));
    }

    /**
     * Removes cipher entry.
     *
     * @param name entry to remove
     *
     * @throws IllegalArgumentException if entry is not present
     */
    public void remove(String name) {
        Assert.isTrue(contains(name), "no such cipher");
        this.entries.remove(name.toUpperCase(Locale.ENGLISH));
    }

    /**
     * Clears current entries.
     */
    public void clear() {
        this.entries.clear();
    }

    /**
     * Checks if entry is present in factory.
     *
     * @param name entry name
     *
     * @return <code>true</code> if entry is present; <code>false</code> otherwise
     */
    public boolean contains(String name) {
        return this.entries.containsKey(name.toUpperCase(Locale.ENGLISH));
    }

    /**
     * Creates a new cipher instance for the specified entry name.
     *
     * @param name entry name
     *
     * @return a new cipher entry
     *
     * @throws NoSuchAlgorithmException if cipher algorithm is not supported
     * @throws NoSuchPaddingException   if cipher padding type is not supported
     */
    public Cipher createCipher(String name)
            throws NoSuchAlgorithmException, NoSuchPaddingException {
        return getEntry(name).createCipher();
    }

    public Cipher createEncipher(String name, IKeyFactory keyFactory)
            throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException,
                   InvalidKeyException {
        return getEntry(name).createCipher(keyFactory, Cipher.ENCRYPT_MODE);
    }

    public Cipher createDecipher(String name, IKeyFactory keyFactory)
            throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException,
                   InvalidKeyException {
        return getEntry(name).createCipher(keyFactory, Cipher.DECRYPT_MODE);
    }

    /**
     * Returns JCE cipher algorithm (without encryption type and padding).
     *
     * @param name entry name
     *
     * @return cipher algorithm
     */
    public String getAlgorithm(String name) {
        return getEntry(name).getAlgorithm();
    }

    /**
     * Returns key size for the specified entry.
     *
     * @param name entry name
     *
     * @return cipher key size
     */
    public int getKeySize(String name) {
        return getEntry(name).getKeySize();
    }

    /**
     * Returns cipher entry.
     *
     * @param name entry name
     *
     * @return cipher name
     *
     * @throws IllegalArgumentException if entry is not present
     */
    private Entry getEntry(String name) {
        Assert.isTrue(contains(name), "no such cipher");
        return (Entry) this.entries.get(name.toUpperCase(Locale.ENGLISH));
    }

    /**
     * Cipher entry.
     */
    public static class Entry {

        private static final char SEPARATOR = '/';

        private final String name;
        private final String algorithm;
        private final String jceName;
        private final int keySize;

        /**
         * Creates a new entry instance.
         *
         * @param name          entry name
         * @param fullAlgorithm full JCE cipher algorithm
         * @param keySize
         */
        public Entry(String name, String fullAlgorithm, int keySize) {
            Assert.isValidString(name);
            Assert.isValidString(fullAlgorithm);
            Assert.greater(keySize, 0, "bad key size");

            this.name = name;
            this.jceName = fullAlgorithm;
            this.keySize = keySize;

            int index = this.jceName.indexOf(SEPARATOR);
            this.algorithm = (index >= 0) ? this.jceName.substring(0, index) : this.jceName;
        }

        /**
         * Returns entry name.
         *
         * @return entry name
         */
        public String getName() {
            return this.name;
        }

        /**
         * Returns cipher algorithm name.
         *
         * @return cipher algorithm name
         */
        public String getAlgorithm() {
            return this.algorithm;
        }

        /**
         * Returns full JCE cipher name.
         *
         * @return full JCE cipher name
         */
        public String getJceName() {
            return this.jceName;
        }

        /**
         * Returns cipher key size.
         *
         * @return cipher key size
         */
        public int getKeySize() {
            return this.keySize;
        }

        /**
         * Creates cipher instance.
         *
         * @return a new cipher instance
         *
         * @throws NoSuchAlgorithmException if cipher algorithm is not supported
         * @throws NoSuchPaddingException   if cipher padding type is not supported
         */
        public Cipher createCipher()
                throws NoSuchAlgorithmException, NoSuchPaddingException {
            return Cipher.getInstance(this.jceName);
        }

        public Cipher createCipher(IKeyFactory keyFactory, int opmode)
                throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException,
                       InvalidKeyException {
            Cipher cipher = Cipher.getInstance(this.jceName);
            Key key = keyFactory.createKey(this.algorithm, this.keySize);
            cipher.init(opmode, key, new IvParameterSpec(keyFactory.createIv(cipher.getBlockSize())));

            return cipher;
        }

    }   // Entry

}

