diff options
Diffstat (limited to 'src/main/model')
-rw-r--r-- | src/main/model/GroupObserver.java | 48 | ||||
-rw-r--r-- | src/main/model/ObservedData.java | 47 | ||||
-rw-r--r-- | src/main/model/Observer.java | 33 | ||||
-rw-r--r-- | src/main/model/asn1/ObjectIdentifier.java | 16 | ||||
-rw-r--r-- | src/main/model/ca/CertificationAuthority.java | 94 | ||||
-rw-r--r-- | src/main/model/ca/Template.java | 2 | ||||
-rw-r--r-- | src/main/model/x501/AttributeTypeAndValue.java | 9 | ||||
-rw-r--r-- | src/main/model/x501/Name.java | 107 |
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 |