Bug 578232 - Improve trust preference page

Provide support for showing details of PGP keys in a format similar to
what key servers display.

Change-Id: I17c53ce9252c61c1c4592d0ae4fbdf585dd98c9f
Signed-off-by: Ed Merks <ed.merks@gmail.com>
Reviewed-on: https://git.eclipse.org/r/c/equinox/rt.equinox.p2/+/190746
diff --git a/bundles/org.eclipse.equinox.p2.ui.sdk/src/org/eclipse/equinox/internal/p2/ui/sdk/ProvSDKMessages.java b/bundles/org.eclipse.equinox.p2.ui.sdk/src/org/eclipse/equinox/internal/p2/ui/sdk/ProvSDKMessages.java
index 3470f3e..26d9793 100644
--- a/bundles/org.eclipse.equinox.p2.ui.sdk/src/org/eclipse/equinox/internal/p2/ui/sdk/ProvSDKMessages.java
+++ b/bundles/org.eclipse.equinox.p2.ui.sdk/src/org/eclipse/equinox/internal/p2/ui/sdk/ProvSDKMessages.java
@@ -64,6 +64,7 @@
 	public static String TrustPreferencePage_DateNotYetvalid;
 	public static String TrustPreferencePage_DateNotYetValid;
 	public static String TrustPreferencePage_DateValid;
+	public static String TrustPreferencePage_Details;
 	public static String TrustPreferencePage_Export;
 	public static String TrustPreferencePage_FingerprintIdColumn;
 	public static String TrustPreferencePage_NameColumn;
diff --git a/bundles/org.eclipse.equinox.p2.ui.sdk/src/org/eclipse/equinox/internal/p2/ui/sdk/TrustPreferencePage.java b/bundles/org.eclipse.equinox.p2.ui.sdk/src/org/eclipse/equinox/internal/p2/ui/sdk/TrustPreferencePage.java
index 957f190..1a8f1de 100644
--- a/bundles/org.eclipse.equinox.p2.ui.sdk/src/org/eclipse/equinox/internal/p2/ui/sdk/TrustPreferencePage.java
+++ b/bundles/org.eclipse.equinox.p2.ui.sdk/src/org/eclipse/equinox/internal/p2/ui/sdk/TrustPreferencePage.java
@@ -26,6 +26,7 @@
 import org.eclipse.core.runtime.*;
 import org.eclipse.equinox.internal.p2.artifact.processors.pgp.PGPPublicKeyStore;
 import org.eclipse.equinox.internal.p2.engine.phases.CertificateChecker;
+import org.eclipse.equinox.internal.p2.ui.dialogs.PGPPublicKeyViewDialog;
 import org.eclipse.equinox.internal.p2.ui.viewers.CertificateLabelProvider;
 import org.eclipse.equinox.p2.core.IProvisioningAgent;
 import org.eclipse.equinox.p2.engine.IProfileRegistry;
@@ -143,7 +144,8 @@
 				return ProvSDKMessages.TrustPreferencePage_DateValid;
 			}
 			Instant expires = pgp.getCreationTime().toInstant().plus(validSeconds, ChronoUnit.SECONDS);
