summary refs log tree commit diff stats
path: root/src/main/java/ganarchy/friendcode
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/ganarchy/friendcode')
-rw-r--r--src/main/java/ganarchy/friendcode/client/FriendCodeScreen.java5
-rw-r--r--src/main/java/ganarchy/friendcode/sam/I2PSamAuthUtil.java81
-rw-r--r--src/main/java/ganarchy/friendcode/sam/I2PSamControl.java14
-rw-r--r--src/main/java/ganarchy/friendcode/sam/I2PSamStateMachine.java22
-rw-r--r--src/main/java/ganarchy/friendcode/util/ConfigUtil.java106
-rw-r--r--src/main/java/ganarchy/friendcode/util/KeyUtil.java8
6 files changed, 211 insertions, 25 deletions
diff --git a/src/main/java/ganarchy/friendcode/client/FriendCodeScreen.java b/src/main/java/ganarchy/friendcode/client/FriendCodeScreen.java
index 9496942..4bd4971 100644
--- a/src/main/java/ganarchy/friendcode/client/FriendCodeScreen.java
+++ b/src/main/java/ganarchy/friendcode/client/FriendCodeScreen.java
@@ -2,7 +2,7 @@ package ganarchy.friendcode.client;
 
 import ganarchy.friendcode.FriendCode;
 import ganarchy.friendcode.mixin.FriendCodeIntegratedServerExt;
-import net.fabricmc.api.EnvType;
+import ganarchy.friendcode.util.KeyUtil;import net.fabricmc.api.EnvType;
 import net.fabricmc.api.Environment;
 import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.gui.screen.OpenToLanScreen;
