package ui.gui; import annotations.Assoc; 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.Utils; import ui.gui.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.gui.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 | * | |+-+--------+------+-----+------++-+------+---------+-----------+ * | Private key: Generate || (Issued Certs) || (All Templates) | * | CA certificate: Install CSR || || | * +------------------------------++------------------------------++------------------------------+ * +-----+----------+-------------++-----+----------+-------------++-----+----------+-------------+ * |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). */ @Assoc(partOf = true) private final JPanel rootPanel = new JPanel(new BoxLayout(rootPane, BoxLayout.PAGE_AXIS)); /** * Common toolbar buttons */ @Assoc(partOf = true) private final JButton buttonToolbarLoad = btn("Load", "open.png", this::onLoad); @Assoc(partOf = true) private final JButton buttonToolbarSave = btn("Save", "saveall.png", this::onSave); /** * Toolbar that switches with the tab. */ @Assoc(partOf = true) private final JPanel panelContextAwareToolbar = new JPanel(new CardLayout(0, 0)); /** * Tab root. */ @Assoc(partOf = true) private final JTabbedPane tabbedPane = new JTabbedPane(); /** * CA tab */ @Assoc(partOf = true) private final JLabel labelCACertificate = new JLabel(); @Assoc(partOf = true) private final JLabel labelPrivateKey = new JLabel(); @Assoc(partOf = true) private final JButton buttonGenPrivKey = btn("Generate", 'G', this::onGeneratePrivateKey); @Assoc(partOf = true) private final JButton buttonInstallCA = btn("Install", 'I', this::onInstallCA); @Assoc(partOf = true) private final JButton buttonGenCSR = btn("CSR", this::onSignCSR); @Assoc(partOf = true) private final QRPanel panelQR = new QRPanel(256); @Assoc(partOf = true) private final JToolBar toolbarCA = new JToolBar(); @Assoc(partOf = true) private final JButton buttonCAToolbarCRL = btn("CRL", "publisher.png", this::onCRL); /** * Certs tab */ @Assoc(partOf = true) private JPanel panelCertsTab; @Assoc(partOf = true) private final JTable tableCerts = new JTable(); @Assoc(partOf = true) private final CertTableModel modelCerts = new CertTableModel(); @Assoc(partOf = true) private final JToolBar toolbarCerts = new JToolBar(); @Assoc(partOf = true) private final JButton buttonCertsToolbarNew = btn("Sign", "new.png", this::onIssue); @Assoc(partOf = true) private final JButton buttonCertsToolbarRevoke = btn("Revoke", "deletetest.png", this::onRevokeCert); @Assoc(partOf = true) private final JButton buttonCertsToolbarExport = btn("Export", "export.png", this::onExportCert); /** * Templates tab */ @Assoc(partOf = true) private JPanel panelTmpTab; @Assoc(partOf = true) private final JTable tableTemplates = new JTable(); @Assoc(partOf = true) private final TemplateTableModel modelTemplates = new TemplateTableModel(); @Assoc(partOf = true) private final JToolBar toolbarTemplates = new JToolBar(); @Assoc(partOf = true) private final JButton buttonTemplatesToolbarNew = btn("New", "new.png", this::onNewTemplate); @Assoc(partOf = true) private final JButton buttonTemplatesToolbarEnable = btn("Enable", "enable.png", this::onEnableTemplate); @Assoc(partOf = true) private final JButton buttonTemplatesToolbarDisable = btn("Disable", "disable.png", this::onDisableTemplate); @Assoc(partOf = true) private final JButton buttonTemplatesToolbarDelete = btn("Delete", "deletetest.png", this::onDeleteTemplate); /** * Logs region */ @Assoc(partOf = true) private final JPanel panelLogs; @Assoc(partOf = true) private final JTable tableAuditLogs = new JTable(); @Assoc(partOf = true) private final LogTableModel modelAuditLogs = new LogTableModel(); /** * Status region */ @Assoc(partOf = true) private final JLabel labelStatus = new JLabel(); private ObservedData unsaved = new ObservedData<>(false, this::acceptUnsaved); /** * CA and observers */ @Assoc(partOf = true) 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(); } /** * EFFECTS: Set the QR code data to either null (no public key) or PEM-encoded PKCS#1 public key. */ private void renderQRPublicKey() { if (ca.getPublicKey() == null) { panelQR.setData(null); return; } panelQR.setData(Utils.toPEM(Utils.byteToByte(ca.getPublicKey().getEncoded()), "PUBLIC KEY")); } // -----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(panelQR, new GCBuilder().gridY(3).fill(HORIZONTAL).build()); panelTabCA.add(new JPanel(), new GCBuilder().gridXY(4, 4).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() { renderQRPublicKey(); 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("CA: %s
Issued by: %s", 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); } } /** * 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); } }