/***** 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) 2006, 2007 Ola Bini <ola@ologix.com>
 * Copyright (C) 2007 Wiliam N Dortch <bill.dortch@gmail.com>
 *
 * 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;

import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.math.BigInteger;
import java.security.*;
import java.security.interfaces.DSAKey;
import java.security.interfaces.DSAPrivateKey;
import java.security.interfaces.DSAPublicKey;
import java.security.spec.DSAPrivateKeySpec;
import java.security.spec.DSAPublicKeySpec;
import java.security.spec.InvalidKeySpecException;

import org.jruby.Ruby;
import org.jruby.RubyBoolean;
import org.jruby.RubyClass;
import org.jruby.RubyFixnum;
import org.jruby.RubyModule;
import org.jruby.RubyNumeric;
import org.jruby.RubyString;
import org.jruby.anno.JRubyMethod;
import org.jruby.exceptions.RaiseException;
import org.jruby.ext.openssl.impl.CipherSpec;
import org.jruby.ext.openssl.x509store.PEMInputOutput;
import org.jruby.runtime.Arity;
import org.jruby.runtime.ObjectAllocator;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.Visibility;
import org.jruby.util.ByteList;

import static org.jruby.ext.openssl.OpenSSL.*;
import static org.jruby.ext.openssl.impl.PKey.readDSAPrivateKey;
import static org.jruby.ext.openssl.impl.PKey.readDSAPublicKey;
import static org.jruby.ext.openssl.impl.PKey.toDerDSAKey;
import static org.jruby.ext.openssl.PKey._PKey;

/**
 * @author <a href="mailto:ola.bini@ki.se">Ola Bini</a>
 */
public class PKeyDSA extends PKey {
    private static final long serialVersionUID = 6351851846414049890L;

    private static final ObjectAllocator ALLOCATOR = new ObjectAllocator() {
        public PKeyDSA allocate(Ruby runtime, RubyClass klass) { return new PKeyDSA(runtime, klass); }
    };

    public static void createPKeyDSA(final Ruby runtime, final RubyModule PKey, final RubyClass PKeyPKey) {
        RubyClass DSA = PKey.defineClassUnder("DSA", PKeyPKey, ALLOCATOR);
        RubyClass PKeyError = PKey.getClass("PKeyError");
        PKey.defineClassUnder("DSAError", PKeyError, PKeyError.getAllocator());

        DSA.defineAnnotatedMethods(PKeyDSA.class);
    }

    static RubyClass _DSA(final Ruby runtime) {
        return _PKey(runtime).getClass("DSA");
    }

    public PKeyDSA(Ruby runtime, RubyClass type) {
        super(runtime, type);
    }

    public PKeyDSA(Ruby runtime, RubyClass type, DSAPrivateKey privKey, DSAPublicKey pubKey) {
        super(runtime, type);
        this.privateKey = privKey;
        this.publicKey = pubKey;
    }

    PKeyDSA(Ruby runtime, DSAPublicKey pubKey) {
        this(runtime, _DSA(runtime), null, pubKey);
    }

    private volatile DSAPublicKey publicKey;
    private volatile transient DSAPrivateKey privateKey;

    // specValues holds individual DSAPublicKeySpec components. this allows
    // a public key to be constructed incrementally, as required by the
    // current implementation of Net::SSH.
    // (see net-ssh-1.1.2/lib/net/ssh/transport/ossl/buffer.rb #read_keyblob)
    private transient volatile BigInteger dsa_x;
    private transient volatile BigInteger dsa_y;
    private transient volatile BigInteger dsa_p;
    private transient volatile BigInteger dsa_q;
    private transient volatile BigInteger dsa_g;

    @Override
    public IRubyObject initialize_copy(final IRubyObject original) {
        if (this == original) return this;
        checkFrozen();

        final PKeyDSA that = (PKeyDSA) original;
        this.publicKey = that.publicKey;
        this.privateKey = that.privateKey;
        this.dsa_x = that.dsa_x;
        this.dsa_y = that.dsa_y;
        this.dsa_p = that.dsa_p;
        this.dsa_q = that.dsa_q;
        this.dsa_g = that.dsa_g;
        return this;
    }

    @Override
    public PublicKey getPublicKey() { return publicKey; }