-			return expires.isBefore(Instant.now()) ? NLS.bind(ProvSDKMessages.TrustPreferencePage_DateExpiredSince, expires)
+			return expires.isBefore(Instant.now())
+					? NLS.bind(ProvSDKMessages.TrustPreferencePage_DateExpiredSince, expires)
 					: NLS.bind(ProvSDKMessages.TrustPreferencePage_DataValidExpires, expires);
 		}, x509 -> {
 			try {
@@ -265,12 +267,30 @@
 		removeButton.setEnabled(false);
 		setVerticalButtonLayoutData(removeButton);
 
+		Runnable details = () -> {
+			Object element = viewer.getStructuredSelection().getFirstElement();
+			if (element instanceof X509Certificate) {
+				// create and open dialog for certificate chain
+				CertificateLabelProvider.openDialog(getShell(), (X509Certificate) element);
+			} else {
+				new PGPPublicKeyViewDialog(getShell(), (PGPPublicKey) element,
+						provisioningAgent.getService(PGPPublicKeyService.class)).open();
+			}
+		};
+
+		Button detailsButton = new Button(buttonComposite, SWT.PUSH);
+		detailsButton.setText(ProvSDKMessages.TrustPreferencePage_Details);
+		detailsButton.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> details.run()));
+		detailsButton.setEnabled(false);
+		setVerticalButtonLayoutData(detailsButton);
+
 		viewer.addPostSelectionChangedListener(e -> {
 			List<Object> selectedKeys = getSelectedKeys();
 			exportButton.setEnabled(selectedKeys.size() == 1);
 			Collection<PGPPublicKey> keys = trustedKeys.all();
 			removeButton.setEnabled(
 					selectedKeys.stream().anyMatch(o -> keys.contains(o) || trustedCertificates.contains(o)));
+			detailsButton.setEnabled(selectedKeys.size() == 1);
 		});
 
 		Button trustAllButton = WidgetFactory.button(SWT.CHECK).text(ProvSDKMessages.TrustPreferencePage_TrustAll)
@@ -306,14 +326,7 @@
 			}
 		}));
 
-		viewer.addDoubleClickListener(e -> {
-			StructuredSelection selection = (StructuredSelection) e.getSelection();
-			Object element = selection.getFirstElement();
-			if (element instanceof X509Certificate) {
-				// create and open dialog for certificate chain
-				CertificateLabelProvider.openDialog(getShell(), (X509Certificate) element);
-			}
-		});
+		viewer.addDoubleClickListener(e -> details.run());
 
 		typeColumn.getColumn().pack();
 
diff --git a/bundles/org.eclipse.equinox.p2.ui.sdk/src/org/eclipse/equinox/internal/p2/ui/sdk/messages.properties b/bundles/org.eclipse.equinox.p2.ui.sdk/src/org/eclipse/equinox/internal/p2/ui/sdk/messages.properties
index 3b1c616..93df84f 100644
--- a/bundles/org.eclipse.equinox.p2.ui.sdk/src/org/eclipse/equinox/internal/p2/ui/sdk/messages.properties
+++ b/bundles/org.eclipse.equinox.p2.ui.sdk/src/org/eclipse/equinox/internal/p2/ui/sdk/messages.properties
@@ -48,6 +48,7 @@
 TrustPreferencePage_DateNotYetvalid=\u274C Not yet valid
 TrustPreferencePage_DateNotYetValid=\u274C Not yet valid, starts {0}
 TrustPreferencePage_DateValid=\u2714\uFE0F Valid
+TrustPreferencePage_Details=\uD83D\uDD0D D&etails...
 TrustPreferencePage_Export=Export...
 TrustPreferencePage_FingerprintIdColumn=Fingerprint/Id
 TrustPreferencePage_NameColumn=Name
diff --git a/bundles/org.eclipse.equinox.p2.ui/META-INF/MANIFEST.MF b/bundles/org.eclipse.equinox.p2.ui/META-INF/MANIFEST.MF
index b66eb2c..433d2e8 100644
--- a/bundles/org.eclipse.equinox.p2.ui/META-INF/MANIFEST.MF
+++ b/bundles/org.eclipse.equinox.p2.ui/META-INF/MANIFEST.MF
@@ -19,6 +19,7 @@
  org.eclipse.equinox.internal.p2.ui.dialogs;
   x-friends:="org.eclipse.equinox.p2.ui.admin,
    org.eclipse.equinox.p2.ui.sdk.scheduler,
+   org.eclipse.equinox.p2.ui.sdk,
    org.eclipse.pde.ui,
    org.eclipse.equinox.p2.ui.importexport",
  org.eclipse.equinox.internal.p2.ui.model;
