/*
 * Copyright 2004 - 2013 Wayne Grant
 *           2013 - 2025 Kai Kramer
 *
 * This file is part of KeyStore Explorer.
 *
 * KeyStore Explorer is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * KeyStore Explorer 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with KeyStore Explorer.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.kse.gui.dialogs.extensions;

import java.awt.Container;
import java.awt.Dialog;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.IOException;
import java.security.KeyPair;
import java.security.PublicKey;
import java.util.ResourceBundle;

import javax.swing.AbstractAction;
import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JSeparator;
import javax.swing.KeyStroke;

import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.DERIA5String;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.asn1.x509.SubjectKeyIdentifier;
import org.bouncycastle.util.IPAddress;
import org.kse.KSE;
import org.kse.crypto.CryptoException;
import org.kse.crypto.keypair.KeyPairType;
import org.kse.crypto.keypair.KeyPairUtil;
import org.kse.crypto.publickey.KeyIdentifierGenerator;
import org.kse.crypto.x509.ExtendedKeyUsageType;
import org.kse.crypto.x509.X500NameUtils;
import org.kse.crypto.x509.X509Ext;
import org.kse.crypto.x509.X509ExtensionSet;
import org.kse.crypto.x509.X509ExtensionType;
import org.kse.gui.PlatformUtil;
import org.kse.gui.components.JEscDialog;
import org.kse.gui.error.DError;
import org.kse.utilities.DialogViewer;

import net.miginfocom.swing.MigLayout;

/**
 * Allows selection of X.509 Extensions to add to a certificate.
 */
public class DSelectStandardExtensionTemplate extends JEscDialog {
    private static final long serialVersionUID = 1L;

    private static ResourceBundle res = ResourceBundle.getBundle("org/kse/gui/dialogs/extensions/resources");

    private static final String CANCEL_KEY = "CANCEL_KEY";

    private JRadioButton jrbCA;
    private JRadioButton jrbSslServer;
    private JRadioButton jrbSslClient;
    private JRadioButton jrbCodeSigning;
    private JPanel jpButtons;
    private JButton jbOK;
    private JButton jbCancel;

    private X509ExtensionSet extensions = new X509ExtensionSet();

    private PublicKey authorityPublicKey;
    private PublicKey subjectPublicKey;
    private X500Name subjectCertName;
    private byte[] issuerSkiExt;

    private boolean cancelled = true;

    /**
     * Creates a new DSelectStandardExtensionTemplate dialog.
     *
     * @param parent             Parent frame
     * @param authorityPublicKey Key of issuer certificate
     * @param subjectPublicKey   Key of new certificate
     * @param subjectCertName    Subject DN
     * @param issuerSkiExt       Subject key identifier extension of issuer certificate
     */
    public DSelectStandardExtensionTemplate(JDialog parent, PublicKey authorityPublicKey, PublicKey subjectPublicKey,
                                            X500Name subjectCertName, byte[] issuerSkiExt) {
        super(parent, Dialog.ModalityType.DOCUMENT_MODAL);
        this.authorityPublicKey = authorityPublicKey;
        this.subjectPublicKey = subjectPublicKey;
        this.subjectCertName = subjectCertName;
        this.issuerSkiExt = issuerSkiExt;
        setTitle(res.getString("DSelectStandardExtensionTemplate.Title"));
        initComponents();
    }

