/***** BEGIN LICENSE BLOCK *****
 * Version: EPL 1.0/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Eclipse Public
 * License Version 1.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.eclipse.org/legal/epl-v10.html
 *
 * Software distributed under the License is distributed on an "AS
 * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * rights and limitations under the License.
 *
 * Copyright (C) 2010 Hiroshi Nakamura <nahi@ruby-lang.org>
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either of the GNU General Public License Version 2 or later (the "GPL"),
 * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the EPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the EPL, the GPL or the LGPL.
 ***** END LICENSE BLOCK *****/
package org.jruby.ext.openssl.impl;

import java.io.IOException;
import java.math.BigInteger;

import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.interfaces.DSAParams;
import java.security.interfaces.DSAPrivateKey;
import java.security.interfaces.DSAPublicKey;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAPrivateCrtKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.DSAPrivateKeySpec;
import java.security.spec.DSAPublicKeySpec;
import java.security.spec.ECParameterSpec;
import java.security.spec.ECPrivateKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.RSAPrivateCrtKeySpec;
import java.security.spec.RSAPublicKeySpec;
import java.security.spec.X509EncodedKeySpec;
import javax.crypto.spec.DHParameterSpec;

import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.DLSequence;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.asn1.sec.ECPrivateKeyStructure;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.asn1.x9.X9ObjectIdentifiers;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
import org.bouncycastle.jce.spec.ECPublicKeySpec;

import org.jruby.ext.openssl.SecurityHelper;

/**
 *
 * Handles PKey related ASN.1 handling.
 *
 * @author <a href="mailto:nahi@ruby-lang.org">Hiroshi Nakamura</a>
 */
public class PKey {

