/*
 * Copyright (c) 2010-2025 Contributors to the openHAB project
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 */
package org.openhab.core.io.jetty.certificate.internal;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.spec.ECField;
import java.security.spec.ECFieldFp;
import java.security.spec.ECParameterSpec;
import java.security.spec.ECPoint;
import java.security.spec.ECPrivateKeySpec;
import java.security.spec.ECPublicKeySpec;
import java.security.spec.EllipticCurve;
import java.security.spec.InvalidKeySpecException;
import java.util.Date;
import java.util.Random;

import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 *
 * @author Kai Kreuzer - Initial contribution
 */
public class CertificateGenerator implements BundleActivator {

    private static final String JETTY_KEYSTORE_PATH_PROPERTY = "jetty.keystore.path";
    private static final String KEYSTORE_PASSWORD = "openhab";
    private static final String KEYSTORE_ENTRY_ALIAS = "mykey";
    private static final String KEYSTORE_JKS_TYPE = "JKS";
    private static final String CURVE_NAME = "prime256v1";
    private static final String KEY_PAIR_GENERATOR_TYPE = "EC";
    private static final String KEY_FACTORY_TYPE = "EC";
    private static final String CONTENT_SIGNER_ALGORITHM = "SHA256withECDSA";
    private static final String CERTIFICATE_X509_TYPE = "X.509";
    private static final String X500_NAME = "CN=openhab.org, OU=None, O=None, L=None, C=None";

    private Logger logger;

    private File keystoreFile;

    @Override
    public void start(BundleContext context) throws Exception {
        logger = LoggerFactory.getLogger(CertificateGenerator.class);
        try {
            KeyStore keystore = ensureKeystore();

            if (!isCertificateInKeystore(keystore)) {
                logger.debug("{} alias not found. Generating a new certificate.", KEYSTORE_ENTRY_ALIAS);
                generateCertificate(keystore);
            } else {
                logger.debug("{} alias found. Do nothing.", KEYSTORE_ENTRY_ALIAS);
            }
        } catch (CertificateException | KeyStoreException e) {
            logger.error("Failed to generate a new SSL Certificate.", e);
        }
    }

    @Override
    public void stop(BundleContext context) throws Exception {
        // Nothing to do.
    }

    /**
     * Ensure that the keystore exist and is readable. If not, create a new one.
     *
     * @throws KeyStoreException if the creation of the keystore fails or if it is not readable.
     */
    private KeyStore ensureKeystore() throws KeyStoreException {
        String keystorePath = System.getProperty(JETTY_KEYSTORE_PATH_PROPERTY);
        keystoreFile = new File(keystorePath);
        KeyStore keyStore = KeyStore.getInstance(KEYSTORE_JKS_TYPE);
        if (!keystoreFile.exists()) {
            try {
                logger.debug("No keystore found. Creation of {}", keystoreFile.getAbsolutePath());
                boolean newFileCreated = keystoreFile.createNewFile();
                if (newFileCreated) {
                    keyStore.load(null, null);
                } else {
                    throw new IOException("Keystore file creation failed.");
                }
            } catch (IOException | NoSuchAlgorithmException | CertificateException e) {
                throw new KeyStoreException("Failed to create the keystore " + keystoreFile.getAbsolutePath(), e);
            }
        } else {
            try (InputStream keystoreStream = new FileInputStream(keystoreFile)) {
                logger.debug("Keystore found. Trying to load {}", keystoreFile.getAbsolutePath());
                keyStore.load(keystoreStream, KEYSTORE_PASSWORD.toCharArray());
            } catch (NoSuchAlgorithmException | CertificateException | IOException e) {
                throw new KeyStoreException("Failed to load the keystore " + keystoreFile.getAbsolutePath(), e);
            }
        }

        return keyStore;
    }

    /**
     * Check if the keystore contains a certificate with the KEYSTORE_ENTRY_ALIAS.
     *
     * @param keystore
     * @return true if the alias is already present in the keystore, else false.
     * @throws KeyStoreException If the keystore cannot be read.
     */
    private boolean isCertificateInKeystore(KeyStore keystore) throws KeyStoreException {
        return keystore.getCertificate(KEYSTORE_ENTRY_ALIAS) != null;
    }