    @Override
    public PrivateKey getPrivateKey() { return privateKey; }

    @Override
    public String getAlgorithm() { return "DSA"; }

    @JRubyMethod(name = "generate", meta = true)
    public static IRubyObject generate(IRubyObject self, IRubyObject arg) {
        final Ruby runtime = self.getRuntime();
        final int keySize = RubyNumeric.fix2int(arg);
        return dsaGenerate(runtime, new PKeyDSA(runtime, (RubyClass) self), keySize);
    }

    /*
     * c: dsa_generate
     */
    private static PKeyDSA dsaGenerate(final Ruby runtime,
        PKeyDSA dsa, int keySize) throws RaiseException {
        try {
            KeyPairGenerator gen = SecurityHelper.getKeyPairGenerator("DSA");
            gen.initialize(keySize, getSecureRandom(runtime));
            KeyPair pair = gen.generateKeyPair();
            dsa.privateKey = (DSAPrivateKey) pair.getPrivate();
            dsa.publicKey = (DSAPublicKey) pair.getPublic();
            return dsa;
        }
        catch (NoSuchAlgorithmException e) {
            throw newDSAError(runtime, e.getMessage());
        }
        catch (RuntimeException e) {
            throw newDSAError(runtime, e.getMessage(), e);
        }
    }

    static PKeyDSA newInstance(final Ruby runtime, final PublicKey publicKey) {
        //if ( publicKey instanceof DSAPublicKey ) {
        return new PKeyDSA(runtime, (DSAPublicKey) publicKey);
        //}
    }

    @JRubyMethod(rest = true, visibility = Visibility.PRIVATE)
    public IRubyObject initialize(final ThreadContext context, final IRubyObject[] args) {
        final Ruby runtime = context.runtime;

        if ( Arity.checkArgumentCount(runtime, args, 0, 2) == 0 ) {
            this.privateKey = null; this.publicKey = null; return this;
        }

        IRubyObject arg = args[0]; IRubyObject pass = null;
        if ( args.length > 1 ) pass = args[1];

        if ( arg instanceof RubyFixnum ) {
            int keySize = RubyNumeric.fix2int((RubyFixnum) arg);
            return dsaGenerate(context.runtime, this, keySize);
        }

        final char[] passwd = password(pass);
        final RubyString str = readInitArg(context, arg);
        final String strJava = str.toString();

        Object key = null;
        final KeyFactory dsaFactory;
        try {
            dsaFactory = SecurityHelper.getKeyFactory("DSA");
        }
        catch (NoSuchAlgorithmException e) {
            throw runtime.newRuntimeError("unsupported key algorithm (DSA)");
        }
        catch (RuntimeException e) {
            throw runtime.newRuntimeError("unsupported key algorithm (DSA) " + e);
        }
        // TODO: ugly NoClassDefFoundError catching for no BC env. How can we remove this?
        boolean noClassDef = false;
        if ( key == null && ! noClassDef ) { // PEM_read_bio_DSAPrivateKey
            try {
                key = readPrivateKey(strJava, passwd);
            }
            catch (NoClassDefFoundError e) { noClassDef = true; debugStackTrace(runtime, e); }
            catch (PEMInputOutput.PasswordRequiredException retry) {
                if ( ttySTDIN(context) ) {
                    try { key = readPrivateKey(strJava, passwordPrompt(context)); }
                    catch (Exception e) { debugStackTrace(runtime, e); }
                }
            }
            catch (Exception e) { debugStackTrace(runtime, e); }
        }
        if ( key == null && ! noClassDef ) { // PEM_read_bio_DSAPublicKey
            try {
                key = PEMInputOutput.readDSAPublicKey(new StringReader(strJava), passwd);
            }
            catch (NoClassDefFoundError e) { noClassDef = true; debugStackTrace(runtime, e); }
            catch (Exception e) { debugStackTrace(runtime, e); }
        }
        if ( key == null && ! noClassDef ) { // PEM_read_bio_DSA_PUBKEY
            try {
                key = PEMInputOutput.readDSAPubKey(new StringReader(strJava));
            }
            catch (NoClassDefFoundError e) { noClassDef = true; debugStackTrace(runtime, e); }
            catch (Exception e) { debugStackTrace(runtime, e); }
        }
        if ( key == null && ! noClassDef ) { // d2i_DSAPrivateKey_bio
            try {
                key = readDSAPrivateKey(dsaFactory, str.getBytes());
            }
            catch (NoClassDefFoundError e) { noClassDef = true; debugStackTrace(runtime, e); }
            catch (InvalidKeySpecException e) { debug(runtime, "PKeyDSA could not read private key", e); }
            catch (IOException e) { debug(runtime, "PKeyDSA could not read private key", e); }
            catch (RuntimeException e) {
                if ( isKeyGenerationFailure(e) ) debug(runtime, "PKeyDSA could not read private key", e);
                else debugStackTrace(runtime, e);
            }
        }
        if ( key == null && ! noClassDef ) { // d2i_DSA_PUBKEY_bio
            try {
                key = readDSAPublicKey(dsaFactory, str.getBytes());
            }
            catch (NoClassDefFoundError e) { noClassDef = true; debugStackTrace(runtime, e); }
            catch (InvalidKeySpecException e) { debug(runtime, "PKeyDSA could not read public key", e); }
            catch (IOException e) { debug(runtime, "PKeyDSA could not read public key", e); }
            catch (RuntimeException e) {
                if ( isKeyGenerationFailure(e) ) debug(runtime, "PKeyDSA could not read public key", e);
                else debugStackTrace(runtime, e);
            }
        }

        if ( key == null ) key = tryPKCS8EncodedKey(runtime, dsaFactory, str.getBytes());
        if ( key == null ) key = tryX509EncodedKey(runtime, dsaFactory, str.getBytes());

        if ( key == null ) throw newDSAError(runtime, "Neither PUB key nor PRIV key:");

        if ( key instanceof KeyPair ) {
            final PublicKey pubKey = ((KeyPair) key).getPublic();
            final PrivateKey privKey = ((KeyPair) key).getPrivate();
            if ( ! ( privKey instanceof DSAPrivateKey ) ) {
                if ( privKey == null ) {
                    throw newDSAError(runtime, "Neither PUB key nor PRIV key: (private key is null)");
                }
                throw newDSAError(runtime, "Neither PUB key nor PRIV key: (invalid key type " + privKey.getClass().getName() + ")");
            }
            this.privateKey = (DSAPrivateKey) privKey;
            this.publicKey = (DSAPublicKey) pubKey;
        }
        else if ( key instanceof DSAPrivateKey ) {
            this.privateKey = (DSAPrivateKey) key;
        }
        else if ( key instanceof DSAPublicKey ) {
            this.publicKey = (DSAPublicKey) key; this.privateKey = null;
        }
        else {
            throw newDSAError(runtime, "Neither PUB key nor PRIV key: "  + key.getClass().getName());
        }
        return this;
    }

