/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package javax.crypto;

import java.io.IOException;
import java.security.AlgorithmParameters;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Provider;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import org.apache.harmony.security.asn1.ASN1Any;
import org.apache.harmony.security.asn1.ASN1Implicit;
import org.apache.harmony.security.asn1.ASN1Integer;
import org.apache.harmony.security.asn1.ASN1OctetString;
import org.apache.harmony.security.asn1.ASN1Sequence;
import org.apache.harmony.security.asn1.ASN1SetOf;
import org.apache.harmony.security.asn1.ASN1Type;
import org.apache.harmony.security.utils.AlgNameMapper;
import org.apache.harmony.security.x509.AlgorithmIdentifier;


/**
 * This class implements the {@code EncryptedPrivateKeyInfo} ASN.1 type as
 * specified in <a href="http://www.ietf.org/rfc/rfc5208.txt">PKCS
 * #8 - Private-Key Information Syntax Standard</a>.
 * <p>
 * The definition of ASN.1 is as follows:
 * <dl>
 * EncryptedPrivateKeyInfo ::= SEQUENCE {
 * <dd>encryptionAlgorithm AlgorithmIdentifier,</dd>
 * <dd>encryptedData OCTET STRING }</dd>
 * </dl>
 * <dl>
 * AlgorithmIdentifier ::= SEQUENCE {
 * <dd>algorithm OBJECT IDENTIFIER,</dd>
 * <dd>parameters ANY DEFINED BY algorithm OPTIONAL }</dd>
 * </dl>
 */
public class EncryptedPrivateKeyInfo {
    // Encryption algorithm name
    private String algName;
    // Encryption algorithm parameters
    private final AlgorithmParameters algParameters;
    // Encrypted private key data
    private final byte[] encryptedData;
    // Encryption algorithm OID
    private String oid;
    // This EncryptedPrivateKeyInfo ASN.1 DER encoding
    private volatile byte[] encoded;

    /**
     * Creates an {@code EncryptedPrivateKeyInfo} instance from its encoded
     * representation by parsing it.
     *
     * @param encoded
     *            the encoded representation of this object
     * @throws IOException
     *             if parsing the encoded representation fails.
     * @throws NullPointerException
     *             if {@code encoded} is {@code null}.
     */
    public EncryptedPrivateKeyInfo(byte[] encoded) throws IOException {
        if (encoded == null) {
            throw new NullPointerException("encoded == null");
        }
        this.encoded = new byte[encoded.length];
        System.arraycopy(encoded, 0, this.encoded, 0, encoded.length);
        Object[] values;

        values = (Object[])asn1.decode(encoded);

        AlgorithmIdentifier aId = (AlgorithmIdentifier) values[0];

        algName = aId.getAlgorithm();
        // algName == oid now
        boolean mappingExists = mapAlgName();
        // algName == name from map oid->name if mapping exists, or
        // algName == oid if mapping does not exist

        AlgorithmParameters aParams = null;
        byte[] params = aId.getParameters();
        if (params != null && !isNullValue(params)) {
            try {
                aParams = AlgorithmParameters.getInstance(algName);
                aParams.init(aId.getParameters());
                if (!mappingExists) {
                    algName = aParams.getAlgorithm();
                }
            } catch (NoSuchAlgorithmException e) {
            }
        }
        algParameters = aParams;

        encryptedData = (byte[]) values[1];
    }

    private static boolean isNullValue(byte[] toCheck) {
        return toCheck[0] == 5 && toCheck[1] == 0;
    }

    /**
     * Creates an {@code EncryptedPrivateKeyInfo} instance from an algorithm
     * name and its encrypted data.
     *
     * @param encrAlgName
     *            the name of an algorithm.
     * @param encryptedData
     *            the encrypted data.
     * @throws NoSuchAlgorithmException
     *             if the {@code encrAlgName} is not a supported algorithm.
     * @throws NullPointerException
     *             if {@code encrAlgName} or {@code encryptedData} is {@code
     *             null}.
     * @throws IllegalArgumentException
     *             if {@code encryptedData} is empty.
     */
    public EncryptedPrivateKeyInfo(String encrAlgName, byte[] encryptedData)
        throws NoSuchAlgorithmException {
        if (encrAlgName == null) {
            throw new NullPointerException("the algName parameter is null");
        }
        this.algName = encrAlgName;
        if (!mapAlgName()) {
            throw new NoSuchAlgorithmException("Unsupported algorithm: " + this.algName);
        }
        if (encryptedData == null) {
            throw new NullPointerException("encryptedData == null");
        }
        if (encryptedData.length == 0) {
            throw new IllegalArgumentException("encryptedData.length == 0");
        }
        this.encryptedData = new byte[encryptedData.length];
        System.arraycopy(encryptedData, 0,
                this.encryptedData, 0, encryptedData.length);
        this.algParameters = null;
    }

