summary refs log tree commit diff stats
path: root/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java')
-rw-r--r--src/main/java/ganarchy/friendcode/client/CodeType.java16
-rw-r--r--src/main/java/ganarchy/friendcode/client/FriendCodeScreen.java51
-rw-r--r--src/main/java/ganarchy/friendcode/client/I2PSamPinger.java6
-rw-r--r--src/main/java/ganarchy/friendcode/client/SamProxyThread.java2
-rw-r--r--src/main/java/ganarchy/friendcode/sam/I2PSamAuthUtil.java32
-rw-r--r--src/main/java/ganarchy/friendcode/sam/I2PSamCommand.java3
-rw-r--r--src/main/java/ganarchy/friendcode/sam/I2PSamControl.java102
-rw-r--r--src/main/java/ganarchy/friendcode/sam/I2PSamStateMachine.java21
-rw-r--r--src/main/java/ganarchy/friendcode/util/KeyUtil.java7
9 files changed, 191 insertions, 49 deletions
diff --git a/src/main/java/ganarchy/friendcode/client/CodeType.java b/src/main/java/ganarchy/friendcode/client/CodeType.java
new file mode 100644
index 0000000..e9e5c57
--- /dev/null
+++ b/src/main/java/ganarchy/friendcode/client/CodeType.java
@@ -0,0 +1,16 @@
+package ganarchy.friendcode.client;
+
+import net.minecraft.text.Text;public enum CodeType {
+    SESSION("friendcode.code_type.session"),
+    WORLD("friendcode.code_type.world");
+
+    private final Text translatableText;
+
+    CodeType(String textKey) {
+        this.translatableText = Text.translatable(textKey);
+    }
+
+    public Text getSimpleTranslatableName(CodeType this) {
+        return this.translatableText;
+    }
+}
diff --git a/src/main/java/ganarchy/friendcode/client/FriendCodeScreen.java b/src/main/java/ganarchy/friendcode/client/FriendCodeScreen.java
index 38703a5..9496942 100644
--- a/src/main/java/ganarchy/friendcode/client/FriendCodeScreen.java
+++ b/src/main/java/ganarchy/friendcode/client/FriendCodeScreen.java
@@ -15,21 +15,23 @@ import net.minecraft.screen.ScreenTexts;
 import net.minecraft.server.network.ServerPlayerEntity;
 import net.minecraft.text.MutableText;
 import net.minecraft.text.Text;
