From 97c48aa55abca34478f470fa99c3a150c6629f16 Mon Sep 17 00:00:00 2001 From: SoniEx2 Date: Mon, 25 Jul 2022 21:38:13 -0300 Subject: Fix SAM bridge security --- .../friendcode/client/FriendCodeScreen.java | 5 +- .../ganarchy/friendcode/sam/I2PSamAuthUtil.java | 81 +++++++++++++--- .../ganarchy/friendcode/sam/I2PSamControl.java | 14 +-- .../friendcode/sam/I2PSamStateMachine.java | 22 ++++- .../java/ganarchy/friendcode/util/ConfigUtil.java | 106 +++++++++++++++++++++ .../java/ganarchy/friendcode/util/KeyUtil.java | 8 ++ 6 files changed, 211 insertions(+), 25 deletions(-) create mode 100644 src/main/java/ganarchy/friendcode/util/ConfigUtil.java (limited to 'src/main/java/ganarchy') 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. - * + *

* 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. - * + *

* 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; + } } -- cgit 1.4.1