Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
import fr.xephi.authme.command.ExecutableCommand;
import fr.xephi.authme.datasource.converter.AuthPlusConverter;
import fr.xephi.authme.datasource.converter.Converter;
import fr.xephi.authme.datasource.converter.LibreLoginConverter;
import fr.xephi.authme.datasource.converter.LimboAuthConverter;
import fr.xephi.authme.datasource.converter.MySqlToSqlite;
import fr.xephi.authme.datasource.converter.NLoginConverter;
import fr.xephi.authme.datasource.converter.SqliteToSql;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.output.ConsoleLoggerFactory;
Expand Down Expand Up @@ -78,6 +81,9 @@ private static Class<? extends Converter> getConverterClassFromArgs(List<String>
private static Map<String, Class<? extends Converter>> getConverters() {
return ImmutableSortedMap.<String, Class<? extends Converter>>naturalOrder()
.put("authplus", AuthPlusConverter.class)
.put("librelogin", LibreLoginConverter.class)
.put("limboauth", LimboAuthConverter.class)
.put("nlogin", NLoginConverter.class)
.put("sqlitetosql", SqliteToSql.class)
.put("mysqltosqlite", MySqlToSqlite.class)
.build();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package fr.xephi.authme.datasource.converter;

import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.DatabaseSettings;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

/**
* Base class for converters that read from an external plugin's MySQL/MariaDB table.
* <p>
* The source database is assumed to share the same host, port, database name, and credentials
* as configured in AuthMe's {@code config.yml}. SQLite source databases are not supported.
*/
abstract class AbstractSqlPluginConverter implements Converter {

private final DataSource dataSource;
private final Settings settings;

AbstractSqlPluginConverter(Settings settings, DataSource dataSource) {
this.settings = settings;
this.dataSource = dataSource;
}

protected DataSource getDataSource() {
return dataSource;
}

/**
* Opens a JDBC connection to the database configured in AuthMe's settings.
*
* @return an open connection
* @throws SQLException if the connection cannot be established
*/
protected Connection openConnection() throws SQLException {
String host = settings.getProperty(DatabaseSettings.MYSQL_HOST);
String port = settings.getProperty(DatabaseSettings.MYSQL_PORT);
String database = settings.getProperty(DatabaseSettings.MYSQL_DATABASE);
String user = settings.getProperty(DatabaseSettings.MYSQL_USERNAME);
String pass = settings.getProperty(DatabaseSettings.MYSQL_PASSWORD);
String url = "jdbc:mysql://" + host + ":" + port + "/" + database
+ "?useUnicode=true&characterEncoding=utf-8";
return DriverManager.getConnection(url, user, pass);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package fr.xephi.authme.datasource.converter;

import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import fr.xephi.authme.security.crypts.HashedPassword;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.util.UuidUtils;
import org.bukkit.command.CommandSender;

import javax.inject.Inject;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.Base64;
import java.util.Locale;
import java.nio.charset.StandardCharsets;

import static fr.xephi.authme.util.Utils.logAndSendMessage;

/**
* Converts data from LibreLogin to AuthMe.
* <p>
* LibreLogin stores accounts in the {@code librepremium_data} table of the configured database.
* This converter expects that LibreLogin and AuthMe share the same MySQL/MariaDB database.
* <p>
* <b>Algorithm mapping:</b>
* <ul>
* <li>{@code BCrypt-2A} → configure AuthMe with {@code passwordHash: BCRYPT}</li>
* <li>{@code Argon2-ID} → configure AuthMe with {@code passwordHash: ARGON2}</li>
* <li>{@code SHA-256} → configure AuthMe with {@code passwordHash: SHA256} (same computation)</li>
* <li>{@code SHA-512} → configure AuthMe with {@code passwordHash: DOUBLE_SHA512}</li>
* <li>{@code LOGIT-SHA-256} → configure AuthMe with {@code passwordHash: SALTEDSHA256}</li>
* </ul>
* If accounts use mixed algorithms, set the most common one in AuthMe's config and have remaining
* players reset their password.
*/
public class LibreLoginConverter extends AbstractSqlPluginConverter {

private static final String TABLE = "librepremium_data";
private static final String QUERY = "SELECT last_nickname, hashed_password, salt, algo, "
+ "ip, email, joined, last_seen, uuid, premium_uuid, secret FROM " + TABLE;

private final ConsoleLogger logger = ConsoleLoggerFactory.get(LibreLoginConverter.class);

@Inject
LibreLoginConverter(Settings settings, DataSource dataSource) {
super(settings, dataSource);
}

@Override
public void execute(CommandSender sender) {
try (Connection conn = openConnection();
PreparedStatement ps = conn.prepareStatement(QUERY);
ResultSet rs = ps.executeQuery()) {

long imported = 0;
long skipped = 0;
while (rs.next()) {
String realName = rs.getString("last_nickname");
if (realName == null || realName.isEmpty()) {
continue;
}
String name = realName.toLowerCase(Locale.ROOT);

if (getDataSource().isAuthAvailable(name)) {
++skipped;
continue;
}

HashedPassword password = buildHashedPassword(
rs.getString("hashed_password"),
rs.getString("salt"),
rs.getString("algo"),
name);
if (password == null) {
continue;
}

Timestamp joined = rs.getTimestamp("joined");
Timestamp lastSeen = rs.getTimestamp("last_seen");

PlayerAuth.Builder builder = PlayerAuth.builder()
.name(name)
.realName(realName)
.password(password)
.lastIp(rs.getString("ip"))
.email(rs.getString("email"))
.registrationDate(joined != null ? joined.getTime() : 0L)
.lastLogin(lastSeen != null ? lastSeen.getTime() : null)
.totpKey(rs.getString("secret"))
.uuid(UuidUtils.parseUuidSafely(rs.getString("uuid")))
.premiumUuid(UuidUtils.parseUuidSafely(rs.getString("premium_uuid")));

getDataSource().saveAuth(builder.build());
++imported;
}

logAndSendMessage(sender, "LibreLogin conversion: " + imported + " account(s) imported, "
+ skipped + " skipped (already exist)");

} catch (SQLException e) {
logAndSendMessage(sender, "LibreLogin conversion failed: " + e.getMessage());
logger.logException("LibreLogin conversion error:", e);
}
}

private HashedPassword buildHashedPassword(String hash, String salt, String algo, String name) {
if (hash == null || algo == null) {
return null;
}
switch (algo) {
case "BCrypt-2A":
return new HashedPassword(hash);

case "Argon2-ID":
// LibreLogin stores Argon2 as a Base64-encoded PHC string
String phc = decodeArgon2(hash);
if (phc == null) {
logger.warning("Could not decode Argon2-ID hash for player '" + name + "', skipping");
return null;
}
return new HashedPassword(phc);

case "SHA-256":
// LibreLogin SHA-256: sha256(sha256(pw) + salt) — same computation as AuthMe SHA256.
// Reconstruct the AuthMe format: $SHA$<salt>$<hash>
if (salt == null) {
logger.warning("Null salt for SHA-256 account '" + name + "', skipping");
return null;
}
return new HashedPassword("$SHA$" + salt + "$" + hash);

case "SHA-512":
// LibreLogin SHA-512: sha512(sha512(pw) + salt) — maps to AuthMe DOUBLE_SHA512 (separate salt).
if (salt == null) {
logger.warning("Null salt for SHA-512 account '" + name + "', skipping");
return null;
}
return new HashedPassword(hash, salt);

case "LOGIT-SHA-256":
// LibreLogin LOGIT-SHA-256: sha256(pw + salt) — maps to AuthMe SALTEDSHA256 (separate salt).
if (salt == null) {
logger.warning("Null salt for LOGIT-SHA-256 account '" + name + "', skipping");
return null;
}
return new HashedPassword(hash, salt);

default:
logger.warning("Unknown LibreLogin algorithm '" + algo + "' for player '" + name + "', skipping");
return null;
}
}

private String decodeArgon2(String value) {
if (value.startsWith("$argon2")) {
return value;
}
try {
String decoded = new String(Base64.getDecoder().decode(value), StandardCharsets.UTF_8);
if (decoded.startsWith("$argon2")) {
return decoded;
}
} catch (IllegalArgumentException ignored) {
// fall through
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package fr.xephi.authme.datasource.converter;

import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import fr.xephi.authme.security.crypts.HashedPassword;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.util.UuidUtils;
import org.bukkit.command.CommandSender;

import javax.inject.Inject;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Locale;

import static fr.xephi.authme.util.Utils.logAndSendMessage;

/**
* Converts data from LimboAuth to AuthMe.
* <p>
* LimboAuth stores accounts in the {@code AUTH} table of the configured database.
* This converter expects that LimboAuth and AuthMe share the same MySQL/MariaDB database.
* <p>
* LimboAuth uses BCrypt exclusively for new registrations, so configure AuthMe with
* {@code passwordHash: BCRYPT} before running this converter.
*/
public class LimboAuthConverter extends AbstractSqlPluginConverter {

private static final String TABLE = "AUTH";
private static final String QUERY = "SELECT NICKNAME, LOWERCASENICKNAME, HASH, IP, "
+ "REGDATE, LOGINDATE, UUID, PREMIUMUUID, TOTPTOKEN FROM " + TABLE;

private final ConsoleLogger logger = ConsoleLoggerFactory.get(LimboAuthConverter.class);

@Inject
LimboAuthConverter(Settings settings, DataSource dataSource) {
super(settings, dataSource);
}

@Override
public void execute(CommandSender sender) {
try (Connection conn = openConnection();
PreparedStatement ps = conn.prepareStatement(QUERY);
ResultSet rs = ps.executeQuery()) {

long imported = 0;
long skipped = 0;
while (rs.next()) {
String realName = rs.getString("NICKNAME");
String name = rs.getString("LOWERCASENICKNAME");
if (name == null || name.isEmpty()) {
if (realName != null) {
name = realName.toLowerCase(Locale.ROOT);
} else {
continue;
}
}
if (realName == null) {
realName = name;
}

if (getDataSource().isAuthAvailable(name)) {
++skipped;
continue;
}

String hash = rs.getString("HASH");
if (hash == null || hash.isEmpty()) {
logger.warning("No hash for player '" + name + "', skipping");
continue;
}

long regDate = rs.getLong("REGDATE");
long loginDate = rs.getLong("LOGINDATE");

PlayerAuth.Builder builder = PlayerAuth.builder()
.name(name)
.realName(realName)
.password(new HashedPassword(hash))
.lastIp(rs.getString("IP"))
.registrationDate(regDate)
.lastLogin(loginDate > 0 ? loginDate : null)
.totpKey(rs.getString("TOTPTOKEN"))
.uuid(UuidUtils.parseUuidSafely(rs.getString("UUID")))
.premiumUuid(UuidUtils.parseUuidSafely(rs.getString("PREMIUMUUID")));

getDataSource().saveAuth(builder.build());
++imported;
}

logAndSendMessage(sender, "LimboAuth conversion: " + imported + " account(s) imported, "
+ skipped + " skipped (already exist)");

} catch (SQLException e) {
logAndSendMessage(sender, "LimboAuth conversion failed: " + e.getMessage());
logger.logException("LimboAuth conversion error:", e);
}
}
}
Loading
Loading