    /**
     * Generate a new certificate and store it in the given keystore.
     *
     * @param keystore
     * @throws CertificateException if the certificate generation has failed.
     * @throws KeyStoreException If save of the keystore has failed.
     */
    private void generateCertificate(KeyStore keystore) throws CertificateException, KeyStoreException {
        try {
            long startTime = System.currentTimeMillis();
            org.bouncycastle.jce.spec.ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec(CURVE_NAME);
            ECField field = new ECFieldFp(ecSpec.getCurve().getField().getCharacteristic());
            EllipticCurve curve = new EllipticCurve(field, ecSpec.getCurve().getA().toBigInteger(),
                    ecSpec.getCurve().getB().toBigInteger());
            ECPoint pointG = new ECPoint(ecSpec.getG().getXCoord().toBigInteger(),
                    ecSpec.getG().getYCoord().toBigInteger());
            ECParameterSpec spec = new ECParameterSpec(curve, pointG, ecSpec.getN(), ecSpec.getH().intValue());
            KeyPairGenerator g = KeyPairGenerator.getInstance(KEY_PAIR_GENERATOR_TYPE);
            g.initialize(spec, new SecureRandom());
            KeyPair keysPair = g.generateKeyPair();

            ECPrivateKeySpec ecPrivSpec = new ECPrivateKeySpec(((ECPrivateKey) keysPair.getPrivate()).getS(), spec);
            ECPublicKeySpec ecPublicSpec = new ECPublicKeySpec(((ECPublicKey) keysPair.getPublic()).getW(), spec);
            KeyFactory kf = KeyFactory.getInstance(KEY_FACTORY_TYPE);
            PrivateKey privateKey = kf.generatePrivate(ecPrivSpec);
            PublicKey publicKey = kf.generatePublic(ecPublicSpec);

            logger.debug("Keys generated in {} ms.", (System.currentTimeMillis() - startTime));

            X500Name issuerDN = new X500Name(X500_NAME);
            Integer randomNumber = new Random().nextInt();
            BigInteger serialNumber = BigInteger.valueOf(randomNumber >= 0 ? randomNumber : randomNumber * -1);
            Date notBefore = new Date(System.currentTimeMillis() - 1000L * 60 * 60 * 24 * 30);
            Date notAfter = new Date(System.currentTimeMillis() + (1000L * 60 * 60 * 24 * 365 * 10));
            X500Name subjectDN = new X500Name(X500_NAME);
            byte[] publickeyb = publicKey.getEncoded();
            ASN1Sequence sequence = (ASN1Sequence) ASN1Primitive.fromByteArray(publickeyb);
            SubjectPublicKeyInfo subPubKeyInfo = new SubjectPublicKeyInfo(sequence);
            X509v3CertificateBuilder v3CertGen = new X509v3CertificateBuilder(issuerDN, serialNumber, notBefore,
                    notAfter, subjectDN, subPubKeyInfo);

            ContentSigner contentSigner = new JcaContentSignerBuilder(CONTENT_SIGNER_ALGORITHM)
                    .build(keysPair.getPrivate());
            X509CertificateHolder certificateHolder = v3CertGen.build(contentSigner);

            Certificate certificate = java.security.cert.CertificateFactory.getInstance(CERTIFICATE_X509_TYPE)
                    .generateCertificate(new ByteArrayInputStream(
                            ByteBuffer.wrap(certificateHolder.toASN1Structure().getEncoded()).array()));

            logger.debug("Total certificate generation time: {} ms.", (System.currentTimeMillis() - startTime));

            keystore.setKeyEntry(KEYSTORE_ENTRY_ALIAS, privateKey, KEYSTORE_PASSWORD.toCharArray(),
                    new java.security.cert.Certificate[] { certificate });

            logger.debug("Save the keystore into {}.", keystoreFile.getAbsolutePath());

            keystore.store(new FileOutputStream(keystoreFile), KEYSTORE_PASSWORD.toCharArray());
        } catch (NoSuchAlgorithmException | InvalidKeySpecException | IOException | OperatorCreationException
                | InvalidAlgorithmParameterException e) {
            throw new CertificateException("Failed to generate the new certificate.", e);
        }
    }
}