-import net.minecraft.world.GameMode;
+import net.minecraft.util.WorldSavePath;import net.minecraft.world.GameMode;
 
 import java.io.IOException;
 
 @Environment(EnvType.CLIENT)
 public class FriendCodeScreen extends Screen {
-    private static final Text ALLOW_COMMANDS_TEXT = Text.translatable("selectWorld.allowCommands");
-    private static final Text GAME_MODE_TEXT = Text.translatable("selectWorld.gameMode");
-    private static final Text OTHER_PLAYERS_TEXT = Text.translatable("lanServer.otherPlayers");
+    private static final Text
+        ALLOW_COMMANDS_TEXT = Text.translatable("selectWorld.allowCommands"),
+        GAME_MODE_TEXT = Text.translatable("selectWorld.gameMode"),
+        CODE_TYPE_TEXT = Text.translatable("friendcode.code_type"),
+        OTHER_PLAYERS_TEXT = Text.translatable("lanServer.otherPlayers"),
+        START_SHARING = Text.translatable("friendcode.start");
     private final Screen parent;
+    private CodeType codeType = CodeType.SESSION;
     private GameMode gameMode = GameMode.SURVIVAL;
     private boolean allowCommands;
 
-    private static final Text START_SHARING = Text.translatable("friendcode.start");
-
     public FriendCodeScreen(Screen parent) {
         super(Text.translatable("friendcode.title"));
         this.parent = parent;
@@ -51,7 +53,12 @@ public class FriendCodeScreen extends Screen {
         this.addDrawableChild(
             CyclingButtonWidget
             .builder(GameMode::getSimpleTranslatableName)
-            .values(GameMode.SURVIVAL, GameMode.SPECTATOR, GameMode.CREATIVE, GameMode.ADVENTURE)
+            .values(
+                GameMode.SURVIVAL,
+                GameMode.SPECTATOR,
+                GameMode.CREATIVE,
+                GameMode.ADVENTURE
+            )
             .initially(this.gameMode)
             .build(
                 this.width / 2 - 155,
@@ -79,6 +86,24 @@ public class FriendCodeScreen extends Screen {
             )
         );
 
+        // friend code type button
+        this.addDrawableChild(
+            CyclingButtonWidget
+            .builder(CodeType::getSimpleTranslatableName)
+            .values(CodeType.SESSION, CodeType.WORLD)
+            .initially(this.codeType)
+            .build(
+                this.width / 2 - 155,
+                125,
+                310,
+                20,
+                CODE_TYPE_TEXT,
+                (button, codeType) -> {
+                    this.codeType = codeType;
+                }
+            )
+        ).active = false;
+
         // start sharing
         this.addDrawableChild(new ButtonWidget(
             this.width / 2 - 155,
@@ -89,7 +114,7 @@ public class FriendCodeScreen extends Screen {
             button -> {
                 // FIXME
                 int i = NetworkUtils.findLocalPort();
-                var samPinger = openToFriends(this.client, this.gameMode, this.allowCommands, i);
+                var samPinger = openToFriends(this.client, this.codeType, this.gameMode, this.allowCommands, i);
                 MutableText text = samPinger != null ? Text.translatable("commands.publish.started", i) : Text.translatable("commands.publish.failed");
                 this.client.inGameHud.getChatHud().addMessage(text);
                 this.client.updateWindowTitle();
@@ -108,14 +133,20 @@ public class FriendCodeScreen extends Screen {
         ));
     }
 
-    private static I2PSamPinger openToFriends(MinecraftClient client, GameMode gameMode, boolean allowCommands, int port) {
+    private static I2PSamPinger openToFriends(MinecraftClient client, CodeType codeType, GameMode gameMode, boolean allowCommands, int port) {
         try {
+            String privateKey = null;
+            if (codeType == CodeType.WORLD) {
+                // FIXME
+                var worldDir = client.getServer().submit(() -> client.getServer().getSavePath(WorldSavePath.ROOT)).join();
+                var keyFile = worldDir.resolve("friendcode.key");
+            }
             client.loadBlockList();
             client.getServer().getNetworkIo().bind(null, port);
             FriendCode.LOGGER.info("Started serving on {}", port);
             ((FriendCodeIntegratedServerExt) client.getServer()).lanPort(port);
             // reuse LAN pinger machinery instead of rolling our own
-            var lanPinger = new I2PSamPinger(client.getServer().getServerMotd(), "" + port);
+            var lanPinger = new I2PSamPinger(client.getServer().getServerMotd(), "" + port, privateKey);
             ((FriendCodeIntegratedServerExt) client.getServer()).lanPinger(lanPinger);
             lanPinger.start();
             ((FriendCodeIntegratedServerExt) client.getServer()).forcedGameMode(gameMode);
diff --git a/src/main/java/ganarchy/friendcode/client/I2PSamPinger.java b/src/main/java/ganarchy/friendcode/client/I2PSamPinger.java
index 0476c61..e0824b9 100644
--- a/src/main/java/ganarchy/friendcode/client/I2PSamPinger.java
+++ b/src/main/java/ganarchy/friendcode/client/I2PSamPinger.java
@@ -14,11 +14,11 @@ public class I2PSamPinger extends LanServerPinger implements LanSendPing {
     private volatile Status status = Status.IDLE;
     private volatile boolean stopSam;
 
-    public I2PSamPinger(String motd, String port) throws IOException {
+    public I2PSamPinger(String motd, String port, String privateKey) throws IOException {
         super(motd, port);
         // cba to mixin
         this.port = port;
-        this.sam = new I2PSamControl(true);
+        this.sam = new I2PSamControl(true, privateKey);
     }
 
     @Override
@@ -96,7 +96,7 @@ public class I2PSamPinger extends LanServerPinger implements LanSendPing {
     }
 
     public String pubkey() {
-        return this.sam.pubkey();
+        return this.sam.publicKey();
     }
 
     public void stopSam() {
diff --git a/src/main/java/ganarchy/friendcode/client/SamProxyThread.java b/src/main/java/ganarchy/friendcode/client/SamProxyThread.java
index c7db41f..2938455 100644
--- a/src/main/java/ganarchy/friendcode/client/SamProxyThread.java
+++ b/src/main/java/ganarchy/friendcode/client/SamProxyThread.java
@@ -38,7 +38,7 @@ public class SamProxyThread extends Thread {
         // who cares if you have 3 threads just to connect to a minecraft server/friend code
         // it's only 3 threads
         // the "server" is far more efficient, only requiring one
-        try (final I2PSamControl control = new I2PSamControl(zeroHop)) {
+        try (final I2PSamControl control = new I2PSamControl(zeroHop, null)) {
             if (!control.connect()) {
                 this.running = false;
                 this.status = Status.CONNECTION_FAILED;
diff --git a/src/main/java/ganarchy/friendcode/sam/I2PSamAuthUtil.java b/src/main/java/ganarchy/friendcode/sam/I2PSamAuthUtil.java
new file mode 100644
index 0000000..fad222c
--- /dev/null
+++ b/src/main/java/ganarchy/friendcode/sam/I2PSamAuthUtil.java
@@ -0,0 +1,32 @@
+package ganarchy.friendcode.sam;
+
+/**
+* Helper for I2P SAM authentication.
+*/
+public class I2PSamAuthUtil {
+    /**
+    * Returns the currently active SAM auth pair.
+    */
+    public static AuthenticationPair getAuthPair() {
+        return new AuthenticationPair("minecraft_friendcode", "friendcode");
+    }
+
+    /**
+    * Generates and stores a modern auth pair.
+    *
+    * @return The generated auth pair.
+    */
+    public static AuthenticationPair upgradeAuthPair() {
+        return new AuthenticationPair("minecraft_friendcode", "friendcode");
+    }
+
+    /**
+    * Returns whether strong auth is enabled.
+    */
+    public static boolean isStrongAuth() {
+        return false;
+    }
+
+    public record AuthenticationPair(String user, String password) {
+    }
+}
diff --git a/src/main/java/ganarchy/friendcode/sam/I2PSamCommand.java b/src/main/java/ganarchy/friendcode/sam/I2PSamCommand.java
index b1cece8..c453bf0 100644
--- a/src/main/java/ganarchy/friendcode/sam/I2PSamCommand.java
+++ b/src/main/java/ganarchy/friendcode/sam/I2PSamCommand.java
@@ -3,7 +3,6 @@ package ganarchy.friendcode.sam;
 import com.google.common.collect.ImmutableMap;
 
 import java.util.Map;
-import java.util.regex.Pattern;
 
 /**
  * An I2P SAM command.
@@ -56,7 +55,7 @@ record I2PSamCommand(String name, String opcode, Map<String, String> parameters)
                 throw new IllegalArgumentException("PING/PONG does not accept parameters");
             }
             // skip any other validation
-            return;
+            //return;
         }
 
         // everything else is fine
diff --git a/src/main/java/ganarchy/friendcode/sam/I2PSamControl.java b/src/main/java/ganarchy/friendcode/sam/I2PSamControl.java
index f470bb0..693cba3 100644
--- a/src/main/java/ganarchy/friendcode/sam/I2PSamControl.java
+++ b/src/main/java/ganarchy/friendcode/sam/I2PSamControl.java
@@ -10,11 +10,27 @@ import java.net.Socket;
 
 public class I2PSamControl extends I2PSamStateMachine {
     private final boolean zeroHop;
-    private String pubkey;
+    /**
+     * The session's private key.
+     *
+     * The threat model assumes the local machine (including RAM) to be trusted.
+     */
+    private String privateKey;
+    /**
+     * The session's public key.
+     */
+    private String publicKey;
     private String id;
 
-    public I2PSamControl(boolean zeroHop) {
+    /**
+     * Creates a new SAM control socket.
+     *
+     * @param zeroHop Whether to use zero-hop tunnels.
+     * @param privateKey The (optional) identity private key, for persistent friend codes.
+     */
+    public I2PSamControl(boolean zeroHop, String privateKey) {
         this.zeroHop = zeroHop;
+        this.privateKey = privateKey;
     }
 
     @Override
@@ -44,15 +60,16 @@ public class I2PSamControl extends I2PSamStateMachine {
         try {
             // try to enable auth
             this.enableAuth();
-            // FIXME "Friend Code Type: World" vs "Friend Code Type: Session"
-            // generate our keys
-            this.sendCommand(new I2PSamCommand(
-                "DEST", "GENERATE",
-                ImmutableMap.of("SIGNATURE_TYPE", "EdDSA_SHA512_Ed25519")
-            ));
-            var dest = this.getCommand("DEST", "REPLY");
-            this.pubkey = dest.parameters().get("PUB");
-            var privkey = dest.parameters().get("PRIV");
+            if (this.privateKey == null) {
+                // generate our keys
+                this.sendCommand(new I2PSamCommand(
+                        "DEST", "GENERATE",
+                        ImmutableMap.of("SIGNATURE_TYPE", "EdDSA_SHA512_Ed25519")
+                ));
+                var dest = this.getCommand("DEST", "REPLY");
+                this.publicKey = dest.parameters().get("PUB");
+                this.privateKey = dest.parameters().get("PRIV");
+            }
             // setup our session
             int i = 0;
             String status;
@@ -70,7 +87,7 @@ public class I2PSamControl extends I2PSamStateMachine {
                     ).put(
                         "ID", this.id
                     ).put(
-                        "DESTINATION", privkey
+                        "DESTINATION", this.privateKey
                     ).put(
                         "inbound.length", this.zeroHop ? "0" : "1"
                     ).put(
@@ -88,7 +105,19 @@ public class I2PSamControl extends I2PSamStateMachine {
                     ).build()
                 ));
             } while ("DUPLICATED_ID".equals(status = this.getCommand("SESSION", "STATUS").parameters().get("RESULT")));
-            return "OK".equals(status);
+            if ("OK".equals(status)) {
+                // find our public key (if we're using persistent keys)
+                if (this.publicKey == null) {
+                    this.sendCommand(new I2PSamCommand("NAMING", "LOOKUP", ImmutableMap.of("NAME", "ME")));
+                    final var lookup = this.getCommand("NAMING", "REPLY");
+                    if (!"OK".equals(lookup.parameters().get("RESULT"))) {
+                        return false;
+                    }
+                    this.publicKey = lookup.parameters().get("VALUE");
+                }
+                return true;
+            }
+            return false;
         } catch (IOException e) {
             return false;
         }
@@ -111,10 +140,17 @@ public class I2PSamControl extends I2PSamStateMachine {
     }
 
     /**
-     * Returns the session pubkey.
+     * Returns the session public key.
      */
-    public String pubkey() {
-        return this.pubkey;
+    public String publicKey() {
+        return this.publicKey;
+    }
+
+    /**
+     * Returs the session private key.
+     */
+    public String privateKey() {
+        return this.privateKey;
     }
 
     /**
@@ -123,15 +159,39 @@ public class I2PSamControl extends I2PSamStateMachine {
      * @throws IOException If an I/O error occurs.
      */
     private void enableAuth() throws IOException {
-        // enable auth (if it hasn't been explicitly disabled by the user), for the user's sake
+        // enable auth (if it hasn't been explicitly disabled by the user), for
+        // the user's sake
         // (ugh i2p you really fucked up by not making this the default)
-        // btw tor does this with a filesystem path to an auth cookie (which is much more secure) but we digress
+        // btw tor does this with a filesystem path to an auth cookie (which is
+        // much more secure) but we digress.
+        // check if we're already using strong auth.
+        if (I2PSamAuthUtil.isStrongAuth()) {
+            return;
+        }
+        // attempt to delete old user - versions 1.0.0 and 1.0.1 of the mod are
+        // deprecated, unsupported, and should never be used.
+        this.sendCommand(new I2PSamCommand(
+            "AUTH", "REMOVE",
+            ImmutableMap.of("USER", "minecraft_friendcode")
+        ));
+        // check if removed.
+        var removed = "OK".equals(
+            this.getCommand("AUTH", "STATUS").parameters().get("RESULT")
+        );
+        // generate secure auth pair
+        var auth = I2PSamAuthUtil.upgradeAuthPair();
+        // add new user
         this.sendCommand(new I2PSamCommand(
             "AUTH", "ADD",
-            ImmutableMap.of("USER", "minecraft_friendcode", "PASSWORD", "friendcode")
+            ImmutableMap.of("USER", auth.user(), "PASSWORD", auth.password())
         ));
-        // if the user already exists, don't enable auth
-        if ("OK".equals(this.getCommand("AUTH", "STATUS").parameters().get("RESULT"))) {
+        if ("OK".equals(
+            this.getCommand("AUTH", "STATUS").parameters().get("RESULT")
+        )) {
+            // old user existed - don't mess with auth enable.
+            if (removed) {
+                return;
+            }
             this.sendCommand(new I2PSamCommand("AUTH", "ENABLE"));
             // ignore the response on this one, just get it off the queue
             this.getCommand("AUTH", "STATUS");
diff --git a/src/main/java/ganarchy/friendcode/sam/I2PSamStateMachine.java b/src/main/java/ganarchy/friendcode/sam/I2PSamStateMachine.java
index 1c55bc6..57424dd 100644
--- a/src/main/java/ganarchy/friendcode/sam/I2PSamStateMachine.java
+++ b/src/main/java/ganarchy/friendcode/sam/I2PSamStateMachine.java
@@ -13,22 +13,12 @@ import java.util.Iterator;
 import java.util.Objects;
 
 public abstract class I2PSamStateMachine implements Closeable {
-    // FIXME user/password from config
-    private static final I2PSamCommand HELLO_MESSAGE = new I2PSamCommand(
-        "HELLO", "VERSION",
-        ImmutableMap.of(
-            "MIN", "3.2",
-            "USER", "minecraft_friendcode",
-            "PASSWORD", "friendcode"
-        )
-    );
     private Socket samSocket;
     private PushbackReader reader;
     private OutputStreamWriter writer;
     private final ArrayDeque<I2PSamCommand> queue = new ArrayDeque<>();
     private boolean started;
     private boolean connected;
-    private String id;
 
     protected I2PSamStateMachine() {
     }
@@ -69,8 +59,15 @@ public abstract class I2PSamStateMachine implements Closeable {
 
 
             // send HELLO
-            // FIXME let the user change the credentials
-            this.sendCommand(HELLO_MESSAGE);
+            var auth = I2PSamAuthUtil.getAuthPair();
+            this.sendCommand(new I2PSamCommand(
+                "HELLO", "VERSION",
+                ImmutableMap.of(
+                    "MIN", "3.2",
+                    "USER", auth.user(),
+                    "PASSWORD", auth.password()
+                )
+            ));
             return "OK".equals(this.getCommand("HELLO", "REPLY").parameters().get("RESULT"));
         } catch (IOException e) {
             return false;
diff --git a/src/main/java/ganarchy/friendcode/util/KeyUtil.java b/src/main/java/ganarchy/friendcode/util/KeyUtil.java
new file mode 100644
index 0000000..cc6396c
--- /dev/null
+++ b/src/main/java/ganarchy/friendcode/util/KeyUtil.java
@@ -0,0 +1,7 @@
+package ganarchy.friendcode.util;
+
+/**
+* Helper to deal with private keys.
+*/
+public class KeyUtil {
+}