    //private static Object readPrivateKey(final RubyString str, final char[] passwd)
    //    throws PEMInputOutput.PasswordRequiredException, IOException {
    //    return PEMInputOutput.readDSAPrivateKey(new StringReader(str.toString()), passwd);
    //}

    @JRubyMethod(name = "public?")
    public RubyBoolean public_p() {
        return publicKey != null ? getRuntime().getTrue() : getRuntime().getFalse();
    }

    @JRubyMethod(name = "private?")
    public RubyBoolean private_p() {
        return privateKey != null ? getRuntime().getTrue() : getRuntime().getFalse();
    }

    @Override
    @JRubyMethod(name = "to_der")
    public RubyString to_der() {
        final byte[] bytes;
        try {
            bytes = toDerDSAKey(publicKey, privateKey);
        }
        catch (NoClassDefFoundError e) {
            throw newDSAError(getRuntime(), bcExceptionMessage(e));
        }
        catch (IOException e) {
            throw newDSAError(getRuntime(), e.getMessage(), e);
        }
        return StringHelper.newString(getRuntime(), bytes);
    }

    @JRubyMethod
    public RubyString to_text() {
        StringBuilder result = new StringBuilder();
        if (privateKey != null) {
            int len = privateKey.getParams().getP().bitLength();
            result.append("Private-Key: (").append(len).append(" bit)").append("\n");
            result.append("priv:");
            addSplittedAndFormatted(result, privateKey.getX());
        }
        result.append("pub:");
        addSplittedAndFormatted(result, publicKey.getY());
        result.append("P:");
        addSplittedAndFormatted(result, publicKey.getParams().getP());
        result.append("Q:");
        addSplittedAndFormatted(result, publicKey.getParams().getQ());
        result.append("G:");
        addSplittedAndFormatted(result, publicKey.getParams().getG());
        return RubyString.newString(getRuntime(), result);
    }

