Compare commits

...

9 Commits

4 changed files with 360 additions and 197 deletions

View File

@ -63,6 +63,13 @@ public class SAMBridge implements Runnable, ClientApp {
private volatile Thread _runner;
private final Object _v3DGServerLock = new Object();
private SAMv3DatagramServer _v3DGServer;
/**
* Pluggable "Secure Session Manager" for interactive, GUI-based session
* confirmation. This will block the SAM Handler Factory at the HELLO phase.
* during the createSAMHandler call. If it's null, then no interactive session
* will be used and SAM will work without it.
*/
private final SAMSecureSessionInterface _secureSession;
/**
* filename in which the name to private key mapping should
@ -101,11 +108,11 @@ public class SAMBridge implements Runnable, ClientApp {
protected static final int DEFAULT_DATAGRAM_PORT_INT = 7655;
protected static final String DEFAULT_DATAGRAM_PORT = Integer.toString(DEFAULT_DATAGRAM_PORT_INT);
/**
* For ClientApp interface.
* Recommended constructor for external use.
* Does NOT open the listener socket or start threads; caller must call startup()
* Does NOT open the listener socket or start threads; caller must call
* startup()
*
* @param mgr may be null
* @param args non-null
@ -115,6 +122,7 @@ public class SAMBridge implements Runnable, ClientApp {
public SAMBridge(I2PAppContext context, ClientAppManager mgr, String[] args) throws Exception {
_log = context.logManager().getLog(SAMBridge.class);
_mgr = mgr;
_secureSession = null;
Options options = getOptions(args);
_listenHost = options.host;
_listenPort = options.port;
@ -129,6 +137,30 @@ public class SAMBridge implements Runnable, ClientApp {
_state = INITIALIZED;
}
/**
* Build a new SAM bridge.
* NOT recommended for external use.
*
* Opens the listener socket but does NOT start the thread, and there's no
* way to do that externally.
* Use main(), or use the other constructor and call startup().
*
* Deprecated for external use, to be made private.
*
* @param listenHost hostname to listen for SAM connections on ("0.0.0.0" for
* all)
* @param listenPort port number to listen for SAM connections on
* @param i2cpProps set of I2CP properties for finding and communicating with
* the router
* @param persistFile location to store/load named keys to/from
* @throws RuntimeException if a server socket can't be opened
*/
public SAMBridge(String listenHost, int listenPort, boolean isSSL, Properties i2cpProps,
String persistFile, File configFile) {
this(listenHost, listenPort, isSSL, i2cpProps,
persistFile, configFile, null);
}
/**
* Build a new SAM bridge.
@ -140,19 +172,26 @@ public class SAMBridge implements Runnable, ClientApp {
*
* Deprecated for external use, to be made private.
*
* @param listenHost hostname to listen for SAM connections on ("0.0.0.0" for all)
* @param listenHost hostname to listen for SAM connections on ("0.0.0.0" for
* all)
* @param listenPort port number to listen for SAM connections on
* @param i2cpProps set of I2CP properties for finding and communicating with the router
* @param i2cpProps set of I2CP properties for finding and communicating
* with the router
* @param persistFile location to store/load named keys to/from
* @param secureSession an instance of a Secure Session to use
* @throws RuntimeException if a server socket can't be opened
*
* @since 1.8.0
*/
public SAMBridge(String listenHost, int listenPort, boolean isSSL, Properties i2cpProps,
String persistFile, File configFile) {
String persistFile, File configFile, SAMSecureSessionInterface secureSession) {
_log = I2PAppContext.getGlobalContext().logManager().getLog(SAMBridge.class);
_mgr = null;
_listenHost = listenHost;
_listenPort = listenPort;
_useSSL = isSSL;
_secureSession = secureSession;
if (_useSSL && !SystemVersion.isJava7())
throw new IllegalArgumentException("SSL requires Java 7 or higher");
this.i2cpProps = i2cpProps;
@ -209,21 +248,21 @@ public class SAMBridge implements Runnable, ClientApp {
* @return null if the name does not exist, or if it is improperly formatted
*/
/****
public Destination getDestination(String name) {
synchronized (nameToPrivKeys) {
String val = nameToPrivKeys.get(name);
if (val == null) return null;
try {
Destination d = new Destination();
d.fromBase64(val);
return d;
} catch (DataFormatException dfe) {
_log.error("Error retrieving the destination from " + name, dfe);
nameToPrivKeys.remove(name);
return null;
}
}
}
* public Destination getDestination(String name) {
* synchronized (nameToPrivKeys) {
* String val = nameToPrivKeys.get(name);
* if (val == null) return null;
* try {
* Destination d = new Destination();
* d.fromBase64(val);
* return d;
* } catch (DataFormatException dfe) {
* _log.error("Error retrieving the destination from " + name, dfe);
* nameToPrivKeys.remove(name);
* return null;
* }
* }
* }
****/
/**
@ -237,7 +276,8 @@ public class SAMBridge implements Runnable, ClientApp {
public String getKeystream(String name) {
synchronized (nameToPrivKeys) {
String val = nameToPrivKeys.get(name);
if (val == null) return null;
if (val == null)
return null;
return val;
}
}
@ -308,6 +348,7 @@ public class SAMBridge implements Runnable, ClientApp {
/**
* Handlers must call on startup
*
* @since 0.9.20
*/
public void register(Handler handler) {
@ -320,6 +361,7 @@ public class SAMBridge implements Runnable, ClientApp {
/**
* Handlers must call on stop
*
* @since 0.9.20
*/
public void unregister(Handler handler) {
@ -332,6 +374,7 @@ public class SAMBridge implements Runnable, ClientApp {
/**
* Stop all the handlers.
*
* @since 0.9.20
*/
private void stopHandlers() {
@ -384,7 +427,6 @@ public class SAMBridge implements Runnable, ClientApp {
}
}
////// begin ClientApp interface, use only if using correct construtor
/**
@ -470,17 +512,27 @@ public class SAMBridge implements Runnable, ClientApp {
////// end ClientApp helpers
private static class HelpRequestedException extends Exception {static final long serialVersionUID=0x1;}
private static class HelpRequestedException extends Exception {
static final long serialVersionUID = 0x1;
}
/**
* Usage:
* <pre>SAMBridge [ keyfile [listenHost ] listenPort [ name=val ]* ]</pre>
*
* <pre>
* SAMBridge [ keyfile [listenHost ] listenPort [ name=val ]* ]
* </pre>
*
* or:
* <pre>SAMBridge [ name=val ]* </pre>
*
* <pre>
* SAMBridge [ name=val ]*
* </pre>
*
* name=val options are passed to the I2CP code to build a session,
* allowing the bridge to specify an alternate I2CP host and port, tunnel
* depth, etc.
*
* @param args [ keyfile [ listenHost ] listenPort [ name=val ]* ]
*/
public static void main(String args[]) {
@ -529,7 +581,10 @@ public class SAMBridge implements Runnable, ClientApp {
private final File configFile;
public Options(String host, int port, boolean isSSL, Properties opts, String keyFile, File configFile) {
this.host = host; this.port = port; this.opts = opts; this.keyFile = keyFile;
this.host = host;
this.port = port;
this.opts = opts;
this.keyFile = keyFile;
this.isSSL = isSSL;
this.configFile = configFile;
}
@ -537,18 +592,27 @@ public class SAMBridge implements Runnable, ClientApp {
/**
* Usage:
* <pre>SAMBridge [ keyfile [listenHost ] listenPort [ name=val ]* ]</pre>
*
* <pre>
* SAMBridge [ keyfile [listenHost ] listenPort [ name=val ]* ]
* </pre>
*
* or:
* <pre>SAMBridge [ name=val ]* </pre>
*
* <pre>
* SAMBridge [ name=val ]*
* </pre>
*
* name=val options are passed to the I2CP code to build a session,
* allowing the bridge to specify an alternate I2CP host and port, tunnel
* depth, etc.
*
* @param args [ keyfile [ listenHost ] listenPort [ name=val ]* ]
* @return non-null Options or throws Exception
* @throws HelpRequestedException on command line problems
* @throws IllegalArgumentException if specified config file does not exist
* @throws IOException if specified config file cannot be read, or on SSL keystore problems
* @throws IOException if specified config file cannot be read, or
* on SSL keystore problems
* @since 0.9.6
*/
private static Options getOptions(String args[]) throws Exception {
@ -642,7 +706,8 @@ public class SAMBridge implements Runnable, ClientApp {
if (!isSSL)
isSSL = Boolean.parseBoolean(opts.getProperty(PROP_SAM_SSL));
if (isSSL) {
// must do this before we add command line opts since we may be writing them back out
// must do this before we add command line opts since we may be writing them
// back out
boolean shouldSave = SSLUtil.verifyKeyStore(opts);
if (shouldSave)
DataHelper.storeProps(opts, file);
@ -657,6 +722,7 @@ public class SAMBridge implements Runnable, ClientApp {
/**
* Parse key=value options starting at startArgs.
*
* @param props out parameter, any options found are added
* @throws HelpRequestedException on any item not of the form key=value.
*/
@ -705,7 +771,8 @@ public class SAMBridge implements Runnable, ClientApp {
}
public void run() {
if (serverSocket == null) return;
if (serverSocket == null)
return;
changeState(RUNNING);
if (_mgr != null)
_mgr.register(this);
@ -738,7 +805,8 @@ public class SAMBridge implements Runnable, ClientApp {
_log.debug("SAM handler has not been instantiated");
try {
s.close();
} catch (IOException e) {}
} catch (IOException e) {
}
return;
}
handler.startHandling();
@ -747,9 +815,15 @@ public class SAMBridge implements Runnable, ClientApp {
_log.error("SAM error: " + e.getMessage(), e);
String reply = "HELLO REPLY RESULT=I2P_ERROR MESSAGE=\"" + e.getMessage() + "\"\n";
SAMHandler.writeString(reply, s);
try { s.close(); } catch (IOException ioe) {}
try {
s.close();
} catch (IOException ioe) {
}
} catch (Exception ee) {
try { s.close(); } catch (IOException ioe) {}
try {
s.close();
} catch (IOException ioe) {
}
_log.log(Log.CRIT, "Unexpected error handling SAM connection", ee);
} finally {
parent.unregister(this);
@ -758,7 +832,10 @@ public class SAMBridge implements Runnable, ClientApp {
/** @since 0.9.20 */
public void stopHandling() {
try { s.close(); } catch (IOException ioe) {}
try {
s.close();
} catch (IOException ioe) {
}
}
}
new I2PAppThread(new HelloHandler(s, this), "SAM HelloHandler").start();
@ -776,8 +853,10 @@ public class SAMBridge implements Runnable, ClientApp {
_log.debug("Shutting down, closing server socket");
if (serverSocket != null)
serverSocket.close();
} catch (IOException e) {}
I2PAppContext.getGlobalContext().portMapper().unregister(_useSSL ? PortMapper.SVC_SAM_SSL : PortMapper.SVC_SAM);
} catch (IOException e) {
}
I2PAppContext.getGlobalContext().portMapper()
.unregister(_useSSL ? PortMapper.SVC_SAM_SSL : PortMapper.SVC_SAM);
stopHandlers();
changeState(STOPPED);
}
@ -787,4 +866,26 @@ public class SAMBridge implements Runnable, ClientApp {
public void saveConfig() throws IOException {
DataHelper.storeProps(i2cpProps, _configFile);
}
/*
* Returns the interactive Secure Session manager which requires SAM
* applications to seek "approval" for their initial connections from the user
* before they can start the session.
*
* @since 1.8.0
*/
public SAMSecureSessionInterface secureSession() {
if (_secureSession == null) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("SAMBridge.secureSession() called when secureSession is null, creating default I2CP auth");
boolean attemptauth = Boolean.parseBoolean(i2cpProps.getProperty(SAMBridge.PROP_AUTH));
if (attemptauth) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("SAMBridge.secureSession() called when authentication is enabled");
SAMSecureSessionInterface secureSession = new SAMSecureSession();
return secureSession;
}
}
return _secureSession;
}
}

View File

@ -15,9 +15,7 @@ import java.nio.channels.SocketChannel;
import java.util.Properties;
import net.i2p.I2PAppContext;
import net.i2p.data.DataHelper;
import net.i2p.util.Log;
import net.i2p.util.PasswordManager;
import net.i2p.util.VersionComparator;
/**
@ -35,13 +33,16 @@ class SAMHandlerFactory {
*
* @param s Socket attached to SAM client
* @param i2cpProps config options for our i2cp connection
* @throws SAMException if the connection handshake (HELLO message) was malformed
* @return A SAM protocol handler, or null if the client closed before the handshake
* @throws SAMException if the connection handshake (HELLO message) was
* malformed
* @return A SAM protocol handler, or null if the client closed before the
* handshake
*/
public static SAMHandler createSAMHandler(SocketChannel s, Properties i2cpProps,
SAMBridge parent) throws SAMException {
String line;
Log log = I2PAppContext.getGlobalContext().logManager().getLog(SAMHandlerFactory.class);
SAMSecureSessionInterface secureSession = parent.secureSession();
try {
Socket sock = s.socket();
@ -88,25 +89,10 @@ class SAMHandlerFactory {
return null;
}
if (Boolean.parseBoolean(i2cpProps.getProperty(SAMBridge.PROP_AUTH))) {
String user = props.getProperty("USER");
String pw = props.getProperty("PASSWORD");
if (user == null || pw == null) {
if (user == null)
log.logAlways(Log.WARN, "SAM authentication failed");
else
log.logAlways(Log.WARN, "SAM authentication failed, user: " + user);
throw new SAMException("USER and PASSWORD required");
}
String savedPW = i2cpProps.getProperty(SAMBridge.PROP_PW_PREFIX + user + SAMBridge.PROP_PW_SUFFIX);
if (savedPW == null) {
log.logAlways(Log.WARN, "SAM authentication failed, user: " + user);
throw new SAMException("Authorization failed");
}
PasswordManager pm = new PasswordManager(I2PAppContext.getGlobalContext());
if (!pm.checkHash(savedPW, pw)) {
log.logAlways(Log.WARN, "SAM authentication failed, user: " + user);
throw new SAMException("Authorization failed");
if (secureSession != null) {
boolean approval = secureSession.approveOrDenySecureSession(i2cpProps, props);
if (!approval) {
throw new SAMException("SAM connection cancelled by user request");
}
}

View File

@ -0,0 +1,49 @@
package net.i2p.sam;
import java.util.Properties;
import net.i2p.I2PAppContext;
import net.i2p.util.Log;
import net.i2p.util.PasswordManager;
/**
*
* This is the "default" implementation of the SAMSecureSession @interface
* that behaves exactly like SAM without interactive authentication. It uses
* the i2cp username and password properties for authentication. Implementers
* can add their own means of authentication by substituting this interface
* for their own.
*
* @since 1.8.0
*/
public class SAMSecureSession implements SAMSecureSessionInterface {
private final Log log = I2PAppContext.getGlobalContext().logManager().getLog(SAMHandlerFactory.class);
/**
* Authenticate based on the i2cp username/password.
*
* @since 1.8.0
*/
public boolean approveOrDenySecureSession(Properties i2cpProps, Properties props) throws SAMException {
String user = props.getProperty("USER");
String pw = props.getProperty("PASSWORD");
if (user == null || pw == null) {
if (user == null)
log.logAlways(Log.WARN, "SAM authentication failed");
else
log.logAlways(Log.WARN, "SAM authentication failed, user: " + user);
throw new SAMException("USER and PASSWORD required");
}
String savedPW = i2cpProps.getProperty(SAMBridge.PROP_PW_PREFIX + user + SAMBridge.PROP_PW_SUFFIX);
if (savedPW == null) {
log.logAlways(Log.WARN, "SAM authentication failed, user: " + user);
throw new SAMException("Authorization failed");
}
PasswordManager pm = new PasswordManager(I2PAppContext.getGlobalContext());
if (!pm.checkHash(savedPW, pw)) {
log.logAlways(Log.WARN, "SAM authentication failed, user: " + user);
throw new SAMException("Authorization failed");
}
return true;
}
}

View File

@ -0,0 +1,27 @@
package net.i2p.sam;
import java.util.Properties;
/**
* SAMSecureSessionInterface is used for implementing interactive authentication
* to SAM applications. It needs to be implemented by a class for Desktop and
* Android applications and passed to the SAM bridge when constructed.
*
* It is NOT required that a SAM API have this feature. It is recommended that
* it be implemented for platforms which have a very hostile malware landscape
* like Android.
*
* @since 1.8.0
*/
public interface SAMSecureSessionInterface {
/**
* Within this function, read and accept input from a user to approve a SAM
* connection. Return false by default
*
* if the connection is approved by user input:
*
* @since 1.8.0
* @return true
*/
public boolean approveOrDenySecureSession(Properties i2cpProps, Properties props) throws SAMException;
}