From 6475cecd795511b12fe07653a0c54394828a2f53 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Thu, 31 Dec 2015 13:33:00 +0100 Subject: [PATCH] Add tools task to generate an encryption algorithm overview --- .../authme/security/crypts/CRAZYCRYPT1.java | 2 - .../fr/xephi/authme/security/crypts/IPB3.java | 2 +- .../fr/xephi/authme/security/crypts/MYBB.java | 6 + .../fr/xephi/authme/security/crypts/WBB3.java | 6 + src/tools/docs/hash_algorithms.md | 78 ++++++++++ .../EncryptionMethodInfoGatherer.java | 135 ++++++++++++++++++ .../HashAlgorithmsDescriptionTask.java | 100 +++++++++++++ src/tools/hashmethods/MethodDescription.java | 85 +++++++++++ src/tools/hashmethods/hash_algorithms.tpl.md | 53 +++++++ .../hashmethods/hash_algorithms_row.tpl.md | 1 + 10 files changed, 465 insertions(+), 3 deletions(-) create mode 100644 src/tools/docs/hash_algorithms.md create mode 100644 src/tools/hashmethods/EncryptionMethodInfoGatherer.java create mode 100644 src/tools/hashmethods/HashAlgorithmsDescriptionTask.java create mode 100644 src/tools/hashmethods/MethodDescription.java create mode 100644 src/tools/hashmethods/hash_algorithms.tpl.md create mode 100644 src/tools/hashmethods/hash_algorithms_row.tpl.md diff --git a/src/main/java/fr/xephi/authme/security/crypts/CRAZYCRYPT1.java b/src/main/java/fr/xephi/authme/security/crypts/CRAZYCRYPT1.java index 269c1365e..cd8af9249 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/CRAZYCRYPT1.java +++ b/src/main/java/fr/xephi/authme/security/crypts/CRAZYCRYPT1.java @@ -10,8 +10,6 @@ import fr.xephi.authme.security.crypts.description.HasSalt; import java.nio.charset.Charset; import java.security.MessageDigest; -@Recommendation(Usage.DO_NOT_USE) -@HasSalt(SaltType.USERNAME) public class CRAZYCRYPT1 extends UsernameSaltMethod { private static final char[] CRYPTCHARS = diff --git a/src/main/java/fr/xephi/authme/security/crypts/IPB3.java b/src/main/java/fr/xephi/authme/security/crypts/IPB3.java index 85789b881..4abfe5d45 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/IPB3.java +++ b/src/main/java/fr/xephi/authme/security/crypts/IPB3.java @@ -8,7 +8,7 @@ import fr.xephi.authme.security.crypts.description.Usage; import static fr.xephi.authme.security.HashUtils.md5; -@Recommendation(Usage.DO_NOT_USE) +@Recommendation(Usage.ACCEPTABLE) @HasSalt(value = SaltType.TEXT, length = 5) public class IPB3 extends SeparateSaltMethod { diff --git a/src/main/java/fr/xephi/authme/security/crypts/MYBB.java b/src/main/java/fr/xephi/authme/security/crypts/MYBB.java index 97418640c..555f3213a 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/MYBB.java +++ b/src/main/java/fr/xephi/authme/security/crypts/MYBB.java @@ -1,9 +1,15 @@ package fr.xephi.authme.security.crypts; import fr.xephi.authme.security.RandomString; +import fr.xephi.authme.security.crypts.description.HasSalt; +import fr.xephi.authme.security.crypts.description.Recommendation; +import fr.xephi.authme.security.crypts.description.SaltType; +import fr.xephi.authme.security.crypts.description.Usage; import static fr.xephi.authme.security.HashUtils.md5; +@Recommendation(Usage.ACCEPTABLE) +@HasSalt(value = SaltType.TEXT, length = 8) public class MYBB extends SeparateSaltMethod { @Override diff --git a/src/main/java/fr/xephi/authme/security/crypts/WBB3.java b/src/main/java/fr/xephi/authme/security/crypts/WBB3.java index 6cc123005..28e975856 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/WBB3.java +++ b/src/main/java/fr/xephi/authme/security/crypts/WBB3.java @@ -1,9 +1,15 @@ package fr.xephi.authme.security.crypts; import fr.xephi.authme.security.RandomString; +import fr.xephi.authme.security.crypts.description.HasSalt; +import fr.xephi.authme.security.crypts.description.Recommendation; +import fr.xephi.authme.security.crypts.description.SaltType; +import fr.xephi.authme.security.crypts.description.Usage; import static fr.xephi.authme.security.HashUtils.sha1; +@Recommendation(Usage.ACCEPTABLE) +@HasSalt(value = SaltType.TEXT, length = 40) public class WBB3 extends SeparateSaltMethod { @Override diff --git a/src/tools/docs/hash_algorithms.md b/src/tools/docs/hash_algorithms.md new file mode 100644 index 000000000..2996fd8ce --- /dev/null +++ b/src/tools/docs/hash_algorithms.md @@ -0,0 +1,78 @@ + + + +## Hash Algorithms +AuthMe supports the following hash algorithms for storing your passwords safely. + + +Algorithm | Recommendation | Hash length | ASCII | | Salt type | Length | Separate? | +--------- | -------------- | ----------- | ----- | - | --------- | ------ | --------- | +BCRYPT | Recommended | 60 | | | Text | | +BCRYPT2Y | Recommended | 60 | | | Text | 22 | +CRAZYCRYPT1 | Do not use | 128 | | | Username | | +DOUBLEMD5 | Do not use | 32 | | | None | | +IPB3 | Acceptable | 32 | | | Text | 5 | Y +JOOMLA | Recommended | 65 | | | Text | 32 | +MD5 | Do not use | 32 | | | None | | +MD5VB | Acceptable | 56 | | | Text | 16 | +MYBB | Acceptable | 32 | | | Text | 8 | Y +PBKDF2 | Does not work | 329 | | | Text | 12 | +PBKDF2DJANGO | Acceptable | 77 | Y | | Text | 12 | +PHPBB | Acceptable | 34 | | | Text | 16 | +PHPFUSION | Do not use | 64 | Y | | | | Y +ROYALAUTH | Do not use | 128 | | | None | | +SALTED2MD5 | Acceptable | 32 | | | Text | | Y +SALTEDSHA512 | Recommended | 128 | | | | | Y +SHA1 | Do not use | 40 | | | None | | +SHA256 | Recommended | 86 | | | Text | 16 | +SHA512 | Do not use | 128 | | | None | | +SMF | Do not use | 40 | | | Username | | +WBB3 | Acceptable | 40 | | | Text | 40 | Y +WBB4 | Does not work | 60 | | | Text | 8 | +WHIRLPOOL | Do not use | 128 | | | None | | +WORDPRESS | Do not use | 34 | | | Text | 9 | +XAUTH | Recommended | 140 | | | Text | 12 | +CUSTOM | | | | | | | | + + + +### Columns +#### Algorithm +The algorithm is the hashing algorithm used to store passwords with. Default is SHA256 and is recommended. +You can change the hashing algorithm in the config.yml: under `security`, locate `passwordHash`. + +#### Recommendation +The recommendation lists our usage recommendation in terms of how secure it is (not how _well_ the algorithm works!). +- Recommended: The hash algorithm appears to be cryptographically secure and is one we recommend. +- Acceptable: There are safer algorithms that can be chosen but using the algorithm is generally OK. +- Do not use: Hash algorithm isn't sufficiently secure. Use only if required to hook into another system. +- Does not work: The algorithm does not work properly; do not use. + +#### Hash Length +The length of the hashes the algorithm produces. Note that the hash length is not (primarily) indicative of +whether an algorithm is secure or not. + +#### ASCII +If denoted with a **y**, means that the algorithm is restricted to ASCII characters only, i.e. it will simply ignore +"special characters" such as `ÿ` or `Â`. Note that we do not recommend the use of "special characters" in passwords. + +#### Salt Columns +Before hashing, a _salt_ may be appended to the password to make the hash more secure. The following columns describe +the salt the algorithm uses. + + +##### Salt Type +We do not recommend the usage +of any algorithm that doesn't use a randomly generated text as salt. This "salt type" column indicates what type of +salt the algorithm uses: +- Text: randomly generated text (see also the following column, "Length") +- Username: the salt is constructed from the username (bad) +- None: the algorithm uses no salt (bad) + +##### Length +If applicable (salt type is "Text"), indicates the length of the generated salt. The longer the better. +If this column is empty when the salt type is "Text", it typically means the salt length can be defined in config.yml. + +##### Separate +If denoted with a **y**, it means that the salt is stored in a separate column in the database. This is neither good +or bad. diff --git a/src/tools/hashmethods/EncryptionMethodInfoGatherer.java b/src/tools/hashmethods/EncryptionMethodInfoGatherer.java new file mode 100644 index 000000000..07c588ea5 --- /dev/null +++ b/src/tools/hashmethods/EncryptionMethodInfoGatherer.java @@ -0,0 +1,135 @@ +package hashmethods; + +import fr.xephi.authme.security.HashAlgorithm; +import fr.xephi.authme.security.crypts.EncryptionMethod; +import fr.xephi.authme.security.crypts.HexSaltedMethod; +import fr.xephi.authme.security.crypts.description.AsciiRestricted; +import fr.xephi.authme.security.crypts.description.HasSalt; +import fr.xephi.authme.security.crypts.description.Recommendation; + +import java.lang.annotation.Annotation; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import static com.google.common.collect.Sets.newHashSet; + +/** + * Gathers information on {@link fr.xephi.authme.security.crypts.EncryptionMethod} implementations based on + * the annotations in {@link fr.xephi.authme.security.crypts.description}. + */ +public class EncryptionMethodInfoGatherer { + + @SuppressWarnings("unchecked") + private final static Set> RELEVANT_ANNOTATIONS = + newHashSet(HasSalt.class, Recommendation.class, AsciiRestricted.class); + + private Map descriptions; + + public EncryptionMethodInfoGatherer() { + descriptions = new LinkedHashMap<>(); + constructDescriptions(); + } + + public Map getDescriptions() { + return descriptions; + } + + private void constructDescriptions() { + for (HashAlgorithm algorithm : HashAlgorithm.values()) { + Class methodClazz = algorithm.getClazz(); + if (!HashAlgorithm.CUSTOM.equals(algorithm) && !methodClazz.isAnnotationPresent(Deprecated.class)) { + MethodDescription description = createDescription(methodClazz); + descriptions.put(algorithm, description); + } + } + } + + private static MethodDescription createDescription(Class clazz) { + EncryptionMethod method = instantiateMethod(clazz); + MethodDescription description = new MethodDescription(clazz); + description.setHashLength(method.computeHash("test", "user").getHash().length()); + description.setHasSeparateSalt(method.hasSeparateSalt()); + + Map, Annotation> annotationMap = gatherAnnotations(clazz); + if (annotationMap.containsKey(HasSalt.class)) { + setSaltInformation(description, returnTyped(annotationMap, HasSalt.class), method); + } + if (annotationMap.containsKey(Recommendation.class)) { + description.setUsage(returnTyped(annotationMap, Recommendation.class).value()); + } + if (annotationMap.containsKey(AsciiRestricted.class)) { + description.setAsciiRestricted(true); + } + return description; + } + + private static Map, Annotation> gatherAnnotations(Class methodClass) { + // Note ljacqu 20151231: The map could be Map, Annotation> and it has the constraint + // that for a key Class, the value is of type T. We write a simple "Class" for brevity. + Map, Annotation> collection = new HashMap<>(); + Class currentMethodClass = methodClass; + while (currentMethodClass != null) { + getRelevantAnnotations(currentMethodClass, collection); + currentMethodClass = getSuperClass(currentMethodClass); + } + return collection; + } + + // Parameters could be Class; Map, Annotation> + // but the constraint doesn't have any technical relevance, so just clutters the code + private static void getRelevantAnnotations(Class methodClass, Map, Annotation> collection) { + for (Annotation annotation : methodClass.getAnnotations()) { + if (RELEVANT_ANNOTATIONS.contains(annotation.annotationType()) + && !collection.containsKey(annotation.annotationType())) { + collection.put(annotation.annotationType(), annotation); + } + } + } + + /** + * Returns the super class of the given encryption method if it is also of EncryptionMethod type. + * (Anything beyond EncryptionMethod is not of interest.) + */ + private static Class getSuperClass(Class methodClass) { + Class zuper = methodClass.getSuperclass(); + if (EncryptionMethod.class.isAssignableFrom(zuper)) { + return zuper; + } + return null; + } + + /** + * Set the salt information for the given encryption method and the found {@link HasSalt} annotation. + * Also gets the salt length from {@link HexSaltedMethod#getSaltLength()} for such instances. + * + * @param description The description to update + * @param hasSalt The associated HasSalt annotation + * @param method The encryption method + */ + private static void setSaltInformation(MethodDescription description, HasSalt hasSalt, EncryptionMethod method) { + description.setSaltType(hasSalt.value()); + if (hasSalt.length() != 0) { + description.setSaltLength(hasSalt.length()); + } else if (method instanceof HexSaltedMethod) { + int saltLength = ((HexSaltedMethod) method).getSaltLength(); + description.setSaltLength(saltLength); + } + } + + private static EncryptionMethod instantiateMethod(Class clazz) { + try { + return clazz.newInstance(); + } catch (InstantiationException | IllegalAccessException e) { + throw new RuntimeException("Could not instantiate " + clazz, e); + } + } + + // Convenience method for retrieving an annotation in a typed fashion. + // We know implicitly that the key of the map always corresponds to the type of the value + private static T returnTyped(Map, Annotation> map, Class key) { + return key.cast(map.get(key)); + } + +} diff --git a/src/tools/hashmethods/HashAlgorithmsDescriptionTask.java b/src/tools/hashmethods/HashAlgorithmsDescriptionTask.java new file mode 100644 index 000000000..5213d6b7d --- /dev/null +++ b/src/tools/hashmethods/HashAlgorithmsDescriptionTask.java @@ -0,0 +1,100 @@ +package hashmethods; + +import fr.xephi.authme.security.HashAlgorithm; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.util.WrapperMock; +import utils.ANewMap; +import utils.FileUtils; +import utils.TagReplacer; +import utils.ToolTask; +import utils.ToolsConstants; + +import java.util.Map; +import java.util.Scanner; + +/** + * Task for generating the markdown page describing the AuthMe hash algorithms. + * + * @see {@link fr.xephi.authme.security.HashAlgorithm} + */ +public class HashAlgorithmsDescriptionTask implements ToolTask { + + private static final String CUR_FOLDER = ToolsConstants.TOOLS_SOURCE_ROOT + "hashmethods/"; + private static final String OUTPUT_FILE = ToolsConstants.DOCS_FOLDER + "hash_algorithms.md"; + + @Override + public void execute(Scanner scanner) { + // Unfortunately, we need the Wrapper to be around to work with Settings, and certain encryption methods + // directly read from the Settings file + WrapperMock.createInstance(); + Settings.bCryptLog2Rounds = 8; + Settings.saltLength = 8; + + // Gather info and construct a row for each method + EncryptionMethodInfoGatherer infoGatherer = new EncryptionMethodInfoGatherer(); + Map descriptions = infoGatherer.getDescriptions(); + final String methodRows = constructMethodRows(descriptions); + + // Write to the docs file + Map tags = ANewMap.with("method_rows", methodRows).build(); + FileUtils.generateFileFromTemplate(CUR_FOLDER + "hash_algorithms.tpl.md", OUTPUT_FILE, tags); + } + + private static String constructMethodRows(Map descriptions) { + final String rowTemplate = FileUtils.readFromFile(CUR_FOLDER + "hash_algorithms_row.tpl.md"); + StringBuilder result = new StringBuilder(); + for (Map.Entry entry : descriptions.entrySet()) { + MethodDescription description = entry.getValue(); + Map tags = ANewMap + .with("name", asString(entry.getKey())) + .and("recommendation", asString(description.getUsage())) + .and("hash_length", asString(description.getHashLength())) + .and("ascii_restricted", asString(description.isAsciiRestricted())) + .and("salt_type", asString(description.getSaltType())) + .and("salt_length", asString(description.getSaltLength())) + .and("separate_salt", asString(description.hasSeparateSalt())) + .build(); + result.append(TagReplacer.applyReplacements(rowTemplate, tags)); + } + return result.toString(); + } + + @Override + public String getTaskName() { + return "describeHashAlgos"; + } + + // ---- + // String representations + // ---- + private static String asString(boolean value) { + return value ? "Y" : ""; + } + + private static String asString(int value) { + return String.valueOf(value); + } + + private static String asString(Integer value) { + if (value == null) { + return ""; + } + return String.valueOf(value); + } + + private static String asString(HashAlgorithm value) { + return value.toString(); + } + + private static > String asString(E value) { + if (value == null) { + return ""; + } + // Get the enum name and replace something like "DO_NOT_USE" to "Do not use" + String enumName = value.toString().replace("_", " "); + return enumName.length() > 2 + ? enumName.substring(0, 1) + enumName.substring(1).toLowerCase() + : enumName; + } + +} diff --git a/src/tools/hashmethods/MethodDescription.java b/src/tools/hashmethods/MethodDescription.java new file mode 100644 index 000000000..45aaf448e --- /dev/null +++ b/src/tools/hashmethods/MethodDescription.java @@ -0,0 +1,85 @@ +package hashmethods; + +import fr.xephi.authme.security.crypts.EncryptionMethod; +import fr.xephi.authme.security.crypts.description.SaltType; +import fr.xephi.authme.security.crypts.description.Usage; + +/** + * Description of a {@link EncryptionMethod}. + */ +public class MethodDescription { + + /** The implementation class the description belongs to. */ + private final Class method; + /** The type of the salt that is used. */ + private SaltType saltType; + /** The length of the salt for SaltType.TEXT salts. */ + private Integer saltLength; + /** The usage recommendation. */ + private Usage usage; + /** Whether or not the encryption method is restricted to ASCII characters for proper functioning. */ + private boolean asciiRestricted; + /** Whether or not the encryption method requires its salt stored separately. */ + private boolean hasSeparateSalt; + /** The length of the hash output, based on a test hash (i.e. assumes same length for all hashes.) */ + private int hashLength; + + public MethodDescription(Class method) { + this.method = method; + } + + + // Trivial getters and setters + public Class getMethod() { + return method; + } + + public SaltType getSaltType() { + return saltType; + } + + public void setSaltType(SaltType saltType) { + this.saltType = saltType; + } + + public Integer getSaltLength() { + return saltLength; + } + + public void setSaltLength(int saltLength) { + this.saltLength = saltLength; + } + + public Usage getUsage() { + return usage; + } + + public void setUsage(Usage usage) { + this.usage = usage; + } + + public boolean isAsciiRestricted() { + return asciiRestricted; + } + + public void setAsciiRestricted(boolean asciiRestricted) { + this.asciiRestricted = asciiRestricted; + } + + public boolean hasSeparateSalt() { + return hasSeparateSalt; + } + + public void setHasSeparateSalt(boolean hasSeparateSalt) { + this.hasSeparateSalt = hasSeparateSalt; + } + + public int getHashLength() { + return hashLength; + } + + public void setHashLength(int hashLength) { + this.hashLength = hashLength; + } + +} diff --git a/src/tools/hashmethods/hash_algorithms.tpl.md b/src/tools/hashmethods/hash_algorithms.tpl.md new file mode 100644 index 000000000..c11237284 --- /dev/null +++ b/src/tools/hashmethods/hash_algorithms.tpl.md @@ -0,0 +1,53 @@ + + + +## Hash Algorithms +AuthMe supports the following hash algorithms for storing your passwords safely. + + +Algorithm | Recommendation | Hash length | ASCII | | Salt type | Length | Separate? | +--------- | -------------- | ----------- | ----- | - | --------- | ------ | --------- | +{method_rows}CUSTOM | | | | | | | | + + + +### Columns +#### Algorithm +The algorithm is the hashing algorithm used to store passwords with. Default is SHA256 and is recommended. +You can change the hashing algorithm in the config.yml: under `security`, locate `passwordHash`. + +#### Recommendation +The recommendation lists our usage recommendation in terms of how secure it is (not how _well_ the algorithm works!). +- Recommended: The hash algorithm appears to be cryptographically secure and is one we recommend. +- Acceptable: There are safer algorithms that can be chosen but using the algorithm is generally OK. +- Do not use: Hash algorithm isn't sufficiently secure. Use only if required to hook into another system. +- Does not work: The algorithm does not work properly; do not use. + +#### Hash Length +The length of the hashes the algorithm produces. Note that the hash length is not (primarily) indicative of +whether an algorithm is secure or not. + +#### ASCII +If denoted with a **y**, means that the algorithm is restricted to ASCII characters only, i.e. it will simply ignore +"special characters" such as `ÿ` or `Â`. Note that we do not recommend the use of "special characters" in passwords. + +#### Salt Columns +Before hashing, a _salt_ may be appended to the password to make the hash more secure. The following columns describe +the salt the algorithm uses. + + +##### Salt Type +We do not recommend the usage +of any algorithm that doesn't use a randomly generated text as salt. This "salt type" column indicates what type of +salt the algorithm uses: +- Text: randomly generated text (see also the following column, "Length") +- Username: the salt is constructed from the username (bad) +- None: the algorithm uses no salt (bad) + +##### Length +If applicable (salt type is "Text"), indicates the length of the generated salt. The longer the better. +If this column is empty when the salt type is "Text", it typically means the salt length can be defined in config.yml. + +##### Separate +If denoted with a **y**, it means that the salt is stored in a separate column in the database. This is neither good +or bad. diff --git a/src/tools/hashmethods/hash_algorithms_row.tpl.md b/src/tools/hashmethods/hash_algorithms_row.tpl.md new file mode 100644 index 000000000..411d11271 --- /dev/null +++ b/src/tools/hashmethods/hash_algorithms_row.tpl.md @@ -0,0 +1 @@ +{name} | {recommendation} | {hash_length} | {ascii_restricted} | | {salt_type} | {salt_length} | {separate_salt}