aboutsummaryrefslogtreecommitdiff
path: root/src/main/model
diff options
context:
space:
mode:
authorYuuta Liang <yuutaw@student.cs.ubc.ca>2023-11-23 08:09:01 +0800
committerYuuta Liang <yuutaw@student.cs.ubc.ca>2023-11-23 08:09:01 +0800
commit65ea6c17a0c1348aa9ef4e158102ddf173936882 (patch)
tree7615366f76b6c94f46d8039aa20091f9ccd5609a /src/main/model
parentb94b18c133f06cb176d8aa8bb40a8e24918d9ed6 (diff)
downloadjca-65ea6c17a0c1348aa9ef4e158102ddf173936882.tar
jca-65ea6c17a0c1348aa9ef4e158102ddf173936882.tar.gz
jca-65ea6c17a0c1348aa9ef4e158102ddf173936882.tar.bz2
jca-65ea6c17a0c1348aa9ef4e158102ddf173936882.zip
Add GUI
Signed-off-by: Yuuta Liang <yuutaw@student.cs.ubc.ca>
Diffstat (limited to 'src/main/model')
-rw-r--r--src/main/model/GroupObserver.java48
-rw-r--r--src/main/model/ObservedData.java47
-rw-r--r--src/main/model/Observer.java33
-rw-r--r--src/main/model/asn1/ObjectIdentifier.java16
-rw-r--r--src/main/model/ca/CertificationAuthority.java94
-rw-r--r--src/main/model/ca/Template.java2
-rw-r--r--src/main/model/x501/AttributeTypeAndValue.java9
-rw-r--r--src/main/model/x501/Name.java107
8 files changed, 325 insertions, 31 deletions
diff --git a/src/main/model/GroupObserver.java b/src/main/model/GroupObserver.java
new file mode 100644
index 0000000..41a9aac
--- /dev/null
+++ b/src/main/model/GroupObserver.java
@@ -0,0 +1,48 @@
+package model;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A group of observers with different types registered.
+ */
+public class GroupObserver implements Observer {
+ /**
+ * The map. Because Java doesn't have dependent maps, they are left here as unchecked.
+ */
+ private final Map<Class, Observer> map = new HashMap<>();
+
+ /**
+ * EFFECTS: Register a data type with an observer. Override existing registrations with the same type.
+ * REQUIRES: cls != null, obs != null.
+ */
+ public <T> void register(Class<T> cls, Observer<T> obs) {
+ map.put(cls, obs);
+ }
+
+ /**
+ * EFFECTS: Notify the registered observer based on the type of 'o' with given arguments.
+ * Does nothing if no registration found. Subclasses are supported as long as a parent class observer is registered.
+ * Specific type match takes priority. Non-specific type match is not guaranteed to be the closest.
+ * REQUIRES: o != null, direction be DIRECTION_*, index >= 0 or == INDEX_NOT_IN_LIST.
+ */
+ @Override
+ public void accept(Object o, int direction, int i) {
+ Observer obs = map.get(o.getClass());
+ if (obs == null) {
+ Class supertype = map.keySet().stream().filter(clz -> clz.isInstance(o)).findFirst().orElse(null);
+ if (supertype == null) {
+ return;
+ }
+ obs = map.get(supertype);
+ }
+ obs.accept(o, direction, i);
+ }
+
+ /**
+ * EFFECTS: Count the observers.
+ */
+ public int getRegisteredObserverCount() {
+ return map.size();
+ }
+}
diff --git a/src/main/model/ObservedData.java b/src/main/model/ObservedData.java
new file mode 100644
index 0000000..5ad91a3
--- /dev/null
+++ b/src/main/model/ObservedData.java
@@ -0,0 +1,47 @@
+package model;
+
+/**
+ * A single observed data that notifies the observer once changed.
+ */
+public class ObservedData<T> {
+ /**
+ * The data.
+ */
+ private T data;
+
+ /**
+ * The observer.
+ */
+ private Observer<T> acceptor;
+
+ /**
+ * EFFECTS: Init with the given initial value and observer.
+ * REQUIRES: acceptor != null
+ */
+ public ObservedData(T initialValue, Observer<T> acceptor) {
+ this.data = initialValue;
+ this.acceptor = acceptor;
+ }
+
+ /**
+ * EFFECTS: Set the data and notify the observer with the new data and DIRECTION_CHANGE + INDEX_NOT_IN_LIST.
+ */
+ public void set(T data) {
+ this.data = data;
+ acceptor.accept(data, Observer.DIRECTION_CHANGE, Observer.INDEX_NOT_IN_LIST);
+ }
+
+ /**
+ * EFFECTS: Get the data.
+ */
+ public T get() {
+ return data;
+ }
+
+ /**
+ * EFFECTS: Get the observer.
+ */
+ public Observer<T> getAcceptor() {
+ return acceptor;
+ }
+}
diff --git a/src/main/model/Observer.java b/src/main/model/Observer.java
new file mode 100644
index 0000000..df9cbc2
--- /dev/null
+++ b/src/main/model/Observer.java
@@ -0,0 +1,33 @@
+package model;
+
+/**
+ * Data observers for GUI data binding.
+ */
+@FunctionalInterface
+public interface Observer<T> {
+ /**
+ * The data is just added to a list. Index will be its index.
+ */
+ int DIRECTION_ADD = 0;
+
+ /**
+ * The data is just removed from a list. Index will be its previous index.
+ */
+ int DIRECTION_REMOVE = 1;
+
+ /**
+ * The data is modified, either or not in a list. Index will be either INDEX_NOT_IN_LIST or its index.
+ */
+ int DIRECTION_CHANGE = 2;
+
+ /**
+ * A special index representing that the data is not in a list.
+ */
+ int INDEX_NOT_IN_LIST = -1;
+
+ /**
+ * EFFECTS: Handle data change.
+ * REQUIRES: data != null, direction be DIRECTION_*, index >= 0 or == INDEX_NOT_IN_LIST.
+ */
+ void accept(T data, int direction, int index);
+}
diff --git a/src/main/model/asn1/ObjectIdentifier.java b/src/main/model/asn1/ObjectIdentifier.java
index f6e850a..64ade8e 100644
--- a/src/main/model/asn1/ObjectIdentifier.java
+++ b/src/main/model/asn1/ObjectIdentifier.java
@@ -105,6 +105,22 @@ public class ObjectIdentifier extends ASN1Object {
}
/**
+ * EFFECTS: Get OID from a known part, case insensitive. Currently known: C CN OU O L DC.
+ * Throws {@link ParseException} if the name is unsupported.
+ */
+ public static Integer[] getKnown(String name) throws ParseException {
+ switch (name.toUpperCase()) {
+ case "C": return OID_C;
+ case "CN": return OID_CN;
+ case "OU": return OID_OU;
+ case "O": return OID_O;
+ case "L": return OID_L;
+ case "DC": return OID_DC;
+ default: throw new ParseException("Unsupported DN part: " + name);
+ }
+ }
+
+ /**
* EFFECTS: Parse input OID bytes.
* REQUIRES: raw.length >= 1
*/
diff --git a/src/main/model/ca/CertificationAuthority.java b/src/main/model/ca/CertificationAuthority.java
index 038d209..5181f1a 100644
--- a/src/main/model/ca/CertificationAuthority.java
+++ b/src/main/model/ca/CertificationAuthority.java
@@ -1,5 +1,6 @@
package model.ca;
+import model.Observer;
import model.asn1.*;
import model.asn1.exceptions.InvalidCAException;
import model.asn1.exceptions.ParseException;
@@ -28,11 +29,12 @@ import java.security.spec.RSAPrivateKeySpec;
import java.security.spec.RSAPublicKeySpec;
import java.time.ZoneId;
import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Stream;
/**
- * Holds a CA private key, its certificate, signed / revoked list, template list, and logs list.
+ * Holds a CA private key, its certificate, signed / revoked list, template list, and logs list. Data can be observed.
*/
public class CertificationAuthority {
public static final int SERIAL_DEFAULT = 1;
@@ -83,7 +85,12 @@ public class CertificationAuthority {
private final String user;
/**
- * EFFECT: Init with the given parameters and user "yuuta".
+ * Data observers.
+ */
+ private final List<Observer> observers;
+
+ /**
+ * EFFECT: Init with the given parameters, user "yuuta", and no observers.
* 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.
@@ -111,11 +118,12 @@ public class CertificationAuthority {
this.templates = new ArrayList<>(templates);
this.logs = new ArrayList<>(logs);
this.user = "yuuta";
+ this.observers = new ArrayList<>();
}
/**
* EFFECT: Init with a null key and null certificate, empty signed, revoked template, and log list,
- * serial at SERIAL_DEFAULT, and user "yuuta".
+ * serial at SERIAL_DEFAULT, user "yuuta", and no observers.
*/
public CertificationAuthority() {
this.key = null;
@@ -127,10 +135,13 @@ public class CertificationAuthority {
this.templates = new ArrayList<>();
this.logs = new ArrayList<>();
this.user = "yuuta";
+ this.observers = new ArrayList<>();
}
/**
* EFFECTS: Generate a new RSA2048 private key. This action will be logged.
+ * Observers will be notified for (RSAPublicKey.class, DIRECTION_CHANGE, INDEX_NOT_IN_LIST).
+ * Observers will be notified for (AuditLogEntry.class, DIRECTION_ADD, i).
* REQUIRES: getPublicKey() is null (i.e., no private key had been installed)
* MODIFIES: this
*/
@@ -140,6 +151,7 @@ public class CertificationAuthority {
final KeyPair pair = gen.generateKeyPair();
this.key = (RSAPrivateKey) pair.getPrivate();
this.publicKey = (RSAPublicKey) pair.getPublic();
+ notif(getPublicKey(), Observer.DIRECTION_CHANGE, Observer.INDEX_NOT_IN_LIST);
log("Generated CA private key.");
}
@@ -159,6 +171,8 @@ public class CertificationAuthority {
/**
* EFFECTS: Load the RSA private and public exponents. This action will be logged.
+ * Observers will be notified for (RSAPublicKey.class, DIRECTION_CHANGE, INDEX_NOT_IN_LIST).
+ * Observers will be notified for (AuditLogEntry.class, DIRECTION_ADD, i).
* 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)
@@ -167,6 +181,7 @@ public class CertificationAuthority {
public void loadKey(BigInteger n, BigInteger p, BigInteger e)
throws NoSuchAlgorithmException, InvalidKeySpecException {
setKey(n, p, e);
+ notif(getPublicKey(), Observer.DIRECTION_CHANGE, Observer.INDEX_NOT_IN_LIST);
log("Installed CA private key.");
}
@@ -252,6 +267,8 @@ public class CertificationAuthority {
* - 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
+ * Observers will be notified for (Certificate.class, DIRECTION_CHANGE, INDEX_NOT_IN_LIST).
+ * Observers will be notified for (AuditLogEntry.class, DIRECTION_ADD, i).
* Throws {@link ParseException} if the cert has invalid extension values.
* This action will be logged.
* REQUIRES:
@@ -261,6 +278,7 @@ public class CertificationAuthority {
public void installCertificate(Certificate certificate) throws InvalidCAException, ParseException {
validateCertificate(certificate);
this.certificate = certificate;
+ notif(certificate, Observer.DIRECTION_CHANGE, Observer.INDEX_NOT_IN_LIST);
log("CA certificate is installed.");
}
@@ -325,6 +343,7 @@ public class CertificationAuthority {
/**
* EFFECT: Generate CSR and sign it, so the CA can request itself a certificate.
+ * Observers will be notified for (AuditLogEntry.class, DIRECTION_ADD, i).
* REQUIRES: The CA cert must not be installed.
* MODIFIES: this (This action will be logged)
*/
@@ -351,6 +370,8 @@ public class CertificationAuthority {
/**
* EFFECTS: Sign the CSR based on the template.
+ * Observers will be notified for (Certificate.class, DIRECTION_ADD, i).
+ * Observers will be notified for (AuditLogEntry.class, DIRECTION_ADD, i).
* REQUIRES: The CA cert must be installed first, req must have a subject, template must be enabled.
* MODIFIES: this
*/
@@ -363,7 +384,8 @@ public class CertificationAuthority {
new BitString(BitString.TAG, null, 0,
signBytes(newCert.encodeValueDER())));
this.signed.add(cert);
- log("Signed a cert with serial number " + cert.getCertificate().getSerialNumber());
+ notif(cert, Observer.DIRECTION_ADD, this.signed.size() - 1);
+ log("Signed a cert with serial number " + cert.getCertificate().getSerialNumber().getLong());
return cert;
}
@@ -406,17 +428,22 @@ public class CertificationAuthority {
/**
* EFFECTS: Add the revocation info to revoked list. This action will be logged.
+ * Observers will be notified for (RevokedCertificate.class, DIRECTION_ADD, i).
+ * Observers will be notified for (AuditLogEntry.class, DIRECTION_ADD, i).
* REQUIRES: revoked should have the serial of an issued certificate; its date should be current.
* MODIFIES: this
*/
public void revoke(RevokedCertificate rev) {
revoked.add(rev);
- log("Certificate " + rev.getSerialNumber().getLong() + " is revoked with reason " + rev.getReason());
+ notif(rev, Observer.DIRECTION_ADD, revoked.size() - 1);
+ log("Certificate " + rev.getSerialNumber().getLong() + " is revoked with reason " + rev.getReason()
+ + " at " + rev.getRevocationDate().getTimestamp().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
}
/**
* EFFECTS: Generate and sign the CRL, based on getRevokedCerts(). The CSR will have current time as thisUpdate with
* no nextUptime, and it will have issuer same as the CA's subject.
+ * Observers will be notified for (AuditLogEntry.class, DIRECTION_ADD, i).
* REQUIRES: The CA cert must be installed first.
* MODIFIES: this (This action will be logged)
*/
@@ -439,10 +466,29 @@ public class CertificationAuthority {
/**
* EFFECTS: Log the action with the current date and user.
+ * Observers will be notified for (AuditLogEntry.class, DIRECTION_ADD, i).
* MODIFIES: this
*/
private void log(String message) {
- this.logs.add(new AuditLogEntry(user, ZonedDateTime.now(), message));
+ final AuditLogEntry i = new AuditLogEntry(user, ZonedDateTime.now(), message);
+ this.logs.add(i);
+ notif(i, Observer.DIRECTION_ADD, logs.size() - 1);
+ }
+
+ /**
+ * EFFECTS: Register the given observer, so it will be called upon changes.
+ * MODIFIES: this
+ */
+ public void registerObserver(final Observer<?> observer) {
+ this.observers.add(observer);
+ }
+
+ /**
+ * EFFECTS: Notify the observers.
+ * REQUIRES: direction must be valid Observer constants, i must be either >= 0 or Observer.INDEX_NOT_IN_LIST.
+ */
+ private void notif(Object o, int direction, int i) {
+ observers.forEach(e -> e.accept(o, direction, i));
}
/**
@@ -460,33 +506,43 @@ public class CertificationAuthority {
/**
* EFFECTS: Install the new template. This action will be logged.
+ * Observers will be notified for (Template.class, DIRECTION_ADD, i).
+ * Observers will be notified for (AuditLogEntry.class, DIRECTION_ADD, i).
* REQUIRES: findTemplate(template.getName(), false) == null
* MODIFIES: this
*/
public void addTemplate(Template template) {
this.templates.add(template);
+ notif(template, Observer.DIRECTION_ADD, templates.size() - 1);
log("Added a new template: " + template.getName());
}
/**
- * EFFECTS: Set the given template to enabled / disabled. This action will be logged.
+ * EFFECTS: Set the given template to enabled / disabled, order will be kept. This action will be logged.
+ * Observers will be notified for (Template.class, DIRECTION_CHANGE, i).
+ * Observers will be notified for (AuditLogEntry.class, DIRECTION_ADD, i).
* REQUIRES: the template is valid (findTemplate does not return null)
* MODIFIES: this
*/
public void setTemplateEnable(Template template, boolean enable) {
final Template t = findTemplate(template.getName(), false);
- templates.remove(t);
- templates.add(new Template(t.getName(), enable, t.getSubject(), t.getValidity()));
+ int i = templates.indexOf(t);
+ templates.set(i, new Template(t.getName(), enable, t.getSubject(), t.getValidity()));
+ notif(template, Observer.DIRECTION_CHANGE, i);
log("Template " + template.getName() + " has been " + (enable ? "enabled" : "disabled"));
}
/**
* EFFECTS: Remove the given template. This action will be logged.
+ * Observers will be notified for (Template.class, DIRECTION_REMOVE, i).
+ * Observers will be notified for (AuditLogEntry.class, DIRECTION_ADD, i).
* REQUIRES: the template is valid (findTemplate does not return null)
* MODIFIES: this
*/
public void removeTemplate(Template template) {
+ int i = templates.indexOf(template);
templates.remove(findTemplate(template.getName(), false));
+ notif(template, Observer.DIRECTION_REMOVE, i);
log("Template " + template.getName() + " is removed");
}
@@ -496,40 +552,28 @@ public class CertificationAuthority {
return certificate;
}
- /**
- * EFFECT: Get a read-only view of the signed certificates.
- */
public List<Certificate> getSigned() {
- return List.copyOf(signed);
+ return signed;
}
- /**
- * EFFECT: Get a read-only view of the revoked certificates.
- */
public List<RevokedCertificate> getRevoked() {
- return List.copyOf(revoked);
+ return revoked;
}
public int getSerial() {
return serial;
}
- /**
- * EFFECT: Get a read-only view of the templates.
- */
public List<Template> getTemplates() {
- return List.copyOf(templates);
+ return templates;
}
public String getUser() {
return user;
}
- /**
- * EFFECT: Get a read-only view of the logs.
- */
public List<AuditLogEntry> getLogs() {
- return List.copyOf(logs);
+ return logs;
}
public RSAPublicKey getPublicKey() {
diff --git a/src/main/model/ca/Template.java b/src/main/model/ca/Template.java
index 84e639e..bc64562 100644
--- a/src/main/model/ca/Template.java
+++ b/src/main/model/ca/Template.java
@@ -41,7 +41,7 @@ public class Template {
boolean enabled,
String commonName,
long validity) throws ParseException {
- this(name, enabled, parseString(commonName), validity);
+ this(name, enabled, commonName == null ? null : parseString(commonName), validity);
}
/**
diff --git a/src/main/model/x501/AttributeTypeAndValue.java b/src/main/model/x501/AttributeTypeAndValue.java
index 54b3352..179d6ff 100644
--- a/src/main/model/x501/AttributeTypeAndValue.java
+++ b/src/main/model/x501/AttributeTypeAndValue.java
@@ -72,11 +72,16 @@ public class AttributeTypeAndValue extends ASN1Object {
/**
* EFFECTS: Return in TYPE=Value format. Type will be either x.x.x.x.x or human-readable strings like CN. Value is
- * input-defined.
+ * input-defined. ',' '+' '=' will be escaped.
*/
@Override
public String toString() {
- return type.toString() + "=" + value.toString();
+ return type.toString().replace(",", "\\,")
+ .replace("=", "\\=")
+ .replace("+", "\\+")
+ + "=" + value.toString().replace(",", "\\,")
+ .replace("=", "\\=")
+ .replace("+", "\\+");
}
public ObjectIdentifier getType() {
diff --git a/src/main/model/x501/Name.java b/src/main/model/x501/Name.java
index 19cde56..7477005 100644
--- a/src/main/model/x501/Name.java
+++ b/src/main/model/x501/Name.java
@@ -1,8 +1,6 @@
package model.x501;
-import model.asn1.ASN1Object;
-import model.asn1.Encodable;
-import model.asn1.Tag;
+import model.asn1.*;
import model.asn1.exceptions.ParseException;
import model.asn1.parsing.BytesReader;
@@ -53,6 +51,109 @@ public class Name extends ASN1Object {
}
/**
+ * EFFECTS: Parse OID after last KV and clear context if input is '='. Otherwise add to context.
+ * Throws {@link ParseException} if input is '+' or ',', or if the oid cannot be recognized.
+ * MODIFIES: context
+ */
+ private static ObjectIdentifier handleKey(char c, List<Character> context) throws ParseException {
+ if (c == '=') {
+ if (context.isEmpty()) {
+ throw new ParseException("Unterminated key");
+ }
+ final ObjectIdentifier oid = new ObjectIdentifier(ObjectIdentifier.TAG, null,
+ ObjectIdentifier.getKnown(
+ context.stream().map(Object::toString).collect(Collectors.joining(""))));
+ context.clear();
+ return oid;
+ } else if (c == '+' || c == ',') {
+ throw new ParseException("Unterminated key part: " + context);
+ } else {
+ context.add(c);
+ return null;
+ }
+ }
+
+ /**
+ * EFFECTS: Parse KV after '='. Clear context.
+ * Throws {@link ParseException} if context is empty.
+ * MODIFIES: context
+ * REQUIRES: curKey to be a valid OID
+ */
+ private static AttributeTypeAndValue flushKV(ObjectIdentifier curKey, List<Character> context)
+ throws ParseException {
+ if (context.isEmpty()) {
+ throw new ParseException("Unterminated value");
+ }
+ final AttributeTypeAndValue tv = new AttributeTypeAndValue(ASN1Object.TAG_SEQUENCE, null, curKey,
+ new PrintableString(PrintableString.TAG, null,
+ context.stream().map(Object::toString).collect(Collectors.joining(""))));
+ context.clear();
+ return tv;
+ }
+
+ /**
+ * EFFECTS: Handle value after =, optionally flush to rdns after ',', or add to curListKT if after '+'. Clears
+ * context if flushed, otherwise add to context. Returns whether switch to the state of reading key.
+ * Throws {@link ParseException} if c is '=' or context is empty.
+ * MODIFIES: context, curListKT, rdns
+ * REQUIRES: curKey to be a valid OID
+ */
+ private static boolean handleValue(char c, List<Character> context, List<AttributeTypeAndValue> curListKT,
+ ObjectIdentifier curKey, List<RelativeDistinguishedName> rdns)
+ throws ParseException {
+ if (c == ',') {
+ if (context.isEmpty()) {
+ throw new ParseException("Unterminated value");
+ }
+ curListKT.add(flushKV(curKey, context));
+ rdns.add(new RelativeDistinguishedName(ASN1Object.TAG_SET, null,
+ curListKT.toArray(AttributeTypeAndValue[]::new)));
+ curListKT.clear();
+ return true;
+ } else if (c == '+') {
+ curListKT.add(flushKV(curKey, context));
+ return true;
+ } else if (c == '=') {
+ throw new ParseException("Unterminated value part: " + context);
+ } else {
+ context.add(c);
+ return false;
+ }
+ }
+
+ /**
+ * EFFECTS: Parse the given DN string into structural X.509 RDN Sequence.
+ * Character literals = + , must be escaped.
+ * Values will always be PrintableString.
+ * Throws {@link ParseException} if invalid.
+ */
+ public static Name parseString(String dn) throws ParseException {
+ char state = 0; // 0 - Key, 1 - Value; MSB: Escaped
+ List<RelativeDistinguishedName> rdns = new ArrayList<>();
+ List<AttributeTypeAndValue> curListKT = new ArrayList<>();
+ ObjectIdentifier curKey = null;
+ List<Character> context = new ArrayList<>();
+ for (char c : (dn + ",").toCharArray()) {
+ if ((state >> 7) == 1) {
+ context.add(c);
+ state &= 127;
+ continue;
+ } else if (c == '\\') {
+ state |= 128;
+ continue;
+ }
+ if (state == 0) {
+ if ((curKey = handleKey(c, context)) != null) {
+ state = 1;
+ }
+ } else if (handleValue(c, context, curListKT, curKey, rdns)) {
+ state = 0;
+ }
+ }
+ return new Name(ASN1Object.TAG_SEQUENCE, null, rdns.toArray(RelativeDistinguishedName[]::new));
+ }
+
+ /**
* EFFECTS: Encode the SEQUENCE OF into DER, keep order. RDNs will be encoded one-by-one.
*/
@Override