package dbProcs; import de.mkammerer.argon2.Argon2; import de.mkammerer.argon2.Argon2Factory; import java.io.IOException; import java.sql.CallableStatement; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Timestamp; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Locale; import java.util.ResourceBundle; import javax.sql.rowset.CachedRowSet; import javax.sql.rowset.RowSetProvider; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.json.JSONArray; import org.json.JSONObject; import org.owasp.encoder.Encode; import servlets.Register; import utils.ModulePlan; import utils.ScoreboardStatus; /** * Used to retrieve information from the Database
*
* This file is part of the Security Shepherd Project. * *

The Security Shepherd project is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software Foundation, either * version 3 of the License, or (at your option) any later version.
* *

The Security Shepherd project is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * PURPOSE. See the GNU General Public License for more details.
* *

You should have received a copy of the GNU General Public License along with the Security * Shepherd project. If not, see . * * @author Mark */ public class Getter { private static final Logger log = LogManager.getLogger(Getter.class); /** Used for scoreboards / progress bars */ private static final int widthOfUnitBar = 11; // px private static final int fieldTrainingCap = 45; private static final int privateCap = 80; private static final int corporalCap = 105; private static final int sergeantCap = 130; private static final int lieutenantCap = 145; private static final int majorCap = 175; private static final int admiralCap = 999; // everything above Major is Admiral /** * Safety cap on the number of rows materialized by {@link #getClassInfo}, {@link * #getPlayersByClass}, and {@link #getAdmins}. These methods return a {@link * javax.sql.rowset.CachedRowSet}, which holds every row in memory. Realistic workloads are well * under this limit (tens to a few hundred per call); the cap exists to fail fast on a runaway * query rather than OOM the JVM. Tracked for proper bounded-collection conversion in #839. */ private static final int MAX_ROWSET_ROWS = 10_000; /** * This method hashes the user submitted password and sends it to the database. The database does * the rest of the work, including Brute Force prevention. * * @param userName The submitted user name to be used in authentication process * @param password The submitted password in plain text to be used in authentication * @return A string array made up of nothing or information to be consumed by the initiating * authentication process. */ public static String[] authUser(String ApplicationRoot, String userName, String password) { log.debug("$$$ Getter.authUser $$$"); log.debug("userName = " + userName); // Phase 1: Fetch user record (short DB hold, ~1-5ms) String userId; String dbUserName; String dbHash; String userRole; int badLoginCount; boolean tempPassword; String classId; Timestamp suspendedUntil; String loginType; boolean tempUsername; try (Connection conn = Database.getCoreConnection(ApplicationRoot); PreparedStatement prestmt = conn.prepareStatement( "SELECT userId, userName, userPass, userRole, badLoginCount, tempPassword, classId," + " suspendedUntil, loginType, tempUsername FROM `users` WHERE userName = ?")) { prestmt.setString(1, userName); try (ResultSet userResult = prestmt.executeQuery()) { if (userResult.next()) { log.debug("User Found"); userId = userResult.getString(1); dbUserName = userResult.getString(2); dbHash = userResult.getString(3); userRole = userResult.getString(4); badLoginCount = userResult.getInt(5); tempPassword = userResult.getBoolean(6); classId = userResult.getString(7); suspendedUntil = userResult.getTimestamp(8); loginType = userResult.getString(9); tempUsername = userResult.getBoolean(10); } else { log.debug("User did not exist"); log.debug("$$$ End authUser $$$"); return null; } } } catch (SQLException e) { log.fatal("authUser failed: " + e.toString()); throw new RuntimeException(e); } // Connection released — all user data extracted into local variables // Fail-fast: reject suspended and SSO users before expensive Argon2 work if (!"login".equals(loginType)) { log.debug("User is SSO user, can't login with password!"); return null; } Timestamp currentTime = new Timestamp(System.currentTimeMillis()); if (suspendedUntil != null && suspendedUntil.after(currentTime)) { return null; } // Phase 2: Verify password (CPU-bound Argon2, no DB connection held) log.debug("Verifying hash"); Argon2 argon2 = Argon2Factory.create(); boolean userVerified = argon2.verify(dbHash, password.toCharArray()); if (!userVerified) { log.debug("Hash did not match, authentication failed"); log.debug("$$$ End authUser $$$"); return null; } // Phase 3: Post-verification DB updates (short DB hold if needed) log.debug("Hash matches"); if (!dbUserName.equalsIgnoreCase(userName)) { log.fatal( "User Name used (" + userName + ") and User Name retrieved (" + dbUserName + ") were not the Same. Nulling Result"); return null; } log.debug("User '" + userName + "' has logged in"); if (badLoginCount > 0) { log.debug("Clearing Bad Login History"); try (Connection conn = Database.getCoreConnection(ApplicationRoot); CallableStatement callstmt = conn.prepareCall("call userBadLoginReset(?)")) { callstmt.setString(1, userId); callstmt.execute(); log.debug("userBadLoginReset executed!"); } catch (SQLException e) { log.fatal("Could not reset bad login count: " + e.toString()); throw new RuntimeException(e); } } String[] result = new String[6]; result[0] = userId; result[1] = dbUserName; result[2] = userRole; result[3] = Boolean.toString(tempPassword); result[4] = classId; result[5] = Boolean.toString(tempUsername); log.debug("$$$ End authUser $$$"); return result; } /** * This method hashes the user submitted password and sends it to the database. The database does * the rest of the work, including Brute Force prevention. * * @param ApplicationRoot The current running context of an application * @param userName The submitted user name to be used in authentication process * @param ssoName * @param password The submitted password in plain text to be used in authentication * @return A string array made up of nothing or information to be consumed by the initiating * authentication process. */ public static String[] authUserSSO( String ApplicationRoot, String classId, String userName, String ssoName, String userRole) { log.debug("$$$ Getter.authUserSSO $$$"); log.debug("ssoName = " + ssoName); log.debug("userName = " + userName); String[] result = new String[6]; String userID = new String(); String newUsername = null; String defaultClass = Register.getDefaultClass(); boolean userFound = false; boolean isTempUsername = false; try { // Phase 1: See if the user already exists, and capture suspension data if so. // Each phase opens its own pooled connection in try-with-resources so the // connection is never held across the Setter.userCreateSSO call (Phase 2). Timestamp suspendedUntil = null; try (Connection conn = Database.getCoreConnection(ApplicationRoot); PreparedStatement prestmt = conn.prepareStatement( "SELECT suspendedUntil FROM `users` WHERE ssoName = ? AND loginType='saml'")) { prestmt.setString(1, ssoName); log.debug("Gathering userFind ResultSet"); try (ResultSet userResult = prestmt.executeQuery()) { log.debug("Opening Result Set from userResult"); if (userResult.next()) { // User found if a row is in the database userFound = true; log.debug("User Found"); suspendedUntil = userResult.getTimestamp(1); } else { userFound = false; } } } if (!userFound) { // Phase 2: User wasn't found, enroll them in database. No pooled // connection is held here; Setter.userCreateSSO borrows its own. boolean userCreated = false; log.debug("User did not exist, create it from SSO data"); try { if (defaultClass.isEmpty()) { log.debug("Adding player to database, with null classId"); newUsername = Setter.userCreateSSO(ApplicationRoot, null, userName, ssoName, userRole); } else // defaultClass is not empty, so It must be set to a class! { log.debug("Adding player to database, to class " + defaultClass); newUsername = Setter.userCreateSSO(ApplicationRoot, defaultClass, userName, ssoName, userRole); } if (newUsername == null) { userCreated = false; } else { userCreated = true; } userName = newUsername; } catch (SQLException e) { String message = "Could not create user " + userName + " with ssoName " + ssoName + " via SSO: " + e.toString(); log.fatal(message); throw new RuntimeException(message); } if (!userCreated) { String message = "Could not create user " + userName + " with ssoName " + ssoName + " via SSO"; log.fatal(message); throw new RuntimeException(message); } log.debug("User created"); } else { log.debug("Getting suspension data"); // Get current system time Timestamp currentTime = new Timestamp(System.currentTimeMillis()); if (suspendedUntil.after(currentTime)) { // User is suspended log.debug("User is suspended"); result = null; return result; } } // Phase 3: Find the generated userID and username by asking the database. try (Connection conn = Database.getCoreConnection(ApplicationRoot); PreparedStatement prestmt = conn.prepareStatement( "SELECT userId, userName, classID, tempUsername FROM `users` WHERE ssoName = ?" + " AND loginType='saml'")) { prestmt.setString(1, ssoName); log.debug("Gathering userResult ResultSet"); try (ResultSet userResult = prestmt.executeQuery()) { log.debug("Opening user list result set"); if (userResult.next()) { userFound = true; log.debug("User Found"); } else { userFound = false; } if (!userFound) { // If user wasn't found at this stage something is quite wrong, so exit // forcefully String message = "User wasn't found after being added!"; log.fatal(message); throw new RuntimeException(message); } userID = userResult.getString(1); userName = userResult.getString(2); classId = userResult.getString(3); // classId isTempUsername = userResult.getBoolean(4); } } log.debug("User '" + userName + "' has logged in via SSO" + " with role " + userRole); result[0] = userID; result[1] = userName; // userName result[2] = userRole; // role result[5] = "false"; // sso logins can't change password result[4] = classId; // classId result[5] = Boolean.toString(isTempUsername); log.debug("$$$ End authUser $$$"); return result; } catch (SQLException e) { log.fatal("authUserSSO failed: " + e.toString()); throw new RuntimeException(e); } } /** * Used to determine if a user has completed a module already * * @param ApplicationRoot The current running context of an application * @param moduleId The module identifier * @param userId The user identifier * @return The module name of the module IF the user has not completed AND the user has previously * opened the challenge. */ public static String checkPlayerResult(String ApplicationRoot, String moduleId, String userId) { log.debug("*** Getter.checkPlayerResult ***"); String result = null; try (Connection conn = Database.getCoreConnection(ApplicationRoot); CallableStatement callstmnt = conn.prepareCall("call userCheckResult(?, ?)")) { log.debug("Preparing userCheckResult call"); callstmnt.setString(1, moduleId); callstmnt.setString(2, userId); log.debug("Executing userCheckResult"); try (ResultSet resultSet = callstmnt.executeQuery()) { resultSet.next(); result = resultSet.getString(1); } } catch (SQLException e) { log.debug("userCheckResult Failure: " + e.toString()); result = null; } log.debug("*** END checkPlayerResult ***"); return result; } /** * Used to decipher whether or not a user exists as a player * * @param userId The user identifier of the player to be found * @return A boolean reflecting the state of existence of the player */ public static boolean findPlayerById(String ApplicationRoot, String userId) { log.debug("*** Getter.findPlayerById ***"); boolean userFound = false; // Get connection try (Connection conn = Database.getCoreConnection(ApplicationRoot); CallableStatement callstmt = conn.prepareCall("call playerFindById(?)")) { log.debug("Gathering playerFindById ResultSet"); callstmt.setString(1, userId); try (ResultSet userFind = callstmt.executeQuery()) { log.debug("Opening Result Set from playerFindById"); userFind.next(); // This will throw an exception if player not found log.debug( "Player Found: " + userFind.getString(1)); // This line will not execute if player not found userFound = true; } } catch (SQLException e) { log.error("Player did not exist: " + e.toString()); userFound = false; } log.debug("*** END findPlayerById ***"); return userFound; } /** * Used to gather all module information for internal functionality. This method is used in * creating View's or in control class operations * * @param ApplicationRoot The current runing context of the application * @return An ArrayList of String arrays that contain the module identifier, module name, module * type and module category of each module in the core database. */ public static ArrayList getAllModuleInfo(String ApplicationRoot) { log.debug("*** Getter.getAllModuleInfo ***"); ArrayList modules = new ArrayList(); try (Connection conn = Database.getCoreConnection(ApplicationRoot); CallableStatement callstmt = conn.prepareCall("call moduleGetAll()")) { log.debug("Gathering moduleGetAll ResultSet"); try (ResultSet resultSet = callstmt.executeQuery()) { log.debug("Opening Result Set from moduleGetAll"); int i = 0; while (resultSet.next()) { String[] result = new String[4]; i++; result[0] = resultSet.getString(1); // moduleId result[1] = resultSet.getString(2); // moduleName result[2] = resultSet.getString(3); // moduleType result[3] = resultSet.getString(4); // mdouleCategory modules.add(result); } log.debug("Returning Array list with " + i + " entries."); } } catch (SQLException e) { log.error("Could not execute query: " + e.toString()); } log.debug("*** END getAllModuleInfo ***"); return modules; } /** * Returns HTML menu for challenges. Challenges are only referenced by their id, The user will * have to go through another servlet to get the module's View address * * @param ApplicationRoot The current running context of the application * @param userId The user identifier of the player * @param lang The Locale the user has enabled * @return HTML menu for challenges * @throws SQLException */ public static String getChallenges(String ApplicationRoot, String userId, Locale lang) throws SQLException { log.debug("*** Getter.getChallenges ***"); String output = new String(); // Getting Translated Level Names ResourceBundle bundle = ResourceBundle.getBundle("i18n.moduleGenerics.moduleNames", lang); // Encoder to prevent XSS try (Connection conn = Database.getCoreConnection(ApplicationRoot); CallableStatement callstmt = conn.prepareCall("call moduleAllInfo(?, ?)")) { callstmt.setString(1, "challenge"); callstmt.setString(2, userId); log.debug("Gathering moduleAllInfo ResultSet"); try (ResultSet challenges = callstmt.executeQuery()) { log.debug("Opening Result Set from moduleAllInfo"); String challengeCategory = new String(); int rowNumber = 0; // Identifies the first row, ie the start of the list. This is slightly // different output to every other row while (challenges.next()) { if (!challengeCategory.equalsIgnoreCase(challenges.getString(2))) { challengeCategory = challenges.getString(2); // log.debug("New Category Detected: " + challengeCategory); if (rowNumber > 0) // output prepared for Every row after row 1 { output += "

  • " + Encode.forHtml(bundle.getString("category." + challengeCategory)) + "\n"; } // Update the current section to the one we have just added to the list currentSection = getTounnamentSectionFromRankNumber(levels.getInt(5)); // Which to Add? switch (currentSection) { case 1: // fieldTraining // log.debug("Starting Field Training List"); levelMasterList += "
    " + bundle.getString("getter.tournamentRank.1") + "