    @JRubyMethod
    public PKeyDSA public_key() {
        return new PKeyDSA(getRuntime(), this.publicKey);
    }

    @Override
    @JRubyMethod(name = { "to_pem", "to_s" }, alias = "export", rest = true)
    public RubyString to_pem(final IRubyObject[] args) {
        Arity.checkArgumentCount(getRuntime(), args, 0, 2);

        CipherSpec spec = null; char[] passwd = null;
        if ( args.length > 0 ) {
            spec = cipherSpec( args[0] );
            if ( args.length > 1 ) passwd = password(args[1]);
        }

        try {
            final StringWriter writer = new StringWriter();
            if ( privateKey != null ) {
                PEMInputOutput.writeDSAPrivateKey(writer, privateKey, spec, passwd);
            }
            else {
                PEMInputOutput.writeDSAPublicKey(writer, publicKey);
            }
            return RubyString.newString(getRuntime(), writer.getBuffer());
        }
        catch (NoClassDefFoundError ncdfe) {
            throw newDSAError(getRuntime(), bcExceptionMessage(ncdfe));
        }
        catch (IOException e) {
            throw newDSAError(getRuntime(), e.getMessage(), e);
        }
    }

    @JRubyMethod // ossl_dsa_sign
    public IRubyObject syssign(IRubyObject data) {
        final Ruby runtime = getRuntime();

        DSAPrivateKey privateKey;
        if ((privateKey = this.privateKey) == null) {
            throw newDSAError(runtime, "Private DSA key needed!");
        }

        try {
            ByteList sign = sign("NONEwithDSA", privateKey, data.convertToString().getByteList()); // DSS1
            return RubyString.newString(runtime, sign);
        }
        catch (GeneralSecurityException ex) {
            throw newDSAError(runtime, ex.getMessage());
        }
    }

    @JRubyMethod // ossl_dsa_verify
    public IRubyObject sysverify(IRubyObject data, IRubyObject sign) {
        final Ruby runtime = getRuntime();
        ByteList sigBytes = convertToString(runtime, sign, "OpenSSL::PKey::DSAError", "invalid signature").getByteList();
        ByteList dataBytes = convertToString(runtime, data, "OpenSSL::PKey::DSAError", "invalid data").getByteList();
        try {
            return runtime.newBoolean( verify("NONEwithDSA", getPublicKey(), dataBytes, sigBytes) );
        }
        catch (NoSuchAlgorithmException e) {
            throw newDSAError(runtime, e.getMessage());
        }
        catch (SignatureException e) {
            throw newDSAError(runtime, "invalid signature");
        }
        catch (InvalidKeyException e) {
            throw newDSAError(runtime, "invalid key");
        }
    }

    private DSAKey getDsaKey() {
        DSAKey result;
        return (result = publicKey) != null ? result : privateKey;
    }

    private IRubyObject toBN(BigInteger value) {
        return value == null ? getRuntime().getNil() : BN.newBN(getRuntime(), value);
    }

    private synchronized BigInteger getP() {
        DSAKey key = getDsaKey();
        if (key != null) {
            return key.getParams().getP();
        }
        return dsa_p;
    }

    @JRubyMethod(name = "p")
    public IRubyObject get_p() {
        return toBN(getP());
    }

    @JRubyMethod(name = "p=")
    public synchronized IRubyObject set_p(IRubyObject p) {
        return setKeySpecComponent(SPEC_P, p);
    }

    private synchronized BigInteger getQ() {
        DSAKey key = getDsaKey();
        if (key != null) {
            return key.getParams().getQ();
        }
        return dsa_q;
    }

    @JRubyMethod(name = "q")
    public IRubyObject get_q() {
        return toBN(getQ());
    }

    @JRubyMethod(name = "q=")
    public synchronized IRubyObject set_q(IRubyObject q) {
        return setKeySpecComponent(SPEC_Q, q);
    }