    /**
     * Creates an {@code EncryptedPrivateKeyInfo} instance from the
     * encryption algorithm parameters an its encrypted data.
     *
     * @param algParams
     *            the encryption algorithm parameters.
     * @param encryptedData
     *            the encrypted data.
     * @throws NoSuchAlgorithmException
     *             if the algorithm name of the specified {@code algParams}
     *             parameter is not supported.
     * @throws NullPointerException
     *             if {@code algParams} or {@code encryptedData} is
     *             {@code null}.
     */
    public EncryptedPrivateKeyInfo(AlgorithmParameters algParams, byte[] encryptedData)
        throws NoSuchAlgorithmException {
        if (algParams == null) {
            throw new NullPointerException("algParams == null");
        }
        this.algParameters = algParams;
        if (encryptedData == null) {
            throw new NullPointerException("encryptedData == null");
        }
        if (encryptedData.length == 0) {
            throw new IllegalArgumentException("encryptedData.length == 0");
        }
        this.encryptedData = new byte[encryptedData.length];
        System.arraycopy(encryptedData, 0,
                this.encryptedData, 0, encryptedData.length);
        this.algName = this.algParameters.getAlgorithm();
        if (!mapAlgName()) {
            throw new NoSuchAlgorithmException("Unsupported algorithm: " + this.algName);
        }
    }

    /**
     * Returns the name of the encryption algorithm.
     *
     * @return the name of the encryption algorithm.
     */
    public String getAlgName() {
        return algName;
    }

    /**
     * Returns the parameters used by the encryption algorithm.
     *
     * @return the parameters used by the encryption algorithm.
     */
    public AlgorithmParameters getAlgParameters() {
        return algParameters;
    }

    /**
     * Returns the encrypted data of this key.
     *
     * @return the encrypted data of this key, each time this method is called a
     *         new array is returned.
     */
    public byte[] getEncryptedData() {
        byte[] ret = new byte[encryptedData.length];
        System.arraycopy(encryptedData, 0, ret, 0, encryptedData.length);
        return ret;
    }

    /**
     * Returns the {@code PKCS8EncodedKeySpec} object extracted from the
     * encrypted data.
     * <p>
     * The cipher must be initialize in either {@code Cipher.DECRYPT_MODE} or
     * {@code Cipher.UNWRAP_MODE} with the same parameters and key used for
     * encrypting this.
     *
     * @param cipher
     *            the cipher initialized for decrypting the encrypted data.
     * @return the extracted {@code PKCS8EncodedKeySpec}.
     * @throws InvalidKeySpecException
     *             if the specified cipher is not suited to decrypt the
     *             encrypted data.
     * @throws NullPointerException
     *             if {@code cipher} is {@code null}.
     */
    public PKCS8EncodedKeySpec getKeySpec(Cipher cipher)
        throws InvalidKeySpecException {
        if (cipher == null) {
            throw new NullPointerException("cipher == null");
        }
        try {
            byte[] decryptedData = cipher.doFinal(encryptedData);
            try {
                ASN1PrivateKeyInfo.verify(decryptedData);
            } catch (IOException e1) {
                throw new InvalidKeySpecException("Decrypted data does not represent valid PKCS#8 PrivateKeyInfo");
            }
            return new PKCS8EncodedKeySpec(decryptedData);
        } catch (IllegalStateException e) {
            throw new InvalidKeySpecException(e.getMessage());
        } catch (IllegalBlockSizeException e) {
            throw new InvalidKeySpecException(e.getMessage());
        } catch (BadPaddingException e) {
            throw new InvalidKeySpecException(e.getMessage());
        }
    }

    /**
     * Returns the {@code PKCS8EncodedKeySpec} object extracted from the
     * encrypted data.
     *
     * @param decryptKey
     *            the key to decrypt the encrypted data with.
     * @return the extracted {@code PKCS8EncodedKeySpec}.
     * @throws NoSuchAlgorithmException
     *             if no usable cipher can be found to decrypt the encrypted
     *             data.
     * @throws InvalidKeyException
     *             if {@code decryptKey} is not usable to decrypt the encrypted
     *             data.
     * @throws NullPointerException
     *             if {@code decryptKey} is {@code null}.
     */
    public PKCS8EncodedKeySpec getKeySpec(Key decryptKey) throws NoSuchAlgorithmException,
               InvalidKeyException {
        if (decryptKey == null) {
            throw new NullPointerException("decryptKey == null");
        }
        try {
            Cipher cipher = Cipher.getInstance(algName);
            if (algParameters == null) {
                cipher.init(Cipher.DECRYPT_MODE, decryptKey);
            } else {
                cipher.init(Cipher.DECRYPT_MODE, decryptKey, algParameters);
            }
            byte[] decryptedData = cipher.doFinal(encryptedData);
            try {
                ASN1PrivateKeyInfo.verify(decryptedData);
            } catch (IOException e1) {
                throw invalidKey();
            }
            return new PKCS8EncodedKeySpec(decryptedData);
        } catch (NoSuchPaddingException e) {
            throw new NoSuchAlgorithmException(e.getMessage());
        } catch (InvalidAlgorithmParameterException e) {
            throw new NoSuchAlgorithmException(e.getMessage());
        } catch (IllegalStateException e) {
            throw new InvalidKeyException(e.getMessage());
        } catch (IllegalBlockSizeException e) {
            throw new InvalidKeyException(e.getMessage());
        } catch (BadPaddingException e) {
            throw new InvalidKeyException(e.getMessage());
        }
    }

