[tor-commits] [metrics-lib/release] Implements task-19607.

karsten at torproject.org karsten at torproject.org
Wed Jun 7 07:06:30 UTC 2017


commit df6bdc8d9bdd0ee0bf66b6d5587751bc3f86aa46
Author: iwakeh <iwakeh at torproject.org>
Date:   Fri May 26 08:52:00 2017 +0000

    Implements task-19607.
    Use enums for keywords as well as enum sets and maps.
    Use constants for repeated strings.
---
 CHANGELOG.md                                       |   5 +
 .../impl/BridgeExtraInfoDescriptorImpl.java        |   2 +-
 .../descriptor/impl/BridgeNetworkStatusImpl.java   |  10 +-
 .../descriptor/impl/BridgePoolAssignmentImpl.java  |  20 +-
 .../impl/BridgeServerDescriptorImpl.java           |   2 +-
 .../torproject/descriptor/impl/DescriptorImpl.java | 185 +++++++++--------
 .../descriptor/impl/DirSourceEntryImpl.java        |  82 ++++----
 .../impl/DirectoryKeyCertificateImpl.java          |  95 +++++----
 .../descriptor/impl/DirectorySignatureImpl.java    |  23 ++-
 .../descriptor/impl/ExtraInfoDescriptorImpl.java   | 218 +++++++++++----------
 .../java/org/torproject/descriptor/impl/Key.java   | 177 +++++++++++++++++
 .../descriptor/impl/MicrodescriptorImpl.java       |  56 +++---
 .../descriptor/impl/NetworkStatusEntryImpl.java    |  70 ++++---
 .../descriptor/impl/NetworkStatusImpl.java         |  29 +--
 .../descriptor/impl/RelayDirectoryImpl.java        | 137 ++++++-------
 .../impl/RelayExtraInfoDescriptorImpl.java         |   2 +-
 .../impl/RelayNetworkStatusConsensusImpl.java      |  90 ++++-----
 .../descriptor/impl/RelayNetworkStatusImpl.java    | 102 +++++-----
 .../impl/RelayNetworkStatusVoteImpl.java           | 168 ++++++++--------
 .../descriptor/impl/RelayServerDescriptorImpl.java |   2 +-
 .../descriptor/impl/ServerDescriptorImpl.java      | 192 +++++++++---------
 21 files changed, 930 insertions(+), 737 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index c76bbc1..0e39147 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,11 @@
      descriptor lines, as permitted by dir-spec.txt.
    - Streamline digest method names.
 
+ * Minor changes
+   - Turn keyword strings into enums and use the appropriate enum sets
+     and maps to avoid repeating string literals and to use more speedy
+     collection types.
+
 
 # Changes in version 1.6.0 - 2017-02-17
 
