aboutsummaryrefslogtreecommitdiff
path: root/src/main/ui
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/ui')
-rw-r--r--src/main/ui/IssueScreen.java138
-rw-r--r--src/main/ui/JCA.java203
-rw-r--r--src/main/ui/Main.java4
-rw-r--r--src/main/ui/MainScreen.java200
-rw-r--r--src/main/ui/MgmtScreen.java170
-rw-r--r--src/main/ui/Screen.java31
-rw-r--r--src/main/ui/TemplateSetScreen.java131
-rw-r--r--src/main/ui/TemplatesScreen.java108
-rw-r--r--src/main/ui/UIHandler.java45
-rw-r--r--src/main/ui/Utils.java44
10 files changed, 1049 insertions, 25 deletions
diff --git a/src/main/ui/IssueScreen.java b/src/main/ui/IssueScreen.java
new file mode 100644
index 0000000..e152b0d
--- /dev/null
+++ b/src/main/ui/IssueScreen.java
@@ -0,0 +1,138 @@
+package ui;
+
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import model.ca.Template;
+import model.csr.CertificationRequest;
+import model.pki.cert.Certificate;
+
+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];
+ }
+
+ @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");
+ }
+
+ @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");
+ }
+
+ @Override
+ public void commit() {
+ try {
+ Certificate certificate = session.getCa().signCert(incomingCSR.getCertificationRequestInfo(), template);
+ System.out.println(Utils.toPEM(certificate.encodeDER(), "CERTIFICATE"));
+ session.log("A certificate was issued.");
+ session.setScreen(Screen.MAIN);
+ } catch (Throwable e) {
+ System.out.println(e.getMessage());
+ }
+ }
+
+ private void handleIssueSetSubject(String val) {
+ try {
+ template.setSubject(val);
+ } catch (ParseException e) {
+ System.out.println(e.getMessage());
+ }
+ }
+
+ 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.setValidity(i);
+ } catch (NumberFormatException ignored) {
+ System.out.println("Invalid validity days");
+ }
+ }
+
+ 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) {
+ switch (args[0]) {
+ case "set":
+ handleIssueSet(args);
+ break;
+ default:
+ help();
+ break;
+ }
+ }
+
+ /**
+ * EFFECTS: Clear the certificates and return main.
+ * MODIFIES: this
+ */
+ @Override
+ public Screen exit() {
+ incomingCSR = null;
+ template = null;
+ return Screen.MAIN;
+ }
+
+ @Override
+ public String getPS1() {
+ return String.format("/%s/ %%", template.getSubject() == null
+ ? incomingCSR.getCertificationRequestInfo().getSubject()
+ : template.getSubject());
+ }
+}
diff --git a/src/main/ui/JCA.java b/src/main/ui/JCA.java
new file mode 100644
index 0000000..f9467ea
--- /dev/null
+++ b/src/main/ui/JCA.java
@@ -0,0 +1,203 @@
+package ui;
+
+import model.asn1.exceptions.ParseException;
+import model.ca.AuditLogEntry;
+import model.ca.CACertificate;
+import model.ca.Template;
+
+import java.nio.charset.StandardCharsets;
+import java.security.NoSuchAlgorithmException;
+import java.time.ZonedDateTime;
+import java.util.*;
+
+/**
+ * Main program
+ */
+public class JCA {
+ /**
+ * The current screen.
+ */
+ private UIHandler screen;
+
+ /**
+ * 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;
+
+ /**
+ * Templates
+ */
+ private final List<Template> templates;
+
+ /**
+ * The CA
+ */
+ private final CACertificate ca;
+
+ /**
+ * Audit logs
+ */
+ private final List<AuditLogEntry> logs;
+
+ /**
+ * Current user
+ */
+ private final String user;
+
+ /**
+ * EFFECTS: Init with main screen, empty templates, logs, user 'yuuta', and generate a private key with no CA cert.
+ * Throws {@link NoSuchAlgorithmException} when crypto issue happens.
+ */
+ public JCA() throws NoSuchAlgorithmException {
+ 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.templates = new ArrayList<>();
+ this.ca = new CACertificate();
+ this.logs = new ArrayList<>();
+ this.user = "yuuta";
+
+ this.ca.generateKey();
+ }
+
+ /**
+ * 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("The CA is not installed yet");
+ return false;
+ } else if (!requireInstalled && ca.getCertificate() != null) {
+ System.out.println("The CA is 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);
+ }
+
+ /**
+ * EFFECTS: Find the template based on name, or null if not found.
+ */
+ public Template findTemplate(String name, boolean requireEnabled) {
+ Optional<Template> opt = templates.stream().filter(temp -> {
+ if (requireEnabled && !temp.isEnabled()) {
+ return false;
+ }
+ return temp.getName().equals(name);
+ }).findFirst();
+ return opt.orElse(null);
+ }
+
+ /**
+ * 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);
+ }
+
+ private void handleLine(String... args) {
+ if (!args[0].isBlank()) {
+ switch (args[0]) {
+ case "help":
+ screen.help();
+ break;
+ case "show":
+ screen.show();
+ break;
+ case "commit":
+ screen.commit();
+ break;
+ case "exit":
+ setScreen(screen.exit());
+ break;
+ default:
+ screen.command(args);
+ break;
+ }
+ }
+ printPS1();
+ }
+
+ private void printPS1() {
+ System.out.printf("%s@JCA %s ", user, screen.getPS1());
+ }
+
+ /**
+ * EFFECT: Log an action to the audit log
+ * MODIFIES: this
+ */
+ public void log(String action) {
+ this.logs.add(new AuditLogEntry(user, ZonedDateTime.now(), action));
+ }
+
+ /**
+ * EFFECTS: Run the program
+ */
+ public void run() {
+ printPS1();
+ final Scanner scanner = new Scanner(System.in);
+ while (true) {
+ handleLine(scanner.nextLine().split(" "));
+ }
+ }
+
+ public List<Template> getTemplates() {
+ return templates;
+ }
+
+ public CACertificate getCa() {
+ return ca;
+ }
+
+ public List<AuditLogEntry> getLogs() {
+ return logs;
+ }
+}
diff --git a/src/main/ui/Main.java b/src/main/ui/Main.java
index d80be21..1429532 100644
--- a/src/main/ui/Main.java
+++ b/src/main/ui/Main.java
@@ -1,7 +1,7 @@
package ui;
public class Main {
- public static void main(String[] args) {
-
+ public static void main(String[] args) throws Throwable {
+ new JCA().run();
}
}
diff --git a/src/main/ui/MainScreen.java b/src/main/ui/MainScreen.java
new file mode 100644
index 0000000..69cb32c
--- /dev/null
+++ b/src/main/ui/MainScreen.java
@@ -0,0 +1,200 @@
+package ui;
+
+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.Reason;
+import model.pki.crl.RevokedCertificate;
+
+import java.io.*;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.Optional;
+
+public class MainScreen implements UIHandler {
+ private final JCA session;
+
+ /**
+ * EFFECTS: Init with the parent session.
+ */
+ public MainScreen(JCA session) {
+ this.session = session;
+ }
+
+ @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");
+ }
+
+ @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");
+ });
+ }
+
+ private CertificationRequest handleIssueInputCSR() {
+ try {
+ return new CertificationRequest(new BytesReader(session.handleInputPEM("CERTIFICATE REQUEST")),
+ false);
+ } catch (ParseException e) {
+ System.out.println(e.getMessage());
+ return null;
+ }
+ }
+
+ private void handleIssue(String... args) {
+ if (!session.checkCA(true)) {
+ return;
+ }
+ if (args.length <= 1) {
+ System.out.println("Usage: issue <template>");
+ return;
+ }
+ Template tmp = session.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();
+ }
+
+ 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.log("A certificate has been revoked.");
+ } catch (IllegalArgumentException ignored) {
+ System.out.println("Illegal serial number or reason");
+ }
+ }
+
+ 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 or reason");
+ } catch (IOException e) {
+ System.out.println(e.getMessage());
+ }
+ }
+
+ private void handleCRL() {
+ if (!session.checkCA(true)) {
+ return;
+ }
+ try {
+ System.out.println(Utils.toPEM(session.getCa().signCRL().encodeDER(), "X509 CRL"));
+ session.log("A CRL was signed");
+ } catch (Throwable e) {
+ System.out.println(e.getMessage());
+ }
+ }
+
+ private void handleLog() {
+ session.getLogs().forEach(System.out::println);
+ }
+
+ @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;
+ case "log":
+ handleLog();
+ return;
+ }
+ help();
+ }
+
+ @Override
+ public Screen exit() {
+ return null;
+ }
+
+ @Override
+ public String getPS1() {
+ return "/ %";
+ }
+}
diff --git a/src/main/ui/MgmtScreen.java b/src/main/ui/MgmtScreen.java
new file mode 100644
index 0000000..613aa50
--- /dev/null
+++ b/src/main/ui/MgmtScreen.java
@@ -0,0 +1,170 @@
+package ui;
+
+import model.asn1.ASN1Object;
+import model.asn1.BitString;
+import model.asn1.Bool;
+import model.asn1.ObjectIdentifier;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import model.csr.CertificationRequest;
+import model.pki.SubjectPublicKeyInfo;
+import model.pki.cert.Certificate;
+import model.pki.cert.Extension;
+import model.pki.cert.TbsCertificate;
+
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.BitSet;
+
+public class MgmtScreen implements UIHandler {
+ private final JCA session;
+
+ /**
+ * EFFECTS: Init with the parent session.
+ */
+ public MgmtScreen(JCA session) {
+ this.session = session;
+ }
+
+ @Override
+ public void help() {
+ System.out.print("show\tView the public key and CA certificate\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() {
+ System.out.printf("Public Key:\t%s\n",
+ Base64.getEncoder().encodeToString(session.getCa().getPublicKey().getEncoded()));
+ 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())));
+ }
+
+ private void handleCSR() {
+ if (!session.checkCA(false)) {
+ return;
+ }
+ try {
+ CertificationRequest req = session.getCa().signCSR();
+ System.out.println(Utils.toPEM(req.encodeDER(), "CERTIFICATE REQUEST"));
+ session.log("Signed CA CSR.");
+ } catch (Throwable e) {
+ System.out.println(e.getMessage());
+ }
+ }
+
+ private void validateCACertificateVersion(Certificate cert) throws ParseException {
+ if (cert.getCertificate().getVersion() == null
+ || cert.getCertificate().getVersion().getLong() != TbsCertificate.VERSION_V3) {
+ throw new ParseException("The input certificate must be V3");
+ }
+ }
+
+ private void validateCACertificatePublicKey(Certificate cert) throws ParseException {
+ final SubjectPublicKeyInfo expectedPKInfo = session.getCa().getCAPublicKeyInfo();
+ if (!Arrays.equals(cert.getCertificate().getSubjectPublicKeyInfo().getAlgorithm().getType().getInts(),
+ expectedPKInfo.getAlgorithm().getType().getInts())
+ || !Arrays.equals(cert.getCertificate().getSubjectPublicKeyInfo().getSubjectPublicKey().getVal(),
+ expectedPKInfo.getSubjectPublicKey().getVal())) {
+ throw new ParseException("The input certificate does not have the corresponding public key");
+ }
+ }
+
+ private void validateCACertificateBasicConstraints(Certificate cert) throws ParseException {
+ final Extension basicConstraints = cert.getCertificate().getExtension(ObjectIdentifier.OID_BASIC_CONSTRAINTS);
+ if (basicConstraints == null
+ || basicConstraints.getExtnValue().getBytes().length <= 0) {
+ throw new ParseException("The certificate does not have a valid basicConstraints extension.");
+ }
+ final ASN1Object basicConstraintsValue =
+ new ASN1Object(new BytesReader(basicConstraints.getExtnValue().getBytes()), false);
+ if (basicConstraintsValue.getLength() <= 0) {
+ throw new ParseException("The certificate does not have a valid basicConstraints extension.");
+ }
+ final ASN1Object bool =
+ ASN1Object.parse(new BytesReader(basicConstraintsValue.encodeValueDER()), false);
+ if (!(bool instanceof Bool)
+ || !((Bool) bool).getValue()) {
+ throw new ParseException("The certificate does not have a valid basicConstraints extension.");
+ }
+ }
+
+ private void validateCACertificateKeyUsage(Certificate cert) throws ParseException {
+ final Extension keyUsage = cert.getCertificate().getExtension(ObjectIdentifier.OID_KEY_USAGE);
+ if (keyUsage == null
+ || keyUsage.getExtnValue().getBytes().length <= 0) {
+ throw new ParseException("The certificate does not have a valid keyUsage extension.");
+ }
+ final ASN1Object keyUsageValue =
+ ASN1Object.parse(new BytesReader(keyUsage.getExtnValue().getBytes()), false);
+ if (keyUsageValue.getLength() <= 0
+ || !(keyUsageValue instanceof BitString)) {
+ throw new ParseException("The certificate does not have a valid keyUsage extension.");
+ }
+ final BitSet bitSet = BitSet.valueOf(Utils.byteToByte(((BitString) keyUsageValue).getVal()));
+ if (!bitSet.get(7) || !bitSet.get(2) || !bitSet.get(1)) {
+ throw new ParseException("The certificate does not have a valid keyUsage extension.");
+ }
+ }
+
+ private void handleInstall() {
+ if (!session.checkCA(false)) {
+ return;
+ }
+ try {
+ final Byte[] in = session.handleInputPEM("CERTIFICATE");
+ Certificate cert = new Certificate(new BytesReader(in), false);
+ validateCACertificateVersion(cert);
+ validateCACertificatePublicKey(cert);
+ validateCACertificateBasicConstraints(cert);
+ validateCACertificateKeyUsage(cert);
+ session.getCa().installCertificate(cert);
+ session.log("A CA certificate is installed.");
+ } catch (ParseException e) {
+ System.out.println(e.getMessage());
+ }
+ }
+
+ @Override
+ public void command(String... args) {
+ switch (args[0]) {
+ case "csr":
+ handleCSR();
+ break;
+ case "install":
+ handleInstall();
+ break;
+ default:
+ help();
+ break;
+ }
+ }
+
+ /**
+ * EFFECTS: Go to main menu
+ */
+ @Override
+ public Screen exit() {
+ return Screen.MAIN;
+ }
+
+ @Override
+ public String getPS1() {
+ return "/ca/ #";
+ }
+} \ No newline at end of file
diff --git a/src/main/ui/Screen.java b/src/main/ui/Screen.java
new file mode 100644
index 0000000..31370e1
--- /dev/null
+++ b/src/main/ui/Screen.java
@@ -0,0 +1,31 @@
+package ui;
+
+/**
+ * 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/TemplateSetScreen.java b/src/main/ui/TemplateSetScreen.java
new file mode 100644
index 0000000..9a31f50
--- /dev/null
+++ b/src/main/ui/TemplateSetScreen.java
@@ -0,0 +1,131 @@
+package ui;
+
+import model.asn1.exceptions.ParseException;
+import model.ca.Template;
+
+public class TemplateSetScreen implements UIHandler {
+ private final JCA session;
+
+ /**
+ * EFFECTS: Init with session.
+ */
+ public TemplateSetScreen(JCA session) {
+ this.session = session;
+ }
+
+ private Template template;
+
+ @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");
+ }
+
+ private void handleSetSubject(String val) {
+ try {
+ template.setSubject(val);
+ } catch (ParseException e) {
+ System.out.println(e.getMessage());
+ }
+ }
+
+ 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.setValidity(i);
+ } catch (NumberFormatException ignored) {
+ System.out.println("Invalid validity days");
+ }
+ }
+
+ 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.getTemplates().add(template);
+ session.setScreen(Screen.TEMPLATES);
+ session.log("A new template is added.");
+ }
+
+ /**
+ * EFFECTS: Show template info.
+ */
+ @Override
+ public void show() {
+ System.out.println("Subject:\t" + template.getSubject());
+ System.out.println("Validity:\t" + template.getValidity() + " days");
+ }
+
+ @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/TemplatesScreen.java b/src/main/ui/TemplatesScreen.java
new file mode 100644
index 0000000..9b0bf3e
--- /dev/null
+++ b/src/main/ui/TemplatesScreen.java
@@ -0,0 +1,108 @@
+package ui;
+
+import model.ca.Template;
+
+public class TemplatesScreen implements UIHandler {
+ private final JCA session;
+
+ /**
+ * EFFECTS: Init with the session.
+ */
+ public TemplatesScreen(JCA session) {
+ this.session = session;
+ }
+
+ @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");
+ }
+
+ @Override
+ public void show() {
+ session.getTemplates().forEach(tem ->
+ System.out.printf("%s[%s]\t%s\t%d Days\n",
+ tem.getName(),
+ tem.isEnabled() ? "ENABLED" : "DISABLED",
+ tem.getSubject(),
+ tem.getValidity()));
+ }
+
+ private void handleAdd(String... args) {
+ if (args.length <= 1) {
+ System.out.println("Usage: add <name>");
+ return;
+ }
+ if (session.findTemplate(args[1], false) != null) {
+ System.out.println("The template already exists.");
+ return;
+ }
+
+ session.setScreen(Screen.TEMPLATE_SET,
+ new Template(args[1], false, null, 30));
+ }
+
+ 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.findTemplate(args[1], false);
+ if (tmp == null) {
+ System.out.println("Cannot find the template specified");
+ return;
+ }
+ tmp.setEnabled(enable);
+ session.log("A template was enabled / disabled.");
+ }
+
+ private void handleDelete(String... args) {
+ if (args.length <= 1) {
+ System.out.println("Usage: delete <template>");
+ return;
+ }
+ Template tmp = session.findTemplate(args[1], true);
+ if (tmp == null) {
+ System.out.println("Cannot find the template specified");
+ return;
+ }
+ session.getTemplates().remove(tmp);
+ session.log("A template was deleted.");
+ }
+
+ @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;
+ }
+ }
+
+ @Override
+ public Screen exit() {
+ return Screen.MAIN;
+ }
+
+ @Override
+ public String getPS1() {
+ return "/templates/ %";
+ }
+}
diff --git a/src/main/ui/UIHandler.java b/src/main/ui/UIHandler.java
new file mode 100644
index 0000000..f451542
--- /dev/null
+++ b/src/main/ui/UIHandler.java
@@ -0,0 +1,45 @@
+package ui;
+
+/**
+ * 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();
+}
diff --git a/src/main/ui/Utils.java b/src/main/ui/Utils.java
index ccb244e..3ed1300 100644
--- a/src/main/ui/Utils.java
+++ b/src/main/ui/Utils.java
@@ -49,19 +49,8 @@ public final class Utils {
}
/**
- * EFFECTS: Pack the big-endian bytes into a 64bit integer.
- * Throws {@link model.asn1.exceptions.ParseException} if the value is too large.
- */
- public static long bytesToLong(Byte[] array) throws ParseException {
- try {
- return new BigInteger(byteToByte(array)).longValueExact();
- } catch (ArithmeticException ignored) {
- throw new ParseException("Value is too large.");
- }
- }
-
- /**
- * EFFECTS: Unpack the multibyte 64bit integer to its shortest array of byte format.
+ * EFFECTS: Unpack the multibyte 64bit integer to its shortest array of byte format. Remove leading empty byte
+ * if that number is not zero.
*/
public static Byte[] valToByte(long val) {
byte[] v = BigInteger.valueOf(val).toByteArray();
@@ -77,15 +66,8 @@ public final class Utils {
}
/**
- * EFFECTS: Parse the two-digit octet string into an unsigned byte, preserving leading zero and negative values.
- * REQUIRES: The input octet must be a two-char string, with each char matching [0-9][A-F].
- */
- public static Byte parseByte(String octet) {
- return (byte) Integer.parseInt(octet, 16);
- }
-
- /**
- * EFFECTS: Decode the input PEM file, with optional check on tags.
+ * EFFECTS: Decode the input PEM file, with optional check on tags. \n must present after each line, optional after
+ * the last.
* Throws {@link ParseException} if the desiredTag is specified but the input does not have the specific tag, or
* if the input does not have any tags at all (not a PEM).
*/
@@ -93,7 +75,7 @@ public final class Utils {
final String str = new String(byteToByte(input), StandardCharsets.UTF_8);
Pattern pattern =
Pattern.compile("^-----BEGIN " + desiredTag
- + "-----$\n^(.*)$\n^-----END " + desiredTag + "-----$",
+ + "-----$\n^(.*)$\n^-----END " + desiredTag + "-----$\n*",
Pattern.DOTALL | Pattern.MULTILINE);
final Matcher matcher = pattern.matcher(str);
if (!matcher.matches()) {
@@ -102,4 +84,20 @@ public final class Utils {
final String b64 = matcher.group(1).replace("\n", "");
return byteToByte(Base64.getDecoder().decode(b64));
}
+
+ /**
+ * EFFECTS: Base64 encode the input bytes and convert them into PEM format.
+ * REQUIRES: desiredTag must be upper case and not empty.
+ */
+ public static String toPEM(Byte[] input, String tag) {
+ StringBuilder builder = new StringBuilder();
+ builder.append("-----BEGIN ");
+ builder.append(tag);
+ builder.append("-----\n");
+ builder.append(Base64.getEncoder().encodeToString(byteToByte(input)));
+ builder.append("\n-----END ");
+ builder.append(tag);
+ builder.append("-----");
+ return builder.toString();
+ }
}