| /******************************************************************************* |
| * Copyright (c) 2021 Red Hat Inc. |
| * |
| * 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.sdk; |
| |
| import static org.eclipse.swt.events.SelectionListener.widgetSelectedAdapter; |
| |
| import java.io.*; |
| import java.nio.file.Files; |
| import java.nio.file.Paths; |
| import java.security.cert.*; |
| import java.time.Instant; |
| import java.time.temporal.ChronoUnit; |
| import java.util.*; |
| import java.util.List; |
| import java.util.function.Function; |
| import org.bouncycastle.bcpg.ArmoredOutputStream; |
| import org.bouncycastle.openpgp.PGPPublicKey; |
| 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.viewers.CertificateLabelProvider; |
| import org.eclipse.equinox.p2.core.IProvisioningAgent; |
| import org.eclipse.equinox.p2.engine.IProfileRegistry; |
| import org.eclipse.equinox.p2.repository.spi.PGPPublicKeyService; |
| import org.eclipse.jface.dialogs.*; |
| import org.eclipse.jface.layout.TableColumnLayout; |
| import org.eclipse.jface.preference.PreferencePage; |
| import org.eclipse.jface.resource.JFaceResources; |
| import org.eclipse.jface.util.Policy; |
| import org.eclipse.jface.viewers.*; |
| import org.eclipse.jface.widgets.LabelFactory; |
| import org.eclipse.jface.widgets.WidgetFactory; |
| import org.eclipse.jface.window.Window; |
| import org.eclipse.osgi.util.NLS; |
| import org.eclipse.swt.SWT; |
| import org.eclipse.swt.dnd.*; |
| import org.eclipse.swt.events.SelectionListener; |
| import org.eclipse.swt.graphics.Image; |
| import org.eclipse.swt.graphics.Point; |
| import org.eclipse.swt.layout.GridData; |
| import org.eclipse.swt.layout.GridLayout; |
| import org.eclipse.swt.widgets.*; |
| import org.eclipse.ui.IWorkbench; |
| import org.eclipse.ui.IWorkbenchPreferencePage; |
| import org.osgi.framework.Bundle; |
| import org.osgi.framework.Constants; |
| |
| public class TrustPreferencePage extends PreferencePage implements IWorkbenchPreferencePage { |
| |
| private static final String EXPORT_FILTER_PATH = "exportFilterPath"; //$NON-NLS-1$ |
| private static final String ADD_FILTER_PATH = "addFilterPath"; //$NON-NLS-1$ |
| |
| private CertificateChecker certificateChecker; |
| private Set<Certificate> trustedCertificates; |
| private PGPPublicKeyStore trustedKeys; |
| private Map<PGPPublicKey, Set<Bundle>> contributedTrustedKeys; |
| private boolean dirty; |
| private TableViewer viewer; |
| |
| public TrustPreferencePage() { |
| super(ProvSDKMessages.TrustPreferencePage_title); |
| } |
| |
| @Override |
| public void init(IWorkbench workbench) { |
| // nothing to do |
| } |
| |
| @Override |
| protected Control createContents(Composite parent) { |
| IProvisioningAgent provisioningAgent = ProvSDKUIActivator.getDefault().getProvisioningAgent(); |
| certificateChecker = new CertificateChecker(provisioningAgent); |
| certificateChecker |
| .setProfile(provisioningAgent.getService(IProfileRegistry.class).getProfile(IProfileRegistry.SELF)); |
| trustedCertificates = new LinkedHashSet<>(certificateChecker.getPreferenceTrustedCertificates()); |
| contributedTrustedKeys = certificateChecker.getContributedTrustedKeys(); |
| trustedKeys = certificateChecker.getPreferenceTrustedKeys(); |
| |
| Composite res = new Composite(parent, SWT.NONE); |
| res.setLayout(new GridLayout(2, false)); |
| |
| // Ensure that the message supports wrapping for a long text message. |
| GridData data = new GridData(SWT.FILL, SWT.DEFAULT, true, false, 2, 1); |
| data.widthHint = convertWidthInCharsToPixels(90); |
| LabelFactory factory = WidgetFactory.label(SWT.WRAP).text(ProvSDKMessages.TrustPreferencePage_pgpIntro) |
| .font(parent.getFont()).layoutData(data); |
| factory.create(res); |
| |
| TableColumnLayout tableColumnLayout = new TableColumnLayout(); |
| Composite tableComposite = WidgetFactory.composite(SWT.NONE) |
| .layoutData(new GridData(SWT.FILL, SWT.FILL, true, true)).layout(tableColumnLayout).create(res); |
| Table keyTable = WidgetFactory.table(SWT.H_SCROLL | SWT.V_SCROLL | SWT.MULTI | SWT.BORDER | SWT.FULL_SELECTION) |
| .headerVisible(true).linesVisible(true).font(parent.getFont()).create(tableComposite); |
| viewer = new TableViewer(keyTable); |
| keyTable.setHeaderVisible(true); |
| viewer.setContentProvider(new ArrayContentProvider()); |
| |
| // This column is packed later. |
| TableViewerColumn typeColumn = createColumn(viewer, ProvSDKMessages.TrustPreferencePage_TypeColumn, |
| key -> "PGP", cert -> "x509", tableColumnLayout, 1); //$NON-NLS-1$ //$NON-NLS-2$ |
| |
| createColumn(viewer, ProvSDKMessages.TrustPreferencePage_FingerprintIdColumn, |
| key -> PGPPublicKeyService.toHex(key.getFingerprint()).toUpperCase(Locale.ROOT), |
| cert -> cert.getSerialNumber().toString(), tableColumnLayout, 10); |
| |
| createColumn(viewer, ProvSDKMessages.TrustPreferencePage_NameColumn, key -> { |
| List<String> userIds = new ArrayList<>(); |
| key.getUserIDs().forEachRemaining(userIds::add); |
| return String.join(", ", userIds); //$NON-NLS-1$ |
| }, cert -> CertificateLabelProvider.getText(cert), tableColumnLayout, 15); |
| |
| createColumn(viewer, ProvSDKMessages.TrustPreferencePage_Contributor, key -> { |
| { |
| Set<String> contributors = new LinkedHashSet<>(); |
| if (trustedKeys.all().contains(key)) { |
| contributors.add(ProvSDKMessages.TrustPreferencePage_PreferenceContributor); |
| } |
| Set<Bundle> bundles = contributedTrustedKeys.get(key); |
| if (bundles != null) { |
| Set<String> bundleContributors = new TreeSet<>(Policy.getComparator()); |
| bundles.stream().map(bundle -> getBundleName(bundle)).forEach(bundleContributors::add); |
| contributors.addAll(bundleContributors); |
| } |
| return String.join(", ", contributors); //$NON-NLS-1$ |
| } |
| }, cert -> ProvSDKMessages.TrustPreferencePage_PreferenceContributor, tableColumnLayout, |
| contributedTrustedKeys.isEmpty() ? 8 : 15); |
| |
| createColumn(viewer, ProvSDKMessages.TrustPreferencePage_ValidityColumn, pgp -> { |
| if (pgp.getCreationTime().after(Date.from(Instant.now()))) { |
| return NLS.bind(ProvSDKMessages.TrustPreferencePage_DateNotYetValid, pgp.getCreationTime()); |
| } |
| long validSeconds = pgp.getValidSeconds(); |
| if (validSeconds == 0) { |
| return ProvSDKMessages.TrustPreferencePage_DateValid; |
| } |
| Instant expires = pgp.getCreationTime().toInstant().plus(validSeconds, ChronoUnit.SECONDS); |
| return expires.isBefore(Instant.now()) ? NLS.bind(ProvSDKMessages.TrustPreferencePage_DateExpiredSince, expires) |
| : NLS.bind(ProvSDKMessages.TrustPreferencePage_DataValidExpires, expires); |
| }, x509 -> { |
| try { |
| x509.checkValidity(); |
| return ProvSDKMessages.TrustPreferencePage_DateValid; |
| } catch (CertificateExpiredException expired) { |
| return ProvSDKMessages.TrustPreferencePage_DateExpired; |
| } catch (CertificateNotYetValidException notYetValid) { |
| return ProvSDKMessages.TrustPreferencePage_DateNotYetvalid; |
| } |
| }, tableColumnLayout, 8); |
| |
| updateInput(); |
| |
| Composite buttonComposite = createVerticalButtonBar(res); |
| buttonComposite.setLayoutData(new GridData(SWT.DEFAULT, SWT.BEGINNING, false, false)); |
| |
| Button exportButton = new Button(buttonComposite, SWT.PUSH); |
| exportButton.setText(ProvSDKMessages.TrustPreferencePage_export); |
| setVerticalButtonLayoutData(exportButton); |
| exportButton.setEnabled(false); |
| |
| exportButton.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> { |
| Object element = viewer.getStructuredSelection().getFirstElement(); |
| FileDialog dialog = new FileDialog(getShell(), SWT.SAVE); |
| dialog.setFilterPath(getFilterPath(EXPORT_FILTER_PATH)); |
| dialog.setText(ProvSDKMessages.TrustPreferencePage_fileExportTitle); |
| dialog.setFilterExtensions(new String[] { "*.asc" }); //$NON-NLS-1$ |
| FileDialog destination = new FileDialog(exportButton.getShell(), SWT.SAVE); |
| destination.setFilterPath(getFilterPath(EXPORT_FILTER_PATH)); |
| destination.setText(ProvSDKMessages.TrustPreferencePage_Export); |
| if (element instanceof X509Certificate) { |
| X509Certificate cert = (X509Certificate) element; |
| destination.setFilterExtensions(new String[] { "*.der" }); //$NON-NLS-1$ |
| destination.setFileName(cert.getSerialNumber().toString() + ".der"); //$NON-NLS-1$ |
| String path = destination.open(); |
| setFilterPath(EXPORT_FILTER_PATH, destination.getFilterPath()); |
| if (path == null) { |
| return; |
| } |
| File destinationFile = new File(path); |
| try (FileOutputStream output = new FileOutputStream(destinationFile)) { |
| output.write(cert.getEncoded()); |
| } catch (IOException | CertificateEncodingException ex) { |
| ProvSDKUIActivator.getDefault().getLog() |
| .log(new Status(IStatus.ERROR, ProvSDKUIActivator.PLUGIN_ID, ex.getMessage(), ex)); |
| } |
| } else { |
| PGPPublicKey key = (PGPPublicKey) element; |
| destination.setFilterExtensions(new String[] { "*.asc" }); //$NON-NLS-1$ |
| destination.setFileName(PGPPublicKeyService.toHex(key.getFingerprint()).toUpperCase() + ".asc"); //$NON-NLS-1$ |
| String path = destination.open(); |
| setFilterPath(EXPORT_FILTER_PATH, destination.getFilterPath()); |
| if (path == null) { |
| return; |
| } |
| File destinationFile = new File(path); |
| try (OutputStream output = new ArmoredOutputStream(new FileOutputStream(destinationFile))) { |
| key.encode(output); |
| } catch (IOException ex) { |
| ProvSDKUIActivator.getDefault().getLog() |
| .log(new Status(IStatus.ERROR, ProvSDKUIActivator.PLUGIN_ID, ex.getMessage(), ex)); |
| } |
| } |
| })); |
| |
| Button addButton = new Button(buttonComposite, SWT.PUSH); |
| addButton.setText(ProvSDKMessages.TrustPreferencePage_addPGPKeyButtonLabel); |
| addButton.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> { |
| FileDialog dialog = new FileDialog(getShell(), SWT.OPEN); |
| dialog.setFilterPath(getFilterPath(ADD_FILTER_PATH)); |
| dialog.setText(ProvSDKMessages.TrustPreferencePage_fileImportTitle); |
| dialog.setFilterExtensions(new String[] { "*.asc;*.der" }); //$NON-NLS-1$ |
| String path = dialog.open(); |
| setFilterPath(ADD_FILTER_PATH, dialog.getFilterPath()); |
| if (path == null) { |
| return; |
| } |
| |
| if (path.endsWith(".der")) { //$NON-NLS-1$ |
| try { |
| CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); //$NON-NLS-1$ |
| try (InputStream input = Files.newInputStream(Paths.get(path))) { |
| Collection<? extends Certificate> certificates = certificateFactory.generateCertificates(input); |
| trustedCertificates.addAll(certificates); |
| updateInput(); |
| viewer.setSelection(new StructuredSelection(certificates.toArray()), true); |
| } |
| } catch (IOException | CertificateException ex) { |
| ProvSDKUIActivator.getDefault().getLog() |
| .log(new Status(IStatus.ERROR, ProvSDKUIActivator.PLUGIN_ID, ex.getMessage(), ex)); |
| } |
| } else { |
| HashSet<Object> oldKeys = new HashSet<>(trustedKeys.all()); |
| trustedKeys.add(new File(path)); |
| |
| HashSet<Object> newKeys = new HashSet<>(trustedKeys.all()); |
| newKeys.removeAll(oldKeys); |
| updateInput(); |
| viewer.setSelection(new StructuredSelection(newKeys.toArray()), true); |
| } |
| dirty = true; |
| })); |
| setVerticalButtonLayoutData(addButton); |
| |
| Button removeButton = new Button(buttonComposite, SWT.PUSH); |
| removeButton.setText(ProvSDKMessages.TrustPreferencePage_removePGPKeyButtonLabel); |
| removeButton.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> { |
| for (Object key : getSelectedKeys()) { |
| if (key instanceof PGPPublicKey) { |
| trustedKeys.remove((PGPPublicKey) key); |
| } else { |
| trustedCertificates.remove(key); |
| } |
| } |
| updateInput(); |
| dirty = true; |
| })); |
| removeButton.setEnabled(false); |
| setVerticalButtonLayoutData(removeButton); |
| |
| 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))); |
| }); |
| |
| Button trustAllButton = WidgetFactory.button(SWT.CHECK).text(ProvSDKMessages.TrustPreferencePage_TrustAll) |
| .font(JFaceResources.getDialogFont()).create(res); |
| setButtonLayoutData(trustAllButton).verticalSpan = 2; |
| trustAllButton.setSelection(certificateChecker.isTrustAlways()); |
| trustAllButton.addSelectionListener(widgetSelectedAdapter(e -> { |
| if (trustAllButton.getSelection()) { |
| // Prompt the user to ensure they really understand what they've chosen, the |
| // risk, and where the preference is stored if they wish to change it in the |
| // future. Also ensure that the default button is no so that they must |
| // explicitly click the yes button, not just hit enter. |
| MessageDialog messageDialog = new MessageDialog(getShell(), |
| ProvSDKMessages.TrustPreferencePage_TrustAllConfirmationTitle, null, |
| ProvSDKMessages.TrustPreferencePage_TrustAllConfirmationDescription, MessageDialog.QUESTION, |
| new String[] { ProvSDKMessages.TrustPreferencePage_TrustAllYes, |
| ProvSDKMessages.TrustPreferencePage_TrustAllNo }, |
| 1) { |
| @Override |
| public Image getImage() { |
| return getWarningImage(); |
| } |
| }; |
| int result = messageDialog.open(); |
| if (result != Window.OK) { |
| certificateChecker.setTrustAlways(false); |
| // Restore the setting. |
| trustAllButton.setSelection(false); |
| } else { |
| certificateChecker.setTrustAlways(true); |
| } |
| |
| } |
| })); |
| |
| 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); |
| } |
| }); |
| |
| typeColumn.getColumn().pack(); |
| |
| createMenu(); |
| |
| return res; |
| } |
| |
| private void createMenu() { |
| Control control = viewer.getControl(); |
| Menu menu = new Menu(control); |
| control.setMenu(menu); |
| MenuItem item = new MenuItem(menu, SWT.PUSH); |
| item.setText(ProvSDKMessages.TrustPreferencePage_CopyFingerprint); |
| item.addSelectionListener(widgetSelectedAdapter(e -> { |
| Object element = viewer.getStructuredSelection().getFirstElement(); |
| if (element instanceof PGPPublicKey) { |
| Clipboard clipboard = new Clipboard(getShell().getDisplay()); |
| clipboard.setContents(new Object[] { |
| PGPPublicKeyService.toHex(((PGPPublicKey) element).getFingerprint()).toUpperCase(Locale.ROOT) }, |
| new Transfer[] { TextTransfer.getInstance() }); |
| clipboard.dispose(); |
| } |
| })); |
| |
| viewer.addSelectionChangedListener( |
| e -> item.setEnabled(viewer.getStructuredSelection().getFirstElement() instanceof PGPPublicKey)); |
| } |
| |
| private TableViewerColumn createColumn(TableViewer tableViewer, String text, Function<PGPPublicKey, String> pgpMap, |
| Function<X509Certificate, String> x509Map, TableColumnLayout tableColumnLayout, int columnWeight) { |
| TableViewerColumn column = new TableViewerColumn(tableViewer, SWT.NONE); |
| column.getColumn().setText(text); |
| column.setLabelProvider(new PGPOrX509ColumnLabelProvider(pgpMap, x509Map)); |
| |
| tableColumnLayout.setColumnData(column.getColumn(), new ColumnWeightData(columnWeight)); |
| return column; |
| } |
| |
| private void updateInput() { |
| Set<PGPPublicKey> input = new TreeSet<>((k1, k2) -> { |
| return Arrays.compare(k1.getFingerprint(), k2.getFingerprint()); |
| }); |
| input = new LinkedHashSet<>(); |
| Collection<PGPPublicKey> all = trustedKeys.all(); |
| input = new TreeSet<>((k1, k2) -> { |
| boolean contains1 = all.contains(k1); |
| boolean contains2 = all.contains(k2); |
| if (contains1 != contains2) { |
| if (contains1) { |
| return -1; |
| } |
| return 1; |
| } |
| return PGPPublicKeyService.toHex(k1.getFingerprint()) |
| .compareTo(PGPPublicKeyService.toHex(k2.getFingerprint())); |
| }); |
| input.addAll(all); |
| input.addAll(contributedTrustedKeys.keySet()); |
| |
| LinkedHashSet<Object> allInput = new LinkedHashSet<>(); |
| allInput.addAll(trustedCertificates); |
| allInput.addAll(input); |
| viewer.setInput(allInput); |
| } |
| |
| @SuppressWarnings("unchecked") |
| private List<Object> getSelectedKeys() { |
| return viewer.getStructuredSelection().toList(); |
| } |
| |
| private Composite createVerticalButtonBar(Composite parent) { |
| // Create composite. |
| Composite composite = new Composite(parent, SWT.NONE); |
| initializeDialogUnits(composite); |
| |
| // create a layout with spacing and margins appropriate for the font |
| // size. |
| GridLayout layout = new GridLayout(); |
| layout.numColumns = 1; |
| layout.marginWidth = 5; |
| layout.marginHeight = 0; |
| layout.horizontalSpacing = convertHorizontalDLUsToPixels(IDialogConstants.HORIZONTAL_SPACING); |
| layout.verticalSpacing = convertVerticalDLUsToPixels(IDialogConstants.VERTICAL_SPACING); |
| composite.setLayout(layout); |
| |
| return composite; |
| } |
| |
| private GridData setVerticalButtonLayoutData(Button button) { |
| GridData data = new GridData(GridData.HORIZONTAL_ALIGN_FILL); |
| int widthHint = convertHorizontalDLUsToPixels(IDialogConstants.BUTTON_WIDTH); |
| Point minSize = button.computeSize(SWT.DEFAULT, SWT.DEFAULT, true); |
| data.widthHint = Math.max(widthHint, minSize.x); |
| button.setLayoutData(data); |
| return data; |
| } |
| |
| private String getFilterPath(String key) { |
| IDialogSettings dialogSettings = DialogSettings |
| .getOrCreateSection(ProvSDKUIActivator.getDefault().getDialogSettings(), getClass().getName()); |
| String filterPath = dialogSettings.get(key); |
| if (filterPath == null) { |
| filterPath = System.getProperty("user.home"); //$NON-NLS-1$ |
| } |
| return filterPath; |
| } |
| |
| private void setFilterPath(String key, String filterPath) { |
| if (filterPath != null) { |
| IDialogSettings dialogSettings = DialogSettings |
| .getOrCreateSection(ProvSDKUIActivator.getDefault().getDialogSettings(), getClass().getName()); |
| dialogSettings.put(key, filterPath); |
| } |
| } |
| |
| private String getBundleName(Bundle bundle) { |
| String value = bundle.getHeaders().get(Constants.BUNDLE_NAME); |
| return value == null ? bundle.getSymbolicName() : Platform.getResourceString(bundle, value); |
| } |
| |
| @Override |
| protected void performDefaults() { |
| trustedCertificates = new LinkedHashSet<>(certificateChecker.getPreferenceTrustedCertificates()); |
| trustedKeys = certificateChecker.getPreferenceTrustedKeys(); |
| updateInput(); |
| super.performDefaults(); |
| } |
| |
| @Override |
| public boolean performOk() { |
| if (dirty) { |
| IStatus persistTrustedCertificates = certificateChecker.persistTrustedCertificates(trustedCertificates); |
| IStatus persistTrustedKeys = certificateChecker.persistTrustedKeys(trustedKeys); |
| dirty = false; |
| return persistTrustedKeys.isOK() && persistTrustedCertificates.isOK(); |
| } |
| return true; |
| } |
| |
| private static class PGPOrX509ColumnLabelProvider extends ColumnLabelProvider { |
| private Function<PGPPublicKey, String> pgpMap; |
| private Function<X509Certificate, String> x509map; |
| |
| public PGPOrX509ColumnLabelProvider(Function<PGPPublicKey, String> pgpMap, |
| Function<X509Certificate, String> x509map) { |
| this.pgpMap = pgpMap; |
| this.x509map = x509map; |
| } |
| |
| @Override |
| public String getText(Object element) { |
| if (element instanceof PGPPublicKey) { |
| return pgpMap.apply((PGPPublicKey) element); |
| } |
| if (element instanceof X509Certificate) { |
| return x509map.apply((X509Certificate) element); |
| } |
| return super.getText(element); |
| } |
| } |
| |
| } |