diff --git a/.gitignore b/.gitignore index 4bde45e..6aa6c9e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ run/ +run*/ build/ remappedSrc/ .gradle/ diff --git a/README.md b/README.md index 0fa1f65..96f8e32 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ Yet another skyblock.net mod for 1.19.2 | Chat Rank Removal | Hides the [Rank] part of global chat messages | | Tool Saver | Saves your tools when they run low on durability | | Share Button | Adds a button to chests/shulkers to share them to https://skyblock.onl/item quickly. | +| Chatcryption | RSA Encrypts private message communications. Communicates public key via plaintext once per session. **Note**: This means if a person communicates changes public keys (e.g. MITM attempted, changed computers without copying key, etc.) messages from them will be unreadable until you restart. Also, there is no downgrading to plaintext and it's either all encrypted or all plaintext. | ## Commands diff --git a/gradle.properties b/gradle.properties index 08cc04b..a703058 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,7 +10,7 @@ org.gradle.parallel=true # Mod Properties - mod_version = 1.7.0 + mod_version = 1.8.0 maven_group = com.anotherpillow archives_base_name = skyplusplus diff --git a/src/main/java/com/anotherpillow/skyplusplus/client/SkyPlusPlusClient.java b/src/main/java/com/anotherpillow/skyplusplus/client/SkyPlusPlusClient.java index 2396e8f..afb215b 100644 --- a/src/main/java/com/anotherpillow/skyplusplus/client/SkyPlusPlusClient.java +++ b/src/main/java/com/anotherpillow/skyplusplus/client/SkyPlusPlusClient.java @@ -63,9 +63,12 @@ public class SkyPlusPlusClient implements ClientModInitializer { @Override public void onInitializeClient() { + SkyPlusPlusConfig.configInstance.load(); config = SkyPlusPlusConfig.configInstance.getConfig(); client = MinecraftClient.getInstance(); + Chatcryption.generateKeysAndSave(); + BetterChangeBiome.register(); BetterCrateKeys.register(); ShowEmptyShops.register(); @@ -144,6 +147,7 @@ else if (pos.getManhattanDistance(new BlockPos(4000, 175, 2000)) > 200 GetHeadTextureCommand.register(dispatcher); GetNBTJsonCommand.register(dispatcher); ShareCommand.register(dispatcher); + SinkholeCommand.register(dispatcher); }); AttackBlockCallback.EVENT.register((PlayerEntity player, World world, Hand hand, BlockPos pos, Direction direction) -> { diff --git a/src/main/java/com/anotherpillow/skyplusplus/commands/SinkholeCommand.java b/src/main/java/com/anotherpillow/skyplusplus/commands/SinkholeCommand.java new file mode 100644 index 0000000..9b4399b --- /dev/null +++ b/src/main/java/com/anotherpillow/skyplusplus/commands/SinkholeCommand.java @@ -0,0 +1,26 @@ +package com.anotherpillow.skyplusplus.commands; + +import com.anotherpillow.skyplusplus.config.SkyPlusPlusConfig; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.minecraft.client.MinecraftClient; + +import java.util.Timer; +import java.util.TimerTask; + +public class SinkholeCommand { + public static void register(CommandDispatcher dispatcher) { + MinecraftClient client = MinecraftClient.getInstance(); + + dispatcher.register(ClientCommandManager.literal("sky++_sinkhole") + .then(ClientCommandManager.argument("extra", StringArgumentType.greedyString()) + .executes(context -> { + return 1; + }) + ) + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/anotherpillow/skyplusplus/config/SkyPlusPlusConfig.java b/src/main/java/com/anotherpillow/skyplusplus/config/SkyPlusPlusConfig.java index 53e43e7..e414f23 100644 --- a/src/main/java/com/anotherpillow/skyplusplus/config/SkyPlusPlusConfig.java +++ b/src/main/java/com/anotherpillow/skyplusplus/config/SkyPlusPlusConfig.java @@ -85,6 +85,7 @@ public class SkyPlusPlusConfig { @ConfigEntry public boolean lowerCommandsEnabled = true; @ConfigEntry public String joinCommandsList = ""; @ConfigEntry public boolean copyChatMessageButtonEnabled = false; + @ConfigEntry public boolean chatcryptionEnabled = false; @ConfigEntry public boolean enableDiscordRPC = true; @@ -668,6 +669,20 @@ public static Screen getConfigScreen(Screen parentScreen) { .controller(TickBoxController::new) //?} .build()) + .option(Option.createBuilder(boolean.class) + .name(Text.translatable("skyplusplus.config.tweaks-improvements.chatcryption")) + //? if >1.19.2 { + /*.description(OptionDescription.of(Text.translatable("skyplusplus.config.tweaks-improvements.chatcryption"))) + *///?} else { + .tooltip(Text.translatable("skyplusplus.config.tweaks-improvements.chatcryption-desc")) + //?} + .binding(defaults.chatcryptionEnabled, () -> config.chatcryptionEnabled, v -> config.chatcryptionEnabled = v) + //? if >1.19.2 { + /*.controller(TickBoxControllerBuilder::create) + *///?} else { + .controller(TickBoxController::new) + //?} + .build()) .build()) .category(ConfigCategory.createBuilder() .name(Text.translatable("skyplusplus.config.discord-rpc.title")) diff --git a/src/main/java/com/anotherpillow/skyplusplus/features/Chatcryption.java b/src/main/java/com/anotherpillow/skyplusplus/features/Chatcryption.java new file mode 100644 index 0000000..3861175 --- /dev/null +++ b/src/main/java/com/anotherpillow/skyplusplus/features/Chatcryption.java @@ -0,0 +1,501 @@ +package com.anotherpillow.skyplusplus.features; + +import com.anotherpillow.skyplusplus.client.SkyPlusPlusClient; +import com.anotherpillow.skyplusplus.util.Chat; +import com.anotherpillow.skyplusplus.util.Server; +import com.anotherpillow.skyplusplus.util.StringChecker; +import com.anotherpillow.skyplusplus.util.TextStringifier; +import com.google.common.reflect.TypeToken; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.text.Text; + +import javax.crypto.Cipher; +import java.io.*; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.*; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.stream.Collectors; + +import static com.anotherpillow.skyplusplus.util.Filesystem.resolveConfigPath; +import static java.awt.SystemColor.text; + +public class Chatcryption { + private static class KeyStore { + private static final Path CONFIG_FILE = resolveConfigPath("skyplusplus-keys.json"); + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + private static ConcurrentHashMap keys = new ConcurrentHashMap<>(); + + public static PublicKey base64ToPublicKey(String base64Key) throws Exception { + byte[] keyBytes = Base64.getDecoder().decode(base64Key); + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + return keyFactory.generatePublic(keySpec); + } + + public static void load() { + try { + if (Files.exists(CONFIG_FILE)) { + String json = Files.readString(CONFIG_FILE); + if (!json.trim().isEmpty()) { + Map serializedKeys = GSON.fromJson(json, new TypeToken>() {}.getType()); + + keys.clear(); + for (Map.Entry entry : serializedKeys.entrySet()) { + try { + PublicKey publicKey = base64ToPublicKey(entry.getValue()); + keys.put(entry.getKey(), publicKey); + } catch (Exception e) { + System.err.println("Failed to load key for player " + entry.getKey() + ": " + e.getMessage()); + } + } + } + } + } catch (IOException e) { + System.err.println("Failed to load keys from file: " + e.getMessage()); + } + } + + public static void save() { + try { + Map serializedKeys = new HashMap<>(); + for (Map.Entry entry : keys.entrySet()) { + String encodedKey = Base64.getEncoder().encodeToString(entry.getValue().getEncoded()); + serializedKeys.put(entry.getKey(), encodedKey); + } + + String json = GSON.toJson(serializedKeys); + Files.createDirectories(CONFIG_FILE.getParent()); + Files.writeString(CONFIG_FILE, json); + } catch (IOException e) { + System.err.println("Failed to save keys to file: " + e.getMessage()); + } + } + + public static void importKey(String playerName, PublicKey publicKey) { + keys.put(playerName, publicKey); + save();//autosave + } + + public static boolean hasKeyForPlayer(String name) { + return keys.containsKey(name); + } + + public static PublicKey getKeyForPlayer(String name) { + return keys.get(name); + } + } + + private static class IDEncoder { + private static final Random generator = new Random(SkyPlusPlusClient.client.player.getUuid().hashCode() + System.currentTimeMillis()); + private static final short uid = (short) (generator.nextInt(Short.MAX_VALUE - Short.MIN_VALUE + 1) + Short.MIN_VALUE);; + private static short counter = 0; + + public static int getNext() { + int n = ((uid & 0xFFFF) << 16) | (++counter & 0xFFFF); + // Chat.send(Chat.addLogo(String.format("§6Generated next ID: %d", n))); + return n; + } + } + + private static class SplitIncomingMessage { + public int id; + public int size; + + private final Map parts = new HashMap<>(); + + public SplitIncomingMessage(int id, int size) { + this.id = id; + this.size = size; + } + + public void addMessage(int pos, String message) { + if (pos > this.size) return; + // Chat.send(Chat.addLogo(String.format("Incoming split message %d: Adding [%s] @ %d", this.id, message, pos))); + this.parts.put(pos, message); + } + + public String merge(boolean silentFail) { + StringBuilder out = new StringBuilder(); + for (int i = 0; i < this.size; i++) { + String got = this.parts.get(i); + if (got == null) out.append(silentFail ? "" : String.format("[failed receiving %d/%d]", i, this.size)); + else out.append(got); + } + return out.toString(); + } + + public boolean isComplete() { + for (int i = 0; i < this.size; i++) { + if (!this.parts.containsKey(i)) { + // Chat.send(Chat.addLogo(String.format("incoming message %d is not complete @ %d (keys: %s)", this.id, i, this.parts.keySet().toString()))); + return false; + }; + } + return true; + } + } + + private static final String ALGORITHM = "RSA"; + private static final String TRANSFORMATION = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"; + + public static final List messageAliases = Arrays.asList("w", "whisper", "m", "message", "msg", "t", "tell", "pm", "epm", "emsg", "etell", "ewhisper"); + public static final List replyAliases = Arrays.asList("r", "reply", "er", "ereply"); + + public static final String MESSAGE_START_SIGN = "$+EM"; // [S]ky[+]+ [E]ncrypted [M]essage + public static final Integer PROTOCOL_VERSION = 1; + + public static final String REQUESTING_PUBLIC_KEY_UNIQUE = "{PUB_REQ}"; + public static final String REQUESTING_PUBLIC_KEY_COMMENT = "# &6A Sky++ user is requesting your public key to send encrypted messages. If you do not use Sky++, let them know you don't and can't continue."; + public static final String MESSAGE_SEND_UNIQUE = "{DM}"; + public static final String SHARING_PUBLIC_KEY_UNIQUE = "{PK}"; + + public static PublicKey SELF_PUB = null; + public static PrivateKey SELF_PRIV = null; + + private static final Map SplitIncomingMessages = new HashMap<>(); + + public static String lastCommunicatedUser = null; + + public static void requestPublicKey(String username) { + Chat.sendCommandToServer(String.format("message %s %s%d%s%s", username, MESSAGE_START_SIGN, PROTOCOL_VERSION, REQUESTING_PUBLIC_KEY_UNIQUE, REQUESTING_PUBLIC_KEY_COMMENT)); + } + + public static void sharePublicKey(String username) { + String b64PublicKey = Base64.getEncoder().encodeToString(SELF_PUB.getEncoded()); + int uid = IDEncoder.getNext(); + + // split into 128 character chunks (why 128? idk felt like it) + List chunked = new ArrayList<>(); + for (int start = 0; start < b64PublicKey.length(); start += 128) { + int end = Math.min(b64PublicKey.length(), start + 128); + chunked.add(b64PublicKey.substring(start, end)); + } + + Timer timer = new Timer(); + + Iterator iterator = chunked.iterator(); + + // loop every 400ms to reduce spam kick chance + timer.schedule(new TimerTask() { + private int i = 0; + @Override + public void run() { + if (i < chunked.size()) { + Chat.send(Chat.addLogo(String.format("§7Sending public key part §5%d§8/§5%d§7 to §5%s", i + 1, chunked.size(), username))); + Chat.sendCommandToServer(String.format("message %s %s%d%sM%d_%d/%d:%s", + username, + MESSAGE_START_SIGN, + PROTOCOL_VERSION, + SHARING_PUBLIC_KEY_UNIQUE, + uid, + i + 1, // human-readable 1/4 not 0/4 for start + chunked.size(), + chunked.get(i) + )); + i++; + } else timer.cancel(); + + } + }, 0, 400); + } + + public static void processOutgoingChatMessage(String destinationUsername, String messageContent) { + if (!KeyStore.hasKeyForPlayer(destinationUsername.toLowerCase())) { + Chat.send(Chat.addLogo(String.format( + "§cFailed to send message - %s's public key is not in database. Requesting now. Try sending message again later.", destinationUsername))); + requestPublicKey(destinationUsername); + return; + } + try { + // for some reason can't encrypt more than 190 bytes + if (messageContent.length() > 175) { + Chat.send(Chat.addLogo(String.format("§cFailed to encrypt message due to it being too long. Cut it down by %d characters and try again.", + messageContent.length() - 175))); + } + PublicKey publicKey = KeyStore.getKeyForPlayer(destinationUsername.toLowerCase()); + byte[] byteEncrypted = encrypt(messageContent, publicKey); + String b64Encrypted = Base64.getEncoder().encodeToString(byteEncrypted); + int uid = IDEncoder.getNext(); + + List chunked = new ArrayList<>(); + for (int start = 0; start < b64Encrypted.length(); start += 128) { + int end = Math.min(b64Encrypted.length(), start + 128); + chunked.add(b64Encrypted.substring(start, end)); + } + Timer timer = new Timer(); + + Iterator iterator = chunked.iterator(); + + // loop every 400ms to reduce spam kick chance + timer.schedule(new TimerTask() { + private int i = 0; + @Override + public void run() { + if (i < chunked.size()) { + Chat.send(Chat.addLogo(String.format("§7Sending encrypted private message part §5%d§8/§5%d§7 to §5%s", i + 1, chunked.size(), destinationUsername))); + lastCommunicatedUser = destinationUsername; + Chat.sendCommandToServer(String.format("message %s %s%d%sM%d_%d/%d:%s", + destinationUsername, + MESSAGE_START_SIGN, + PROTOCOL_VERSION, + MESSAGE_SEND_UNIQUE, + uid, + i + 1, // human-readable 1/4 not 0/4 for start + chunked.size(), + chunked.get(i) + )); + i++; + } else timer.cancel(); + + } + }, 0, 400); +// Chat.sendCommandToServer("message " + destinationUsername + " " + MESSAGE_START_SIGN + "hihihihihi"); +// Chat.sendCommandToServer(String.format("message %s %s%d%sM%d_%d/%d:%s", +// username, +// MESSAGE_START_SIGN, +// PROTOCOL_VERSION, +// SHARING_PUBLIC_KEY_UNIQUE, +// uid, +// i + 1, // human-readable 1/4 not 0/4 for start +// chunked.size(), +// chunked.get(i) +// )); + } catch (Exception e) { + Chat.send(Chat.addLogo(String.format("§cFailed to encrypt message. Received error %s", e.getMessage()))); + } + } + + public static boolean processIncomingMessage(Text msg) { + String stringified = TextStringifier.from(msg); + String cleaned = stringified.replaceAll("&[0-9A-Fa-fKkL-Ol-oRrXx]", ""); + Matcher matcher = StringChecker.directMessageRawPattern.matcher(cleaned); + if (matcher.find()) { + String username = matcher.group(1); + String content = matcher.group(2); + + lastCommunicatedUser = username; + + if (!content.startsWith(MESSAGE_START_SIGN)) return false; + + String expectedPreUnique = (MESSAGE_START_SIGN + PROTOCOL_VERSION.toString()); + String startingUnique = content.substring(expectedPreUnique.length()); + + if (startingUnique.startsWith(REQUESTING_PUBLIC_KEY_UNIQUE)) { + sharePublicKey(username); + } else if (startingUnique.startsWith(MESSAGE_SEND_UNIQUE)) { + // i reallyy should clean this up + // Chat.send(Chat.addLogo("received encrypted message")); + String dmMarkMessage = startingUnique.substring(MESSAGE_SEND_UNIQUE.length()); + String dmSpecifics = Arrays.stream(dmMarkMessage.split("M")) + .skip(1) + .collect(Collectors.joining("M")); + + String uid_str = dmSpecifics.split("_")[0]; + Integer uid = Integer.parseInt(uid_str); + + SplitIncomingMessage splitMessage = SplitIncomingMessages.get(uid); + if (splitMessage != null && splitMessage.isComplete()) { + // don't need to re-get same message when it's done already + Chat.send(Chat.addLogo(String.format("§7Already have full message (was trying to be resent) (%d parts), skipping.", splitMessage.size))); + return true; + } + + String partsAndMessage = Arrays.stream(dmSpecifics.split("_")) + .skip(1) + .collect(Collectors.joining("_")); + String partCounter = partsAndMessage.split(":")[0]; + Integer partSoFar = Integer.parseInt(partCounter.split("/")[0]); + Integer partIndex = partSoFar - 1; + Integer partTotal = Integer.parseInt(partCounter.split("/")[1]); + + Chat.send(Chat.addLogo(String.format("§7Received encrypted message part §5%d§8/§5%d§7 from §5%s", partSoFar, partTotal, username))); + + String encryptedMessage = Arrays.stream(partsAndMessage.split(":")) + .skip(1) + .collect(Collectors.joining(":")); + + if (!SplitIncomingMessages.containsKey(uid)) { + splitMessage = new SplitIncomingMessage(uid, partTotal); + SplitIncomingMessages.put(uid, splitMessage); + }; + + splitMessage.addMessage(partIndex, encryptedMessage); + + if (splitMessage.isComplete()) { + String message = splitMessage.merge(false); + if (message.contains("[failed receiving")) { + Chat.send(Chat.addLogo("§cFailed to decode message, one or more sections did not deliver correctly. Received: " + message)); + return false; + } + try { + String decrypted = decrypt(Base64.getDecoder().decode(message), SELF_PRIV); + Chat.send(String.format("§l§2[§r§aE§l§2] §8[§5%s §7-> me§8]§f: %s", username, decrypted)); + } catch (Exception e) { + Chat.send(Chat.addLogo("§cFailed to decrypt message. Received error: §4" + e.getMessage())); + } + } + + return true; + } else if (startingUnique.startsWith(SHARING_PUBLIC_KEY_UNIQUE)) { + // see this is where I probably should've used regex but didn't + String pKeyMessage = startingUnique.substring(SHARING_PUBLIC_KEY_UNIQUE.length()); + String pKeySpecifics = Arrays.stream(pKeyMessage.split("M")) + .skip(1) + .collect(Collectors.joining("M")); + + String uid_str = pKeySpecifics.split("_")[0]; + Integer uid = Integer.parseInt(uid_str); + + SplitIncomingMessage splitMessage = SplitIncomingMessages.get(uid); + if (splitMessage != null && splitMessage.isComplete()) { + // don't need to re-get same message when it's done already + Chat.send(Chat.addLogo("§7Already have full message (was trying to be resent), skipping.")); + return true; + } + + String partsAndMessage = Arrays.stream(pKeySpecifics.split("_")) + .skip(1) + .collect(Collectors.joining("_")); + String partCounter = partsAndMessage.split(":")[0]; + Integer partSoFar = Integer.parseInt(partCounter.split("/")[0]); + Integer partIndex = partSoFar - 1; + Integer partTotal = Integer.parseInt(partCounter.split("/")[1]); + + Chat.send(Chat.addLogo(String.format("§7Received public key part §5%d§8/§5%d§7 from §5%s", partSoFar, partTotal, username))); + + String encryptedMessage = Arrays.stream(partsAndMessage.split(":")) + .skip(1) + .collect(Collectors.joining(":")); + + if (!SplitIncomingMessages.containsKey(uid) || splitMessage == null) { + splitMessage = new SplitIncomingMessage(uid, partTotal); + SplitIncomingMessages.put(uid, splitMessage); + }; + + splitMessage.addMessage(partIndex, encryptedMessage); + + if (splitMessage.isComplete()) { + String fullKey = splitMessage.merge(false); + if (fullKey.contains("[failed receiving")) { + Chat.send(Chat.addLogo("§cFailed to decode public key, one or more sections did not deliver correctly. Received: " + fullKey)); + return false; + } + try { + KeyStore.importKey(username.toLowerCase(), KeyStore.base64ToPublicKey(fullKey)); + Chat.send(Chat.addLogo(String.format("§7Imported §5%s§7's public key.", username))); + } catch (Exception e) { + Chat.send(Chat.addLogo("§cFailed to import public key. Received error: §4" + e.getMessage())); + } + } + + return true; + } + + // Chat.send(String.format("received encrypted message from: %s with content %s", username, content)); + } + + return false; // true to cancel it being added to screen + } + + private static byte[] encrypt(String data, PublicKey key) throws Exception { + Cipher cipher = Cipher.getInstance(TRANSFORMATION); + cipher.init(Cipher.ENCRYPT_MODE, key); + return cipher.doFinal(data.getBytes(StandardCharsets.UTF_8)); + } + + private static String decrypt(byte[] encrypted, PrivateKey key) throws Exception { + Cipher cipher = Cipher.getInstance(TRANSFORMATION); + cipher.init(Cipher.DECRYPT_MODE, key); + byte[] plainBytes = cipher.doFinal(encrypted); + return new String(plainBytes, StandardCharsets.UTF_8); + } + + public static KeyPair generateKeyPair(int keySize) throws NoSuchAlgorithmException { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(keySize); + return kpg.generateKeyPair(); + } + + public static KeyPair generateKeyPair() throws NoSuchAlgorithmException { + return generateKeyPair(2048); + } + + public static void loadSelfKeys() { + try { + Path pub = FabricLoader.getInstance() + .getConfigDir() + .resolve("sky++/keys/$self-pub.key"); + Path priv = FabricLoader.getInstance() + .getConfigDir() + .resolve("sky++/keys/$self-priv.key"); + + String pubB64 = Files.readString(pub).trim(); + String privB64 = Files.readString(priv).trim(); + + SELF_PUB = loadPublicKey(pubB64); + SELF_PRIV = loadPrivateKey(privB64); + } catch (Exception e) { + throw new RuntimeException("Failed to load self keys", e); + } + } + + private static PrivateKey loadPrivateKey(String base64) throws Exception { + byte[] encoded = Base64.getDecoder().decode(base64); + PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(encoded); + KeyFactory kf = KeyFactory.getInstance(ALGORITHM); + return kf.generatePrivate(spec); + } + + public static PublicKey loadPublicKey(String base64) throws Exception { + byte[] encoded = Base64.getDecoder().decode(base64); + X509EncodedKeySpec spec = new X509EncodedKeySpec(encoded); + KeyFactory kf = KeyFactory.getInstance(ALGORITHM); + return kf.generatePublic(spec); + } + + public static void generateKeysAndSave() { + try { + Path folder = FabricLoader.getInstance() + .getConfigDir() + .resolve("sky++/keys"); + Files.createDirectories(folder); + + Path priv = FabricLoader.getInstance() + .getConfigDir() + .resolve("sky++/keys/$self-priv.key"); + Path pub = FabricLoader.getInstance() + .getConfigDir() + .resolve("sky++/keys/$self-pub.key"); + if (new File(priv.toString()).exists() || new File(pub.toString()).exists()) { + loadSelfKeys(); + return; + } + KeyPair pair = generateKeyPair(); + + Files.writeString( + pub, + Base64.getEncoder().encodeToString(pair.getPublic().getEncoded()) + ); + + Files.writeString( + priv, + Base64.getEncoder().encodeToString(pair.getPrivate().getEncoded()) + ); + + loadSelfKeys(); + } catch (Exception e) { + System.out.println("[err] failed to write sky++ priv/pub keys " + e.getMessage() + e.toString()); + } + } +} diff --git a/src/main/java/com/anotherpillow/skyplusplus/features/SlotLocker.java b/src/main/java/com/anotherpillow/skyplusplus/features/SlotLocker.java index e483229..cf9ed9d 100644 --- a/src/main/java/com/anotherpillow/skyplusplus/features/SlotLocker.java +++ b/src/main/java/com/anotherpillow/skyplusplus/features/SlotLocker.java @@ -14,6 +14,8 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import static com.anotherpillow.skyplusplus.util.Filesystem.resolveConfigPath; + public class SlotLocker { public static Slot hoveredSlot = null; public static ConcurrentHashMap lockedSlots = new ConcurrentHashMap<>(); @@ -22,12 +24,6 @@ public class SlotLocker { private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); private static final Type MAP_TYPE = new TypeToken>() {}.getType(); - private static Path resolveConfigPath(String fileName) { - return net.fabricmc.loader.api.FabricLoader.getInstance() - .getConfigDir() - .resolve(fileName); - } - public static void save() throws IOException { // Convert IntArrayList → int[] for JSON ConcurrentHashMap plain = new ConcurrentHashMap<>(); diff --git a/src/main/java/com/anotherpillow/skyplusplus/features/SmartTP.java b/src/main/java/com/anotherpillow/skyplusplus/features/SmartTP.java index 15075cd..f5ccb4b 100644 --- a/src/main/java/com/anotherpillow/skyplusplus/features/SmartTP.java +++ b/src/main/java/com/anotherpillow/skyplusplus/features/SmartTP.java @@ -17,17 +17,8 @@ public class SmartTP { public static void teleport(String username) { MinecraftClient client = MinecraftClient.getInstance(); - //? if >1.19.2 { - /*ClientPlayNetworkHandler handler = client.getNetworkHandler(); - if (handler == null) return; - handler.sendCommand("unlock"); - handler.sendCommand("tpahere" + username); - *///?} else { - ClientPlayerEntity player = client.player; - if (player == null) return; - player.sendCommand("unlock", Text.empty()); - player.sendCommand("tpahere " + username, Text.empty()); - //?} + Chat.sendCommandToServer("unlock"); + Chat.sendCommandToServer("tpahere " + username); awaitingLock = true; @@ -40,9 +31,9 @@ public void run() { //? if >1.19.2 { - /*handler.sendCommand("lock"); + /*Chat.sendCommandToServer("lock"); *///?} else { - player.sendCommand("lock", Text.empty()); + Chat.sendCommandToServer("lock"); //?} } }; @@ -73,9 +64,9 @@ public void run() { awaitingLock = false; //? if >1.19.2 { - /*handler.sendCommand("lock"); + /*Chat.sendCommandToServer("lock"); *///?} else { - player.sendCommand("lock", Text.empty()); + Chat.sendCommandToServer("lock"); //?} } }; diff --git a/src/main/java/com/anotherpillow/skyplusplus/mixin/ChatMixin.java b/src/main/java/com/anotherpillow/skyplusplus/mixin/ChatMixin.java index 693fe54..efee5b3 100644 --- a/src/main/java/com/anotherpillow/skyplusplus/mixin/ChatMixin.java +++ b/src/main/java/com/anotherpillow/skyplusplus/mixin/ChatMixin.java @@ -45,6 +45,11 @@ public abstract class ChatMixin { // System.out.println(Text.Serializer.toJson(text_message)); String message = text_message.getString(); + if (message.contains(Chatcryption.MESSAGE_START_SIGN) && Chatcryption.processIncomingMessage(text_message)) { + callback.cancel(); + return; + } + if (config.hideVisitingMessages && StringChecker.welcomeIslandCheck(message)) callback.cancel(); diff --git a/src/main/java/com/anotherpillow/skyplusplus/mixin/ClientPlayerEntityMixin.java b/src/main/java/com/anotherpillow/skyplusplus/mixin/ClientPlayerEntityMixin.java index 26f81e6..86589fb 100644 --- a/src/main/java/com/anotherpillow/skyplusplus/mixin/ClientPlayerEntityMixin.java +++ b/src/main/java/com/anotherpillow/skyplusplus/mixin/ClientPlayerEntityMixin.java @@ -1,12 +1,11 @@ package com.anotherpillow.skyplusplus.mixin; +import com.anotherpillow.skyplusplus.features.Chatcryption; import com.anotherpillow.skyplusplus.features.SlotLocker; import com.anotherpillow.skyplusplus.util.Server; import it.unimi.dsi.fastutil.ints.IntArrayList; import net.minecraft.client.MinecraftClient; import net.minecraft.client.network.ClientPlayerEntity; -import net.minecraft.entity.player.PlayerEntity; -import net.minecraft.entity.player.PlayerInventory; import net.minecraft.item.ItemStack; // was yarn 1.20.2-pre3 i think //? if >=1.20.2 { @@ -17,13 +16,17 @@ import net.minecraft.text.Text; import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.ModifyArg; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import com.anotherpillow.skyplusplus.config.SkyPlusPlusConfig; +import com.anotherpillow.skyplusplus.client.SkyPlusPlusClient; import com.anotherpillow.skyplusplus.util.Chat; + +import java.util.Arrays; + @Mixin(ClientPlayerEntity.class) public class ClientPlayerEntityMixin { @@ -58,4 +61,63 @@ private void stopDropSelectedItem(boolean entireStack, CallbackInfoReturnable1.19.2 { +/*import net.minecraft.client.network.ClientPlayNetworkHandler; +*///?} else { +//?} import net.minecraft.client.network.ClientPlayerEntity; import net.minecraft.text.MutableText; import net.minecraft.text.Text; diff --git a/src/main/java/com/anotherpillow/skyplusplus/util/Filesystem.java b/src/main/java/com/anotherpillow/skyplusplus/util/Filesystem.java new file mode 100644 index 0000000..881d6c7 --- /dev/null +++ b/src/main/java/com/anotherpillow/skyplusplus/util/Filesystem.java @@ -0,0 +1,11 @@ +package com.anotherpillow.skyplusplus.util; + +import java.nio.file.Path; + +public class Filesystem { + public static Path resolveConfigPath(String fileName) { + return net.fabricmc.loader.api.FabricLoader.getInstance() + .getConfigDir() + .resolve(fileName); + } +} diff --git a/src/main/java/com/anotherpillow/skyplusplus/util/StringChecker.java b/src/main/java/com/anotherpillow/skyplusplus/util/StringChecker.java index db0e958..af23457 100644 --- a/src/main/java/com/anotherpillow/skyplusplus/util/StringChecker.java +++ b/src/main/java/com/anotherpillow/skyplusplus/util/StringChecker.java @@ -24,6 +24,7 @@ public class StringChecker { public static Pattern playersOnlineJoinPattern = Pattern.compile("Players online: \\(\\d+/\\d+\\) - World Time: \\d+:\\d+ [A-Z]M"); public static Pattern raffleWinPattern = Pattern.compile("\\[SBRaffle\\] Congratulations go to [A-Z0-9_\\.]+ for winning [0-9\\.]+$ with \\d tickets"); public static Pattern visitingTitlePattern = Pattern.compile("§6\\-=§e[A-Za-z0-9_\\.]{1,16}'s Island§6=\\-"); // even names ending with "s" still have "'s" + public static Pattern directMessageRawPattern = Pattern.compile("^(?:)?\\[(?:\\[[A-Za-z]+?\\] )?([A-Za-z0-9\\_]{0,16})(?:@[a-zA-Z0-9\\_\\-]+)? -> me\\] (.+)"); public static String COLOUR_CODE_STRING = "[&§][0-9a-fkrl-o]"; public static Pattern COLOUR_CODE = Pattern.compile(COLOUR_CODE_STRING); diff --git a/src/main/java/com/anotherpillow/skyplusplus/util/TextStringifier.java b/src/main/java/com/anotherpillow/skyplusplus/util/TextStringifier.java index 221b8a5..6c8940e 100644 --- a/src/main/java/com/anotherpillow/skyplusplus/util/TextStringifier.java +++ b/src/main/java/com/anotherpillow/skyplusplus/util/TextStringifier.java @@ -63,6 +63,6 @@ public static String from(Text text) { return Optional.empty(); }, Style.EMPTY); - return out.toString().replace("§", "&"); + return out.toString().replaceAll("§", "&").replaceAll("&r", "&f"); // r is banned but f is default. } } diff --git a/src/main/resources/assets/skyplusplus/lang/en_us.json b/src/main/resources/assets/skyplusplus/lang/en_us.json index babc69f..1d93487 100644 --- a/src/main/resources/assets/skyplusplus/lang/en_us.json +++ b/src/main/resources/assets/skyplusplus/lang/en_us.json @@ -117,6 +117,8 @@ "skyplusplus.config.tweaks-improvements.join-commands-desc": "Commands to run upon joining the server. Separate with ,. Leave empty for none.", "skyplusplus.config.tweaks-improvements.copy-chat-message": "Copy Chat Messages", "skyplusplus.config.tweaks-improvements.copy-chat-message-desc": "Adds a button to the start of chat messages to copy them.", + "skyplusplus.config.tweaks-improvements.chatcryption": "Chatcryption", + "skyplusplus.config.tweaks-improvements.chatcryption-desc": "RSA encrypts private message communications. Requires all players you are messaging to use Sky++ with this enabled, it won't downgrade to plaintext.", "skyplusplus.config.discord-rpc.title": "Discord RPC", "skyplusplus.config.discord-rpc.enable": "Enable Discord RPC",