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