From bd5d341e67a6f8ef222d0c25d17da8c05954fd20 Mon Sep 17 00:00:00 2001 From: DNx5 Date: Thu, 31 Dec 2015 11:05:18 +0700 Subject: [PATCH] Xenforo support. - Added getPassword method in DataSource and all implementations. --- .../authme/datasource/CacheDataSource.java | 11 +++ .../xephi/authme/datasource/DataSource.java | 10 +++ .../fr/xephi/authme/datasource/FlatFile.java | 10 +++ .../fr/xephi/authme/datasource/MySQL.java | 43 +++++++++--- .../fr/xephi/authme/datasource/SQLite.java | 30 +++++++- .../authme/security/PasswordSecurity.java | 8 +-- .../xephi/authme/security/crypts/BCRYPT.java | 40 ++++++----- .../security/crypts/HashedPassword.java | 8 +-- .../fr/xephi/authme/security/crypts/XF.java | 70 +++---------------- 9 files changed, 130 insertions(+), 100 deletions(-) diff --git a/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java b/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java index 2351c6cb..2887b9ee 100644 --- a/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java +++ b/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java @@ -9,6 +9,7 @@ import com.google.common.cache.RemovalListeners; import com.google.common.cache.RemovalNotification; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.cache.auth.PlayerCache; +import fr.xephi.authme.security.crypts.HashedPassword; import java.util.ArrayList; import java.util.List; @@ -65,6 +66,16 @@ public class CacheDataSource implements DataSource { return getAuth(user) != null; } + @Override + public HashedPassword getPassword(String user) { + user = user.toLowerCase(); + Optional pAuthOpt = cachedAuths.getIfPresent(user); + if (pAuthOpt != null && pAuthOpt.isPresent()) { + return pAuthOpt.get().getPassword(); + } + return source.getPassword(user); + } + /** * Method getAuth. * diff --git a/src/main/java/fr/xephi/authme/datasource/DataSource.java b/src/main/java/fr/xephi/authme/datasource/DataSource.java index 10f74b92..9e5f051a 100644 --- a/src/main/java/fr/xephi/authme/datasource/DataSource.java +++ b/src/main/java/fr/xephi/authme/datasource/DataSource.java @@ -1,6 +1,7 @@ package fr.xephi.authme.datasource; import fr.xephi.authme.cache.auth.PlayerAuth; +import fr.xephi.authme.security.crypts.HashedPassword; import java.util.List; @@ -17,6 +18,15 @@ public interface DataSource { */ boolean isAuthAvailable(String user); + /** + * Method getPassword. + * + * @param user String + * + * @return String + */ + HashedPassword getPassword(String user); + /** * Method getAuth. * diff --git a/src/main/java/fr/xephi/authme/datasource/FlatFile.java b/src/main/java/fr/xephi/authme/datasource/FlatFile.java index 55646ddc..da18625b 100644 --- a/src/main/java/fr/xephi/authme/datasource/FlatFile.java +++ b/src/main/java/fr/xephi/authme/datasource/FlatFile.java @@ -14,6 +14,7 @@ import fr.xephi.authme.AuthMe; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.cache.auth.PlayerCache; +import fr.xephi.authme.security.crypts.HashedPassword; import fr.xephi.authme.settings.Settings; /** @@ -87,6 +88,15 @@ public class FlatFile implements DataSource { return false; } + @Override + public HashedPassword getPassword(String user) { + PlayerAuth auth = getAuth(user); + if (auth != null) { + return auth.getPassword(); + } + return null; + } + /** * Method saveAuth. * diff --git a/src/main/java/fr/xephi/authme/datasource/MySQL.java b/src/main/java/fr/xephi/authme/datasource/MySQL.java index e0c91955..e3ce9b5f 100644 --- a/src/main/java/fr/xephi/authme/datasource/MySQL.java +++ b/src/main/java/fr/xephi/authme/datasource/MySQL.java @@ -6,10 +6,18 @@ import fr.xephi.authme.AuthMe; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.security.HashAlgorithm; +import fr.xephi.authme.security.crypts.HashedPassword; +import fr.xephi.authme.security.crypts.XF; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.util.StringUtils; -import java.sql.*; +import java.sql.Blob; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; import java.util.ArrayList; import java.util.List; @@ -244,6 +252,25 @@ public class MySQL implements DataSource { return false; } + @Override + public HashedPassword getPassword(String user) { + try (Connection con = getConnection()) { + String sql = "SELECT " + columnPassword + "," + columnSalt + " FROM " + tableName + + " WHERE " + columnName + "=?;"; + PreparedStatement pst = con.prepareStatement(sql); + pst.setString(1, user.toLowerCase()); + ResultSet rs = pst.executeQuery(); + if (rs.next()) { + return new HashedPassword(rs.getString(columnPassword), + !columnSalt.isEmpty() ? rs.getString(columnSalt) : null); + } + } catch (SQLException ex) { + ConsoleLogger.showError(ex.getMessage()); + ConsoleLogger.writeStackTrace(ex); + } + return null; + } + @Override public synchronized PlayerAuth getAuth(String user) { PlayerAuth pAuth; @@ -280,8 +307,7 @@ public class MySQL implements DataSource { if (rs.next()) { Blob blob = rs.getBlob("data"); byte[] bytes = blob.getBytes(1, (int) blob.length()); - // TODO #137: Need to find out how the salt is loaded and need to pass it along to setHash() - // pAuth.setPassword(new String(bytes)); + pAuth.setPassword(new HashedPassword(XF.getHashFromBlob(bytes))); } } } catch (SQLException ex) { @@ -470,11 +496,9 @@ public class MySQL implements DataSource { rs = pst.executeQuery(); if (rs.next()) { int id = rs.getInt(columnID); - // Insert password in the correct table pst2 = con.prepareStatement("INSERT INTO xf_user_authenticate (user_id, scheme_class, data) VALUES (?,?,?);"); pst2.setInt(1, id); pst2.setString(2, "XenForo_Authentication_Core12"); - // TODO #137: Need to verify that the salt info is also being passed on... byte[] bytes = auth.getPassword().getHash().getBytes(); Blob blob = con.createBlob(); blob.setBytes(1, bytes); @@ -524,7 +548,6 @@ public class MySQL implements DataSource { // Insert password in the correct table sql = "UPDATE xf_user_authenticate SET data=? WHERE " + columnID + "=?;"; PreparedStatement pst2 = con.prepareStatement(sql); - // TODO #137: What about the salt? byte[] bytes = auth.getPassword().getHash().getBytes(); Blob blob = con.createBlob(); blob.setBytes(1, bytes); @@ -757,7 +780,7 @@ public class MySQL implements DataSource { } @Override - public synchronized List getAllAuthsByEmail(String email){ + public synchronized List getAllAuthsByEmail(String email) { List countEmail = new ArrayList<>(); try (Connection con = getConnection()) { String sql = "SELECT " + columnName + " FROM " + tableName + " WHERE " + columnEmail + "=?;"; @@ -920,8 +943,7 @@ public class MySQL implements DataSource { if (rs2.next()) { Blob blob = rs2.getBlob("data"); byte[] bytes = blob.getBytes(1, (int) blob.length()); - // TODO #137: Need to pass the hash and the salt here - // pAuth.setPassword(new String(bytes)); + pAuth.setPassword(new HashedPassword(XF.getHashFromBlob(bytes))); } rs2.close(); } @@ -968,8 +990,7 @@ public class MySQL implements DataSource { if (rs2.next()) { Blob blob = rs2.getBlob("data"); byte[] bytes = blob.getBytes(1, (int) blob.length()); - // TODO #137: Need to pass the hash and the salt here - // pAuth.setHash(new String(bytes)); + pAuth.setPassword(new HashedPassword(XF.getHashFromBlob(bytes))); } rs2.close(); } diff --git a/src/main/java/fr/xephi/authme/datasource/SQLite.java b/src/main/java/fr/xephi/authme/datasource/SQLite.java index 474c5035..1eef222f 100644 --- a/src/main/java/fr/xephi/authme/datasource/SQLite.java +++ b/src/main/java/fr/xephi/authme/datasource/SQLite.java @@ -6,7 +6,12 @@ import fr.xephi.authme.security.crypts.HashedPassword; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.util.StringUtils; -import java.sql.*; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; import java.util.ArrayList; import java.util.List; @@ -167,6 +172,29 @@ public class SQLite implements DataSource { } } + @Override + public HashedPassword getPassword(String user) { + PreparedStatement pst = null; + ResultSet rs = null; + try { + pst = con.prepareStatement("SELECT " + columnPassword + "," + columnSalt + + " FROM " + tableName + " WHERE " + columnName + "=?"); + pst.setString(1, user); + rs = pst.executeQuery(); + if (rs.next()) { + return new HashedPassword(rs.getString(columnPassword), + !columnSalt.isEmpty() ? rs.getString(columnSalt) : null); + } + } catch (SQLException ex) { + ConsoleLogger.showError(ex.getMessage()); + ConsoleLogger.writeStackTrace(ex); + } finally { + close(rs); + close(pst); + } + return null; + } + /** * Method getAuth. * diff --git a/src/main/java/fr/xephi/authme/security/PasswordSecurity.java b/src/main/java/fr/xephi/authme/security/PasswordSecurity.java index 56ed1e4c..75a1fcdc 100644 --- a/src/main/java/fr/xephi/authme/security/PasswordSecurity.java +++ b/src/main/java/fr/xephi/authme/security/PasswordSecurity.java @@ -36,12 +36,8 @@ public class PasswordSecurity { } public boolean comparePassword(String password, String playerName) { - // TODO ljacqu 20151230: Defining a dataSource.getPassword() method would be more efficient - PlayerAuth auth = dataSource.getAuth(playerName); - if (auth != null) { - return comparePassword(password, auth.getPassword(), playerName); - } - return false; + HashedPassword auth = dataSource.getPassword(playerName); + return auth != null && comparePassword(password, auth, playerName); } public boolean comparePassword(String password, HashedPassword hashedPassword, String playerName) { diff --git a/src/main/java/fr/xephi/authme/security/crypts/BCRYPT.java b/src/main/java/fr/xephi/authme/security/crypts/BCRYPT.java index b7602e7f..eb8c717a 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/BCRYPT.java +++ b/src/main/java/fr/xephi/authme/security/crypts/BCRYPT.java @@ -15,9 +15,9 @@ package fr.xephi.authme.security.crypts; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.security.crypts.description.HasSalt; -import fr.xephi.authme.security.crypts.description.Usage; 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 fr.xephi.authme.settings.Settings; import fr.xephi.authme.util.StringUtils; @@ -25,43 +25,45 @@ import java.io.UnsupportedEncodingException; import java.security.SecureRandom; /** + *

* BCrypt implements OpenBSD-style Blowfish password hashing using the scheme * described in "A Future-Adaptable Password Scheme" by Niels Provos and David * Mazieres. - *

+ *

* This password hashing system tries to thwart off-line password cracking using * a computationally-intensive hashing algorithm, based on Bruce Schneier's * Blowfish cipher. The work factor of the algorithm is parameterised, so it can * be increased as computers get faster. - *

+ *

* Usage is really simple. To hash a password for the first time, call the * hashpw method with a random salt, like this: - *

+ *

* - * String pw_hash = BCrypt.hashpw(plain_password, BCrypt.gensalt());
+ * String pw_hash = BCrypt.hashpw(plain_password, BCrypt.gensalt());
*
- *

+ *

* To check whether a plaintext password matches one that has been hashed * previously, use the checkpw method: - *

+ *

* - * if (BCrypt.checkpw(candidate_password, stored_hash))
- *     System.out.println("It matches");
- * else
- *     System.out.println("It does not match");
+ * if (BCrypt.checkpw(candidate_password, stored_hash))
+ *     System.out.println("It matches");
+ * else
+ *     System.out.println("It does not match");
*
- *

+ *

* The gensalt() method takes an optional parameter (log_rounds) that determines * the computational complexity of the hashing: - *

+ *

* - * String strong_salt = BCrypt.gensalt(10)
- * String stronger_salt = BCrypt.gensalt(12)
+ * String strong_salt = BCrypt.gensalt(10)
+ * String stronger_salt = BCrypt.gensalt(12)
*
- *

+ *

* The amount of work increases exponentially (2**log_rounds), so each increment * is twice as much work. The default log_rounds is 10, and the valid range is 4 * to 31. + *

* * @author Damien Miller * @version 0.2 @@ -102,8 +104,11 @@ public class BCRYPT implements EncryptionMethod { * @param d the byte array to encode * @param len the number of bytes to encode * - * @return base64-encoded string * @throws IllegalArgumentException if the length is invalid * @throws IllegalArgumentException + * @return base64-encoded string + * + * @throws IllegalArgumentException if the length is invalid */ + private static String encode_base64(byte d[], int len) throws IllegalArgumentException { int off = 0; @@ -160,6 +165,7 @@ public class BCRYPT implements EncryptionMethod { * @param maxolen the maximum number of bytes to decode * * @return an array containing the decoded bytes + * * @throws IllegalArgumentException if maxolen is invalid */ private static byte[] decode_base64(String s, int maxolen) diff --git a/src/main/java/fr/xephi/authme/security/crypts/HashedPassword.java b/src/main/java/fr/xephi/authme/security/crypts/HashedPassword.java index ab1a7eeb..f2e40aa4 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/HashedPassword.java +++ b/src/main/java/fr/xephi/authme/security/crypts/HashedPassword.java @@ -4,7 +4,7 @@ package fr.xephi.authme.security.crypts; * The result of a hash computation. See {@link #salt} for details. */ public class HashedPassword { - + /** The generated hash. */ private final String hash; /** @@ -36,13 +36,13 @@ public class HashedPassword { public HashedPassword(String hash) { this(hash, null); } - + public String getHash() { return hash; } - + public String getSalt() { return salt; } - + } diff --git a/src/main/java/fr/xephi/authme/security/crypts/XF.java b/src/main/java/fr/xephi/authme/security/crypts/XF.java index 2a74d2fa..cd3bcf16 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/XF.java +++ b/src/main/java/fr/xephi/authme/security/crypts/XF.java @@ -1,74 +1,22 @@ package fr.xephi.authme.security.crypts; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; -public class XF implements EncryptionMethod { - - @Override - public String computeHash(String password, String salt, String name) { - return getSha256(getSha256(password) + regmatch("\"salt\";.:..:\"(.*)\";.:.:\"hashFunc\"", salt)); - } - - @Override - public HashedPassword computeHash(String password, String name) { - String salt = generateSalt(); - return new HashedPassword(computeHash(password, salt, null), salt); - } - - @Override - public boolean comparePassword(String password, HashedPassword hashedPassword, String name) { - // TODO #137: Write the comparePassword method. See commit 121d323 for what was here previously; it was - // utter non-sense - return false; - } - - // TODO #137: If this method corresponds to HashUtils.sha256(), use it instead of this - private String getSha256(String password) { - MessageDigest md = null; - try { - md = MessageDigest.getInstance("SHA-256"); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - md.update(password.getBytes()); - byte byteData[] = md.digest(); - StringBuilder sb = new StringBuilder(); - for (byte element : byteData) { - sb.append(Integer.toString((element & 0xff) + 0x100, 16).substring(1)); - } - StringBuilder hexString = new StringBuilder(); - for (byte element : byteData) { - String hex = Integer.toHexString(0xff & element); - if (hex.length() == 1) { - hexString.append('0'); - } - hexString.append(hex); - } - return hexString.toString(); - } +public class XF extends BCRYPT { + private static final Pattern HASH_PATTERN = Pattern.compile("\"hash\";s.*\"(.*)?\""); @Override public String generateSalt() { - // TODO #137: Find out what kind of salt format XF uses to generate new passwords - return ""; + return BCRYPT.gensalt(); } - @Override - public boolean hasSeparateSalt() { - return true; - } - - private String regmatch(String pattern, String line) { - List allMatches = new ArrayList<>(); - Matcher m = Pattern.compile(pattern).matcher(line); - while (m.find()) { - allMatches.add(m.group(1)); + public static String getHashFromBlob(byte[] blob) { + String line = new String(blob); + Matcher m = HASH_PATTERN.matcher(line); + if (m.find()) { + return m.group(1); } - return allMatches.get(0); + return "*"; // what? } }