    private void initComponents() {

        jrbCA = new JRadioButton(res.getString("DSelectStandardExtensionTemplate.jrbCA.text"), false);
        PlatformUtil.setMnemonic(jrbCA, res.getString("DSelectStandardExtensionTemplate.jrbCA.mnemonic").charAt(0));
        jrbCA.setToolTipText(res.getString("DSelectStandardExtensionTemplate.jrbCA.tooltip"));
        jrbCA.setSelected(true);

        jrbSslServer = new JRadioButton(res.getString("DSelectStandardExtensionTemplate.jrbSslServer.text"), false);
        PlatformUtil.setMnemonic(jrbSslServer,
                                 res.getString("DSelectStandardExtensionTemplate.jrbSslServer.mnemonic").charAt(0));
        jrbSslServer.setToolTipText(res.getString("DSelectStandardExtensionTemplate.jrbSslServer.tooltip"));

        jrbSslClient = new JRadioButton(res.getString("DSelectStandardExtensionTemplate.jrbSslClient.text"), false);
        PlatformUtil.setMnemonic(jrbSslClient,
                                 res.getString("DSelectStandardExtensionTemplate.jrbSslClient.mnemonic").charAt(0));
        jrbSslClient.setToolTipText(res.getString("DSelectStandardExtensionTemplate.jrbSslClient.tooltip"));

        jrbCodeSigning = new JRadioButton(res.getString("DSelectStandardExtensionTemplate.jrbCodeSigning.text"), false);
        PlatformUtil.setMnemonic(jrbCodeSigning,
                                 res.getString("DSelectStandardExtensionTemplate.jrbCodeSigning.mnemonic").charAt(0));
        jrbCodeSigning.setToolTipText(res.getString("DSelectStandardExtensionTemplate.jrbCodeSigning.tooltip"));

        ButtonGroup buttonGroup = new ButtonGroup();
        buttonGroup.add(jrbCA);
        buttonGroup.add(jrbSslServer);
        buttonGroup.add(jrbSslClient);
        buttonGroup.add(jrbCodeSigning);

        jbOK = new JButton(res.getString("DAddExtensions.jbOK.text"));

        jbCancel = new JButton(res.getString("DAddExtensions.jbCancel.text"));
        jbCancel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
                .put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), CANCEL_KEY);

        jpButtons = PlatformUtil.createDialogButtonPanel(jbOK, jbCancel, "insets 0");

        // layout
        Container pane = getContentPane();
        pane.setLayout(new MigLayout("insets dialog, fill", "[]", "[]"));
        pane.add(jrbCA, "growx, wrap");
        pane.add(jrbSslServer, "wrap");
        pane.add(jrbSslClient, "wrap");
        pane.add(jrbCodeSigning, "wrap");
        pane.add(new JSeparator(), "spanx, growx, wrap");
        pane.add(jpButtons, "right, spanx");

        jbOK.addActionListener(evt -> okPressed());

        jbCancel.addActionListener(evt -> cancelPressed());
        jbCancel.getActionMap().put(CANCEL_KEY, new AbstractAction() {
            private static final long serialVersionUID = 1L;

            @Override
            public void actionPerformed(ActionEvent evt) {
                cancelPressed();
            }
        });

        addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent evt) {
                closeDialog();
            }
        });

        setResizable(false);

        getRootPane().setDefaultButton(jbOK);

        pack();
    }

    public X509ExtensionSet getExtensionSet() {
        return extensions;
    }

    public boolean wasCancelled() {
        return cancelled;
    }

    private void okPressed() {
        setExtensionsAsSelected();
        closeDialog();
    }

    private void setExtensionsAsSelected() {

        X509ExtensionSet extensionSet = new X509ExtensionSet();
        try {
            addAuthorityKeyIdentifier(extensionSet);
            addSubjectKeyIdentifier(extensionSet);

            if (jrbCA.isSelected()) {
                addBasicConstraints(extensionSet);
                addKeyUsage(extensionSet, KeyUsage.keyCertSign | KeyUsage.cRLSign);
            }

            if (jrbSslClient.isSelected()) {
                addKeyUsage(extensionSet, KeyUsage.digitalSignature | KeyUsage.keyEncipherment);
                addExtKeyUsage(extensionSet, ExtendedKeyUsageType.CLIENT_AUTH.oid());
            }

            if (jrbSslServer.isSelected()) {
                addKeyUsage(extensionSet, KeyUsage.digitalSignature | KeyUsage.keyEncipherment);
                addExtKeyUsage(extensionSet, ExtendedKeyUsageType.SERVER_AUTH.oid());
                addSAN(extensionSet, subjectCertName);
            }

            if (jrbCodeSigning.isSelected()) {
                addKeyUsage(extensionSet, KeyUsage.digitalSignature);
                addExtKeyUsage(extensionSet, ExtendedKeyUsageType.CODE_SIGNING.oid());
            }

            this.extensions = extensionSet;

            this.cancelled = false;
        } catch (Exception e) {
            DError.displayError(this, e);
        }
    }

    private void addSAN(X509ExtensionSet extensionSet, X500Name subjectDN) throws IOException {

        GeneralName[] generalNames = new GeneralName[1];

        if (subjectDN == null) {
            // add extension but without value so we can remind the user later to add a value
            generalNames[0] = new GeneralName(GeneralName.dNSName, new DERIA5String(""));
        } else {
            // if subject DN already exists, we use value of CN
            String cn = X500NameUtils.extractCN(subjectDN);
            if (cn.isEmpty()) {
                generalNames[0] = new GeneralName(GeneralName.dNSName, new DERIA5String(""));
            } else {
                if (IPAddress.isValid(cn)) {
                    generalNames[0] = new GeneralName(GeneralName.iPAddress, cn);
                } else {
                    generalNames[0] = new GeneralName(GeneralName.dNSName, new DERIA5String(cn));
                }
            }
        }

        byte[] sanEncoded = X509Ext.wrapInOctetString(new GeneralNames(generalNames).getEncoded());

        extensionSet.addExtension(X509ExtensionType.SUBJECT_ALTERNATIVE_NAME.oid(), false, sanEncoded);
    }

    private void addAuthorityKeyIdentifier(X509ExtensionSet extensionSet) throws CryptoException, IOException {
        byte[] keyIdentifier = new KeyIdentifierGenerator(authorityPublicKey).generate160BitHashId();
        if (issuerSkiExt != null) {
            // The *issuer* subject key identifier is the *issued* cert's authority key identifier
            keyIdentifier = SubjectKeyIdentifier.getInstance(X509Ext.unwrapExtension(issuerSkiExt))
                    .getKeyIdentifier();
        }
        AuthorityKeyIdentifier aki = new AuthorityKeyIdentifier(keyIdentifier);
        byte[] akiEncoded = X509Ext.wrapInOctetString(aki.getEncoded());
        extensionSet.addExtension(X509ExtensionType.AUTHORITY_KEY_IDENTIFIER.oid(), false, akiEncoded);
    }

    private void addSubjectKeyIdentifier(X509ExtensionSet extensionSet) throws CryptoException, IOException {
        KeyIdentifierGenerator skiGenerator = new KeyIdentifierGenerator(subjectPublicKey);
        SubjectKeyIdentifier ski = new SubjectKeyIdentifier(skiGenerator.generate160BitHashId());
        byte[] skiEncoded = X509Ext.wrapInOctetString(ski.getEncoded());
        extensionSet.addExtension(X509ExtensionType.SUBJECT_KEY_IDENTIFIER.oid(), false, skiEncoded);
    }

    private void addBasicConstraints(X509ExtensionSet extensionSet) throws IOException {
        BasicConstraints bc = new BasicConstraints(true);
        byte[] bcEncoded = X509Ext.wrapInOctetString(bc.getEncoded());
        extensionSet.addExtension(X509ExtensionType.BASIC_CONSTRAINTS.oid(), true, bcEncoded);
    }

    private void addKeyUsage(X509ExtensionSet extensionSet, int usage) throws IOException {
        KeyUsage ku = new KeyUsage(usage);
        byte[] kuEncoded = X509Ext.wrapInOctetString(ku.getEncoded());
        extensionSet.addExtension(X509ExtensionType.KEY_USAGE.oid(), true, kuEncoded);
    }

    private void addExtKeyUsage(X509ExtensionSet extensionSet, String ekuOid) throws IOException {
        ExtendedKeyUsage eku = new ExtendedKeyUsage(
                new KeyPurposeId[] { KeyPurposeId.getInstance(new ASN1ObjectIdentifier(ekuOid)) });
        byte[] ekuEncoded = X509Ext.wrapInOctetString(eku.getEncoded());
        extensionSet.addExtension(X509ExtensionType.EXTENDED_KEY_USAGE.oid(), false, ekuEncoded);
    }

    private void cancelPressed() {
        closeDialog();
    }

    private void closeDialog() {
        setVisible(false);
        dispose();
    }

    // for quick UI testing
    public static void main(String[] args) throws Exception {
        final KeyPair issuerKP = KeyPairUtil.generateKeyPair(KeyPairType.RSA, 1024, KSE.BC);
        final KeyPair subjectKP = KeyPairUtil.generateKeyPair(KeyPairType.RSA, 1024, KSE.BC);

        DSelectStandardExtensionTemplate dialog = new DSelectStandardExtensionTemplate(new JDialog(),
                                                                                       issuerKP.getPublic(),
                                                                                       subjectKP.getPublic(),
                                                                                       new X500Name(
                                                                                               "cn=www.example.com"),
                                                                                       null);
        DialogViewer.run(dialog);
    }
}
