aboutsummaryrefslogtreecommitdiff
path: root/src/main/ui/tui
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/ui/tui')
-rw-r--r--src/main/ui/tui/IssueScreen.java168
-rw-r--r--src/main/ui/tui/JCA.java220
-rw-r--r--src/main/ui/tui/MainScreen.java234
-rw-r--r--src/main/ui/tui/MgmtScreen.java153
-rw-r--r--src/main/ui/tui/Screen.java31
-rw-r--r--src/main/ui/tui/TemplateSetScreen.java157
-rw-r--r--src/main/ui/tui/TemplatesScreen.java138
-rw-r--r--src/main/ui/tui/UIHandler.java45
8 files changed, 1146 insertions, 0 deletions
diff --git a/src/main/ui/tui/IssueScreen.java b/src/main/ui/tui/IssueScreen.java
new file mode 100644
index 0000000..26ceb02
--- /dev/null
+++ b/src/main/ui/tui/IssueScreen.java
@@ -0,0 +1,168 @@
+package ui.tui;
+
+import model.asn1.exceptions.ParseException;
+import model.ca.Template;
+import model.csr.CertificationRequest;
+import model.pki.cert.Certificate;
+import model.x501.Name;
+import ui.Utils;
+
+/**
+ * The screen that accepts a CSR and template and allows user to change its properties and issue.
+ */
+public class IssueScreen implements UIHandler {
+ private final JCA session;
+
+ private Template template;
+ private CertificationRequest incomingCSR;
+
+ /**
+ * EFFECTS: Init with the session.
+ */
+ public IssueScreen(JCA session) {
+ this.session = session;
+ }
+
+ /**
+ * EFFECTS: Set current template and CSR in use by args.
+ * REQUIRES: args.length = 2, args[0] instanceof CertificateRequest, args[1] instanceof Template
+ * MODIFIES: args[1]
+ */
+ @Override
+ public void enter(Object... args) {
+ this.incomingCSR = (CertificationRequest) args[0];
+ this.template = (Template) args[1];
+ }
+
+ /**
+ * EFFECTS: Print help.
+ */
+ @Override
+ public void help() {
+ System.out.print("show\tView the current certificate\n"
+ + "set\tSet properties or template\n"
+ + "commit\tIssue the certificate\n"
+ + "exit\tDiscard and go to main menu\n"
+ + "help\tPrint this message\n");
+ }
+
+ /**
+ * EFFECTS: Print pending cert info.
+ */
+ @Override
+ public void show() {
+ System.out.println("Requested Subject:\t" + incomingCSR.getCertificationRequestInfo().getSubject());
+ System.out.println("Subject:\t" + (template.getSubject() == null
+ ? incomingCSR.getCertificationRequestInfo().getSubject()
+ : template.getSubject()));
+ System.out.println("Template:\t" + template.getName());
+ System.out.println("Validity:\t" + template.getValidity() + " days");
+ }
+
+ /**
+ * EFFECTS: Issue the cert and log it.
+ * MODIFIES: session
+ */
+ @Override
+ public void commit() {
+ try {
+ Certificate certificate = session.getCa().signCert(incomingCSR.getCertificationRequestInfo(), template);
+ session.save();
+ System.out.println(Utils.toPEM(certificate.encodeDER(), "CERTIFICATE"));
+ session.setScreen(Screen.MAIN);
+ } catch (Throwable e) {
+ System.out.println(e.getMessage());
+ }
+ }
+
+ /**
+ * EFFECTS: Set or unset the subject.
+ * MODIFIES: this
+ */
+ private void handleIssueSetSubject(String val) {
+ try {
+ if (val == null) {
+ template = new Template(template.getName(), template.isEnabled(), (Name) null, template.getValidity());
+ } else {
+ template = new Template(template.getName(), template.isEnabled(), val, template.getValidity());
+ }
+ } catch (ParseException e) {
+ System.out.println(e.getMessage());
+ }
+ }
+
+ /**
+ * EFFECTS: Set or unset the validity.
+ * MODIFIES: this
+ */
+ private void handleIssueSetValidity(String val) {
+ if (val == null) {
+ System.out.println("Cannot unset validity");
+ return;
+ }
+ try {
+ long i = Long.parseLong(val);
+ if (i <= 0) {
+ System.out.println("Invalid validity days");
+ return;
+ }
+ template = new Template(template.getName(), template.isEnabled(), template.getSubject(), i);
+ } catch (NumberFormatException ignored) {
+ System.out.println("Invalid validity days");
+ }
+ }
+
+ /**
+ * EFFECTS: Handle the set command.
+ * MODIFIES: this
+ */
+ private void handleIssueSet(String... args) {
+ if (args.length != 2 && args.length != 3) {
+ System.out.println("Usage: set <key> <value>");
+ System.out.println("Supported keys: subject validity");
+ return;
+ }
+ String val = args.length == 3 ? args[2] : null;
+ switch (args[1]) {
+ case "subject":
+ handleIssueSetSubject(val);
+ break;
+ case "validity":
+ handleIssueSetValidity(val);
+ break;
+ default:
+ System.out.println("Unknown key");
+ break;
+ }
+ }
+
+ @Override
+ public void command(String... args) {
+ if (args[0].equals("set")) {
+ handleIssueSet(args);
+ } else {
+ help();
+ }
+ }
+
+ /**
+ * EFFECTS: Clear the certificates and return main.
+ * MODIFIES: this
+ */
+ @Override
+ public Screen exit() {
+ incomingCSR = null;
+ template = null;
+ return Screen.MAIN;
+ }
+
+ /**
+ * EFFECTS: Return "/subj/ %"
+ */
+ @Override
+ public String getPS1() {
+ return String.format("/%s/ %%", template.getSubject() == null
+ ? incomingCSR.getCertificationRequestInfo().getSubject()
+ : template.getSubject());
+ }
+}
diff --git a/src/main/ui/tui/JCA.java b/src/main/ui/tui/JCA.java
new file mode 100644
index 0000000..c23d610
--- /dev/null
+++ b/src/main/ui/tui/JCA.java
@@ -0,0 +1,220 @@
+package ui.tui;
+
+import model.asn1.exceptions.InvalidDBException;
+import model.asn1.exceptions.ParseException;
+import model.ca.CertificationAuthority;
+import persistence.Decoder;
+import persistence.FS;
+import ui.Utils;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.InvalidKeySpecException;
+import java.util.Scanner;
+
+/**
+ * Main program
+ */
+public class JCA {
+ /**
+ * Default db file (./data/ca.json)
+ */
+ private static final Path PATH_DEFAULT = Path.of("data", "ca.json");
+
+ /**
+ * Instances of the five screens;
+ */
+ private final UIHandler mainScreen;
+ private final UIHandler mgmtScreen;
+ private final UIHandler issueScreen;
+ private final UIHandler templatesScreen;
+ private final UIHandler templateSetScreen;
+ /**
+ * The CA
+ */
+ private CertificationAuthority ca;
+ /**
+ * The current screen.
+ */
+ private UIHandler screen;
+
+ /**
+ * There are unsaved changes.
+ */
+ private boolean unsaved = false;
+
+ /**
+ * EFFECTS: Init with main screen and empty CA. No private key and no CA cert.
+ * Throws {@link NoSuchAlgorithmException} when crypto issue happens.
+ */
+ public JCA() throws NoSuchAlgorithmException, InvalidKeySpecException {
+ this.mainScreen = new MainScreen(this);
+ this.mgmtScreen = new MgmtScreen(this);
+ this.issueScreen = new IssueScreen(this);
+ this.templatesScreen = new TemplatesScreen(this);
+ this.templateSetScreen = new TemplateSetScreen(this);
+
+ setScreen(Screen.MAIN);
+
+ this.ca = new CertificationAuthority();
+ }
+
+ /**
+ * EFFECT: Checks if the CA is installed or not (according to the desired state) and print if not matching. Returns
+ * true if matching.
+ */
+ public boolean checkCA(boolean requireInstalled) {
+ if (requireInstalled && ca.getCertificate() == null) {
+ System.out.println("No CA installed");
+ return false;
+ } else if (!requireInstalled && ca.getCertificate() != null) {
+ System.out.println("CA already installed");
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * EFFECTS: Read PEM from stdin, matched the given tag.
+ * Throws {@link ParseException} if the input is incorrect.
+ */
+ public Byte[] handleInputPEM(String desiredTag) throws ParseException {
+ final Scanner scanner = new Scanner(System.in);
+ StringBuilder in = new StringBuilder();
+ while (true) {
+ final String line = scanner.nextLine();
+ in.append(line);
+ in.append("\n");
+ if (line.matches("-----END .*-----")) {
+ break;
+ }
+ }
+ return Utils.parsePEM(Utils.byteToByte(in.toString().getBytes(StandardCharsets.UTF_8)), desiredTag);
+ }
+
+ /**
+ * EFFECT: Set the current screen with optional args. Exit the program when mode is null.
+ * MODIFIES: this
+ */
+ public void setScreen(Screen mode, Object... args) {
+ if (mode == null) {
+ System.exit(0);
+ }
+ switch (mode) {
+ case MAIN:
+ this.screen = mainScreen;
+ break;
+ case MGMT:
+ this.screen = mgmtScreen;
+ break;
+ case ISSUE:
+ this.screen = issueScreen;
+ break;
+ case TEMPLATES:
+ this.screen = templatesScreen;
+ break;
+ case TEMPLATE_SET:
+ this.screen = templateSetScreen;
+ break;
+ }
+ screen.enter(args);
+ }
+
+ /**
+ * EFFECTS: Read the database file and replace all local states.
+ * MODIFIES: this
+ */
+ private void load() {
+ if (unsaved) {
+ System.out.println("Current database is not saved yet.");
+ return;
+ }
+ try {
+ this.ca = Decoder.decodeCA(FS.read(PATH_DEFAULT));
+ } catch (InvalidDBException | NoSuchAlgorithmException e) {
+ System.out.println(e.getMessage());
+ if (e.getCause() != null) {
+ System.out.println(e.getCause().getMessage());
+ e.getCause().printStackTrace();
+ }
+ }
+ }
+
+ /**
+ * EFFECTS: Handle input line
+ * MODIFIES: this
+ */
+ private void handleLine(String... args) {
+ if (args[0].equals("log")) {
+ ca.getLogs().forEach(System.out::println);
+ return;
+ }
+ if ("help".equals(args[0])) {
+ screen.help();
+ System.out.println("log\tView audit logs");
+ System.out.println("load\tLoad database");
+ System.out.println("save\tSave database");
+ } else if ("commit".equals(args[0])) {
+ screen.commit();
+ } else if ("show".equals(args[0])) {
+ screen.show();
+ } else if ("exit".equals(args[0])) {
+ setScreen(screen.exit());
+ } else if ("save".equals(args[0])) {
+ save();
+ } else if ("load".equals(args[0])) {
+ load();
+ } else {
+ screen.command(args);
+ }
+ printPS1();
+ }
+
+ /**
+ * EFFECTS: Print the '*user@JCA PS1' line
+ */
+ private void printPS1() {
+ System.out.printf("%s%s@JCA %s ",
+ unsaved ? "*" : "",
+ ca.getUser(),
+ screen.getPS1());
+ }
+
+ /**
+ * EFFECTS: Save the DB
+ */
+ public void save() {
+ try {
+ FS.write(PATH_DEFAULT, Decoder.encodeCA(ca));
+ unsaved = false;
+ } catch (IOException e) {
+ System.out.println(e.getMessage());
+ }
+ }
+
+ /**
+ * EFFECTS: Run the program
+ */
+ public void run() {
+ printPS1();
+ final Scanner scanner = new Scanner(System.in);
+ while (true) {
+ String[] args = scanner.nextLine().split(" ");
+ if (args.length <= 0 || args[0].isBlank()) {
+ printPS1();
+ continue;
+ }
+ handleLine(args);
+ }
+ }
+
+ public void setUnsaved(boolean unsaved) {
+ this.unsaved = unsaved;
+ }
+
+ public CertificationAuthority getCa() {
+ return ca;
+ }
+}
diff --git a/src/main/ui/tui/MainScreen.java b/src/main/ui/tui/MainScreen.java
new file mode 100644
index 0000000..860eff7
--- /dev/null
+++ b/src/main/ui/tui/MainScreen.java
@@ -0,0 +1,234 @@
+package ui.tui;
+
+import model.asn1.ASN1Object;
+import model.asn1.UtcTime;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import model.ca.Template;
+import model.csr.CertificationRequest;
+import model.pki.cert.Certificate;
+import model.pki.crl.CertificateList;
+import model.pki.crl.Reason;
+import model.pki.crl.RevokedCertificate;
+import ui.Utils;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.Optional;
+
+/**
+ * The main screen that handles submenus (mgmt / issue / template), list certs, revoke certs, generate CRLs.
+ */
+public class MainScreen implements UIHandler {
+ private final JCA session;
+
+ /**
+ * EFFECTS: Init with the parent session.
+ */
+ public MainScreen(JCA session) {
+ this.session = session;
+ }
+
+ /**
+ * EFFECTS: Print help
+ */
+ @Override
+ public void help() {
+ System.out.print("mgmt\tView and manage the CA certificate\n"
+ + "issue\tIssue a certificate\n"
+ + "show\tList all issued certificates\n"
+ + "export\tExport a certificate to file (DER)\n"
+ + "template\tManage templates\n"
+ + "revoke\tRevoke a certificate\n"
+ + "crl\t\tSign CRL\n"
+ + "log\t\tView audit logs\n"
+ + "exit\tExit\n"
+ + "help\tPrint this message\n");
+ }
+
+ /**
+ * EFFECTS: Print each issued cert in Subject Serial Status format.
+ */
+ @Override
+ public void show() {
+ session.getCa().getSigned().forEach(cert -> {
+ System.out.printf("%s\t%d\t%s\n",
+ cert.getCertificate().getSubject().toString(),
+ cert.getCertificate().getSerialNumber().getLong(),
+ session.getCa().getRevoked().stream().anyMatch(rev -> rev.getSerialNumber().getLong()
+ == cert.getCertificate().getSerialNumber().getLong()) ? "REVOKED" : "OK");
+ });
+ }
+
+ /**
+ * EFFECTS: Read the input CSR.
+ */
+ private CertificationRequest handleIssueInputCSR() {
+ try {
+ return new CertificationRequest(new BytesReader(session.handleInputPEM("CERTIFICATE REQUEST")),
+ false);
+ } catch (ParseException e) {
+ System.out.println(e.getMessage());
+ return null;
+ }
+ }
+
+ /**
+ * EFFECTS: Handle the issue command. Read CSR, find template, switch to issue screen.
+ */
+ private void handleIssue(String... args) {
+ if (!session.checkCA(true)) {
+ return;
+ }
+ if (args.length <= 1) {
+ System.out.println("Usage: issue <template>");
+ return;
+ }
+ Template tmp = session.getCa().findTemplate(args[1], true);
+ if (tmp == null) {
+ System.out.println("Cannot find the template specified");
+ return;
+ }
+ CertificationRequest req = handleIssueInputCSR();
+ if (req != null) {
+ session.setScreen(Screen.ISSUE, req, new Template(tmp.getName(),
+ true,
+ tmp.getSubject(),
+ tmp.getValidity()));
+ }
+ }
+
+ /**
+ * EFFECTS: Find issued and not revoked certificate by serial. Return null if not found.
+ */
+ private Certificate findCertBySerial(int serial) {
+ Optional<Certificate> c = session.getCa().getSigned()
+ .stream()
+ .filter(cert -> cert.getCertificate().getSerialNumber().getLong() == serial)
+ .findFirst();
+ if (c.isEmpty()) {
+ System.out.println("Cannot find the certificate specified");
+ return null;
+ }
+ if (session.getCa().getRevoked().stream().anyMatch(rev -> rev.getSerialNumber().getLong() == serial)) {
+ System.out.println("The certificate has already been revoked.");
+ return null;
+ }
+ return c.get();
+ }
+
+ /**
+ * EFFECTS: Handle the revoke command and log it.
+ * MODIFIES: session
+ */
+ private void handleRevoke(String... args) {
+ if (args.length < 3) {
+ System.out.println("Usage: revoke <serial> <reason>");
+ return;
+ }
+ try {
+ final Reason reason = Reason.valueOf(args[2]);
+ int serial = Integer.parseInt(args[1]);
+ Certificate c = findCertBySerial(serial);
+ if (c == null) {
+ return;
+ }
+ session.getCa().revoke(new RevokedCertificate(ASN1Object.TAG_SEQUENCE, null,
+ c.getCertificate().getSerialNumber(),
+ new UtcTime(UtcTime.TAG, null, ZonedDateTime.now(ZoneId.of("UTC"))), reason));
+ session.save();
+ } catch (IllegalArgumentException ignored) {
+ System.out.println("Illegal serial number or reason");
+ }
+ }
+
+ /**
+ * EFFECTS: Export a cert to file
+ */
+ private void handleExport(String... args) {
+ if (args.length < 3) {
+ System.out.println("Usage: export <serial> <path>");
+ return;
+ }
+ try {
+ int serial = Integer.parseInt(args[1]);
+ Certificate c = findCertBySerial(serial);
+ if (c == null) {
+ return;
+ }
+ final File fd = new File(args[2]);
+ final OutputStream out = new FileOutputStream(fd);
+ out.write(Utils.byteToByte(c.encodeDER()));
+ out.close();
+ } catch (IllegalArgumentException ignored) {
+ System.out.println("Illegal serial number");
+ } catch (IOException e) {
+ System.out.println(e.getMessage());
+ }
+ }
+
+ /**
+ * EFFECTS: Issue a CRL and do audit log.
+ * MODIFIES: session
+ */
+ private void handleCRL() {
+ if (!session.checkCA(true)) {
+ return;
+ }
+ try {
+ CertificateList crl = session.getCa().signCRL();
+ session.save();
+ System.out.println(Utils.toPEM(crl.encodeDER(), "X509 CRL"));
+ } catch (Throwable e) {
+ System.out.println(e.getMessage());
+ }
+ }
+
+ /**
+ * EFFECTS: Handle commands
+ */
+ @Override
+ public void command(String... args) {
+ switch (args[0]) {
+ case "mgmt":
+ session.setScreen(Screen.MGMT);
+ return;
+ case "issue":
+ handleIssue(args);
+ return;
+ case "revoke":
+ handleRevoke(args);
+ return;
+ case "export":
+ handleExport(args);
+ return;
+ case "template":
+ session.setScreen(Screen.TEMPLATES);
+ return;
+ case "crl":
+ handleCRL();
+ return;
+ }
+ help();
+ }
+
+ /**
+ * EFFECTS: Exit the program
+ */
+ @Override
+ public Screen exit() {
+ return null;
+ }
+
+ /**
+ * EFFECTS: return "/ %"
+ */
+ @Override
+ public String getPS1() {
+ return "/ %";
+ }
+}
diff --git a/src/main/ui/tui/MgmtScreen.java b/src/main/ui/tui/MgmtScreen.java
new file mode 100644
index 0000000..007bd0e
--- /dev/null
+++ b/src/main/ui/tui/MgmtScreen.java
@@ -0,0 +1,153 @@
+package ui.tui;
+
+import model.asn1.exceptions.InvalidCAException;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import model.csr.CertificationRequest;
+import model.pki.cert.Certificate;
+import model.pki.cert.TbsCertificate;
+import ui.Utils;
+
+import java.security.NoSuchAlgorithmException;
+import java.util.Base64;
+
+/**
+ * Manage the private key and CA certificate. It can print the public key, generate CSR, and install CA cert.
+ */
+public class MgmtScreen implements UIHandler {
+ private final JCA session;
+
+ /**
+ * EFFECTS: Init with the parent session.
+ */
+ public MgmtScreen(JCA session) {
+ this.session = session;
+ }
+
+ /**
+ * EFFECTS: Print help
+ */
+ @Override
+ public void help() {
+ System.out.print("show\tView the public key and CA certificate\n"
+ + "genkey\tGenerate a RSA private key\n"
+ + "csr\tGenerate a CSR for a upper-level CA to sign\n"
+ + "install\tInstall a CA certificate\n"
+ + "exit\tGo to main menu\n"
+ + "help\tPrint this message\n");
+ }
+
+ /**
+ * EFFECTS: Format the public key and CA
+ */
+ @Override
+ public void show() {
+ if (session.getCa().getPublicKey() == null) {
+ System.out.println("No private key installed");
+ } else {
+ System.out.println("Public Key (RSA2048):");
+ System.out.printf("\tModules:\t\t%s\n", session.getCa().getPublicKey().getModulus().toString(10));
+ System.out.printf("\tPublic Exponent:\t%s\n",
+ session.getCa().getPublicKey().getPublicExponent().toString(16));
+ }
+ if (!session.checkCA(true)) {
+ return;
+ }
+ final TbsCertificate info = session.getCa().getCertificate().getCertificate();
+ System.out.printf("Subject:\t%s\n", info.getSubject().toString());
+ System.out.printf("Issuer:\t%s\n", info.getIssuer().toString());
+ System.out.printf("Not Before:\t%s\n", info.getValidity().getNotBefore().getTimestamp());
+ System.out.printf("Not After:\t%s\n", info.getValidity().getNotAfter().getTimestamp());
+ System.out.printf("Signature:\t%s\n",
+ Base64.getEncoder().encodeToString(Utils.byteToByte(info.getSubjectPublicKeyInfo()
+ .getSubjectPublicKey().getConvertedVal())));
+ }
+
+ /**
+ * EFFECT: Generate a CSR
+ * MODIFIES: session
+ */
+ private void handleCSR() {
+ if (!session.checkCA(false)) {
+ return;
+ }
+ try {
+ CertificationRequest req = session.getCa().signCSR();
+ System.out.println(Utils.toPEM(req.encodeDER(), "CERTIFICATE REQUEST"));
+ session.setUnsaved(true);
+ } catch (Throwable e) {
+ System.out.println(e.getMessage());
+ }
+ }
+
+ /**
+ * EFFECTS: Handle the 'install' command. Read incoming certificate and validate it.
+ * MODIFIES: session
+ */
+ private void handleInstall() {
+ if (!session.checkCA(false)) {
+ return;
+ }
+ try {
+ final Byte[] in = session.handleInputPEM("CERTIFICATE");
+ final Certificate cert = new Certificate(new BytesReader(in), false);
+ session.getCa().installCertificate(cert);
+ session.setUnsaved(true);
+ } catch (InvalidCAException | ParseException e) {
+ System.out.println(e.getMessage());
+ }
+ }
+
+ /**
+ * EFFECTS: Handle the 'genkey' command. Generate a RSA2048 private key.
+ * MODIFIES: session
+ */
+ private void handleGenKey() {
+ if (session.getCa().getPublicKey() != null) {
+ System.out.println("A private key is already installed.");
+ }
+ try {
+ session.getCa().generateKey();
+ session.setUnsaved(true);
+ } catch (NoSuchAlgorithmException e) {
+ System.out.println(e.getMessage());
+ }
+ }
+
+ /**
+ * EFFECTS: Handle commands.
+ */
+ @Override
+ public void command(String... args) {
+ switch (args[0]) {
+ case "genkey":
+ handleGenKey();
+ break;
+ case "csr":
+ handleCSR();
+ break;
+ case "install":
+ handleInstall();
+ break;
+ default:
+ help();
+ break;
+ }
+ }
+
+ /**
+ * EFFECTS: Go to main menu
+ */
+ @Override
+ public Screen exit() {
+ return Screen.MAIN;
+ }
+
+ /**
+ * EFFECTS: return "/ca/ #"
+ */
+ @Override
+ public String getPS1() {
+ return "/ca/ #";
+ }
+} \ No newline at end of file
diff --git a/src/main/ui/tui/Screen.java b/src/main/ui/tui/Screen.java
new file mode 100644
index 0000000..413fe8a
--- /dev/null
+++ b/src/main/ui/tui/Screen.java
@@ -0,0 +1,31 @@
+package ui.tui;
+
+/**
+ * The screen type
+ */
+public enum Screen {
+ /**
+ * Main menu (mgmt, issue, template, crl, show, revoke, log)
+ */
+ MAIN,
+
+ /**
+ * The CA management menu (show, csr, install)
+ */
+ MGMT,
+
+ /**
+ * The issue menu (show, set, commit)
+ */
+ ISSUE,
+
+ /**
+ * The templates menu (show, add, enable, disable, remove)
+ */
+ TEMPLATES,
+
+ /**
+ * The template edit menu (show, set, commit)
+ */
+ TEMPLATE_SET
+}
diff --git a/src/main/ui/tui/TemplateSetScreen.java b/src/main/ui/tui/TemplateSetScreen.java
new file mode 100644
index 0000000..0f6df3a
--- /dev/null
+++ b/src/main/ui/tui/TemplateSetScreen.java
@@ -0,0 +1,157 @@
+package ui.tui;
+
+import model.asn1.exceptions.ParseException;
+import model.ca.Template;
+import model.x501.Name;
+
+/**
+ * The screen that modifies the properties of a single template and add it to the store.
+ */
+public class TemplateSetScreen implements UIHandler {
+ private final JCA session;
+
+ /**
+ * EFFECTS: Init with session.
+ */
+ public TemplateSetScreen(JCA session) {
+ this.session = session;
+ }
+
+ private Template template;
+
+ /**
+ * EFFECTS: Print help
+ */
+ @Override
+ public void help() {
+ System.out.println("show\tView the current template settings\n"
+ + "set\tSet key value\n"
+ + "commit\tSave the template\n"
+ + "exit\tDiscard changes\n"
+ + "help\tPrint this help message\n");
+ }
+
+ /**
+ * EFFECTS: Parse and set / unset the subject of the template
+ * MODIFIES: this
+ */
+ private void handleSetSubject(String val) {
+ try {
+ if (val == null) {
+ template = new Template(template.getName(), template.isEnabled(), (Name) null, template.getValidity());
+ } else {
+ template = new Template(template.getName(), template.isEnabled(), val, template.getValidity());
+ }
+ } catch (ParseException e) {
+ System.out.println(e.getMessage());
+ }
+ }
+
+ /**
+ * EFFECTS: Set the validity of the template to the given integer
+ * MODIFIES: this
+ */
+ private void handleSetValidity(String val) {
+ if (val == null) {
+ System.out.println("Cannot unset validity");
+ return;
+ }
+ try {
+ long i = Long.parseLong(val);
+ if (i <= 0) {
+ System.out.println("Invalid validity days");
+ return;
+ }
+ template = new Template(template.getName(), template.isEnabled(), template.getSubject(), i);
+ } catch (NumberFormatException ignored) {
+ System.out.println("Invalid validity days");
+ }
+ }
+
+ /**
+ * EFFECTS: Handle the `set` command.
+ * MODIFIES: this
+ */
+ private void handleSet(String... args) {
+ if (args.length != 2 && args.length != 3) {
+ System.out.println("Usage: set <key> <value>");
+ System.out.println("Supported keys: subject validity");
+ return;
+ }
+ String val = args.length == 3 ? args[2] : null;
+ switch (args[1]) {
+ case "subject":
+ handleSetSubject(val);
+ break;
+ case "validity":
+ handleSetValidity(val);
+ break;
+ default:
+ System.out.println("Unknown key");
+ break;
+ }
+ }
+
+ /**
+ * EFFECTS: Add the template to store and switch to templates screen.
+ * MODIFIES: session
+ */
+ @Override
+ public void commit() {
+ session.getCa().addTemplate(template);
+ session.setUnsaved(true);
+ session.setScreen(Screen.TEMPLATES);
+ }
+
+ /**
+ * EFFECTS: Show template info.
+ */
+ @Override
+ public void show() {
+ System.out.println("Subject:\t" + template.getSubject());
+ System.out.println("Validity:\t" + template.getValidity() + " days");
+ }
+
+ /**
+ * EFFECTS: Handle commands
+ */
+ @Override
+ public void command(String... args) {
+ switch (args[0]) {
+ case "set":
+ handleSet(args);
+ break;
+ default:
+ case "help":
+ help();
+ break;
+ }
+ }
+
+ /**
+ * EFFECTS: Return to templates list and clear the current template in editing.
+ */
+ @Override
+ public Screen exit() {
+ template = null;
+ return Screen.TEMPLATES;
+ }
+
+ /**
+ * EFFECTS: yuuta@JCA /templates/name/ %
+ */
+ @Override
+ public String getPS1() {
+ return String.format("/templates/%s/ %%", template.getName());
+ }
+
+ /**
+ * EFFECT: Edit args[0].
+ * REQUIRES: args.length = 1; args[0] instanceof Template
+ * MODIFIES: args[0]
+ */
+ @Override
+ public void enter(Object... args) {
+ template = (Template) args[0];
+ }
+} \ No newline at end of file
diff --git a/src/main/ui/tui/TemplatesScreen.java b/src/main/ui/tui/TemplatesScreen.java
new file mode 100644
index 0000000..6b7f18c
--- /dev/null
+++ b/src/main/ui/tui/TemplatesScreen.java
@@ -0,0 +1,138 @@
+package ui.tui;
+
+import model.ca.Template;
+import model.x501.Name;
+
+/**
+ * The screen that allows users to list templates and manage them.
+ */
+public class TemplatesScreen implements UIHandler {
+ private final JCA session;
+
+ /**
+ * EFFECTS: Init with the session.
+ */
+ public TemplatesScreen(JCA session) {
+ this.session = session;
+ }
+
+ /**
+ * EFFECTS: Print help.
+ */
+ @Override
+ public void help() {
+ System.out.println("show\tList templates\n"
+ + "add\tCreate a new template\n"
+ + "enable\tEnable a template\n"
+ + "disable\tDisable a template\n"
+ + "delete\tDelete a template\n"
+ + "exit\tGo to main menu\n"
+ + "help\tPrint this message");
+ }
+
+ /**
+ * EFFECTS: List templates in Name[ENABLED / DISABLED] Subject Validity format.
+ */
+ @Override
+ public void show() {
+ session.getCa().getTemplates().forEach(tem ->
+ System.out.printf("%s[%s]\t%s\t%d Days\n",
+ tem.getName(),
+ tem.isEnabled() ? "ENABLED" : "DISABLED",
+ tem.getSubject(),
+ tem.getValidity()));
+ }
+
+ /**
+ * EFFECTS: Create a new template with the given name and switch to the template set screen.
+ */
+ private void handleAdd(String... args) {
+ if (args.length <= 1) {
+ System.out.println("Usage: add <name>");
+ return;
+ }
+ if (session.getCa().findTemplate(args[1], false) != null) {
+ System.out.println("The template already exists.");
+ return;
+ }
+
+ session.setScreen(Screen.TEMPLATE_SET,
+ new Template(args[1], false, (Name) null, 30));
+ }
+
+ /**
+ * EFFECTS: Handle the enable / disable commands.
+ * MODIFIES: session
+ */
+ private void handleEnableDisable(boolean enable, String... args) {
+ if (args.length <= 1) {
+ System.out.printf("Usage: %s <template>\n", enable ? "enable" : "disable");
+ return;
+ }
+ Template tmp = session.getCa().findTemplate(args[1], false);
+ if (tmp == null) {
+ System.out.println("Cannot find the template specified");
+ return;
+ }
+ session.getCa().setTemplateEnable(tmp, enable);
+ session.setUnsaved(true);
+ }
+
+ /**
+ * EFFECTS: Handle the delete command
+ * MODIFIES: session
+ */
+ private void handleDelete(String... args) {
+ if (args.length <= 1) {
+ System.out.println("Usage: delete <template>");
+ return;
+ }
+ Template tmp = session.getCa().findTemplate(args[1], true);
+ if (tmp == null) {
+ System.out.println("Cannot find the template specified");
+ return;
+ }
+ session.getCa().removeTemplate(tmp);
+ session.setUnsaved(true);
+ }
+
+ /**
+ * EFFECTS: Handle commands.
+ */
+ @Override
+ public void command(String... args) {
+ switch (args[0]) {
+ case "add":
+ handleAdd(args);
+ break;
+ case "enable":
+ handleEnableDisable(true, args);
+ break;
+ case "disable":
+ handleEnableDisable(false, args);
+ break;
+ case "delete":
+ handleDelete(args);
+ break;
+ default:
+ help();
+ break;
+ }
+ }
+
+ /**
+ * EFFECTS: Go to main menu.
+ */
+ @Override
+ public Screen exit() {
+ return Screen.MAIN;
+ }
+
+ /**
+ * EFFECTS: Return "/templates/ %"
+ */
+ @Override
+ public String getPS1() {
+ return "/templates/ %";
+ }
+}
diff --git a/src/main/ui/tui/UIHandler.java b/src/main/ui/tui/UIHandler.java
new file mode 100644
index 0000000..672fd50
--- /dev/null
+++ b/src/main/ui/tui/UIHandler.java
@@ -0,0 +1,45 @@
+package ui.tui;
+
+/**
+ * Represents a screen
+ */
+public interface UIHandler {
+ /**
+ * EFFECTS: Called when the screen is switched to.
+ */
+ default void enter(Object... args) {
+
+ }
+
+ /**
+ * EFFECTS: Show objects. command() will not be called.
+ */
+ void show();
+
+ /**
+ * EFFECTS: Commit changes and exit. command() will not be called.
+ */
+ default void commit() {
+ }
+
+ /**
+ * EFFECTS: Discard changes and exit. command() will not be called. Returns the next screen.
+ */
+ Screen exit();
+
+ /**
+ * EFFECTS: Run help. command() will not be called.
+ */
+ void help();
+
+ /**
+ * EFFECTS: Any commands rather than commit / exit / help.
+ * REQUIRES: args != null && args.length >= 1
+ */
+ void command(String... args);
+
+ /**
+ * EFFECTS: Return the current PS1 prompt.
+ */
+ String getPS1();
+}