From 9493df1fd22fa00d25b75a054228a4f213fe54fe Mon Sep 17 00:00:00 2001 From: Muhammad Tamir Date: Thu, 19 Jun 2025 20:48:59 +0700 Subject: [PATCH] Git Credentials Manager --- app/build.gradle.kts | 1 + .../org/yuemi/commands/CommandRegistrar.java | 14 +- .../org/yuemi/commands/GitRootCommand.java | 4 +- .../org/yuemi/commands/SubcommandHandler.java | 14 +- .../subcommands/GitLoginSubcommand.java | 49 +++++++ .../subcommands/GitLogoutSubcommand.java | 31 +++++ .../subcommands/GitWhoamiSubcommand.java | 35 +++++ .../GitCredentialDatabaseManager.java | 130 ++++++++++++++++++ .../org/yuemi/git/H2PasswordProvider.java | 50 +++++++ 9 files changed, 323 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/org/yuemi/commands/subcommands/GitLoginSubcommand.java create mode 100644 app/src/main/java/org/yuemi/commands/subcommands/GitLogoutSubcommand.java create mode 100644 app/src/main/java/org/yuemi/commands/subcommands/GitWhoamiSubcommand.java create mode 100644 app/src/main/java/org/yuemi/database/GitCredentialDatabaseManager.java create mode 100644 app/src/main/java/org/yuemi/git/H2PasswordProvider.java diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6f58806..424ff21 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -18,6 +18,7 @@ repositories { dependencies { compileOnly("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT") implementation("org.eclipse.jgit:org.eclipse.jgit:6.6.0.202305301015-r") + implementation("com.h2database:h2:2.2.224") } // Disable default JAR task diff --git a/app/src/main/java/org/yuemi/commands/CommandRegistrar.java b/app/src/main/java/org/yuemi/commands/CommandRegistrar.java index 6b3673b..11108b4 100644 --- a/app/src/main/java/org/yuemi/commands/CommandRegistrar.java +++ b/app/src/main/java/org/yuemi/commands/CommandRegistrar.java @@ -1,6 +1,8 @@ package org.yuemi.commands; import org.bukkit.plugin.java.JavaPlugin; +import org.yuemi.git.H2PasswordProvider; +import org.yuemi.commands.GitRootCommand; public class CommandRegistrar { @@ -11,6 +13,16 @@ public class CommandRegistrar { } public void registerAll() { - plugin.getCommand("git").setExecutor(new GitRootCommand(plugin)); + try { + H2PasswordProvider provider = new H2PasswordProvider(plugin.getDataFolder()); + String[] passwords = provider.getOrGeneratePasswords(); + String filePassword = passwords[0]; + String dbPassword = passwords[1]; + + plugin.getCommand("git").setExecutor(new GitRootCommand(plugin, filePassword, dbPassword)); + + } catch (Exception e) { + plugin.getLogger().severe("§cFailed to initialize Git command: " + e.getMessage()); + } } } diff --git a/app/src/main/java/org/yuemi/commands/GitRootCommand.java b/app/src/main/java/org/yuemi/commands/GitRootCommand.java index 1b8e1b3..36add84 100644 --- a/app/src/main/java/org/yuemi/commands/GitRootCommand.java +++ b/app/src/main/java/org/yuemi/commands/GitRootCommand.java @@ -12,9 +12,9 @@ public class GitRootCommand implements CommandExecutor { private final JavaPlugin plugin; private final SubcommandHandler handler; - public GitRootCommand(JavaPlugin plugin) { + public GitRootCommand(JavaPlugin plugin, String filePassword, String dbPassword) { this.plugin = plugin; - this.handler = new SubcommandHandler(plugin); + this.handler = new SubcommandHandler(plugin, filePassword, dbPassword); } @Override diff --git a/app/src/main/java/org/yuemi/commands/SubcommandHandler.java b/app/src/main/java/org/yuemi/commands/SubcommandHandler.java index 07f2c51..b153539 100644 --- a/app/src/main/java/org/yuemi/commands/SubcommandHandler.java +++ b/app/src/main/java/org/yuemi/commands/SubcommandHandler.java @@ -12,14 +12,21 @@ import org.yuemi.commands.subcommands.GitFetchSubcommand; import org.yuemi.commands.subcommands.GitPullSubcommand; import org.yuemi.commands.subcommands.GitStatusSubcommand; import org.yuemi.commands.subcommands.GitHelpSubcommand; +import org.yuemi.commands.subcommands.GitLoginSubcommand; +import org.yuemi.commands.subcommands.GitWhoamiSubcommand; +import org.yuemi.commands.subcommands.GitLogoutSubcommand; import java.util.HashMap; import java.util.Map; public class SubcommandHandler { private final Map commands = new HashMap<>(); - - public SubcommandHandler(JavaPlugin plugin) { + private final String filePassword; + private final String dbPassword; + + public SubcommandHandler(JavaPlugin plugin, String filePassword, String dbPassword) { + this.filePassword = filePassword; + this.dbPassword = dbPassword; commands.put("add", new GitAddSubcommand(plugin)); commands.put("commit", new GitCommitSubcommand(plugin)); commands.put("push", new GitPushSubcommand(plugin)); @@ -30,6 +37,9 @@ public class SubcommandHandler { commands.put("pull", new GitPullSubcommand(plugin)); commands.put("status", new GitStatusSubcommand(plugin)); commands.put("help", new GitHelpSubcommand(plugin)); + commands.put("login", new GitLoginSubcommand(plugin, filePassword, dbPassword)); + commands.put("whoami", new GitWhoamiSubcommand(plugin, filePassword, dbPassword)); + commands.put("logout", new GitLogoutSubcommand(plugin, filePassword, dbPassword)); } public void handle(CommandSender sender, String name, String[] args) { diff --git a/app/src/main/java/org/yuemi/commands/subcommands/GitLoginSubcommand.java b/app/src/main/java/org/yuemi/commands/subcommands/GitLoginSubcommand.java new file mode 100644 index 0000000..7149f53 --- /dev/null +++ b/app/src/main/java/org/yuemi/commands/subcommands/GitLoginSubcommand.java @@ -0,0 +1,49 @@ +package org.yuemi.commands.subcommands; + +import org.bukkit.command.CommandSender; +import org.bukkit.plugin.java.JavaPlugin; +import org.yuemi.commands.SubcommandExecutor; +import org.yuemi.git.GitCredentialDatabaseManager; + +import java.io.File; + +public class GitLoginSubcommand implements SubcommandExecutor { + + private final JavaPlugin plugin; + private final GitCredentialDatabaseManager credentialDB; + + public GitLoginSubcommand(JavaPlugin plugin, String filePassword, String dbPassword) { + this.plugin = plugin; + File pluginFolder = plugin.getDataFolder(); + this.credentialDB = new GitCredentialDatabaseManager(pluginFolder, filePassword, dbPassword); + } + + @Override + public void execute(CommandSender sender, String[] args) { + plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { + String username = null; + String token = null; + + for (String arg : args) { + if (arg.startsWith("--username=")) { + username = arg.substring("--username=".length()); + } else if (arg.startsWith("--token=")) { + token = arg.substring("--token=".length()); + } + } + + if (username == null || token == null) { + sender.sendMessage("§cUsage: /git login --username= --token="); + return; + } + + try { + boolean replaced = credentialDB.saveCredentials(username, token, true); + sender.sendMessage("§aGit credentials " + (replaced ? "updated" : "saved") + " securely."); + } catch (Exception e) { + sender.sendMessage("§cFailed to save credentials: " + e.getMessage()); + plugin.getLogger().warning("Git login error: " + e.getMessage()); // No token or username in logs + } + }); + } +} diff --git a/app/src/main/java/org/yuemi/commands/subcommands/GitLogoutSubcommand.java b/app/src/main/java/org/yuemi/commands/subcommands/GitLogoutSubcommand.java new file mode 100644 index 0000000..883cee3 --- /dev/null +++ b/app/src/main/java/org/yuemi/commands/subcommands/GitLogoutSubcommand.java @@ -0,0 +1,31 @@ +package org.yuemi.commands.subcommands; + +import org.bukkit.command.CommandSender; +import org.bukkit.plugin.java.JavaPlugin; +import org.yuemi.commands.SubcommandExecutor; +import org.yuemi.git.GitCredentialDatabaseManager; + +import java.io.File; + +public class GitLogoutSubcommand implements SubcommandExecutor { + + private final JavaPlugin plugin; + private final GitCredentialDatabaseManager credentialDB; + + public GitLogoutSubcommand(JavaPlugin plugin, String filePassword, String dbPassword) { + this.plugin = plugin; + this.credentialDB = new GitCredentialDatabaseManager(plugin.getDataFolder(), filePassword, dbPassword); + } + + @Override + public void execute(CommandSender sender, String[] args) { + plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { + try { + boolean deleted = credentialDB.deleteCredentials(); + sender.sendMessage(deleted ? "§aLogged out successfully." : "§eNo credentials to delete."); + } catch (Exception e) { + sender.sendMessage("§cFailed to delete credentials: " + e.getMessage()); + } + }); + } +} diff --git a/app/src/main/java/org/yuemi/commands/subcommands/GitWhoamiSubcommand.java b/app/src/main/java/org/yuemi/commands/subcommands/GitWhoamiSubcommand.java new file mode 100644 index 0000000..dc621bf --- /dev/null +++ b/app/src/main/java/org/yuemi/commands/subcommands/GitWhoamiSubcommand.java @@ -0,0 +1,35 @@ +package org.yuemi.commands.subcommands; + +import org.bukkit.command.CommandSender; +import org.bukkit.plugin.java.JavaPlugin; +import org.yuemi.commands.SubcommandExecutor; +import org.yuemi.git.GitCredentialDatabaseManager; + +import java.io.File; + +public class GitWhoamiSubcommand implements SubcommandExecutor { + + private final JavaPlugin plugin; + private final GitCredentialDatabaseManager credentialDB; + + public GitWhoamiSubcommand(JavaPlugin plugin, String filePassword, String dbPassword) { + this.plugin = plugin; + this.credentialDB = new GitCredentialDatabaseManager(plugin.getDataFolder(), filePassword, dbPassword); + } + + @Override + public void execute(CommandSender sender, String[] args) { + plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { + try { + String[] creds = credentialDB.getCredentials(); + if (creds != null) { + sender.sendMessage("§aLogged in as: §f" + creds[0]); + } else { + sender.sendMessage("§eNo Git credentials saved."); + } + } catch (Exception e) { + sender.sendMessage("§cError fetching credentials: " + e.getMessage()); + } + }); + } +} diff --git a/app/src/main/java/org/yuemi/database/GitCredentialDatabaseManager.java b/app/src/main/java/org/yuemi/database/GitCredentialDatabaseManager.java new file mode 100644 index 0000000..60947eb --- /dev/null +++ b/app/src/main/java/org/yuemi/database/GitCredentialDatabaseManager.java @@ -0,0 +1,130 @@ +package org.yuemi.git; + +import java.io.File; +import java.security.SecureRandom; +import java.sql.*; +import java.util.Base64; + +public class GitCredentialDatabaseManager { + + private final String dbPath; + private final String filePassword; + private final String dbPassword; + + public GitCredentialDatabaseManager(File pluginFolder, String filePassword, String dbPassword) { + this.dbPath = new File(pluginFolder, "gitcreds").getAbsolutePath(); + this.filePassword = filePassword; + this.dbPassword = dbPassword; + try { + Class.forName("org.h2.Driver"); // Ensure the H2 JDBC driver is registered + } catch (ClassNotFoundException e) { + throw new RuntimeException("H2 Driver not found. Make sure it's shaded correctly.", e); + } + initialize(); + } + + private Connection getConnection() throws SQLException { + String jdbcUrl = "jdbc:h2:file:" + dbPath + ";CIPHER=AES"; + String user = "sa"; + String password = filePassword + " " + dbPassword; + return DriverManager.getConnection(jdbcUrl, user, password); + } + + private void initialize() { + try (Connection conn = getConnection()) { + try (Statement stmt = conn.createStatement()) { + stmt.executeUpdate(""" + CREATE TABLE IF NOT EXISTS credentials ( + id IDENTITY PRIMARY KEY, + username VARCHAR NOT NULL, + token VARCHAR NOT NULL, + salt VARCHAR + ); + """); + } + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public boolean saveCredentials(String username, String token, boolean withSalt) throws SQLException { + String salt = withSalt ? generateSalt() : null; + String tokenToStore = withSalt ? encodeWithSalt(token, salt) : token; + boolean replaced = false; + + try (Connection conn = getConnection()) { + try (Statement stmt = conn.createStatement()) { + ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM credentials"); + if (rs.next() && rs.getInt(1) > 0) { + replaced = true; + stmt.executeUpdate("DELETE FROM credentials"); + } + } + + try (PreparedStatement ps = conn.prepareStatement(""" + INSERT INTO credentials (username, token, salt) + VALUES (?, ?, ?) + """)) { + ps.setString(1, username); + ps.setString(2, tokenToStore); + ps.setString(3, salt); + ps.executeUpdate(); + } + } + + return replaced; + } + + public String[] getCredentials() throws SQLException { + try (Connection conn = getConnection()) { + try (Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT username, token, salt FROM credentials LIMIT 1")) { + if (rs.next()) { + String username = rs.getString("username"); + String token = rs.getString("token"); + String salt = rs.getString("salt"); + if (salt != null) { + token = decodeWithSalt(token, salt); + } + return new String[]{username, token}; + } + } + } + } + return null; + } + + public boolean deleteCredentials() throws SQLException { + try (Connection conn = getConnection()) { + try (Statement stmt = conn.createStatement()) { + return stmt.executeUpdate("DELETE FROM credentials") > 0; + } + } + } + + private String generateSalt() { + byte[] salt = new byte[16]; + new SecureRandom().nextBytes(salt); + return Base64.getEncoder().encodeToString(salt); + } + + private String encodeWithSalt(String token, String salt) { + byte[] tokenBytes = token.getBytes(); + byte[] saltBytes = Base64.getDecoder().decode(salt); + byte[] result = new byte[tokenBytes.length]; + for (int i = 0; i < tokenBytes.length; i++) { + result[i] = (byte) (tokenBytes[i] ^ saltBytes[i % saltBytes.length]); + } + return Base64.getEncoder().encodeToString(result); + } + + private String decodeWithSalt(String encoded, String salt) { + byte[] tokenBytes = Base64.getDecoder().decode(encoded); + byte[] saltBytes = Base64.getDecoder().decode(salt); + byte[] result = new byte[tokenBytes.length]; + for (int i = 0; i < tokenBytes.length; i++) { + result[i] = (byte) (tokenBytes[i] ^ saltBytes[i % saltBytes.length]); + } + return new String(result); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/yuemi/git/H2PasswordProvider.java b/app/src/main/java/org/yuemi/git/H2PasswordProvider.java new file mode 100644 index 0000000..c4b439d --- /dev/null +++ b/app/src/main/java/org/yuemi/git/H2PasswordProvider.java @@ -0,0 +1,50 @@ +package org.yuemi.git; + +import java.io.*; +import java.security.SecureRandom; +import java.util.Base64; +import java.util.Properties; + +public class H2PasswordProvider { + + private final File configFile; + + public H2PasswordProvider(File pluginFolder) { + if (!pluginFolder.exists()) { + pluginFolder.mkdirs(); + } + this.configFile = new File(pluginFolder, "credentials.properties"); + } + + public String[] getOrGeneratePasswords() throws IOException { + if (configFile.exists()) { + Properties props = new Properties(); + try (FileInputStream in = new FileInputStream(configFile)) { + props.load(in); + return new String[] { + props.getProperty("filePassword"), + props.getProperty("dbPassword") + }; + } + } else { + String filePassword = generateRandomPassword(); + String dbPassword = generateRandomPassword(); + + Properties props = new Properties(); + props.setProperty("filePassword", filePassword); + props.setProperty("dbPassword", dbPassword); + + try (FileOutputStream out = new FileOutputStream(configFile)) { + props.store(out, "GitCraft H2 Passwords - Do not share"); + } + + return new String[] { filePassword, dbPassword }; + } + } + + private String generateRandomPassword() { + byte[] randomBytes = new byte[16]; + new SecureRandom().nextBytes(randomBytes); + return Base64.getEncoder().encodeToString(randomBytes); + } +}