@@ -137,9 +137,10 @@ public class FriendCodeScreen extends Screen {
         try {
             String privateKey = null;
             if (codeType == CodeType.WORLD) {
-                // FIXME
+                // FIXME this currently does nothing.
                 var worldDir = client.getServer().submit(() -> client.getServer().getSavePath(WorldSavePath.ROOT)).join();
                 var keyFile = worldDir.resolve("friendcode.key");
+                privateKey = KeyUtil.readKeyFile(keyFile);
             }
             client.loadBlockList();
             client.getServer().getNetworkIo().bind(null, port);
diff --git a/src/main/java/ganarchy/friendcode/sam/I2PSamAuthUtil.java b/src/main/java/ganarchy/friendcode/sam/I2PSamAuthUtil.java
index fad222c..ff84551 100644
--- a/src/main/java/ganarchy/friendcode/sam/I2PSamAuthUtil.java
+++ b/src/main/java/ganarchy/friendcode/sam/I2PSamAuthUtil.java
@@ -1,32 +1,87 @@
 package ganarchy.friendcode.sam;
 
+import ganarchy.friendcode.util.ConfigUtil;
+import org.apache.commons.codec.binary.Base32;
+
+import java.security.SecureRandom;
+import java.util.Properties;
+
 /**
-* Helper for I2P SAM authentication.
-*/
+ * Helper for I2P SAM authentication.
+ */
 public class I2PSamAuthUtil {
     /**
-    * Returns the currently active SAM auth pair.
-    */
+     * The default username. It is used by default.
+     */
+    private static final String DEFAULT_USERNAME = "minecraft_friendcode";
+
+    /**
+     * Fallback authentication password, used on first install.
+     */
+    private static final AuthenticationPair INSECURE_FALLBACK =
+        new AuthenticationPair(DEFAULT_USERNAME, "friendcode");
+
+    /**
+     * Returns the currently active SAM auth pair.
+     */
     public static AuthenticationPair getAuthPair() {
-        return new AuthenticationPair("minecraft_friendcode", "friendcode");
+        AuthenticationPair strongAuthPair = getStrongAuthPair();
+        if (strongAuthPair != null) {
+            return strongAuthPair;
+        }
+        return INSECURE_FALLBACK;
     }
 
     /**
-    * Generates and stores a modern auth pair.
-    *
-    * @return The generated auth pair.
-    */
+     * Generates and stores a modern auth pair.
+     *
+     * @return The generated auth pair.
+     */
     public static AuthenticationPair upgradeAuthPair() {
-        return new AuthenticationPair("minecraft_friendcode", "friendcode");
+        var rand = new SecureRandom();
+        var bytes = new byte[16];
+        rand.nextBytes(bytes);
+        var b32 = new Base32().encodeToString(bytes);
+        Properties auth = new Properties();
+        auth.setProperty("i2p.sam.username", DEFAULT_USERNAME);
+        auth.setProperty("i2p.sam.password", b32);
+        if (ConfigUtil.updateSettings(auth)) {
+            return new AuthenticationPair(DEFAULT_USERNAME, b32);
+        } else {
+            return INSECURE_FALLBACK;
+        }
     }
 
     /**
-    * Returns whether strong auth is enabled.
-    */
+     * Returns whether strong auth is enabled.
+     */
     public static boolean isStrongAuth() {
-        return false;
+        return getStrongAuthPair() != null;
     }
 
+
+    /**
+     * Returns the currently active strong SAM auth pair, or null if using the
+     * weak fallback.
+     */
+    private static AuthenticationPair getStrongAuthPair() {
+        Properties auth = new Properties();
+        if (ConfigUtil.getSettings(auth)) {
+            String username = auth.getProperty("i2p.sam.username");
+            String password = auth.getProperty("i2p.sam.password");
+            if (username != null && password != null) {
+                return new AuthenticationPair(username, password);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * An authentication pair.
+     *
+     * @param user     The username.
+     * @param password The password.
+     */
     public record AuthenticationPair(String user, String password) {
     }
 }
diff --git a/src/main/java/ganarchy/friendcode/sam/I2PSamControl.java b/src/main/java/ganarchy/friendcode/sam/I2PSamControl.java
index 693cba3..50748e6 100644
--- a/src/main/java/ganarchy/friendcode/sam/I2PSamControl.java
+++ b/src/main/java/ganarchy/friendcode/sam/I2PSamControl.java
@@ -12,7 +12,7 @@ public class I2PSamControl extends I2PSamStateMachine {
     private final boolean zeroHop;
     /**
      * The session's private key.
-     *
+     * <p>
      * The threat model assumes the local machine (including RAM) to be trusted.
      */
     private String privateKey;
@@ -25,7 +25,7 @@ public class I2PSamControl extends I2PSamStateMachine {
     /**
      * Creates a new SAM control socket.
      *
-     * @param zeroHop Whether to use zero-hop tunnels.
+     * @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) {
@@ -39,12 +39,12 @@ public class I2PSamControl extends I2PSamStateMachine {
             // try IPv6 first
             // there's no Inet6Address.getLoopbackAddress() which is unfortunate.
             final Socket samSocket = new Socket();
-            samSocket.connect(new InetSocketAddress(Inet6Address.getByAddress("localhost", new byte[] {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, 0), 7656), 3000);
+            samSocket.connect(new InetSocketAddress(Inet6Address.getByAddress("localhost", new byte[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, 0), 7656), 3000);
             return this.connect(samSocket);
         } catch (IOException e) {
             try {
                 final Socket samSocket = new Socket();
-                samSocket.connect(new InetSocketAddress(Inet4Address.getByAddress("localhost", new byte[] {127,0,0,1}), 7656), 3000);
+                samSocket.connect(new InetSocketAddress(Inet4Address.getByAddress("localhost", new byte[] {127, 0, 0, 1}), 7656), 3000);
                 return this.connect(samSocket);
             } catch (IOException ex) {
                 return false;
@@ -63,8 +63,8 @@ public class I2PSamControl extends I2PSamStateMachine {
             if (this.privateKey == null) {
                 // generate our keys
                 this.sendCommand(new I2PSamCommand(
-                        "DEST", "GENERATE",
-                        ImmutableMap.of("SIGNATURE_TYPE", "EdDSA_SHA512_Ed25519")
+                    "DEST", "GENERATE",
+                    ImmutableMap.of("SIGNATURE_TYPE", "EdDSA_SHA512_Ed25519")
                 ));
                 var dest = this.getCommand("DEST", "REPLY");
                 this.publicKey = dest.parameters().get("PUB");
@@ -125,6 +125,7 @@ public class I2PSamControl extends I2PSamStateMachine {
 
     /**
      * Creates a stream forwarder.
+     *
      * @return A stream forwarder.
      */
     public I2PSamStreamForwarder forwardStream(String port) {
@@ -133,6 +134,7 @@ public class I2PSamControl extends I2PSamStateMachine {
 
     /**
      * Creates a stream forwarder.
+     *
      * @return A stream connector.
      */
     public I2PSamStreamConnector connectStream(String b32) {
diff --git a/src/main/java/ganarchy/friendcode/sam/I2PSamStateMachine.java b/src/main/java/ganarchy/friendcode/sam/I2PSamStateMachine.java
index 57424dd..37deda3 100644
--- a/src/main/java/ganarchy/friendcode/sam/I2PSamStateMachine.java
+++ b/src/main/java/ganarchy/friendcode/sam/I2PSamStateMachine.java
@@ -2,6 +2,7 @@ package ganarchy.friendcode.sam;
 
 import com.google.common.collect.ImmutableMap;
 import ganarchy.friendcode.FriendCode;
+import ganarchy.friendcode.util.ConfigUtil;
 import net.minecraft.util.Util;
 
 import java.io.*;
@@ -68,7 +69,18 @@ public abstract class I2PSamStateMachine implements Closeable {
                     "PASSWORD", auth.password()
                 )
             ));
-            return "OK".equals(this.getCommand("HELLO", "REPLY").parameters().get("RESULT"));
+            var replyParams = this.getCommand("HELLO", "REPLY").parameters();
+            if ("I2P_ERROR".equals(replyParams.get("RESULT"))) {
+                var msg = replyParams.getOrDefault("MESSAGE", "");
+                FriendCode.LOGGER.error("Couldn't connect to I2P: {}", msg);
+                FriendCode.LOGGER.error(
+                    "If the above error is about authorization,"
+                        + " please create the relevant file at {} and provide"
+                        + " i2p.sam.username and i2p.sam.password.",
+                    ConfigUtil.getGlobalConfigFilePath()
+                );
+            }
+            return "OK".equals(replyParams.get("RESULT"));
         } catch (IOException e) {
             return false;
         }
@@ -76,7 +88,7 @@ public abstract class I2PSamStateMachine implements Closeable {
 
     /**
      * Attempts to step the SAM session.
-     *
+     * <p>
      * This will generally not block, unless something went wrong.
      */
     public void tryStep() {
@@ -143,7 +155,7 @@ public abstract class I2PSamStateMachine implements Closeable {
     /**
      * Reads a name from the SAM session.
      *
-     * @param name The command name.
+     * @param name   The command name.
      * @param opcode The subcommand name.
      * @return The name line, or null if something went wrong.
      * @throws IOException If an I/O error occurs.
@@ -190,6 +202,7 @@ public abstract class I2PSamStateMachine implements Closeable {
 
     /**
      * Returns the SAM bridge address.
+     *
      * @return The SAM bridge address.
      */
     protected SocketAddress getSamBridgeAddress() {
@@ -201,12 +214,13 @@ public abstract class I2PSamStateMachine implements Closeable {
 
     /**
      * Closes this connection.
+     *
      * @throws IOException As per {@link Socket#close()}.
      */
     public void close() throws IOException {
         if (this.connected) {
             this.samSocket.close();
-	}
+        }
     }
 
     protected Socket unwrap() {
diff --git a/src/main/java/ganarchy/friendcode/util/ConfigUtil.java b/src/main/java/ganarchy/friendcode/util/ConfigUtil.java
new file mode 100644
index 0000000..b2d6af2
--- /dev/null
+++ b/src/main/java/ganarchy/friendcode/util/ConfigUtil.java
@@ -0,0 +1,106 @@
+package ganarchy.friendcode.util;
+
+import ganarchy.friendcode.FriendCode;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Properties;
+
+import static org.apache.commons.lang3.SystemUtils.USER_HOME;
+import static org.apache.commons.lang3.SystemUtils.getEnvironmentVariable;
+
+/**
+ * Config utils.
+ */
+public class ConfigUtil {
+    /**
+     * The message stored in the config. Could probably use some improvement.
+     */
+    private static final String CONFIG_MESSAGE =
+        "This is the friendcode config file."
+            + " It's created when you open your world to a friend code.";
+    /**
+     * The (cached) global friendcode config dir.
+     */
+    private static File confdir;
+
+    /**
+     * Creates and returns the global friendcode config dir.
+     */
+    public static File getGlobalConfig() {
+        if (confdir != null) {
+            return confdir;
+        }
+        var configHome = getEnvironmentVariable("XDG_CONFIG_HOME", "");
+        if (!configHome.isEmpty()) {
+            confdir = Path.of(configHome, "mc_friendcode").toFile();
+        } else {
+            confdir = Path.of(USER_HOME, ".config", "mc_friendcode").toFile();
+        }
+        confdir.mkdirs();
+        return confdir;
+    }
+
+    /**
+     * Returns the path to the global friendcode config file.
+     */
+    public static Path getGlobalConfigFilePath() {
+        return getGlobalConfig().toPath().resolve("config.properties");
+    }
+
+    /**
+     * Retrieves the settings from the global config file and stores them in
+     * the given object.
+     *
+     * @param properties Where to store the read config.
+     * @return Whether reading was successful.
+     */
+    public static boolean getSettings(Properties properties) {
+        try {
+            var inputStream = Files.newInputStream(
+                getGlobalConfigFilePath()
+            );
+            var reader = new InputStreamReader(
+                inputStream, StandardCharsets.UTF_8
+            );
+            properties.load(reader);
+            return true;
+        } catch (IOException e) {
+            return false;
+        }
+    }
+
+    /**
+     * Updates the global config file with the settings in the given object.
+     *
+     * @param properties The settings to add to the config.
+     * @return Whether writing was successful.
+     */
+    public static boolean updateSettings(Properties properties) {
+        var prop = new Properties();
+        if (!getSettings(prop)) {
+            FriendCode.LOGGER.warn(
+                "Couldn't read global config." +
+                    " If it doesn't exist, it will be created."
+            );
+        }
+        prop.putAll(properties);
+        try {
+            var outputStream = Files.newOutputStream(
+                getGlobalConfigFilePath()
+            );
+            var writer = new OutputStreamWriter(
+                outputStream, StandardCharsets.UTF_8
+            );
+            prop.store(writer, CONFIG_MESSAGE);
+            return true;
+        } 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
index cc6396c..e8e07e5 100644
--- a/src/main/java/ganarchy/friendcode/util/KeyUtil.java
+++ b/src/main/java/ganarchy/friendcode/util/KeyUtil.java
@@ -1,7 +1,15 @@
 package ganarchy.friendcode.util;
 
+import java.nio.file.Path;
+
 /**
 * Helper to deal with private keys.
 */
 public class KeyUtil {
+    public static String readKeyFile(Path keyFile) {
+        return null;
+    }
+    public static boolean writeKeyFile(Path keyFile, String key) {
+        return false;
+    }
 }