diff options
Diffstat (limited to 'src/main/persistence')
-rw-r--r-- | src/main/persistence/Decoder.java | 284 | ||||
-rw-r--r-- | src/main/persistence/FS.java | 46 |
2 files changed, 330 insertions, 0 deletions
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(); + } +} |