diff --git a/build.gradle b/build.gradle index 128f9da..59c6de6 100644 --- a/build.gradle +++ b/build.gradle @@ -12,9 +12,18 @@ repositories { // Loom adds the essential maven repositories to download Minecraft and libraries from automatically. // See https://docs.gradle.org/current/userguide/declaring_repositories.html // for more information about repositories. + maven { + name 'Xander Maven' + url 'https://maven.isxander.dev/releases' + } + maven { + name 'TerraformersMC' + url 'https://maven.terraformersmc.com/releases' + } } dependencies { + implementation group: 'com.google.code.gson', name: 'gson', version: '2.10.1' // To change the versions see the gradle.properties file minecraft "com.mojang:minecraft:${project.minecraft_version}" mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" @@ -22,6 +31,8 @@ dependencies { // Fabric API. This is technically optional, but you probably want it anyway. modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" + modImplementation "dev.isxander.yacl:yet-another-config-lib-fabric:${project.yacl_version}" + modImplementation "com.terraformersmc:modmenu:${project.modmenu_version}" } processResources { diff --git a/gradle.properties b/gradle.properties index 2f94dca..659d6b7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,3 +15,5 @@ org.gradle.jvmargs=-Xmx1G # Dependencies # check this on https://modmuss50.me/fabric.html fabric_version=0.96.11+1.20.4 + yacl_version=3.3.2+1.20.4 + modmenu_version=9.1.0-beta.1 diff --git a/src/main/java/systems/brn/chatencryptor/ChatCoder.java b/src/main/java/systems/brn/chatencryptor/ChatCoder.java new file mode 100644 index 0000000..853c64b --- /dev/null +++ b/src/main/java/systems/brn/chatencryptor/ChatCoder.java @@ -0,0 +1,24 @@ +package systems.brn.chatencryptor; + +public class ChatCoder { + // Encode byte array to BMP characters + public static String encodeToBmp(byte[] byteArray) { + StringBuilder bmpText = new StringBuilder(); + for (int i = 0; i < byteArray.length; i += 2) { + char bmpChar = (char) ((byteArray[i] & 0xFF) << 8 | (byteArray[i + 1] & 0xFF)); + bmpText.append(bmpChar); + } + return bmpText.toString(); + } + + // Decode BMP characters to byte array + public static byte[] decodeFromBmp(String bmpString) { + byte[] byteArray = new byte[bmpString.length() * 2]; + for (int i = 0; i < bmpString.length(); i++) { + char bmpChar = bmpString.charAt(i); + byteArray[i * 2] = (byte) ((bmpChar >> 8) & 0xFF); + byteArray[i * 2 + 1] = (byte) (bmpChar & 0xFF); + } + return byteArray; + } +} diff --git a/src/main/java/systems/brn/chatencryptor/Config.java b/src/main/java/systems/brn/chatencryptor/Config.java new file mode 100644 index 0000000..26661dc --- /dev/null +++ b/src/main/java/systems/brn/chatencryptor/Config.java @@ -0,0 +1,134 @@ +package systems.brn.chatencryptor; + +import com.google.gson.GsonBuilder; +import dev.isxander.yacl3.api.*; +import dev.isxander.yacl3.api.controller.StringControllerBuilder; +import dev.isxander.yacl3.api.controller.TickBoxControllerBuilder; +import dev.isxander.yacl3.config.v2.api.ConfigClassHandler; +import dev.isxander.yacl3.config.v2.api.SerialEntry; +import dev.isxander.yacl3.config.v2.api.serializer.GsonConfigSerializerBuilder; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Base64; + +public class Config { + public static ConfigClassHandler HANDLER = ConfigClassHandler.createBuilder(Config.class) + .id(new Identifier("securechat", "securechat_config")) + .serializer(config -> GsonConfigSerializerBuilder.create(config) + .setPath(FabricLoader.getInstance().getConfigDir().resolve("securechat.json")) + .appendGsonBuilder(GsonBuilder::setPrettyPrinting) // not needed, pretty print by default + .build()) + .build(); + + @SerialEntry + public boolean enabled = false; + @SerialEntry + public String SecretKey = ""; + + @SerialEntry + public String Iv = ""; + public IvParameterSpec getRawIv() { + byte[] byteIv = Base64.getDecoder().decode(HANDLER.instance().Iv); + return new IvParameterSpec(byteIv); + } + + public SecretKey getRawKey() { + byte[] binary_key = Base64.getDecoder().decode(HANDLER.instance().SecretKey); + // Convert byte array back to SecretKey + return new SecretKeySpec(binary_key, 0, binary_key.length, "AES"); + } + + public Boolean isEnabled() { + return HANDLER.instance().enabled; + } + + private static IvParameterSpec generateIv() { + byte[] iv = new byte[16]; + new SecureRandom().nextBytes(iv); + return new IvParameterSpec(iv); + } + + private String getDefaultIv() { + IvParameterSpec generatedIv = generateIv(); + byte [] bytesIv = generatedIv.getIV(); + return Base64.getEncoder().encodeToString(bytesIv); + } + + private SecretKey generateKey() { + KeyGenerator kg; + try { + kg = KeyGenerator.getInstance("AES"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + kg.init(256); + return kg.generateKey(); + } + + private String getDefaultKey() { + SecretKey generatedKey = generateKey(); + byte[] encodedKey = generatedKey.getEncoded(); + return Base64.getEncoder().encodeToString(encodedKey); + } + + private void setEnabled(Boolean newEnabled){ + + HANDLER.instance().enabled = newEnabled; + HANDLER.save(); + } + private void setIv(String newIv){ + + HANDLER.instance().Iv = newIv; + HANDLER.save(); + } + private void setSecretKey(String newSecretKey){ + + HANDLER.instance().SecretKey = newSecretKey; + HANDLER.save(); + } + + public Screen makeConfig(Screen parentScreen) { + HANDLER.load(); + return YetAnotherConfigLib.createBuilder() + .title(Text.literal("SecureChat Config.")) + .category(ConfigCategory.createBuilder() + .name(Text.literal("General")) + .tooltip(Text.literal("Here you can set some generic settings.")) + .group(OptionGroup.createBuilder() + .name(Text.literal("General")) + .description(OptionDescription.of(Text.literal("Here you can set some generic settings."))) + .option(Option.createBuilder() + .name(Text.literal("Enabled")) + .description(OptionDescription.of(Text.literal("This will enable the encryption."))) + .binding(false, () -> HANDLER.instance().enabled, this::setEnabled) + .controller(TickBoxControllerBuilder::create) + .build()) + .option(Option.createBuilder() + .name(Text.literal("Secret key")) + .description(OptionDescription.of(Text.literal("This be the key that will be used."))) + .binding(getDefaultKey(), () -> HANDLER.instance().Iv, this::setIv) + .controller(StringControllerBuilder::create) + .build()) + .option(Option.createBuilder() + .name(Text.literal("Initialization vector")) + .description(OptionDescription.of(Text.literal("This be the initialization vector that will be used."))) + .binding(getDefaultIv(), () -> HANDLER.instance().SecretKey, this::setSecretKey) + .controller(StringControllerBuilder::create) + .build()) + .build()) + .build()) + .build().generateScreen(parentScreen); + } +} diff --git a/src/main/java/systems/brn/chatencryptor/ModMenuIntegration.java b/src/main/java/systems/brn/chatencryptor/ModMenuIntegration.java new file mode 100644 index 0000000..bca02e2 --- /dev/null +++ b/src/main/java/systems/brn/chatencryptor/ModMenuIntegration.java @@ -0,0 +1,15 @@ +package systems.brn.chatencryptor; + +import com.terraformersmc.modmenu.api.ConfigScreenFactory; +import com.terraformersmc.modmenu.api.ModMenuApi; + +public class ModMenuIntegration implements ModMenuApi { + @Override + public ConfigScreenFactory getModConfigScreenFactory() { + return parent -> { + Config config = new Config(); + Config.HANDLER.load(); + return config.makeConfig(parent); + }; + } +} diff --git a/src/main/java/systems/brn/chatencryptor/SecureChat.java b/src/main/java/systems/brn/chatencryptor/SecureChat.java index 7355fc9..635e615 100644 --- a/src/main/java/systems/brn/chatencryptor/SecureChat.java +++ b/src/main/java/systems/brn/chatencryptor/SecureChat.java @@ -17,46 +17,33 @@ import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import java.nio.charset.StandardCharsets; -import java.security.*; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; import java.time.Instant; -import java.util.Arrays; + + public class SecureChat implements ClientModInitializer { - private PublicKey publicKey; - private PrivateKey privateKey; - - private void initKeys() { - KeyPairGenerator kpg; - try { - kpg = KeyPairGenerator.getInstance("RSA"); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - kpg.initialize(512); - KeyPair kp = kpg.genKeyPair(); - publicKey = kp.getPublic(); - privateKey = kp.getPrivate(); - } - private boolean decryptChatMessage(Text message, @Nullable SignedMessage signedMessage, @Nullable GameProfile sender, MessageType.Parameters params, Instant receptionTimestamp) { TranslatableTextContent content = (TranslatableTextContent) message.getContent(); String message_content = content.getArg(1).getString(); String player_name = content.getArg(0).getString(); if(message_content.startsWith("®") && message_content.endsWith("®")){ try { + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.ENCRYPT_MODE, Config.HANDLER.instance().getRawKey(), Config.HANDLER.instance().getRawIv()); String strippedMessage = message_content.replace("®", ""); - byte[] decodedMessage = strippedMessage.getBytes(java.nio.charset.StandardCharsets.UTF_16); - byte[] unpaddedMessage = Arrays.copyOfRange(decodedMessage, 2, decodedMessage.length); - Cipher decryptingCipher = Cipher.getInstance("RSA"); - decryptingCipher.init(Cipher.DECRYPT_MODE, privateKey); - decryptingCipher.update(unpaddedMessage); + byte[] decodedMessage = ChatCoder.decodeFromBmp(strippedMessage); + Cipher decryptingCipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + decryptingCipher.update(decodedMessage); String decryptedMessage = new String(decryptingCipher.doFinal()); String outputMessage = "{" + player_name + "} " + decryptedMessage; MinecraftClient.getInstance().inGameHud.getChatHud().addMessage(Text.of(outputMessage)); return false; } - catch (IllegalBlockSizeException | BadPaddingException | NoSuchAlgorithmException | NoSuchPaddingException | + catch (IllegalBlockSizeException | BadPaddingException | NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException | InvalidKeyException e){ return true; } @@ -65,25 +52,29 @@ public class SecureChat implements ClientModInitializer { } private String encryptChatMessage(String message) { - String encodedMessage; - try { - Cipher encryptingCipher = Cipher.getInstance("RSA"); - encryptingCipher.init(Cipher.ENCRYPT_MODE, publicKey); - encryptingCipher.update(message.getBytes(StandardCharsets.UTF_8)); - byte[] encryptedMessage = encryptingCipher.doFinal(); - encodedMessage = new String(encryptedMessage, java.nio.charset.StandardCharsets.UTF_16); - } catch (IllegalBlockSizeException | BadPaddingException | InvalidKeyException | NoSuchPaddingException | - NoSuchAlgorithmException e) { - throw new RuntimeException(e); + if(Config.HANDLER.instance().isEnabled()){ + String encodedMessage; + try { + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.DECRYPT_MODE, Config.HANDLER.instance().getRawKey(), Config.HANDLER.instance().getRawIv()); + cipher.update(message.getBytes(StandardCharsets.UTF_8)); + byte[] encryptedMessage = cipher.doFinal(); + encodedMessage = ChatCoder.encodeToBmp(encryptedMessage); + } catch (IllegalBlockSizeException | BadPaddingException | InvalidKeyException | NoSuchPaddingException | InvalidAlgorithmParameterException | + NoSuchAlgorithmException e) { + return message; + } + return '®' + encodedMessage + '®'; + } + else { + return message; } - return '®' + encodedMessage + '®'; } @Override public void onInitializeClient() { ClientLifecycleEvents.CLIENT_STARTED.register(client -> { // Register event listener for ClientTickEvents.END_CLIENT_TICK - initKeys(); ClientReceiveMessageEvents.ALLOW_CHAT.register(this::decryptChatMessage); ClientSendMessageEvents.MODIFY_CHAT.register(this::encryptChatMessage); }); diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index fa1133b..fc06435 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -17,11 +17,15 @@ "entrypoints": { "client": [ "systems.brn.chatencryptor.SecureChat" + ], + "modmenu": [ + "systems.brn.chatencryptor.ModMenuIntegration" ] }, "depends": { "fabricloader": ">=${loader_version}", "fabric": "*", - "minecraft": "${minecraft_version}" + "minecraft": "${minecraft_version}", + "yet_another_config_lib_v3": "*" } } diff --git a/src/main/resources/icon.png b/src/main/resources/icon.png index b676f58..7038887 100644 Binary files a/src/main/resources/icon.png and b/src/main/resources/icon.png differ