    public static KeyPair readPrivateKey(final byte[] input, final String type)
        throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
        KeySpec pubSpec; KeySpec privSpec;
        ASN1Sequence seq = (ASN1Sequence) new ASN1InputStream(input).readObject();
        if ( type.equals("RSA") ) {
            ASN1Integer mod = (ASN1Integer) seq.getObjectAt(1);
            ASN1Integer pubExp = (ASN1Integer) seq.getObjectAt(2);
            ASN1Integer privExp = (ASN1Integer) seq.getObjectAt(3);
            ASN1Integer p1 = (ASN1Integer) seq.getObjectAt(4);
            ASN1Integer p2 = (ASN1Integer) seq.getObjectAt(5);
            ASN1Integer exp1 = (ASN1Integer) seq.getObjectAt(6);
            ASN1Integer exp2 = (ASN1Integer) seq.getObjectAt(7);
            ASN1Integer crtCoef = (ASN1Integer) seq.getObjectAt(8);
            pubSpec = new RSAPublicKeySpec(mod.getValue(), pubExp.getValue());
            privSpec = new RSAPrivateCrtKeySpec(mod.getValue(), pubExp.getValue(), privExp.getValue(), p1.getValue(), p2.getValue(), exp1.getValue(),
                    exp2.getValue(), crtCoef.getValue());
        }
        else if ( type.equals("DSA") ) {
            ASN1Integer p = (ASN1Integer) seq.getObjectAt(1);
            ASN1Integer q = (ASN1Integer) seq.getObjectAt(2);
            ASN1Integer g = (ASN1Integer) seq.getObjectAt(3);
            ASN1Integer y = (ASN1Integer) seq.getObjectAt(4);
            ASN1Integer x = (ASN1Integer) seq.getObjectAt(5);
            privSpec = new DSAPrivateKeySpec(x.getValue(), p.getValue(), q.getValue(), g.getValue());
            pubSpec = new DSAPublicKeySpec(y.getValue(), p.getValue(), q.getValue(), g.getValue());
        }
        else if ( type.equals("ECDSA") ) {
            return readECPrivateKey(input);
        }
        else {
            throw new IllegalStateException("unsupported type: " + type);
        }
        KeyFactory fact = SecurityHelper.getKeyFactory(type);
        return new KeyPair(fact.generatePublic(pubSpec), fact.generatePrivate(privSpec));
    }

    // d2i_PrivateKey_bio
    public static KeyPair readPrivateKey(byte[] input) throws IOException,
        NoSuchAlgorithmException, InvalidKeySpecException {
        KeyPair key = null;
        try {
            key = readRSAPrivateKey(input);
        }
        catch (NoSuchAlgorithmException e) { throw e; /* should not happen */ }
        catch (InvalidKeySpecException e) {
            // ignore
        }
        if (key == null) {
            try {
                key = readDSAPrivateKey(input);
            }
            catch (NoSuchAlgorithmException e) { throw e; /* should not happen */ }
            catch (InvalidKeySpecException e) {
                // ignore
            }
        }
        return key;
    }

    // d2i_PUBKEY_bio
    public static PublicKey readPublicKey(byte[] input) throws IOException,
        NoSuchAlgorithmException, InvalidKeySpecException {
        PublicKey key = null;
        try {
            key = readRSAPublicKey(input);
        }
        catch (NoSuchAlgorithmException e) { throw e; /* should not happen */ }
        catch (InvalidKeySpecException e) {
            // ignore
        }
        if (key == null) {
            try {
                key = readDSAPublicKey(input);
            }
            catch (NoSuchAlgorithmException e) { throw e; /* should not happen */ }
            catch (InvalidKeySpecException e) {
                // ignore
            }
        }
        return key;
    }

    // d2i_RSAPrivateKey_bio
    public static KeyPair readRSAPrivateKey(final byte[] input)
        throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
        return readRSAPrivateKey(SecurityHelper.getKeyFactory("RSA"), input);
    }

    public static KeyPair readRSAPrivateKey(final KeyFactory rsaFactory, final byte[] input)
        throws IOException, InvalidKeySpecException {
        ASN1Sequence seq = (ASN1Sequence) new ASN1InputStream(input).readObject();
        if ( seq.size() == 9 ) {
            BigInteger mod = ((ASN1Integer) seq.getObjectAt(1)).getValue();
            BigInteger pubexp = ((ASN1Integer) seq.getObjectAt(2)).getValue();
            BigInteger privexp = ((ASN1Integer) seq.getObjectAt(3)).getValue();
            BigInteger primep = ((ASN1Integer) seq.getObjectAt(4)).getValue();
            BigInteger primeq = ((ASN1Integer) seq.getObjectAt(5)).getValue();
            BigInteger primeep = ((ASN1Integer) seq.getObjectAt(6)).getValue();
            BigInteger primeeq = ((ASN1Integer) seq.getObjectAt(7)).getValue();
            BigInteger crtcoeff = ((ASN1Integer) seq.getObjectAt(8)).getValue();
            PrivateKey priv = rsaFactory.generatePrivate(new RSAPrivateCrtKeySpec(mod, pubexp, privexp, primep, primeq, primeep, primeeq, crtcoeff));
            PublicKey pub = rsaFactory.generatePublic(new RSAPublicKeySpec(mod, pubexp));
            return new KeyPair(pub, priv);
        }
        return null;
    }

    // d2i_RSAPublicKey_bio
    public static PublicKey readRSAPublicKey(final byte[] input)
        throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
        return readRSAPublicKey(SecurityHelper.getKeyFactory("RSA"), input);
    }

    public static PublicKey readRSAPublicKey(final KeyFactory rsaFactory, final byte[] input)
        throws IOException, InvalidKeySpecException {
        ASN1Sequence seq = (ASN1Sequence) new ASN1InputStream(input).readObject();
        if ( seq.size() == 2 ) {
            BigInteger mod = ((ASN1Integer) seq.getObjectAt(0)).getValue();
            BigInteger pubexp = ((ASN1Integer) seq.getObjectAt(1)).getValue();
            return rsaFactory.generatePublic(new RSAPublicKeySpec(mod, pubexp));
        }
        return null;
    }

    // d2i_DSAPrivateKey_bio
    public static KeyPair readDSAPrivateKey(final byte[] input)
        throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
        return readDSAPrivateKey(SecurityHelper.getKeyFactory("DSA"), input);
    }

    public static KeyPair readDSAPrivateKey(final KeyFactory dsaFactory, final byte[] input)
        throws IOException, InvalidKeySpecException {
        ASN1Sequence seq = (ASN1Sequence) new ASN1InputStream(input).readObject();
        if ( seq.size() == 6 ) {
            BigInteger p = ((ASN1Integer) seq.getObjectAt(1)).getValue();
            BigInteger q = ((ASN1Integer) seq.getObjectAt(2)).getValue();
            BigInteger g = ((ASN1Integer) seq.getObjectAt(3)).getValue();
            BigInteger y = ((ASN1Integer) seq.getObjectAt(4)).getValue();
            BigInteger x = ((ASN1Integer) seq.getObjectAt(5)).getValue();
            PrivateKey priv = dsaFactory.generatePrivate(new DSAPrivateKeySpec(x, p, q, g));
            PublicKey pub = dsaFactory.generatePublic(new DSAPublicKeySpec(y, p, q, g));
            return new KeyPair(pub, priv);
        }
        return null;
    }

    // d2i_DSA_PUBKEY_bio
    public static PublicKey readDSAPublicKey(final byte[] input)
        throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
        return readDSAPublicKey(SecurityHelper.getKeyFactory("DSA"), input);
    }

    public static PublicKey readDSAPublicKey(final KeyFactory dsaFactory, final byte[] input)
        throws IOException, InvalidKeySpecException {
        ASN1Sequence seq = (ASN1Sequence) new ASN1InputStream(input).readObject();
        if ( seq.size() == 4 ) {
            BigInteger y = ((ASN1Integer) seq.getObjectAt(0)).getValue();
            BigInteger p = ((ASN1Integer) seq.getObjectAt(1)).getValue();
            BigInteger q = ((ASN1Integer) seq.getObjectAt(2)).getValue();
            BigInteger g = ((ASN1Integer) seq.getObjectAt(3)).getValue();
            return dsaFactory.generatePublic(new DSAPublicKeySpec(y, p, q, g));
        }
        return null;
    }

    // d2i_DHparams_bio
    public static DHParameterSpec readDHParameter(final byte[] input) throws IOException {
        ASN1InputStream aIn = new ASN1InputStream(input);
        ASN1Sequence seq = (ASN1Sequence) aIn.readObject();
        BigInteger p = ((ASN1Integer) seq.getObjectAt(0)).getValue();
        BigInteger g = ((ASN1Integer) seq.getObjectAt(1)).getValue();
        return new DHParameterSpec(p, g);
    }

    public static KeyPair readECPrivateKey(final byte[] input)
        throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
        return readECPrivateKey(SecurityHelper.getKeyFactory("ECDSA"), input);
    }

    public static KeyPair readECPrivateKey(final KeyFactory ecFactory, final byte[] input)
        throws IOException, InvalidKeySpecException {
        try {
            ECPrivateKeyStructure pKey = new ECPrivateKeyStructure((ASN1Sequence) ASN1Primitive.fromByteArray(input));
            AlgorithmIdentifier   algId = new AlgorithmIdentifier(X9ObjectIdentifiers.id_ecPublicKey, pKey.getParameters());
            PrivateKeyInfo        privInfo = new PrivateKeyInfo(algId, pKey.toASN1Primitive());
            SubjectPublicKeyInfo  pubInfo = new SubjectPublicKeyInfo(algId, pKey.getPublicKey().getBytes());
            PKCS8EncodedKeySpec   privSpec = new PKCS8EncodedKeySpec(privInfo.getEncoded());
            X509EncodedKeySpec    pubSpec = new X509EncodedKeySpec(pubInfo.getEncoded());
            //KeyFactory            fact = KeyFactory.getInstance("ECDSA", provider);

            ECPrivateKey privateKey = (ECPrivateKey) ecFactory.generatePrivate(privSpec);
            if ( algId.getParameters() instanceof ASN1ObjectIdentifier ) {
                privateKey = ECPrivateKeyWithName.wrap(privateKey, (ASN1ObjectIdentifier) algId.getParameters());
            }
            return new KeyPair(ecFactory.generatePublic(pubSpec), privateKey);
        }
        catch (ClassCastException ex) {
            throw new IOException("wrong ASN.1 object found in stream", ex);
        }
        //catch (Exception ex) {
        //    throw new IOException("problem parsing EC private key: " + ex);
        //}
    }

    public static byte[] toDerRSAKey(RSAPublicKey pubKey, RSAPrivateCrtKey privKey) throws IOException {
        ASN1EncodableVector vec = new ASN1EncodableVector();
        if ( pubKey != null && privKey == null ) {
            vec.add(new ASN1Integer(pubKey.getModulus()));
            vec.add(new ASN1Integer(pubKey.getPublicExponent()));
        }
        else {
            vec.add(new ASN1Integer(BigInteger.ZERO));
            vec.add(new ASN1Integer(privKey.getModulus()));
            vec.add(new ASN1Integer(privKey.getPublicExponent()));
            vec.add(new ASN1Integer(privKey.getPrivateExponent()));
            vec.add(new ASN1Integer(privKey.getPrimeP()));
            vec.add(new ASN1Integer(privKey.getPrimeQ()));
            vec.add(new ASN1Integer(privKey.getPrimeExponentP()));
            vec.add(new ASN1Integer(privKey.getPrimeExponentQ()));
            vec.add(new ASN1Integer(privKey.getCrtCoefficient()));
        }
        return new DLSequence(vec).getEncoded();
    }

    public static byte[] toDerDSAKey(DSAPublicKey pubKey, DSAPrivateKey privKey) throws IOException {
        if ( pubKey != null && privKey == null ) {
            return pubKey.getEncoded();
        }
        if ( privKey != null && pubKey != null ) {
            ASN1EncodableVector vec = new ASN1EncodableVector();
            final DSAParams params = privKey.getParams();
            vec.add(new ASN1Integer(BigInteger.ZERO));
            vec.add(new ASN1Integer(params.getP()));
            vec.add(new ASN1Integer(params.getQ()));
            vec.add(new ASN1Integer(params.getG()));
            vec.add(new ASN1Integer(pubKey.getY()));
            vec.add(new ASN1Integer(privKey.getX()));
            return new DLSequence(vec).getEncoded();
        }
        if ( privKey == null ) {
            throw new IllegalArgumentException("private key as well as public key are null");
        }
        return privKey.getEncoded();
    }

    public static byte[] toDerDHKey(BigInteger p, BigInteger g) throws IOException {
        ASN1EncodableVector vec = new ASN1EncodableVector();
        if ( p != null ) vec.add( new ASN1Integer(p) );
        if ( g != null ) vec.add( new ASN1Integer(g) );
        return new DLSequence(vec).getEncoded();
    }
}