    private synchronized BigInteger getG() {
        DSAKey key = getDsaKey();
        if (key != null) {
            return key.getParams().getG();
        }
        return dsa_g;
    }

    @JRubyMethod(name = "g")
    public IRubyObject get_g() {
        return toBN(getG());
    }

    @JRubyMethod(name = "g=")
    public synchronized IRubyObject set_g(IRubyObject g) {
        return setKeySpecComponent(SPEC_G, g);
    }

    @JRubyMethod(name = "priv_key")
    public synchronized IRubyObject get_priv_key() {
        DSAPrivateKey key;
        if ((key = this.privateKey) != null) {
            return toBN(key.getX());
        }
        return toBN(dsa_x);
    }

    @JRubyMethod(name = "priv_key=")
    public synchronized IRubyObject set_priv_key(IRubyObject priv_key) {
        return setKeySpecComponent(SPEC_X, priv_key);
    }

    @JRubyMethod(name = "pub_key")
    public synchronized IRubyObject get_pub_key() {
        DSAPublicKey key;
        if ( ( key = this.publicKey ) != null ) {
            return toBN(key.getY());
        }
        return toBN(dsa_y);
    }

    @JRubyMethod(name = "pub_key=")
    public synchronized IRubyObject set_pub_key(IRubyObject pub_key) {
        return setKeySpecComponent(SPEC_Y, pub_key);
    }

    private IRubyObject setKeySpecComponent(final int index, final IRubyObject value) {
        final BigInteger val = BN.getBigInteger(value);

        switch (index) {
            case SPEC_X: this.dsa_x = val; break;
            case SPEC_Y: this.dsa_y = val; break;
            case SPEC_P: this.dsa_p = val; break;
            case SPEC_Q: this.dsa_q = val; break;
            case SPEC_G: this.dsa_g = val; break;
        }

        // Don't access the dsa_p, dsa_q and dsa_g fields directly. They may
        // have already been consumed and cleared.
        BigInteger _dsa_p = getP();
        BigInteger _dsa_q = getQ();
        BigInteger _dsa_g = getG();

        if ( dsa_x != null && _dsa_p != null && _dsa_q != null && _dsa_g != null ) {
            // we now have all private key components. create the key :
            DSAPrivateKeySpec spec = new DSAPrivateKeySpec(dsa_x, _dsa_p, _dsa_q, _dsa_g);
            try {
                this.privateKey = (DSAPrivateKey) SecurityHelper.getKeyFactory("DSA").generatePrivate(spec);
            }
            catch (InvalidKeySpecException e) {
                throw newDSAError(getRuntime(), "invalid keyspec", e);
            }
            catch (NoSuchAlgorithmException e) {
                throw newDSAError(getRuntime(), "unsupported key algorithm (DSA)", e);
            }
            // clear out the specValues
            this.dsa_x = this.dsa_p = this.dsa_q = this.dsa_g = null;
        }

        if ( dsa_y != null && _dsa_p != null && _dsa_q != null && _dsa_g != null ) {
            // we now have all public key components. create the key :
            DSAPublicKeySpec spec = new DSAPublicKeySpec(dsa_y, _dsa_p, _dsa_q, _dsa_g);
            try {
                this.publicKey = (DSAPublicKey) SecurityHelper.getKeyFactory("DSA").generatePublic(spec);
            }
            catch (InvalidKeySpecException e) {
                throw newDSAError(getRuntime(), "invalid keyspec", e);
            }
            catch (NoSuchAlgorithmException e) {
                throw newDSAError(getRuntime(), "unsupported key algorithm (DSA)", e);
            }
            // clear out the specValues
            this.dsa_y = this.dsa_p = this.dsa_q = this.dsa_g = null;
        }

        return value;
    }

    private static final int SPEC_X = 0;
    private static final int SPEC_Y = 1;
    private static final int SPEC_P = 2;
    private static final int SPEC_Q = 3;
    private static final int SPEC_G = 4;

    public static RaiseException newDSAError(Ruby runtime, String message) {
        return Utils.newError(runtime, _PKey(runtime).getClass("DSAError"), message);
    }

    static RaiseException newDSAError(Ruby runtime, String message, Exception cause) {
        return Utils.newError(runtime, _PKey(runtime).getClass("DSAError"), message, cause);
    }

}// PKeyDSA
