Git Credentials Manager

This commit is contained in:
Muhammad Tamir
2025-06-19 20:48:59 +07:00
parent 13c1f09090
commit 9493df1fd2
9 changed files with 323 additions and 5 deletions

View File

@@ -18,6 +18,7 @@ repositories {
dependencies { dependencies {
compileOnly("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT") 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("org.eclipse.jgit:org.eclipse.jgit:6.6.0.202305301015-r")
implementation("com.h2database:h2:2.2.224")
} }
// Disable default JAR task // Disable default JAR task

View File

@@ -1,6 +1,8 @@
package org.yuemi.commands; package org.yuemi.commands;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
import org.yuemi.git.H2PasswordProvider;
import org.yuemi.commands.GitRootCommand;
public class CommandRegistrar { public class CommandRegistrar {
@@ -11,6 +13,16 @@ public class CommandRegistrar {
} }
public void registerAll() { 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());
}
} }
} }

View File

@@ -12,9 +12,9 @@ public class GitRootCommand implements CommandExecutor {
private final JavaPlugin plugin; private final JavaPlugin plugin;
private final SubcommandHandler handler; private final SubcommandHandler handler;
public GitRootCommand(JavaPlugin plugin) { public GitRootCommand(JavaPlugin plugin, String filePassword, String dbPassword) {
this.plugin = plugin; this.plugin = plugin;
this.handler = new SubcommandHandler(plugin); this.handler = new SubcommandHandler(plugin, filePassword, dbPassword);
} }
@Override @Override

View File

@@ -12,14 +12,21 @@ import org.yuemi.commands.subcommands.GitFetchSubcommand;
import org.yuemi.commands.subcommands.GitPullSubcommand; import org.yuemi.commands.subcommands.GitPullSubcommand;
import org.yuemi.commands.subcommands.GitStatusSubcommand; import org.yuemi.commands.subcommands.GitStatusSubcommand;
import org.yuemi.commands.subcommands.GitHelpSubcommand; 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.HashMap;
import java.util.Map; import java.util.Map;
public class SubcommandHandler { public class SubcommandHandler {
private final Map<String, SubcommandExecutor> commands = new HashMap<>(); private final Map<String, SubcommandExecutor> commands = new HashMap<>();
private final String filePassword;
public SubcommandHandler(JavaPlugin plugin) { 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("add", new GitAddSubcommand(plugin));
commands.put("commit", new GitCommitSubcommand(plugin)); commands.put("commit", new GitCommitSubcommand(plugin));
commands.put("push", new GitPushSubcommand(plugin)); commands.put("push", new GitPushSubcommand(plugin));
@@ -30,6 +37,9 @@ public class SubcommandHandler {
commands.put("pull", new GitPullSubcommand(plugin)); commands.put("pull", new GitPullSubcommand(plugin));
commands.put("status", new GitStatusSubcommand(plugin)); commands.put("status", new GitStatusSubcommand(plugin));
commands.put("help", new GitHelpSubcommand(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) { public void handle(CommandSender sender, String name, String[] args) {

View File

@@ -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=<user> --token=<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
}
});
}
}

View File

@@ -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());
}
});
}
}

View File

@@ -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());
}
});
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}