summary refs log blame commit diff stats
path: root/src/main/java/ganarchy/friendcode/sam/I2PSamControl.java
blob: 50748e6f91e8d7c56c068100f70eb5aadcb75504 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12











                                                       

                                 
          






                                                                                

                      


                                        
                                                         


                                                                                          
                               
                                     







                                                                                 
                                                                                                                                                                         



                                                      
                                                                                                                                        














                                               


                                                   

                                                                             




                                                                
















                                                                                                                   
                                                      
















                                                                                                                       












                                                                                                           






                                  
      







                                                                               
      






                                                                              







                                      
       

                                







                                                  

                                                                               
                                                                        


















                                                                               

                                           
                                                                             
           






                                                                        






































                                                                                                     
package ganarchy.friendcode.sam;

import com.google.common.collect.ImmutableMap;

import java.io.IOException;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetSocketAddress;
import java.net.Socket;

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;
    /**
     * The session's public key.
     */
    private String publicKey;
    private String id;

    /**
     * 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
    public boolean connect() {
        try {
            // 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);
            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);
                return this.connect(samSocket);
            } catch (IOException ex) {
                return false;
            }
        }
    }

    @Override
    public boolean start() {
        if (!super.start()) {
            return false;
        }
        try {
            // try to enable auth
            this.enableAuth();
            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;
            do {
                i++;
                // we want a session with the given privkey and zero hops for maximum performance
                // this isn't anonymous but then it's not meant to be - we're using I2P as a free STUN/TURN service
                // the connecting clients handle the need for TURN by setting their hop count to 1 as needed
                this.id = "minecraft_friendcode_" + i;
                final ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
                this.sendCommand(new I2PSamCommand(
                    "SESSION", "CREATE",
                    builder.put(
                        "STYLE", "STREAM"
                    ).put(
                        "ID", this.id
                    ).put(
                        "DESTINATION", this.privateKey
                    ).put(
                        "inbound.length", this.zeroHop ? "0" : "1"
                    ).put(
                        "outbound.length", this.zeroHop ? "0" : "1"
                    ).put(
                        "inbound.allowZeroHop", "true"
                    ).put(
                        "outbound.allowZeroHop", "true"
                    ).put(
                        "shouldBundleReplyInfo", "true"
                    ).put(
                        "i2cp.dontPublishLeaseSet", "false"
                    ).put(
                        "streaming.maxWindowSize", "1024"
                    ).build()
                ));
            } while ("DUPLICATED_ID".equals(status = this.getCommand("SESSION", "STATUS").parameters().get("RESULT")));
            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;
        }
    }

    /**
     * Creates a stream forwarder.
     *
     * @return A stream forwarder.
     */
    public I2PSamStreamForwarder forwardStream(String port) {
        return new I2PSamStreamForwarder(this.getSamBridgeAddress(), id, port);
    }

    /**
     * Creates a stream forwarder.
     *
     * @return A stream connector.
     */
    public I2PSamStreamConnector connectStream(String b32) {
        return new I2PSamStreamConnector(this.getSamBridgeAddress(), id, b32);
    }

    /**
     * Returns the session public key.
     */
    public String publicKey() {
        return this.publicKey;
    }

    /**
     * Returs the session private key.
     */
    public String privateKey() {
        return this.privateKey;
    }

    /**
     * Set up and enable auth.
     *
     * @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
        // (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.
        // 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", auth.user(), "PASSWORD", auth.password())
        ));
        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");
        }
    }

    public NameStatus checkName(String target) {
        try {
            this.sendCommand(new I2PSamCommand("NAMING", "LOOKUP", ImmutableMap.of("NAME", target)));
            I2PSamCommand result = this.getCommand("NAMING", "REPLY");
            if (result.parameters().get("RESULT") == null) {
                return NameStatus.FAILED;
            }
            switch (result.parameters().get("RESULT")) {
                case "OK" -> {
                    return NameStatus.OK;
                }
                case "INVALID_KEY" -> {
                    return NameStatus.INVALID;
                }
                case "KEY_NOT_FOUND" -> {
                    return NameStatus.UNKNOWN;
                }
                default -> {
                    return NameStatus.FAILED;
                }
            }
        } catch (IOException e) {
            return NameStatus.FAILED;
        }
    }

    public enum NameStatus {
        OK,
        INVALID,
        UNKNOWN,
        FAILED;
    }
}