From 02d6ff2b092a75a450d2bfb3d8cd45f74872b051 Mon Sep 17 00:00:00 2001 From: Carlos Ortiz Date: Wed, 11 Sep 2024 20:03:29 -0600 Subject: [PATCH 1/5] Add PrefillInputDialog and refactor CertificateHelper Introduced PrefillInputDialog for providing user input with default values. Refactored CertificateHelper to be a service and updated method implementations to support this change. Modified JKSView, PEMFileEditor, and ImportCert to utilize the updated CertificateHelper service and dialog. --- .../com/jmpeax/ssltoolbox/jks/JKSView.java | 11 ++- .../ssltoolbox/jks/actions/ImportCert.java | 28 ++++--- .../jks/actions/PrefillInputDialog.java | 43 ++++++++++ .../jmpeax/ssltoolbox/pem/PEMFileEditor.java | 15 ++-- .../ssltoolbox/svc/CertificateHelper.java | 84 ++++++++++++++----- 5 files changed, 141 insertions(+), 40 deletions(-) create mode 100644 src/main/java/com/jmpeax/ssltoolbox/jks/actions/PrefillInputDialog.java diff --git a/src/main/java/com/jmpeax/ssltoolbox/jks/JKSView.java b/src/main/java/com/jmpeax/ssltoolbox/jks/JKSView.java index db522f4..f66d9a2 100644 --- a/src/main/java/com/jmpeax/ssltoolbox/jks/JKSView.java +++ b/src/main/java/com/jmpeax/ssltoolbox/jks/JKSView.java @@ -5,10 +5,8 @@ import com.intellij.openapi.actionSystem.ActionManager; import com.intellij.openapi.actionSystem.ActionToolbar; import com.intellij.openapi.actionSystem.DataContext; -import com.intellij.openapi.fileChooser.FileChooser; -import com.intellij.openapi.fileChooser.FileChooserDescriptor; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.ui.Messages; +import com.intellij.openapi.application.ApplicationManager; + import com.intellij.openapi.vfs.VirtualFile; import com.intellij.ui.components.JBLabel; import com.intellij.ui.components.JBList; @@ -18,7 +16,7 @@ import com.jmpeax.ssltoolbox.svc.CertificateHelper; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.slf4j.LoggerFactory; + import javax.swing.*; import java.awt.*; @@ -142,7 +140,8 @@ private JPanel createUnlockButton() { char[] password = passwordField.getPassword(); if (password != null) { try { - this.certs = CertificateHelper.getKeyStoreCerts(file.getInputStream(), password); + var certificateHelper = ApplicationManager.getApplication().getService(CertificateHelper.class); + this.certs = certificateHelper.getKeyStoreCerts(file.getInputStream(), password); updateView(certs); panel.removeAll(); panel.add(buildToolBar()); diff --git a/src/main/java/com/jmpeax/ssltoolbox/jks/actions/ImportCert.java b/src/main/java/com/jmpeax/ssltoolbox/jks/actions/ImportCert.java index b4654b9..2513e21 100644 --- a/src/main/java/com/jmpeax/ssltoolbox/jks/actions/ImportCert.java +++ b/src/main/java/com/jmpeax/ssltoolbox/jks/actions/ImportCert.java @@ -1,10 +1,10 @@ package com.jmpeax.ssltoolbox.jks.actions; -import com.intellij.icons.AllIcons; + import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.CommonDataKeys; -import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.fileChooser.FileChooser; import com.intellij.openapi.fileChooser.FileChooserDescriptor; import com.intellij.openapi.ui.Messages; @@ -16,14 +16,13 @@ public class ImportCert extends AnAction { - private static final Logger LOGGER = Logger.getInstance(ImportCert.class); - @Override public void actionPerformed(@NotNull AnActionEvent e) { try { - var f = e.getData(CommonDataKeys.VIRTUAL_FILE); - if (f == null) { - LOGGER.warn("JKS not send to the Action"); + var certificateHelper = ApplicationManager.getApplication().getService(CertificateHelper.class); + var ksVirtualFile = e.getData(CommonDataKeys.VIRTUAL_FILE); + if (ksVirtualFile == null) { + Messages.showErrorDialog("No Keystore file", "No Keystore File"); return; } var descriptor = new FileChooserDescriptor( @@ -36,13 +35,22 @@ public void actionPerformed(@NotNull AnActionEvent e) { ); VirtualFile file = FileChooser.chooseFile(descriptor, null, null); if (file == null) { - LOGGER.warn("User did not select a certificate"); + Messages.showErrorDialog("No certificate selected", "No Certificate Selected"); + return; + } + + var certAlias = certificateHelper.getCertificate(file) + .stream().findFirst().map(certificateHelper::getCommonName).orElse(""); + var selectedAlias = new PrefillInputDialog(certAlias, "Certificate Alias", "Certificate Alias"); + var ok = selectedAlias.showAndGet(); + if (!ok) { + Messages.showErrorDialog("No alias provided", "No Alias Provided"); return; } - Messages.showInputDialog("Cert alias","Certificate Alias", Messages.getQuestionIcon()); var pwd = Messages.showPasswordDialog("Keystore password", "KeyStore Password"); if (pwd != null && !pwd.isBlank()) { - CertificateHelper.importCertificate(f.getInputStream(), file.getInputStream(), pwd.toCharArray()); + certificateHelper.importCertificate(ksVirtualFile, file,selectedAlias.getInput(), pwd.toCharArray()); + Messages.showInfoMessage("Certificate imported", "Certificate Imported"); } } catch (IOException ex) { throw new RuntimeException(ex); diff --git a/src/main/java/com/jmpeax/ssltoolbox/jks/actions/PrefillInputDialog.java b/src/main/java/com/jmpeax/ssltoolbox/jks/actions/PrefillInputDialog.java new file mode 100644 index 0000000..42e6339 --- /dev/null +++ b/src/main/java/com/jmpeax/ssltoolbox/jks/actions/PrefillInputDialog.java @@ -0,0 +1,43 @@ +package com.jmpeax.ssltoolbox.jks.actions; + +import com.intellij.openapi.ui.DialogWrapper; +import com.intellij.openapi.ui.ValidationInfo; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; + +public class PrefillInputDialog extends DialogWrapper { + private final JTextField textField; + private final String inputLabel; + + public PrefillInputDialog(String initialText,String title,String inputLabel) { + super(false); + this.textField = new JTextField(initialText); + this.inputLabel = inputLabel; + init(); + setTitle(title); + } + + @Nullable + @Override + protected JComponent createCenterPanel() { + JPanel panel = new JPanel(new BorderLayout()); + panel.add(new JLabel(this.inputLabel), BorderLayout.NORTH); + panel.add(textField, BorderLayout.CENTER); + return panel; + } + + @Nullable + @Override + protected ValidationInfo doValidate() { + if (textField.getText().trim().isEmpty()) { + return new ValidationInfo("Input cannot be empty", textField); + } + return super.doValidate(); + } + + public String getInput() { + return textField.getText(); + } +} \ No newline at end of file diff --git a/src/main/java/com/jmpeax/ssltoolbox/pem/PEMFileEditor.java b/src/main/java/com/jmpeax/ssltoolbox/pem/PEMFileEditor.java index ae9e865..1c6d8a7 100644 --- a/src/main/java/com/jmpeax/ssltoolbox/pem/PEMFileEditor.java +++ b/src/main/java/com/jmpeax/ssltoolbox/pem/PEMFileEditor.java @@ -2,6 +2,7 @@ import com.intellij.diff.util.FileEditorBase; import com.intellij.icons.AllIcons; +import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.ui.components.JBTabbedPane; import com.jmpeax.ssltoolbox.svc.CertificateHelper; @@ -15,24 +16,28 @@ import java.security.cert.X509Certificate; public class PEMFileEditor extends FileEditorBase { + private final VirtualFile file; private final JBTabbedPane panel; + + public PEMFileEditor(@NotNull VirtualFile file) throws CertificateException, IOException { this.file = file; this.panel = new JBTabbedPane(); - var c = CertificateHelper.getCertificate(file.getInputStream()); + var certificateHelper = ApplicationManager.getApplication().getService(CertificateHelper.class); + var c = certificateHelper.getCertificate(file); if (c.isEmpty()){ this.panel.add(new JLabel("Error loading " + file.getName())); }else { - c.forEach(this::buildTabbedPane); + c.forEach(x509Certificate -> buildTabbedPane(certificateHelper, x509Certificate)); } } - private void buildTabbedPane(X509Certificate x509Certificate) { - var name = CertificateHelper.getCommonName(x509Certificate); - if (CertificateHelper.isValid(x509Certificate)) { + private void buildTabbedPane(CertificateHelper certificateHelper,X509Certificate x509Certificate) { + var name = certificateHelper.getCommonName(x509Certificate); + if (certificateHelper.isValid(x509Certificate)) { this.panel.addTab(name, new PemView(x509Certificate)); } else { this.panel.addTab(name, AllIcons.Ide.FatalError, new PemView(x509Certificate)); diff --git a/src/main/java/com/jmpeax/ssltoolbox/svc/CertificateHelper.java b/src/main/java/com/jmpeax/ssltoolbox/svc/CertificateHelper.java index 3f8cf5f..f28fa04 100644 --- a/src/main/java/com/jmpeax/ssltoolbox/svc/CertificateHelper.java +++ b/src/main/java/com/jmpeax/ssltoolbox/svc/CertificateHelper.java @@ -1,12 +1,17 @@ package com.jmpeax.ssltoolbox.svc; +import com.intellij.openapi.components.Service; import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.vfs.VirtualFile; import org.jetbrains.annotations.NotNull; import javax.security.auth.x500.X500Principal; -import java.io.FileInputStream; -import java.io.InputStream; +import java.io.*; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; @@ -22,16 +27,17 @@ * This class contains the following methods: *

* 1. getCertificate - This static method takes an InputStream containing a certificate and returns a Set of X509Certificates. It uses the X.509 CertificateFactory to generate the - * certificates from the input stream. + * certificates from the input stream. *

* 2. getCommonName - This static method takes an X509Certificate and returns the Common Name (CN) from the subject of the certificate. It uses regular expressions to extract the - * CN from the subject's name. + * CN from the subject's name. *

* 3. isValid - This static method takes an X509Certificate and checks if it is valid. It uses the checkValidity() method of the certificate to perform the validation. *

* Please note that this class relies on a logger instance from the Logger class for logging purposes. */ -public class CertificateHelper { +@Service() +public final class CertificateHelper { private static final Logger LOGGER = Logger.getInstance(CertificateHelper.class); @@ -41,14 +47,12 @@ public class CertificateHelper { * @param certificateInput the input stream from which to retrieve the certificates * @return a set of X.509 certificates obtained from the input stream */ - public static Set getCertificate(InputStream certificateInput){ - try { + public @NotNull Set getCertificate(@NotNull VirtualFile certificateInput) { + try (var is = certificateInput.getInputStream()) { var fac = CertificateFactory.getInstance("X.509"); - return fac.generateCertificates(certificateInput).stream() - .map(c -> (X509Certificate) c) - .collect(Collectors.toCollection(LinkedHashSet::new)); - } catch (CertificateException e) { - LOGGER.error("Unable to create Certification Factory",e); + return fac.generateCertificates(is).stream().map(c -> (X509Certificate) c).collect(Collectors.toCollection(LinkedHashSet::new)); + } catch (IOException | CertificateException e) { + LOGGER.error("Unable to create Certification Factory", e); } return Set.of(); } @@ -60,7 +64,7 @@ public static Set getCertificate(InputStream certificateInput){ * @return the Common Name (CN) extracted from the subject of the certificate */ @NotNull - public static String getCommonName(@NotNull X509Certificate certificate) { + public String getCommonName(@NotNull X509Certificate certificate) { X500Principal principal = certificate.getSubjectX500Principal(); String subject = principal.getName(); Pattern pattern = Pattern.compile("CN=([^,]*)"); @@ -77,7 +81,7 @@ public static String getCommonName(@NotNull X509Certificate certificate) { * @param certificate the X509Certificate to be checked for validity * @return true if the certificate is valid, false otherwise */ - public static boolean isValid(@NotNull X509Certificate certificate) { + public boolean isValid(@NotNull X509Certificate certificate) { try { certificate.checkValidity(); return true; @@ -87,11 +91,17 @@ public static boolean isValid(@NotNull X509Certificate certificate) { return false; } - public static Map getKeyStoreCerts(InputStream keystoreIS, char[] keystorePassword) { - Map certs = new LinkedHashMap<>(); + /** + * Retrieves a map of X.509 certificates from the given keystore input stream. + * + * @param keystoreIS the input stream from which to load the keystore + * @param keystorePassword the password used to protect the integrity of the keystore + * @return a map where the keys are certificate aliases and the values are X.509 certificates + */ + public @NotNull Map getKeyStoreCerts(@NotNull InputStream keystoreIS, char[] keystorePassword) { + Map certs = new LinkedHashMap<>(); try { - KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); - keystore.load(keystoreIS, keystorePassword); + var keystore = openKeyStore(keystoreIS, keystorePassword); Enumeration aliases = keystore.aliases(); aliases.asIterator().forEachRemaining(alias -> { try { @@ -109,7 +119,43 @@ public static Map getKeyStoreCerts(InputStream keystoreI return certs; } - public static void importCertificate(@NotNull InputStream inputStream, @NotNull InputStream inputStream1, char[] charArray) { + /** + * Opens a keystore from the specified input stream using the provided password. + * + * @param keystoreIS the input stream from which to load the keystore + * @param keystorePassword the password used to protect the integrity of the keystore + * @return a loaded KeyStore instance + * @throws KeyStoreException if no Provider supports a KeyStoreSpi implementation for the specified type + * @throws CertificateException if any of the certificates in the keystore could not be loaded + * @throws IOException if there is an I/O or format problem with the keystore data + * @throws NoSuchAlgorithmException if the algorithm used to check the integrity of the keystore cannot be found + */ + private KeyStore openKeyStore(@NotNull InputStream keystoreIS, char[] keystorePassword) throws KeyStoreException, CertificateException, IOException, NoSuchAlgorithmException { + KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); + keystore.load(keystoreIS, keystorePassword); + return keystore; + } + + /** + * Imports a certificate into a keystore file. + * + * @param keyStoreFile the keystore file into which the certificate will be imported + * @param certToImport the certificate file to be imported + * @param alias the alias under which the certificate will be stored + * @param password the password for the keystore + * @throws IOException if an I/O error occurs during the operation + */ + public void importCertificate(@NotNull VirtualFile keyStoreFile, @NotNull VirtualFile certToImport, @NotNull String alias, char[] password) throws IOException { + try (var keystoreIS = keyStoreFile.getInputStream()) { + var keystore = openKeyStore(keystoreIS, password); + var cert = getCertificate(certToImport).stream().findFirst().orElseThrow(); + keystore.setCertificateEntry(alias, cert); + var out = Files.newOutputStream(keyStoreFile.toNioPath(), StandardOpenOption.WRITE); + keyStoreFile.refresh(false, false); + keystore.store(out, password); + } catch (Exception e) { + LOGGER.error("Error importing certificate", e); + } } } From a46ba8d639929075eae0fa2a4a6035b1ad4ec440 Mon Sep 17 00:00:00 2001 From: Carlos Ortiz Date: Thu, 12 Sep 2024 18:32:09 -0600 Subject: [PATCH 2/5] ssl-toolbox-2 [FEAT]: Import Certificates to the JKS Keystore --- .../com/jmpeax/ssltoolbox/jks/JKSView.java | 49 ++++--------------- .../ssltoolbox/jks/actions/ImportCert.java | 4 +- .../jks/actions/ListenerDataContext.java | 37 ++++++++++++++ src/main/resources/META-INF/plugin.xml | 1 + 4 files changed, 50 insertions(+), 41 deletions(-) create mode 100644 src/main/java/com/jmpeax/ssltoolbox/jks/actions/ListenerDataContext.java diff --git a/src/main/java/com/jmpeax/ssltoolbox/jks/JKSView.java b/src/main/java/com/jmpeax/ssltoolbox/jks/JKSView.java index f66d9a2..0a2544d 100644 --- a/src/main/java/com/jmpeax/ssltoolbox/jks/JKSView.java +++ b/src/main/java/com/jmpeax/ssltoolbox/jks/JKSView.java @@ -1,10 +1,8 @@ package com.jmpeax.ssltoolbox.jks; import com.intellij.icons.AllIcons; -import com.intellij.openapi.actionSystem.ActionGroup; -import com.intellij.openapi.actionSystem.ActionManager; -import com.intellij.openapi.actionSystem.ActionToolbar; -import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.Disposable; +import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.vfs.VirtualFile; @@ -12,6 +10,8 @@ import com.intellij.ui.components.JBList; import com.intellij.ui.components.JBPasswordField; import com.intellij.util.ui.JBUI; +import com.jmpeax.ssltoolbox.jks.actions.ImportCert; +import com.jmpeax.ssltoolbox.jks.actions.ListenerDataContext; import com.jmpeax.ssltoolbox.pem.PemView; import com.jmpeax.ssltoolbox.svc.CertificateHelper; import org.jetbrains.annotations.NotNull; @@ -159,10 +159,12 @@ private JPanel buildToolBar() { ActionGroup actionGroup = (ActionGroup) ActionManager.getInstance().getAction("JKS-Actions"); ActionToolbar actionToolBar = ActionManager.getInstance().createActionToolbar("JKS-Actions-Toolbar", actionGroup, true); actionToolBar.setTargetComponent(this); + DataContext dataContext = new ListenerDataContext(file, (alias, cert) -> { - DataContext dataContext = dataId -> this.file; + }) actionToolBar.getComponent().putClientProperty(DataContext.class, dataContext); + JPanel panel = new JPanel(new GridLayout(1,1)); GridBagConstraints gbc = new GridBagConstraints(); gbc.insets = JBUI.insets(5); @@ -172,39 +174,7 @@ private JPanel buildToolBar() { gbc.gridy = 0; panel.add(actionToolBar.getComponent(), gbc); return panel; -// -// JButton openButton = new JButton(AllIcons.ToolbarDecorator.Import); -// openButton.setToolTipText("Open"); -// openButton.setPreferredSize(new Dimension(AllIcons.ToolbarDecorator.Import.getIconWidth() + 10, AllIcons.ToolbarDecorator.Import.getIconHeight() + 10)); -// openButton.setBorderPainted(false); -// openButton.addActionListener(e -> { -// var descriptor = new FileChooserDescriptor( -// true, // Choose Files -// false, -// false, -// false, -// false, -// false -// ); -// VirtualFile file = FileChooser.chooseFile(descriptor, null, null); -// if (file != null) { -// var str = Messages.showInputDialog("Enter Alias for " + file.getName(), "Alias for Imported Certificate", null); -// LoggerFactory.getLogger(JKSView.class).info("Selected file: alias {} {}", str, file.getPath()); -// this.listModel.addElement(str); -// } -// }); -// gbc.gridx = 0; -// gbc.gridy = 0; -// panel.add(openButton, gbc); -// JButton saveButton = new JButton(AllIcons.ToolbarDecorator.Export); -// saveButton.setPreferredSize(new Dimension(AllIcons.ToolbarDecorator.Export.getIconWidth() + 10, AllIcons.ToolbarDecorator.Export.getIconHeight() + 10)); -// saveButton.setToolTipText("Save"); -// saveButton.addActionListener(e -> { -// }); -// gbc.gridx = 1; -// panel.add(saveButton, gbc); -// -// return panel; + } private void updateView(Map certs) { @@ -222,8 +192,7 @@ private void updateView(Map certs) { }); listPanel.removeAll(); listPanel.add(new JScrollPane(list), BorderLayout.CENTER); -// revalidate(); -// repaint(); + } public @Nullable JComponent getUnlockText() { diff --git a/src/main/java/com/jmpeax/ssltoolbox/jks/actions/ImportCert.java b/src/main/java/com/jmpeax/ssltoolbox/jks/actions/ImportCert.java index 2513e21..4080c03 100644 --- a/src/main/java/com/jmpeax/ssltoolbox/jks/actions/ImportCert.java +++ b/src/main/java/com/jmpeax/ssltoolbox/jks/actions/ImportCert.java @@ -16,6 +16,7 @@ public class ImportCert extends AnAction { + @Override public void actionPerformed(@NotNull AnActionEvent e) { try { @@ -38,7 +39,6 @@ public void actionPerformed(@NotNull AnActionEvent e) { Messages.showErrorDialog("No certificate selected", "No Certificate Selected"); return; } - var certAlias = certificateHelper.getCertificate(file) .stream().findFirst().map(certificateHelper::getCommonName).orElse(""); var selectedAlias = new PrefillInputDialog(certAlias, "Certificate Alias", "Certificate Alias"); @@ -51,7 +51,9 @@ public void actionPerformed(@NotNull AnActionEvent e) { if (pwd != null && !pwd.isBlank()) { certificateHelper.importCertificate(ksVirtualFile, file,selectedAlias.getInput(), pwd.toCharArray()); Messages.showInfoMessage("Certificate imported", "Certificate Imported"); + } + } catch (IOException ex) { throw new RuntimeException(ex); } diff --git a/src/main/java/com/jmpeax/ssltoolbox/jks/actions/ListenerDataContext.java b/src/main/java/com/jmpeax/ssltoolbox/jks/actions/ListenerDataContext.java new file mode 100644 index 0000000..fc2779b --- /dev/null +++ b/src/main/java/com/jmpeax/ssltoolbox/jks/actions/ListenerDataContext.java @@ -0,0 +1,37 @@ +package com.jmpeax.ssltoolbox.jks.actions; + +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataKey; +import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.security.cert.X509Certificate; + +public class ListenerDataContext implements DataContext { + private final VirtualFile file; + private final OnImport onImport; + + public ListenerDataContext(VirtualFile file,OnImport onImport) { + this.file = file; + this.onImport = onImport; + } + + @Override + public @Nullable Object getData(@NotNull String dataId) { + return file; + } + + @Override + public @Nullable T getData(@NotNull DataKey key) { + return DataContext.super.getData(key); + } + + public void update(String alias, X509Certificate certificate){ + onImport.imported(alias,certificate); + } + + public interface OnImport { + void imported(String alias, X509Certificate cert); + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index ed92e04..6f8db05 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -54,6 +54,7 @@ extensions="pem;cer;der;crt;ca-bundle;p7b;p7c"/> + From 5ab16917a882882234a7d3da94e39df03f696003 Mon Sep 17 00:00:00 2001 From: Carlos Ortiz Date: Thu, 12 Sep 2024 18:39:49 -0600 Subject: [PATCH 3/5] Refactor data context handling and interface visibility Simplify DataContext setup in JKSView and refine OnImport interface visibility. This change includes removing an unused import and adding a method to retrieve the view component in ImportCert. --- src/main/java/com/jmpeax/ssltoolbox/jks/JKSView.java | 5 +---- .../com/jmpeax/ssltoolbox/jks/actions/ImportCert.java | 11 ++++++++++- .../ssltoolbox/jks/actions/ListenerDataContext.java | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/jmpeax/ssltoolbox/jks/JKSView.java b/src/main/java/com/jmpeax/ssltoolbox/jks/JKSView.java index 0a2544d..484021f 100644 --- a/src/main/java/com/jmpeax/ssltoolbox/jks/JKSView.java +++ b/src/main/java/com/jmpeax/ssltoolbox/jks/JKSView.java @@ -11,7 +11,6 @@ import com.intellij.ui.components.JBPasswordField; import com.intellij.util.ui.JBUI; import com.jmpeax.ssltoolbox.jks.actions.ImportCert; -import com.jmpeax.ssltoolbox.jks.actions.ListenerDataContext; import com.jmpeax.ssltoolbox.pem.PemView; import com.jmpeax.ssltoolbox.svc.CertificateHelper; import org.jetbrains.annotations.NotNull; @@ -159,9 +158,7 @@ private JPanel buildToolBar() { ActionGroup actionGroup = (ActionGroup) ActionManager.getInstance().getAction("JKS-Actions"); ActionToolbar actionToolBar = ActionManager.getInstance().createActionToolbar("JKS-Actions-Toolbar", actionGroup, true); actionToolBar.setTargetComponent(this); - DataContext dataContext = new ListenerDataContext(file, (alias, cert) -> { - - }) + DataContext dataContext = dataId -> this.file; actionToolBar.getComponent().putClientProperty(DataContext.class, dataContext); diff --git a/src/main/java/com/jmpeax/ssltoolbox/jks/actions/ImportCert.java b/src/main/java/com/jmpeax/ssltoolbox/jks/actions/ImportCert.java index 4080c03..3cfb352 100644 --- a/src/main/java/com/jmpeax/ssltoolbox/jks/actions/ImportCert.java +++ b/src/main/java/com/jmpeax/ssltoolbox/jks/actions/ImportCert.java @@ -4,11 +4,13 @@ import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.actionSystem.PlatformDataKeys; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.fileChooser.FileChooser; import com.intellij.openapi.fileChooser.FileChooserDescriptor; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.vfs.VirtualFile; +import com.jmpeax.ssltoolbox.jks.JKSView; import com.jmpeax.ssltoolbox.svc.CertificateHelper; import org.jetbrains.annotations.NotNull; @@ -51,11 +53,18 @@ public void actionPerformed(@NotNull AnActionEvent e) { if (pwd != null && !pwd.isBlank()) { certificateHelper.importCertificate(ksVirtualFile, file,selectedAlias.getInput(), pwd.toCharArray()); Messages.showInfoMessage("Certificate imported", "Certificate Imported"); - + getViewComponent(e); } } catch (IOException ex) { throw new RuntimeException(ex); } } + + private JKSView getViewComponent(@NotNull AnActionEvent e) { + // Retrieve your view component from context, e.g., using the data context from the event + return e.getData(PlatformDataKeys.CONTEXT_COMPONENT) instanceof JKSView + ? (JKSView) e.getData(PlatformDataKeys.CONTEXT_COMPONENT) + : null; + } } diff --git a/src/main/java/com/jmpeax/ssltoolbox/jks/actions/ListenerDataContext.java b/src/main/java/com/jmpeax/ssltoolbox/jks/actions/ListenerDataContext.java index fc2779b..cefff6f 100644 --- a/src/main/java/com/jmpeax/ssltoolbox/jks/actions/ListenerDataContext.java +++ b/src/main/java/com/jmpeax/ssltoolbox/jks/actions/ListenerDataContext.java @@ -31,7 +31,7 @@ public void update(String alias, X509Certificate certificate){ onImport.imported(alias,certificate); } - public interface OnImport { + interface OnImport { void imported(String alias, X509Certificate cert); } } From d11a6a10c314462bcf6a5d1a94a570ebde6b81a6 Mon Sep 17 00:00:00 2001 From: Carlos Ortiz Date: Thu, 12 Sep 2024 20:27:43 -0600 Subject: [PATCH 4/5] Add export certificate functionality Added the ability to export certificates from the JKS store. Updated the plugin.xml to include the export action and implemented relevant methods in JKSView and CertificateHelper to handle the export process. --- .../com/jmpeax/ssltoolbox/jks/JKSView.java | 21 +++++-- .../ssltoolbox/jks/actions/ExportCert.java | 58 +++++++++++++++++++ .../ssltoolbox/jks/actions/ImportCert.java | 4 +- .../ssltoolbox/svc/CertificateHelper.java | 13 +++++ src/main/resources/META-INF/plugin.xml | 6 +- 5 files changed, 95 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/jmpeax/ssltoolbox/jks/actions/ExportCert.java diff --git a/src/main/java/com/jmpeax/ssltoolbox/jks/JKSView.java b/src/main/java/com/jmpeax/ssltoolbox/jks/JKSView.java index 484021f..1dcf5c5 100644 --- a/src/main/java/com/jmpeax/ssltoolbox/jks/JKSView.java +++ b/src/main/java/com/jmpeax/ssltoolbox/jks/JKSView.java @@ -1,7 +1,6 @@ package com.jmpeax.ssltoolbox.jks; import com.intellij.icons.AllIcons; -import com.intellij.openapi.Disposable; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.application.ApplicationManager; @@ -10,7 +9,6 @@ import com.intellij.ui.components.JBList; import com.intellij.ui.components.JBPasswordField; import com.intellij.util.ui.JBUI; -import com.jmpeax.ssltoolbox.jks.actions.ImportCert; import com.jmpeax.ssltoolbox.pem.PemView; import com.jmpeax.ssltoolbox.svc.CertificateHelper; import org.jetbrains.annotations.NotNull; @@ -28,6 +26,7 @@ import java.util.Set; public class JKSView extends JPanel { + private JBList list; private PemView pemView; private final VirtualFile file; @@ -141,7 +140,7 @@ private JPanel createUnlockButton() { try { var certificateHelper = ApplicationManager.getApplication().getService(CertificateHelper.class); this.certs = certificateHelper.getKeyStoreCerts(file.getInputStream(), password); - updateView(certs); + addCertificate(certs); panel.removeAll(); panel.add(buildToolBar()); revalidate(); @@ -174,7 +173,7 @@ private JPanel buildToolBar() { } - private void updateView(Map certs) { + private void addCertificate(Map certs) { certs.keySet().forEach(listModel::addElement); list = new JBList<>(listModel); list.setCellRenderer(new IconListRenderer()); @@ -189,7 +188,6 @@ private void updateView(Map certs) { }); listPanel.removeAll(); listPanel.add(new JScrollPane(list), BorderLayout.CENTER); - } public @Nullable JComponent getUnlockText() { @@ -204,4 +202,17 @@ public Component getListCellRendererComponent(JList list, Object value, int i return label; } } + + public void addCertificate(String alias, X509Certificate certificate) { + listModel.addElement(alias); + certs.put(alias, certificate); + } + + public String getSelectedCertificate() { + return list.getSelectedValue(); + } + public void removeCertificate(String alias) { + listModel.remove(listModel.indexOf(alias)); + certs.remove(alias); + } } \ No newline at end of file diff --git a/src/main/java/com/jmpeax/ssltoolbox/jks/actions/ExportCert.java b/src/main/java/com/jmpeax/ssltoolbox/jks/actions/ExportCert.java new file mode 100644 index 0000000..ffc95e6 --- /dev/null +++ b/src/main/java/com/jmpeax/ssltoolbox/jks/actions/ExportCert.java @@ -0,0 +1,58 @@ +package com.jmpeax.ssltoolbox.jks.actions; + + +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.actionSystem.PlatformDataKeys; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.fileChooser.FileChooser; +import com.intellij.openapi.fileChooser.FileChooserDescriptor; +import com.intellij.openapi.ui.Messages; +import com.intellij.openapi.vfs.VirtualFile; +import com.jmpeax.ssltoolbox.jks.JKSView; +import com.jmpeax.ssltoolbox.svc.CertificateHelper; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.util.Objects; + +public class ExportCert extends AnAction { + + + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + + var certificateHelper = ApplicationManager.getApplication().getService(CertificateHelper.class); + var ksVirtualFile = e.getData(CommonDataKeys.VIRTUAL_FILE); + var view = getViewComponent(e); + if (view == null) { + Messages.showErrorDialog("No JKS view found", "No JKS View"); + } + var selectedAlias = view.getSelectedCertificate(); + if (selectedAlias == null) { + Messages.showErrorDialog("No certificate selected", "No Certificate Selected"); + } + var descriptor = new FileChooserDescriptor( + false, // Choose Files + true, + false, + false, + false, + false + ); + var pwd = Messages.showPasswordDialog("Keystore password", "KeyStore Password"); + if (pwd != null && !pwd.isBlank()) { + certificateHelper.exportCertificate(ksVirtualFile, selectedAlias, pwd.toCharArray()); + Messages.showInfoMessage("Certificate exported", "Certificate Exported"); + view.removeCertificate(selectedAlias); + } + } + + private JKSView getViewComponent(@NotNull AnActionEvent e) { + // Retrieve your view component from context, e.g., using the data context from the event + return e.getData(PlatformDataKeys.CONTEXT_COMPONENT) instanceof JKSView + ? (JKSView) e.getData(PlatformDataKeys.CONTEXT_COMPONENT) + : null; + } +} diff --git a/src/main/java/com/jmpeax/ssltoolbox/jks/actions/ImportCert.java b/src/main/java/com/jmpeax/ssltoolbox/jks/actions/ImportCert.java index 3cfb352..7fa398a 100644 --- a/src/main/java/com/jmpeax/ssltoolbox/jks/actions/ImportCert.java +++ b/src/main/java/com/jmpeax/ssltoolbox/jks/actions/ImportCert.java @@ -15,6 +15,7 @@ import org.jetbrains.annotations.NotNull; import java.io.IOException; +import java.util.Objects; public class ImportCert extends AnAction { @@ -53,7 +54,8 @@ public void actionPerformed(@NotNull AnActionEvent e) { if (pwd != null && !pwd.isBlank()) { certificateHelper.importCertificate(ksVirtualFile, file,selectedAlias.getInput(), pwd.toCharArray()); Messages.showInfoMessage("Certificate imported", "Certificate Imported"); - getViewComponent(e); + Objects.requireNonNull(getViewComponent(e)).addCertificate(selectedAlias.getInput(), + certificateHelper.getCertificate(file).stream().findFirst().orElseThrow()); } } catch (IOException ex) { diff --git a/src/main/java/com/jmpeax/ssltoolbox/svc/CertificateHelper.java b/src/main/java/com/jmpeax/ssltoolbox/svc/CertificateHelper.java index f28fa04..b3c2a44 100644 --- a/src/main/java/com/jmpeax/ssltoolbox/svc/CertificateHelper.java +++ b/src/main/java/com/jmpeax/ssltoolbox/svc/CertificateHelper.java @@ -4,6 +4,7 @@ import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.vfs.VirtualFile; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import javax.security.auth.x500.X500Principal; import java.io.*; @@ -156,6 +157,18 @@ public void importCertificate(@NotNull VirtualFile keyStoreFile, @NotNull Virtua } catch (Exception e) { LOGGER.error("Error importing certificate", e); } + } + public @Nullable X509Certificate exportCertificate(VirtualFile ksVirtualFile, + String selectedAlias, + char[] password) { + try(var is = ksVirtualFile.getInputStream()) { + var keystore = openKeyStore(is, password); + var cert = keystore.getCertificate(selectedAlias); + return (X509Certificate) cert; + } catch (Exception e) { + LOGGER.error("Error exporting certificate", e); + return null; + } } } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 6f8db05..a699d1c 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -64,7 +64,11 @@ popup="true" icon="AllIcons.Ide.ConfigFile"> + icon="AllIcons.Ide.IncomingChangesOn"> + + From 000ac3d1ac47421a690a2d1775b50c92e367d6ab Mon Sep 17 00:00:00 2001 From: Carlos Ortiz Date: Thu, 12 Sep 2024 20:27:43 -0600 Subject: [PATCH 5/5] Add delete certificate action and enhance export functionality Introduced a new 'Delete' action for certificates in the plugin. Enhanced the export functionality to allow saving the certificate to a specified file location, and added logging for better error tracking. --- .../com/jmpeax/ssltoolbox/jks/JKSView.java | 3 +- .../ssltoolbox/jks/actions/DeletetCert.java | 47 +++++++++++++++++++ .../ssltoolbox/jks/actions/ExportCert.java | 45 ++++++++++++------ .../com/jmpeax/ssltoolbox/pem/PemView.java | 8 ++-- .../ssltoolbox/svc/CertificateHelper.java | 37 ++++++++++++++- src/main/resources/META-INF/plugin.xml | 4 ++ 6 files changed, 125 insertions(+), 19 deletions(-) create mode 100644 src/main/java/com/jmpeax/ssltoolbox/jks/actions/DeletetCert.java diff --git a/src/main/java/com/jmpeax/ssltoolbox/jks/JKSView.java b/src/main/java/com/jmpeax/ssltoolbox/jks/JKSView.java index 1dcf5c5..5ee6215 100644 --- a/src/main/java/com/jmpeax/ssltoolbox/jks/JKSView.java +++ b/src/main/java/com/jmpeax/ssltoolbox/jks/JKSView.java @@ -104,7 +104,7 @@ private JPanel createUnlockButton() { this.passwordField = new JBPasswordField(); panel.setBorder(JBUI.Borders.customLineBottom(JBUI.CurrentTheme.Toolbar.SEPARATOR_COLOR)); JButton unlockButton = getButton(passwordField, panel); - JLabel passwordLabel = new JBLabel("Enter password: "); + JBLabel passwordLabel = new JBLabel("Enter password: "); passwordField.requestFocusInWindow(); // Set focus traversal keys for the password field to move to the unlock button @@ -211,6 +211,7 @@ public void addCertificate(String alias, X509Certificate certificate) { public String getSelectedCertificate() { return list.getSelectedValue(); } + public void removeCertificate(String alias) { listModel.remove(listModel.indexOf(alias)); certs.remove(alias); diff --git a/src/main/java/com/jmpeax/ssltoolbox/jks/actions/DeletetCert.java b/src/main/java/com/jmpeax/ssltoolbox/jks/actions/DeletetCert.java new file mode 100644 index 0000000..c005777 --- /dev/null +++ b/src/main/java/com/jmpeax/ssltoolbox/jks/actions/DeletetCert.java @@ -0,0 +1,47 @@ +package com.jmpeax.ssltoolbox.jks.actions; + + +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.actionSystem.PlatformDataKeys; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.ui.Messages; +import com.jmpeax.ssltoolbox.jks.JKSView; +import com.jmpeax.ssltoolbox.svc.CertificateHelper; +import org.jetbrains.annotations.NotNull; + +public class DeletetCert extends AnAction { + + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + + var certificateHelper = ApplicationManager.getApplication().getService(CertificateHelper.class); + var ksVirtualFile = e.getData(CommonDataKeys.VIRTUAL_FILE); + var view = getViewComponent(e); + if (view == null) { + Messages.showErrorDialog("No JKS view found", "No JKS View"); + return; + } + var selectedAlias = view.getSelectedCertificate(); + if (selectedAlias == null) { + Messages.showErrorDialog("No certificate selected", "No Certificate Selected"); + return; + } + var pwd = Messages.showPasswordDialog("Keystore password", "KeyStore Password"); + if (pwd != null && !pwd.isBlank()) { + certificateHelper.removeCertificate(ksVirtualFile, selectedAlias,pwd.toCharArray()); + view.removeCertificate(selectedAlias); + Messages.showInfoMessage("Certificate removed", "Certificate Removed"); + } + } + + + + private JKSView getViewComponent(@NotNull AnActionEvent e) { + // Retrieve your view component from context, e.g., using the data context from the event + return e.getData(PlatformDataKeys.CONTEXT_COMPONENT) instanceof JKSView + ? (JKSView) e.getData(PlatformDataKeys.CONTEXT_COMPONENT) + : null; + } +} diff --git a/src/main/java/com/jmpeax/ssltoolbox/jks/actions/ExportCert.java b/src/main/java/com/jmpeax/ssltoolbox/jks/actions/ExportCert.java index ffc95e6..8a4a4b6 100644 --- a/src/main/java/com/jmpeax/ssltoolbox/jks/actions/ExportCert.java +++ b/src/main/java/com/jmpeax/ssltoolbox/jks/actions/ExportCert.java @@ -6,19 +6,23 @@ import com.intellij.openapi.actionSystem.CommonDataKeys; import com.intellij.openapi.actionSystem.PlatformDataKeys; import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.fileChooser.FileChooser; -import com.intellij.openapi.fileChooser.FileChooserDescriptor; +import com.intellij.openapi.fileChooser.FileChooserFactory; +import com.intellij.openapi.fileChooser.FileSaverDescriptor; import com.intellij.openapi.ui.Messages; -import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.VirtualFileWrapper; import com.jmpeax.ssltoolbox.jks.JKSView; import com.jmpeax.ssltoolbox.svc.CertificateHelper; import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Objects; public class ExportCert extends AnAction { + private static final Logger LOGGER = LoggerFactory.getLogger(ExportCert.class); @Override public void actionPerformed(@NotNull AnActionEvent e) { @@ -28,24 +32,37 @@ public void actionPerformed(@NotNull AnActionEvent e) { var view = getViewComponent(e); if (view == null) { Messages.showErrorDialog("No JKS view found", "No JKS View"); + return; } var selectedAlias = view.getSelectedCertificate(); if (selectedAlias == null) { Messages.showErrorDialog("No certificate selected", "No Certificate Selected"); + return; } - var descriptor = new FileChooserDescriptor( - false, // Choose Files - true, - false, - false, - false, - false - ); + var descriptor = new FileSaverDescriptor("Export Certificate", "Export certificate", "cer"); + var pwd = Messages.showPasswordDialog("Keystore password", "KeyStore Password"); if (pwd != null && !pwd.isBlank()) { - certificateHelper.exportCertificate(ksVirtualFile, selectedAlias, pwd.toCharArray()); - Messages.showInfoMessage("Certificate exported", "Certificate Exported"); - view.removeCertificate(selectedAlias); + var f = FileChooserFactory.getInstance().createSaveFileDialog(descriptor, view).save(selectedAlias + ".cer"); + var cert = certificateHelper.exportCertificateToByte(ksVirtualFile, selectedAlias, pwd.toCharArray()); + if (f != null) { + ApplicationManager.getApplication().runWriteAction(() -> writeFile(f, cert)); + Messages.showInfoMessage("Certificate exported", "Certificate Exported"); + } else { + Messages.showErrorDialog("No file selected", "No File Selected"); + } + } + } + + private void writeFile(VirtualFileWrapper f, ByteArrayOutputStream cert) { + try { + var file = f.getVirtualFile(true); + if (file != null) { + file.setBinaryContent(cert.toByteArray()); + } + } catch (IOException e) { + LOGGER.error("Error writing file", e); + Messages.showErrorDialog("Error writing file", "Error Writing File"); } } diff --git a/src/main/java/com/jmpeax/ssltoolbox/pem/PemView.java b/src/main/java/com/jmpeax/ssltoolbox/pem/PemView.java index 749cb7f..78b704c 100644 --- a/src/main/java/com/jmpeax/ssltoolbox/pem/PemView.java +++ b/src/main/java/com/jmpeax/ssltoolbox/pem/PemView.java @@ -1,6 +1,7 @@ package com.jmpeax.ssltoolbox.pem; import com.intellij.ui.components.JBPanel; +import com.intellij.ui.components.JBTextField; import com.intellij.util.ui.JBUI; import com.jmpeax.ssltoolbox.utils.Messages; import org.jetbrains.annotations.NotNull; @@ -35,7 +36,7 @@ * This method is used to create the text fields displaying the certificate details in the panel. *

* The `PemView` class does not provide any public methods or properties other than the constructor.*/ -public class PemView extends JBPanel { +public class PemView extends JBPanel { public PemView(@NotNull X509Certificate certificate) { super(new GridBagLayout()); @@ -138,9 +139,10 @@ private String formatDate(Date date) { return isoFormatter.format(offsetDateTime); } - private JTextField buildText(String text){ - var textField = new JTextField(text,30); + private JBTextField buildText(String text){ + var textField = new JBTextField(text,30); textField.setEditable(false); + textField.setCaretPosition(0); return textField; } diff --git a/src/main/java/com/jmpeax/ssltoolbox/svc/CertificateHelper.java b/src/main/java/com/jmpeax/ssltoolbox/svc/CertificateHelper.java index b3c2a44..dbd9203 100644 --- a/src/main/java/com/jmpeax/ssltoolbox/svc/CertificateHelper.java +++ b/src/main/java/com/jmpeax/ssltoolbox/svc/CertificateHelper.java @@ -162,7 +162,7 @@ public void importCertificate(@NotNull VirtualFile keyStoreFile, @NotNull Virtua public @Nullable X509Certificate exportCertificate(VirtualFile ksVirtualFile, String selectedAlias, char[] password) { - try(var is = ksVirtualFile.getInputStream()) { + try (var is = ksVirtualFile.getInputStream()) { var keystore = openKeyStore(is, password); var cert = keystore.getCertificate(selectedAlias); return (X509Certificate) cert; @@ -171,4 +171,39 @@ public void importCertificate(@NotNull VirtualFile keyStoreFile, @NotNull Virtua return null; } } + + public @NotNull ByteArrayOutputStream exportCertificateToByte(VirtualFile ksVirtualFile, + String selectedAlias, + char[] password) { + var cert = exportCertificate(ksVirtualFile, selectedAlias, password); + if (cert == null) { + return new ByteArrayOutputStream(); + } + return exportCertificate(cert); + } + + public @NotNull ByteArrayOutputStream exportCertificate(@NotNull X509Certificate certificate) { + try { + var out = new ByteArrayOutputStream(); + out.write("-----BEGIN CERTIFICATE-----\n".getBytes()); + out.write(Base64.getEncoder().encode(certificate.getEncoded())); + out.write("\n-----END CERTIFICATE-----\n".getBytes()); + return out; + } catch (Exception e) { + LOGGER.error("Error exporting certificate", e); + return new ByteArrayOutputStream(); + } + } + + public void removeCertificate(VirtualFile ksVirtualFile, String certAlias, char[] password) { + try (var is = ksVirtualFile.getInputStream(); + var out = Files.newOutputStream(ksVirtualFile.toNioPath(), StandardOpenOption.WRITE)) { + var keystore = openKeyStore(is, password); + keystore.deleteEntry(certAlias); + keystore.store(out, password); + ksVirtualFile.refresh(false, false); + } catch (Exception e) { + LOGGER.error("Error removing certificate", e); + } + } } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index a699d1c..b21fe59 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -70,6 +70,10 @@ text="Export" description="Export certificate" icon="AllIcons.Ide.OutgoingChangesOn"> + +