diff options
author | Yuuta Liang <yuutaw@student.cs.ubc.ca> | 2023-11-28 18:19:39 -0800 |
---|---|---|
committer | Yuuta Liang <yuutaw@student.cs.ubc.ca> | 2023-11-28 18:19:39 -0800 |
commit | 1073af21305360bd33903c533cdac57e9f936294 (patch) | |
tree | 2c2d9c343ffe2577286fb53e016f06f6cdc53cbf /src/main/ui/gui | |
parent | e13adbb9a9146dd5ece890449e3cad958a502f86 (diff) | |
download | jca-1073af21305360bd33903c533cdac57e9f936294.tar jca-1073af21305360bd33903c533cdac57e9f936294.tar.gz jca-1073af21305360bd33903c533cdac57e9f936294.tar.bz2 jca-1073af21305360bd33903c533cdac57e9f936294.zip |
Move TUI and GUI into separate packages
Signed-off-by: Yuuta Liang <yuutaw@student.cs.ubc.ca>
Diffstat (limited to 'src/main/ui/gui')
-rw-r--r-- | src/main/ui/gui/IssueDialog.java | 101 | ||||
-rw-r--r-- | src/main/ui/gui/MainUI.java | 696 | ||||
-rw-r--r-- | src/main/ui/gui/RevokeDialog.java | 184 | ||||
-rw-r--r-- | src/main/ui/gui/TemplateEditDialog.java | 73 | ||||
-rw-r--r-- | src/main/ui/gui/widgets/CertEditDialog.java | 108 | ||||
-rw-r--r-- | src/main/ui/gui/widgets/CertTableModel.java | 129 | ||||
-rw-r--r-- | src/main/ui/gui/widgets/GCBuilder.java | 213 | ||||
-rw-r--r-- | src/main/ui/gui/widgets/LogTableModel.java | 79 | ||||
-rw-r--r-- | src/main/ui/gui/widgets/QRPanel.java | 77 | ||||
-rw-r--r-- | src/main/ui/gui/widgets/TemplateTableModel.java | 105 | ||||
-rw-r--r-- | src/main/ui/gui/widgets/UIUtils.java | 168 |
11 files changed, 1933 insertions, 0 deletions
diff --git a/src/main/ui/gui/IssueDialog.java b/src/main/ui/gui/IssueDialog.java new file mode 100644 index 0000000..8dcd6a8 --- /dev/null +++ b/src/main/ui/gui/IssueDialog.java @@ -0,0 +1,101 @@ +package ui.gui; + +import model.asn1.exceptions.ParseException; +import model.ca.Template; +import model.csr.CertificationRequest; +import model.x501.Name; +import ui.gui.widgets.CertEditDialog; + +import javax.swing.*; +import java.awt.event.ActionEvent; +import java.util.List; + +import static ui.gui.widgets.UIUtils.alert; + +/** + * Dialog that allows user to choose a template and edit the incoming CSR properties to get it signed. + */ +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/gui/MainUI.java b/src/main/ui/gui/MainUI.java new file mode 100644 index 0000000..9fa65ef --- /dev/null +++ b/src/main/ui/gui/MainUI.java @@ -0,0 +1,696 @@ +package ui.gui; + +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). + */ + 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 QRPanel panelQR = new QRPanel(256); + 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(); + } + + /** + * 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("<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); + } + } + + /** + * 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/gui/RevokeDialog.java b/src/main/ui/gui/RevokeDialog.java new file mode 100644 index 0000000..9cebc74 --- /dev/null +++ b/src/main/ui/gui/RevokeDialog.java @@ -0,0 +1,184 @@ +package ui.gui; + +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.gui.widgets.GCBuilder; +import ui.gui.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.gui.widgets.UIUtils.alert; + +/** + * A dialog that presents user with cert info, revocation reason, and revocation time. + * +----------------------------+ + * |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/gui/TemplateEditDialog.java b/src/main/ui/gui/TemplateEditDialog.java new file mode 100644 index 0000000..557d207 --- /dev/null +++ b/src/main/ui/gui/TemplateEditDialog.java @@ -0,0 +1,73 @@ +package ui.gui; + +import model.asn1.exceptions.ParseException; +import model.ca.Template; +import model.x501.Name; +import ui.gui.widgets.CertEditDialog; + +import javax.swing.*; +import java.awt.event.ActionEvent; +import java.util.function.Function; + +import static ui.gui.widgets.UIUtils.alert; + +/** + * A dialog that allows users to input template name, subject, and validity. + */ +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/gui/widgets/CertEditDialog.java b/src/main/ui/gui/widgets/CertEditDialog.java new file mode 100644 index 0000000..6c440a3 --- /dev/null +++ b/src/main/ui/gui/widgets/CertEditDialog.java @@ -0,0 +1,108 @@ +package ui.gui.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.gui.widgets.UIUtils.btn; + +/** + * A common dialog for cert / template editing. It will close upon Esc or Cancel. + * +---------------------------++---------------------------++---------------------------+ + * | Dialog X || New Template X || Issue new certificate X | + * | || || | + * |Template: (TBD) ||Template: _________||Template: (Drop down)| + * |Subject: _________||Subject: _________||Subject: _________ | + * |Validity (Days): (Spinner)||Validity (Days): (Spinner)||Validity (Days): (Spinner)| + * | || || | + * | Button Cancel|| Add Cancel|| Issue Cancel| + * +---------------------------++---------------------------++---------------------------+ + */ +public abstract class CertEditDialog<T> extends JDialog { + /** + * The result. + */ + protected T res; + + /** + * Root pane. + */ + protected JPanel contentPane = new JPanel(); + protected JButton buttonOK = UIUtils.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, UIUtils.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/gui/widgets/CertTableModel.java b/src/main/ui/gui/widgets/CertTableModel.java new file mode 100644 index 0000000..f10d621 --- /dev/null +++ b/src/main/ui/gui/widgets/CertTableModel.java @@ -0,0 +1,129 @@ +package ui.gui.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/gui/widgets/GCBuilder.java b/src/main/ui/gui/widgets/GCBuilder.java new file mode 100644 index 0000000..a571aab --- /dev/null +++ b/src/main/ui/gui/widgets/GCBuilder.java @@ -0,0 +1,213 @@ +package ui.gui.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/gui/widgets/LogTableModel.java b/src/main/ui/gui/widgets/LogTableModel.java new file mode 100644 index 0000000..a7b52ac --- /dev/null +++ b/src/main/ui/gui/widgets/LogTableModel.java @@ -0,0 +1,79 @@ +package ui.gui.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/gui/widgets/QRPanel.java b/src/main/ui/gui/widgets/QRPanel.java new file mode 100644 index 0000000..ebd70af --- /dev/null +++ b/src/main/ui/gui/widgets/QRPanel.java @@ -0,0 +1,77 @@ +package ui.gui.widgets; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.WriterException; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.qrcode.QRCodeWriter; + +import javax.swing.*; +import java.awt.*; +import java.util.Collections; + +import static com.google.zxing.EncodeHintType.ERROR_CORRECTION; +import static com.google.zxing.qrcode.decoder.ErrorCorrectionLevel.L; + +/** + * A fixed-size panel displaying a QR code. + */ +public class QRPanel extends JPanel { + private final int size; + + private BitMatrix data; + + /** + * EFFECTS: Init with the specific rectangular size. Min size = max size = preferred size. + */ + public QRPanel(int size) { + super(); + setSize(new Dimension(size, size)); + setMaximumSize(new Dimension(size, size)); + setMinimumSize(new Dimension(size, size)); + setPreferredSize(new Dimension(size, size)); + this.size = size; + } + + /** + * EFFECTS: Draw the QR code. Leave white if no data is set. + * MODIFIES: this + */ + @Override + protected void paintComponent(Graphics graphics) { + super.paintComponent(graphics); + graphics.setColor(Color.WHITE); + graphics.fillRect(0, 0, size, size); + + if (data == null) { + return; + } + + graphics.setColor(Color.BLACK); + for (int i = 0; i < size; i++) { + for (int j = 0; j < size; j++) { + if (data.get(i, j)) { + graphics.fillRect(i, j, 1, 1); + } + } + } + } + + /** + * EFFECTS: Set data and repaint. Nullable. + * MODIFIES: this + */ + public void setData(String data) { + if (data == null) { + this.data = null; + invalidate(); + return; + } + try { + this.data = new QRCodeWriter().encode(data, BarcodeFormat.QR_CODE, size, size, + Collections.singletonMap(ERROR_CORRECTION, L)); + } catch (WriterException e) { + e.printStackTrace(); + } + invalidate(); + } +} diff --git a/src/main/ui/gui/widgets/TemplateTableModel.java b/src/main/ui/gui/widgets/TemplateTableModel.java new file mode 100644 index 0000000..de23b54 --- /dev/null +++ b/src/main/ui/gui/widgets/TemplateTableModel.java @@ -0,0 +1,105 @@ +package ui.gui.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/gui/widgets/UIUtils.java b/src/main/ui/gui/widgets/UIUtils.java new file mode 100644 index 0000000..bb09a72 --- /dev/null +++ b/src/main/ui/gui/widgets/UIUtils.java @@ -0,0 +1,168 @@ +package ui.gui.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; + } +} |