/*******************************************************************************
 * Copyright (c) 2021 Red Hat Inc. and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *******************************************************************************/
package org.eclipse.equinox.internal.p2.artifact.processors.pgp;

import java.io.*;
import java.util.*;
import java.util.stream.Collectors;
import org.bouncycastle.bcpg.ArmoredInputStream;
import org.bouncycastle.openpgp.*;
import org.bouncycastle.openpgp.bc.BcPGPObjectFactory;
import org.bouncycastle.openpgp.bc.BcPGPPublicKeyRingCollection;
import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider;
import org.eclipse.core.runtime.*;
import org.eclipse.equinox.internal.p2.artifact.repository.Activator;
import org.eclipse.equinox.internal.p2.core.helpers.LogHelper;
import org.eclipse.equinox.internal.provisional.p2.artifact.repository.processing.ProcessingStep;
import org.eclipse.equinox.p2.core.IProvisioningAgent;
import org.eclipse.equinox.p2.repository.artifact.*;
import org.eclipse.osgi.util.NLS;

/**
 * This processing step verifies PGP signatures are correct (ie artifact was not
 * tampered during fetch). Note that is does <b>not</b> deal with trust. Dealing
 * with trusted signers is done as part of CheckTrust touchpoint and phase.
 */
public final class PGPSignatureVerifier extends ProcessingStep {

	/**
	 * ID of the registering
	 * <code>org.eclipse.equinox.p2.artifact.repository.processingSteps</tt>
	 * extension.
	 */
	public static final String ID = "org.eclipse.equinox.p2.processing.PGPSignatureCheck"; //$NON-NLS-1$

	private static Map<Long, PGPPublicKey> knownKeys = new HashMap<>();

	public static final String PGP_SIGNER_KEYS_PROPERTY_NAME = "pgp.publicKeys"; //$NON-NLS-1$
	public static final String PGP_SIGNATURES_PROPERTY_NAME = "pgp.signatures"; //$NON-NLS-1$
	private Collection<PGPSignature> signaturesToVerify;

	public PGPSignatureVerifier() {
		super();
		link(nullOutputStream(), new NullProgressMonitor()); // this is convenience for tests
	}

	private static Collection<PGPSignature> getSignatures(IArtifactDescriptor artifact)
			throws IOException, PGPException {
		String signatureText = unnormalizedPGPProperty(artifact.getProperty(PGP_SIGNATURES_PROPERTY_NAME));
		if (signatureText == null) {
			return Collections.emptyList();
		}
		List<PGPSignature> res = new ArrayList<>();
		try (InputStream in = new ArmoredInputStream(new ByteArrayInputStream(signatureText.getBytes()))) {
			PGPObjectFactory pgpFactory = new BcPGPObjectFactory(in);
			Object o = pgpFactory.nextObject();
			PGPSignatureList signatureList = new PGPSignatureList(new PGPSignature[0]);
			if (o instanceof PGPCompressedData) {
				PGPCompressedData pgpCompressData = (PGPCompressedData) o;
				pgpFactory = new BcPGPObjectFactory(pgpCompressData.getDataStream());
				signatureList = (PGPSignatureList) pgpFactory.nextObject();
			} else if (o instanceof PGPSignatureList) {
				signatureList = (PGPSignatureList) o;
			}
			signatureList.iterator().forEachRemaining(res::add);
		}
		return res;
	}

	@Override
	public void initialize(IProvisioningAgent agent, IProcessingStepDescriptor descriptor,
			IArtifactDescriptor context) {
		super.initialize(agent, descriptor, context);
//		1. verify declared public keys have signature from a trusted key, if so, add to KeyStore
//		2. verify artifact signature matches signture of given keys, and at least 1 of this key is trusted
		String signatureText = unnormalizedPGPProperty(context.getProperty(PGP_SIGNATURES_PROPERTY_NAME));
		if (signatureText == null) {
			setStatus(Status.OK_STATUS);
			return;
		}
		try {
			signaturesToVerify = getSignatures(context);
		} catch (Exception ex) {
			setStatus(new Status(IStatus.ERROR, Activator.ID, Messages.Error_CouldNotLoadSignature, ex));
			return;
		}
		if (signaturesToVerify.isEmpty()) {
			setStatus(Status.OK_STATUS);
			return;
		}

		IArtifactRepository repository = context.getRepository();
		knownKeys.putAll(readPublicKeys(context.getProperty(PGP_SIGNER_KEYS_PROPERTY_NAME),
				repository != null ? repository.getProperty(PGP_SIGNER_KEYS_PROPERTY_NAME) : null));
		for (PGPSignature signature : signaturesToVerify) {
			PGPPublicKey publicKey = knownKeys.get(signature.getKeyID());
			if (publicKey == null) {
				setStatus(new Status(IStatus.ERROR, Activator.ID,
						NLS.bind(Messages.Error_publicKeyNotFound, signature.getKeyID())));
				return;
			}
			try {
				signature.init(new BcPGPContentVerifierBuilderProvider(), publicKey);
			} catch (PGPException ex) {
				setStatus(new Status(IStatus.ERROR, Activator.ID, ex.getMessage(), ex));
				return;
			}
		}
	}