diff --git a/src/main/java/org/torproject/descriptor/impl/BridgeExtraInfoDescriptorImpl.java b/src/main/java/org/torproject/descriptor/impl/BridgeExtraInfoDescriptorImpl.java
index 080dde1..0518392 100644
--- a/src/main/java/org/torproject/descriptor/impl/BridgeExtraInfoDescriptorImpl.java
+++ b/src/main/java/org/torproject/descriptor/impl/BridgeExtraInfoDescriptorImpl.java
@@ -19,7 +19,7 @@ public class BridgeExtraInfoDescriptorImpl
     List<ExtraInfoDescriptor> parsedDescriptors = new ArrayList<>();
     List<byte[]> splitDescriptorsBytes =
         DescriptorImpl.splitRawDescriptorBytes(descriptorsBytes,
-        "extra-info ");
+        Key.EXTRA_INFO.keyword + SP);
     for (byte[] descriptorBytes : splitDescriptorsBytes) {
       ExtraInfoDescriptor parsedDescriptor =
           new BridgeExtraInfoDescriptorImpl(descriptorBytes,
diff --git a/src/main/java/org/torproject/descriptor/impl/BridgeNetworkStatusImpl.java b/src/main/java/org/torproject/descriptor/impl/BridgeNetworkStatusImpl.java
index 80aba01..88411e6 100644
--- a/src/main/java/org/torproject/descriptor/impl/BridgeNetworkStatusImpl.java
+++ b/src/main/java/org/torproject/descriptor/impl/BridgeNetworkStatusImpl.java
@@ -71,16 +71,16 @@ public class BridgeNetworkStatusImpl extends NetworkStatusImpl
     this.enoughMtbfInfo = -1;
     this.ignoringAdvertisedBws = -1;
 
-    Scanner scanner = new Scanner(new String(headerBytes)).useDelimiter("\n");
+    Scanner scanner = new Scanner(new String(headerBytes)).useDelimiter(NL);
     while (scanner.hasNext()) {
       String line = scanner.next();
       String[] parts = line.split("[ \t]+");
-      String keyword = parts[0];
-      switch (keyword) {
-        case "published":
+      Key key = Key.get(parts[0]);
+      switch (key) {
+        case PUBLISHED:
           this.parsePublishedLine(line, parts);
           break;
-        case "flag-thresholds":
+        case FLAG_THRESHOLDS:
           this.parseFlagThresholdsLine(line, parts);
           break;
         default:
diff --git a/src/main/java/org/torproject/descriptor/impl/BridgePoolAssignmentImpl.java b/src/main/java/org/torproject/descriptor/impl/BridgePoolAssignmentImpl.java
index e2579e5..609797e 100644
--- a/src/main/java/org/torproject/descriptor/impl/BridgePoolAssignmentImpl.java
+++ b/src/main/java/org/torproject/descriptor/impl/BridgePoolAssignmentImpl.java
@@ -7,11 +7,9 @@ import org.torproject.descriptor.BridgePoolAssignment;
 import org.torproject.descriptor.DescriptorParseException;
 
 import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashSet;
+import java.util.EnumSet;
 import java.util.List;
 import java.util.Scanner;
-import java.util.Set;
 import java.util.SortedMap;
 import java.util.TreeMap;
 
@@ -24,7 +22,7 @@ public class BridgePoolAssignmentImpl extends DescriptorImpl
     List<BridgePoolAssignment> parsedDescriptors = new ArrayList<>();
     List<byte[]> splitDescriptorsBytes =
         DescriptorImpl.splitRawDescriptorBytes(descriptorsBytes,
-        "bridge-pool-assignment ");
+        Key.BRIDGE_POOL_ASSIGNMENT.keyword + SP);
     for (byte[] descriptorBytes : splitDescriptorsBytes) {
       BridgePoolAssignment parsedDescriptor =
           new BridgePoolAssignmentImpl(descriptorBytes,
@@ -39,20 +37,18 @@ public class BridgePoolAssignmentImpl extends DescriptorImpl
       throws DescriptorParseException {
     super(descriptorBytes, failUnrecognizedDescriptorLines, false);
     this.parseDescriptorBytes();
-    Set<String> exactlyOnceKeywords = new HashSet<>(Arrays.asList(
-        new String[] { "bridge-pool-assignment" }));
-    this.checkExactlyOnceKeywords(exactlyOnceKeywords);
-    this.checkFirstKeyword("bridge-pool-assignment");
-    this.clearParsedKeywords();
+    this.checkExactlyOnceKeys(EnumSet.of(Key.BRIDGE_POOL_ASSIGNMENT));
+    this.checkFirstKey(Key.BRIDGE_POOL_ASSIGNMENT);
+    this.clearParsedKeys();
     return;
   }
 
   private void parseDescriptorBytes() throws DescriptorParseException {
     Scanner scanner = new Scanner(new String(this.rawDescriptorBytes))
-        .useDelimiter("\n");
+        .useDelimiter(NL);
     while (scanner.hasNext()) {
       String line = scanner.next();
-      if (line.startsWith("bridge-pool-assignment ")) {
+      if (line.startsWith(Key.BRIDGE_POOL_ASSIGNMENT.keyword + SP)) {
         this.parseBridgePoolAssignmentLine(line);
       } else {
         this.parseBridgeLine(line);
@@ -80,7 +76,7 @@ public class BridgePoolAssignmentImpl extends DescriptorImpl
     }
     String fingerprint = ParseHelper.parseTwentyByteHexString(line,
         parts[0]);
-    String poolAndDetails = line.substring(line.indexOf(" ") + 1);
+    String poolAndDetails = line.substring(line.indexOf(SP) + 1);
     this.entries.put(fingerprint, poolAndDetails);
   }
 
diff --git a/src/main/java/org/torproject/descriptor/impl/BridgeServerDescriptorImpl.java b/src/main/java/org/torproject/descriptor/impl/BridgeServerDescriptorImpl.java
index 43548fc..900f6cd 100644
--- a/src/main/java/org/torproject/descriptor/impl/BridgeServerDescriptorImpl.java
+++ b/src/main/java/org/torproject/descriptor/impl/BridgeServerDescriptorImpl.java
@@ -19,7 +19,7 @@ public class BridgeServerDescriptorImpl extends ServerDescriptorImpl
     List<ServerDescriptor> parsedDescriptors = new ArrayList<>();
     List<byte[]> splitDescriptorsBytes =
         DescriptorImpl.splitRawDescriptorBytes(descriptorsBytes,
-        "router ");
+        Key.ROUTER.keyword + SP);
     for (byte[] descriptorBytes : splitDescriptorsBytes) {
       ServerDescriptor parsedDescriptor =
           new BridgeServerDescriptorImpl(descriptorBytes,
diff --git a/src/main/java/org/torproject/descriptor/impl/DescriptorImpl.java b/src/main/java/org/torproject/descriptor/impl/DescriptorImpl.java
index 49f4a2e..f963fef 100644
--- a/src/main/java/org/torproject/descriptor/impl/DescriptorImpl.java
+++ b/src/main/java/org/torproject/descriptor/impl/DescriptorImpl.java
@@ -8,7 +8,7 @@ import org.torproject.descriptor.DescriptorParseException;
 
 import java.io.UnsupportedEncodingException;
 import java.util.ArrayList;
-import java.util.HashMap;
+import java.util.EnumMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Scanner;
@@ -16,6 +16,10 @@ import java.util.Set;
 
 public abstract class DescriptorImpl implements Descriptor {
 
+  public static final String NL = "\n";
+
+  public static final String SP = " ";
+
   protected static List<Descriptor> parseDescriptors(
       byte[] rawDescriptorBytes, String fileName,
       boolean failUnrecognizedDescriptorLines)
@@ -30,33 +34,38 @@ public abstract class DescriptorImpl implements Descriptor {
         first100Chars.length);
     String firstLines = new String(first100Chars);
     if (firstLines.startsWith("@type network-status-consensus-3 1.")
-        || firstLines.startsWith("@type network-status-microdesc-"
-            + "consensus-3 1.")
-        || ((firstLines.startsWith("network-status-version 3")
-        || firstLines.contains("\nnetwork-status-version 3"))
-        && firstLines.contains("\nvote-status consensus\n"))) {
+        || firstLines.startsWith(
+            "@type network-status-microdesc-consensus-3 1.")
+        || ((firstLines.startsWith(
+            Key.NETWORK_STATUS_VERSION.keyword + SP + "3")
+        || firstLines.contains(
+            NL + Key.NETWORK_STATUS_VERSION.keyword + SP + "3"))
+        && firstLines.contains(
+            NL + Key.VOTE_STATUS.keyword + SP + "consensus" + NL))) {
       parsedDescriptors.addAll(RelayNetworkStatusConsensusImpl
           .parseConsensuses(rawDescriptorBytes,
           failUnrecognizedDescriptorLines));
     } else if (firstLines.startsWith("@type network-status-vote-3 1.")
-        || ((firstLines.startsWith("network-status-version 3\n")
-        || firstLines.contains("\nnetwork-status-version 3\n"))
-        && firstLines.contains("\nvote-status vote\n"))) {
+        || ((firstLines.startsWith(
+            Key.NETWORK_STATUS_VERSION.keyword + SP + "3" + NL)
+        || firstLines.contains(
+            NL + Key.NETWORK_STATUS_VERSION.keyword + SP + "3" + NL))
+        && firstLines.contains(
+            NL + Key.VOTE_STATUS.keyword + SP + "vote" + NL))) {
       parsedDescriptors.addAll(RelayNetworkStatusVoteImpl
           .parseVotes(rawDescriptorBytes,
           failUnrecognizedDescriptorLines));
     } else if (firstLines.startsWith("@type bridge-network-status 1.")
-        || firstLines.startsWith("r ")) {
+        || firstLines.startsWith(Key.R.keyword + SP)) {
       parsedDescriptors.add(new BridgeNetworkStatusImpl(
           rawDescriptorBytes, fileName, failUnrecognizedDescriptorLines));
-    } else if (firstLines.startsWith(
-        "@type bridge-server-descriptor 1.")) {
+    } else if (firstLines.startsWith("@type bridge-server-descriptor 1.")) {
       parsedDescriptors.addAll(BridgeServerDescriptorImpl
           .parseDescriptors(rawDescriptorBytes,
           failUnrecognizedDescriptorLines));
     } else if (firstLines.startsWith("@type server-descriptor 1.")
-        || firstLines.startsWith("router ")
-        || firstLines.contains("\nrouter ")) {
+        || firstLines.startsWith(Key.ROUTER.keyword + SP)
+        || firstLines.contains(NL + Key.ROUTER.keyword + SP)) {
       parsedDescriptors.addAll(RelayServerDescriptorImpl
           .parseDescriptors(rawDescriptorBytes,
           failUnrecognizedDescriptorLines));
@@ -65,42 +74,45 @@ public abstract class DescriptorImpl implements Descriptor {
           .parseDescriptors(rawDescriptorBytes,
           failUnrecognizedDescriptorLines));
     } else if (firstLines.startsWith("@type extra-info 1.")
-        || firstLines.startsWith("extra-info ")
-        || firstLines.contains("\nextra-info ")) {
+        || firstLines.startsWith(Key.EXTRA_INFO.keyword + SP)
+        || firstLines.contains(NL + Key.EXTRA_INFO.keyword + SP)) {
       parsedDescriptors.addAll(RelayExtraInfoDescriptorImpl
           .parseDescriptors(rawDescriptorBytes,
           failUnrecognizedDescriptorLines));
     } else if (firstLines.startsWith("@type microdescriptor 1.")
-        || firstLines.startsWith("onion-key\n")
-        || firstLines.contains("\nonion-key\n")) {
+        || firstLines.startsWith(Key.ONION_KEY.keyword + NL)
+        || firstLines.contains(NL + Key.ONION_KEY.keyword + NL)) {
       parsedDescriptors.addAll(MicrodescriptorImpl
           .parseDescriptors(rawDescriptorBytes,
           failUnrecognizedDescriptorLines));
     } else if (firstLines.startsWith("@type bridge-pool-assignment 1.")
-        || firstLines.startsWith("bridge-pool-assignment ")
-        || firstLines.contains("\nbridge-pool-assignment ")) {
+        || firstLines.startsWith(Key.BRIDGE_POOL_ASSIGNMENT.keyword + SP)
+        || firstLines.contains(NL + Key.BRIDGE_POOL_ASSIGNMENT.keyword + SP)) {
       parsedDescriptors.addAll(BridgePoolAssignmentImpl
           .parseDescriptors(rawDescriptorBytes,
           failUnrecognizedDescriptorLines));
     } else if (firstLines.startsWith("@type dir-key-certificate-3 1.")
-        || firstLines.startsWith("dir-key-certificate-version ")
-        || firstLines.contains("\ndir-key-certificate-version ")) {
+        || firstLines.startsWith(Key.DIR_KEY_CERTIFICATE_VERSION.keyword + SP)
+        || firstLines.contains(
+            NL + Key.DIR_KEY_CERTIFICATE_VERSION.keyword + SP)) {
       parsedDescriptors.addAll(DirectoryKeyCertificateImpl
           .parseDescriptors(rawDescriptorBytes,
           failUnrecognizedDescriptorLines));
     } else if (firstLines.startsWith("@type tordnsel 1.")
-        || firstLines.startsWith("ExitNode ")
-        || firstLines.contains("\nExitNode ")) {
+        || firstLines.startsWith("ExitNode" + SP)
+        || firstLines.contains(NL + "ExitNode" + SP)) {
       parsedDescriptors.add(new ExitListImpl(rawDescriptorBytes, fileName,
           failUnrecognizedDescriptorLines));
     } else if (firstLines.startsWith("@type network-status-2 1.")
-        || firstLines.startsWith("network-status-version 2\n")
-        || firstLines.contains("\nnetwork-status-version 2\n")) {
+        || firstLines.startsWith(
+            Key.NETWORK_STATUS_VERSION.keyword + SP + "2" + NL)
+        || firstLines.contains(
+            NL + Key.NETWORK_STATUS_VERSION.keyword + SP + "2" + NL)) {
       parsedDescriptors.add(new RelayNetworkStatusImpl(rawDescriptorBytes,
           failUnrecognizedDescriptorLines));
     } else if (firstLines.startsWith("@type directory 1.")
-        || firstLines.startsWith("signed-directory\n")
-        || firstLines.contains("\nsigned-directory\n")) {
+        || firstLines.startsWith(Key.SIGNED_DIRECTORY.keyword + NL)
+        || firstLines.contains(NL + Key.SIGNED_DIRECTORY.keyword + NL)) {
       parsedDescriptors.add(new RelayDirectoryImpl(rawDescriptorBytes,
           failUnrecognizedDescriptorLines));
     } else if (firstLines.startsWith("@type torperf 1.")) {
@@ -116,7 +128,7 @@ public abstract class DescriptorImpl implements Descriptor {
   protected static List<byte[]> splitRawDescriptorBytes(
       byte[] rawDescriptorBytes, String startToken) {
     List<byte[]> rawDescriptors = new ArrayList<>();
-    String splitToken = "\n" + startToken;
+    String splitToken = NL + startToken;
     String ascii;
     try {
       ascii = new String(rawDescriptorBytes, "US-ASCII");
@@ -126,7 +138,7 @@ public abstract class DescriptorImpl implements Descriptor {
     int endAllDescriptors = rawDescriptorBytes.length;
     int startAnnotations = 0;
     boolean containsAnnotations = ascii.startsWith("@")
-        || ascii.contains("\n@");
+        || ascii.contains(NL + "@");
     while (startAnnotations < endAllDescriptors) {
       int startDescriptor;
       if (ascii.indexOf(startToken, startAnnotations) == 0) {
@@ -141,7 +153,7 @@ public abstract class DescriptorImpl implements Descriptor {
       }
       int endDescriptor = -1;
       if (containsAnnotations) {
-        endDescriptor = ascii.indexOf("\n@", startDescriptor);
+        endDescriptor = ascii.indexOf(NL + "@", startDescriptor);
       }
       if (endDescriptor < 0) {
         endDescriptor = ascii.indexOf(splitToken, startDescriptor);
@@ -183,7 +195,7 @@ public abstract class DescriptorImpl implements Descriptor {
     this.failUnrecognizedDescriptorLines =
         failUnrecognizedDescriptorLines;
     this.cutOffAnnotations(rawDescriptorBytes);
-    this.countKeywords(rawDescriptorBytes, blankLinesAllowed);
+    this.countKeys(rawDescriptorBytes, blankLinesAllowed);
   }
 
   /* Parse annotation lines from the descriptor bytes. */
@@ -194,8 +206,8 @@ public abstract class DescriptorImpl implements Descriptor {
     String ascii = new String(rawDescriptorBytes);
     int start = 0;
     while ((start == 0 && ascii.startsWith("@"))
-        || (start > 0 && ascii.indexOf("\n@", start - 1) >= 0)) {
-      int end = ascii.indexOf("\n", start);
+        || (start > 0 && ascii.indexOf(NL + "@", start - 1) >= 0)) {
+      int end = ascii.indexOf(NL, start);
       if (end < 0) {
         throw new DescriptorParseException("Annotation line does not "
             + "contain a newline.");
@@ -217,130 +229,129 @@ public abstract class DescriptorImpl implements Descriptor {
     return new ArrayList<>(this.annotations);
   }
 
-  private String firstKeyword;
+  private Key firstKey = Key.EMPTY;
 
-  private String lastKeyword;
+  private Key lastKey = Key.EMPTY;
 
-  private Map<String, Integer> parsedKeywords = new HashMap<>();
+  private Map<Key, Integer> parsedKeys = new EnumMap<>(Key.class);
 
   /* Count parsed keywords for consistency checks by subclasses. */
-  private void countKeywords(byte[] rawDescriptorBytes,
+  private void countKeys(byte[] rawDescriptorBytes,
       boolean blankLinesAllowed) throws DescriptorParseException {
     if (rawDescriptorBytes.length == 0) {
       throw new DescriptorParseException("Descriptor is empty.");
     }
     String descriptorString = new String(rawDescriptorBytes);
-    if (!blankLinesAllowed && (descriptorString.startsWith("\n")
-        || descriptorString.contains("\n\n"))) {
+    if (!blankLinesAllowed && (descriptorString.startsWith(NL)
+        || descriptorString.contains(NL + NL))) {
       throw new DescriptorParseException("Blank lines are not allowed.");
     }
     boolean skipCrypto = false;
-    Scanner scanner = new Scanner(descriptorString).useDelimiter("\n");
+    Scanner scanner = new Scanner(descriptorString).useDelimiter(NL);
     while (scanner.hasNext()) {
       String line = scanner.next();
-      if (line.startsWith("-----BEGIN")) {
+      if (line.startsWith(Key.CRYPTO_BEGIN.keyword)) {
         skipCrypto = true;
-      } else if (line.startsWith("-----END")) {
+      } else if (line.startsWith(Key.CRYPTO_END.keyword)) {
         skipCrypto = false;
       } else if (!line.isEmpty() && !line.startsWith("@")
           && !skipCrypto) {
-        String lineNoOpt = line.startsWith("opt ")
-            ? line.substring("opt ".length()) : line;
-        String keyword = lineNoOpt.split(" ", -1)[0];
+        String lineNoOpt = line.startsWith(Key.OPT.keyword + SP)
+            ? line.substring(Key.OPT.keyword.length() + 1) : line;
+        String keyword = lineNoOpt.split(SP, -1)[0];
         if (keyword.equals("")) {
           throw new DescriptorParseException("Illegal keyword in line '"
               + line + "'.");
         }
-        if (this.firstKeyword == null) {
-          this.firstKeyword = keyword;
+        Key key = Key.get(keyword);
+        if (Key.EMPTY == this.firstKey) {
+          this.firstKey = key;
         }
-        lastKeyword = keyword;
-        if (parsedKeywords.containsKey(keyword)) {
-          parsedKeywords.put(keyword, parsedKeywords.get(keyword) + 1);
+        lastKey = key;
+        if (parsedKeys.containsKey(key)) {
+          parsedKeys.put(key, parsedKeys.get(key) + 1);
         } else {
-          parsedKeywords.put(keyword, 1);
+          parsedKeys.put(key, 1);
         }
       }
     }
   }
 
-  protected void checkFirstKeyword(String keyword)
+  protected void checkFirstKey(Key key)
       throws DescriptorParseException {
-    if (this.firstKeyword == null
-        || !this.firstKeyword.equals(keyword)) {
-      throw new DescriptorParseException("Keyword '" + keyword + "' must "
+    if (this.firstKey != key) {
+      throw new DescriptorParseException("Keyword '" + key.keyword + "' must "
           + "be contained in the first line.");
     }
   }
 
-  protected void checkLastKeyword(String keyword)
+  protected void checkLastKey(Key key)
       throws DescriptorParseException {
-    if (this.lastKeyword == null
-        || !this.lastKeyword.equals(keyword)) {
-      throw new DescriptorParseException("Keyword '" + keyword + "' must "
+    if (this.lastKey != key) {
+      throw new DescriptorParseException("Keyword '" + key.keyword + "' must "
           + "be contained in the last line.");
     }
   }
 
-  protected void checkExactlyOnceKeywords(Set<String> keywords)
+  protected void checkExactlyOnceKeys(Set<Key> keys)
       throws DescriptorParseException {
-    for (String keyword : keywords) {
+    for (Key key : keys) {
       int contained = 0;
-      if (this.parsedKeywords.containsKey(keyword)) {
-        contained = this.parsedKeywords.get(keyword);
+      if (this.parsedKeys.containsKey(key)) {
+        contained = this.parsedKeys.get(key);
       }
       if (contained != 1) {
-        throw new DescriptorParseException("Keyword '" + keyword + "' is "
+        throw new DescriptorParseException("Keyword '" + key.keyword + "' is "
             + "contained " + contained + " times, but must be contained "
             + "exactly once.");
       }
     }
   }
 
-  protected void checkAtLeastOnceKeywords(Set<String> keywords)
+  protected void checkAtLeastOnceKeys(Set<Key> keys)
       throws DescriptorParseException {
-    for (String keyword : keywords) {
-      if (!this.parsedKeywords.containsKey(keyword)) {
-        throw new DescriptorParseException("Keyword '" + keyword + "' is "
+    for (Key key : keys) {
+      if (!this.parsedKeys.containsKey(key)) {
+        throw new DescriptorParseException("Keyword '" + key.keyword + "' is "
             + "contained 0 times, but must be contained at least once.");
       }
     }
   }
 
-  protected void checkAtMostOnceKeywords(Set<String> keywords)
+  protected void checkAtMostOnceKeys(Set<Key> keys)
       throws DescriptorParseException {
-    for (String keyword : keywords) {
-      if (this.parsedKeywords.containsKey(keyword)
-          && this.parsedKeywords.get(keyword) > 1) {
-        throw new DescriptorParseException("Keyword '" + keyword + "' is "
-            + "contained " + this.parsedKeywords.get(keyword) + " times, "
+    for (Key key : keys) {
+      if (this.parsedKeys.containsKey(key)
+          && this.parsedKeys.get(key) > 1) {
+        throw new DescriptorParseException("Keyword '" + key.keyword + "' is "
+            + "contained " + this.parsedKeys.get(key) + " times, "
             + "but must be contained at most once.");
       }
     }
   }
 
-  protected void checkKeywordsDependOn(Set<String> dependentKeywords,
-      String dependingKeyword) throws DescriptorParseException {
-    for (String dependentKeyword : dependentKeywords) {
-      if (this.parsedKeywords.containsKey(dependentKeyword)
-          && !this.parsedKeywords.containsKey(dependingKeyword)) {
-        throw new DescriptorParseException("Keyword '" + dependentKeyword
-            + "' is contained, but keyword '" + dependingKeyword + "' is "
+  protected void checkKeysDependOn(Set<Key> dependentKeys,
+      Key dependingKey) throws DescriptorParseException {
+    for (Key dependentKey : dependentKeys) {
+      if (this.parsedKeys.containsKey(dependentKey)
+          && !this.parsedKeys.containsKey(dependingKey)) {
+        throw new DescriptorParseException("Keyword '" + dependentKey.keyword
+            + "' is contained, but keyword '" + dependingKey.keyword + "' is "
             + "not.");
       }
     }
   }
 
-  protected int getKeywordCount(String keyword) {
-    if (!this.parsedKeywords.containsKey(keyword)) {
+  protected int getKeyCount(Key key) {
+    if (!this.parsedKeys.containsKey(key)) {
       return 0;
     } else {
-      return this.parsedKeywords.get(keyword);
+      return this.parsedKeys.get(key);
     }
   }
 
-  protected void clearParsedKeywords() {
-    this.parsedKeywords = null;
+  protected void clearParsedKeys() {
+    this.parsedKeys = null;
   }
 }
 
diff --git a/src/main/java/org/torproject/descriptor/impl/DirSourceEntryImpl.java b/src/main/java/org/torproject/descriptor/impl/DirSourceEntryImpl.java
index 539d211..fd2c783 100644
--- a/src/main/java/org/torproject/descriptor/impl/DirSourceEntryImpl.java
+++ b/src/main/java/org/torproject/descriptor/impl/DirSourceEntryImpl.java
@@ -3,14 +3,17 @@
 
 package org.torproject.descriptor.impl;
 
+import static org.torproject.descriptor.impl.DescriptorImpl.NL;
+import static org.torproject.descriptor.impl.DescriptorImpl.SP;
+
 import org.torproject.descriptor.DescriptorParseException;
 import org.torproject.descriptor.DirSourceEntry;
 
 import java.util.ArrayList;
+import java.util.EnumSet;
 import java.util.List;
 import java.util.Scanner;
-import java.util.SortedSet;
-import java.util.TreeSet;
+import java.util.Set;
 
 public class DirSourceEntryImpl implements DirSourceEntry {
 
@@ -37,72 +40,67 @@ public class DirSourceEntryImpl implements DirSourceEntry {
     this.dirSourceEntryBytes = dirSourceEntryBytes;
     this.failUnrecognizedDescriptorLines =
         failUnrecognizedDescriptorLines;
-    this.initializeKeywords();
     this.parseDirSourceEntryBytes();
-    this.checkAndClearKeywords();
+    this.checkAndClearKeys();
   }
 
-  private SortedSet<String> exactlyOnceKeywords;
-
-  private SortedSet<String> atMostOnceKeywords;
+  private Set<Key> exactlyOnceKeys = EnumSet.of(
+      Key.DIR_SOURCE, Key.VOTE_DIGEST);
 
-  private void initializeKeywords() {
-    this.exactlyOnceKeywords = new TreeSet<>();
-    this.exactlyOnceKeywords.add("dir-source");
-    this.exactlyOnceKeywords.add("vote-digest");
-    this.atMostOnceKeywords = new TreeSet<>();
-    this.atMostOnceKeywords.add("contact");
-  }
+  private Set<Key> atMostOnceKeys = EnumSet.of(Key.CONTACT);
 
-  private void parsedExactlyOnceKeyword(String keyword)
+  private void parsedExactlyOnceKey(Key key)
       throws DescriptorParseException {
-    if (!this.exactlyOnceKeywords.contains(keyword)) {
-      throw new DescriptorParseException("Duplicate '" + keyword
+    if (!this.exactlyOnceKeys.contains(key)) {
+      throw new DescriptorParseException("Duplicate '" + key.keyword
           + "' line in dir-source.");
     }
-    this.exactlyOnceKeywords.remove(keyword);
+    this.exactlyOnceKeys.remove(key);
   }
 
-  private void parsedAtMostOnceKeyword(String keyword)
+  private void parsedAtMostOnceKey(Key key)
       throws DescriptorParseException {
-    if (!this.atMostOnceKeywords.contains(keyword)) {
-      throw new DescriptorParseException("Duplicate " + keyword + "line "
+    if (!this.atMostOnceKeys.contains(key)) {
+      throw new DescriptorParseException("Duplicate " + key.keyword + "line "
           + "in dir-source.");
     }
-    this.atMostOnceKeywords.remove(keyword);
+    this.atMostOnceKeys.remove(key);
   }
 
-  private void checkAndClearKeywords() throws DescriptorParseException {
-    if (!this.exactlyOnceKeywords.isEmpty()) {
-      throw new DescriptorParseException("dir-source does not contain a '"
-          + this.exactlyOnceKeywords.first() + "' line.");
+  private void checkAndClearKeys() throws DescriptorParseException {
+    if (!this.exactlyOnceKeys.isEmpty()) {
+      for (Key key : this.exactlyOnceKeys) {
+        throw new DescriptorParseException("dir-source does not contain a '"
+            + key.keyword + "' line.");
+      }
     }
-    this.exactlyOnceKeywords = null;
-    this.atMostOnceKeywords = null;
+    this.exactlyOnceKeys = null;
+    this.atMostOnceKeys = null;
   }
 
   private void parseDirSourceEntryBytes()
       throws DescriptorParseException {
     Scanner scanner = new Scanner(new String(this.dirSourceEntryBytes))
-        .useDelimiter("\n");
+        .useDelimiter(NL);
     boolean skipCrypto = false;
     while (scanner.hasNext()) {
       String line = scanner.next();
-      String[] parts = line.split(" ");
-      switch (parts[0]) {
-        case "dir-source":
+      String[] parts = line.split(SP);
+      Key key = Key.get(parts[0]);
+      switch (key) {
+        case DIR_SOURCE:
           this.parseDirSourceLine(line);
           break;
-        case "contact":
+        case CONTACT:
           this.parseContactLine(line);
           break;
-        case "vote-digest":
+        case VOTE_DIGEST:
           this.parseVoteDigestLine(line);
           break;
-        case "-----BEGIN":
+        case CRYPTO_BEGIN:
           skipCrypto = true;
           break;
-        case "-----END":
+        case CRYPTO_END:
           skipCrypto = false;
           break;
         default:
@@ -123,7 +121,7 @@ public class DirSourceEntryImpl implements DirSourceEntry {
 
   private void parseDirSourceLine(String line)
       throws DescriptorParseException {
-    this.parsedExactlyOnceKeyword("dir-source");
+    this.parsedExactlyOnceKey(Key.DIR_SOURCE);
     String[] parts = line.split("[ \t]+");
     if (parts.length != 7) {
       throw new DescriptorParseException("Invalid line '" + line + "'.");
@@ -133,7 +131,7 @@ public class DirSourceEntryImpl implements DirSourceEntry {
       nickname = nickname.substring(0, nickname.length()
           - "-legacy".length());
       this.isLegacy = true;
-      this.parsedExactlyOnceKeyword("vote-digest");
+      this.parsedExactlyOnceKey(Key.VOTE_DIGEST);
     }
     this.nickname = ParseHelper.parseNickname(line, nickname);
     this.identity = ParseHelper.parseTwentyByteHexString(line, parts[2]);
@@ -149,9 +147,9 @@ public class DirSourceEntryImpl implements DirSourceEntry {
 
   private void parseContactLine(String line)
       throws DescriptorParseException {
-    this.parsedAtMostOnceKeyword("contact");
-    if (line.length() > "contact ".length()) {
-      this.contactLine = line.substring("contact ".length());
+    this.parsedAtMostOnceKey(Key.CONTACT);
+    if (line.length() > Key.CONTACT.keyword.length() + 1) {
+      this.contactLine = line.substring(Key.CONTACT.keyword.length() + 1);
     } else {
       this.contactLine = "";
     }
@@ -159,7 +157,7 @@ public class DirSourceEntryImpl implements DirSourceEntry {
 
   private void parseVoteDigestLine(String line)
       throws DescriptorParseException {
-    this.parsedExactlyOnceKeyword("vote-digest");
+    this.parsedExactlyOnceKey(Key.VOTE_DIGEST);
     String[] parts = line.split("[ \t]+");
     if (parts.length != 2) {
       throw new DescriptorParseException("Invalid line '" + line + "'.");
diff --git a/src/main/java/org/torproject/descriptor/impl/DirectoryKeyCertificateImpl.java b/src/main/java/org/torproject/descriptor/impl/DirectoryKeyCertificateImpl.java
index 64df7aa..0bb08fd 100644
--- a/src/main/java/org/torproject/descriptor/impl/DirectoryKeyCertificateImpl.java
+++ b/src/main/java/org/torproject/descriptor/impl/DirectoryKeyCertificateImpl.java
@@ -10,8 +10,7 @@ import java.io.UnsupportedEncodingException;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashSet;
+import java.util.EnumSet;
 import java.util.List;
 import java.util.Scanner;
 import java.util.Set;
@@ -27,7 +26,7 @@ public class DirectoryKeyCertificateImpl extends DescriptorImpl
     List<DirectoryKeyCertificate> parsedDescriptors = new ArrayList<>();
     List<byte[]> splitDescriptorsBytes =
         DirectoryKeyCertificateImpl.splitRawDescriptorBytes(
-            descriptorsBytes, "dir-key-certificate-version ");
+            descriptorsBytes, Key.DIR_KEY_CERTIFICATE_VERSION.keyword + SP);
     for (byte[] descriptorBytes : splitDescriptorsBytes) {
       DirectoryKeyCertificate parsedDescriptor =
           new DirectoryKeyCertificateImpl(descriptorBytes,
@@ -43,90 +42,90 @@ public class DirectoryKeyCertificateImpl extends DescriptorImpl
     super(rawDescriptorBytes, failUnrecognizedDescriptorLines, false);
     this.parseDescriptorBytes();
     this.calculateDigest();
-    Set<String> exactlyOnceKeywords = new HashSet<>(Arrays.asList((
-        "dir-key-certificate-version,fingerprint,dir-identity-key,"
-        + "dir-key-published,dir-key-expires,dir-signing-key,"
-        + "dir-key-certification").split(",")));
-    this.checkExactlyOnceKeywords(exactlyOnceKeywords);
-    Set<String> atMostOnceKeywords = new HashSet<>(Arrays.asList((
-        "dir-address,dir-key-crosscert").split(",")));
-    this.checkAtMostOnceKeywords(atMostOnceKeywords);
-    this.checkFirstKeyword("dir-key-certificate-version");
-    this.checkLastKeyword("dir-key-certification");
-    this.clearParsedKeywords();
+    Set<Key> exactlyOnceKeys = EnumSet.of(
+        Key.DIR_KEY_CERTIFICATE_VERSION, Key.FINGERPRINT, Key.DIR_IDENTITY_KEY,
+        Key.DIR_KEY_PUBLISHED, Key.DIR_KEY_EXPIRES, Key.DIR_SIGNING_KEY,
+        Key.DIR_KEY_CERTIFICATION);
+    this.checkExactlyOnceKeys(exactlyOnceKeys);
+    Set<Key> atMostOnceKeys = EnumSet.of(
+        Key.DIR_ADDRESS, Key.DIR_KEY_CROSSCERT);
+    this.checkAtMostOnceKeys(atMostOnceKeys);
+    this.checkFirstKey(Key.DIR_KEY_CERTIFICATE_VERSION);
+    this.checkLastKey(Key.DIR_KEY_CERTIFICATION);
+    this.clearParsedKeys();
   }
 
   private void parseDescriptorBytes() throws DescriptorParseException {
     Scanner scanner = new Scanner(new String(this.rawDescriptorBytes))
-        .useDelimiter("\n");
-    String nextCrypto = "";
+        .useDelimiter(NL);
+    Key nextCrypto = Key.EMPTY;
     StringBuilder crypto = null;
     while (scanner.hasNext()) {
       String line = scanner.next();
       String[] parts = line.split("[ \t]+");
-      String keyword = parts[0];
-      switch (keyword) {
-        case "dir-key-certificate-version":
+      Key key = Key.get(parts[0]);
+      switch (key) {
+        case DIR_KEY_CERTIFICATE_VERSION:
           this.parseDirKeyCertificateVersionLine(line, parts);
           break;
-        case "dir-address":
+        case DIR_ADDRESS:
           this.parseDirAddressLine(line, parts);
           break;
-        case "fingerprint":
+        case FINGERPRINT:
           this.parseFingerprintLine(line, parts);
           break;
-        case "dir-identity-key":
+        case DIR_IDENTITY_KEY:
           this.parseDirIdentityKeyLine(line, parts);
-          nextCrypto = "dir-identity-key";
+          nextCrypto = key;
           break;
-        case "dir-key-published":
+        case DIR_KEY_PUBLISHED:
           this.parseDirKeyPublishedLine(line, parts);
           break;
-        case "dir-key-expires":
+        case DIR_KEY_EXPIRES:
           this.parseDirKeyExpiresLine(line, parts);
           break;
-        case "dir-signing-key":
+        case DIR_SIGNING_KEY:
           this.parseDirSigningKeyLine(line, parts);
-          nextCrypto = "dir-signing-key";
+          nextCrypto = key;
           break;
-        case "dir-key-crosscert":
+        case DIR_KEY_CROSSCERT:
           this.parseDirKeyCrosscertLine(line, parts);
-          nextCrypto = "dir-key-crosscert";
+          nextCrypto = key;
           break;
-        case "dir-key-certification":
+        case DIR_KEY_CERTIFICATION:
           this.parseDirKeyCertificationLine(line, parts);
-          nextCrypto = "dir-key-certification";
+          nextCrypto = key;
           break;
-        case "-----BEGIN":
+        case CRYPTO_BEGIN:
           crypto = new StringBuilder();
-          crypto.append(line).append("\n");
+          crypto.append(line).append(NL);
           break;
-        case "-----END":
-          crypto.append(line).append("\n");
+        case CRYPTO_END:
+          crypto.append(line).append(NL);
           String cryptoString = crypto.toString();
           crypto = null;
           switch (nextCrypto) {
-            case "dir-identity-key":
+            case DIR_IDENTITY_KEY:
               this.dirIdentityKey = cryptoString;
               break;
-            case "dir-signing-key":
+            case DIR_SIGNING_KEY:
               this.dirSigningKey = cryptoString;
               break;
-            case "dir-key-crosscert":
+            case DIR_KEY_CROSSCERT:
               this.dirKeyCrosscert = cryptoString;
               break;
-            case "dir-key-certification":
+            case DIR_KEY_CERTIFICATION:
               this.dirKeyCertification = cryptoString;
               break;
             default:
               throw new DescriptorParseException("Unrecognized crypto "
                   + "block in directory key certificate.");
           }
-          nextCrypto = "";
+          nextCrypto = Key.EMPTY;
           break;
         default:
           if (crypto != null) {
-            crypto.append(line).append("\n");
+            crypto.append(line).append(NL);
           } else {
             if (this.failUnrecognizedDescriptorLines) {
               throw new DescriptorParseException("Unrecognized line '"
@@ -144,7 +143,7 @@ public class DirectoryKeyCertificateImpl extends DescriptorImpl
 
   private void parseDirKeyCertificateVersionLine(String line,
       String[] parts) throws DescriptorParseException {
-    if (!line.equals("dir-key-certificate-version 3")) {
+    if (!line.equals(Key.DIR_KEY_CERTIFICATE_VERSION.keyword + SP + "3")) {
       throw new DescriptorParseException("Illegal directory key "
           + "certificate version number in line '" + line + "'.");
     }
@@ -174,7 +173,7 @@ public class DirectoryKeyCertificateImpl extends DescriptorImpl
 
   private void parseDirIdentityKeyLine(String line, String[] parts)
       throws DescriptorParseException {
-    if (!line.equals("dir-identity-key")) {
+    if (!line.equals(Key.DIR_IDENTITY_KEY.keyword)) {
       throw new DescriptorParseException("Illegal line '" + line + "'.");
     }
   }
@@ -193,21 +192,21 @@ public class DirectoryKeyCertificateImpl extends DescriptorImpl
 
   private void parseDirSigningKeyLine(String line, String[] parts)
       throws DescriptorParseException {
-    if (!line.equals("dir-signing-key")) {
+    if (!line.equals(Key.DIR_SIGNING_KEY.keyword)) {
       throw new DescriptorParseException("Illegal line '" + line + "'.");
     }
   }
 
   private void parseDirKeyCrosscertLine(String line, String[] parts)
       throws DescriptorParseException {
-    if (!line.equals("dir-key-crosscert")) {
+    if (!line.equals(Key.DIR_KEY_CROSSCERT.keyword)) {
       throw new DescriptorParseException("Illegal line '" + line + "'.");
     }
   }
 
   private void parseDirKeyCertificationLine(String line, String[] parts)
       throws DescriptorParseException {
-    if (!line.equals("dir-key-certification")) {
+    if (!line.equals(Key.DIR_KEY_CERTIFICATION.keyword)) {
       throw new DescriptorParseException("Illegal line '" + line + "'.");
     }
   }
@@ -215,8 +214,8 @@ public class DirectoryKeyCertificateImpl extends DescriptorImpl
   private void calculateDigest() throws DescriptorParseException {
     try {
       String ascii = new String(this.getRawDescriptorBytes(), "US-ASCII");
-      String startToken = "dir-key-certificate-version ";
-      String sigToken = "\ndir-key-certification\n";
+      String startToken = Key.DIR_KEY_CERTIFICATE_VERSION.keyword + SP;
+      String sigToken = NL + Key.DIR_KEY_CERTIFICATION.keyword + NL;
       int start = ascii.indexOf(startToken);
       int sig = ascii.indexOf(sigToken) + sigToken.length();
       if (start >= 0 && sig >= 0 && sig > start) {
diff --git a/src/main/java/org/torproject/descriptor/impl/DirectorySignatureImpl.java b/src/main/java/org/torproject/descriptor/impl/DirectorySignatureImpl.java
index 7cf427a..674b634 100644
--- a/src/main/java/org/torproject/descriptor/impl/DirectorySignatureImpl.java
+++ b/src/main/java/org/torproject/descriptor/impl/DirectorySignatureImpl.java
@@ -3,6 +3,9 @@
 
 package org.torproject.descriptor.impl;
 
+import static org.torproject.descriptor.impl.DescriptorImpl.NL;
+import static org.torproject.descriptor.impl.DescriptorImpl.SP;
+
 import org.torproject.descriptor.DescriptorParseException;
 import org.torproject.descriptor.DirectorySignature;
 
@@ -36,14 +39,14 @@ public class DirectorySignatureImpl implements DirectorySignature {
   private void parseDirectorySignatureBytes()
       throws DescriptorParseException {
     Scanner scanner = new Scanner(new String(this.directorySignatureBytes))
-        .useDelimiter("\n");
+        .useDelimiter(NL);
     StringBuilder crypto = null;
     while (scanner.hasNext()) {
       String line = scanner.next();
-      String[] parts = line.split(" ", -1);
-      String keyword = parts[0];
-      switch (keyword) {
-        case "directory-signature":
+      String[] parts = line.split(SP, -1);
+      Key key = Key.get(parts[0]);
+      switch (key) {
+        case DIRECTORY_SIGNATURE:
           int algorithmOffset = 0;
           switch (parts.length) {
             case 4:
@@ -61,19 +64,19 @@ public class DirectorySignatureImpl implements DirectorySignature {
           this.signingKeyDigest = ParseHelper.parseHexString(
               line, parts[2 + algorithmOffset]);
           break;
-        case "-----BEGIN":
+        case CRYPTO_BEGIN:
           crypto = new StringBuilder();
-          crypto.append(line).append("\n");
+          crypto.append(line).append(NL);
           break;
-        case "-----END":
-          crypto.append(line).append("\n");
+        case CRYPTO_END:
+          crypto.append(line).append(NL);
           String cryptoString = crypto.toString();
           crypto = null;
           this.signature = cryptoString;
           break;
         default:
           if (crypto != null) {
-            crypto.append(line).append("\n");
+            crypto.append(line).append(NL);
           } else {
             if (this.failUnrecognizedDescriptorLines) {
               throw new DescriptorParseException("Unrecognized line '"
diff --git a/src/main/java/org/torproject/descriptor/impl/ExtraInfoDescriptorImpl.java b/src/main/java/org/torproject/descriptor/impl/ExtraInfoDescriptorImpl.java
index dd1bc07..fd55925 100644
--- a/src/main/java/org/torproject/descriptor/impl/ExtraInfoDescriptorImpl.java
+++ b/src/main/java/org/torproject/descriptor/impl/ExtraInfoDescriptorImpl.java
@@ -14,8 +14,8 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Comparator;
+import java.util.EnumSet;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Scanner;
@@ -28,6 +28,16 @@ import javax.xml.bind.DatatypeConverter;
 public abstract class ExtraInfoDescriptorImpl extends DescriptorImpl
     implements ExtraInfoDescriptor {
 
+  private Set<Key> exactlyOnceKeys = EnumSet.of(
+      Key.EXTRA_INFO, Key.PUBLISHED);
+
+  private static final Set<Key> atMostOnceKeys = EnumSet.of(
+      Key.IDENTITY_ED25519, Key.MASTER_KEY_ED25519, Key.READ_HISTORY,
+      Key.WRITE_HISTORY, Key.DIRREQ_READ_HISTORY, Key.DIRREQ_WRITE_HISTORY,
+      Key.GEOIP_DB_DIGEST, Key.GEOIP6_DB_DIGEST, Key.ROUTER_SIG_ED25519,
+      Key.ROUTER_SIGNATURE, Key.ROUTER_DIGEST_SHA256, Key.ROUTER_DIGEST,
+      Key.PADDING_COUNTS);
+
   protected ExtraInfoDescriptorImpl(byte[] descriptorBytes,
       boolean failUnrecognizedDescriptorLines)
       throws DescriptorParseException {
@@ -35,231 +45,223 @@ public abstract class ExtraInfoDescriptorImpl extends DescriptorImpl
     this.parseDescriptorBytes();
     this.calculateDigest();
     this.calculateDigestSha256();
-    Set<String> exactlyOnceKeywords = new HashSet<>(Arrays.asList((
-        "extra-info,published").split(",")));
-    this.checkExactlyOnceKeywords(exactlyOnceKeywords);
-    Set<String> dirreqStatsKeywords = new HashSet<>(Arrays.asList((
-        "dirreq-stats-end,dirreq-v2-ips,dirreq-v3-ips,dirreq-v2-reqs,"
-        + "dirreq-v3-reqs,dirreq-v2-share,dirreq-v3-share,dirreq-v2-resp,"
-        + "dirreq-v3-resp,dirreq-v2-direct-dl,dirreq-v3-direct-dl,"
-        + "dirreq-v2-tunneled-dl,dirreq-v3-tunneled-dl,").split(",")));
-    Set<String> entryStatsKeywords = new HashSet<>(Arrays.asList(
-        "entry-stats-end,entry-ips".split(",")));
-    Set<String> cellStatsKeywords = new HashSet<>(Arrays.asList((
-        "cell-stats-end,cell-processed-cells,cell-queued-cells,"
-        + "cell-time-in-queue,cell-circuits-per-decile").split(",")));
-    Set<String> connBiDirectStatsKeywords = new HashSet<>(
-        Arrays.asList("conn-bi-direct".split(",")));
-    Set<String> exitStatsKeywords = new HashSet<>(Arrays.asList((
-        "exit-stats-end,exit-kibibytes-written,exit-kibibytes-read,"
-        + "exit-streams-opened").split(",")));
-    Set<String> bridgeStatsKeywords = new HashSet<>(Arrays.asList(
-        "bridge-stats-end,bridge-stats-ips".split(",")));
-    Set<String> atMostOnceKeywords = new HashSet<>(Arrays.asList((
-        "identity-ed25519,master-key-ed25519,read-history,write-history,"
-        + "dirreq-read-history,dirreq-write-history,geoip-db-digest,"
-        + "router-sig-ed25519,router-signature,router-digest-sha256,"
-        + "router-digest").split(",")));
-    atMostOnceKeywords.addAll(dirreqStatsKeywords);
-    atMostOnceKeywords.addAll(entryStatsKeywords);
-    atMostOnceKeywords.addAll(cellStatsKeywords);
-    atMostOnceKeywords.addAll(connBiDirectStatsKeywords);
-    atMostOnceKeywords.addAll(exitStatsKeywords);
-    atMostOnceKeywords.addAll(bridgeStatsKeywords);
-    atMostOnceKeywords.add("padding-counts");
-    this.checkAtMostOnceKeywords(atMostOnceKeywords);
-    this.checkKeywordsDependOn(dirreqStatsKeywords, "dirreq-stats-end");
-    this.checkKeywordsDependOn(entryStatsKeywords, "entry-stats-end");
-    this.checkKeywordsDependOn(cellStatsKeywords, "cell-stats-end");
-    this.checkKeywordsDependOn(exitStatsKeywords, "exit-stats-end");
-    this.checkKeywordsDependOn(bridgeStatsKeywords, "bridge-stats-end");
-    this.checkFirstKeyword("extra-info");
-    this.clearParsedKeywords();
+    this.checkExactlyOnceKeys(exactlyOnceKeys);
+    Set<Key> dirreqStatsKeys = EnumSet.of(
+        Key.DIRREQ_STATS_END, Key.DIRREQ_V2_IPS, Key.DIRREQ_V3_IPS,
+        Key.DIRREQ_V2_REQS, Key.DIRREQ_V3_REQS, Key.DIRREQ_V2_SHARE,
+        Key.DIRREQ_V3_SHARE, Key.DIRREQ_V2_RESP, Key.DIRREQ_V3_RESP,
+        Key.DIRREQ_V2_DIRECT_DL, Key.DIRREQ_V3_DIRECT_DL,
+        Key.DIRREQ_V2_TUNNELED_DL, Key.DIRREQ_V3_TUNNELED_DL);
+    Set<Key> entryStatsKeys = EnumSet.of(
+        Key.ENTRY_STATS_END, Key.ENTRY_IPS);
+    Set<Key> cellStatsKeys = EnumSet.of(
+        Key.CELL_STATS_END, Key.CELL_PROCESSED_CELLS, Key.CELL_QUEUED_CELLS,
+        Key.CELL_TIME_IN_QUEUE, Key.CELL_CIRCUITS_PER_DECILE);
+    Set<Key> connBiDirectStatsKeys = EnumSet.of(Key.CONN_BI_DIRECT);
+    Set<Key> exitStatsKeys = EnumSet.of(
+        Key.EXIT_STATS_END, Key.EXIT_KIBIBYTES_WRITTEN, Key.EXIT_KIBIBYTES_READ,
+        Key.EXIT_STREAMS_OPENED);
+    Set<Key> bridgeStatsKeys = EnumSet.of(
+        Key.BRIDGE_STATS_END, Key.BRIDGE_IPS);
+    atMostOnceKeys.addAll(dirreqStatsKeys);
+    atMostOnceKeys.addAll(entryStatsKeys);
+    atMostOnceKeys.addAll(cellStatsKeys);
+    atMostOnceKeys.addAll(connBiDirectStatsKeys);
+    atMostOnceKeys.addAll(exitStatsKeys);
+    atMostOnceKeys.addAll(bridgeStatsKeys);
+    this.checkAtMostOnceKeys(atMostOnceKeys);
+    this.checkKeysDependOn(dirreqStatsKeys, Key.DIRREQ_STATS_END);
+    this.checkKeysDependOn(entryStatsKeys, Key.ENTRY_STATS_END);
+    this.checkKeysDependOn(cellStatsKeys, Key.CELL_STATS_END);
+    this.checkKeysDependOn(exitStatsKeys, Key.EXIT_STATS_END);
+    this.checkKeysDependOn(bridgeStatsKeys, Key.BRIDGE_STATS_END);
+    this.checkFirstKey(Key.EXTRA_INFO);
+    this.clearParsedKeys();
     return;
   }
 
   private void parseDescriptorBytes() throws DescriptorParseException {
     Scanner scanner = new Scanner(new String(this.rawDescriptorBytes))
-        .useDelimiter("\n");
-    String nextCrypto = "";
+        .useDelimiter(NL);
+    Key nextCrypto = Key.EMPTY;
     List<String> cryptoLines = null;
     while (scanner.hasNext()) {
       String line = scanner.next();
-      String lineNoOpt = line.startsWith("opt ")
-          ? line.substring("opt ".length()) : line;
+      String lineNoOpt = line.startsWith(Key.OPT.keyword + SP)
+          ? line.substring(Key.OPT.keyword.length() + 1) : line;
       String[] partsNoOpt = lineNoOpt.split("[ \t]+");
-      String keyword = partsNoOpt[0];
-      switch (keyword) {
-        case "extra-info":
+      Key key = Key.get(partsNoOpt[0]);
+      switch (key) {
+        case EXTRA_INFO:
           this.parseExtraInfoLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "published":
+        case PUBLISHED:
           this.parsePublishedLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "read-history":
+        case READ_HISTORY:
           this.parseReadHistoryLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "write-history":
+        case WRITE_HISTORY:
           this.parseWriteHistoryLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "geoip-db-digest":
+        case GEOIP_DB_DIGEST:
           this.parseGeoipDbDigestLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "geoip6-db-digest":
+        case GEOIP6_DB_DIGEST:
           this.parseGeoip6DbDigestLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "geoip-start-time":
+        case GEOIP_START_TIME:
           this.parseGeoipStartTimeLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "geoip-client-origins":
+        case GEOIP_CLIENT_ORIGINS:
           this.parseGeoipClientOriginsLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "dirreq-stats-end":
+        case DIRREQ_STATS_END:
           this.parseDirreqStatsEndLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "dirreq-v2-ips":
+        case DIRREQ_V2_IPS:
           this.parseDirreqV2IpsLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "dirreq-v3-ips":
+        case DIRREQ_V3_IPS:
           this.parseDirreqV3IpsLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "dirreq-v2-reqs":
+        case DIRREQ_V2_REQS:
           this.parseDirreqV2ReqsLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "dirreq-v3-reqs":
+        case DIRREQ_V3_REQS:
           this.parseDirreqV3ReqsLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "dirreq-v2-share":
+        case DIRREQ_V2_SHARE:
           this.parseDirreqV2ShareLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "dirreq-v3-share":
+        case DIRREQ_V3_SHARE:
           this.parseDirreqV3ShareLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "dirreq-v2-resp":
+        case DIRREQ_V2_RESP:
           this.parseDirreqV2RespLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "dirreq-v3-resp":
+        case DIRREQ_V3_RESP:
           this.parseDirreqV3RespLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "dirreq-v2-direct-dl":
+        case DIRREQ_V2_DIRECT_DL:
           this.parseDirreqV2DirectDlLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "dirreq-v3-direct-dl":
+        case DIRREQ_V3_DIRECT_DL:
           this.parseDirreqV3DirectDlLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "dirreq-v2-tunneled-dl":
+        case DIRREQ_V2_TUNNELED_DL:
           this.parseDirreqV2TunneledDlLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "dirreq-v3-tunneled-dl":
+        case DIRREQ_V3_TUNNELED_DL:
           this.parseDirreqV3TunneledDlLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "dirreq-read-history":
+        case DIRREQ_READ_HISTORY:
           this.parseDirreqReadHistoryLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "dirreq-write-history":
+        case DIRREQ_WRITE_HISTORY:
           this.parseDirreqWriteHistoryLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "entry-stats-end":
+        case ENTRY_STATS_END:
           this.parseEntryStatsEndLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "entry-ips":
+        case ENTRY_IPS:
           this.parseEntryIpsLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "cell-stats-end":
+        case CELL_STATS_END:
           this.parseCellStatsEndLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "cell-processed-cells":
+        case CELL_PROCESSED_CELLS:
           this.parseCellProcessedCellsLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "cell-queued-cells":
+        case CELL_QUEUED_CELLS:
           this.parseCellQueuedCellsLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "cell-time-in-queue":
+        case CELL_TIME_IN_QUEUE:
           this.parseCellTimeInQueueLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "cell-circuits-per-decile":
+        case CELL_CIRCUITS_PER_DECILE:
           this.parseCellCircuitsPerDecileLine(line, lineNoOpt,
               partsNoOpt);
           break;
-        case "conn-bi-direct":
+        case CONN_BI_DIRECT:
           this.parseConnBiDirectLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "exit-stats-end":
+        case EXIT_STATS_END:
           this.parseExitStatsEndLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "exit-kibibytes-written":
+        case EXIT_KIBIBYTES_WRITTEN:
           this.parseExitKibibytesWrittenLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "exit-kibibytes-read":
+        case EXIT_KIBIBYTES_READ:
           this.parseExitKibibytesReadLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "exit-streams-opened":
+        case EXIT_STREAMS_OPENED:
           this.parseExitStreamsOpenedLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "bridge-stats-end":
+        case BRIDGE_STATS_END:
           this.parseBridgeStatsEndLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "bridge-ips":
+        case BRIDGE_IPS:
           this.parseBridgeStatsIpsLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "bridge-ip-versions":
+        case BRIDGE_IP_VERSIONS:
           this.parseBridgeIpVersionsLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "bridge-ip-transports":
+        case BRIDGE_IP_TRANSPORTS:
           this.parseBridgeIpTransportsLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "transport":
+        case TRANSPORT:
           this.parseTransportLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "hidserv-stats-end":
+        case HIDSERV_STATS_END:
           this.parseHidservStatsEndLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "hidserv-rend-relayed-cells":
+        case HIDSERV_REND_RELAYED_CELLS:
           this.parseHidservRendRelayedCellsLine(line, lineNoOpt,
               partsNoOpt);
           break;
-        case "hidserv-dir-onions-seen":
+        case HIDSERV_DIR_ONIONS_SEEN:
           this.parseHidservDirOnionsSeenLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "padding-counts":
+        case PADDING_COUNTS:
           this.parsePaddingCountsLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "identity-ed25519":
+        case IDENTITY_ED25519:
           this.parseIdentityEd25519Line(line, lineNoOpt, partsNoOpt);
-          nextCrypto = "identity-ed25519";
+          nextCrypto = key;
           break;
-        case "master-key-ed25519":
+        case MASTER_KEY_ED25519:
           this.parseMasterKeyEd25519Line(line, lineNoOpt, partsNoOpt);
           break;
-        case "router-sig-ed25519":
+        case ROUTER_SIG_ED25519:
           this.parseRouterSigEd25519Line(line, lineNoOpt, partsNoOpt);
           break;
-        case "router-signature":
+        case ROUTER_SIGNATURE:
           this.parseRouterSignatureLine(line, lineNoOpt, partsNoOpt);
-          nextCrypto = "router-signature";
+          nextCrypto = key;
           break;
-        case "router-digest":
+        case ROUTER_DIGEST:
           this.parseRouterDigestLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "router-digest-sha256":
+        case ROUTER_DIGEST_SHA256:
           this.parseRouterDigestSha256Line(line, lineNoOpt, partsNoOpt);
           break;
-        case "-----BEGIN":
+        case CRYPTO_BEGIN:
           cryptoLines = new ArrayList<>();
           cryptoLines.add(line);
           break;
-        case "-----END":
+        case CRYPTO_END:
           cryptoLines.add(line);
           StringBuilder sb = new StringBuilder();
           for (String cryptoLine : cryptoLines) {
-            sb.append("\n").append(cryptoLine);
+            sb.append(NL).append(cryptoLine);
           }
           String cryptoString = sb.toString().substring(1);
           switch (nextCrypto) {
-            case "router-signature":
+            case ROUTER_SIGNATURE:
               this.routerSignature = cryptoString;
               break;
-            case "identity-ed25519":
+            case IDENTITY_ED25519:
               this.identityEd25519 = cryptoString;
               this.parseIdentityEd25519CryptoBlock(cryptoString);
               break;
@@ -276,7 +278,7 @@ public abstract class ExtraInfoDescriptorImpl extends DescriptorImpl
               }
           }
           cryptoLines = null;
-          nextCrypto = "";
+          nextCrypto = Key.EMPTY;
           break;
         default:
           if (cryptoLines != null) {
@@ -845,8 +847,8 @@ public abstract class ExtraInfoDescriptorImpl extends DescriptorImpl
     }
     try {
       String ascii = new String(this.getRawDescriptorBytes(), "US-ASCII");
-      String startToken = "extra-info ";
-      String sigToken = "\nrouter-signature\n";
+      String startToken = Key.EXTRA_INFO.keyword + SP;
+      String sigToken = NL + Key.ROUTER_SIGNATURE.keyword + NL;
       int start = ascii.indexOf(startToken);
       int sig = ascii.indexOf(sigToken) + sigToken.length();
       if (start >= 0 && sig >= 0 && sig > start) {
@@ -876,7 +878,7 @@ public abstract class ExtraInfoDescriptorImpl extends DescriptorImpl
     }
     try {
       String ascii = new String(this.getRawDescriptorBytes(), "US-ASCII");
-      String startToken = "extra-info ";
+      String startToken = Key.EXTRA_INFO.keyword + SP;
       String sigToken = "\n-----END SIGNATURE-----\n";
       int start = ascii.indexOf(startToken);
       int sig = ascii.indexOf(sigToken) + sigToken.length();
diff --git a/src/main/java/org/torproject/descriptor/impl/Key.java b/src/main/java/org/torproject/descriptor/impl/Key.java
new file mode 100644
index 0000000..763ecbf
--- /dev/null
+++ b/src/main/java/org/torproject/descriptor/impl/Key.java
@@ -0,0 +1,177 @@
+package org.torproject.descriptor.impl;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public enum Key {
+
+  EMPTY("the-empty-key"),
+  INVALID("the-invalid-key"),
+  
+  /* crypto keys */
+  CRYPTO_BEGIN("-----BEGIN"),
+  CRYPTO_END("-----END"),
+
+  /* descriptor keys (in alphabetic order) */
+  A("a"),
+  ACCEPT("accept"),
+  ALLOW_SINGLE_HOP_EXITS("allow-single-hop-exits"),
+  BANDWIDTH("bandwidth"),
+  BANDWIDTH_WEIGHTS("bandwidth-weights"),
+  BRIDGE_IPS("bridge-ips"),
+  BRIDGE_IP_TRANSPORTS("bridge-ip-transports"),
+  BRIDGE_IP_VERSIONS("bridge-ip-versions"),
+  BRIDGE_POOL_ASSIGNMENT("bridge-pool-assignment"),
+  BRIDGE_STATS_END("bridge-stats-end"),
+  CACHES_EXTRA_INFO("caches-extra-info"),
+  CELL_CIRCUITS_PER_DECILE("cell-circuits-per-decile"),
+  CELL_PROCESSED_CELLS("cell-processed-cells"),
+  CELL_QUEUED_CELLS("cell-queued-cells"),
+  CELL_STATS_END("cell-stats-end"),
+  CELL_TIME_IN_QUEUE("cell-time-in-queue"),
+  CLIENT_VERSIONS("client-versions"),
+  CONN_BI_DIRECT("conn-bi-direct"),
+  CONSENSUS_METHOD("consensus-method"),
+  CONSENSUS_METHODS("consensus-methods"),
+  CONTACT("contact"),
+  DIRCACHEPORT("dircacheport"),
+  DIRECTORY_FOOTER("directory-footer"),
+  DIRECTORY_SIGNATURE("directory-signature"),
+  DIRREQ_READ_HISTORY("dirreq-read-history"),
+  DIRREQ_STATS_END("dirreq-stats-end"),
+  DIRREQ_V2_DIRECT_DL("dirreq-v2-direct-dl"),
+  DIRREQ_V2_IPS("dirreq-v2-ips"),
+  DIRREQ_V2_REQS("dirreq-v2-reqs"),
+  DIRREQ_V2_RESP("dirreq-v2-resp"),
+  DIRREQ_V2_SHARE("dirreq-v2-share"),
+  DIRREQ_V2_TUNNELED_DL("dirreq-v2-tunneled-dl"),
+  DIRREQ_V3_DIRECT_DL("dirreq-v3-direct-dl"),
+  DIRREQ_V3_IPS("dirreq-v3-ips"),
+  DIRREQ_V3_REQS("dirreq-v3-reqs"),
+  DIRREQ_V3_RESP("dirreq-v3-resp"),
+  DIRREQ_V3_SHARE("dirreq-v3-share"),
+  DIRREQ_V3_TUNNELED_DL("dirreq-v3-tunneled-dl"),
+  DIRREQ_WRITE_HISTORY("dirreq-write-history"),
+  DIR_ADDRESS("dir-address"),
+  DIR_IDENTITY_KEY("dir-identity-key"),
+  DIR_KEY_CERTIFICATE_VERSION("dir-key-certificate-version"),
+  DIR_KEY_CERTIFICATION("dir-key-certification"),
+  DIR_KEY_CROSSCERT("dir-key-crosscert"),
+  DIR_KEY_EXPIRES("dir-key-expires"),
+  DIR_KEY_PUBLISHED("dir-key-published"),
+  DIR_OPTIONS("dir-options"),
+  DIR_SIGNING_KEY("dir-signing-key"),
+  DIR_SOURCE("dir-source"),
+  ENTRY_IPS("entry-ips"),
+  ENTRY_STATS_END("entry-stats-end"),
+  EVENTDNS("eventdns"),
+  EXIT_KIBIBYTES_READ("exit-kibibytes-read"),
+  EXIT_KIBIBYTES_WRITTEN("exit-kibibytes-written"),
+  EXIT_STATS_END("exit-stats-end"),
+  EXIT_STREAMS_OPENED("exit-streams-opened"),
+  EXTRA_INFO("extra-info"),
+  EXTRA_INFO_DIGEST("extra-info-digest"),
+  FAMILY("family"),
+  FINGERPRINT("fingerprint"),
+  FLAG_THRESHOLDS("flag-thresholds"),
+  FRESH_UNTIL("fresh-until"),
+  GEOIP6_DB_DIGEST("geoip6-db-digest"),
+  GEOIP_CLIENT_ORIGINS("geoip-client-origins"),
+  GEOIP_DB_DIGEST("geoip-db-digest"),
+  GEOIP_START_TIME("geoip-start-time"),
+  HIBERNATING("hibernating"),
+  HIDDEN_SERVICE_DIR("hidden-service-dir"),
+  HIDSERV_DIR_ONIONS_SEEN("hidserv-dir-onions-seen"),
+  HIDSERV_REND_RELAYED_CELLS("hidserv-rend-relayed-cells"),
+  HIDSERV_STATS_END("hidserv-stats-end"),
+  ID("id"),
+  IDENTITY_ED25519("identity-ed25519"),
+  IPV6_POLICY("ipv6-policy"),
+  KNOWN_FLAGS("known-flags"),
+  LEGACY_DIR_KEY("legacy-dir-key"),
+  LEGACY_KEY("legacy-key"),
+  M("m"),
+  MASTER_KEY_ED25519("master-key-ed25519"),
+  NETWORK_STATUS_VERSION("network-status-version"),
+  NTOR_ONION_KEY("ntor-onion-key"),
+  NTOR_ONION_KEY_CROSSCERT("ntor-onion-key-crosscert"),
+  ONION_KEY("onion-key"),
+  ONION_KEY_CROSSCERT("onion-key-crosscert"),
+  OPT("opt"),
+  OR_ADDRESS("or-address"),
+  P("p"),
+  P6("p6"),
+  PACKAGE("package"),
+  PADDING_COUNTS("padding-counts"),
+  PARAMS("params"),
+  PLATFORM("platform"),
+  PR("pr"),
+  PROTO("proto"),
+  PROTOCOLS("protocols"),
+  PUBLISHED("published"),
+  R("r"),
+  READ_HISTORY("read-history"),
+  RECOMMENDED_CLIENT_PROTOCOLS("recommended-client-protocols"),
+  RECOMMENDED_RELAY_PROTOCOLS("recommended-relay-protocols"),
+  RECOMMENDED_SOFTWARE("recommended-software"),
+  REJECT("reject"),
+  REQUIRED_CLIENT_PROTOCOLS("required-client-protocols"),
+  REQUIRED_RELAY_PROTOCOLS("required-relay-protocols"),
+  ROUTER("router"),
+  ROUTER_DIGEST("router-digest"),
+  ROUTER_DIGEST_SHA256("router-digest-sha256"),
+  ROUTER_SIGNATURE("router-signature"),
+  ROUTER_SIG_ED25519("router-sig-ed25519"),
+  ROUTER_STATUS("router-status"),
+  RUNNING_ROUTERS("running-routers"),
+  S("s"),
+  SERVER_VERSIONS("server-versions"),
+  SHARED_RAND_COMMIT("shared-rand-commit"),
+  SHARED_RAND_CURRENT_VALUE("shared-rand-current-value"),
+  SHARED_RAND_PARTICIPATE("shared-rand-participate"),
+  SHARED_RAND_PREVIOUS_VALUE("shared-rand-previous-value"),
+  SIGNED_DIRECTORY("signed-directory"),
+  SIGNING_KEY("signing-key"),
+  TRANSPORT("transport"),
+  TUNNELLED_DIR_SERVER("tunnelled-dir-server"),
+  UPTIME("uptime"),
+  V("v"),
+  VALID_AFTER("valid-after"),
+  VALID_UNTIL("valid-until"),
+  VOTE_DIGEST("vote-digest"),
+  VOTE_STATUS("vote-status"),
+  VOTING_DELAY("voting-delay"),
+  W("w"),
+  WRITE_HISTORY("write-history");
+
+  /** The keyword as it appears in descriptors. */
+  public final String keyword;
+
+  private static final Map<String, Key> keywordMap = new HashMap<>();
+  static {
+    for (Key key : values()) {
+      keywordMap.put(key.keyword, key);
+    }
+    keywordMap.remove(INVALID.keyword);
+    keywordMap.remove(EMPTY.keyword);
+  }
+
+  private Key(String keyword) {
+    this.keyword = keyword;
+  }
+
+  /** Retrieve a Key for a keyword.
+   *  Returns Key.INVALID for non-existing keywords. */
+  public static Key get(String keyword) {
+    Key res = INVALID;
+    try {
+      res = keywordMap.get(keyword);
+    } catch (Throwable th) {
+      res = INVALID;
+    }
+    if (null == res) {
+      res = INVALID;
+    }
+    return res;
+  }
+}
diff --git a/src/main/java/org/torproject/descriptor/impl/MicrodescriptorImpl.java b/src/main/java/org/torproject/descriptor/impl/MicrodescriptorImpl.java
index 5c94371..e8329cc 100644
--- a/src/main/java/org/torproject/descriptor/impl/MicrodescriptorImpl.java
+++ b/src/main/java/org/torproject/descriptor/impl/MicrodescriptorImpl.java
@@ -11,7 +11,7 @@ import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.HashSet;
+import java.util.EnumSet;
 import java.util.List;
 import java.util.Scanner;
 import java.util.Set;
@@ -28,7 +28,7 @@ public class MicrodescriptorImpl extends DescriptorImpl
     List<Microdescriptor> parsedDescriptors = new ArrayList<>();
     List<byte[]> splitDescriptorsBytes =
         DescriptorImpl.splitRawDescriptorBytes(descriptorsBytes,
-        "onion-key\n");
+        Key.ONION_KEY + NL);
     for (byte[] descriptorBytes : splitDescriptorsBytes) {
       Microdescriptor parsedDescriptor =
           new MicrodescriptorImpl(descriptorBytes,
@@ -44,21 +44,19 @@ public class MicrodescriptorImpl extends DescriptorImpl
     super(descriptorBytes, failUnrecognizedDescriptorLines, false);
     this.parseDescriptorBytes();
     this.calculateDigest();
-    Set<String> exactlyOnceKeywords = new HashSet<>(Arrays.asList(
-        "onion-key".split(",")));
-    this.checkExactlyOnceKeywords(exactlyOnceKeywords);
-    Set<String> atMostOnceKeywords = new HashSet<>(Arrays.asList((
-        "ntor-onion-key,family,p,p6,id").split(",")));
-    this.checkAtMostOnceKeywords(atMostOnceKeywords);
-    this.checkFirstKeyword("onion-key");
-    this.clearParsedKeywords();
+    this.checkExactlyOnceKeys(EnumSet.of(Key.ONION_KEY));
+    Set<Key> atMostOnceKeys = EnumSet.of(
+        Key.NTOR_ONION_KEY, Key.FAMILY, Key.P, Key.P6, Key.ID);
+    this.checkAtMostOnceKeys(atMostOnceKeys);
+    this.checkFirstKey(Key.ONION_KEY);
+    this.clearParsedKeys();
     return;
   }
 
   private void parseDescriptorBytes() throws DescriptorParseException {
     Scanner scanner = new Scanner(new String(this.rawDescriptorBytes))
-        .useDelimiter("\n");
-    String nextCrypto = "";
+        .useDelimiter(NL);
+    Key nextCrypto = Key.EMPTY;
     StringBuilder crypto = null;
     while (scanner.hasNext()) {
       String line = scanner.next();
@@ -66,49 +64,49 @@ public class MicrodescriptorImpl extends DescriptorImpl
         continue;
       }
       String[] parts = line.split("[ \t]+");
-      String keyword = parts[0];
-      switch (keyword) {
-        case "onion-key":
+      Key key = Key.get(parts[0]);
+      switch (key) {
+        case ONION_KEY:
           this.parseOnionKeyLine(line, parts);
-          nextCrypto = "onion-key";
+          nextCrypto = key;
           break;
-        case "ntor-onion-key":
+        case NTOR_ONION_KEY:
           this.parseNtorOnionKeyLine(line, parts);
           break;
-        case "a":
+        case A:
           this.parseALine(line, parts);
           break;
-        case "family":
+        case FAMILY:
           this.parseFamilyLine(line, parts);
           break;
-        case "p":
+        case P:
           this.parsePLine(line, parts);
           break;
-        case "p6":
+        case P6:
           this.parseP6Line(line, parts);
           break;
-        case "id":
+        case ID:
           this.parseIdLine(line, parts);
           break;
-        case "-----BEGIN":
+        case CRYPTO_BEGIN:
           crypto = new StringBuilder();
-          crypto.append(line).append("\n");
+          crypto.append(line).append(NL);
           break;
-        case "-----END":
-          crypto.append(line).append("\n");
+        case CRYPTO_END:
+          crypto.append(line).append(NL);
           String cryptoString = crypto.toString();
           crypto = null;
-          if (nextCrypto.equals("onion-key")) {
+          if (nextCrypto.equals(Key.ONION_KEY)) {
             this.onionKey = cryptoString;
           } else {
             throw new DescriptorParseException("Unrecognized crypto "
                 + "block in microdescriptor.");
           }
-          nextCrypto = "";
+          nextCrypto = Key.EMPTY;
           break;
         default:
           if (crypto != null) {
-            crypto.append(line).append("\n");
+            crypto.append(line).append(NL);
           } else {
             ParseHelper.parseKeyword(line, parts[0]);
             if (this.failUnrecognizedDescriptorLines) {
diff --git a/src/main/java/org/torproject/descriptor/impl/NetworkStatusEntryImpl.java b/src/main/java/org/torproject/descriptor/impl/NetworkStatusEntryImpl.java
index 152546a..cb4eca8 100644
--- a/src/main/java/org/torproject/descriptor/impl/NetworkStatusEntryImpl.java
+++ b/src/main/java/org/torproject/descriptor/impl/NetworkStatusEntryImpl.java
@@ -3,11 +3,15 @@
 
 package org.torproject.descriptor.impl;
 
+import static org.torproject.descriptor.impl.DescriptorImpl.NL;
+import static org.torproject.descriptor.impl.DescriptorImpl.SP;
+
 import org.torproject.descriptor.DescriptorParseException;
 import org.torproject.descriptor.NetworkStatusEntry;
 
 import java.util.ArrayList;
 import java.util.BitSet;
+import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -46,34 +50,25 @@ public class NetworkStatusEntryImpl implements NetworkStatusEntry {
     this.microdescConsensus = microdescConsensus;
     this.failUnrecognizedDescriptorLines =
         failUnrecognizedDescriptorLines;
-    this.initializeKeywords();
     this.parseStatusEntryBytes();
-    this.clearAtMostOnceKeywords();
+    this.clearAtMostOnceKeys();
   }
 
-  private SortedSet<String> atMostOnceKeywords;
-
-  private void initializeKeywords() {
-    this.atMostOnceKeywords = new TreeSet<>();
-    this.atMostOnceKeywords.add("s");
-    this.atMostOnceKeywords.add("v");
-    this.atMostOnceKeywords.add("pr");
-    this.atMostOnceKeywords.add("w");
-    this.atMostOnceKeywords.add("p");
-  }
+  private Set<Key> atMostOnceKeys = EnumSet.of(
+      Key.S, Key.V, Key.PR, Key.W, Key.P);
 
-  private void parsedAtMostOnceKeyword(String keyword)
+  private void parsedAtMostOnceKey(Key key)
       throws DescriptorParseException {
-    if (!this.atMostOnceKeywords.contains(keyword)) {
-      throw new DescriptorParseException("Duplicate '" + keyword
+    if (!this.atMostOnceKeys.contains(key)) {
+      throw new DescriptorParseException("Duplicate '" + key.keyword
           + "' line in status entry.");
     }
-    this.atMostOnceKeywords.remove(keyword);
+    this.atMostOnceKeys.remove(key);
   }
 
   private void parseStatusEntryBytes() throws DescriptorParseException {
     Scanner scanner = new Scanner(new String(this.statusEntryBytes))
-        .useDelimiter("\n");
+        .useDelimiter(NL);
     String line = null;
     if (!scanner.hasNext() || !(line = scanner.next()).startsWith("r ")) {
       throw new DescriptorParseException("Status entry must start with "
@@ -83,32 +78,33 @@ public class NetworkStatusEntryImpl implements NetworkStatusEntry {
     this.parseRLine(line, rlineParts);
     while (scanner.hasNext()) {
       line = scanner.next();
-      String[] parts = !line.startsWith("opt ") ? line.split("[ \t]+")
-          : line.substring("opt ".length()).split("[ \t]+");
-      String keyword = parts[0];
-      switch (keyword) {
-        case "a":
+      String[] parts = !line.startsWith(Key.OPT.keyword + SP)
+          ? line.split("[ \t]+")
+          : line.substring(Key.OPT.keyword.length() + 1).split("[ \t]+");
+      Key key = Key.get(parts[0]);
+      switch (key) {
+        case A:
           this.parseALine(line, parts);
           break;
-        case "s":
+        case S:
           this.parseSLine(line, parts);
           break;
-        case "v":
+        case V:
           this.parseVLine(line, parts);
           break;
-        case "pr":
+        case PR:
           this.parsePrLine(line, parts);
           break;
-        case "w":
+        case W:
           this.parseWLine(line, parts);
           break;
-        case "p":
+        case P:
           this.parsePLine(line, parts);
           break;
-        case "m":
+        case M:
           this.parseMLine(line, parts);
           break;
-        case "id":
+        case ID:
           this.parseIdLine(line, parts);
           break;
         default:
@@ -167,7 +163,7 @@ public class NetworkStatusEntryImpl implements NetworkStatusEntry {
 
   private void parseSLine(String line, String[] parts)
       throws DescriptorParseException {
-    this.parsedAtMostOnceKeyword("s");
+    this.parsedAtMostOnceKey(Key.S);
     BitSet flags = new BitSet(flagIndexes.size());
     for (int i = 1; i < parts.length; i++) {
       String flag = parts[i];
@@ -182,9 +178,9 @@ public class NetworkStatusEntryImpl implements NetworkStatusEntry {
 
   private void parseVLine(String line, String[] parts)
       throws DescriptorParseException {
-    this.parsedAtMostOnceKeyword("v");
+    this.parsedAtMostOnceKey(Key.V);
     String noOptLine = line;
-    if (noOptLine.startsWith("opt ")) {
+    if (noOptLine.startsWith(Key.OPT.keyword + SP)) {
       noOptLine = noOptLine.substring(4);
     }
     if (noOptLine.length() < 3) {
@@ -197,13 +193,13 @@ public class NetworkStatusEntryImpl implements NetworkStatusEntry {
 
   private void parsePrLine(String line, String[] parts)
       throws DescriptorParseException {
-    this.parsedAtMostOnceKeyword("pr");
+    this.parsedAtMostOnceKey(Key.PR);
     this.protocols = ParseHelper.parseProtocolVersions(line, line, parts);
   }
 
   private void parseWLine(String line, String[] parts)
       throws DescriptorParseException {
-    this.parsedAtMostOnceKeyword("w");
+    this.parsedAtMostOnceKey(Key.W);
     SortedMap<String, Integer> pairs =
         ParseHelper.parseKeyValueIntegerPairs(line, parts, 1, "=");
     if (pairs.isEmpty()) {
@@ -225,7 +221,7 @@ public class NetworkStatusEntryImpl implements NetworkStatusEntry {
 
   private void parsePLine(String line, String[] parts)
       throws DescriptorParseException {
-    this.parsedAtMostOnceKeyword("p");
+    this.parsedAtMostOnceKey(Key.P);
     boolean isValid = true;
     if (parts.length != 3) {
       isValid = false;
@@ -280,8 +276,8 @@ public class NetworkStatusEntryImpl implements NetworkStatusEntry {
     }
   }
 
-  private void clearAtMostOnceKeywords() {
-    this.atMostOnceKeywords = null;
+  private void clearAtMostOnceKeys() {
+    this.atMostOnceKeys = null;
   }
 
   private String nickname;
diff --git a/src/main/java/org/torproject/descriptor/impl/NetworkStatusImpl.java b/src/main/java/org/torproject/descriptor/impl/NetworkStatusImpl.java
index e3891a6..f67c32a 100644
--- a/src/main/java/org/torproject/descriptor/impl/NetworkStatusImpl.java
+++ b/src/main/java/org/torproject/descriptor/impl/NetworkStatusImpl.java
@@ -34,15 +34,16 @@ public abstract class NetworkStatusImpl extends DescriptorImpl {
       throw new DescriptorParseException("Descriptor is empty.");
     }
     String descriptorString = new String(rawDescriptorBytes);
-    int firstRIndex = this.findFirstIndexOfKeyword(descriptorString, "r");
+    int firstRIndex = this.findFirstIndexOfKeyword(descriptorString,
+        Key.R.keyword);
     int endIndex = descriptorString.length();
     int firstDirectorySignatureIndex = this.findFirstIndexOfKeyword(
-        descriptorString, "directory-signature");
+        descriptorString, Key.DIRECTORY_SIGNATURE.keyword);
     if (firstDirectorySignatureIndex < 0) {
       firstDirectorySignatureIndex = endIndex;
     }
     int directoryFooterIndex = this.findFirstIndexOfKeyword(
-        descriptorString, "directory-footer");
+        descriptorString, Key.DIRECTORY_FOOTER.keyword);
     if (directoryFooterIndex < 0) {
       directoryFooterIndex = firstDirectorySignatureIndex;
     }
@@ -50,7 +51,8 @@ public abstract class NetworkStatusImpl extends DescriptorImpl {
       firstRIndex = directoryFooterIndex;
     }
     int firstDirSourceIndex = !containsDirSourceEntries ? -1
-        : this.findFirstIndexOfKeyword(descriptorString, "dir-source");
+        : this.findFirstIndexOfKeyword(descriptorString,
+        Key.DIR_SOURCE.keyword);
     if (firstDirSourceIndex < 0) {
       firstDirSourceIndex = firstRIndex;
     }
@@ -79,10 +81,10 @@ public abstract class NetworkStatusImpl extends DescriptorImpl {
       String keyword) {
     if (descriptorString.startsWith(keyword)) {
       return 0;
-    } else if (descriptorString.contains("\n" + keyword + " ")) {
-      return descriptorString.indexOf("\n" + keyword + " ") + 1;
-    } else if (descriptorString.contains("\n" + keyword + "\n")) {
-      return descriptorString.indexOf("\n" + keyword + "\n") + 1;
+    } else if (descriptorString.contains(NL + keyword + SP)) {
+      return descriptorString.indexOf(NL + keyword + SP) + 1;
+    } else if (descriptorString.contains(NL + keyword + NL)) {
+      return descriptorString.indexOf(NL + keyword + NL) + 1;
     } else {
       return -1;
     }
@@ -99,7 +101,8 @@ public abstract class NetworkStatusImpl extends DescriptorImpl {
   private void parseDirSourceBytes(String descriptorString, int start,
       int end) throws DescriptorParseException {
     List<byte[]> splitDirSourceBytes =
-        this.splitByKeyword(descriptorString, "dir-source", start, end);
+        this.splitByKeyword(
+            descriptorString, Key.DIR_SOURCE.keyword, start, end);
     for (byte[] dirSourceBytes : splitDirSourceBytes) {
       this.parseDirSource(dirSourceBytes);
     }
@@ -108,7 +111,7 @@ public abstract class NetworkStatusImpl extends DescriptorImpl {
   private void parseStatusEntryBytes(String descriptorString, int start,
       int end) throws DescriptorParseException {
     List<byte[]> splitStatusEntryBytes =
-        this.splitByKeyword(descriptorString, "r", start, end);
+        this.splitByKeyword(descriptorString, Key.R.keyword, start, end);
     for (byte[] statusEntryBytes : splitStatusEntryBytes) {
       this.parseStatusEntry(statusEntryBytes);
     }
@@ -125,7 +128,7 @@ public abstract class NetworkStatusImpl extends DescriptorImpl {
   private void parseDirectorySignatureBytes(String descriptorString,
       int start, int end) throws DescriptorParseException {
     List<byte[]> splitDirectorySignatureBytes = this.splitByKeyword(
-        descriptorString, "directory-signature", start, end);
+        descriptorString, Key.DIRECTORY_SIGNATURE.keyword, start, end);
     for (byte[] directorySignatureBytes : splitDirectorySignatureBytes) {
       this.parseDirectorySignature(directorySignatureBytes);
     }
@@ -136,9 +139,9 @@ public abstract class NetworkStatusImpl extends DescriptorImpl {
     List<byte[]> splitParts = new ArrayList<>();
     int from = start;
     while (from < end) {
-      int to = descriptorString.indexOf("\n" + keyword + " ", from);
+      int to = descriptorString.indexOf(NL + keyword + SP, from);
       if (to < 0) {
-        to = descriptorString.indexOf("\n" + keyword + "\n", from);
+        to = descriptorString.indexOf(NL + keyword + NL, from);
       }
       if (to < 0) {
         to = end;
diff --git a/src/main/java/org/torproject/descriptor/impl/RelayDirectoryImpl.java b/src/main/java/org/torproject/descriptor/impl/RelayDirectoryImpl.java
index fc8f07b..fae54f4 100644
--- a/src/main/java/org/torproject/descriptor/impl/RelayDirectoryImpl.java
+++ b/src/main/java/org/torproject/descriptor/impl/RelayDirectoryImpl.java
@@ -12,8 +12,7 @@ import java.io.UnsupportedEncodingException;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashSet;
+import java.util.EnumSet;
 import java.util.List;
 import java.util.Scanner;
 import java.util.Set;
@@ -29,7 +28,7 @@ public class RelayDirectoryImpl extends DescriptorImpl
     List<RelayDirectory> parsedDirectories = new ArrayList<>();
     List<byte[]> splitDirectoriesBytes =
         DescriptorImpl.splitRawDescriptorBytes(directoriesBytes,
-        "signed-directory\n");
+        Key.SIGNED_DIRECTORY.keyword + NL);
     for (byte[] directoryBytes : splitDirectoriesBytes) {
       RelayDirectory parsedDirectory =
           new RelayDirectoryImpl(directoryBytes,
@@ -45,28 +44,28 @@ public class RelayDirectoryImpl extends DescriptorImpl
     super(directoryBytes, failUnrecognizedDescriptorLines, true);
     this.splitAndParseParts(rawDescriptorBytes);
     this.calculateDigest();
-    Set<String> exactlyOnceKeywords = new HashSet<>(Arrays.asList((
-        "signed-directory,recommended-software,"
-        + "directory-signature").split(",")));
-    this.checkExactlyOnceKeywords(exactlyOnceKeywords);
-    Set<String> atMostOnceKeywords = new HashSet<>(Arrays.asList(
-        "dir-signing-key,running-routers,router-status".split(",")));
-    this.checkAtMostOnceKeywords(atMostOnceKeywords);
-    this.checkFirstKeyword("signed-directory");
-    this.clearParsedKeywords();
+    Set<Key> exactlyOnceKeys = EnumSet.of(
+        Key.SIGNED_DIRECTORY, Key.RECOMMENDED_SOFTWARE,
+        Key.DIRECTORY_SIGNATURE);
+    this.checkExactlyOnceKeys(exactlyOnceKeys);
+    Set<Key> atMostOnceKeys = EnumSet.of(
+        Key.DIR_SIGNING_KEY, Key.RUNNING_ROUTERS, Key.ROUTER_STATUS);
+    this.checkAtMostOnceKeys(atMostOnceKeys);
+    this.checkFirstKey(Key.SIGNED_DIRECTORY);
+    this.clearParsedKeys();
   }
 
   private void calculateDigest() throws DescriptorParseException {
     try {
       String ascii = new String(this.getRawDescriptorBytes(), "US-ASCII");
-      String startToken = "signed-directory\n";
-      String sigToken = "\ndirectory-signature ";
+      String startToken = Key.SIGNED_DIRECTORY.keyword + NL;
+      String sigToken = NL + Key.DIRECTORY_SIGNATURE.keyword + SP;
       if (!ascii.contains(sigToken)) {
         return;
       }
       int start = ascii.indexOf(startToken);
       int sig = ascii.indexOf(sigToken) + sigToken.length();
-      sig = ascii.indexOf("\n", sig) + 1;
+      sig = ascii.indexOf(NL, sig) + 1;
       if (start >= 0 && sig >= 0 && sig > start) {
         byte[] forDigest = new byte[sig - start];
         System.arraycopy(this.getRawDescriptorBytes(), start,
@@ -94,9 +93,9 @@ public class RelayDirectoryImpl extends DescriptorImpl
     String descriptorString = new String(rawDescriptorBytes);
     int startIndex = 0;
     int firstRouterIndex = this.findFirstIndexOfKeyword(descriptorString,
-        "router");
+        Key.ROUTER.keyword);
     int directorySignatureIndex = this.findFirstIndexOfKeyword(
-        descriptorString, "directory-signature");
+        descriptorString, Key.DIRECTORY_SIGNATURE.keyword);
     int endIndex = descriptorString.length();
     if (directorySignatureIndex < 0) {
       directorySignatureIndex = endIndex;
@@ -122,10 +121,10 @@ public class RelayDirectoryImpl extends DescriptorImpl
       String keyword) {
     if (descriptorString.startsWith(keyword)) {
       return 0;
-    } else if (descriptorString.contains("\n" + keyword + " ")) {
-      return descriptorString.indexOf("\n" + keyword + " ") + 1;
-    } else if (descriptorString.contains("\n" + keyword + "\n")) {
-      return descriptorString.indexOf("\n" + keyword + "\n") + 1;
+    } else if (descriptorString.contains(NL + keyword + SP)) {
+      return descriptorString.indexOf(NL + keyword + SP) + 1;
+    } else if (descriptorString.contains(NL + keyword + NL)) {
+      return descriptorString.indexOf(NL + keyword + NL) + 1;
     } else {
       return -1;
     }
@@ -142,7 +141,7 @@ public class RelayDirectoryImpl extends DescriptorImpl
   private void parseServerDescriptorBytes(String descriptorString,
       int start, int end) throws DescriptorParseException {
     List<byte[]> splitServerDescriptorBytes =
-        this.splitByKeyword(descriptorString, "router", start, end);
+        this.splitByKeyword(descriptorString, Key.ROUTER.keyword, start, end);
     for (byte[] statusEntryBytes : splitServerDescriptorBytes) {
       this.parseServerDescriptor(statusEntryBytes);
     }
@@ -162,9 +161,9 @@ public class RelayDirectoryImpl extends DescriptorImpl
     List<byte[]> splitParts = new ArrayList<>();
     int from = start;
     while (from < end) {
-      int to = descriptorString.indexOf("\n" + keyword + " ", from);
+      int to = descriptorString.indexOf(NL + keyword + SP, from);
       if (to < 0) {
-        to = descriptorString.indexOf("\n" + keyword + "\n", from);
+        to = descriptorString.indexOf(NL + keyword + NL, from);
       }
       if (to < 0) {
         to = end;
@@ -187,9 +186,9 @@ public class RelayDirectoryImpl extends DescriptorImpl
 
   private void parseHeader(byte[] headerBytes)
       throws DescriptorParseException {
-    Scanner scanner = new Scanner(new String(headerBytes)).useDelimiter("\n");
+    Scanner scanner = new Scanner(new String(headerBytes)).useDelimiter(NL);
     String publishedLine = null;
-    String nextCrypto = "";
+    Key nextCrypto = Key.EMPTY;
     String runningRoutersLine = null;
     String routerStatusLine = null;
     StringBuilder crypto = null;
@@ -198,15 +197,15 @@ public class RelayDirectoryImpl extends DescriptorImpl
       if (line.isEmpty() || line.startsWith("@")) {
         continue;
       }
-      String lineNoOpt = line.startsWith("opt ")
-          ? line.substring("opt ".length()) : line;
+      String lineNoOpt = line.startsWith(Key.OPT.keyword + SP)
+          ? line.substring(Key.OPT.keyword.length() + 1) : line;
       String[] partsNoOpt = lineNoOpt.split("[ \t]+");
-      String keyword = partsNoOpt[0];
-      switch (keyword) {
-        case "signed-directory":
+      Key key = Key.get(partsNoOpt[0]);
+      switch (key) {
+        case SIGNED_DIRECTORY:
           this.parseSignedDirectoryLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "published":
+        case PUBLISHED:
           if (publishedLine != null) {
             throw new DescriptorParseException("Keyword 'published' is "
                 + "contained more than once, but must be contained "
@@ -215,39 +214,39 @@ public class RelayDirectoryImpl extends DescriptorImpl
             publishedLine = line;
           }
           break;
-        case "dir-signing-key":
+        case DIR_SIGNING_KEY:
           this.parseDirSigningKeyLine(line, lineNoOpt, partsNoOpt);
-          nextCrypto = "dir-signing-key";
+          nextCrypto = key;
           break;
-        case "recommended-software":
+        case RECOMMENDED_SOFTWARE:
           this.parseRecommendedSoftwareLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "running-routers":
+        case RUNNING_ROUTERS:
           runningRoutersLine = line;
           break;
-        case "router-status":
+        case ROUTER_STATUS:
           routerStatusLine = line;
           break;
-        case "-----BEGIN":
+        case CRYPTO_BEGIN:
           crypto = new StringBuilder();
-          crypto.append(line).append("\n");
+          crypto.append(line).append(NL);
           break;
-        case "-----END":
-          crypto.append(line).append("\n");
+        case CRYPTO_END:
+          crypto.append(line).append(NL);
           String cryptoString = crypto.toString();
           crypto = null;
-          if (nextCrypto.equals("dir-signing-key")
+          if (nextCrypto.equals(Key.DIR_SIGNING_KEY)
               && this.dirSigningKey == null) {
             this.dirSigningKey = cryptoString;
           } else {
             throw new DescriptorParseException("Unrecognized crypto "
                 + "block in v1 directory.");
           }
-          nextCrypto = "";
+          nextCrypto = Key.EMPTY;
           break;
         default:
           if (crypto != null) {
-            crypto.append(line).append("\n");
+            crypto.append(line).append(NL);
           } else {
             if (this.failUnrecognizedDescriptorLines) {
               throw new DescriptorParseException("Unrecognized line '"
@@ -265,15 +264,17 @@ public class RelayDirectoryImpl extends DescriptorImpl
       throw new DescriptorParseException("Keyword 'published' is "
           + "contained 0 times, but must be contained exactly once.");
     } else {
-      String publishedLineNoOpt = publishedLine.startsWith("opt ")
-          ? publishedLine.substring("opt ".length()) : publishedLine;
+      String publishedLineNoOpt = publishedLine.startsWith(Key.OPT.keyword + SP)
+          ? publishedLine.substring(Key.OPT.keyword.length() + 1)
+          : publishedLine;
       String[] publishedPartsNoOpt = publishedLineNoOpt.split("[ \t]+");
       this.parsePublishedLine(publishedLine, publishedLineNoOpt,
           publishedPartsNoOpt);
     }
     if (routerStatusLine != null) {
-      String routerStatusLineNoOpt = routerStatusLine.startsWith("opt ")
-          ? routerStatusLine.substring("opt ".length())
+      String routerStatusLineNoOpt =
+          routerStatusLine.startsWith(Key.OPT.keyword + SP)
+          ? routerStatusLine.substring(Key.OPT.keyword.length() + 1)
           : routerStatusLine;
       String[] routerStatusPartsNoOpt =
           routerStatusLineNoOpt.split("[ \t]+");
@@ -281,8 +282,8 @@ public class RelayDirectoryImpl extends DescriptorImpl
           routerStatusPartsNoOpt);
     } else if (runningRoutersLine != null) {
       String runningRoutersLineNoOpt =
-          runningRoutersLine.startsWith("opt ")
-          ? runningRoutersLine.substring("opt ".length())
+          runningRoutersLine.startsWith(Key.OPT.keyword + SP)
+          ? runningRoutersLine.substring(Key.OPT.keyword.length() + 1)
           : runningRoutersLine;
       String[] runningRoutersPartsNoOpt =
           runningRoutersLineNoOpt.split("[ \t]+");
@@ -308,39 +309,39 @@ public class RelayDirectoryImpl extends DescriptorImpl
   private void parseDirectorySignature(byte[] directorySignatureBytes)
       throws DescriptorParseException {
     Scanner scanner = new Scanner(new String(directorySignatureBytes))
-        .useDelimiter("\n");
-    String nextCrypto = "";
+        .useDelimiter(NL);
+    Key nextCrypto = Key.EMPTY;
     StringBuilder crypto = null;
     while (scanner.hasNext()) {
       String line = scanner.next();
-      String lineNoOpt = line.startsWith("opt ")
-          ? line.substring("opt ".length()) : line;
+      String lineNoOpt = line.startsWith(Key.OPT.keyword + SP)
+          ? line.substring(Key.OPT.keyword.length() + 1) : line;
       String[] partsNoOpt = lineNoOpt.split("[ \t]+");
-      String keyword = partsNoOpt[0];
-      switch (keyword) {
-        case "directory-signature":
+      Key key = Key.get(partsNoOpt[0]);
+      switch (key) {
+        case DIRECTORY_SIGNATURE:
           this.parseDirectorySignatureLine(line, lineNoOpt, partsNoOpt);
-          nextCrypto = "directory-signature";
+          nextCrypto = key;
           break;
-        case "-----BEGIN":
+        case CRYPTO_BEGIN:
           crypto = new StringBuilder();
-          crypto.append(line).append("\n");
+          crypto.append(line).append(NL);
           break;
-        case "-----END":
-          crypto.append(line).append("\n");
+        case CRYPTO_END:
+          crypto.append(line).append(NL);
           String cryptoString = crypto.toString();
           crypto = null;
-          if (nextCrypto.equals("directory-signature")) {
+          if (nextCrypto.equals(Key.DIRECTORY_SIGNATURE)) {
             this.directorySignature = cryptoString;
           } else {
             throw new DescriptorParseException("Unrecognized crypto "
                 + "block in v2 network status.");
           }
-          nextCrypto = "";
+          nextCrypto = Key.EMPTY;
           break;
         default:
           if (crypto != null) {
-            crypto.append(line).append("\n");
+            crypto.append(line).append(NL);
           } else if (this.failUnrecognizedDescriptorLines) {
             throw new DescriptorParseException("Unrecognized line '"
                 + line + "' in v2 network status.");
@@ -356,7 +357,7 @@ public class RelayDirectoryImpl extends DescriptorImpl
 
   private void parseSignedDirectoryLine(String line, String lineNoOpt,
       String[] partsNoOpt) throws DescriptorParseException {
-    if (!lineNoOpt.equals("signed-directory")) {
+    if (!lineNoOpt.equals(Key.SIGNED_DIRECTORY.keyword)) {
       throw new DescriptorParseException("Illegal line '" + line + "'.");
     }
   }
@@ -379,11 +380,11 @@ public class RelayDirectoryImpl extends DescriptorImpl
       sb.append("-----BEGIN RSA PUBLIC KEY-----\n");
       String keyString = partsNoOpt[1];
       while (keyString.length() > 64) {
-        sb.append(keyString.substring(0, 64)).append("\n");
+        sb.append(keyString.substring(0, 64)).append(NL);
         keyString = keyString.substring(64);
       }
       if (keyString.length() > 0) {
-        sb.append(keyString).append("\n");
+        sb.append(keyString).append(NL);
       }
       sb.append("-----END RSA PUBLIC KEY-----\n");
       this.dirSigningKey = sb.toString();
diff --git a/src/main/java/org/torproject/descriptor/impl/RelayExtraInfoDescriptorImpl.java b/src/main/java/org/torproject/descriptor/impl/RelayExtraInfoDescriptorImpl.java
index 3ec4869..6ee86b1 100644
--- a/src/main/java/org/torproject/descriptor/impl/RelayExtraInfoDescriptorImpl.java
+++ b/src/main/java/org/torproject/descriptor/impl/RelayExtraInfoDescriptorImpl.java
@@ -19,7 +19,7 @@ public class RelayExtraInfoDescriptorImpl
     List<ExtraInfoDescriptor> parsedDescriptors = new ArrayList<>();
     List<byte[]> splitDescriptorsBytes =
         DescriptorImpl.splitRawDescriptorBytes(descriptorsBytes,
-        "extra-info ");
+        Key.EXTRA_INFO.keyword + SP);
     for (byte[] descriptorBytes : splitDescriptorsBytes) {
       ExtraInfoDescriptor parsedDescriptor =
           new RelayExtraInfoDescriptorImpl(descriptorBytes,
diff --git a/src/main/java/org/torproject/descriptor/impl/RelayNetworkStatusConsensusImpl.java b/src/main/java/org/torproject/descriptor/impl/RelayNetworkStatusConsensusImpl.java
index ef135d4..4570931 100644
--- a/src/main/java/org/torproject/descriptor/impl/RelayNetworkStatusConsensusImpl.java
+++ b/src/main/java/org/torproject/descriptor/impl/RelayNetworkStatusConsensusImpl.java
@@ -11,7 +11,7 @@ import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.HashSet;
+import java.util.EnumSet;
 import java.util.List;
 import java.util.Scanner;
 import java.util.Set;
@@ -33,7 +33,7 @@ public class RelayNetworkStatusConsensusImpl extends NetworkStatusImpl
         new ArrayList<>();
     List<byte[]> splitConsensusBytes =
         DescriptorImpl.splitRawDescriptorBytes(consensusesBytes,
-        "network-status-version 3");
+        Key.NETWORK_STATUS_VERSION.keyword + SP + "3");
     for (byte[] consensusBytes : splitConsensusBytes) {
       RelayNetworkStatusConsensus parsedConsensus =
           new RelayNetworkStatusConsensusImpl(consensusBytes,
@@ -47,27 +47,27 @@ public class RelayNetworkStatusConsensusImpl extends NetworkStatusImpl
       boolean failUnrecognizedDescriptorLines)
       throws DescriptorParseException {
     super(consensusBytes, failUnrecognizedDescriptorLines, true, false);
-    Set<String> exactlyOnceKeywords = new HashSet<>(Arrays.asList((
-        "vote-status,consensus-method,valid-after,fresh-until,"
-        + "valid-until,voting-delay,known-flags").split(",")));
-    this.checkExactlyOnceKeywords(exactlyOnceKeywords);
-    Set<String> atMostOnceKeywords = new HashSet<>(Arrays.asList((
-        "client-versions,server-versions,recommended-client-protocols,"
-        + "recommended-relay-protocols,required-client-protocols,"
-        + "required-relay-protocols,params,shared-rand-previous-value,"
-        + "shared-rand-current-value,directory-footer,bandwidth-weights")
-        .split(",")));
-    this.checkAtMostOnceKeywords(atMostOnceKeywords);
-    this.checkFirstKeyword("network-status-version");
-    this.clearParsedKeywords();
+    Set<Key> exactlyOnceKeys = EnumSet.of(
+        Key.VOTE_STATUS, Key.CONSENSUS_METHOD, Key.VALID_AFTER, Key.FRESH_UNTIL,
+        Key.VALID_UNTIL, Key.VOTING_DELAY, Key.KNOWN_FLAGS);
+    this.checkExactlyOnceKeys(exactlyOnceKeys);
+    Set<Key> atMostOnceKeys = EnumSet.of(
+        Key.CLIENT_VERSIONS, Key.SERVER_VERSIONS,
+        Key.RECOMMENDED_CLIENT_PROTOCOLS, Key.RECOMMENDED_RELAY_PROTOCOLS,
+        Key.REQUIRED_CLIENT_PROTOCOLS, Key.REQUIRED_RELAY_PROTOCOLS, Key.PARAMS,
+        Key.SHARED_RAND_PREVIOUS_VALUE, Key.SHARED_RAND_CURRENT_VALUE,
+        Key.DIRECTORY_FOOTER, Key.BANDWIDTH_WEIGHTS);
+    this.checkAtMostOnceKeys(atMostOnceKeys);
+    this.checkFirstKey(Key.NETWORK_STATUS_VERSION);
+    this.clearParsedKeys();
     this.calculateDigest();
   }
 
   private void calculateDigest() throws DescriptorParseException {
     try {
       String ascii = new String(this.getRawDescriptorBytes(), "US-ASCII");
-      String startToken = "network-status-version ";
-      String sigToken = "\ndirectory-signature ";
+      String startToken = Key.NETWORK_STATUS_VERSION.keyword + SP;
+      String sigToken = NL + Key.DIRECTORY_SIGNATURE.keyword + SP;
       if (!ascii.contains(sigToken)) {
         return;
       }
@@ -94,64 +94,64 @@ public class RelayNetworkStatusConsensusImpl extends NetworkStatusImpl
 
   protected void parseHeader(byte[] headerBytes)
       throws DescriptorParseException {
-    Scanner scanner = new Scanner(new String(headerBytes)).useDelimiter("\n");
+    Scanner scanner = new Scanner(new String(headerBytes)).useDelimiter(NL);
     while (scanner.hasNext()) {
       String line = scanner.next();
       String[] parts = line.split("[ \t]+");
-      String keyword = parts[0];
-      switch (keyword) {
-        case "network-status-version":
+      Key key = Key.get(parts[0]);
+      switch (key) {
+        case NETWORK_STATUS_VERSION:
           this.parseNetworkStatusVersionLine(line, parts);
           break;
-        case "vote-status":
+        case VOTE_STATUS:
           this.parseVoteStatusLine(line, parts);
           break;
-        case "consensus-method":
+        case CONSENSUS_METHOD:
           this.parseConsensusMethodLine(line, parts);
           break;
-        case "valid-after":
+        case VALID_AFTER:
           this.parseValidAfterLine(line, parts);
           break;
-        case "fresh-until":
+        case FRESH_UNTIL:
           this.parseFreshUntilLine(line, parts);
           break;
-        case "valid-until":
+        case VALID_UNTIL:
           this.parseValidUntilLine(line, parts);
           break;
-        case "voting-delay":
+        case VOTING_DELAY:
           this.parseVotingDelayLine(line, parts);
           break;
-        case "client-versions":
+        case CLIENT_VERSIONS:
           this.parseClientVersionsLine(line, parts);
           break;
-        case "server-versions":
+        case SERVER_VERSIONS:
           this.parseServerVersionsLine(line, parts);
           break;
-        case "recommended-client-protocols":
+        case RECOMMENDED_CLIENT_PROTOCOLS:
           this.parseRecommendedClientProtocolsLine(line, parts);
           break;
-        case "recommended-relay-protocols":
+        case RECOMMENDED_RELAY_PROTOCOLS:
           this.parseRecommendedRelayProtocolsLine(line, parts);
           break;
-        case "required-client-protocols":
+        case REQUIRED_CLIENT_PROTOCOLS:
           this.parseRequiredClientProtocolsLine(line, parts);
           break;
-        case "required-relay-protocols":
+        case REQUIRED_RELAY_PROTOCOLS:
           this.parseRequiredRelayProtocolsLine(line, parts);
           break;
-        case "package":
+        case PACKAGE:
           this.parsePackageLine(line, parts);
           break;
-        case "known-flags":
+        case KNOWN_FLAGS:
           this.parseKnownFlagsLine(line, parts);
           break;
-        case "params":
+        case PARAMS:
           this.parseParamsLine(line, parts);
           break;
-        case "shared-rand-previous-value":
+        case SHARED_RAND_PREVIOUS_VALUE:
           this.parseSharedRandPreviousValueLine(line, parts);
           break;
-        case "shared-rand-current-value":
+        case SHARED_RAND_CURRENT_VALUE:
           this.parseSharedRandCurrentValueLine(line, parts);
           break;
         default:
@@ -188,15 +188,15 @@ public class RelayNetworkStatusConsensusImpl extends NetworkStatusImpl
 
   protected void parseFooter(byte[] footerBytes)
       throws DescriptorParseException {
-    Scanner scanner = new Scanner(new String(footerBytes)).useDelimiter("\n");
+    Scanner scanner = new Scanner(new String(footerBytes)).useDelimiter(NL);
     while (scanner.hasNext()) {
       String line = scanner.next();
       String[] parts = line.split("[ \t]+");
-      String keyword = parts[0];
-      switch (keyword) {
-        case "directory-footer":
+      Key key = Key.get(parts[0]);
+      switch (key) {
+        case DIRECTORY_FOOTER:
           break;
-        case "bandwidth-weights":
+        case BANDWIDTH_WEIGHTS:
           this.parseBandwidthWeightsLine(line, parts);
           break;
         default:
@@ -215,7 +215,7 @@ public class RelayNetworkStatusConsensusImpl extends NetworkStatusImpl
 
   private void parseNetworkStatusVersionLine(String line, String[] parts)
       throws DescriptorParseException {
-    if (!line.startsWith("network-status-version 3")) {
+    if (!line.startsWith(Key.NETWORK_STATUS_VERSION.keyword + SP + "3")) {
       throw new DescriptorParseException("Illegal network status version "
           + "number in line '" + line + "'.");
     }
@@ -335,7 +335,7 @@ public class RelayNetworkStatusConsensusImpl extends NetworkStatusImpl
     if (this.packageLines == null) {
       this.packageLines = new ArrayList<>();
     }
-    this.packageLines.add(line.substring("package ".length()));
+    this.packageLines.add(line.substring(Key.PACKAGE.keyword.length() + 1));
   }
 
   private void parseKnownFlagsLine(String line, String[] parts)
diff --git a/src/main/java/org/torproject/descriptor/impl/RelayNetworkStatusImpl.java b/src/main/java/org/torproject/descriptor/impl/RelayNetworkStatusImpl.java
index 143540c..121cdc9 100644
--- a/src/main/java/org/torproject/descriptor/impl/RelayNetworkStatusImpl.java
+++ b/src/main/java/org/torproject/descriptor/impl/RelayNetworkStatusImpl.java
@@ -11,7 +11,7 @@ import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.HashSet;
+import java.util.EnumSet;
 import java.util.List;
 import java.util.Scanner;
 import java.util.Set;
@@ -42,29 +42,29 @@ public class RelayNetworkStatusImpl extends NetworkStatusImpl
       boolean failUnrecognizedDescriptorLines)
       throws DescriptorParseException {
     super(statusBytes, failUnrecognizedDescriptorLines, false, true);
-    Set<String> exactlyOnceKeywords = new HashSet<>(Arrays.asList((
-        "network-status-version,dir-source,fingerprint,contact,"
-        + "dir-signing-key,published").split(",")));
-    this.checkExactlyOnceKeywords(exactlyOnceKeywords);
-    Set<String> atMostOnceKeywords = new HashSet<>(Arrays.asList(
-        "dir-options,client-versions,server-versions".split(",")));
-    this.checkAtMostOnceKeywords(atMostOnceKeywords);
-    this.checkFirstKeyword("network-status-version");
-    this.clearParsedKeywords();
+    Set<Key> exactlyOnceKeys = EnumSet.of(
+        Key.NETWORK_STATUS_VERSION, Key.DIR_SOURCE, Key.FINGERPRINT,
+        Key.CONTACT, Key.DIR_SIGNING_KEY, Key.PUBLISHED);
+    this.checkExactlyOnceKeys(exactlyOnceKeys);
+    Set<Key> atMostOnceKeys = EnumSet.of(
+        Key.DIR_OPTIONS, Key.CLIENT_VERSIONS, Key.SERVER_VERSIONS);
+    this.checkAtMostOnceKeys(atMostOnceKeys);
+    this.checkFirstKey(Key.NETWORK_STATUS_VERSION);
+    this.clearParsedKeys();
     this.calculateDigest();
   }
 
   private void calculateDigest() throws DescriptorParseException {
     try {
       String ascii = new String(this.getRawDescriptorBytes(), "US-ASCII");
-      String startToken = "network-status-version ";
-      String sigToken = "\ndirectory-signature ";
+      String startToken = Key.NETWORK_STATUS_VERSION.keyword + SP;
+      String sigToken = NL + Key.DIRECTORY_SIGNATURE.keyword + SP;
       if (!ascii.contains(sigToken)) {
         return;
       }
       int start = ascii.indexOf(startToken);
       int sig = ascii.indexOf(sigToken) + sigToken.length();
-      sig = ascii.indexOf("\n", sig) + 1;
+      sig = ascii.indexOf(NL, sig) + 1;
       if (start >= 0 && sig >= 0 && sig > start) {
         byte[] forDigest = new byte[sig - start];
         System.arraycopy(this.getRawDescriptorBytes(), start,
@@ -86,8 +86,8 @@ public class RelayNetworkStatusImpl extends NetworkStatusImpl
 
   protected void parseHeader(byte[] headerBytes)
       throws DescriptorParseException {
-    Scanner scanner = new Scanner(new String(headerBytes)).useDelimiter("\n");
-    String nextCrypto = "";
+    Scanner scanner = new Scanner(new String(headerBytes)).useDelimiter(NL);
+    Key nextCrypto = Key.EMPTY;
     StringBuilder crypto = null;
     while (scanner.hasNext()) {
       String line = scanner.next();
@@ -95,55 +95,55 @@ public class RelayNetworkStatusImpl extends NetworkStatusImpl
         continue;
       }
       String[] parts = line.split("[ \t]+");
-      String keyword = parts[0];
-      switch (keyword) {
-        case "network-status-version":
+      Key key = Key.get(parts[0]);
+      switch (key) {
+        case NETWORK_STATUS_VERSION:
           this.parseNetworkStatusVersionLine(line, parts);
           break;
-        case "dir-source":
+        case DIR_SOURCE:
           this.parseDirSourceLine(line, parts);
           break;
-        case "fingerprint":
+        case FINGERPRINT:
           this.parseFingerprintLine(line, parts);
           break;
-        case "contact":
+        case CONTACT:
           this.parseContactLine(line, parts);
           break;
-        case "dir-signing-key":
+        case DIR_SIGNING_KEY:
           this.parseDirSigningKeyLine(line, parts);
-          nextCrypto = "dir-signing-key";
+          nextCrypto = key;
           break;
-        case "client-versions":
+        case CLIENT_VERSIONS:
           this.parseClientVersionsLine(line, parts);
           break;
-        case "server-versions":
+        case SERVER_VERSIONS:
           this.parseServerVersionsLine(line, parts);
           break;
-        case "published":
+        case PUBLISHED:
           this.parsePublishedLine(line, parts);
           break;
-        case "dir-options":
+        case DIR_OPTIONS:
           this.parseDirOptionsLine(line, parts);
           break;
-        case "-----BEGIN":
+        case CRYPTO_BEGIN:
           crypto = new StringBuilder();
-          crypto.append(line).append("\n");
+          crypto.append(line).append(NL);
           break;
-        case "-----END":
-          crypto.append(line).append("\n");
+        case CRYPTO_END:
+          crypto.append(line).append(NL);
           String cryptoString = crypto.toString();
           crypto = null;
-          if (nextCrypto.equals("dir-signing-key")) {
+          if (nextCrypto.equals(Key.DIR_SIGNING_KEY)) {
             this.dirSigningKey = cryptoString;
           } else {
             throw new DescriptorParseException("Unrecognized crypto "
                 + "block in v2 network status.");
           }
-          nextCrypto = "";
+          nextCrypto = Key.EMPTY;
           break;
         default:
           if (crypto != null) {
-            crypto.append(line).append("\n");
+            crypto.append(line).append(NL);
           } else if (this.failUnrecognizedDescriptorLines) {
             throw new DescriptorParseException("Unrecognized line '"
                 + line + "' in v2 network status.");
@@ -166,37 +166,37 @@ public class RelayNetworkStatusImpl extends NetworkStatusImpl
   protected void parseDirectorySignature(byte[] directorySignatureBytes)
       throws DescriptorParseException {
     Scanner scanner = new Scanner(new String(directorySignatureBytes))
-        .useDelimiter("\n");
-    String nextCrypto = "";
+        .useDelimiter(NL);
+    Key nextCrypto = Key.EMPTY;
     StringBuilder crypto = null;
     while (scanner.hasNext()) {
       String line = scanner.next();
       String[] parts = line.split("[ \t]+");
-      String keyword = parts[0];
-      switch (keyword) {
-        case "directory-signature":
+      Key key = Key.get(parts[0]);
+      switch (key) {
+        case DIRECTORY_SIGNATURE:
           this.parseDirectorySignatureLine(line, parts);
-          nextCrypto = "directory-signature";
+          nextCrypto = key;
           break;
-        case "-----BEGIN":
+        case CRYPTO_BEGIN:
           crypto = new StringBuilder();
-          crypto.append(line).append("\n");
+          crypto.append(line).append(NL);
           break;
-        case "-----END":
-          crypto.append(line).append("\n");
+        case CRYPTO_END:
+          crypto.append(line).append(NL);
           String cryptoString = crypto.toString();
           crypto = null;
-          if (nextCrypto.equals("directory-signature")) {
+          if (nextCrypto.equals(Key.DIRECTORY_SIGNATURE)) {
             this.directorySignature = cryptoString;
           } else {
             throw new DescriptorParseException("Unrecognized crypto "
                 + "block in v2 network status.");
           }
-          nextCrypto = "";
+          nextCrypto = Key.EMPTY;
           break;
         default:
           if (crypto != null) {
-            crypto.append(line).append("\n");
+            crypto.append(line).append(NL);
           } else if (this.failUnrecognizedDescriptorLines) {
             throw new DescriptorParseException("Unrecognized line '"
                 + line + "' in v2 network status.");
@@ -212,7 +212,7 @@ public class RelayNetworkStatusImpl extends NetworkStatusImpl
 
   private void parseNetworkStatusVersionLine(String line, String[] parts)
       throws DescriptorParseException {
-    if (!line.equals("network-status-version 2")) {
+    if (!line.equals(Key.NETWORK_STATUS_VERSION.keyword + SP + "2")) {
       throw new DescriptorParseException("Illegal network status version "
           + "number in line '" + line + "'.");
     }
@@ -246,8 +246,8 @@ public class RelayNetworkStatusImpl extends NetworkStatusImpl
 
   private void parseContactLine(String line, String[] parts)
       throws DescriptorParseException {
-    if (line.length() > "contact ".length()) {
-      this.contactLine = line.substring("contact ".length());
+    if (line.length() > Key.CONTACT.keyword.length() + 1) {
+      this.contactLine = line.substring(Key.CONTACT.keyword.length() + 1);
     } else {
       this.contactLine = "";
     }
@@ -255,7 +255,7 @@ public class RelayNetworkStatusImpl extends NetworkStatusImpl
 
   private void parseDirSigningKeyLine(String line, String[] parts)
       throws DescriptorParseException {
-    if (!line.equals("dir-signing-key")) {
+    if (!line.equals(Key.DIR_SIGNING_KEY.keyword)) {
       throw new DescriptorParseException("Illegal line '" + line + "'.");
     }
   }
diff --git a/src/main/java/org/torproject/descriptor/impl/RelayNetworkStatusVoteImpl.java b/src/main/java/org/torproject/descriptor/impl/RelayNetworkStatusVoteImpl.java
index 05f5cc2..c645928 100644
--- a/src/main/java/org/torproject/descriptor/impl/RelayNetworkStatusVoteImpl.java
+++ b/src/main/java/org/torproject/descriptor/impl/RelayNetworkStatusVoteImpl.java
@@ -12,7 +12,7 @@ import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.HashSet;
+import java.util.EnumSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Scanner;
@@ -33,7 +33,7 @@ public class RelayNetworkStatusVoteImpl extends NetworkStatusImpl
     List<RelayNetworkStatusVote> parsedVotes = new ArrayList<>();
     List<byte[]> splitVotesBytes =
         DescriptorImpl.splitRawDescriptorBytes(votesBytes,
-        "network-status-version 3");
+        Key.NETWORK_STATUS_VERSION.keyword + SP + "3");
     for (byte[] voteBytes : splitVotesBytes) {
       RelayNetworkStatusVote parsedVote =
           new RelayNetworkStatusVoteImpl(voteBytes,
@@ -47,27 +47,25 @@ public class RelayNetworkStatusVoteImpl extends NetworkStatusImpl
       boolean failUnrecognizedDescriptorLines)
       throws DescriptorParseException {
     super(voteBytes, failUnrecognizedDescriptorLines, false, false);
-    Set<String> exactlyOnceKeywords = new HashSet<>(Arrays.asList((
-        "vote-status,published,valid-after,fresh-until,"
-        + "valid-until,voting-delay,known-flags,dir-source,"
-        + "dir-key-certificate-version,fingerprint,dir-key-published,"
-        + "dir-key-expires,dir-identity-key,dir-signing-key,"
-        + "dir-key-certification").split(",")));
-    this.checkExactlyOnceKeywords(exactlyOnceKeywords);
-    Set<String> atMostOnceKeywords = new HashSet<>(Arrays.asList((
-        "consensus-methods,client-versions,server-versions,"
-        + "recommended-client-protocols,recommended-relay-protocols,"
-        + "required-client-protocols,required-relay-protocols,"
-        + "flag-thresholds,params,contact,shared-rand-participate,"
-        + "shared-rand-previous-value,shared-rand-current-value,"
-        + "legacy-key,dir-key-crosscert,dir-address,directory-footer")
-        .split(",")));
-    this.checkAtMostOnceKeywords(atMostOnceKeywords);
-    Set<String> atLeastOnceKeywords = new HashSet<>(Arrays.asList(
-        "directory-signature"));
-    this.checkAtLeastOnceKeywords(atLeastOnceKeywords);
-    this.checkFirstKeyword("network-status-version");
-    this.clearParsedKeywords();
+    Set<Key> exactlyOnceKeys = EnumSet.of(
+        Key.VOTE_STATUS, Key.PUBLISHED, Key.VALID_AFTER, Key.FRESH_UNTIL,
+        Key.VALID_UNTIL, Key.VOTING_DELAY, Key.KNOWN_FLAGS, Key.DIR_SOURCE,
+        Key.DIR_KEY_CERTIFICATE_VERSION, Key.FINGERPRINT, Key.DIR_KEY_PUBLISHED,
+        Key.DIR_KEY_EXPIRES, Key.DIR_IDENTITY_KEY, Key.DIR_SIGNING_KEY,
+        Key.DIR_KEY_CERTIFICATION);
+    this.checkExactlyOnceKeys(exactlyOnceKeys);
+    Set<Key> atMostOnceKeys = EnumSet.of(
+        Key.CONSENSUS_METHODS, Key.CLIENT_VERSIONS, Key.SERVER_VERSIONS,
+        Key.RECOMMENDED_CLIENT_PROTOCOLS, Key.RECOMMENDED_RELAY_PROTOCOLS,
+        Key.REQUIRED_CLIENT_PROTOCOLS, Key.REQUIRED_RELAY_PROTOCOLS,
+        Key.FLAG_THRESHOLDS, Key.PARAMS, Key.CONTACT,
+        Key.SHARED_RAND_PARTICIPATE, Key.SHARED_RAND_PREVIOUS_VALUE,
+        Key.SHARED_RAND_CURRENT_VALUE, Key.LEGACY_KEY, Key.DIR_KEY_CROSSCERT,
+        Key.DIR_ADDRESS, Key.DIRECTORY_FOOTER);
+    this.checkAtMostOnceKeys(atMostOnceKeys);
+    this.checkAtLeastOnceKeys(EnumSet.of(Key.DIRECTORY_SIGNATURE));
+    this.checkFirstKey(Key.NETWORK_STATUS_VERSION);
+    this.clearParsedKeys();
     this.calculateDigest();
   }
 
@@ -116,150 +114,150 @@ public class RelayNetworkStatusVoteImpl extends NetworkStatusImpl
     this.enoughMtbfInfo = -1;
     this.ignoringAdvertisedBws = -1;
 
-    Scanner scanner = new Scanner(new String(headerBytes)).useDelimiter("\n");
-    String nextCrypto = "";
+    Scanner scanner = new Scanner(new String(headerBytes)).useDelimiter(NL);
+    Key nextCrypto = Key.EMPTY;
     StringBuilder crypto = null;
     while (scanner.hasNext()) {
       String line = scanner.next();
       String[] parts = line.split("[ \t]+");
-      String keyword = parts[0];
-      switch (keyword) {
-        case "network-status-version":
+      Key key = Key.get(parts[0]);
+      switch (key) {
+        case NETWORK_STATUS_VERSION:
           this.parseNetworkStatusVersionLine(line, parts);
           break;
-        case "vote-status":
+        case VOTE_STATUS:
           this.parseVoteStatusLine(line, parts);
           break;
-        case "consensus-methods":
+        case CONSENSUS_METHODS:
           this.parseConsensusMethodsLine(line, parts);
           break;
-        case "published":
+        case PUBLISHED:
           this.parsePublishedLine(line, parts);
           break;
-        case "valid-after":
+        case VALID_AFTER:
           this.parseValidAfterLine(line, parts);
           break;
-        case "fresh-until":
+        case FRESH_UNTIL:
           this.parseFreshUntilLine(line, parts);
           break;
-        case "valid-until":
+        case VALID_UNTIL:
           this.parseValidUntilLine(line, parts);
           break;
-        case "voting-delay":
+        case VOTING_DELAY:
           this.parseVotingDelayLine(line, parts);
           break;
-        case "client-versions":
+        case CLIENT_VERSIONS:
           this.parseClientVersionsLine(line, parts);
           break;
-        case "server-versions":
+        case SERVER_VERSIONS:
           this.parseServerVersionsLine(line, parts);
           break;
-        case "recommended-client-protocols":
+        case RECOMMENDED_CLIENT_PROTOCOLS:
           this.parseRecommendedClientProtocolsLine(line, parts);
           break;
-        case "recommended-relay-protocols":
+        case RECOMMENDED_RELAY_PROTOCOLS:
           this.parseRecommendedRelayProtocolsLine(line, parts);
           break;
-        case "required-client-protocols":
+        case REQUIRED_CLIENT_PROTOCOLS:
           this.parseRequiredClientProtocolsLine(line, parts);
           break;
-        case "required-relay-protocols":
+        case REQUIRED_RELAY_PROTOCOLS:
           this.parseRequiredRelayProtocolsLine(line, parts);
           break;
-        case "package":
+        case PACKAGE:
           this.parsePackageLine(line, parts);
           break;
-        case "known-flags":
+        case KNOWN_FLAGS:
           this.parseKnownFlagsLine(line, parts);
           break;
-        case "flag-thresholds":
+        case FLAG_THRESHOLDS:
           this.parseFlagThresholdsLine(line, parts);
           break;
-        case "params":
+        case PARAMS:
           this.parseParamsLine(line, parts);
           break;
-        case "dir-source":
+        case DIR_SOURCE:
           this.parseDirSourceLine(line, parts);
           break;
-        case "contact":
+        case CONTACT:
           this.parseContactLine(line, parts);
           break;
-        case "shared-rand-participate":
+        case SHARED_RAND_PARTICIPATE:
           this.parseSharedRandParticipateLine(line, parts);
           break;
-        case "shared-rand-commit":
+        case SHARED_RAND_COMMIT:
           this.parseSharedRandCommitLine(line, parts);
           break;
-        case "shared-rand-previous-value":
+        case SHARED_RAND_PREVIOUS_VALUE:
           this.parseSharedRandPreviousValueLine(line, parts);
           break;
-        case "shared-rand-current-value":
+        case SHARED_RAND_CURRENT_VALUE:
           this.parseSharedRandCurrentValueLine(line, parts);
           break;
-        case "dir-key-certificate-version":
+        case DIR_KEY_CERTIFICATE_VERSION:
           this.parseDirKeyCertificateVersionLine(line, parts);
           break;
-        case "dir-address":
+        case DIR_ADDRESS:
           this.parseDirAddressLine(line, parts);
           break;
-        case "fingerprint":
+        case FINGERPRINT:
           this.parseFingerprintLine(line, parts);
           break;
-        case "legacy-dir-key":
+        case LEGACY_DIR_KEY:
           this.parseLegacyDirKeyLine(line, parts);
           break;
-        case "dir-key-published":
+        case DIR_KEY_PUBLISHED:
           this.parseDirKeyPublished(line, parts);
           break;
-        case "dir-key-expires":
+        case DIR_KEY_EXPIRES:
           this.parseDirKeyExpiresLine(line, parts);
           break;
-        case "dir-identity-key":
+        case DIR_IDENTITY_KEY:
           this.parseDirIdentityKeyLine(line, parts);
-          nextCrypto = "dir-identity-key";
+          nextCrypto = key;
           break;
-        case "dir-signing-key":
+        case DIR_SIGNING_KEY:
           this.parseDirSigningKeyLine(line, parts);
-          nextCrypto = "dir-signing-key";
+          nextCrypto = key;
           break;
-        case "dir-key-crosscert":
+        case DIR_KEY_CROSSCERT:
           this.parseDirKeyCrosscertLine(line, parts);
-          nextCrypto = "dir-key-crosscert";
+          nextCrypto = key;
           break;
-        case "dir-key-certification":
+        case DIR_KEY_CERTIFICATION:
           this.parseDirKeyCertificationLine(line, parts);
-          nextCrypto = "dir-key-certification";
+          nextCrypto = key;
           break;
-        case "-----BEGIN":
+        case CRYPTO_BEGIN:
           crypto = new StringBuilder();
-          crypto.append(line).append("\n");
+          crypto.append(line).append(NL);
           break;
-        case "-----END":
-          crypto.append(line).append("\n");
+        case CRYPTO_END:
+          crypto.append(line).append(NL);
           String cryptoString = crypto.toString();
           crypto = null;
           switch (nextCrypto) {
-            case "dir-identity-key":
+            case DIR_IDENTITY_KEY:
               this.dirIdentityKey = cryptoString;
               break;
-            case "dir-signing-key":
+            case DIR_SIGNING_KEY:
               this.dirSigningKey = cryptoString;
               break;
-            case "dir-key-crosscert":
+            case DIR_KEY_CROSSCERT:
               this.dirKeyCrosscert = cryptoString;
               break;
-            case "dir-key-certification":
+            case DIR_KEY_CERTIFICATION:
               this.dirKeyCertification = cryptoString;
               break;
             default:
               throw new DescriptorParseException("Unrecognized crypto "
                   + "block in vote.");
           }
-          nextCrypto = "";
+          nextCrypto = Key.EMPTY;
           break;
         default:
           if (crypto != null) {
-            crypto.append(line).append("\n");
+            crypto.append(line).append(NL);
           } else {
             if (this.failUnrecognizedDescriptorLines) {
               throw new DescriptorParseException("Unrecognized line '"
@@ -277,7 +275,7 @@ public class RelayNetworkStatusVoteImpl extends NetworkStatusImpl
 
   private void parseNetworkStatusVersionLine(String line, String[] parts)
       throws DescriptorParseException {
-    if (!line.equals("network-status-version 3")) {
+    if (!line.equals(Key.NETWORK_STATUS_VERSION.keyword + SP + "3")) {
       throw new DescriptorParseException("Illegal network status version "
           + "number in line '" + line + "'.");
     }
@@ -399,7 +397,7 @@ public class RelayNetworkStatusVoteImpl extends NetworkStatusImpl
     if (this.packageLines == null) {
       this.packageLines = new ArrayList<>();
     }
-    this.packageLines.add(line.substring("package ".length()));
+    this.packageLines.add(line.substring(Key.PACKAGE.keyword.length() + 1));
   }
 
   private void parseKnownFlagsLine(String line, String[] parts)
@@ -492,8 +490,8 @@ public class RelayNetworkStatusVoteImpl extends NetworkStatusImpl
 
   private void parseContactLine(String line, String[] parts)
       throws DescriptorParseException {
-    if (line.length() > "contact ".length()) {
-      this.contactLine = line.substring("contact ".length());
+    if (line.length() > Key.CONTACT.keyword.length() + 1) {
+      this.contactLine = line.substring(Key.CONTACT.keyword.length() + 1);
     } else {
       this.contactLine = "";
     }
@@ -604,38 +602,38 @@ public class RelayNetworkStatusVoteImpl extends NetworkStatusImpl
 
   private void parseDirIdentityKeyLine(String line, String[] parts)
       throws DescriptorParseException {
-    if (!line.equals("dir-identity-key")) {
+    if (!line.equals(Key.DIR_IDENTITY_KEY.keyword)) {
       throw new DescriptorParseException("Illegal line '" + line + "'.");
     }
   }
 
   private void parseDirSigningKeyLine(String line, String[] parts)
       throws DescriptorParseException {
-    if (!line.equals("dir-signing-key")) {
+    if (!line.equals(Key.DIR_SIGNING_KEY.keyword)) {
       throw new DescriptorParseException("Illegal line '" + line + "'.");
     }
   }
 
   private void parseDirKeyCrosscertLine(String line, String[] parts)
       throws DescriptorParseException {
-    if (!line.equals("dir-key-crosscert")) {
+    if (!line.equals(Key.DIR_KEY_CROSSCERT.keyword)) {
       throw new DescriptorParseException("Illegal line '" + line + "'.");
     }
   }
 
   private void parseDirKeyCertificationLine(String line, String[] parts)
       throws DescriptorParseException {
-    if (!line.equals("dir-key-certification")) {
+    if (!line.equals(Key.DIR_KEY_CERTIFICATION.keyword)) {
       throw new DescriptorParseException("Illegal line '" + line + "'.");
     }
   }
 
   protected void parseFooter(byte[] footerBytes)
       throws DescriptorParseException {
-    Scanner scanner = new Scanner(new String(footerBytes)).useDelimiter("\n");
+    Scanner scanner = new Scanner(new String(footerBytes)).useDelimiter(NL);
     while (scanner.hasNext()) {
       String line = scanner.next();
-      if (!line.equals("directory-footer")) {
+      if (!line.equals(Key.DIRECTORY_FOOTER.keyword)) {
         if (this.failUnrecognizedDescriptorLines) {
           throw new DescriptorParseException("Unrecognized line '"
               + line + "' in vote.");
diff --git a/src/main/java/org/torproject/descriptor/impl/RelayServerDescriptorImpl.java b/src/main/java/org/torproject/descriptor/impl/RelayServerDescriptorImpl.java
index 3353ec1..eefa24f 100644
--- a/src/main/java/org/torproject/descriptor/impl/RelayServerDescriptorImpl.java
+++ b/src/main/java/org/torproject/descriptor/impl/RelayServerDescriptorImpl.java
@@ -19,7 +19,7 @@ public class RelayServerDescriptorImpl extends ServerDescriptorImpl
     List<ServerDescriptor> parsedDescriptors = new ArrayList<>();
     List<byte[]> splitDescriptorsBytes =
         DescriptorImpl.splitRawDescriptorBytes(descriptorsBytes,
-        "router ");
+        Key.ROUTER.keyword + SP);
     for (byte[] descriptorBytes : splitDescriptorsBytes) {
       ServerDescriptor parsedDescriptor =
           new RelayServerDescriptorImpl(descriptorBytes,
diff --git a/src/main/java/org/torproject/descriptor/impl/ServerDescriptorImpl.java b/src/main/java/org/torproject/descriptor/impl/ServerDescriptorImpl.java
index ac113d2..70bd42c 100644
--- a/src/main/java/org/torproject/descriptor/impl/ServerDescriptorImpl.java
+++ b/src/main/java/org/torproject/descriptor/impl/ServerDescriptorImpl.java
@@ -12,7 +12,7 @@ import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.HashSet;
+import java.util.EnumSet;
 import java.util.List;
 import java.util.Scanner;
 import java.util.Set;
@@ -25,6 +25,20 @@ import javax.xml.bind.DatatypeConverter;
 public abstract class ServerDescriptorImpl extends DescriptorImpl
     implements ServerDescriptor {
 
+  private static final Set<Key> atMostOnce = EnumSet.of(
+      Key.IDENTITY_ED25519, Key.MASTER_KEY_ED25519, Key.PLATFORM, Key.PROTO,
+      Key.FINGERPRINT, Key.HIBERNATING, Key.UPTIME, Key.CONTACT, Key.FAMILY,
+      Key.READ_HISTORY, Key.WRITE_HISTORY, Key.EVENTDNS, Key.CACHES_EXTRA_INFO,
+      Key.EXTRA_INFO_DIGEST, Key.HIDDEN_SERVICE_DIR, Key.PROTOCOLS,
+      Key.ALLOW_SINGLE_HOP_EXITS, Key.ONION_KEY, Key.SIGNING_KEY,
+      Key.IPV6_POLICY, Key.NTOR_ONION_KEY, Key.ONION_KEY_CROSSCERT,
+      Key.NTOR_ONION_KEY_CROSSCERT, Key.TUNNELLED_DIR_SERVER,
+      Key.ROUTER_SIG_ED25519, Key.ROUTER_SIGNATURE, Key.ROUTER_DIGEST_SHA256,
+      Key.ROUTER_DIGEST);
+
+  private static final Set<Key> exactlyOnce = EnumSet.of(
+      Key.ROUTER, Key.BANDWIDTH, Key.PUBLISHED);
+
   protected ServerDescriptorImpl(byte[] descriptorBytes,
       boolean failUnrecognizedDescriptorLines)
       throws DescriptorParseException {
@@ -32,184 +46,173 @@ public abstract class ServerDescriptorImpl extends DescriptorImpl
     this.parseDescriptorBytes();
     this.calculateDigest();
     this.calculateDigestSha256();
-    Set<String> exactlyOnceKeywords = new HashSet<>(Arrays.asList(
-        "router,bandwidth,published".split(",")));
-    this.checkExactlyOnceKeywords(exactlyOnceKeywords);
-    Set<String> atMostOnceKeywords = new HashSet<>(Arrays.asList((
-        "identity-ed25519,master-key-ed25519,platform,proto,fingerprint,"
-        + "hibernating,uptime,contact,family,read-history,write-history,"
-        + "eventdns,caches-extra-info,extra-info-digest,"
-        + "hidden-service-dir,protocols,allow-single-hop-exits,onion-key,"
-        + "signing-key,ipv6-policy,ntor-onion-key,onion-key-crosscert,"
-        + "ntor-onion-key-crosscert,tunnelled-dir-server,"
-        + "router-sig-ed25519,router-signature,router-digest-sha256,"
-        + "router-digest").split(",")));
-    this.checkAtMostOnceKeywords(atMostOnceKeywords);
-    this.checkFirstKeyword("router");
-    if (this.getKeywordCount("accept") == 0
-        && this.getKeywordCount("reject") == 0) {
+    this.checkExactlyOnceKeys(exactlyOnce);
+    this.checkAtMostOnceKeys(atMostOnce);
+    this.checkFirstKey(Key.ROUTER);
+    if (this.getKeyCount(Key.ACCEPT) == 0
+        && this.getKeyCount(Key.REJECT) == 0) {
       throw new DescriptorParseException("Either keyword 'accept' or "
           + "'reject' must be contained at least once.");
     }
-    this.clearParsedKeywords();
+    this.clearParsedKeys();
     return;
   }
 
   private void parseDescriptorBytes() throws DescriptorParseException {
     Scanner scanner = new Scanner(new String(this.rawDescriptorBytes))
-        .useDelimiter("\n");
-    String nextCrypto = "";
+        .useDelimiter(NL);
+    Key nextCrypto = Key.EMPTY;
     List<String> cryptoLines = null;
     while (scanner.hasNext()) {
       String line = scanner.next();
       if (line.startsWith("@")) {
         continue;
       }
-      String lineNoOpt = line.startsWith("opt ")
-          ? line.substring("opt ".length()) : line;
+      String lineNoOpt = line.startsWith(Key.OPT.keyword + SP)
+          ? line.substring(Key.OPT.keyword.length() + 1) : line;
       String[] partsNoOpt = lineNoOpt.split("[ \t]+");
-      String keyword = partsNoOpt[0];
-      switch (keyword) {
-        case "router":
+      Key key = Key.get(partsNoOpt[0]);
+      switch (key) {
+        case ROUTER:
           this.parseRouterLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "or-address":
+        case OR_ADDRESS:
           this.parseOrAddressLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "bandwidth":
+        case BANDWIDTH:
           this.parseBandwidthLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "platform":
+        case PLATFORM:
           this.parsePlatformLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "proto":
+        case PROTO:
           this.parseProtoLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "published":
+        case PUBLISHED:
           this.parsePublishedLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "fingerprint":
+        case FINGERPRINT:
           this.parseFingerprintLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "hibernating":
+        case HIBERNATING:
           this.parseHibernatingLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "uptime":
+        case UPTIME:
           this.parseUptimeLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "onion-key":
+        case ONION_KEY:
           this.parseOnionKeyLine(line, lineNoOpt, partsNoOpt);
-          nextCrypto = "onion-key";
+          nextCrypto = key;
           break;
-        case "signing-key":
+        case SIGNING_KEY:
           this.parseSigningKeyLine(line, lineNoOpt, partsNoOpt);
-          nextCrypto = "signing-key";
+          nextCrypto = key;
           break;
-        case "accept":
+        case ACCEPT:
           this.parseAcceptLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "reject":
+        case REJECT:
           this.parseRejectLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "router-signature":
+        case ROUTER_SIGNATURE:
           this.parseRouterSignatureLine(line, lineNoOpt, partsNoOpt);
-          nextCrypto = "router-signature";
+          nextCrypto = key;
           break;
-        case "contact":
+        case CONTACT:
           this.parseContactLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "family":
+        case FAMILY:
           this.parseFamilyLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "read-history":
+        case READ_HISTORY:
           this.parseReadHistoryLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "write-history":
+        case WRITE_HISTORY:
           this.parseWriteHistoryLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "eventdns":
+        case EVENTDNS:
           this.parseEventdnsLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "caches-extra-info":
+        case CACHES_EXTRA_INFO:
           this.parseCachesExtraInfoLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "extra-info-digest":
+        case EXTRA_INFO_DIGEST:
           this.parseExtraInfoDigestLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "hidden-service-dir":
+        case HIDDEN_SERVICE_DIR:
           this.parseHiddenServiceDirLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "protocols":
+        case PROTOCOLS:
           this.parseProtocolsLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "allow-single-hop-exits":
+        case ALLOW_SINGLE_HOP_EXITS:
           this.parseAllowSingleHopExitsLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "dircacheport":
+        case DIRCACHEPORT:
           this.parseDircacheportLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "router-digest":
+        case ROUTER_DIGEST:
           this.parseRouterDigestLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "router-digest-sha256":
+        case ROUTER_DIGEST_SHA256:
           this.parseRouterDigestSha256Line(line, lineNoOpt, partsNoOpt);
           break;
-        case "ipv6-policy":
+        case IPV6_POLICY:
           this.parseIpv6PolicyLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "ntor-onion-key":
+        case NTOR_ONION_KEY:
           this.parseNtorOnionKeyLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "identity-ed25519":
+        case IDENTITY_ED25519:
           this.parseIdentityEd25519Line(line, lineNoOpt, partsNoOpt);
-          nextCrypto = "identity-ed25519";
+          nextCrypto = key;
           break;
-        case "master-key-ed25519":
+        case MASTER_KEY_ED25519:
           this.parseMasterKeyEd25519Line(line, lineNoOpt, partsNoOpt);
           break;
-        case "router-sig-ed25519":
+        case ROUTER_SIG_ED25519:
           this.parseRouterSigEd25519Line(line, lineNoOpt, partsNoOpt);
           break;
-        case "onion-key-crosscert":
+        case ONION_KEY_CROSSCERT:
           this.parseOnionKeyCrosscert(line, lineNoOpt, partsNoOpt);
-          nextCrypto = "onion-key-crosscert";
+          nextCrypto = key;
           break;
-        case "ntor-onion-key-crosscert":
+        case NTOR_ONION_KEY_CROSSCERT:
           this.parseNtorOnionKeyCrosscert(line, lineNoOpt, partsNoOpt);
-          nextCrypto = "ntor-onion-key-crosscert";
+          nextCrypto = key;
           break;
-        case "tunnelled-dir-server":
+        case TUNNELLED_DIR_SERVER:
           this.parseTunnelledDirServerLine(line, lineNoOpt, partsNoOpt);
           break;
-        case "-----BEGIN":
+        case CRYPTO_BEGIN:
           cryptoLines = new ArrayList<>();
           cryptoLines.add(line);
           break;
-        case "-----END":
+        case CRYPTO_END:
           cryptoLines.add(line);
           StringBuilder sb = new StringBuilder();
           for (String cryptoLine : cryptoLines) {
-            sb.append("\n").append(cryptoLine);
+            sb.append(NL).append(cryptoLine);
           }
           String cryptoString = sb.toString().substring(1);
           switch (nextCrypto) {
-            case "onion-key":
+            case ONION_KEY:
               this.onionKey = cryptoString;
               break;
-            case "signing-key":
+            case SIGNING_KEY:
               this.signingKey = cryptoString;
               break;
-            case "router-signature":
+            case ROUTER_SIGNATURE:
               this.routerSignature = cryptoString;
               break;
-            case "identity-ed25519":
+            case IDENTITY_ED25519:
               this.identityEd25519 = cryptoString;
               this.parseIdentityEd25519CryptoBlock(cryptoString);
               break;
-            case "onion-key-crosscert":
+            case ONION_KEY_CROSSCERT:
               this.onionKeyCrosscert = cryptoString;
               break;
-            case "ntor-onion-key-crosscert":
+            case NTOR_ONION_KEY_CROSSCERT:
               this.ntorOnionKeyCrosscert = cryptoString;
               break;
             default:
@@ -224,8 +227,9 @@ public abstract class ServerDescriptorImpl extends DescriptorImpl
               }
           }
           cryptoLines = null;
-          nextCrypto = "";
+          nextCrypto = Key.EMPTY;
           break;
+        case INVALID:
         default:
           if (cryptoLines != null) {
             cryptoLines.add(line);
@@ -301,8 +305,8 @@ public abstract class ServerDescriptorImpl extends DescriptorImpl
 
   private void parsePlatformLine(String line, String lineNoOpt,
       String[] partsNoOpt) throws DescriptorParseException {
-    if (lineNoOpt.length() > "platform ".length()) {
-      this.platform = lineNoOpt.substring("platform ".length());
+    if (lineNoOpt.length() > Key.PLATFORM.keyword.length() + 1) {
+      this.platform = lineNoOpt.substring(Key.PLATFORM.keyword.length() + 1);
     } else {
       this.platform = "";
     }
@@ -322,11 +326,12 @@ public abstract class ServerDescriptorImpl extends DescriptorImpl
 
   private void parseFingerprintLine(String line, String lineNoOpt,
       String[] partsNoOpt) throws DescriptorParseException {
-    if (lineNoOpt.length() != "fingerprint".length() + 5 * 10) {
+    if (lineNoOpt.length() != Key.FINGERPRINT.keyword.length() + 5 * 10) {
       throw new DescriptorParseException("Illegal line '" + line + "'.");
     }
     this.fingerprint = ParseHelper.parseTwentyByteHexString(line,
-        lineNoOpt.substring("fingerprint ".length()).replaceAll(" ", ""));
+        lineNoOpt.substring(Key.FINGERPRINT.keyword.length() + 1)
+            .replaceAll(SP, ""));
   }
 
   private void parseHibernatingLine(String line, String lineNoOpt,
@@ -358,14 +363,14 @@ public abstract class ServerDescriptorImpl extends DescriptorImpl
 
   private void parseOnionKeyLine(String line, String lineNoOpt,
       String[] partsNoOpt) throws DescriptorParseException {
-    if (!lineNoOpt.equals("onion-key")) {
+    if (!lineNoOpt.equals(Key.ONION_KEY.keyword)) {
       throw new DescriptorParseException("Illegal line '" + line + "'.");
     }
   }
 
   private void parseSigningKeyLine(String line, String lineNoOpt,
       String[] partsNoOpt) throws DescriptorParseException {
-    if (!lineNoOpt.equals("signing-key")) {
+    if (!lineNoOpt.equals(Key.SIGNING_KEY.keyword)) {
       throw new DescriptorParseException("Illegal line '" + line + "'.");
     }
   }
@@ -391,15 +396,15 @@ public abstract class ServerDescriptorImpl extends DescriptorImpl
 
   private void parseRouterSignatureLine(String line, String lineNoOpt,
       String[] partsNoOpt) throws DescriptorParseException {
-    if (!lineNoOpt.equals("router-signature")) {
+    if (!lineNoOpt.equals(Key.ROUTER_SIGNATURE.keyword)) {
       throw new DescriptorParseException("Illegal line '" + line + "'.");
     }
   }
 
   private void parseContactLine(String line, String lineNoOpt,
       String[] partsNoOpt) throws DescriptorParseException {
-    if (lineNoOpt.length() > "contact ".length()) {
-      this.contact = lineNoOpt.substring("contact ".length());
+    if (lineNoOpt.length() > Key.CONTACT.keyword.length() + 1) {
+      this.contact = lineNoOpt.substring(Key.CONTACT.keyword.length() + 1);
     } else {
       this.contact = "";
     }
@@ -455,7 +460,7 @@ public abstract class ServerDescriptorImpl extends DescriptorImpl
 
   private void parseCachesExtraInfoLine(String line, String lineNoOpt,
       String[] partsNoOpt) throws DescriptorParseException {
-    if (!lineNoOpt.equals("caches-extra-info")) {
+    if (!lineNoOpt.equals(Key.CACHES_EXTRA_INFO.keyword)) {
       throw new DescriptorParseException("Illegal line '" + line + "'.");
     }
     this.cachesExtraInfo = true;
@@ -532,7 +537,7 @@ public abstract class ServerDescriptorImpl extends DescriptorImpl
 
   private void parseAllowSingleHopExitsLine(String line, String lineNoOpt,
       String[] partsNoOpt) throws DescriptorParseException {
-    if (!lineNoOpt.equals("allow-single-hop-exits")) {
+    if (!lineNoOpt.equals(Key.ALLOW_SINGLE_HOP_EXITS.keyword)) {
       throw new DescriptorParseException("Illegal line '" + line + "'.");
     }
     this.allowSingleHopExits = true;
@@ -568,9 +573,9 @@ public abstract class ServerDescriptorImpl extends DescriptorImpl
     if (partsNoOpt.length != 3) {
       isValid = false;
     } else {
-      switch (partsNoOpt[1]) {
-        case "accept":
-        case "reject":
+      switch (Key.get(partsNoOpt[1])) {
+        case ACCEPT:
+        case REJECT:
           this.ipv6DefaultPolicy = partsNoOpt[1];
           this.ipv6PortList = partsNoOpt[2];
           String[] ports = partsNoOpt[2].split(",", -1);
@@ -581,6 +586,7 @@ public abstract class ServerDescriptorImpl extends DescriptorImpl
             }
           }
           break;
+        case INVALID:
         default:
           isValid = false;
       }
@@ -626,7 +632,7 @@ public abstract class ServerDescriptorImpl extends DescriptorImpl
 
   private void parseTunnelledDirServerLine(String line, String lineNoOpt,
       String[] partsNoOpt) throws DescriptorParseException {
-    if (!lineNoOpt.equals("tunnelled-dir-server")) {
+    if (!lineNoOpt.equals(Key.TUNNELLED_DIR_SERVER.keyword)) {
       throw new DescriptorParseException("Illegal line '" + line + "'.");
     }
     this.tunnelledDirServer = true;
@@ -684,8 +690,8 @@ public abstract class ServerDescriptorImpl extends DescriptorImpl
     }
     try {
       String ascii = new String(this.getRawDescriptorBytes(), "US-ASCII");
-      String startToken = "router ";
-      String sigToken = "\nrouter-signature\n";
+      String startToken = Key.ROUTER.keyword + SP;
+      String sigToken = NL + Key.ROUTER_SIGNATURE.keyword + NL;
       int start = ascii.indexOf(startToken);
       int sig = ascii.indexOf(sigToken) + sigToken.length();
       if (start >= 0 && sig >= 0 && sig > start) {
@@ -715,7 +721,7 @@ public abstract class ServerDescriptorImpl extends DescriptorImpl
     }
     try {
       String ascii = new String(this.getRawDescriptorBytes(), "US-ASCII");
-      String startToken = "router ";
+      String startToken = Key.ROUTER.keyword + SP;
       String sigToken = "\n-----END SIGNATURE-----\n";
       int start = ascii.indexOf(startToken);
       int sig = ascii.indexOf(sigToken) + sigToken.length();





More information about the tor-commits mailing list