From ad810de7474469be41de61759155809b005de604 Mon Sep 17 00:00:00 2001
From: zzz <zzz@mail.i2p>
Date: Mon, 13 Mar 2017 13:48:36 +0000
Subject: [PATCH] i2ptunnel: Add subsession support to servers, no UI yet
 Update subsession javadocs

---
 .../i2p/i2ptunnel/I2PTunnelClientBase.java    |   3 +-
 .../net/i2p/i2ptunnel/I2PTunnelServer.java    |  62 ++++++++
 .../net/i2p/i2ptunnel/TunnelController.java   | 137 +++++++++++++++++-
 .../client/streaming/I2PSocketManager.java    |   4 +
 .../streaming/impl/I2PSocketManagerFull.java  |   4 +
 core/java/src/net/i2p/client/I2PSession.java  |   6 +-
 .../net/i2p/client/impl/I2PSessionImpl.java   |   4 +
 .../src/net/i2p/client/impl/SubSession.java   |   3 +
 8 files changed, 216 insertions(+), 7 deletions(-)

diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java
index a3c9f85660..fa9d267367 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java
@@ -313,8 +313,9 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna
     }
 
     /**
-     *  Add a subsession to a shared client if necessary.
+     *  Add a DSA_SHA1 subsession to the shared client if necessary.
      *
+     *  @return subsession, or null if none was added
      *  @since 0.9.20
      */
     protected static synchronized I2PSession addSubsession(I2PTunnel tunnel) {
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelServer.java
index 46a030dd4e..4d49a2516d 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelServer.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelServer.java
@@ -18,6 +18,7 @@ import java.net.SocketException;
 import java.net.SocketTimeoutException;
 import java.net.UnknownHostException;
 import java.security.GeneralSecurityException;
+import java.util.List;
 import java.util.Map;
 import java.util.Properties;
 import java.util.concurrent.ConcurrentHashMap;
@@ -29,12 +30,14 @@ import java.util.concurrent.TimeUnit;
 import java.util.concurrent.ThreadFactory;
 
 import net.i2p.I2PException;
+import net.i2p.client.I2PClient;
 import net.i2p.client.I2PSession;
 import net.i2p.client.I2PSessionException;
 import net.i2p.client.streaming.I2PServerSocket;
 import net.i2p.client.streaming.I2PSocket;
 import net.i2p.client.streaming.I2PSocketManager;
 import net.i2p.client.streaming.I2PSocketManagerFactory;
+import net.i2p.crypto.SigType;
 import net.i2p.data.Base64;
 import net.i2p.data.Hash;
 import net.i2p.util.EventDispatcher;
@@ -67,6 +70,8 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable {
     private static final boolean DEFAULT_USE_POOL = true;
     public static final String PROP_USE_SSL = "useSSL";
     public static final String PROP_UNIQUE_LOCAL = "enableUniqueLocal";
+    /** @since 0.9.30 */
+    public static final String PROP_ALT_PKF = "altPrivKeyFile";
     /** apparently unused */
     protected static volatile long __serverId = 0;
     /** max number of threads  - this many slowlorisses will DOS this server, but too high could OOM the JVM */
@@ -217,6 +222,9 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable {
                                                                                     portNum, props);
             rv.setName("I2PTunnel Server");
             getTunnel().addSession(rv.getSession());
+            String alt = props.getProperty(PROP_ALT_PKF);
+            if (alt != null)
+                addSubsession(rv, alt);
             return rv;
         } catch (I2PSessionException ise) {
             throw new IllegalArgumentException("Can't create socket manager", ise);
@@ -225,6 +233,44 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable {
         }
     }
 
+    /**
+     *  Add a non-DSA_SHA1 subsession to the DSA_SHA1 server if necessary.
+     *
+     *  @return subsession, or null if none was added
+     *  @since 0.9.30
+     */
+    private I2PSession addSubsession(I2PSocketManager sMgr, String alt) {
+        File altFile = TunnelController.filenameToFile(alt);
+        if (alt == null)
+            return null;
+        I2PSession sess = sMgr.getSession();
+        if (sess.getMyDestination().getSigType() != SigType.DSA_SHA1)
+            return null;
+        Properties props = new Properties();
+        props.putAll(getTunnel().getClientOptions());
+        // fixme get actual sig type
+        String name = props.getProperty("inbound.nickname");
+        if (name != null)
+            props.setProperty("inbound.nickname", name + " (EdDSA)");
+        name = props.getProperty("outbound.nickname");
+        if (name != null)
+            props.setProperty("outbound.nickname", name + " (EdDSA)");
+        props.setProperty(I2PClient.PROP_SIGTYPE, "EdDSA_SHA512_Ed25519");
+        FileInputStream privData = null;
+        try {
+            privData = new FileInputStream(altFile);
+            return sMgr.addSubsession(privData, props);
+        } catch (IOException ioe) {
+            _log.error("Failed to add subssession", ioe);
+            return null;
+        } catch (I2PSessionException ise) {
+            _log.error("Failed to add subssession", ise);
+            return null;
+        } finally {
+            if (privData != null) try { privData.close(); } catch (IOException ioe) {}
+        }
+    }
+
 
     /**
      * Warning, blocks while connecting to router and building tunnels;
@@ -238,6 +284,22 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable {
         while (sockMgr.getSession().isClosed()) {
             try {
                 sockMgr.getSession().connect();
+                // Now connect the subsessions, if any
+                List<I2PSession> subs = sockMgr.getSubsessions();
+                if (!subs.isEmpty()) {
+                    for (I2PSession sub : subs) {
+                        try {
+                            sub.connect();
+                            if (_log.shouldInfo())
+                                _log.info("Connected subsession " + sub);
+                        } catch (I2PSessionException ise) {
+                            // not fatal?
+                            String msg = "Unable to connect subsession " + sub;
+                            this.l.log(msg);
+                            _log.error(msg, ise);
+                        }
+                    }
+                }
             } catch (I2PSessionException ise) {
                 // try to make this error sensible as it will happen...
                 String portNum = getTunnel().port;
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java
index 9079efb01c..2f1619d5c9 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java
@@ -3,6 +3,7 @@ package net.i2p.i2ptunnel;
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.security.GeneralSecurityException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashSet;
@@ -16,12 +17,22 @@ import net.i2p.I2PException;
 import net.i2p.client.I2PClient;
 import net.i2p.client.I2PClientFactory;
 import net.i2p.client.I2PSession;
+import net.i2p.client.I2PSessionException;
+import net.i2p.crypto.KeyGenerator;
 import net.i2p.crypto.SigType;
 import net.i2p.data.Destination;
+import net.i2p.data.KeyCertificate;
+import net.i2p.data.PrivateKey;
+import net.i2p.data.PrivateKeyFile;
+import net.i2p.data.PublicKey;
+import net.i2p.data.SigningPrivateKey;
+import net.i2p.data.SigningPublicKey;
+import net.i2p.data.SimpleDataStructure;
 import net.i2p.i2ptunnel.socks.I2PSOCKSTunnel;
 import net.i2p.util.FileUtil;
 import net.i2p.util.I2PAppThread;
 import net.i2p.util.Log;
+import net.i2p.util.RandomSource;
 import net.i2p.util.SecureFile;
 import net.i2p.util.SecureFileOutputStream;
 import net.i2p.util.SystemVersion;
@@ -83,6 +94,8 @@ public class TunnelController implements Logging {
     private static final String OPT_TAGS_SEND = PFX_OPTION + "crypto.tagsToSend";
     private static final String OPT_LOW_TAGS = PFX_OPTION + "crypto.lowTagThreshold";
     private static final String OPT_SIG_TYPE = PFX_OPTION + I2PClient.PROP_SIGTYPE;
+    /** @since 0.9.30 */
+    private static final String OPT_ALT_PKF = PFX_OPTION + I2PTunnelServer.PROP_ALT_PKF;
 
     /** all of these @since 0.9.14 */
     public static final String TYPE_CONNECT = "connectclient";
@@ -106,7 +119,7 @@ public class TunnelController implements Logging {
      */
     public static final SigType PREFERRED_SIGTYPE;
     static {
-        if (SystemVersion.isARM() || SystemVersion.isGNU() || SystemVersion.isAndroid()) {
+        if (SystemVersion.isGNU() || SystemVersion.isAndroid()) {
             if (SigType.ECDSA_SHA256_P256.isAvailable())
                 PREFERRED_SIGTYPE = SigType.ECDSA_SHA256_P256;
             else
@@ -146,8 +159,13 @@ public class TunnelController implements Logging {
         setConfig(config, prefix);
         _messages = new ArrayList<String>(4);
         boolean keyOK = true;
-        if (createKey && (getType().endsWith("server") || getPersistentClientKey()))
+        if (createKey && (getType().endsWith("server") || getPersistentClientKey())) {
             keyOK = createPrivateKey();
+            if (keyOK && getType().endsWith("server") && !getType().equals(TYPE_STREAMR_SERVER)) {
+                // check rv?
+                createAltPrivateKey();
+            }
+        }
         _state = keyOK && getStartOnLoad() ? TunnelState.START_ON_LOAD : TunnelState.STOPPED;
     }
     
@@ -186,7 +204,7 @@ public class TunnelController implements Logging {
             String destStr = dest.toBase64();
             log("Private key created and saved in " + keyFile.getAbsolutePath());
             log("You should backup this file in a secure place.");
-            log("New destination: " + destStr);
+            log("New alternate destination: " + destStr);
             String b32 = dest.toBase32();
             log("Base32: " + b32);
             File backupDir = new SecureFile(I2PAppContext.getGlobalContext().getConfigDir(), KEY_BACKUP_DIR);
@@ -214,6 +232,99 @@ public class TunnelController implements Logging {
         return true;
     }
     
+    /**
+     * Creates alternate Destination with the same encryption keys as the primary Destination,
+     * but a different signing key.
+     *
+     * Must have already called createPrivateKey() successfully.
+     * Does nothing unless option OPT_ALT_PKF is set with the privkey file name.
+     * Does nothing if the file already exists.
+     *
+     * @return success
+     * @since 0.9.30
+     */
+    private boolean createAltPrivateKey() {
+        if (PREFERRED_SIGTYPE == SigType.DSA_SHA1)
+            return false;
+        File keyFile = getPrivateKeyFile();
+        if (keyFile == null)
+            return false;
+        if (!keyFile.exists())
+            return false;
+        File altFile = getAlternatePrivateKeyFile();
+        if (altFile == null)
+            return false;
+        if (altFile.exists())
+            return true;
+        PrivateKeyFile pkf = new PrivateKeyFile(keyFile);
+        FileOutputStream out = null;
+        try {
+            Destination dest = pkf.getDestination();
+            if (dest == null)
+                return false;
+            if (dest.getSigType() != SigType.DSA_SHA1)
+                return false;
+            PublicKey pub = dest.getPublicKey();
+            PrivateKey priv = pkf.getPrivKey();
+            SimpleDataStructure[] signingKeys = KeyGenerator.getInstance().generateSigningKeys(PREFERRED_SIGTYPE);
+            SigningPublicKey signingPubKey = (SigningPublicKey) signingKeys[0];
+            SigningPrivateKey signingPrivKey = (SigningPrivateKey) signingKeys[1];
+            KeyCertificate cert = new KeyCertificate(signingPubKey);
+            Destination d = new Destination();
+            d.setPublicKey(pub);
+            d.setSigningPublicKey(signingPubKey);
+            d.setCertificate(cert);
+            int len = signingPubKey.length();
+            if (len < 128) {
+                byte[] pad = new byte[128 - len];
+                RandomSource.getInstance().nextBytes(pad);
+                d.setPadding(pad);
+            } else if (len > 128) {
+                // copy of excess data handled in KeyCertificate constructor
+            }
+        
+            out = new SecureFileOutputStream(altFile);
+            d.writeBytes(out);
+            priv.writeBytes(out);
+            signingPrivKey.writeBytes(out);
+            try { out.close(); } catch (IOException ioe) {}
+
+            String destStr = d.toBase64();
+            log("Alternate private key created and saved in " + altFile.getAbsolutePath());
+            log("You should backup this file in a secure place.");
+            log("New destination: " + destStr);
+            String b32 = d.toBase32();
+            log("Base32: " + b32);
+            File backupDir = new SecureFile(I2PAppContext.getGlobalContext().getConfigDir(), KEY_BACKUP_DIR);
+            if (backupDir.isDirectory() || backupDir.mkdir()) {
+                String name = b32 + '-' + I2PAppContext.getGlobalContext().clock().now() + ".dat";
+                File backup = new File(backupDir, name);
+                if (FileUtil.copy(altFile, backup, false, true)) {
+                    SecureFileOutputStream.setPerms(backup);
+                    log("Alternate private key backup saved to " + backup.getAbsolutePath());
+                }
+            }
+            return true;
+        } catch (GeneralSecurityException e) {
+            log("Error creating keys " + e);
+            return false;
+        } catch (I2PSessionException e) {
+            log("Error creating keys " + e);
+            return false;
+        } catch (I2PException e) {
+            log("Error creating keys " + e);
+            return false;
+        } catch (IOException e) {
+            log("Error creating keys " + e);
+            return false;
+        } catch (RuntimeException e) {
+            log("Error creating keys " + e);
+            return false;
+        } finally {
+            if (out != null) try { out.close(); } catch (IOException ioe) {}
+        }
+    }
+    
     public void startTunnelBackground() {
         synchronized (this) {
             if (_state != TunnelState.STOPPED && _state != TunnelState.START_ON_LOAD)
@@ -797,7 +908,25 @@ public class TunnelController implements Logging {
      *  @since 0.9.17
      */
     public File getPrivateKeyFile() {
-        String f = getPrivKeyFile();
+        return filenameToFile(getPrivKeyFile());
+    }
+
+    /**
+     *  Does not necessarily exist.
+     *  @return absolute path or null if unset
+     *  @since 0.9.30
+     */
+    public File getAlternatePrivateKeyFile() {
+        return filenameToFile(_config.getProperty(OPT_ALT_PKF));
+    }
+
+    /**
+     *  Does not necessarily exist.
+     *  @param f relative or absolute path, may be null
+     *  @return absolute path or null
+     *  @since 0.9.30
+     */
+    static File filenameToFile(String f) {
         if (f == null)
             return null;
         f = f.trim();
diff --git a/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketManager.java b/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketManager.java
index ab183a8172..e8638d4a37 100644
--- a/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketManager.java
+++ b/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketManager.java
@@ -38,6 +38,10 @@ public interface I2PSocketManager {
     public I2PSession getSession();
     
     /**
+     *  For a server, you must call connect() on the returned object.
+     *  Connecting the primary session does NOT connect any subsessions.
+     *  If the primary session is not connected, connecting a subsession will connect the primary session first.
+     *
      *  @return a new subsession, non-null
      *  @param privateKeyStream null for transient, if non-null must have same encryption keys as primary session
      *                          and different signing keys
diff --git a/apps/streaming/java/src/net/i2p/client/streaming/impl/I2PSocketManagerFull.java b/apps/streaming/java/src/net/i2p/client/streaming/impl/I2PSocketManagerFull.java
index 94a4c4ae64..ee4c4ecc7a 100644
--- a/apps/streaming/java/src/net/i2p/client/streaming/impl/I2PSocketManagerFull.java
+++ b/apps/streaming/java/src/net/i2p/client/streaming/impl/I2PSocketManagerFull.java
@@ -236,6 +236,10 @@ public class I2PSocketManagerFull implements I2PSocketManager {
     }
     
     /**
+     *  For a server, you must call connect() on the returned object.
+     *  Connecting the primary session does NOT connect any subsessions.
+     *  If the primary session is not connected, connecting a subsession will connect the primary session first.
+     *
      *  @return a new subsession, non-null
      *  @param privateKeyStream null for transient, if non-null must have same encryption keys as primary session
      *                          and different signing keys
diff --git a/core/java/src/net/i2p/client/I2PSession.java b/core/java/src/net/i2p/client/I2PSession.java
index 069907d9cf..d15c04aecd 100644
--- a/core/java/src/net/i2p/client/I2PSession.java
+++ b/core/java/src/net/i2p/client/I2PSession.java
@@ -272,8 +272,10 @@ public interface I2PSession {
     public List<I2PSession> getSubsessions();
 
     /**
-     * Actually connect the session and start receiving/sending messages
-     *
+     * Actually connect the session and start receiving/sending messages.
+     * Connecting a primary session will not automatically connect subsessions.
+     * Connecting a subsession will automatically connect the primary session
+     * if not previously connected.
      */
     public void connect() throws I2PSessionException;
 
diff --git a/core/java/src/net/i2p/client/impl/I2PSessionImpl.java b/core/java/src/net/i2p/client/impl/I2PSessionImpl.java
index 617a8512c9..c5f7495ccc 100644
--- a/core/java/src/net/i2p/client/impl/I2PSessionImpl.java
+++ b/core/java/src/net/i2p/client/impl/I2PSessionImpl.java
@@ -548,6 +548,10 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2
      * Disconnect / destroy from another thread may be called simultaneously and
      * will (should?) interrupt the connect.
      *
+     * Connecting a primary session will not automatically connect subsessions.
+     * Connecting a subsession will automatically connect the primary session
+     * if not previously connected.
+     *
      * @throws I2PSessionException if there is a configuration error or the router is
      *                             not reachable
      */
diff --git a/core/java/src/net/i2p/client/impl/SubSession.java b/core/java/src/net/i2p/client/impl/SubSession.java
index 93a115bff2..6a6706d628 100644
--- a/core/java/src/net/i2p/client/impl/SubSession.java
+++ b/core/java/src/net/i2p/client/impl/SubSession.java
@@ -93,6 +93,9 @@ class SubSession extends I2PSessionMuxedImpl {
      * Disconnect / destroy from another thread may be called simultaneously and
      * will (should?) interrupt the connect.
      *
+     * Connecting a subsession will automatically connect the primary session
+     * if not previously connected.
+     *
      * @throws I2PSessionException if there is a configuration error or the router is
      *                             not reachable
      */
-- 
GitLab