    /**
     * Returns the {@code PKCS8EncodedKeySpec} object extracted from the
     * encrypted data.
     *
     * @param decryptKey
     *            the key to decrypt the encrypted data with.
     * @param providerName
     *            the name of a provider whose cipher implementation should be
     *            used.
     * @return the extracted {@code PKCS8EncodedKeySpec}.
     * @throws NoSuchProviderException
     *             if no provider with {@code providerName} can be found.
     * @throws NoSuchAlgorithmException
     *             if no usable cipher can be found to decrypt the encrypted
     *             data.
     * @throws InvalidKeyException
     *             if {@code decryptKey} is not usable to decrypt the encrypted
     *             data.
     * @throws NullPointerException
     *             if {@code decryptKey} or {@code providerName} is {@code null}
     *             .
     */
    public PKCS8EncodedKeySpec getKeySpec(Key decryptKey, String providerName)
        throws NoSuchProviderException,
               NoSuchAlgorithmException,
               InvalidKeyException {
        if (decryptKey == null) {
            throw new NullPointerException("decryptKey == null");
        }
        if (providerName == null) {
            throw new NullPointerException("providerName == null");
        }
        try {
            Cipher cipher = Cipher.getInstance(algName, providerName);
            if (algParameters == null) {
                cipher.init(Cipher.DECRYPT_MODE, decryptKey);
            } else {
                cipher.init(Cipher.DECRYPT_MODE, decryptKey, algParameters);
            }
            byte[] decryptedData = cipher.doFinal(encryptedData);
            try {
                ASN1PrivateKeyInfo.verify(decryptedData);
            } catch (IOException e1) {
                throw invalidKey();
            }
            return new PKCS8EncodedKeySpec(decryptedData);
        } catch (NoSuchPaddingException e) {
            throw new NoSuchAlgorithmException(e.getMessage());
        } catch (InvalidAlgorithmParameterException e) {
            throw new NoSuchAlgorithmException(e.getMessage());
        } catch (IllegalStateException e) {
            throw new InvalidKeyException(e.getMessage());
        } catch (IllegalBlockSizeException e) {
            throw new InvalidKeyException(e.getMessage());
        } catch (BadPaddingException e) {
            throw new InvalidKeyException(e.getMessage());
        }
    }

    /**
     * Returns the {@code PKCS8EncodedKeySpec} object extracted from the
     * encrypted data.
     *
     * @param decryptKey
     *            the key to decrypt the encrypted data with.
     * @param provider
     *            the provider whose cipher implementation should be used.
     * @return the extracted {@code PKCS8EncodedKeySpec}.
     * @throws NoSuchAlgorithmException
     *             if no usable cipher can be found to decrypt the encrypted
     *             data.
     * @throws InvalidKeyException
     *             if {@code decryptKey} is not usable to decrypt the encrypted
     *             data.
     * @throws NullPointerException
     *             if {@code decryptKey} or {@code provider} is {@code null}.
     */
    public PKCS8EncodedKeySpec getKeySpec(Key decryptKey, Provider provider)
        throws NoSuchAlgorithmException,
               InvalidKeyException {
        if (decryptKey == null) {
            throw new NullPointerException("decryptKey == null");
        }
        if (provider == null) {
            throw new NullPointerException("provider == null");
        }
        try {
            Cipher cipher = Cipher.getInstance(algName, provider);
            if (algParameters == null) {
                cipher.init(Cipher.DECRYPT_MODE, decryptKey);
            } else {
                cipher.init(Cipher.DECRYPT_MODE, decryptKey, algParameters);
            }
            byte[] decryptedData = cipher.doFinal(encryptedData);
            try {
                ASN1PrivateKeyInfo.verify(decryptedData);
            } catch (IOException e1) {
                throw invalidKey();
            }
            return new PKCS8EncodedKeySpec(decryptedData);
        } catch (NoSuchPaddingException e) {
            throw new NoSuchAlgorithmException(e.getMessage());
        } catch (InvalidAlgorithmParameterException e) {
            throw new NoSuchAlgorithmException(e.getMessage());
        } catch (IllegalStateException e) {
            throw new InvalidKeyException(e.getMessage());
        } catch (IllegalBlockSizeException e) {
            throw new InvalidKeyException(e.getMessage());
        } catch (BadPaddingException e) {
            throw new InvalidKeyException(e.getMessage());
        }
    }

