forked from I2P_Developers/i2p.i2p
Compare commits
9 Commits
i2p.i2p.2.
...
sam-secure
Author | SHA1 | Date | |
---|---|---|---|
1039931b29 | |||
6368986ea5 | |||
64c64e42f4 | |||
7f021c6c30 | |||
bc5cdb5b73 | |||
e3556de4f4 | |||
22a9c181ba | |||
9a1a5bfe6b | |||
e09901ef67 |
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
49
apps/sam/java/src/net/i2p/sam/SAMSecureSession.java
Normal file
49
apps/sam/java/src/net/i2p/sam/SAMSecureSession.java
Normal 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;
|
||||
}
|
||||
}
|
27
apps/sam/java/src/net/i2p/sam/SAMSecureSessionInterface.java
Normal file
27
apps/sam/java/src/net/i2p/sam/SAMSecureSessionInterface.java
Normal 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;
|
||||
}
|
Reference in New Issue
Block a user