aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main/model/asn1/exceptions/InvalidDBException.java10
-rw-r--r--src/main/model/ca/CertificationAuthority.java80
-rw-r--r--src/main/persistence/Decoder.java284
-rw-r--r--src/main/persistence/FS.java46
-rw-r--r--src/main/ui/IssueScreen.java8
-rw-r--r--src/main/ui/JCA.java95
-rw-r--r--src/main/ui/MainScreen.java6
-rw-r--r--src/main/ui/MgmtScreen.java3
-rw-r--r--src/main/ui/TemplateSetScreen.java8
-rw-r--r--src/main/ui/TemplatesScreen.java2
-rw-r--r--src/main/ui/Utils.java6
-rw-r--r--src/test/persistence/DecoderTest.java81
-rw-r--r--src/test/persistence/FSTest.java50
-rw-r--r--src/test/ui/UtilsTest.java6
14 files changed, 653 insertions, 32 deletions
diff --git a/src/main/model/asn1/exceptions/InvalidDBException.java b/src/main/model/asn1/exceptions/InvalidDBException.java
new file mode 100644
index 0000000..4068a4b
--- /dev/null
+++ b/src/main/model/asn1/exceptions/InvalidDBException.java
@@ -0,0 +1,10 @@
+package model.asn1.exceptions;
+
+/**
+ * The database is invalid.
+ */
+public class InvalidDBException extends RuntimeException {
+ public InvalidDBException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/src/main/model/ca/CertificationAuthority.java b/src/main/model/ca/CertificationAuthority.java
index feb557c..038d209 100644
--- a/src/main/model/ca/CertificationAuthority.java
+++ b/src/main/model/ca/CertificationAuthority.java
@@ -35,6 +35,8 @@ import java.util.stream.Stream;
* Holds a CA private key, its certificate, signed / revoked list, template list, and logs list.
*/
public class CertificationAuthority {
+ public static final int SERIAL_DEFAULT = 1;
+
/**
* The RSA2048 private key.
*/
@@ -81,14 +83,45 @@ public class CertificationAuthority {
private final String user;
/**
- * EFFECT: Init with a null key and null certificate, empty signed, revoked template, and log list, serial at 1, and
- * user "yuuta".
+ * EFFECT: Init with the given parameters and user "yuuta".
+ * Throws {@link NoSuchAlgorithmException} if the key is specified but RSA is not supported.
+ * Throws {@link InvalidKeySpecException} if the key specified is invalid.
+ * Throws {@link InvalidCAException} or {@link ParseException} if the CA specified is invalid.
+ * REQUIRES: n / p / e must be either all null or all non-null containing RSA2048 module and exponents.
+ * If certificate is non-null, n / p / e must be non-null.
+ */
+ public CertificationAuthority(BigInteger n, BigInteger p, BigInteger e,
+ Certificate certificate,
+ List<Certificate> signed,
+ int serial,
+ List<RevokedCertificate> revoked,
+ List<Template> templates,
+ List<AuditLogEntry> logs)
+ throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidCAException, ParseException {
+ if (n != null) {
+ setKey(n, p, e);
+ }
+ if (certificate != null) {
+ validateCertificate(certificate);
+ }
+ this.certificate = certificate;
+ this.signed = new ArrayList<>(signed);
+ this.serial = serial;
+ this.revoked = new ArrayList<>(revoked);
+ this.templates = new ArrayList<>(templates);
+ this.logs = new ArrayList<>(logs);
+ this.user = "yuuta";
+ }
+
+ /**
+ * EFFECT: Init with a null key and null certificate, empty signed, revoked template, and log list,
+ * serial at SERIAL_DEFAULT, and user "yuuta".
*/
public CertificationAuthority() {
this.key = null;
this.publicKey = null;
this.certificate = null;
- this.serial = 1;
+ this.serial = SERIAL_DEFAULT;
this.signed = new ArrayList<>();
this.revoked = new ArrayList<>();
this.templates = new ArrayList<>();
@@ -111,17 +144,29 @@ public class CertificationAuthority {
}
/**
- * EFFECTS: Load the RSA private and public exponents. This action will be logged.
+ * EFFECTS: Load the RSA private and public exponents.
* Throws {@link NoSuchAlgorithmException} if RSA is not available on the platform.
* Throws {@link InvalidKeySpecException} if the input is invalid.
* REQUIRES: getPublicKey() is null (i.e., no private key had been installed)
* MODIFIES: this
*/
- public void loadKey(BigInteger n, BigInteger p, BigInteger e)
+ private void setKey(BigInteger n, BigInteger p, BigInteger e)
throws NoSuchAlgorithmException, InvalidKeySpecException {
this.key = (RSAPrivateKey) KeyFactory.getInstance("RSA").generatePrivate(new RSAPrivateKeySpec(n, p));
this.publicKey =
(RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(new RSAPublicKeySpec(n, e));
+ }
+
+ /**
+ * EFFECTS: Load the RSA private and public exponents. This action will be logged.
+ * Throws {@link NoSuchAlgorithmException} if RSA is not available on the platform.
+ * Throws {@link InvalidKeySpecException} if the input is invalid.
+ * REQUIRES: getPublicKey() is null (i.e., no private key had been installed)
+ * MODIFIES: this
+ */
+ public void loadKey(BigInteger n, BigInteger p, BigInteger e)
+ throws NoSuchAlgorithmException, InvalidKeySpecException {
+ setKey(n, p, e);
log("Installed CA private key.");
}
@@ -185,6 +230,22 @@ public class CertificationAuthority {
}
/**
+ * EFFECT: Validate the CA certificate. Throws {@link InvalidCAException} if any of the
+ * following are violated:
+ * - It must be a v3 certificate
+ * - The new certificate must have the same algorithm and public key as getPublicKey()
+ * - It must have basicConstraints { cA = TRUE }
+ * - It must contain key usage Digital Signature, Certificate Sign, CRL Sign
+ * Throws {@link ParseException} if the cert has invalid extension values.
+ */
+ private void validateCertificate(Certificate certificate) throws InvalidCAException, ParseException {
+ validateCACertificateVersion(certificate);
+ validateCACertificatePublicKey(certificate);
+ validateCACertificateBasicConstraints(certificate);
+ validateCACertificateKeyUsage(certificate);
+ }
+
+ /**
* EFFECT: Install the CA certificate. Throws {@link InvalidCAException} if any of the
* following are violated:
* - It must be a v3 certificate
@@ -198,10 +259,7 @@ public class CertificationAuthority {
* MODIFIES: this
*/
public void installCertificate(Certificate certificate) throws InvalidCAException, ParseException {
- validateCACertificateVersion(certificate);
- validateCACertificatePublicKey(certificate);
- validateCACertificateBasicConstraints(certificate);
- validateCACertificateKeyUsage(certificate);
+ validateCertificate(certificate);
this.certificate = certificate;
log("CA certificate is installed.");
}
@@ -477,4 +535,8 @@ public class CertificationAuthority {
public RSAPublicKey getPublicKey() {
return publicKey;
}
+
+ public RSAPrivateKey getKey() {
+ return key;
+ }
}
diff --git a/src/main/persistence/Decoder.java b/src/main/persistence/Decoder.java
new file mode 100644
index 0000000..799a4ad
--- /dev/null
+++ b/src/main/persistence/Decoder.java
@@ -0,0 +1,284 @@
+package persistence;
+
+import model.asn1.ASN1Object;
+import model.asn1.ASN1Time;
+import model.asn1.Int;
+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.pki.cert.Certificate;
+import model.pki.crl.Reason;
+import model.pki.crl.RevokedCertificate;
+import model.x501.Name;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import ui.Utils;
+
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.security.NoSuchAlgorithmException;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.InvalidKeySpecException;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+/**
+ * Util class that encodes / decodes the CA into JSON and vice-versa.
+ */
+public class Decoder {
+ /**
+ * EFFECTS: Convert the list into a JSONArray using the supplied mapper and add to the JSONObject. Nothing is added
+ * if the given list is empty.
+ */
+ private static <T> void encodeList(JSONObject o, String key, List<T> list, Function<? super T, ?> mapper) {
+ if (list.isEmpty()) {
+ return;
+ }
+ o.put(key, new JSONArray(list.stream().map(mapper).collect(Collectors.toList())));
+ }
+
+ /**
+ * EFFECTS: Decode the JSONArray into a list of specific objects, using the given mapper. Return empty list if the
+ * o does not contain key.
+ * REQUIRES: If o contains key, it must be a JSONArray.
+ */
+ private static <T> List<T> decodeListFromString(final JSONObject o, String key,
+ Function<? super String, ? extends T> mapper) {
+ return o.keySet().contains(key)
+ ? StreamSupport.stream(o.getJSONArray(key).spliterator(), false)
+ .map(obj -> mapper.apply((String) obj))
+ .collect(Collectors.toList())
+ : Collections.emptyList();
+ }
+
+ /**
+ * EFFECTS: Decode the JSONArray into a list of specific objects, using the given mapper. Return empty list if the
+ * o does not contain key.
+ * REQUIRES: If o contains key, it must be a JSONArray.
+ */
+ private static <T> List<T> decodeListFromObject(final JSONObject o, String key,
+ Function<? super JSONObject, ? extends T> mapper) {
+ return o.keySet().contains(key)
+ ? StreamSupport.stream(o.getJSONArray(key).spliterator(), false)
+ .map(obj -> mapper.apply((JSONObject) obj))
+ .collect(Collectors.toList())
+ : Collections.emptyList();
+ }
+
+ /**
+ * EFFECTS: Decode the JSON object into CertificationAuthority
+ * Throws {@link InvalidDBException} if the JSON is invalid.
+ * Throws {@link NoSuchAlgorithmException} if RSA is not supported.
+ * JSON should be <pre>
+ * {
+ * "key": {
+ * "n": "octet string",
+ * "p": "octet string",
+ * "e": "octet string"
+ * },
+ * "cert": "-----BEGIN CERTIFICATE-----\n...",
+ * "templates": [
+ * {
+ * "name": "123",
+ * "enabled": true,
+ * "subject": "-----BEGIN DISTINGUISHED NAME-----\n...",
+ * "validity": 100
+ * }
+ * ],
+ * "signed": [
+ * "-----BEGIN CERTIFICATE-----\n...",
+ * ],
+ * "revoked": [
+ * {
+ * "serial": "octet string",
+ * "date": "-----BEGIN DATE-----\n",
+ * "reason": "KEY_COMPROMISED"
+ * }
+ * ],
+ * "logs": [
+ * {
+ * "user": "",
+ * "time": "ISO 8601",
+ * "action": ""
+ * }
+ * ],
+ * "serial": 1
+ * }
+ * </pre>
+ * Missing fields will be default.
+ */
+ public static CertificationAuthority decodeCA(JSONObject o) throws InvalidDBException, NoSuchAlgorithmException {
+ try {
+ Certificate caCert = o.keySet().contains("cert") ? decodeCertificate(o.getString("cert")) : null;
+ final List<Template> templates = decodeListFromObject(o, "templates", Decoder::decodeTemplate);
+ final List<Certificate> signed = decodeListFromString(o, "signed", Decoder::decodeCertificate);
+ final List<RevokedCertificate> revoked =
+ decodeListFromObject(o, "revoked", Decoder::decodeRevokedCertificate);
+ final List<AuditLogEntry> logs = decodeListFromObject(o, "logs", Decoder::decodeLog);
+ final int serial = o.keySet().contains("serial") ? o.getInt("serial") :
+ CertificationAuthority.SERIAL_DEFAULT;
+ final BigInteger n = o.keySet().contains("key")
+ ? new BigInteger(o.getJSONObject("key").getString("n"), 16) : null;
+ final BigInteger p = o.keySet().contains("key")
+ ? new BigInteger(o.getJSONObject("key").getString("p"), 16) : null;
+ final BigInteger e = o.keySet().contains("key")
+ ? new BigInteger(o.getJSONObject("key").getString("e"), 16) : null;
+ return new CertificationAuthority(n, p, e, caCert, signed, serial, revoked, templates, logs);
+ } catch (InvalidKeySpecException | NullPointerException | ParseException
+ | InvalidCAException | NumberFormatException | JSONException ex) {
+ throw new InvalidDBException("Invalid JSON format", ex);
+ }
+ }
+
+ /**
+ * EFFECTS: Encode the CertificationAuthority to JSON.
+ */
+ public static JSONObject encodeCA(CertificationAuthority ca) {
+ JSONObject obj = new JSONObject();
+ if (ca.getKey() != null) {
+ obj.put("key", encodeKey(ca.getKey(), ca.getPublicKey()));
+ }
+ if (ca.getCertificate() != null) {
+ obj.put("cert", encodeCertificate(ca.getCertificate()));
+ }
+ if (ca.getSerial() != CertificationAuthority.SERIAL_DEFAULT) {
+ obj.put("serial", ca.getSerial());
+ }
+ encodeList(obj, "templates", ca.getTemplates(), Decoder::encodeTemplate);
+ encodeList(obj, "signed", ca.getSigned(), Decoder::encodeCertificate);
+ encodeList(obj, "revoked", ca.getRevoked(), Decoder::encodeRevokedCertificate);
+ encodeList(obj, "logs", ca.getLogs(), Decoder::encodeLog);
+ return obj;
+ }
+
+ /**
+ * EFFECTS: Encode the n / p / e modulus and exponents into JSON.
+ */
+ private static JSONObject encodeKey(RSAPrivateKey privateKey, RSAPublicKey publicKey) {
+ JSONObject obj = new JSONObject();
+ obj.put("n", privateKey.getModulus().toString(16));
+ obj.put("p", privateKey.getPrivateExponent().toString(16));
+ obj.put("e", publicKey.getPublicExponent().toString(16));
+ return obj;
+ }
+
+ /**
+ * EFFECTS: Encode the log entry into a JSON object.
+ */
+ private static JSONObject encodeLog(AuditLogEntry entry) {
+ JSONObject o = new JSONObject();
+ o.put("user", entry.getUser());
+ o.put("time", entry.getTime().format(DateTimeFormatter.ISO_ZONED_DATE_TIME));
+ o.put("action", entry.getAction());
+ return o;
+ }
+
+ /**
+ * EFFECTS: Decode the log entry from JSON object.
+ * Throws {@link InvalidDBException} if the input is invalid.
+ * REQUIRES: { "user": "", "time": "ISO_LOCAL_DATE_TIME", "action": "" }
+ */
+ private static AuditLogEntry decodeLog(JSONObject o) throws InvalidDBException {
+ try {
+ return new AuditLogEntry(o.getString("user"),
+ ZonedDateTime.parse(o.getString("time"), DateTimeFormatter.ISO_ZONED_DATE_TIME),
+ o.getString("action"));
+ } catch (DateTimeParseException e) {
+ throw new InvalidDBException("Invalid date", e);
+ }
+ }
+
+ /**
+ * EFFECTS: Encode the templates into a JSON array.
+ */
+ private static JSONObject encodeTemplate(Template template) {
+ JSONObject o = new JSONObject();
+ o.put("name", template.getName());
+ o.put("enabled", template.isEnabled());
+ if (template.getSubject() != null) {
+ o.put("subject", Utils.toPEM(template.getSubject().encodeDER(), "DISTINGUISHED NAME"));
+ }
+ o.put("validity", template.getValidity());
+ return o;
+ }
+
+ /**
+ * EFFECTS: Decode the JSON object into a Template.
+ * Throws {@link InvalidDBException} if the subject is invalid.
+ * REQUIRES: { "name": "string", "enabled": boolean, "name": "-----BEGIN DISTINGUISHED NAME-----", "validity": long}
+ */
+ private static Template decodeTemplate(JSONObject o) throws InvalidDBException {
+ try {
+ return new Template(o.getString("name"),
+ o.getBoolean("enabled"),
+ o.keySet().contains("subject") ? new Name(new BytesReader(Utils.parsePEM(
+ Utils.byteToByte(o.getString("subject").getBytes()), "DISTINGUISHED NAME")),
+ false) : null,
+ o.getLong("validity"));
+ } catch (ParseException e) {
+ throw new InvalidDBException("Invalid template", e);
+ }
+ }
+
+ /**
+ * EFFECTS: Encode the certificate to PEM.
+ */
+ private static String encodeCertificate(Certificate cert) {
+ return Utils.toPEM(cert.encodeDER(), "CERTIFICATE");
+ }
+
+ /**
+ * EFFECTS: Decode the JSON pem into Certificate.
+ * Throws {@link InvalidDBException} if the value cannot be parsed.
+ * REQUIRES: -----BEGIN CERTIFICATE----- ...
+ */
+ private static Certificate decodeCertificate(String pem) {
+ try {
+ return new Certificate(new BytesReader(
+ Utils.parsePEM(Utils.byteToByte(pem.getBytes(StandardCharsets.UTF_8)), "CERTIFICATE")),
+ false);
+ } catch (ParseException e) {
+ throw new InvalidDBException("Invalid certificate", e);
+ }
+ }
+
+ /**
+ * EFFECTS: Encode the RevokedCertificate into a JSON object.
+ */
+ private static JSONObject encodeRevokedCertificate(RevokedCertificate rev) {
+ JSONObject o = new JSONObject();
+ o.put("serial", rev.getSerialNumber().getValue().toString(16));
+ o.put("date", Utils.toPEM(rev.getRevocationDate().encodeDER(), "DATE"));
+ o.put("reason", rev.getReason().name());
+ return o;
+ }
+
+ /**
+ * EFFECTS: Decode the RevokedCertificate from JSON.
+ * Throws {@link InvalidDBException} if the value cannot be parsed.
+ * REQUIRES: { "serial": "12ab", "date": "-----BEGIN DATE-----", "reason": "KEY_COMPROMISED" }
+ */
+ private static RevokedCertificate decodeRevokedCertificate(JSONObject o) {
+ try {
+ return new RevokedCertificate(ASN1Object.TAG_SEQUENCE, null,
+ new Int(Int.TAG, null, new BigInteger(o.getString("serial"), 16)),
+ (ASN1Time) ASN1Object.parse(new BytesReader(Utils.parsePEM(Utils.byteToByte(o.getString("date")
+ .getBytes(StandardCharsets.UTF_8)), "DATE")), false),
+ Reason.valueOf(o.getString("reason")));
+ } catch (ParseException | IllegalArgumentException e) {
+ throw new InvalidDBException("Invalid revoked certificate", e);
+ }
+ }
+}
diff --git a/src/main/persistence/FS.java b/src/main/persistence/FS.java
new file mode 100644
index 0000000..3b5222d
--- /dev/null
+++ b/src/main/persistence/FS.java
@@ -0,0 +1,46 @@
+package persistence;
+
+import model.asn1.exceptions.InvalidDBException;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.json.JSONTokener;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+
+/**
+ * Util class for file-system IO.
+ */
+public class FS {
+ /**
+ * EFFECTS: Read and decode the content of given path as UTF-8 into JSON.
+ * Throws {@link InvalidDBException} if IO error occurs or the input cannot be parsed.
+ */
+ public static JSONObject read(Path path) throws InvalidDBException {
+ try {
+ final InputStream fd = Files.newInputStream(path, StandardOpenOption.READ);
+ final JSONObject obj = new JSONObject(new JSONTokener(fd));
+ fd.close();
+ return obj;
+ } catch (IOException | JSONException e) {
+ throw new InvalidDBException("Cannot read or parse the file", e);
+ }
+ }
+
+ /**
+ * EFFECTS: Write the UTF-8 encoded JSON to the given path.
+ * open(2) parameters: <pre>O_WRONLY | O_TRUNC | O_CREAT</pre>
+ * Throws {@link IOException} if the file cannot be opened or written.
+ */
+ public static void write(Path path, JSONObject obj) throws IOException {
+ final OutputStream fd = Files.newOutputStream(path,
+ StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE);
+ fd.write(obj.toString().getBytes(StandardCharsets.UTF_8));
+ fd.close();
+ }
+}
diff --git a/src/main/ui/IssueScreen.java b/src/main/ui/IssueScreen.java
index 5e3ad50..3e70a0a 100644
--- a/src/main/ui/IssueScreen.java
+++ b/src/main/ui/IssueScreen.java
@@ -4,6 +4,7 @@ import model.asn1.exceptions.ParseException;
import model.ca.Template;
import model.csr.CertificationRequest;
import model.pki.cert.Certificate;
+import model.x501.Name;
/**
* The screen that accepts a CSR and template and allows user to change its properties and issue.
@@ -65,6 +66,7 @@ public class IssueScreen implements UIHandler {
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) {
@@ -78,7 +80,11 @@ public class IssueScreen implements UIHandler {
*/
private void handleIssueSetSubject(String val) {
try {
- template = new Template(template.getName(), template.isEnabled(), val, template.getValidity());
+ 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());
}
diff --git a/src/main/ui/JCA.java b/src/main/ui/JCA.java
index 882c546..420ec10 100644
--- a/src/main/ui/JCA.java
+++ b/src/main/ui/JCA.java
@@ -1,9 +1,14 @@
package ui;
+import model.asn1.exceptions.InvalidDBException;
import model.asn1.exceptions.ParseException;
import model.ca.CertificationAuthority;
+import persistence.Decoder;
+import persistence.FS;
+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;
@@ -13,6 +18,11 @@ import java.util.Scanner;
*/
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;
@@ -23,13 +33,18 @@ public class JCA {
/**
* The CA
*/
- private final CertificationAuthority 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.
*/
@@ -106,34 +121,72 @@ public class JCA {
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();
+ }
+ }
+ }
+
private void handleLine(String... args) {
if (args[0].equals("log")) {
ca.getLogs().forEach(System.out::println);
return;
}
- switch (args[0]) {
- case "help":
- screen.help();
- System.out.println("log\tView audit logs");
- break;
- case "show":
- screen.show();
- break;
- case "commit":
- screen.commit();
- break;
- case "exit":
- setScreen(screen.exit());
- break;
- default:
- screen.command(args);
- break;
+ 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@JCA %s ", ca.getUser(), screen.getPS1());
+ 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());
+ }
}
/**
@@ -152,6 +205,10 @@ public class JCA {
}
}
+ public void setUnsaved(boolean unsaved) {
+ this.unsaved = unsaved;
+ }
+
public CertificationAuthority getCa() {
return ca;
}
diff --git a/src/main/ui/MainScreen.java b/src/main/ui/MainScreen.java
index 2eaf882..8a85881 100644
--- a/src/main/ui/MainScreen.java
+++ b/src/main/ui/MainScreen.java
@@ -7,6 +7,7 @@ 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;
@@ -138,6 +139,7 @@ public class MainScreen implements UIHandler {
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");
}
@@ -177,7 +179,9 @@ public class MainScreen implements UIHandler {
return;
}
try {
- System.out.println(Utils.toPEM(session.getCa().signCRL().encodeDER(), "X509 CRL"));
+ CertificateList crl = session.getCa().signCRL();
+ session.save();
+ System.out.println(Utils.toPEM(crl.encodeDER(), "X509 CRL"));
} catch (Throwable e) {
System.out.println(e.getMessage());
}
diff --git a/src/main/ui/MgmtScreen.java b/src/main/ui/MgmtScreen.java
index 0a25bfe..c630a34 100644
--- a/src/main/ui/MgmtScreen.java
+++ b/src/main/ui/MgmtScreen.java
@@ -73,6 +73,7 @@ public class MgmtScreen implements UIHandler {
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());
}
@@ -90,6 +91,7 @@ public class MgmtScreen implements UIHandler {
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());
}
@@ -105,6 +107,7 @@ public class MgmtScreen implements UIHandler {
}
try {
session.getCa().generateKey();
+ session.setUnsaved(true);
} catch (NoSuchAlgorithmException e) {
System.out.println(e.getMessage());
}
diff --git a/src/main/ui/TemplateSetScreen.java b/src/main/ui/TemplateSetScreen.java
index a0b39c1..30d25b9 100644
--- a/src/main/ui/TemplateSetScreen.java
+++ b/src/main/ui/TemplateSetScreen.java
@@ -2,6 +2,7 @@ package ui;
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.
@@ -36,7 +37,11 @@ public class TemplateSetScreen implements UIHandler {
*/
private void handleSetSubject(String val) {
try {
- template = new Template(template.getName(), template.isEnabled(), val, template.getValidity());
+ 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());
}
@@ -94,6 +99,7 @@ public class TemplateSetScreen implements UIHandler {
@Override
public void commit() {
session.getCa().addTemplate(template);
+ session.setUnsaved(true);
session.setScreen(Screen.TEMPLATES);
}
diff --git a/src/main/ui/TemplatesScreen.java b/src/main/ui/TemplatesScreen.java
index e08df50..e622709 100644
--- a/src/main/ui/TemplatesScreen.java
+++ b/src/main/ui/TemplatesScreen.java
@@ -75,6 +75,7 @@ public class TemplatesScreen implements UIHandler {
return;
}
session.getCa().setTemplateEnable(tmp, enable);
+ session.setUnsaved(true);
}
/**
@@ -92,6 +93,7 @@ public class TemplatesScreen implements UIHandler {
return;
}
session.getCa().removeTemplate(tmp);
+ session.setUnsaved(true);
}
/**
diff --git a/src/main/ui/Utils.java b/src/main/ui/Utils.java
index f653ffa..4a9beeb 100644
--- a/src/main/ui/Utils.java
+++ b/src/main/ui/Utils.java
@@ -80,7 +80,11 @@ public final class Utils {
throw new ParseException("Not a valid PEM");
}
final String b64 = matcher.group(1).replace("\n", "");
- return byteToByte(Base64.getDecoder().decode(b64));
+ try {
+ return byteToByte(Base64.getDecoder().decode(b64));
+ } catch (IllegalArgumentException e) {
+ throw new ParseException(e.getMessage());
+ }
}
/**
diff --git a/src/test/persistence/DecoderTest.java b/src/test/persistence/DecoderTest.java
new file mode 100644
index 0000000..96f94e7
--- /dev/null
+++ b/src/test/persistence/DecoderTest.java
@@ -0,0 +1,81 @@
+package persistence;
+
+import model.asn1.exceptions.InvalidDBException;
+import model.ca.CertificationAuthority;
+import org.json.JSONObject;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.nio.file.Path;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class DecoderTest {
+ private JSONObject invalidKey1;
+ private JSONObject invalidKey2;
+ private JSONObject invalidCert;
+ private JSONObject invalidTemplate1;
+ private JSONObject invalidTemplate2;
+ private JSONObject invalidSigned;
+ private JSONObject invalidRevoked1;
+ private JSONObject invalidRevoked2;
+ private JSONObject invalidLog;
+
+ private JSONObject validMinimal;
+ private JSONObject validFull;
+
+ @BeforeEach
+ void setup() {
+ invalidKey1 = FS.read(Path.of("data", "invalid_key_1.json"));
+ invalidKey2 = FS.read(Path.of("data", "invalid_key_2.json"));
+ invalidCert = FS.read(Path.of("data", "invalid_cert.json"));
+ invalidTemplate1 = FS.read(Path.of("data", "invalid_template_1.json"));
+ invalidTemplate2 = FS.read(Path.of("data", "invalid_template_2.json"));
+ invalidSigned = FS.read(Path.of("data", "invalid_signed.json"));
+ invalidRevoked1 = FS.read(Path.of("data", "invalid_revoked_1.json"));
+ invalidRevoked2 = FS.read(Path.of("data", "invalid_revoked_2.json"));
+ invalidLog = FS.read(Path.of("data", "invalid_log.json"));
+
+ validMinimal = FS.read(Path.of("data", "valid_minimal.json"));
+ validFull = FS.read(Path.of("data", "valid_full.json"));
+ }
+
+ @Test
+ void testDecodeSuccessful() throws Throwable {
+ CertificationAuthority ca = Decoder.decodeCA(validMinimal);
+ assertNull(ca.getPublicKey());
+ assertEquals(CertificationAuthority.SERIAL_DEFAULT, ca.getSerial());
+ assertEquals(0, ca.getTemplates().size());
+ assertEquals(0, ca.getSigned().size());
+ assertEquals(0, ca.getLogs().size());
+ assertEquals(0, ca.getRevoked().size());
+ assertNull(ca.getCertificate());
+
+ ca = Decoder.decodeCA(validFull);
+ assertNotNull(ca.getPublicKey());
+ assertNotNull(ca.getKey());
+ assertNotNull(ca.getCertificate());
+ assertEquals(1, ca.getRevoked().size());
+ assertEquals(2, ca.getSigned().size());
+ assertEquals(7, ca.getLogs().size());
+ }
+
+ @Test
+ void testDecodeFail() throws Throwable {
+ assertThrows(InvalidDBException.class, () -> Decoder.decodeCA(invalidKey1));
+ assertThrows(InvalidDBException.class, () -> Decoder.decodeCA(invalidKey2));
+ assertThrows(InvalidDBException.class, () -> Decoder.decodeCA(invalidCert));
+ assertThrows(InvalidDBException.class, () -> Decoder.decodeCA(invalidSigned));
+ assertThrows(InvalidDBException.class, () -> Decoder.decodeCA(invalidTemplate1));
+ assertThrows(InvalidDBException.class, () -> Decoder.decodeCA(invalidTemplate2));
+ assertThrows(InvalidDBException.class, () -> Decoder.decodeCA(invalidLog));
+ assertThrows(InvalidDBException.class, () -> Decoder.decodeCA(invalidRevoked1));
+ assertThrows(InvalidDBException.class, () -> Decoder.decodeCA(invalidRevoked2));
+ }
+
+ @Test
+ void testEncode() throws Throwable {
+ assertTrue(validFull.similar(Decoder.encodeCA(Decoder.decodeCA(validFull))));
+ assertTrue(validMinimal.similar(Decoder.encodeCA(Decoder.decodeCA(validMinimal))));
+ }
+}
diff --git a/src/test/persistence/FSTest.java b/src/test/persistence/FSTest.java
new file mode 100644
index 0000000..d8b8660
--- /dev/null
+++ b/src/test/persistence/FSTest.java
@@ -0,0 +1,50 @@
+package persistence;
+
+import model.asn1.exceptions.InvalidDBException;
+import org.json.JSONObject;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class FSTest {
+ @Test
+ void testReadFail() {
+ // open(2) - EPERM
+ assertThrows(InvalidDBException.class, () -> FS.read(Path.of("/dev/mem")));
+ // open(2) - EISDIR
+ assertThrows(InvalidDBException.class, () -> FS.read(Path.of("/")));
+ // open(2) - ENOENT
+ assertThrows(InvalidDBException.class,
+ () -> FS.read(Path.of("919123082901382901", "9210388888888190231")));
+ // Cannot parse
+ assertThrows(InvalidDBException.class, () -> FS.read(Path.of("/dev/null")));
+ assertThrows(InvalidDBException.class, () -> FS.read(Path.of("README.md")));
+ }
+
+ @Test
+ void testReadSuccess() {
+ assertTrue(FS.read(Path.of("data", "valid_full.json")).keySet().contains("serial"));
+ }
+
+ @Test
+ void testWriteFail() {
+ // open(2) - EISDIR
+ assertThrows(IOException.class, () -> FS.write(Path.of("/"), new JSONObject()));
+ // open(2) - EPERM
+ assertThrows(IOException.class, () -> FS.write(Path.of("/dev/abc"), new JSONObject()));
+ // open(2) - ENOENT
+ assertThrows(IOException.class, () -> FS.write(Path.of("asdjiasoda", "jisdsaod"), new JSONObject()));
+ }
+
+ @Test
+ void testWriteSuccess() throws IOException {
+ FS.write(Path.of("data", ".tmp"), new JSONObject());
+ assertTrue(Files.exists(Path.of("data")));
+ Files.delete(Path.of("data", ".tmp"));
+ }
+}
diff --git a/src/test/ui/UtilsTest.java b/src/test/ui/UtilsTest.java
index 3a6c885..23d2b3e 100644
--- a/src/test/ui/UtilsTest.java
+++ b/src/test/ui/UtilsTest.java
@@ -50,6 +50,12 @@ public class UtilsTest {
"LALA");
});
assertThrows(ParseException.class, () -> {
+ Utils.parsePEM(Utils.byteToByte(("-----BEGIN BLABLA-----\n"
+ + "91023921083910298*@34908302130890123890\n"
+ + "-----END BLABLA-----").getBytes(StandardCharsets.UTF_8)),
+ "BLABLA");
+ });
+ assertThrows(ParseException.class, () -> {
Utils.parsePEM(Utils.byteToByte(("-----BEGIN BLABLA-----"
+ "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB"
+ "-----END BLABLA-----").getBytes(StandardCharsets.UTF_8)),