diff --git a/bundles/org.eclipse.equinox.p2.ui/src/org/eclipse/equinox/internal/p2/ui/ProvUIMessages.java b/bundles/org.eclipse.equinox.p2.ui/src/org/eclipse/equinox/internal/p2/ui/ProvUIMessages.java
index e3804b9..f79aacc 100644
--- a/bundles/org.eclipse.equinox.p2.ui/src/org/eclipse/equinox/internal/p2/ui/ProvUIMessages.java
+++ b/bundles/org.eclipse.equinox.p2.ui/src/org/eclipse/equinox/internal/p2/ui/ProvUIMessages.java
@@ -78,6 +78,7 @@
 	public static String IUGeneralInfoPropertyPage_VersionLabel;
 	public static String IULicensePropertyPage_NoLicense;
 	public static String IULicensePropertyPage_ViewLicenseLabel;
+	public static String PGPPublicKeyViewDialog_Title;
 	public static String ProfileModificationAction_InvalidSelections;
 	public static String ProfileModificationWizardPage_DetailsLabel;
 	public static String ProfileSnapshots_Label;
diff --git a/bundles/org.eclipse.equinox.p2.ui/src/org/eclipse/equinox/internal/p2/ui/dialogs/PGPPublicKeyViewDialog.java b/bundles/org.eclipse.equinox.p2.ui/src/org/eclipse/equinox/internal/p2/ui/dialogs/PGPPublicKeyViewDialog.java
new file mode 100644
index 0000000..e02936a
--- /dev/null
+++ b/bundles/org.eclipse.equinox.p2.ui/src/org/eclipse/equinox/internal/p2/ui/dialogs/PGPPublicKeyViewDialog.java
@@ -0,0 +1,248 @@
+/*******************************************************************************
+ * Copyright (c) 2022 Eclipse contributors 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.ui.dialogs;
+
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.List;
+import org.bouncycastle.bcpg.*;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPSignature;
+import org.eclipse.core.runtime.*;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.equinox.internal.p2.ui.ProvUIMessages;
+import org.eclipse.equinox.p2.repository.spi.PGPPublicKeyService;
+import org.eclipse.jface.dialogs.IDialogConstants;
+import org.eclipse.jface.dialogs.TitleAreaDialog;
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.jface.viewers.StyledString;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.StyledText;
+import org.eclipse.swt.graphics.*;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.*;
+
+/**
+ * Presents information about a key in a format similar to what key servers
+ * display.
+ *
+ * @since 1.2.4
+ */
+public class PGPPublicKeyViewDialog extends TitleAreaDialog {
+
+	private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); //$NON-NLS-1$
+
+	final private PGPPublicKey originalKey;
+
+	final private PGPPublicKeyService keyService;
+
+	private StyledText styledText;
+
+	public PGPPublicKeyViewDialog(Shell parentShell, PGPPublicKey key, PGPPublicKeyService keyService) {
+		super(parentShell);
+		this.originalKey = key;
+		this.keyService = keyService;
+		DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC")); //$NON-NLS-1$
+	}
+
+	@Override
+	protected void configureShell(Shell newShell) {
+		super.configureShell(newShell);
+		newShell.setText(ProvUIMessages.PGPPublicKeyViewDialog_Title);
+		if (keyService != null) {
+			computeVerifiedCertifications(newShell);
+		}
+	}
+
+	@Override
+	protected void setShellStyle(int newShellStyle) {
+		super.setShellStyle(newShellStyle | SWT.RESIZE | SWT.DIALOG_TRIM);
+	}
+
+	@Override
+	protected Control createDialogArea(Composite parent) {
+		Composite composite = (Composite) super.createDialogArea(parent);
+		GridData data = new GridData(SWT.FILL, SWT.FILL, true, true);
+		composite.setLayoutData(data);
+
+		styledText = new StyledText(composite, SWT.READ_ONLY | SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL);
+		styledText.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+
+		// Create a sightly smaller text (mono-space) font.
+		FontData[] fontData = JFaceResources.getTextFont().getFontData();
+		for (FontData fontDataElement : fontData) {
+			fontDataElement.setHeight(fontDataElement.getHeight() - 1);
+		}
+		Font font = new Font(styledText.getDisplay(), fontData);
+		styledText.setFont(font);
+		styledText.addDisposeListener(e -> font.dispose());
+
+		GC gc = new GC(styledText);
+		gc.setFont(font);
+		data.widthHint = convertWidthInCharsToPixels(gc.getFontMetrics(), 110);
+		gc.dispose();
+
+		update(originalKey, Set.of());
+		return composite;
+	}
+
+	@Override
+	protected void createButtonsForButtonBar(Composite parent) {
+		createButton(parent, IDialogConstants.CANCEL_ID, IDialogConstants.CLOSE_LABEL, true).setFocus();
+	}
+
+	@SuppressWarnings("nls")
+	protected void update(PGPPublicKey key, Set<PGPPublicKey> verifiedCertifications) {
+		StyledString content = new StyledString();
+		String fingerprint = PGPPublicKeyService.toHex(key.getFingerprint()).toUpperCase(Locale.ROOT);
+
+		PublicKeyPacket publicKeyPacket = key.getPublicKeyPacket();
+		publicKeyPacket.getAlgorithm();
+		content.append(" ");
+		content.append(publicKeyPacket instanceof PublicSubkeyPacket ? "sub" : "pub", StyledString.QUALIFIER_STYLER);
+		content.append(" ");
+
+		int algorithm = publicKeyPacket.getAlgorithm();
+		switch (algorithm) {
+		case PublicKeyAlgorithmTags.RSA_GENERAL:
+		case PublicKeyAlgorithmTags.RSA_ENCRYPT:
+		case PublicKeyAlgorithmTags.RSA_SIGN: {
+			content.append("rsa");
+			break;
+		}
+		case PublicKeyAlgorithmTags.DSA: {
+			content.append("dsa");
+			break;
+		}
+		case PublicKeyAlgorithmTags.ELGAMAL_ENCRYPT:
+		case PublicKeyAlgorithmTags.ELGAMAL_GENERAL: {
+			content.append("elgamal");
+			break;
+		}
+		default: {
+			content.append("[");
+			content.append(Integer.toString(algorithm));
+			content.append("]");
+			break;
+		}
+
+		}
+		int bitStrength = key.getBitStrength();
+		content.append(Integer.toString(bitStrength));
+		content.append("/");
+		content.append(fingerprint);
+
+		content.append(" ");
+		content.append(DATE_FORMAT.format(key.getCreationTime()));
+
+		content.append(" ");
+		content.append("\n");
+
+		List<String> users = new ArrayList<>();
+		key.getUserIDs().forEachRemaining(users::add);
+		if (!users.isEmpty()) {
+			for (String user : users) {
+				content.append(" ");
+				content.append("uid", StyledString.QUALIFIER_STYLER);
+				content.append(" ");
+				content.append(user, StyledString.COUNTER_STYLER);
+				content.append("\n");
+			}
+		}
+
+		Long subKeyOf = null;
+
+		for (Iterator<PGPSignature> signatures = key.getSignatures(); signatures.hasNext();) {
+			PGPSignature signature = signatures.next();
+			long keyID = signature.getKeyID();
+
+			if (signature.getSignatureType() == PGPSignature.SUBKEY_BINDING) {
+				subKeyOf = keyID;
+			}
+
+			content.append(" ");
+			content.append("sig", StyledString.QUALIFIER_STYLER);
+			content.append(" ");
+			content.append(PGPPublicKeyService.toHex(keyID));
+			content.append(" ");
+			Date creationTime = signature.getCreationTime();
+			String formattedCreationTime = DATE_FORMAT.format(creationTime);
+			content.append(formattedCreationTime);
+			long signatureExpirationTime = signature.getHashedSubPackets().getSignatureExpirationTime();
+			content.append(" ");
+			content.append(signatureExpirationTime == 0 ? formattedCreationTime.replaceAll(".", "_")
+					: DATE_FORMAT.format(new Date(creationTime.getTime() + 1000 * signatureExpirationTime)));
+
+			content.append(" ");
+			Optional<PGPPublicKey> resolvedKey = verifiedCertifications.stream().filter(k -> k.getKeyID() == keyID)
+					.findFirst();
+
+			long keyExpirationTime = signature.getHashedSubPackets().getKeyExpirationTime();
+			content.append(keyExpirationTime == 0 || resolvedKey == null || !resolvedKey.isPresent()
+					? formattedCreationTime.replaceAll(".", "_")
+					: DATE_FORMAT.format(
+							new Date(resolvedKey.get().getCreationTime().getTime() + 1000 * keyExpirationTime)));
+
+			if (resolvedKey != null && resolvedKey.isPresent()) {
+				content.append(" ");
+				content.append(getLabel(resolvedKey.get()), StyledString.COUNTER_STYLER);
+			}
+
+			content.append("\n");
+		}
+
+		styledText.setText(content.getString());
+		styledText.setStyleRanges(content.getStyleRanges());
+
+		List<String> title = new ArrayList<>();
+		if (subKeyOf != null) {
+			long keyID = subKeyOf;
+			verifiedCertifications.stream().filter(k -> k.getKeyID() == keyID).findFirst()
+					.ifPresentOrElse(k -> title.add(getLabel(k)), () -> title.add(PGPPublicKeyService.toHex(keyID)));
+		}
+		title.add((subKeyOf == null ? "" : "sub ") + (users.isEmpty() ? fingerprint : users.get(0)));
+
+		setTitle(String.join("\n", title));
+	}
+
+	private String getLabel(PGPPublicKey key) {
+		Iterator<String> userIDs = key.getUserIDs();
+		if (userIDs.hasNext()) {
+			return userIDs.next();
+
+		}
+		return PGPPublicKeyService.toHex(key.getFingerprint()).toUpperCase(Locale.ROOT);
+	}
+
+	private void computeVerifiedCertifications(Shell shell) {
+		Display display = shell.getDisplay();
+		new Job(PGPPublicKeyViewDialog.class.getName()) {
+			{
+				setSystem(true);
+				setPriority(Job.SHORT);
+			}
+
+			@Override
+			protected IStatus run(IProgressMonitor monitor) {
+				synchronized (keyService) {
+					PGPPublicKey enhancedKey = keyService.addKey(originalKey);
+					Set<PGPPublicKey> verifiedCertifications = keyService.getVerifiedCertifications(originalKey);
+					display.asyncExec(() -> {
+						if (!shell.isDisposed()) {
+							update(enhancedKey, verifiedCertifications);
+						}
+					});
+				}
+				return Status.OK_STATUS;
+			}
+		}.schedule();
+	}
+}
diff --git a/bundles/org.eclipse.equinox.p2.ui/src/org/eclipse/equinox/internal/p2/ui/messages.properties b/bundles/org.eclipse.equinox.p2.ui/src/org/eclipse/equinox/internal/p2/ui/messages.properties
index 26fc363..1d574dc 100644
--- a/bundles/org.eclipse.equinox.p2.ui/src/org/eclipse/equinox/internal/p2/ui/messages.properties
+++ b/bundles/org.eclipse.equinox.p2.ui/src/org/eclipse/equinox/internal/p2/ui/messages.properties
@@ -12,6 +12,7 @@
 #     IBM Corporation - initial API and implementation
 ###############################################################################
 
+PGPPublicKeyViewDialog_Title=PGP Public Key Properties
 ProfileModificationAction_InvalidSelections=Problem determining user request.  Profile id: {0}, Selection count: {1}
 ProfileModificationWizardPage_DetailsLabel=Details
 ProfileSnapshots_Label=Installation History