Updates password comparison to use length obfuscation alg.
diff --git a/core/src/main/java/org/apache/ftpserver/usermanager/ClearTextPasswordEncryptor.java b/core/src/main/java/org/apache/ftpserver/usermanager/ClearTextPasswordEncryptor.java
index e9dc49e..9792bff 100644
--- a/core/src/main/java/org/apache/ftpserver/usermanager/ClearTextPasswordEncryptor.java
+++ b/core/src/main/java/org/apache/ftpserver/usermanager/ClearTextPasswordEncryptor.java
@@ -19,8 +19,7 @@
package org.apache.ftpserver.usermanager;
-
-
+import org.apache.ftpserver.util.PasswordUtil;
/**
* Password encryptor that does no encryption, that is, keps the
@@ -48,7 +47,7 @@
throw new NullPointerException("passwordToCheck can not be null");
}
- return passwordToCheck.equals(storedPassword);
+ return PasswordUtil.secureCompareFast(passwordToCheck, storedPassword);
}
}
diff --git a/core/src/main/java/org/apache/ftpserver/usermanager/Md5PasswordEncryptor.java b/core/src/main/java/org/apache/ftpserver/usermanager/Md5PasswordEncryptor.java
index 3401eb2..a19d6f1 100644
--- a/core/src/main/java/org/apache/ftpserver/usermanager/Md5PasswordEncryptor.java
+++ b/core/src/main/java/org/apache/ftpserver/usermanager/Md5PasswordEncryptor.java
@@ -20,35 +20,34 @@
package org.apache.ftpserver.usermanager;
import org.apache.ftpserver.util.EncryptUtils;
-
+import org.apache.ftpserver.util.PasswordUtil;
/**
- * Password encryptor that hashes the password using MD5. Please note that this form
- * of encryption is sensitive to lookup attacks.
+ * Password encryptor that hashes the password using MD5. Please note that this
+ * form of encryption is sensitive to lookup attacks.
*
* @author <a href="http://mina.apache.org">Apache MINA Project</a>
*/
public class Md5PasswordEncryptor implements PasswordEncryptor {
- /**
- * Hashes the password using MD5
- */
- public String encrypt(String password) {
- return EncryptUtils.encryptMD5(password);
- }
+ /**
+ * Hashes the password using MD5
+ */
+ public String encrypt(String password) {
+ return EncryptUtils.encryptMD5(password);
+ }
- /**
- * {@inheritDoc}
- */
- public boolean matches(String passwordToCheck, String storedPassword) {
- if(storedPassword == null) {
- throw new NullPointerException("storedPassword can not be null");
- }
- if(passwordToCheck == null) {
- throw new NullPointerException("passwordToCheck can not be null");
- }
-
- return encrypt(passwordToCheck).equalsIgnoreCase(storedPassword);
- }
+ /**
+ * {@inheritDoc}
+ */
+ public boolean matches(String passwordToCheck, String storedPassword) {
+ if (storedPassword == null) {
+ throw new NullPointerException("storedPassword can not be null");
+ }
+ if (passwordToCheck == null) {
+ throw new NullPointerException("passwordToCheck can not be null");
+ }
+ return PasswordUtil.secureCompareFast(encrypt(passwordToCheck).toLowerCase(), storedPassword.toLowerCase());
+ }
}
diff --git a/core/src/main/java/org/apache/ftpserver/usermanager/SaltedPasswordEncryptor.java b/core/src/main/java/org/apache/ftpserver/usermanager/SaltedPasswordEncryptor.java
index 1cd1bf5..15d13ac 100644
--- a/core/src/main/java/org/apache/ftpserver/usermanager/SaltedPasswordEncryptor.java
+++ b/core/src/main/java/org/apache/ftpserver/usermanager/SaltedPasswordEncryptor.java
@@ -22,11 +22,12 @@
import java.security.SecureRandom;
import org.apache.ftpserver.util.EncryptUtils;
+import org.apache.ftpserver.util.PasswordUtil;
/**
- * Password encryptor that hashes a salt together with the password using MD5.
- * Using a salt protects against birthday attacks.
- * The hashing is also made in iterations, making lookup attacks much harder.
+ * Password encryptor that hashes a salt together with the password using MD5.
+ * Using a salt protects against birthday attacks. The hashing is also made in
+ * iterations, making lookup attacks much harder.
*
* The algorithm is based on the principles described in
* http://www.jasypt.org/howtoencryptuserpasswords.html
@@ -35,50 +36,51 @@
*/
public class SaltedPasswordEncryptor implements PasswordEncryptor {
- private SecureRandom rnd = new SecureRandom();
+ private SecureRandom rnd = new SecureRandom();
- private static final int MAX_SEED = 99999999;
- private static final int HASH_ITERATIONS = 1000;
+ private static final int MAX_SEED = 99999999;
+ private static final int HASH_ITERATIONS = 1000;
- private String encrypt(String password, String salt) {
- String hash = salt + password;
- for (int i = 0; i < HASH_ITERATIONS; i++) {
- hash = EncryptUtils.encryptMD5(hash);
- }
- return salt + ":" + hash;
- }
+ private String encrypt(String password, String salt) {
+ String hash = salt + password;
+ for (int i = 0; i < HASH_ITERATIONS; i++) {
+ hash = EncryptUtils.encryptMD5(hash);
+ }
+ return salt + ":" + hash;
+ }
- /**
- * Encrypts the password using a salt concatenated with the password
- * and a series of MD5 steps.
- */
- public String encrypt(String password) {
- String seed = Integer.toString(rnd.nextInt(MAX_SEED));
+ /**
+ * Encrypts the password using a salt concatenated with the password and a
+ * series of MD5 steps.
+ */
+ public String encrypt(String password) {
+ String seed = Integer.toString(rnd.nextInt(MAX_SEED));
- return encrypt(password, seed);
- }
+ return encrypt(password, seed);
+ }
- /**
- * {@inheritDoc}
- */
- public boolean matches(String passwordToCheck, String storedPassword) {
- if (storedPassword == null) {
- throw new NullPointerException("storedPassword can not be null");
- }
- if (passwordToCheck == null) {
- throw new NullPointerException("passwordToCheck can not be null");
- }
+ /**
+ * {@inheritDoc}
+ */
+ public boolean matches(String passwordToCheck, String storedPassword) {
+ if (storedPassword == null) {
+ throw new NullPointerException("storedPassword can not be null");
+ }
+ if (passwordToCheck == null) {
+ throw new NullPointerException("passwordToCheck can not be null");
+ }
- // look for divider for hash
- int divider = storedPassword.indexOf(':');
-
- if(divider < 1) {
- throw new IllegalArgumentException("stored password does not contain salt");
- }
-
- String storedSalt = storedPassword.substring(0, divider);
-
- return encrypt(passwordToCheck, storedSalt).equalsIgnoreCase(storedPassword);
- }
+ // look for divider for hash
+ int divider = storedPassword.indexOf(':');
+
+ if (divider < 1) {
+ throw new IllegalArgumentException("stored password does not contain salt");
+ }
+
+ String storedSalt = storedPassword.substring(0, divider);
+
+ return PasswordUtil.secureCompareFast(encrypt(passwordToCheck, storedSalt).toLowerCase(),
+ storedPassword.toLowerCase());
+ }
}
diff --git a/core/src/main/java/org/apache/ftpserver/util/PasswordUtil.java b/core/src/main/java/org/apache/ftpserver/util/PasswordUtil.java
new file mode 100644
index 0000000..62fdc75
--- /dev/null
+++ b/core/src/main/java/org/apache/ftpserver/util/PasswordUtil.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.ftpserver.util;
+
+public class PasswordUtil {
+ /**
+ * Securely compares two strings up to a maximum number of characters in a way
+ * that obscures the password length from timing attacks
+ *
+ * @param input
+ * user input
+ * @param password
+ * correct password
+ * @param loops
+ * number of characters to compare; must be larger than password
+ * length; 1024 is a good number
+ *
+ * @throws IllegalArgumentException
+ * when the limit is less than the password length
+ *
+ * @return true if the passwords match
+ */
+ public static boolean secureCompare(String input, String password, int loops) {
+ if (loops < password.length()) {
+ throw new IllegalArgumentException("loops must be equal or greater than the password length");
+ }
+ /*
+ * Set the default result based on the string lengths; if the lengths do not
+ * match then we know that this comparison should always fail.
+ */
+ int result = (input.length() ^ password.length());
+ /*
+ * Cycle through all of the characters up to the limit value
+ *
+ * Important to note that this loop may return a false positive comparison if
+ * the target string is a repeating set of characters in direct multiples of the
+ * input string. This design fallacy is corrected by the original length
+ * comparison above. The use of modulo this way is intended to prevent compiler
+ * and memory optimizations for retrieving the same char position in sequence.
+ */
+ for (int i = 0; i < loops; i++) {
+ result |= (input.charAt(i % input.length()) ^ password.charAt(i % password.length()));
+ }
+
+ return (result == 0);
+ }
+
+ /**
+ * Securely compares two strings forcing the number of loops equal to password length
+ * thereby obscuring the password length based on user input
+ *
+ * @param input
+ * user input
+ * @param password
+ * correct password
+ * @throws IllegalArgumentException
+ * when the limit is less than the password length
+ *
+ * @return true if the passwords match
+ */
+ public static boolean secureCompareFast(String input, String password) {
+ /*
+ * the number of compare loops
+ */
+ int loops = password.length();
+ /*
+ * Set the default result based on the string lengths; if the lengths do not
+ * match then we know that this comparison should always fail.
+ */
+ int result = (input.length() ^ password.length());
+ /*
+ * Cycle through all of the characters up to the limit value
+ *
+ * Important to note that this loop may return a false positive comparison if
+ * the target string is a repeating set of characters in direct multiples of the
+ * input string. This design fallacy is corrected by the original length
+ * comparison above. The use of modulo this way is intended to prevent compiler
+ * and memory optimizations for retrieving the same char position in sequence.
+ */
+ for (int i = 0; i < loops; i++) {
+ result |= (input.charAt(i % input.length()) ^ password.charAt(i % password.length()));
+ }
+
+ return (result == 0);
+ }
+}