From 354df6d333ffb7b69e92117406c8ce8d61ea09e0 Mon Sep 17 00:00:00 2001 From: SoniEx2 Date: Sun, 24 Jul 2022 21:42:55 -0300 Subject: Prepare for improved SAM auth and World Codes --- .../java/ganarchy/friendcode/client/CodeType.java | 16 ++++ .../friendcode/client/FriendCodeScreen.java | 51 +++++++++-- .../ganarchy/friendcode/client/I2PSamPinger.java | 6 +- .../ganarchy/friendcode/client/SamProxyThread.java | 2 +- .../ganarchy/friendcode/sam/I2PSamAuthUtil.java | 32 +++++++ .../ganarchy/friendcode/sam/I2PSamCommand.java | 3 +- .../ganarchy/friendcode/sam/I2PSamControl.java | 102 ++++++++++++++++----- .../friendcode/sam/I2PSamStateMachine.java | 21 ++--- .../java/ganarchy/friendcode/util/KeyUtil.java | 7 ++ 9 files changed, 191 insertions(+), 49 deletions(-) create mode 100644 src/main/java/ganarchy/friendcode/client/CodeType.java create mode 100644 src/main/java/ganarchy/friendcode/sam/I2PSamAuthUtil.java create mode 100644 src/main/java/ganarchy/friendcode/util/KeyUtil.java (limited to 'src/main/java') 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 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 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 { +} -- cgit 1.4.1