	/**
	 * See // https://www.w3.org/TR/1998/REC-xml-19980210#AVNormalize, newlines
	 * replaced by spaces by parser, needs to be restored
	 *
	 * @param context
	 * @param pgpSignaturesPropertyName
	 * @return fixed PGP armored blocks
	 */
	private static String unnormalizedPGPProperty(String value) {
		if (value == null) {
			return null;
		}
		if (value.contains("\n") || value.contains("\r")) { //$NON-NLS-1$ //$NON-NLS-2$
			return value;
		}
		return value.replace(' ', '\n').replace("-----BEGIN\nPGP\nSIGNATURE-----", "-----BEGIN PGP SIGNATURE-----") //$NON-NLS-1$ //$NON-NLS-2$
				.replace("-----END\nPGP\nSIGNATURE-----", "-----END PGP SIGNATURE-----") //$NON-NLS-1$ //$NON-NLS-2$
				.replace("-----BEGIN\nPGP\nPUBLIC\nKEY\nBLOCK-----", "-----BEGIN PGP PUBLIC KEY BLOCK-----") //$NON-NLS-1$ //$NON-NLS-2$
				.replace("-----END\nPGP\nPUBLIC\nKEY\nBLOCK-----", "-----END PGP PUBLIC KEY BLOCK-----"); //$NON-NLS-1$ //$NON-NLS-2$
	}

	private static Map<Long, PGPPublicKey> readPublicKeys(String armoredPublicKeyring) {
		if (armoredPublicKeyring == null) {
			return Collections.emptyMap();
		}
		Map<Long, PGPPublicKey> res = new HashMap<>();
		try (InputStream stream = PGPUtil
				.getDecoderStream(new ByteArrayInputStream(unnormalizedPGPProperty(armoredPublicKeyring).getBytes()))) {
			PGPPublicKeyRingCollection pgpPub = new BcPGPPublicKeyRingCollection(stream);

			pgpPub.getKeyRings().forEachRemaining(kRing ->
				kRing.getPublicKeys().forEachRemaining(key -> res.put(key.getKeyID(), key))
			);
		} catch (IOException | PGPException e) {
			LogHelper.log(new Status(IStatus.ERROR, Activator.ID, e.getMessage(), e));
		}
		return res;

	}

	private Map<Long, PGPPublicKey> readPublicKeys(String... armoredPublicKeys) {
		Map<Long, PGPPublicKey> keys = new HashMap<>();
		for (String armoredKey : armoredPublicKeys) {
			if (armoredKey != null) {
				keys.putAll(readPublicKeys(armoredKey));
			}
		}
		return keys;
	}

	@Override
	public void write(int b) {
		if (signaturesToVerify != null) {
			signaturesToVerify.iterator().forEachRemaining(signature -> signature.update((byte) b));
		}

	}

	@Override
	public void write(byte[] b) throws IOException {
		getDestination().write(b);
		if (signaturesToVerify != null) {
			signaturesToVerify.iterator().forEachRemaining(signature -> signature.update(b));
		}
	}

	@Override
	public void write(byte[] b, int off, int len) throws IOException {
		getDestination().write(b, off, len);
		if (signaturesToVerify != null) {
			signaturesToVerify.iterator().forEachRemaining(signature -> signature.update(b, off, len));
		}
	}

	@Override
	public void close() {
		if (!getStatus().isOK()) {
			return;
		}
		if (signaturesToVerify == null || signaturesToVerify.isEmpty()) {
			return;
		}
		Iterator<PGPSignature> iterator = signaturesToVerify.iterator();
		while (iterator.hasNext()) {
			PGPSignature signature = iterator.next();
			try {
				if (!signature.verify()) {
					setStatus(new Status(IStatus.ERROR, Activator.ID, Messages.Error_SignatureAndFileDontMatch));
					return;
				}
			} catch (PGPException ex) {
				setStatus(new Status(IStatus.ERROR, Activator.ID, ex.getMessage(), ex));
				return;
			}
		}
		setStatus(Status.OK_STATUS);
	}

	public static Collection<PGPPublicKey> getSigners(IArtifactDescriptor artifact) {
		try {
			return getSignatures(artifact).stream() //
					.mapToLong(PGPSignature::getKeyID) //
					.mapToObj(Long::valueOf) //
					.map(knownKeys::get) //
					.filter(Objects::nonNull).collect(Collectors.toSet());
		} catch (IOException | PGPException e) {
			LogHelper.log(new Status(IStatus.ERROR, Activator.ID, e.getMessage(), e));
			return Collections.emptyList();
		}
	}

	public static void discardKnownKeys() {
		knownKeys.clear();
	}
}
