This commit is contained in:
Bruno Rybársky 2024-04-08 07:21:41 +02:00
parent 22a7d78152
commit 250b36f9ee
8 changed files with 218 additions and 37 deletions

@ -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 {

@ -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

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

@ -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<Config> 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.<Boolean>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.<String>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.<String>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);
}
}

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

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

@ -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": "*"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB