aboutsummaryrefslogtreecommitdiff
path: root/src/main/ui
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/ui')
-rw-r--r--src/main/ui/IssueDialog.java110
-rw-r--r--src/main/ui/Main.java4
-rw-r--r--src/main/ui/MainUI.form111
-rw-r--r--src/main/ui/MainUI.java690
-rw-r--r--src/main/ui/RevokeDialog.java186
-rw-r--r--src/main/ui/TemplateEditDialog.java82
-rw-r--r--src/main/ui/widgets/CertEditDialog.java108
-rw-r--r--src/main/ui/widgets/CertTableModel.java129
-rw-r--r--src/main/ui/widgets/GCBuilder.java213
-rw-r--r--src/main/ui/widgets/LogTableModel.java79
-rw-r--r--src/main/ui/widgets/TemplateTableModel.java105
-rw-r--r--src/main/ui/widgets/UIUtils.java168
12 files changed, 1857 insertions, 128 deletions
diff --git a/src/main/ui/IssueDialog.java b/src/main/ui/IssueDialog.java
new file mode 100644
index 0000000..905b8df
--- /dev/null
+++ b/src/main/ui/IssueDialog.java
@@ -0,0 +1,110 @@
+package ui;
+
+import model.asn1.exceptions.ParseException;
+import model.ca.Template;
+import model.csr.CertificationRequest;
+import model.x501.Name;
+import ui.widgets.CertEditDialog;
+
+import javax.swing.*;
+import java.awt.event.ActionEvent;
+import java.util.List;
+
+import static ui.widgets.UIUtils.alert;
+
+/**
+ * Dialog that allows user to choose a template and edit the incoming CSR properties to get it signed.
+ * ┌───────────────────────────┐
+ * │ Issue new certificate X │
+ * │ │
+ * │Template: (Drop down)│
+ * │Subject: _________ │
+ * │Validity (Days): (Spinner)│
+ * │ │
+ * │ Issue Cancel│
+ * └───────────────────────────┘
+ */
+public class IssueDialog extends CertEditDialog<Template> {
+ /**
+ * The list of templates.
+ */
+ private final List<Template> templates;
+
+ /**
+ * The incoming CSR.
+ */
+ private final CertificationRequest csr;
+
+ /**
+ * Combo box to choose template.
+ */
+ private JComboBox<String> componentTemp;
+
+ /**
+ * The selected template, immutable.
+ */
+ private Template selectedTemplate;
+
+ /**
+ * EFFECTS: Init the dialog with CSR and templates.
+ * REQUIRES: csr must have a subject; templates must have at least one enabled.
+ */
+ public IssueDialog(CertificationRequest csr, List<Template> templates) {
+ super();
+ this.csr = csr;
+ this.templates = templates;
+
+ setTitle("Issue new certificate");
+ buttonOK.setText("Issue");
+ componentTemp.setModel(new DefaultComboBoxModel<>(templates.stream()
+ .filter(Template::isEnabled).map(Template::getName).toArray(String[]::new)));
+ componentTemp.addActionListener(this::onTemplateChange);
+
+ onTemplateChange(null);
+ pack();
+ }
+
+ /**
+ * EFFECTS: Create the templates combo box.
+ * MODIFIES: this
+ */
+ @Override
+ protected JComponent createTemplateComponent() {
+ return componentTemp = new JComboBox<>();
+ }
+
+ /**
+ * EFFECTS: Validate the form, compose a resulting template, close the dialog.
+ * MODIFIES: this
+ */
+ @Override
+ protected void onOK(ActionEvent ev) {
+ if (textFieldSubject.getText().isEmpty()) {
+ alert(this, getTitle(), "Subject must not be empty.");
+ return;
+ }
+ try {
+ res = new Template(selectedTemplate.getName(),
+ true,
+ Name.parseString(textFieldSubject.getText()),
+ ((SpinnerNumberModel) spinnerValidity.getModel()).getNumber().longValue());
+ dispose();
+ } catch (ParseException e) {
+ alert(this, getTitle(), e);
+ }
+ }
+
+ /**
+ * EFFECTS: Handle template change, rewrite the form with the new template.
+ * MODIFIES: this
+ */
+ private void onTemplateChange(ActionEvent ae) {
+ selectedTemplate = templates.stream()
+ .filter(temp -> temp.getName().equals(componentTemp.getSelectedItem()))
+ .findFirst()
+ .get();
+ textFieldSubject.setText(selectedTemplate.getSubject() != null ? selectedTemplate.getSubject().toString() :
+ csr.getCertificationRequestInfo().getSubject().toString());
+ spinnerValidity.setValue(selectedTemplate.getValidity());
+ }
+}
diff --git a/src/main/ui/Main.java b/src/main/ui/Main.java
index 89a0fe3..e3d8d7d 100644
--- a/src/main/ui/Main.java
+++ b/src/main/ui/Main.java
@@ -7,6 +7,8 @@ import javax.swing.*;
public class Main {
public static void main(String[] args) throws Throwable {
FlatIntelliJLaf.setup();
- new MainUI().createWindow();
+ final JFrame frame = new MainUI();
+ frame.pack();
+ frame.setVisible(true);
}
}
diff --git a/src/main/ui/MainUI.form b/src/main/ui/MainUI.form
deleted file mode 100644
index 9ab1492..0000000
--- a/src/main/ui/MainUI.form
+++ /dev/null
@@ -1,111 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="ui.MainUI">
- <grid id="27dc6" binding="rootPanel" layout-manager="GridLayoutManager" row-count="3" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
- <margin top="0" left="0" bottom="0" right="0"/>
- <constraints>
- <xy x="20" y="20" width="500" height="400"/>
- </constraints>
- <properties/>
- <border type="none"/>
- <children>
- <splitpane id="efbf3">
- <constraints>
- <grid row="1" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false">
- <preferred-size width="200" height="200"/>
- </grid>
- </constraints>
- <properties>
- <orientation value="0"/>
- </properties>
- <border type="none"/>
- <children>
- <tabbedpane id="4cbb0" binding="tabbedPane">
- <constraints>
- <splitpane position="left"/>
- </constraints>
- <properties/>
- <border type="none"/>
- <children>
- <grid id="e83bf" layout-manager="GridLayoutManager" row-count="1" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
- <margin top="0" left="0" bottom="0" right="0"/>
- <constraints>
- <tabbedpane title="CA"/>
- </constraints>
- <properties/>
- <border type="none"/>
- <children/>
- </grid>
- <grid id="7d4f3" layout-manager="GridLayoutManager" row-count="1" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
- <margin top="0" left="0" bottom="0" right="0"/>
- <constraints>
- <tabbedpane title="Certificates"/>
- </constraints>
- <properties/>
- <border type="none"/>
- <children>
- <component id="d436b" class="javax.swing.JList" binding="listCertificates">
- <constraints>
- <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="2" anchor="0" fill="3" indent="0" use-parent-layout="false">
- <preferred-size width="150" height="50"/>
- </grid>
- </constraints>
- <properties/>
- </component>
- </children>
- </grid>
- <grid id="5a23a" layout-manager="GridLayoutManager" row-count="1" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
- <margin top="0" left="0" bottom="0" right="0"/>
- <constraints>
- <tabbedpane title="Templates"/>
- </constraints>
- <properties/>
- <border type="none"/>
- <children>
- <component id="1f307" class="javax.swing.JList" binding="listTemplates">
- <constraints>
- <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="2" anchor="0" fill="3" indent="0" use-parent-layout="false">
- <preferred-size width="150" height="50"/>
- </grid>
- </constraints>
- <properties/>
- </component>
- </children>
- </grid>
- </children>
- </tabbedpane>
- <component id="e63cf" class="javax.swing.JList" binding="listLogs">
- <constraints>
- <splitpane position="right"/>
- </constraints>
- <properties/>
- </component>
- </children>
- </splitpane>
- <toolbar id="a2d1e">
- <constraints>
- <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false">
- <preferred-size width="-1" height="20"/>
- </grid>
- </constraints>
- <properties/>
- <border type="none"/>
- <children>
- <component id="5ae86" class="javax.swing.JButton" binding="buttonSave">
- <constraints/>
- <properties>
- <text value="Save"/>
- </properties>
- </component>
- </children>
- </toolbar>
- <component id="54c37" class="javax.swing.JLabel" binding="labelStatus">
- <constraints>
- <grid row="2" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
- </constraints>
- <properties>
- <text value="Ready"/>
- </properties>
- </component>
- </children>
- </grid>
-</form>
diff --git a/src/main/ui/MainUI.java b/src/main/ui/MainUI.java
index ee35a53..4735c55 100644
--- a/src/main/ui/MainUI.java
+++ b/src/main/ui/MainUI.java
@@ -1,22 +1,680 @@
package ui;
+import model.GroupObserver;
+import model.ObservedData;
+import model.Observer;
+import model.asn1.exceptions.InvalidCAException;
+import model.asn1.exceptions.InvalidDBException;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import model.ca.AuditLogEntry;
+import model.ca.CertificationAuthority;
+import model.ca.Template;
+import model.csr.CertificationRequest;
+import model.pki.cert.Certificate;
+import model.pki.crl.CertificateList;
+import model.pki.crl.RevokedCertificate;
+import persistence.Decoder;
+import persistence.FS;
+import ui.widgets.*;
+
import javax.swing.*;
+import javax.swing.event.ChangeEvent;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SignatureException;
+import java.security.interfaces.RSAPublicKey;
+
+import static java.awt.GridBagConstraints.*;
+import static ui.widgets.UIUtils.*;
+
+/**
+ * The main GUI.
+ * ┌──────────────────────────────┐┌──────────────────────────────┐┌──────────────────────────────┐
+ * │ JCA X ││ JCA X ││ JCA X │
+ * ├──────────────────────────────┤├──────────────────────────────┤├──────────────────────────────┤
+ * │Load Save CSR││Load Save Sign Revoke Export││Load Save New Ena Dis Del│
+ * ├────┬───────┬───────────┬─────┤├────┬───────┬──────────┬──────┤├────┬───────┬─────────────────┤
+ * │ CA │ Certs │ Templates │ ││ CA │ Certs │ Templates│ ││ CA │ Certs │ Templates │
+ * ├────┴───────┴───────────┴─────┤├─┬──┴─────┬─┴────┬─────┼──────┤├─┬──┴───┬───┴─────┬───────────┤
+ * │ Welcome to JCA ││ │ Serial │ Subj │ Bef │ To ││ │ Name │ Subj │ Validity │
+ * │ │├─┴────────┴──────┴─────┴──────┤├─┴──────┴─────────┴───────────┤
+ * │ CA certificate: Install CSR ││ (Issued Certs) ││ (All Templates) │
+ * ├──────────────────────────────┤├──────────────────────────────┤├──────────────────────────────┤
+ * ├─────┼──────────┼─────────────┤├─────┼──────────┼─────────────┤├─────┼──────────┼─────────────┤
+ * │Time │ Operator │ Action ││Time │ Operator │ Action ││Time │ Operator │ Action │
+ * ├─────┴──────────┴─────────────┤├─────┴──────────┴─────────────┤├─────┴──────────┴─────────────┤
+ * │ (Audit Logs) ││ (Audit Logs) ││ (Audit Logs) │
+ * ├──────────────────────────────┤├──────────────────────────────┤├──────────────────────────────┤
+ * │ Ready: (Last operation) ││ Unsaved: (Last operation) ││ Ready: (Last Operation) │
+ * └──────────────────────────────┘└──────────────────────────────┘└──────────────────────────────┘
+ */
+public class MainUI extends JFrame {
+ /**
+ * Default db file (./data/ca.json)
+ */
+ private static final Path PATH_DEFAULT = Path.of("data", "ca.json");
+
+ /**
+ * The root panel (Box layout).
+ */
+ private final JPanel rootPanel = new JPanel(new BoxLayout(rootPane, BoxLayout.PAGE_AXIS));
+
+ /**
+ * Common toolbar buttons
+ */
+ private final JButton buttonToolbarLoad = btn("Load", "open.png", this::onLoad);
+ private final JButton buttonToolbarSave = btn("Save", "saveall.png", this::onSave);
+
+ /**
+ * Toolbar that switches with the tab.
+ */
+ private final JPanel panelContextAwareToolbar = new JPanel(new CardLayout(0, 0));
+
+ /**
+ * Tab root.
+ */
+ private final JTabbedPane tabbedPane = new JTabbedPane();
+
+ /**
+ * CA tab
+ */
+ private final JLabel labelCACertificate = new JLabel();
+ private final JLabel labelPrivateKey = new JLabel();
+ private final JButton buttonGenPrivKey = btn("Generate", 'G', this::onGeneratePrivateKey);
+ private final JButton buttonInstallCA = btn("Install", 'I', this::onInstallCA);
+ private final JButton buttonGenCSR = btn("CSR", this::onSignCSR);
+ private final JToolBar toolbarCA = new JToolBar();
+ private final JButton buttonCAToolbarCRL = btn("CRL", "publisher.png", this::onCRL);
+
+ /**
+ * Certs tab
+ */
+ private JPanel panelCertsTab;
+ private final JTable tableCerts = new JTable();
+ private final CertTableModel modelCerts = new CertTableModel();
+ private final JToolBar toolbarCerts = new JToolBar();
+ private final JButton buttonCertsToolbarNew = btn("Sign", "new.png", this::onIssue);
+ private final JButton buttonCertsToolbarRevoke = btn("Revoke", "deletetest.png", this::onRevokeCert);
+ private final JButton buttonCertsToolbarExport = btn("Export", "export.png", this::onExportCert);
+
+ /**
+ * Templates tab
+ */
+ private JPanel panelTmpTab;
+ private final JTable tableTemplates = new JTable();
+ private final TemplateTableModel modelTemplates = new TemplateTableModel();
+ private final JToolBar toolbarTemplates = new JToolBar();
+ private final JButton buttonTemplatesToolbarNew = btn("New", "new.png", this::onNewTemplate);
+ private final JButton buttonTemplatesToolbarEnable = btn("Enable", "enable.png", this::onEnableTemplate);
+ private final JButton buttonTemplatesToolbarDisable = btn("Disable", "disable.png", this::onDisableTemplate);
+ private final JButton buttonTemplatesToolbarDelete = btn("Delete", "deletetest.png", this::onDeleteTemplate);
+
+ /**
+ * Logs region
+ */
+ private final JPanel panelLogs;
+ private final JTable tableAuditLogs = new JTable();
+ private final LogTableModel modelAuditLogs = new LogTableModel();
+
+ /**
+ * Status region
+ */
+ private final JLabel labelStatus = new JLabel();
+ private ObservedData<Boolean> unsaved = new ObservedData<>(false, this::acceptUnsaved);
+
+ /**
+ * CA and observers
+ */
+ private CertificationAuthority ca;
+
+ private final GroupObserver obs = new GroupObserver();
+
+ /**
+ * EFFECTS: Setup the CA and GUI.
+ */
+ public MainUI() {
+ setTitle("JCA");
+ setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+
+ setContentPane(rootPanel);
+ rootPanel.setLayout(new BoxLayout(rootPanel, BoxLayout.PAGE_AXIS));
+
+ rootPanel.add(setupToolbar());
+ final JSplitPane splitPane = new JSplitPane(0);
+ rootPanel.add(splitPane);
+ splitPane.setLeftComponent(setupTabs());
+ rootPanel.getRootPane().setDefaultButton(buttonGenPrivKey);
+
+ splitPane.setRightComponent(panelLogs = defView(scrTbl(tableAuditLogs), "No audit logs"));
+ panelLogs.setPreferredSize(new Dimension(panelLogs.getPreferredSize().width,
+ panelLogs.getPreferredSize().height / 2));
+ panelLogs.setBackground(new Color(-1));
+
+ rootPanel.add(labelStatus);
+ labelStatus.setBorder(BorderFactory.createEmptyBorder(4, 8, 8, 8));
+
+ ca = new CertificationAuthority();
+
+ tableAuditLogs.setModel(modelAuditLogs);
+ tableCerts.setModel(modelCerts);
+ tableTemplates.setModel(modelTemplates);
+
+ tabbedPane.addChangeListener(this::onChangeTab);
+ setupObservers();
+
+ setCA(ca);
+ }
+
+ // -----BEGIN HELPER METHODS-----
+
+ /**
+ * EFFECTS: Rewind the CA and refresh all pages.
+ * MODIFIES: ca (observer), this
+ */
+ private void setCA(CertificationAuthority ca) {
+ this.ca = ca;
+ ca.registerObserver(obs);
+
+ modelAuditLogs.setPtrData(ca.getLogs());
+ modelCerts.setPtrData(ca.getSigned());
+ modelCerts.setPtrRevokedData(ca.getRevoked());
+ modelTemplates.setPtrData(ca.getTemplates());
+
+ renderRefresh();
+ }
+
+ // -----END HELPER METHODS-----
+
+ // -----BEGIN DATA OBSERVERS-----
+
+ /**
+ * EFFECTS: Setup observers
+ * MODIFIES: this
+ */
+ private void setupObservers() {
+ obs.register(AuditLogEntry.class, this::acceptAuditLog);
+ obs.register(Certificate.class, this::acceptCertificate);
+ obs.register(RSAPublicKey.class, this::acceptPrivateKey);
+ obs.register(Template.class, this::acceptTemplate);
+ }
+
+ /**
+ * EFFECTS: Handle new audit log, hide default text, notify the model
+ * MODIFIES: this
+ * REQUIRES: direction == ADD, i >= 0
+ */
+ private void acceptAuditLog(AuditLogEntry auditLogEntry, int direction, int i) {
+ setContentVisible(panelLogs, true);
+ modelAuditLogs.fireTableRowsInserted(ca.getLogs().size() - 1, ca.getLogs().size() - 1);
+ acceptUnsaved(unsaved.get(), Observer.DIRECTION_CHANGE, Observer.INDEX_NOT_IN_LIST);
+ }
+
+ /**
+ * EFFECTS: Handle CA cert or new issued cert, notify CA page or model accordingly, hide certs default text
+ * MODIFIES: this
+ * REQUIRES: direction == CHANGE or ADD, i >= 0 or -1
+ */
+ private void acceptCertificate(Certificate cert, int direction, int i) {
+ if (i == Observer.INDEX_NOT_IN_LIST) {
+ renderCAPage();
+ } else {
+ setContentVisible(panelCertsTab, true);
+ modelCerts.fireTableRowsInserted(i, i);
+ }
+ }
+
+ /**
+ * EFFECTS: Handle added, changed, or deleted template; notify model, show / hide default text.
+ * MODIFIES: this
+ * REQUIRES: i >= 0.
+ */
+ private void acceptTemplate(Template template, int direction, int i) {
+ setContentVisible(panelTmpTab, !ca.getTemplates().isEmpty());
+ switch (direction) {
+ case Observer.DIRECTION_ADD:
+ modelTemplates.fireTableRowsInserted(i, i);
+ break;
+ case Observer.DIRECTION_CHANGE:
+ modelTemplates.fireTableRowsUpdated(i, i);
+ break;
+ case Observer.DIRECTION_REMOVE:
+ modelTemplates.fireTableRowsDeleted(i, i);
+ break;
+ }
+ }
+
+ /**
+ * EFFECTS: Handle added private key. Change buttons / labels accordingly.
+ * MODIFIES: this
+ */
+ private void acceptPrivateKey(RSAPublicKey pubKey, int direction, int i) {
+ renderCAPage();
+ }
+
+ /**
+ * EFFECTS: Handle status label change, set status to unsaved / saved + latest action.
+ * MODIFIES: this
+ */
+ private void acceptUnsaved(Boolean unsaved, int direction, int i) {
+ labelStatus.setText(unsaved ? "Unsaved" : "Ready");
+ if (!ca.getLogs().isEmpty()) {
+ labelStatus.setText(labelStatus.getText() + ": " + ca.getLogs().get(ca.getLogs().size() - 1).getAction());
+ }
+ }
+
+ // -----END DATA OBSERVERS----
+
+ // -----BEGIN RENDERERS-----
+
+ /**
+ * EFFECTS: Setup the toolbar.
+ * MODIFIES: this
+ */
+ private JToolBar setupToolbar() {
+ final JToolBar toolBar = new JToolBar();
+ toolBar.setAlignmentX(Component.LEFT_ALIGNMENT);
+ toolBar.setBorder(BorderFactory.createEmptyBorder(4, 8, 4, 8));
+
+ toolBar.add(buttonToolbarLoad);
+ toolBar.add(buttonToolbarSave);
+
+ toolBar.add(panelContextAwareToolbar);
+
+ panelContextAwareToolbar.add(toolbarTemplates, "CardToolbarTemplates");
+
+ toolbarTemplates.add(Box.createHorizontalGlue());
+ toolbarTemplates.add(buttonTemplatesToolbarNew);
+ toolbarTemplates.add(buttonTemplatesToolbarEnable);
+ toolbarTemplates.add(buttonTemplatesToolbarDisable);
+ toolbarTemplates.add(buttonTemplatesToolbarDelete);
+
+ panelContextAwareToolbar.add(toolbarCerts, "CardToolbarCerts");
+ toolbarCerts.add(Box.createHorizontalGlue());
+ toolbarCerts.add(buttonCertsToolbarNew);
+ toolbarCerts.add(buttonCertsToolbarRevoke);
+ toolbarCerts.add(buttonCertsToolbarExport);
+
+ panelContextAwareToolbar.add(toolbarCA, "CardToolbarCA");
+ toolbarCA.add(Box.createHorizontalGlue());
+ toolbarCA.add(buttonCAToolbarCRL);
+ return toolBar;
+ }
+
+ /**
+ * EFFECTS: Setup the tabs
+ * MODIFIES: this
+ */
+ private JTabbedPane setupTabs() {
+ tabbedPane.setAlignmentX(Component.LEFT_ALIGNMENT);
+ tabbedPane.setBorder(BorderFactory.createEmptyBorder(8, 8, 4, 8));
+
+ final JPanel panelTabCA = new JPanel();
+ panelTabCA.setLayout(new GridBagLayout());
+ tabbedPane.addTab("CA", panelTabCA);
+
+ panelTabCA.add(new JLabel("Welcome to JCA"), new GCBuilder().anchor(WEST).insectTop(8).build());
+ panelTabCA.add(labelPrivateKey, new GCBuilder().gridY(1).anchor(WEST).build());
+ panelTabCA.add(labelCACertificate, new GCBuilder().gridY(2).anchor(WEST).build());
+ panelTabCA.add(buttonGenPrivKey, new GCBuilder().gridXY(1, 1).fill(HORIZONTAL).build());
+ panelTabCA.add(buttonInstallCA, new GCBuilder().gridXY(1, 2).fill(HORIZONTAL).build());
+ panelTabCA.add(buttonGenCSR, new GCBuilder().gridXY(2, 2).fill(HORIZONTAL).build());
+ panelTabCA.add(new JPanel(), new GCBuilder().gridXY(3, 3).expandXY().fill(BOTH).build());
+ tabbedPane.addTab("Certs", panelCertsTab = defView(scrTbl(tableCerts), "No issued certs"));
+ tabbedPane.addTab("Templates", panelTmpTab = defView(scrTbl(tableTemplates), "No templates"));
+
+ return tabbedPane;
+ }
+
+ /**
+ * EFFECTS: Render public key and CA to the CA page and toolbar
+ * MODIFIES: this
+ */
+ private void renderCAPage() {
+ if (ca.getPublicKey() == null) {
+ labelPrivateKey.setText("Private key not installed");
+ } else {
+ labelPrivateKey.setText(String.format("%s key: %s ...",
+ ca.getPublicKey().getAlgorithm(),
+ ca.getPublicKey().getModulus().toString(16).substring(0, 16)));
+ }
+ if (ca.getCertificate() == null) {
+ labelCACertificate.setText("CA certificate not installed");
+ } else {
+ labelCACertificate.setText(String.format("<html>CA: %s<br>Issued by: %s</html>",
+ ca.getCertificate().getCertificate().getSubject().toString(),
+ ca.getCertificate().getCertificate().getIssuer().toString()));
+ }
+ buttonGenPrivKey.setEnabled(ca.getPublicKey() == null);
+ buttonInstallCA.setEnabled(ca.getPublicKey() != null && ca.getCertificate() == null);
+ buttonGenCSR.setEnabled(ca.getPublicKey() != null && ca.getCertificate() == null);
+ buttonCertsToolbarNew.setEnabled(ca.getPublicKey() != null && ca.getCertificate() != null);
+ buttonCAToolbarCRL.setEnabled(ca.getPublicKey() != null && ca.getCertificate() != null);
+ }
+
+ /**
+ * EFFECTS: Reset all GUI to initial state and render the CA again.
+ * MODIFIES: this
+ */
+ private void renderRefresh() {
+ acceptUnsaved(false, Observer.DIRECTION_CHANGE, Observer.INDEX_NOT_IN_LIST);
+ onChangeTab(null);
+ renderCAPage();
+
+ modelAuditLogs.fireTableDataChanged();
+ setContentVisible(panelLogs, !ca.getLogs().isEmpty());
+
+ modelCerts.fireTableDataChanged();
+ setContentVisible(panelCertsTab, !ca.getSigned().isEmpty());
+
+ modelTemplates.fireTableDataChanged();
+ setContentVisible(panelTmpTab, !ca.getTemplates().isEmpty());
+ }
+
+ // -----END RENDERERS-----
+
+ // -----BEGIN ACTION LISTENERS-----
+
+ /**
+ * EFFECTS: Switch toolbar according to tab change.
+ * MODIFIES: this
+ */
+ private void onChangeTab(ChangeEvent ev) {
+ final CardLayout toolbarCardLayout = (CardLayout) panelContextAwareToolbar.getLayout();
+ switch (tabbedPane.getSelectedIndex()) {
+ case 0: {
+ toolbarCerts.setEnabled(false);
+ toolbarTemplates.setEnabled(false);
+ toolbarCardLayout.show(panelContextAwareToolbar, "CardToolbarCA");
+ break;
+ }
+ case 1: {
+ toolbarCerts.setEnabled(true);
+ toolbarTemplates.setEnabled(false);
+ toolbarCardLayout.show(panelContextAwareToolbar, "CardToolbarCerts");
+ break;
+ }
+ case 2: {
+ toolbarCerts.setEnabled(false);
+ toolbarTemplates.setEnabled(true);
+ toolbarCardLayout.show(panelContextAwareToolbar, "CardToolbarTemplates");
+ break;
+ }
+ }
+ }
+
+ /**
+ * EFFECTS: Generate private key.
+ * MODIFIES: this
+ * REQUIRES: No private key / CA is installed.
+ */
+ private void onGeneratePrivateKey(ActionEvent ev) {
+ try {
+ ca.generateKey();
+ unsaved.set(true);
+ } catch (NoSuchAlgorithmException e) {
+ alert(rootPanel, "Generate private key", e);
+ } finally {
+ renderCAPage();
+ }
+ }
+
+ /**
+ * EFFECTS: Sign a CSR and save to disk (in binary form).
+ * MODIFIES: this
+ * REQUIRES: Proper private key installed, no CA.
+ */
+ private void onSignCSR(ActionEvent ev) {
+ final Path p = chooseFile(rootPanel, "DER binary (*.csr)", "csr");
+ if (p == null) {
+ return;
+ }
+
+ try {
+ final OutputStream fd = Files.newOutputStream(p,
+ StandardOpenOption.WRITE,
+ StandardOpenOption.TRUNCATE_EXISTING,
+ StandardOpenOption.CREATE);
+ final CertificationRequest csr = ca.signCSR();
+ fd.write(Utils.byteToByte(csr.encodeDER()));
+ fd.close();
+ unsaved.set(true);
+ } catch (IOException | ParseException | NoSuchAlgorithmException | SignatureException | InvalidKeyException e) {
+ alert(rootPanel, "Sign certification request", e);
+ }
+ }
+
+ /**
+ * EFFECTS: Pick a certificate and install.
+ * MODIFIES: this
+ * REQUIRES: Proper private key is installed, no CA.
+ */
+ private void onInstallCA(ActionEvent ev) {
+ final Path p = chooseFile(rootPanel, "DER X.509 certificate (*.crt, *.pem)", "crt", "pem");
+ if (p == null) {
+ return;
+ }
+
+ try {
+ final Certificate crt = new Certificate(new BytesReader(UIUtils.openDERorPEM(p, "CERTIFICATE")),
+ false);
+ ca.installCertificate(crt);
+ unsaved.set(true);
+ } catch (IOException | ParseException | InvalidCAException e) {
+ alert(rootPanel, "Install CA", e);
+ } finally {
+ renderCAPage();
+ }
+ }
+
+ /**
+ * EFFECTS: Load the database and refresh.
+ * MODIFIES: this
+ */
+ private void onLoad(ActionEvent ev) {
+ if (unsaved.get()) {
+ alert(rootPanel, "Load database from filesystem",
+ "Unable to load: current modifications are not saved.");
+ return;
+ }
+
+ try {
+ setCA(Decoder.decodeCA(FS.read(PATH_DEFAULT)));
+ } catch (NoSuchAlgorithmException | InvalidDBException e) {
+ alert(rootPanel, "Load database from filesystem", e);
+ }
+ }
+
+ /**
+ * EFFECTS: Save database.
+ * MODIFIES: this
+ */
+ private void onSave(ActionEvent ev) {
+ try {
+ FS.write(PATH_DEFAULT, Decoder.encodeCA(this.ca));
+ unsaved.set(false);
+ } catch (IOException e) {
+ alert(rootPanel, "Save database to filesystem", e);
+ }
+ }
+
+ /**
+ * EFFECTS: Enable a template.
+ * MODIFIES: this
+ */
+ private void onEnableTemplate(ActionEvent ev) {
+ if (tableTemplates.getSelectedRow() == -1) {
+ return;
+ }
+
+ final Template t = ca.getTemplates().get(tableTemplates.getSelectedRow());
+ if (t.isEnabled()) {
+ return;
+ }
+ ca.setTemplateEnable(t, true);
+ unsaved.set(true);
+ }
+
+ /**
+ * EFFECTS: Disable a template.
+ * MODIFIES: this
+ */
+ private void onDisableTemplate(ActionEvent ev) {
+ if (tableTemplates.getSelectedRow() == -1) {
+ return;
+ }
+
+ final Template t = ca.getTemplates().get(tableTemplates.getSelectedRow());
+ if (!t.isEnabled()) {
+ return;
+ }
+ ca.setTemplateEnable(t, false);
+ unsaved.set(true);
+ }
+
+ /**
+ * EFFECTS: Delete a template.
+ * MODIFIES: this
+ */
+ private void onDeleteTemplate(ActionEvent ev) {
+ if (tableTemplates.getSelectedRow() == -1) {
+ return;
+ }
+
+ final Template t = ca.getTemplates().get(tableTemplates.getSelectedRow());
+ ca.removeTemplate(t);
+ unsaved.set(true);
+ }
+
+ /**
+ * EFFECTS: Save the selected cert as a DER binary.
+ * MODIFIES: this
+ */
+ private void onExportCert(ActionEvent ev) {
+ if (tableCerts.getSelectedRow() == -1) {
+ return;
+ }
+
+ final Certificate c = ca.getSigned().get(tableCerts.getSelectedRow());
+ final Path p = chooseFile(rootPanel, "DER binary (*.crt)", "crt");
+ if (p == null) {
+ return;
+ }
+ try {
+ final OutputStream fd = Files.newOutputStream(p,
+ StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE);
+ fd.write(Utils.byteToByte(c.encodeDER()));
+ fd.close();
+ } catch (IOException e) {
+ alert(rootPanel, "Export certificate", e);
+ }
+ }
+
+ /**
+ * EFFECTS: Show revocation dialog for the selected cert. Save enforced.
+ * MODIFIES: this
+ */
+ private void onRevokeCert(ActionEvent ev) {
+ if (tableCerts.getSelectedRow() == -1) {
+ return;
+ }
+
+ final Certificate c = ca.getSigned().get(tableCerts.getSelectedRow());
+ if (ca.getRevoked().stream().anyMatch(r ->
+ r.getSerialNumber().getLong() == c.getCertificate().getSerialNumber().getLong())) {
+ return;
+ }
+ final RevokeDialog diag = new RevokeDialog(c);
+ diag.pack();
+ diag.setLocationRelativeTo(this);
+ diag.setVisible(true);
+
+ final RevokedCertificate res = diag.getRes();
+ if (res == null) {
+ return;
+ }
+
+ ca.revoke(res);
+ onSave(null);
+ }
+
+ /**
+ * EFFECTS: Sign a CRL and save to disk. Save enforced.
+ * MODIFIES: this
+ * REQUIRES: Proper private key and CA cert installed.
+ */
+ private void onCRL(ActionEvent ev) {
+ final Path p = chooseFile(rootPanel, "DER CRL (*.crl)", "crl");
+ if (p == null) {
+ return;
+ }
+ try {
+ final OutputStream fd = Files.newOutputStream(p,
+ StandardOpenOption.WRITE,
+ StandardOpenOption.TRUNCATE_EXISTING,
+ StandardOpenOption.CREATE);
+ final CertificateList crl = ca.signCRL();
+ fd.write(Utils.byteToByte(crl.encodeDER()));
+ fd.close();
+ onSave(null);
+ } catch (IOException | SignatureException | InvalidKeyException | NoSuchAlgorithmException e) {
+ alert(rootPanel, "Sign CRL", e);
+ }
+ }
+
+ /**
+ * EFFECTS: Pick a CSR, show issue dialog, sign cert. Save enforced.
+ * MODIFIES: this
+ * REQUIRES: Proper private key / CA is installed.
+ */
+ private void onIssue(ActionEvent ev) {
+ if (ca.getTemplates().stream().noneMatch(Template::isEnabled)) {
+ alert(rootPanel, "Issue new certificate", "No enabled templates.");
+ return;
+ }
+ final Path p = chooseFile(rootPanel, "DER CSR (*.csr, *.pem)", "csr", "pem");
+ if (p == null) {
+ return;
+ }
+ try {
+ CertificationRequest csr = new CertificationRequest(new BytesReader(UIUtils.openDERorPEM(p,
+ "CERTIFICATE REQUEST")), false);
+ final IssueDialog diag = new IssueDialog(csr, ca.getTemplates());
+ diag.setLocationRelativeTo(this);
+ diag.setVisible(true);
+ final Template res = diag.getRes();
+ if (res == null) {
+ return;
+ }
+ ca.signCert(csr.getCertificationRequestInfo(), res);
+ onSave(null);
+ } catch (IOException | ParseException | NoSuchAlgorithmException | SignatureException | InvalidKeyException e) {
+ alert(rootPanel, "Issue new certificate", e);
+ }
+ }
-public class MainUI {
- private JTabbedPane tabbedPane;
- private JPanel rootPanel;
- private JList listTemplates;
- private JList listCertificates;
- private JList listLogs;
- private JLabel labelStatus;
- private JButton buttonSave;
-
- public JFrame createWindow() {
- final JFrame frame = new JFrame("JCA");
- frame.setContentPane(this.rootPanel);
- frame.setVisible(true);
- frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
- frame.pack();
- return frame;
+ /**
+ * EFFECTS: Show the new template dialog and add a template.
+ * MODIFIES: this
+ */
+ private void onNewTemplate(ActionEvent ev) {
+ final TemplateEditDialog diag = new TemplateEditDialog(temp ->
+ ca.getTemplates().stream().anyMatch(t -> t.getName().equals(temp)));
+ diag.pack();
+ diag.setLocationRelativeTo(this);
+ diag.setVisible(true);
+ final Template res = diag.getRes();
+ if (res == null) {
+ return;
+ }
+ ca.addTemplate(res);
+ unsaved.set(true);
}
}
diff --git a/src/main/ui/RevokeDialog.java b/src/main/ui/RevokeDialog.java
new file mode 100644
index 0000000..49c13d1
--- /dev/null
+++ b/src/main/ui/RevokeDialog.java
@@ -0,0 +1,186 @@
+package ui;
+
+import model.asn1.ASN1Object;
+import model.asn1.UtcTime;
+import model.pki.cert.Certificate;
+import model.pki.crl.Reason;
+import model.pki.crl.RevokedCertificate;
+import ui.widgets.GCBuilder;
+import ui.widgets.UIUtils;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.Arrays;
+
+import static java.awt.GridBagConstraints.HORIZONTAL;
+import static java.awt.GridBagConstraints.WEST;
+import static ui.widgets.UIUtils.alert;
+
+/**
+ * A dialog that presents user with cert info, revocation reason, and revocation time.
+ * ┌────────────────────────────┐
+ * │ Revoke Certificate │
+ * │ │
+ * │Revoking: CN=xyz (Serial: 1)│
+ * │Reason: (Drop Down) │
+ * │Time: (ISO-8601 text) │
+ * │ │
+ * │ Revoke Cancel│
+ * └────────────────────────────┘
+ */
+public class RevokeDialog extends JDialog {
+ /**
+ * ISO8601
+ */
+ private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
+
+ /**
+ * The incoming certificate.
+ */
+ private final Certificate crt;
+
+ /**
+ * The result.
+ */
+ private RevokedCertificate res;
+
+
+ /**
+ * Root pane
+ */
+ private JPanel contentPane;
+
+ /**
+ * OK button
+ */
+ private JButton buttonOK;
+
+ /**
+ * Cancel button
+ */
+ private JButton buttonCancel;
+
+ /**
+ * Reason
+ */
+ private JComboBox<String> comboBoxReason;
+
+ /**
+ * Subject (not editable)
+ */
+ private JTextField textFieldSubject;
+
+ /**
+ * Time
+ */
+ private JFormattedTextField formattedTextFieldTime;
+
+ /**
+ * EFFECTS: Init GUI with the given cert.
+ */
+ public RevokeDialog(Certificate crt) {
+ this.crt = crt;
+
+ contentPane = new JPanel();
+ contentPane.setLayout(new BoxLayout(contentPane, BoxLayout.PAGE_AXIS));
+ contentPane.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
+ contentPane.add(renderForm());
+
+ contentPane.add(UIUtils.createActionsPane(buttonOK = new JButton("Revoke"),
+ buttonCancel = new JButton("Cancel")));
+ buttonOK.addActionListener(this::onOK);
+ buttonCancel.addActionListener(this::onCancel);
+
+ setContentPane(contentPane);
+ setModal(true);
+ getRootPane().setDefaultButton(buttonOK);
+ setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
+
+ setTitle("Revoke certificate");
+
+ addWindowListener(new WindowAdapter() {
+ public void windowClosing(WindowEvent e) {
+ onCancel(null);
+ }
+ });
+
+ contentPane.registerKeyboardAction(this::onCancel,
+ KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),
+ JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
+ }
+
+ /**
+ * EFFECTS: Init GUI.
+ * MODIFIES: this
+ */
+ private JPanel renderForm() {
+ final JPanel panelForm = new JPanel();
+ panelForm.setLayout(new GridBagLayout());
+
+ panelForm.add(new JLabel("Revoking: "), new GCBuilder().anchor(WEST).build());
+ panelForm.add(new JLabel("Reason: "), new GCBuilder().gridY(1).anchor(WEST).build());
+ panelForm.add(new JLabel("Time: "), new GCBuilder().gridY(2).anchor(WEST).build());
+
+ textFieldSubject = new JTextField(String.format("%s (Serial: %d)",
+ crt.getCertificate().getSubject().toString(), crt.getCertificate().getSerialNumber().getLong()));
+ textFieldSubject.setEditable(false);
+ panelForm.add(textFieldSubject, new GCBuilder().gridX(1).anchor(WEST).fill(HORIZONTAL).build());
+
+ comboBoxReason = new JComboBox<>(Arrays.stream(Reason.values()).map(Enum::toString).toArray(String[]::new));
+ panelForm.add(comboBoxReason, new GCBuilder().gridXY(1, 1).anchor(WEST).fill(HORIZONTAL).build());
+
+ formattedTextFieldTime = new JFormattedTextField(DATE_FORMAT.toFormat());
+ formattedTextFieldTime.setText(ZonedDateTime.now().format(DATE_FORMAT));
+ panelForm.add(formattedTextFieldTime, new GCBuilder().gridXY(1, 2).anchor(WEST).fill(HORIZONTAL).build());
+
+ panelForm.add(new JPanel(), new GCBuilder().gridXY(1, 3).expandXY().fill(HORIZONTAL).build());
+ return panelForm;
+ }
+
+ /**
+ * EFFECTS: Validate form, set result, and close dialog.
+ * Time must be a valid ISO-8601 string with offset, and it will be converted into UTC.
+ * Reason must be supported.
+ * MODIFIES: this
+ */
+ private void onOK(ActionEvent ev) {
+ try {
+ res = new RevokedCertificate(ASN1Object.TAG_SEQUENCE, null,
+ crt.getCertificate().getSerialNumber(),
+ new UtcTime(UtcTime.TAG, null,
+ ZonedDateTime.parse(formattedTextFieldTime.getText(), DATE_FORMAT)
+ .withZoneSameInstant(ZoneId.of("UTC"))),
+ Reason.valueOf(comboBoxReason.getSelectedItem().toString()));
+ dispose();
+ } catch (DateTimeParseException e) {
+ alert(rootPane, "Revoke certificate", "Invalid time: " + formattedTextFieldTime.getText()
+ + ". It must be a valid ISO8601 time with offset, like '1919-08-10T11:45:14+09:00'.");
+ } catch (IllegalArgumentException e) {
+ alert(rootPane, "Revoke certificate", "Invalid reason.");
+ }
+ }
+
+ /**
+ * EFFECTS: Clear the result and close dialog.
+ * MODIFIES: this
+ */
+ private void onCancel(ActionEvent ev) {
+ res = null;
+ dispose();
+ }
+
+ /**
+ * EFFECTS: Get result.
+ */
+ public RevokedCertificate getRes() {
+ return res;
+ }
+}
diff --git a/src/main/ui/TemplateEditDialog.java b/src/main/ui/TemplateEditDialog.java
new file mode 100644
index 0000000..7c33af6
--- /dev/null
+++ b/src/main/ui/TemplateEditDialog.java
@@ -0,0 +1,82 @@
+package ui;
+
+import model.asn1.exceptions.ParseException;
+import model.ca.Template;
+import model.x501.Name;
+import ui.widgets.CertEditDialog;
+
+import javax.swing.*;
+import java.awt.event.ActionEvent;
+import java.util.function.Function;
+
+import static ui.widgets.UIUtils.alert;
+
+/**
+ * A dialog that allows users to input template name, subject, and validity.
+ * ┌───────────────────────────┐
+ * │ New Template │
+ * │ │
+ * │Template: _________│
+ * │Subject: _________│
+ * │Validity (Days): (Spinner)│
+ * │ │
+ * │ Add Cancel│
+ * └───────────────────────────┘
+ */
+public class TemplateEditDialog extends CertEditDialog<Template> {
+ /**
+ * Callback function to check for name conflict.
+ */
+ private final Function<String, Boolean> dupDetector;
+
+ /**
+ * Text field for name.
+ */
+ private JTextField templateComponent;
+
+ /**
+ * EFFECTS: Init UI, title = New Template, OK button = Add, set dup detector.
+ */
+ public TemplateEditDialog(Function<String, Boolean> dupDetector) {
+ super();
+ this.dupDetector = dupDetector;
+ setTitle("New template");
+ buttonOK.setText("Add");
+ }
+
+ /**
+ * EFFECTS: Initialize the subject text field with JTextField.
+ * MODIFIES: this
+ */
+ @Override
+ protected JComponent createTemplateComponent() {
+ return templateComponent = new JTextField();
+ }
+
+ /**
+ * EFFECTS: Validate the form, set the result, and close the dialog.
+ * Name must not be null; Name must not conflict; Subject must be valid.
+ * MODIFIES: this
+ */
+ @Override
+ protected void onOK(ActionEvent ev) {
+ if (templateComponent.getText().isEmpty()) {
+ alert(rootPane, getTitle(), "The template name must not be empty.");
+ return;
+ }
+ if (dupDetector.apply(templateComponent.getText())) {
+ alert(rootPane, getTitle(), "The template exists.");
+ return;
+ }
+
+ try {
+ res = new Template(templateComponent.getText(),
+ false,
+ textFieldSubject.getText().isBlank() ? null : Name.parseString(textFieldSubject.getText()),
+ (Integer) spinnerValidity.getValue());
+ dispose();
+ } catch (ParseException e) {
+ alert(rootPane, getTitle(), e);
+ }
+ }
+}
diff --git a/src/main/ui/widgets/CertEditDialog.java b/src/main/ui/widgets/CertEditDialog.java
new file mode 100644
index 0000000..ea16b19
--- /dev/null
+++ b/src/main/ui/widgets/CertEditDialog.java
@@ -0,0 +1,108 @@
+package ui.widgets;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+
+import static java.awt.GridBagConstraints.HORIZONTAL;
+import static java.awt.GridBagConstraints.WEST;
+import static ui.widgets.UIUtils.btn;
+
+/**
+ * A common dialog for cert / template editing. It will close upon Esc or Cancel.
+ * ┌───────────────────────────┐
+ * │ Dialog X │
+ * │ │
+ * │Template: (TBD) │
+ * │Subject: _________ │
+ * │Validity (Days): (Spinner)│
+ * │ │
+ * │ Button Cancel│
+ * └───────────────────────────┘
+ */
+public abstract class CertEditDialog<T> extends JDialog {
+ /**
+ * The result.
+ */
+ protected T res;
+
+ /**
+ * Root pane.
+ */
+ protected JPanel contentPane = new JPanel();
+ protected JButton buttonOK = btn("", this::onOK);
+ protected JTextField textFieldSubject = new JTextField();
+ protected JSpinner spinnerValidity =
+ new JSpinner(new SpinnerNumberModel(60, 1, null, 0));
+
+ /**
+ * EFFECTS: Render the dialog, leaving title and OK button text blank.
+ */
+ public CertEditDialog() {
+ contentPane.setLayout(new BoxLayout(contentPane, BoxLayout.PAGE_AXIS));
+ contentPane.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
+ contentPane.add(renderForm());
+
+ contentPane.add(UIUtils.createActionsPane(buttonOK, btn("Cancel", this::onCancel)));
+
+ setContentPane(contentPane);
+ setModal(true);
+ getRootPane().setDefaultButton(buttonOK);
+ setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
+
+ addWindowListener(new WindowAdapter() {
+ public void windowClosing(WindowEvent e) {
+ onCancel(null);
+ }
+ });
+
+ contentPane.registerKeyboardAction(this::onCancel,
+ KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),
+ JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
+ }
+
+ /**
+ * EFFECTS: Render the form.
+ */
+ private JPanel renderForm() {
+ final JPanel panelForm = new JPanel(new GridBagLayout());
+ panelForm.add(new JLabel("Template: "), new GCBuilder().anchor(WEST).build());
+ panelForm.add(new JLabel("Subject: "), new GCBuilder().gridY(1).anchor(WEST).build());
+ panelForm.add(new JLabel("Validity (Days): "), new GCBuilder().gridY(2).anchor(WEST).build());
+ panelForm.add(createTemplateComponent(), new GCBuilder().gridXY(1, 0).anchor(WEST)
+ .fill(HORIZONTAL).build());
+ panelForm.add(textFieldSubject, new GCBuilder().gridXY(1, 1).anchor(WEST).fill(HORIZONTAL).build());
+ panelForm.add(spinnerValidity, new GCBuilder().gridXY(1, 2).anchor(WEST).fill(HORIZONTAL).build());
+ panelForm.add(new JPanel(), new GCBuilder().gridXY(1, 3).expandXY().fill(HORIZONTAL).build());
+ return panelForm;
+ }
+
+ /**
+ * EFFECTS: Create the component for subject.
+ */
+ protected abstract JComponent createTemplateComponent();
+
+ /**
+ * EFFECTS: Handle OK.
+ */
+ protected abstract void onOK(ActionEvent ev);
+
+ /**
+ * EFFECTS: Handle cancel: clear result and close dialog.
+ * MODIFIES: this
+ */
+ private void onCancel(ActionEvent ev) {
+ res = null;
+ dispose();
+ }
+
+ /**
+ * EFFECTS: Get the result.
+ */
+ public T getRes() {
+ return res;
+ }
+}
diff --git a/src/main/ui/widgets/CertTableModel.java b/src/main/ui/widgets/CertTableModel.java
new file mode 100644
index 0000000..0259e1b
--- /dev/null
+++ b/src/main/ui/widgets/CertTableModel.java
@@ -0,0 +1,129 @@
+package ui.widgets;
+
+import model.ca.CertificationAuthority;
+import model.pki.cert.Certificate;
+import model.pki.crl.RevokedCertificate;
+
+import javax.swing.*;
+import javax.swing.table.AbstractTableModel;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+
+/**
+ * Table model that displays issued certificates.
+ */
+public class CertTableModel extends AbstractTableModel {
+ /**
+ * Valid certificate icon.
+ */
+ private static final ImageIcon ICON_OK =
+ new ImageIcon(CertTableModel.class.getResource("/verified.png"));
+
+ /**
+ * Revoked certificate icon (same as toolbar revoke icon).
+ */
+ private static final ImageIcon ICON_REVOKED =
+ new ImageIcon(CertTableModel.class.getResource("/deletetest.png"));
+
+ /**
+ * Columns
+ */
+ private static final String[] COLS = new String[] {
+ "", // Icon
+ "Serial",
+ "Subject",
+ "Signed",
+ "Expires"
+ };
+
+ /**
+ * Pointer to {@link CertificationAuthority#getSigned()}
+ */
+ private List<Certificate> ptrData;
+
+ /**
+ * Pointer to {@link CertificationAuthority#getRevoked()}
+ */
+ private List<RevokedCertificate> ptrRevokedData;
+
+ /**
+ * EFFECTS: Set pointer to certs
+ * MODIFIES: this
+ */
+ public void setPtrData(List<Certificate> ptrData) {
+ this.ptrData = ptrData;
+ }
+
+ /**
+ * EFFECTS: Set pointer to revoked
+ * MODIFIES: this
+ */
+ public void setPtrRevokedData(List<RevokedCertificate> ptrRevokedData) {
+ this.ptrRevokedData = ptrRevokedData;
+ }
+
+ /**
+ * EFFECTS: Count rows.
+ */
+ @Override
+ public int getRowCount() {
+ return ptrData == null ? 0 : ptrData.size();
+ }
+
+ /**
+ * EFFECTS: Count columns.
+ */
+ @Override
+ public int getColumnCount() {
+ return COLS.length;
+ }
+
+ /**
+ * EFFECTS: Get column name.
+ * REQUIRES: column in [9, getColumnCount())
+ */
+ @Override
+ public String getColumnName(int column) {
+ return COLS[column];
+ }
+
+ /**
+ * EFFECTS: Return the value for a cell:
+ * ImageIcon (Valid / Revoked)
+ * String (Serial number)
+ * String (Subject)
+ * String (NotBefore)
+ * String (NotAfter)
+ * Throws {@link IllegalArgumentException} if columnIndex is not in 0 ~ 4
+ * REQUIRES: rowIndex must in range.
+ */
+ @Override
+ public Object getValueAt(int rowIndex, int columnIndex) {
+ final Certificate e = ptrData.get(rowIndex);
+ switch (columnIndex) {
+ case 0: return ptrRevokedData.stream().anyMatch(r ->
+ r.getSerialNumber().getLong() == e.getCertificate().getSerialNumber().getLong()
+ ) ? ICON_REVOKED : ICON_OK;
+ case 1: return e.getCertificate().getSerialNumber().getLong();
+ case 2: return e.getCertificate().getSubject().toString();
+ case 3:
+ return e.getCertificate().getValidity().getNotBefore().getTimestamp()
+ .format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
+ case 4:
+ return e.getCertificate().getValidity().getNotAfter().getTimestamp()
+ .format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
+ default: throw new IllegalArgumentException();
+ }
+ }
+
+ /**
+ * EFFECTS: Return ImageIcon for 0, String otherwise.
+ */
+ @Override
+ public Class<?> getColumnClass(int columnIndex) {
+ switch (columnIndex) {
+ case 0: return ImageIcon.class;
+ default: return String.class;
+ }
+ }
+}
diff --git a/src/main/ui/widgets/GCBuilder.java b/src/main/ui/widgets/GCBuilder.java
new file mode 100644
index 0000000..0590cc1
--- /dev/null
+++ b/src/main/ui/widgets/GCBuilder.java
@@ -0,0 +1,213 @@
+package ui.widgets;
+
+import java.awt.*;
+
+/**
+ * A builder for {@link GridBagConstraints}.
+ */
+public class GCBuilder {
+ /**
+ * Grix X and Y, defaults to {@link GridBagConstraints#RELATIVE}
+ */
+ private int gridX = GridBagConstraints.RELATIVE;
+
+ private int gridY = GridBagConstraints.RELATIVE;
+
+ /**
+ * Weight X and Y, [0.0, 1.0], defaults to 0.0
+ */
+ private double weightX = 0.0;
+
+ private double weightY = 0.0;
+
+ /**
+ * Anchor, defaults to {@link GridBagConstraints#CENTER}
+ */
+ private int anchor = GridBagConstraints.CENTER;
+
+ /**
+ * How to stretch the component, defaults to {@link GridBagConstraints#NONE}
+ */
+ private int fill = GridBagConstraints.NONE;
+
+ /**
+ * Insects in pixels.
+ */
+ private int insectTop = 0;
+
+ private int insectLeft = 0;
+
+ private int insectRight = 0;
+
+ private int insectBottom = 0;
+
+ /**
+ * EFFECTS: Build the {@link GridBagConstraints} based on parameters.
+ * Grid width, grid height, ipad X, ipad Y will be 1, 1, 0, 0.
+ */
+ public GridBagConstraints build() {
+ return new GridBagConstraints(gridX, gridY,
+ 1, 1,
+ weightX, weightY,
+ anchor, fill,
+ new Insets(insectTop, insectLeft, insectBottom, insectRight),
+ 0, 0);
+ }
+
+ /**
+ * EFFECTS: Set grid X and Y.
+ * REQUIRES: > 0
+ * MODIFIES: this
+ */
+ public GCBuilder gridXY(int gridX, int gridY) {
+ return this.gridX(gridX).gridY(gridY);
+ }
+
+ /**
+ * EFFECTS: Set grid X.
+ * REQUIRES: > 0
+ * MODIFIES: this
+ */
+ public GCBuilder gridX(int gridX) {
+ this.gridX = gridX;
+ return this;
+ }
+
+ /**
+ * EFFECTS: Set grid Y.
+ * REQUIRES: > 0
+ * MODIFIES: this
+ */
+ public GCBuilder gridY(int gridY) {
+ this.gridY = gridY;
+ return this;
+ }
+
+ /**
+ * EFFECTS: Set weight X and Y.
+ * REQUIRES: [0.0, 1.0]
+ * MODIFIES: this
+ */
+ public GCBuilder weightXY(double weightX, double weightY) {
+ return this.weightX(weightX).weightY(weightY);
+ }
+
+ /**
+ * EFFECTS: Set weight X and Y to 1.0
+ * MODIFIES: this
+ */
+ public GCBuilder expandXY() {
+ return this.expandX().expandY();
+ }
+
+ /**
+ * EFFECTS: Set weight X to 1.0
+ * MODIFIES: this
+ */
+ public GCBuilder expandX() {
+ return this.weightX(1.0);
+ }
+
+ /**
+ * EFFECTS: Set weight X to 1.0
+ * MODIFIES: this
+ */
+ public GCBuilder weightX(double weightX) {
+ this.weightX = weightX;
+ return this;
+ }
+
+ /**
+ * EFFECTS: Set weight Y to 1.0
+ * MODIFIES: this
+ */
+ public GCBuilder expandY() {
+ return this.weightY(1.0);
+ }
+
+ /**
+ * EFFECTS: Set weight X to 1.0
+ * MODIFIES: this
+ */
+ public GCBuilder weightY(double weightY) {
+ this.weightY = weightY;
+ return this;
+ }
+
+ /**
+ * EFFECTS: Set anchor.
+ * REQUIRES: anchor in {@link GridBagConstraints} constants.
+ * MODIFIES: this
+ */
+ public GCBuilder anchor(int anchor) {
+ this.anchor = anchor;
+ return this;
+ }
+
+ /**
+ * EFFECTS: Set fill.
+ * REQUIRES: fill in {@link GridBagConstraints} constants.
+ * MODIFIES: this
+ */
+ public GCBuilder fill(int fill) {
+ this.fill = fill;
+ return this;
+ }
+
+ /**
+ * EFFECTS: Set insects to be 8, 8, 8, 8
+ * MODIFIES: this
+ */
+ public GCBuilder marginInsects() {
+ return this.insects(8, 8, 8, 8);
+ }
+
+ /**
+ * EFFECTS: Set insects.
+ * REQUIRES: >= 0
+ * MODIFIES: this
+ */
+ public GCBuilder insects(int top, int left, int bottom, int right) {
+ return this.insectTop(top).insectLeft(left).insectBottom(bottom).insectRight(right);
+ }
+
+ /**
+ * EFFECTS: Set top insect
+ * REQUIRES: >= 0
+ * MODIFIES: this
+ */
+ public GCBuilder insectTop(int insectTop) {
+ this.insectTop = insectTop;
+ return this;
+ }
+
+ /**
+ * EFFECTS: Set left insect
+ * REQUIRES: >= 0
+ * MODIFIES: this
+ */
+ public GCBuilder insectLeft(int insectLeft) {
+ this.insectLeft = insectLeft;
+ return this;
+ }
+
+ /**
+ * EFFECTS: Set right insect
+ * REQUIRES: >= 0
+ * MODIFIES: this
+ */
+ public GCBuilder insectRight(int insectRight) {
+ this.insectRight = insectRight;
+ return this;
+ }
+
+ /**
+ * EFFECTS: Set bottom insect
+ * REQUIRES: >= 0
+ * MODIFIES: this
+ */
+ public GCBuilder insectBottom(int insectBottom) {
+ this.insectBottom = insectBottom;
+ return this;
+ }
+}
diff --git a/src/main/ui/widgets/LogTableModel.java b/src/main/ui/widgets/LogTableModel.java
new file mode 100644
index 0000000..dc7dcbd
--- /dev/null
+++ b/src/main/ui/widgets/LogTableModel.java
@@ -0,0 +1,79 @@
+package ui.widgets;
+
+import model.ca.AuditLogEntry;
+import model.ca.CertificationAuthority;
+
+import javax.swing.table.AbstractTableModel;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+
+/**
+ * Table model that displays audit logs.
+ */
+public class LogTableModel extends AbstractTableModel {
+ /**
+ * Columns
+ */
+ private static final String[] COLS = new String[] {
+ "Time",
+ "Operator",
+ "Action"
+ };
+
+ /**
+ * Pointer to the {@link CertificationAuthority#getLogs()}.
+ */
+ private List<AuditLogEntry> ptrData;
+
+ /**
+ * EFFECTS: Set the pointer to templates
+ * MODIFIES: this
+ */
+ public void setPtrData(List<AuditLogEntry> ptrData) {
+ this.ptrData = ptrData;
+ }
+
+ /**
+ * EFFECT: Return number of rows.
+ */
+ @Override
+ public int getRowCount() {
+ return ptrData == null ? 0 : ptrData.size();
+ }
+
+ /**
+ * EFFECT: Return number of columns.
+ */
+ @Override
+ public int getColumnCount() {
+ return COLS.length;
+ }
+
+ /**
+ * EFFECTS: Get column name.
+ * REQUIRES: column in [9, getColumnCount())
+ */
+ @Override
+ public String getColumnName(int column) {
+ return COLS[column];
+ }
+
+ /**
+ * EFFECTS: Return the value for a cell:
+ * String (Time)
+ * String (Operator)
+ * String (Action)
+ * Throws {@link IllegalArgumentException} if columnIndex is not in 0 ~ 2
+ * REQUIRES: rowIndex must in range.
+ */
+ @Override
+ public Object getValueAt(int rowIndex, int columnIndex) {
+ final AuditLogEntry e = ptrData.get(rowIndex);
+ switch (columnIndex) {
+ case 0: return e.getTime().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
+ case 1: return e.getUser();
+ case 2: return e.getAction();
+ default: throw new IllegalArgumentException();
+ }
+ }
+}
diff --git a/src/main/ui/widgets/TemplateTableModel.java b/src/main/ui/widgets/TemplateTableModel.java
new file mode 100644
index 0000000..da4557f
--- /dev/null
+++ b/src/main/ui/widgets/TemplateTableModel.java
@@ -0,0 +1,105 @@
+package ui.widgets;
+
+import model.ca.CertificationAuthority;
+import model.ca.Template;
+
+import javax.swing.*;
+import javax.swing.table.AbstractTableModel;
+import java.util.List;
+
+/**
+ * Table model that displays templates.
+ */
+public class TemplateTableModel extends AbstractTableModel {
+ /**
+ * Template enabled icon, same as toolbar enable icon.
+ */
+ private static final ImageIcon ICON_ENABLED =
+ new ImageIcon(TemplateTableModel.class.getResource("/enable.png"));
+
+ /**
+ * Template disbled icon, same as toolbar enable icon.
+ */
+ private static final ImageIcon ICON_DISABLED =
+ new ImageIcon(TemplateTableModel.class.getResource("/disable.png"));
+
+ /**
+ * Columns
+ */
+ private static final String[] COLS = new String[] {
+ "", // Icon
+ "Name",
+ "Subject",
+ "Validity"
+ };
+
+ /**
+ * Pointer to the {@link CertificationAuthority#getTemplates()}.
+ */
+ private List<Template> ptrData;
+
+ /**
+ * EFFECTS: Set the pointer to templates
+ * MODIFIES: this
+ */
+ public void setPtrData(List<Template> ptrData) {
+ this.ptrData = ptrData;
+ }
+
+ /**
+ * EFFECT: Return number of rows.
+ * REQUIRES: column in [9, getColumnCount())
+ */
+ @Override
+ public int getRowCount() {
+ return ptrData == null ? 0 : ptrData.size();
+ }
+
+ /**
+ * EFFECT: Return number of rows.
+ */
+ @Override
+ public int getColumnCount() {
+ return COLS.length;
+ }
+
+ /**
+ * EFFECTS: Get column name.
+ */
+ @Override
+ public String getColumnName(int column) {
+ return COLS[column];
+ }
+
+ /**
+ * EFFECTS: Return the value for a cell:
+ * ImageIcon (Enabled / Disabled)
+ * String (Name)
+ * String (Subject or Not Set)
+ * String (xx days)
+ * Throws {@link IllegalArgumentException} if columnIndex is not in 0 ~ 3
+ * REQUIRES: rowIndex must in range.
+ */
+ @Override
+ public Object getValueAt(int rowIndex, int columnIndex) {
+ final Template e = ptrData.get(rowIndex);
+ switch (columnIndex) {
+ case 0: return e.isEnabled() ? ICON_ENABLED : ICON_DISABLED;
+ case 1: return e.getName();
+ case 2: return e.getSubject() == null ? "<Not Set>" : e.getSubject().toString();
+ case 3: return String.format("%d days", e.getValidity());
+ default: throw new IllegalArgumentException();
+ }
+ }
+
+ /**
+ * EFFECTS: Return ImageIcon for 0, String otherwise.
+ */
+ @Override
+ public Class<?> getColumnClass(int columnIndex) {
+ switch (columnIndex) {
+ case 0: return ImageIcon.class;
+ default: return String.class;
+ }
+ }
+}
diff --git a/src/main/ui/widgets/UIUtils.java b/src/main/ui/widgets/UIUtils.java
new file mode 100644
index 0000000..4442be3
--- /dev/null
+++ b/src/main/ui/widgets/UIUtils.java
@@ -0,0 +1,168 @@
+package ui.widgets;
+
+import model.asn1.exceptions.ParseException;
+import ui.Utils;
+
+import javax.swing.*;
+import javax.swing.filechooser.FileNameExtensionFilter;
+import java.awt.*;
+import java.awt.event.ActionListener;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.util.stream.IntStream;
+
+import static java.awt.GridBagConstraints.BOTH;
+import static java.awt.GridBagConstraints.HORIZONTAL;
+import static javax.swing.JOptionPane.*;
+
+/**
+ * Useful utilities for building GUI.
+ */
+public class UIUtils {
+ /**
+ * EFFECTS: Create a horizontal actions pane:
+ * -----------------------------------------------------------
+ * | | Button1 | Button2 | Button3 | ButtonN |
+ * -----------------------------------------------------------
+ * REQUIRES: buttons != null
+ */
+ public static JPanel createActionsPane(JButton... buttons) {
+ final JPanel panelAct = new JPanel();
+ panelAct.setLayout(new GridBagLayout());
+ IntStream.range(0, buttons.length)
+ .forEach(i -> panelAct.add(buttons[i],
+ new GCBuilder().gridXY(i + 1, 1).fill(HORIZONTAL).build()));
+ panelAct.add(new JPanel(), new GCBuilder().expandXY().fill(BOTH).build());
+ return panelAct;
+ }
+
+ /**
+ * EFFECTS: Show / hide default text for a card layout container.
+ * MODIFIES: cardLayoutPanel
+ * REQUIRES: cardLayoutPanel must have a card layout; it must have CardContent and CardDefault cards.
+ */
+ public static void setContentVisible(Container cardLayoutPanel, boolean showContent) {
+ switchTo(cardLayoutPanel, showContent ? "CardContent" : "CardDefault");
+ }
+
+ /**
+ * EFFECTS: Switch to the card for a card layout panel.
+ * MODIFIES: cardLayoutPanel
+ * REQUIRES: cardLayoutPanel must have a card layout; it must have "card" card.
+ */
+ public static void switchTo(Container cardLayoutPanel, String card) {
+ ((CardLayout) cardLayoutPanel.getLayout()).show(cardLayoutPanel, card);
+ }
+
+ /**
+ * EFFECTS: Show an error message based on {@link Throwable#getMessage()}
+ * REQUIRES: component must have a frame.
+ */
+ public static void alert(Component component, String title, Throwable e) {
+ alert(component, title, e.getMessage());
+ }
+
+ /**
+ * EFFECTS: Show an error message.
+ * REQUIRES: component must have a frame.
+ */
+ public static void alert(Component component, String title, String message) {
+ showMessageDialog(getFrameForComponent(component),
+ message,
+ title,
+ ERROR_MESSAGE);
+ }
+
+ /**
+ * EFFECTS: Show a file chooser with the given filter title and list of extensions. Starting from cwd.
+ * Return null if cancelled.
+ */
+ public static Path chooseFile(Component component, String filterTitle, String... extensions) {
+ final JFileChooser fc = new JFileChooser();
+ fc.setFileFilter(new FileNameExtensionFilter(filterTitle, extensions));
+ fc.setCurrentDirectory(Paths.get("").toAbsolutePath().toFile());
+ if (fc.showOpenDialog(getFrameForComponent(component)) == JFileChooser.APPROVE_OPTION) {
+ return fc.getSelectedFile().toPath();
+ }
+ return null;
+ }
+
+ /**
+ * EFFECTS: Create a JPanel with CardLayout that has CardDefault set to component and CardContent set to a JLabel
+ * with the default text.
+ */
+ public static JPanel defView(Component component, String defaultText) {
+ final JPanel panel = new JPanel();
+ panel.setLayout(new CardLayout(0, 0));
+
+ JLabel labelDefault = new JLabel(defaultText);
+ labelDefault.setHorizontalAlignment(0);
+ panel.add(labelDefault, "CardDefault");
+ panel.add(component, "CardContent");
+
+ return panel;
+ }
+
+ /**
+ * EFFECTS: Create a JScrollPane-wrapped JTable.
+ * MODIFIES: table
+ */
+ public static JScrollPane scrTbl(JTable table) {
+ final JScrollPane scrollPane = new JScrollPane();
+ table.setFillsViewportHeight(true);
+ scrollPane.setViewportView(table);
+ return scrollPane;
+ }
+
+ /**
+ * EFFECTS: Parse the given path and automatically determine if it is a DER binary or a PEM. Automatically decode
+ * PEM.
+ * Throws {@link IOException} if it cannot be read.
+ * Throws {@link ParseException} if the PEM is invalid.
+ */
+ public static Byte[] openDERorPEM(Path path, String tag) throws IOException, ParseException {
+ final InputStream fd = Files.newInputStream(path, StandardOpenOption.READ);
+ Byte[] bs = Utils.byteToByte(fd.readAllBytes());
+ fd.close();
+ if (bs.length < 1) {
+ throw new ParseException("Invalid file: too short");
+ }
+ if (bs[0] == '-') {
+ bs = Utils.parsePEM(bs, tag);
+ }
+ return bs;
+ }
+
+ /**
+ * EFFECTS: Create a button with the given label and click listener.
+ */
+ public static JButton btn(String string, ActionListener onClick) {
+ final JButton btn = new JButton(string);
+ btn.addActionListener(onClick);
+ return btn;
+ }
+
+ /**
+ * EFFECTS: Create a button with the given label, mnemonic, and click listener.
+ */
+ public static JButton btn(String string, char m, ActionListener onClick) {
+ final JButton btn = new JButton(string);
+ btn.setMnemonic(m);
+ btn.setDisplayedMnemonicIndex(0);
+ btn.addActionListener(onClick);
+ return btn;
+ }
+
+ /**
+ * EFFECTS: Create a button with the given label, mnemonic, icon, and click listener.
+ */
+ public static JButton btn(String string, String icon, ActionListener onClick) {
+ final JButton btn = new JButton(string, new ImageIcon(UIUtils.class.getResource("/" + icon)));
+ btn.addActionListener(onClick);
+ return btn;
+ }
+}