    private InvalidKeyException invalidKey() throws InvalidKeyException {
        throw new InvalidKeyException("Decrypted data does not represent valid PKCS#8 PrivateKeyInfo");
    }

    /**
     * Returns the ASN.1 encoded representation of this object.
     *
     * @return the ASN.1 encoded representation of this object.
     * @throws IOException
     *             if encoding this object fails.
     */
    public byte[] getEncoded() throws IOException {
        if (encoded == null) {
            // Generate ASN.1 encoding:
            encoded = asn1.encode(this);
        }
        byte[] ret = new byte[encoded.length];
        System.arraycopy(encoded, 0, ret, 0, encoded.length);
        return ret;
    }

    // Performs all needed alg name mappings.
    // Returns 'true' if mapping available 'false' otherwise
    private boolean mapAlgName() {
        if (AlgNameMapper.isOID(this.algName)) {
            // OID provided to the ctor
            // get rid of possible leading "OID."
            this.oid = AlgNameMapper.normalize(this.algName);
            // try to find mapping OID->algName
            this.algName = AlgNameMapper.map2AlgName(this.oid);
            // if there is no mapping OID->algName
            // set OID as algName
            if (this.algName == null) {
                this.algName = this.oid;
            }
        } else {
            String stdName = AlgNameMapper.getStandardName(this.algName);
            // Alg name provided to the ctor
            // try to find mapping algName->OID or
            // (algName->stdAlgName)->OID
            this.oid = AlgNameMapper.map2OID(this.algName);
            if (this.oid == null) {
                if (stdName == null) {
                    // no above mappings available
                    return false;
                }
                this.oid = AlgNameMapper.map2OID(stdName);
                if (this.oid == null) {
                    return false;
                }
                this.algName = stdName;
            } else if (stdName != null) {
                this.algName = stdName;
            }
        }
        return true;
    }

    //
    // EncryptedPrivateKeyInfo DER encoder/decoder.
    // EncryptedPrivateKeyInfo ASN.1 definition
    // (as defined in PKCS #8: Private-Key Information Syntax Standard
    //  http://www.ietf.org/rfc/rfc2313.txt)
    //
    // EncryptedPrivateKeyInfo ::=  SEQUENCE {
    //      encryptionAlgorithm   AlgorithmIdentifier,
    //      encryptedData   OCTET STRING }
    //

    private static final byte[] nullParam = new byte[] { 5, 0 };

    private static final ASN1Sequence asn1 = new ASN1Sequence(new ASN1Type[] {
            AlgorithmIdentifier.ASN1, ASN1OctetString.getInstance() }) {

                @Override
                protected void getValues(Object object, Object[] values) {

                    EncryptedPrivateKeyInfo epki = (EncryptedPrivateKeyInfo) object;

                    try {
                        byte[] algParmsEncoded = (epki.algParameters == null) ? nullParam
                                : epki.algParameters.getEncoded();
                        values[0] = new AlgorithmIdentifier(epki.oid, algParmsEncoded);
                        values[1] = epki.encryptedData;
                    } catch (IOException e) {
                        throw new RuntimeException(e.getMessage());
                    }
                }
    };

    // PrivateKeyInfo DER decoder.
    // PrivateKeyInfo ASN.1 definition
    // (as defined in PKCS #8: Private-Key Information Syntax Standard
    //  http://www.ietf.org/rfc/rfc2313.txt)
    //
    //
    //    PrivateKeyInfo ::= SEQUENCE {
    //        version Version,
    //        privateKeyAlgorithm PrivateKeyAlgorithmIdentifier,
    //        privateKey PrivateKey,
    //        attributes [0] IMPLICIT Attributes OPTIONAL }
    //
    //      Version ::= INTEGER
    //
    //      PrivateKeyAlgorithmIdentifier ::= AlgorithmIdentifier
    //
    //      PrivateKey ::= OCTET STRING
    //
    //      Attributes ::= SET OF Attribute

    private static final ASN1SetOf ASN1Attributes = new ASN1SetOf(ASN1Any.getInstance());

    private static final ASN1Sequence ASN1PrivateKeyInfo = new ASN1Sequence(
            new ASN1Type[] { ASN1Integer.getInstance(), AlgorithmIdentifier.ASN1,
                    ASN1OctetString.getInstance(),
                    new ASN1Implicit(0, ASN1Attributes) }) {
        {
            setOptional(3); //attributes are optional
        }
    };
}
