diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1819252 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# ignore output folders +.idea/ +out/ +target/ + +# ignore files +config.json +*.iml + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..227e0bf --- /dev/null +++ b/LICENSE @@ -0,0 +1,30 @@ +shadowsocks-java is distributed under the following BSD-style license: + +Copyright (c) 2015, Blake +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +3. The name of the author may not be used to endorse or promote +products derived from this software without specific prior +written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..efb7df4 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +shadowsocks-java +================ + +shadowsocks-java is a pure JAVA client for [shadowsocks](https://github.com/shadowsocks/shadowsocks) project. + +Only tested AES encryption. + +### Requirements + * Bouncy Castle v1.5.2 [Release](https://www.bouncycastle.org/) + * json-simple v1.1.1 [Release](https://code.google.com/p/json-simple/) + +### Developers + * Using Non-blocking server + Config config = new Config("SS_SERVER_IP", "SS_SERVER_PORT", "LOCAL_IP", "LOCAL_PORT", "CIPHER_NAME", "PASSWORD"); + NioLocalServer server = new NioLocalServer(config); + new Thread(server).start(); + + * Using blocking server + Config config = new Config("SS_SERVER_IP", "SS_SERVER_PORT", "LOCAL_IP", "LOCAL_PORT", "CIPHER_NAME", "PASSWORD"); + LocalServer server = new LocalServer(config); + new Thread(server).start(); \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..e437ae5 --- /dev/null +++ b/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + + com.stfl + shadowsocks-java + 0.1-SNAPSHOT + + + UTF-8 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.7 + 1.7 + + + + + + + + org.bouncycastle + bcprov-jdk15on + 1.52 + + + com.googlecode.json-simple + json-simple + 1.1.1 + + + + junit + junit + + + org.hamcrest + hamcrest-core + + + + + diff --git a/src/main/java/com/stfl/Main.java b/src/main/java/com/stfl/Main.java new file mode 100644 index 0000000..8379930 --- /dev/null +++ b/src/main/java/com/stfl/Main.java @@ -0,0 +1,125 @@ +package com.stfl; + +import com.stfl.misc.Config; +import com.stfl.misc.Util; +import com.stfl.network.LocalServer; +import com.stfl.network.NioLocalServer; +import com.stfl.ss.CryptFactory; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.logging.Logger; + +public class Main { + + public static void main(String[] args) { + Logger logger = Logger.getLogger(Main.class.getName()); + Config config; + + config = parseArgument(args); + if (config == null) { + printUsage(); + return; + } + + String s = config.saveToJson(); + PrintWriter writer = null; + try { + writer = new PrintWriter("config.json"); + writer.println(s); + writer.close(); + } catch (FileNotFoundException e) { + logger.warning("Unable to save config"); + } + + try { + //LocalServer server = new LocalServer(config); + NioLocalServer server = new NioLocalServer(config); + Thread t = new Thread(server); + t.start(); + logger.info("Shadowsocks-Java v" + Util.getVersion()); + logger.info("Server starts at port: " + config.getLocalPort()); + t.join(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private static Config parseArgument(String[] args) { + Config config = new Config(); + + if (args.length == 2) { + if (args[0].equals("--config")) { + Path path = Paths.get(args[1]); + try { + String json = new String(Files.readAllBytes(path)); + config.loadFromJson(json); + } catch (IOException e) { + System.out.println("Unable to read configuration file: " + args[1]); + return null; + } + return config; + } + else { + return null; + } + } + + if (args.length != 8) { + return null; + } + + // parse arguments + for (int i = 0; i < args.length; i+=2) { + String[] tempArgs; + if (args[i].equals("--local")) { + tempArgs = args[i+1].split(":"); + if (tempArgs.length < 2) { + System.out.println("Invalid argument: " + args[i]); + return null; + } + config.setLocalIpAddress(tempArgs[0]); + config.setLocalPort(Integer.parseInt(tempArgs[1])); + } + else if (args[i].equals("--remote")) { + tempArgs = args[i+1].split(":"); + if (tempArgs.length < 2) { + System.out.println("Invalid argument: " + args[i]); + return null; + } + config.setRemoteIpAddress(tempArgs[0]); + config.setRemotePort(Integer.parseInt(tempArgs[1])); + } + else if (args[i].equals("--cipher")) { + config.setMethod(args[i+1]); + } + else if (args[i].equals("--password")) { + config.setPassword(args[i + 1]); + } + } + + return config; + } + + private static void printUsage() { + System.out.println("Usage: ss --[option] value --[option] value..."); + System.out.println("Option:"); + System.out.println(" --local [IP:PORT]"); + System.out.println(" --remote [IP:PORT]"); + System.out.println(" --cipher [CIPHER_NAME]"); + System.out.println(" --password [PASSWORD]"); + System.out.println(" --config [CONFIG_FILE]"); + System.out.println("Support Ciphers:"); + for (String s : CryptFactory.getSupportedCiphers()) { + System.out.printf(" %s\n", s); + } + System.out.println("Example:"); + System.out.println(" ss --local \"127.0.0.1:1080\" --remote \"[SS_SERVER_IP]:8080\" --cipher \"aes-256-cfb\" --password \"HelloWorld\""); + System.out.println(" ss --config config.json"); + + } +} diff --git a/src/main/java/com/stfl/misc/Config.java b/src/main/java/com/stfl/misc/Config.java new file mode 100644 index 0000000..380eca3 --- /dev/null +++ b/src/main/java/com/stfl/misc/Config.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2015, Blake + * All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. The name of the author may not be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package com.stfl.misc; + +import org.json.simple.JSONObject; +import org.json.simple.JSONValue; +import com.stfl.ss.AesCrypt; + +/** + * Data class for configuration to bring up server + */ +public class Config { + private String _ipAddr; + private int _port; + private String _localIpAddr; + private int _localPort; + private String _method; + private String _password; + private String _logLevel; + private boolean _isSock5Server; + + public Config() { + load(); + } + + public Config(String ipAddr, int port, String localIpAddr, int localPort, String method, String password) { + this(); + _ipAddr = ipAddr; + _port = port; + _localIpAddr = localIpAddr; + _localPort = localPort; + _method = method; + _password = password; + } + + public void setRemoteIpAddress(String value) { + _ipAddr = value; + } + + public String getRemoteIpAddress() { + return _ipAddr; + } + + public void setLocalIpAddress(String value) { + _localIpAddr = value; + } + + public String getLocalIpAddress() { + return _localIpAddr; + } + + public void setRemotePort(int value) { + _port = value; + } + + public int getRemotePort() { + return _port; + } + + public void setLocalPort(int value) { + _localPort = value; + } + + public int getLocalPort() { + return _localPort; + } + + public void setSocks5Server(boolean value) { + _isSock5Server =value; + } + + public boolean isSock5Server() { + return _isSock5Server; + } + + public void setMethod(String value) { + _method = value; + } + + public String getMethod() { + return _method; + } + + public void setPassword(String value) { + _password = value; + } + + public String getPassword() { + return _password; + } + + public void setLogLevel(String value) { + _logLevel = value; + Log.init(getLogLevel()); + } + + public String getLogLevel() { + return _logLevel; + } + + public void load() { + loadFromJson("{}"); + } + + public void loadFromJson(String jsonStr) { + JSONObject jObj = (JSONObject)JSONValue.parse(jsonStr); + _ipAddr = (String)jObj.getOrDefault("remoteIpAddress", ""); + _port = ((Number)jObj.getOrDefault("remotePort", 1080)).intValue(); + _localIpAddr = (String)jObj.getOrDefault("localIpAddress", "127.0.0.1"); + _localPort = ((Number)jObj.getOrDefault("localPort", 1082)).intValue(); + _method = (String)jObj.getOrDefault("method", AesCrypt.CIPHER_AES_256_CFB); + _password = (String)jObj.getOrDefault("password", ""); + _logLevel = (String)jObj.getOrDefault("logLevel", "INFO"); + _isSock5Server = (Boolean) jObj.getOrDefault("isSocks5Server", true); + setLogLevel(_logLevel); + } + + public String saveToJson() { + JSONObject jObj = new JSONObject(); + jObj.put("remoteIpAddress", _ipAddr); + jObj.put("remotePort", _port); + jObj.put("localIpAddress", _localIpAddr); + jObj.put("localPort", _localPort); + jObj.put("method", _method); + jObj.put("password", _password); + jObj.put("isSocks5Server", _isSock5Server); + + return Util.prettyPrintJson(jObj); + } +} diff --git a/src/main/java/com/stfl/misc/Log.java b/src/main/java/com/stfl/misc/Log.java new file mode 100644 index 0000000..6cbf2ff --- /dev/null +++ b/src/main/java/com/stfl/misc/Log.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2015, Blake + * All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. The name of the author may not be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package com.stfl.misc; + +import java.util.Locale; +import java.util.Properties; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.logging.SimpleFormatter; + +/** + * Initialized level of root logger + */ +public class Log { + public static void init() { + init(Level.INFO); + } + + public static void init(Level level) { + // disable message localization + Locale.setDefault(Locale.ENGLISH); + // config log output format + Properties props = System.getProperties(); + props.setProperty("java.util.logging.SimpleFormatter.format", "%1$tY-%1$tb-%1$td %1$tT [%4$s] %5$s%n"); + // set log level and format + Logger rootLogger = Logger.getLogger(""); + rootLogger.setLevel(level); + Handler[] handlers = rootLogger.getHandlers(); + for (Handler h : handlers) { + h.setLevel(level); + h.setFormatter(new SimpleFormatter()); + } + } + + public static void init(String level) { + Level l = Level.parse(level); + init(l); + } +} diff --git a/src/main/java/com/stfl/misc/Util.java b/src/main/java/com/stfl/misc/Util.java new file mode 100644 index 0000000..b93e8c7 --- /dev/null +++ b/src/main/java/com/stfl/misc/Util.java @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2015, Blake + * All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. The name of the author may not be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package com.stfl.misc; + +import org.json.simple.JSONObject; + +import java.io.*; +import java.security.SecureRandom; + +/** + * Helper class + */ +public class Util { + + public static String getVersion() { + return "0.1"; + } + + public static String byteArrayToStr(byte[] a) { + StringBuilder sb = new StringBuilder(a.length * 2); + for(byte b: a) + sb.append(String.format("%x", b & 0xff)); + return sb.toString(); + } + + public static byte[] randomBytes(int size) { + byte[] bytes = new byte[size]; + new SecureRandom().nextBytes(bytes); + return bytes; + } + + public static String getErrorMessage(Throwable e) { + Writer writer = new StringWriter(); + PrintWriter pWriter = new PrintWriter(writer); + e.printStackTrace(pWriter); + return writer.toString(); + } + + public static String prettyPrintJson(JSONObject jObj) { + String retValue; + StringWriter writer = new StringWriter() { + private final static String indent = " "; + private final String LINE_SEP = System.getProperty("line.separator"); + private int indentLevel = 0; + + @Override + public void write(int c) { + char ch = (char) c; + if (ch == '[' || ch == '{') { + super.write(c); + super.write(LINE_SEP); + indentLevel++; + writeIndentation(); + } + else if (ch == ']' || ch == '}') { + super.write(LINE_SEP); + indentLevel--; + writeIndentation(); + super.write(c); + } + else if (ch == ':') { + super.write(c); + super.write(" "); + } + else if (ch == ',') { + super.write(c); + super.write(LINE_SEP); + writeIndentation(); + } + else { + super.write(c); + } + + } + + private void writeIndentation() + { + for (int i = 0; i < indentLevel; i++) + { + super.write(indent); + } + } + }; + + try { + jObj.writeJSONString(writer); + retValue = writer.toString(); + } catch (IOException e) { + // something wrong with writer, use the original method + retValue = jObj.toJSONString(); + } + + return retValue; + } + + public static String getRequestedHostInfo(byte[] data) { + String ret = ""; + int port; + int neededLength; + switch (data[0]) { + case 0x1: + // IP v4 Address + // 4 bytes of IP, 2 bytes of port + neededLength = 6; + if (data.length > neededLength) { + port = getPort(data[5], data[6]); + ret = String.format("%d.%d.%d.%d:%d", data[1], data[2], data[3], data[4], port); + } + break; + case 0x3: + // domain + neededLength = data[1]; + if (data.length > neededLength + 3) { + try { + port = getPort(data[neededLength + 2], data[neededLength + 3]); + String domain = new String(data, 2, neededLength, "UTF-8"); + ret = String.format("%s:%d", domain, port); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + } + break; + case 0x4: + // IP v6 Address + // 16 bytes of IP, 2 bytes of port + neededLength = 18; + if (data.length > neededLength) { + port = getPort(data[17], data[18]); + ret = String.format("%x%x:%x%x:%x%x:%x%x:%x%x:%x%x:%x%x:%x%x:%d", + data[1], data[2], data[3], data[4], data[5], data[6], data[7], data[8], + data[9], data[10], data[11], data[12], data[13], data[14], data[15], data[16], + port); + } + break; + } + + return ret; + } + + public static short byteToUnsignedByte(byte b) { + return (short)(b & 0xff); + } + + private static int getPort(byte b, byte b1) { + return byteToUnsignedByte(b) << 8 | byteToUnsignedByte(b1); + } +} diff --git a/src/main/java/com/stfl/network/LocalServer.java b/src/main/java/com/stfl/network/LocalServer.java new file mode 100644 index 0000000..add0069 --- /dev/null +++ b/src/main/java/com/stfl/network/LocalServer.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2015, Blake + * All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. The name of the author may not be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package com.stfl.network; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.security.InvalidAlgorithmParameterException; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.logging.Logger; + +import com.stfl.misc.Config; +import com.stfl.misc.Util; +import com.stfl.network.io.PipeSocket; +import com.stfl.ss.CryptFactory; + +/** + * Blocking local server for shadowsocks + */ +public class LocalServer implements Runnable { + private Config _config; + private ServerSocket _serverSocket; + private Logger logger = Logger.getLogger(LocalServer.class.getName()); + private Executor _executor; + + public LocalServer(Config config) throws IOException, InvalidAlgorithmParameterException { + if (!CryptFactory.isCipherExisted(config.getMethod())) { + throw new InvalidAlgorithmParameterException(config.getMethod()); + } + _config = config; + _serverSocket = new ServerSocket(config.getLocalPort(), 128); + _executor = Executors.newCachedThreadPool(); + } + + @Override + public void run() { + while (true) { + try { + Socket localSocket = _serverSocket.accept(); + PipeSocket pipe = new PipeSocket(_executor, localSocket, _config); + _executor.execute(pipe); + } catch (IOException e) { + logger.warning(Util.getErrorMessage(e)); + } + } + } + + public void close() { + try { + _serverSocket.close(); + } catch (IOException e) { + logger.warning(Util.getErrorMessage(e)); + } + } + +} diff --git a/src/main/java/com/stfl/network/NioLocalServer.java b/src/main/java/com/stfl/network/NioLocalServer.java new file mode 100644 index 0000000..0ea68d0 --- /dev/null +++ b/src/main/java/com/stfl/network/NioLocalServer.java @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2015, Blake + * All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. The name of the author may not be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package com.stfl.network; + +import com.stfl.misc.Config; +import com.stfl.misc.Util; +import com.stfl.network.nio.*; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.nio.channels.spi.SelectorProvider; +import java.security.InvalidAlgorithmParameterException; +import java.util.*; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.logging.Logger; + +/** + * Non-blocking local server for shadowsocks + */ +public class NioLocalServer extends SocketHandlerBase { + private Logger logger = Logger.getLogger(NioLocalServer.class.getName()); + + private ServerSocketChannel _serverChannel; + private RemoteSocketHandler _remoteSocketHandler; + private Executor _executor; + + public NioLocalServer(Config config) throws IOException, InvalidAlgorithmParameterException { + super(config); + _executor = Executors.newCachedThreadPool(); + + // init remote socket handler + _remoteSocketHandler = new RemoteSocketHandler(_config); + _executor.execute(_remoteSocketHandler); + } + + @Override + protected Selector initSelector() throws IOException { + Selector socketSelector = SelectorProvider.provider().openSelector(); + _serverChannel = ServerSocketChannel.open(); + _serverChannel.configureBlocking(false); + InetSocketAddress isa = new InetSocketAddress(_config.getLocalIpAddress(), _config.getLocalPort()); + _serverChannel.socket().bind(isa); + _serverChannel.register(socketSelector, SelectionKey.OP_ACCEPT); + + return socketSelector; + } + + @Override + protected boolean processPendingRequest(ChangeRequest request) { + switch (request.type) { + case ChangeRequest.CHANGE_SOCKET_OP: + SelectionKey key = request.socket.keyFor(_selector); + if ((key != null) && key.isValid()) { + key.interestOps(request.op); + } else { + logger.warning("NioLocalServer::processPendingRequest (drop): " + key + request.socket); + } + break; + case ChangeRequest.CLOSE_CHANNEL: + cleanUp(request.socket); + break; + } + return true; + } + + @Override + protected void processSelect(SelectionKey key) { + // Handle event + try { + if (key.isAcceptable()) { + accept(key); + } else if (key.isReadable()) { + read(key); + } else if (key.isWritable()) { + write(key); + } + } + catch (IOException e) { + cleanUp((SocketChannel)key.channel()); + } + } + + private void accept(SelectionKey key) throws IOException { + // local socket established + ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel(); + SocketChannel socketChannel = serverSocketChannel.accept(); + socketChannel.configureBlocking(false); + socketChannel.register(_selector, SelectionKey.OP_READ); + + // prepare local socket write queue + createWriteBuffer(socketChannel); + + // create pipe between local and remote socket + PipeWorker pipe = _remoteSocketHandler.createPipe(this, socketChannel, _config.getRemoteIpAddress(), _config.getRemotePort()); + _pipes.put(socketChannel, pipe); + _executor.execute(pipe); + } + + private void read(SelectionKey key) throws IOException { + SocketChannel socketChannel = (SocketChannel) key.channel(); + int readCount; + PipeWorker pipe = _pipes.get(socketChannel); + byte[] data; + + if (pipe == null) { + // should not happen + cleanUp(socketChannel); + return; + } + + _readBuffer.clear(); + try { + readCount = socketChannel.read(_readBuffer); + } catch (IOException e) { + cleanUp(socketChannel); + return; + } + + if (readCount == -1) { + cleanUp(socketChannel); + return; + } + + /* + There are two stage of establish Sock5: + 1. ACK (3 bytes) + 2. HELLO (3 bytes + dst info) + as Client sending HELLO, it might contain dst info. + In this case, server needs to send back HELLO response to client and start the remote socket right away, + otherwise, client will wait until timeout. + */ + data = _readBuffer.array(); + if (!pipe.isSock5Initialized()) { + byte[] temp = pipe.getSocks5Response(data); + send(new ChangeRequest(socketChannel, ChangeRequest.CHANGE_SOCKET_OP, SelectionKey.OP_WRITE), temp); + if (readCount > 3) { + readCount -= 3; + temp = new byte[readCount]; + System.arraycopy(data, 3, temp, 0, readCount); + data = temp; + logger.info("Connected to: " + Util.getRequestedHostInfo(data)); + } + } + + if (pipe.isSock5Initialized()) { + pipe.processData(data, readCount, true); + } + } + + private void write(SelectionKey key) throws IOException { + SocketChannel socketChannel = (SocketChannel) key.channel(); + + synchronized (_pendingData) { + List queue = (List) _pendingData.get(socketChannel); + if (queue == null) { + logger.warning("LocalSocket::write queue = null: " + socketChannel); + return; + } + + // Write data + while (!queue.isEmpty()) { + ByteBuffer buf = (ByteBuffer) queue.get(0); + socketChannel.write(buf); + if (buf.remaining() > 0) { + break; + } + queue.remove(0); + } + + if (queue.isEmpty()) { + key.interestOps(SelectionKey.OP_READ); + } + } + } + + @Override + protected void cleanUp(SocketChannel socketChannel) { + //logger.warning("LocalSocket closed: " + socketChannel); + super.cleanUp(socketChannel); + + PipeWorker pipe = _pipes.get(socketChannel); + if (pipe != null) { + pipe.close(); + _pipes.remove(socketChannel); + logger.fine("LocalSocket closed: " + pipe.socketInfo); + } + else { + logger.fine("LocalSocket closed (NULL): " + socketChannel); + } + + } + + @Override + public void close() { + super.close(); + try { + _serverChannel.close(); + _remoteSocketHandler.close(); + for (PipeWorker p : _pipes.values()) { + p.close(); + } + } catch (IOException e) { + logger.warning(Util.getErrorMessage(e)); + } + } +} diff --git a/src/main/java/com/stfl/network/Socks5.java b/src/main/java/com/stfl/network/Socks5.java new file mode 100644 index 0000000..57f68ee --- /dev/null +++ b/src/main/java/com/stfl/network/Socks5.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2015, Blake + * All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. The name of the author may not be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package com.stfl.network; + +import com.stfl.misc.Config; + +/** + * Provide local socks5 statue and required response + */ +public class Socks5 { + public enum STAGE {SOCK5_ACK, SOCKS_HELLO, SOCKS_READY} + private STAGE _stage; + + public Socks5(Config config) { + if (config.isSock5Server()) { + _stage = STAGE.SOCK5_ACK; + } + else { + _stage = STAGE.SOCKS_READY; + } + } + + public boolean isReady() { + return _stage == STAGE.SOCKS_READY; + } + + public byte[] getResponse(byte[] data) { + byte[] respData = null; + + switch (_stage) { + case SOCK5_ACK: + if (data[0] != 0x5) { + respData = new byte[] {0, 91}; + } + else { + respData = new byte[] {5, 0}; + } + _stage = STAGE.SOCKS_HELLO; + break; + case SOCKS_HELLO: + respData = new byte[] {5, 0, 0, 1, 0, 0, 0, 0, 0, 0}; + _stage = STAGE.SOCKS_READY; + break; + default: + // TODO: exception + break; + + } + + return respData; + } +} diff --git a/src/main/java/com/stfl/network/io/PipeSocket.java b/src/main/java/com/stfl/network/io/PipeSocket.java new file mode 100644 index 0000000..8b3717c --- /dev/null +++ b/src/main/java/com/stfl/network/io/PipeSocket.java @@ -0,0 +1,267 @@ +/* + * Copyright (c) 2015, Blake + * All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. The name of the author may not be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package com.stfl.network.io; + +import com.stfl.misc.Config; +import com.stfl.misc.Util; +import com.stfl.network.Socks5; +import com.stfl.ss.CryptFactory; +import com.stfl.ss.ICrypt; + +import java.io.*; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +/** + * Pipe local and remote sockets while server is running under blocking mode. + */ +public class PipeSocket implements Runnable { + private Logger logger = Logger.getLogger(PipeSocket.class.getName()); + + private final int BUFFER_SIZE = 16384; + private final int SOCK5_BUFFER_SIZE = 3; + private final int TIMEOUT = 10000; // 10s + private Socket _remote; + private Socket _local; + private Socks5 _socks5; + private ICrypt _crypt; + private boolean isClosed; + private Executor _executor; + private Config _config; + + public PipeSocket(Executor executor, Socket socket, Config config) throws IOException { + _executor = executor; + _local = socket; + _local.setSoTimeout(TIMEOUT); + _config = config; + _crypt = CryptFactory.get(_config.getMethod(), _config.getPassword()); + _socks5 = new Socks5(_config); + } + + @Override + public void run() { + try { + _remote = initRemote(_config); + _remote.setSoTimeout(TIMEOUT); + } catch (IOException e) { + close(); + logger.warning(Util.getErrorMessage(e)); + return; + } + + _executor.execute(getLocalWorker()); + _executor.execute(getRemoteWorker()); + } + + private Socket initRemote(Config config) throws IOException { + return new Socket(config.getRemoteIpAddress(), config.getRemotePort()); + } + + private Runnable getLocalWorker() { + return new Runnable() { + private boolean isFirstPacket = true; + @Override + public void run() { + InputStream reader; + byte[] sock5Buffer = new byte[SOCK5_BUFFER_SIZE]; + byte[] dataBuffer = new byte[BUFFER_SIZE]; + byte[] buffer; + int readCount; + + // prepare local stream + try { + reader = _local.getInputStream(); + } catch (IOException e) { + logger.info(e.toString()); + return; + } + + // start to process data from local socket + while (true) { + try { + if (!_socks5.isReady()) { + buffer = sock5Buffer; + } + else { + buffer = dataBuffer; + } + + // read data + readCount = reader.read(buffer); + if (readCount < 1) { + throw new IOException("Local socket closed (Read)!"); + } + + // initialize socks5 + if (!_socks5.isReady()) { + buffer = _socks5.getResponse(buffer); + if (!_sendLocal(buffer, buffer.length)) { + throw new IOException("Local socket closed (sock5Init-Write)!"); + } + continue; + } + + if (isFirstPacket) { + isFirstPacket = false; + logger.info("Connected to: " + Util.getRequestedHostInfo(buffer)); + } + + // send data to remote socket + if (!sendRemote(buffer, readCount)) { + throw new IOException("Remote socket closed (Write)!"); + } + } catch (SocketTimeoutException e) { + continue; + } catch (IOException e) { + logger.fine(Util.getErrorMessage(e)); + break; + } + } + close(); + logger.fine(String.format("localWorker exit, Local=%s, Remote=%s\n", _local, _remote)); + } + }; + } + + private Runnable getRemoteWorker() { + return new Runnable() { + @Override + public void run() { + InputStream reader; + int readCount; + byte[] buffer = new byte[BUFFER_SIZE]; + + // prepare remote stream + try { + reader = _remote.getInputStream(); + } catch (IOException e) { + logger.info(e.toString()); + return; + } + + // start to process data from remote socket + while (true) { + try { + readCount = reader.read(buffer); + + if (readCount < 1) { + throw new IOException("Remote socket closed (Read)!"); + } + + // send data to local socket + if (!sendLocal(buffer, readCount)) { + throw new IOException("Local socket closed (Write)!"); + } + } catch (SocketTimeoutException e) { + continue; + } catch (IOException e) { + logger.fine(Util.getErrorMessage(e)); + break; + } + + } + close(); + logger.fine(String.format("remoteWorker exit, Local=%s, Remote=%s\n", _local, _remote)); + } + }; + } + + private void close() { + if (isClosed) { + return; + } + isClosed = true; + + try { + _local.shutdownInput(); + _local.shutdownOutput(); + _local.close(); + } catch (IOException e) { + logger.fine("PipeSocket failed to close local socket (I/O exception)!"); + } + try { + if (_remote != null) { + _remote.shutdownInput(); + _remote.shutdownOutput(); + _remote.close(); + } + } catch (IOException e) { + logger.fine("PipeSocket failed to close remote socket (I/O exception)!"); + } + } + + private boolean sendRemote(byte[] data, int length) { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + _crypt.encrypt(data, length, stream); + byte[] sendData = stream.toByteArray(); + + return _sendRemote(sendData, sendData.length); + } + + private boolean _sendRemote(byte[] data, int length) { + try { + if (length > 0) { + OutputStream outStream = _remote.getOutputStream(); + outStream.write(data, 0, length); + } + else { + logger.info("Nothing to sendRemote!\n"); + } + } catch (IOException e) { + logger.info(Util.getErrorMessage(e)); + return false; + } + + return true; + } + + private boolean sendLocal(byte[] data, int length) { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + _crypt.decrypt(data, length, stream); + byte[] sendData = stream.toByteArray(); + + return _sendLocal(sendData, sendData.length); + } + + private boolean _sendLocal(byte[] data, int length) { + try { + OutputStream outStream = _local.getOutputStream(); + outStream.write(data, 0, length); + } catch (IOException e) { + logger.info(Util.getErrorMessage(e)); + return false; + } + return true; + } +} diff --git a/src/main/java/com/stfl/network/nio/ChangeRequest.java b/src/main/java/com/stfl/network/nio/ChangeRequest.java new file mode 100644 index 0000000..5bdc094 --- /dev/null +++ b/src/main/java/com/stfl/network/nio/ChangeRequest.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2015, Blake + * All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. The name of the author may not be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package com.stfl.network.nio; + +import java.nio.channels.SocketChannel; + +/** + * Request for nio socket handler + */ +public class ChangeRequest { + public static final int REGISTER_CHANNEL = 1; + public static final int CHANGE_SOCKET_OP = 2; + public static final int CLOSE_CHANNEL = 3; + + public SocketChannel socket; + public int type; + public int op; + + public ChangeRequest(SocketChannel socket, int type, int op) { + this.socket = socket; + this.type = type; + this.op = op; + } + + public ChangeRequest(SocketChannel socket, int type) { + this(socket, type, 0); + } +} diff --git a/src/main/java/com/stfl/network/nio/ISocketHandler.java b/src/main/java/com/stfl/network/nio/ISocketHandler.java new file mode 100644 index 0000000..3b504ba --- /dev/null +++ b/src/main/java/com/stfl/network/nio/ISocketHandler.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2015, Blake + * All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. The name of the author may not be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package com.stfl.network.nio; + +/** + * Interface of socket handler + */ +public interface ISocketHandler { + void send(ChangeRequest request, byte[] data); + void send(ChangeRequest request); +} diff --git a/src/main/java/com/stfl/network/nio/PipeEvent.java b/src/main/java/com/stfl/network/nio/PipeEvent.java new file mode 100644 index 0000000..105f595 --- /dev/null +++ b/src/main/java/com/stfl/network/nio/PipeEvent.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2015, Blake + * All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. The name of the author may not be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package com.stfl.network.nio; + +/** + * pipe event for pipe worker + */ +public class PipeEvent { + public byte[] data; + public boolean isEncrypted; + + public PipeEvent() {} + + public PipeEvent(byte[] data, boolean isEncrypted) { + this.data = data; + this.isEncrypted = isEncrypted; + } +} diff --git a/src/main/java/com/stfl/network/nio/PipeWorker.java b/src/main/java/com/stfl/network/nio/PipeWorker.java new file mode 100644 index 0000000..a12cce9 --- /dev/null +++ b/src/main/java/com/stfl/network/nio/PipeWorker.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2015, Blake + * All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. The name of the author may not be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package com.stfl.network.nio; + +import com.stfl.misc.Config; +import com.stfl.misc.Util; +import com.stfl.network.Socks5; +import com.stfl.ss.CryptFactory; +import com.stfl.ss.ICrypt; + +import java.io.ByteArrayOutputStream; +import java.nio.channels.SelectionKey; +import java.nio.channels.SocketChannel; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.logging.Logger; + + +public class PipeWorker implements Runnable { + private Logger logger = Logger.getLogger(PipeWorker.class.getName()); + private SocketChannel _localChannel; + private SocketChannel _remoteChannel; + private ISocketHandler _localSocketHandler; + private ISocketHandler _remoteSocketHandler; + private Socks5 _socks5; + private ICrypt _crypt; + public String socketInfo; + private ByteArrayOutputStream _outStream; + private BlockingQueue _processQueue; + private volatile boolean requestedClose; + + public PipeWorker(ISocketHandler localHandler, SocketChannel localChannel, ISocketHandler remoteHandler, SocketChannel remoteChannel, Config config) { + _localChannel = localChannel; + _remoteChannel = remoteChannel; + _localSocketHandler = localHandler; + _remoteSocketHandler = remoteHandler; + _crypt = CryptFactory.get(config.getMethod(), config.getPassword()); + _socks5 = new Socks5(config); + _outStream = new ByteArrayOutputStream(16384); + _processQueue = new LinkedBlockingQueue(); + requestedClose = false; + socketInfo = String.format("Local: %s, Remote: %s", localChannel, remoteChannel); + } + + public void close() { + requestedClose = true; + processData(null, 0, false); + } + + public boolean isSock5Initialized() { + return _socks5.isReady(); + } + + public byte[] getSocks5Response(byte[] data) { + return _socks5.getResponse(data); + } + + public void processData(byte[] data, int count, boolean isEncrypted) { + if (data != null) { + byte[] dataCopy = new byte[count]; + System.arraycopy(data, 0, dataCopy, 0, count); + _processQueue.add(new PipeEvent(dataCopy, isEncrypted)); + } + else { + _processQueue.add(new PipeEvent()); + } + } + + @Override + public void run() { + PipeEvent event; + ISocketHandler socketHandler; + SocketChannel channel; + + while(true) { + // make sure all the requests in the queue are processed + if (_processQueue.isEmpty() && requestedClose) { + logger.fine("PipeWorker closed: " + this.socketInfo); + if (_localChannel.isOpen()) { + _localSocketHandler.send(new ChangeRequest(_localChannel, ChangeRequest.CLOSE_CHANNEL)); + } + if (_remoteChannel.isOpen()) { + _remoteSocketHandler.send(new ChangeRequest(_remoteChannel, ChangeRequest.CLOSE_CHANNEL)); + } + break; + } + + try { + event = (PipeEvent)_processQueue.take(); + + // check is other thread is requested to close sockets + if (event.data == null) { + continue; + } + + // clear stream for new data + _outStream.reset(); + + if (event.isEncrypted) { + _crypt.encrypt(event.data, _outStream); + channel = _remoteChannel; + socketHandler = _remoteSocketHandler; + } + else { + _crypt.decrypt(event.data, _outStream); + channel = _localChannel; + socketHandler = _localSocketHandler; + } + + // data is ready to send to socket + ChangeRequest request = new ChangeRequest(channel, ChangeRequest.CHANGE_SOCKET_OP, SelectionKey.OP_WRITE); + socketHandler.send(request, _outStream.toByteArray()); + } catch (InterruptedException e) { + logger.fine(Util.getErrorMessage(e)); + break; + } + } + } +} diff --git a/src/main/java/com/stfl/network/nio/RemoteSocketHandler.java b/src/main/java/com/stfl/network/nio/RemoteSocketHandler.java new file mode 100644 index 0000000..5a16e9e --- /dev/null +++ b/src/main/java/com/stfl/network/nio/RemoteSocketHandler.java @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2015, Blake + * All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. The name of the author may not be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package com.stfl.network.nio; + +import com.stfl.misc.Config; +import com.stfl.misc.Util; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; +import java.nio.channels.spi.SelectorProvider; +import java.security.InvalidAlgorithmParameterException; +import java.util.List; +import java.util.logging.Logger; + +/** + * Handler for processing all IO event for remote sockets + */ +public class RemoteSocketHandler extends SocketHandlerBase { + private Logger logger = Logger.getLogger(RemoteSocketHandler.class.getName()); + + public RemoteSocketHandler(Config config) throws IOException, InvalidAlgorithmParameterException { + super(config); + } + + @Override + protected Selector initSelector() throws IOException { + return SelectorProvider.provider().openSelector(); + } + + @Override + protected boolean processPendingRequest(ChangeRequest request) { + if ((request.type != ChangeRequest.REGISTER_CHANNEL) && request.socket.isConnectionPending()) { + return false; + } + + SelectionKey key; + switch (request.type) { + case ChangeRequest.CHANGE_SOCKET_OP: + key = request.socket.keyFor(_selector); + if ((key != null) && key.isValid()) { + key.interestOps(request.op); + } else { + logger.warning("RemoteSocketHandler::processPendingRequest (drop): " + key + request.socket); + } + break; + case ChangeRequest.REGISTER_CHANNEL: + try { + request.socket.register(_selector, request.op); + } catch (ClosedChannelException e) { + // socket get closed by remote + logger.warning(e.toString()); + cleanUp(request.socket); + } + break; + case ChangeRequest.CLOSE_CHANNEL: + cleanUp(request.socket); + break; + } + + return true; + } + + @Override + protected void processSelect(SelectionKey key) { + try { + if (key.isConnectable()) { + finishConnection(key); + } else if (key.isReadable()) { + read(key); + } else if (key.isWritable()) { + write(key); + } + } catch (IOException e) { + cleanUp((SocketChannel) key.channel()); + } + } + + public PipeWorker createPipe(ISocketHandler localHandler, SocketChannel localChannel, String ipAddress, int port) throws IOException { + // prepare remote socket + SocketChannel socketChannel = SocketChannel.open(); + socketChannel.configureBlocking(false); + socketChannel.connect(new InetSocketAddress(ipAddress, port)); + + // create write buffer for specified socket + createWriteBuffer(socketChannel); + + // create pipe worker for handling encrypt and decrypt + PipeWorker pipe = new PipeWorker(localHandler, localChannel, this, socketChannel, _config); + + // setup pipe info + //pipe.setRemoteChannel(socketChannel); + _pipes.put(socketChannel, pipe); + + synchronized(_pendingRequest) { + _pendingRequest.add(new ChangeRequest(socketChannel, ChangeRequest.REGISTER_CHANNEL, SelectionKey.OP_CONNECT)); + } + + return pipe; + } + + private void read(SelectionKey key) throws IOException { + SocketChannel socketChannel = (SocketChannel) key.channel(); + PipeWorker pipe = _pipes.get(socketChannel); + if (pipe == null) { + // should not happen + cleanUp(socketChannel); + return; + } + + // clear read buffer for new data + _readBuffer.clear(); + + // read data + int readCount; + try { + readCount = socketChannel.read(_readBuffer); + } catch (IOException e) { + // remote socket closed + cleanUp(socketChannel); + + return; + } + + if (readCount == -1) { + cleanUp(socketChannel); + return; + } + + // Handle the response + pipe.processData(_readBuffer.array(), readCount, false); + } + + private void write(SelectionKey key) throws IOException { + SocketChannel socketChannel = (SocketChannel) key.channel(); + + synchronized (_pendingData) { + List queue = (List) _pendingData.get(socketChannel); + if (queue == null) { + logger.warning("RemoteSocket::write queue = null: " + socketChannel); + return; + } + + // write data to socket + while (!queue.isEmpty()) { + ByteBuffer buf = (ByteBuffer) queue.get(0); + socketChannel.write(buf); + if (buf.remaining() > 0) { + break; + } + queue.remove(0); + } + + if (queue.isEmpty()) { + key.interestOps(SelectionKey.OP_READ); + } + } + } + + private void finishConnection(SelectionKey key) throws IOException { + SocketChannel socketChannel = (SocketChannel) key.channel(); + + try { + socketChannel.finishConnect(); + } catch (IOException e) { + logger.warning("RemoteSocketHandler::finishConnection I/O exception: " + e.toString()); + cleanUp(socketChannel); + return; + } + + key.interestOps(SelectionKey.OP_WRITE); + } + + @Override + protected void cleanUp(SocketChannel socketChannel) { + super.cleanUp(socketChannel); + + PipeWorker pipe = _pipes.get(socketChannel); + if (pipe != null) { + pipe.close(); + _pipes.remove(socketChannel); + logger.fine("RemoteSocket closed: " + pipe.socketInfo); + } + else { + logger.fine("RemoteSocket closed (NULL): " + socketChannel); + } + } +} diff --git a/src/main/java/com/stfl/network/nio/SocketHandlerBase.java b/src/main/java/com/stfl/network/nio/SocketHandlerBase.java new file mode 100644 index 0000000..ecc0968 --- /dev/null +++ b/src/main/java/com/stfl/network/nio/SocketHandlerBase.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2015, Blake + * All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. The name of the author may not be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package com.stfl.network.nio; + +import com.stfl.misc.Config; +import com.stfl.misc.Util; +import com.stfl.ss.CryptFactory; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; +import java.security.InvalidAlgorithmParameterException; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.logging.Logger; + +/** + * Base class of socket handler for processing all IO event for sockets + */ +public abstract class SocketHandlerBase implements Runnable, ISocketHandler { + private Logger logger = Logger.getLogger(SocketHandlerBase.class.getName()); + protected Selector _selector; + protected Config _config; + protected List _pendingRequest = new LinkedList(); + protected Map _pendingData = new HashMap(); + protected ConcurrentMap _pipes = new ConcurrentHashMap<>(); + protected ByteBuffer _readBuffer = ByteBuffer.allocate(16384); + + protected abstract Selector initSelector() throws IOException; + protected abstract boolean processPendingRequest(ChangeRequest request); + protected abstract void processSelect(SelectionKey key); + + + public SocketHandlerBase(Config config) throws IOException, InvalidAlgorithmParameterException { + if (!CryptFactory.isCipherExisted(config.getMethod())) { + throw new InvalidAlgorithmParameterException(config.getMethod()); + } + _config = config; + _selector = initSelector(); + } + + @Override + public void run() { + while (true) { + try { + synchronized (_pendingRequest) { + Iterator changes = _pendingRequest.iterator(); + while (changes.hasNext()) { + ChangeRequest change = (ChangeRequest) changes.next(); + if (!processPendingRequest(change)) + break; + changes.remove(); + } + } + + // wait events from selected channels + _selector.select(); + + Iterator selectedKeys = _selector.selectedKeys().iterator(); + while (selectedKeys.hasNext()) { + SelectionKey key = (SelectionKey) selectedKeys.next(); + selectedKeys.remove(); + + if (!key.isValid()) { + continue; + } + + processSelect(key); + } + } + catch (Exception e) { + logger.warning(Util.getErrorMessage(e)); + } + } + } + + protected void createWriteBuffer(SocketChannel socketChannel) { + synchronized (_pendingData) { + if (!_pendingData.containsKey(socketChannel)) { + List queue = new ArrayList(); + _pendingData.put(socketChannel, queue); + } + } + } + + protected void cleanUp(SocketChannel socketChannel) { + + try { + socketChannel.close(); + } catch (IOException e) { + logger.info(Util.getErrorMessage(e)); + } + SelectionKey key = socketChannel.keyFor(_selector); + if (key != null) { + key.cancel(); + } + + if (_pendingData.containsKey(socketChannel)) { + _pendingData.remove(socketChannel); + } + } + + @Override + public void send(ChangeRequest request, byte[] data) { + synchronized (_pendingRequest) { + _pendingRequest.add(request); + + switch (request.type) { + case ChangeRequest.CHANGE_SOCKET_OP: + synchronized (_pendingData) { + List queue = (List) _pendingData.get(request.socket); + + // in general case, the write queue is always existed, unless, the socket has been shutdown + if (queue != null) { + queue.add(ByteBuffer.wrap(data)); + } + else { + logger.warning(Util.getErrorMessage(new Throwable("Socket is closed! dropping this request"))); + } + } + break; + } + } + + _selector.wakeup(); + } + + @Override + public void send(ChangeRequest request) { + send(request, null); + } + + public void close() { + try { + _selector.close(); + + } catch (IOException e) { + logger.warning(Util.getErrorMessage(e)); + } + + } +} diff --git a/src/main/java/com/stfl/ss/AesCrypt.java b/src/main/java/com/stfl/ss/AesCrypt.java new file mode 100644 index 0000000..2923a66 --- /dev/null +++ b/src/main/java/com/stfl/ss/AesCrypt.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2015, Blake + * All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. The name of the author may not be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package com.stfl.ss; + +import org.bouncycastle.crypto.StreamBlockCipher; +import org.bouncycastle.crypto.engines.AESEngine; +import org.bouncycastle.crypto.engines.AESFastEngine; +import org.bouncycastle.crypto.modes.CFBBlockCipher; +import org.bouncycastle.crypto.modes.OFBBlockCipher; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.io.ByteArrayOutputStream; +import java.security.InvalidAlgorithmParameterException; +import java.util.HashMap; +import java.util.Map; + +/** + * AES Crypt implementation + */ +public class AesCrypt extends CryptBase { + + public final static String CIPHER_AES_128_CFB = "aes-128-cfb"; + public final static String CIPHER_AES_192_CFB = "aes-192-cfb"; + public final static String CIPHER_AES_256_CFB = "aes-256-cfb"; + public final static String CIPHER_AES_128_OFB = "aes-128-ofb"; + public final static String CIPHER_AES_192_OFB = "aes-192-ofb"; + public final static String CIPHER_AES_256_OFB = "aes-256-ofb"; + + public static Map getCiphers() { + Map ciphers = new HashMap<>(); + ciphers.put(CIPHER_AES_128_CFB, AesCrypt.class.getName()); + ciphers.put(CIPHER_AES_192_CFB, AesCrypt.class.getName()); + ciphers.put(CIPHER_AES_256_CFB, AesCrypt.class.getName()); + ciphers.put(CIPHER_AES_128_OFB, AesCrypt.class.getName()); + ciphers.put(CIPHER_AES_192_OFB, AesCrypt.class.getName()); + ciphers.put(CIPHER_AES_256_OFB, AesCrypt.class.getName()); + + return ciphers; + } + + public AesCrypt(String name, String password) { + super(name, password); + } + + @Override + public int getKeyLength() { + if(_name.equals(CIPHER_AES_128_CFB) || _name.equals(CIPHER_AES_128_OFB)) { + return 16; + } + else if (_name.equals(CIPHER_AES_192_CFB) || _name.equals(CIPHER_AES_192_OFB)) { + return 24; + } + else if (_name.equals(CIPHER_AES_256_CFB) || _name.equals(CIPHER_AES_256_OFB)) { + return 32; + } + + return 0; + } + + @Override + protected StreamBlockCipher getCipher(boolean isEncrypted) throws InvalidAlgorithmParameterException { + AESFastEngine engine = new AESFastEngine(); + StreamBlockCipher cipher; + + if (_name.equals(CIPHER_AES_128_CFB)) { + cipher = new CFBBlockCipher(engine, getIVLength() * 8); + } + else if (_name.equals(CIPHER_AES_192_CFB)) { + cipher = new CFBBlockCipher(engine, getIVLength() * 8); + } + else if (_name.equals(CIPHER_AES_256_CFB)) { + cipher = new CFBBlockCipher(engine, getIVLength() * 8); + } + else if (_name.equals(CIPHER_AES_128_OFB)) { + cipher = new OFBBlockCipher(engine, getIVLength() * 8); + } + else if (_name.equals(CIPHER_AES_192_OFB)) { + cipher = new OFBBlockCipher(engine, getIVLength() * 8); + } + else if (_name.equals(CIPHER_AES_256_OFB)) { + cipher = new OFBBlockCipher(engine, getIVLength() * 8); + } + else { + throw new InvalidAlgorithmParameterException(_name); + } + + return cipher; + } + + @Override + public int getIVLength() { + return 16; + } + + @Override + protected SecretKey getKey() { + return new SecretKeySpec(_ssKey.getEncoded(), "AES"); + } + + @Override + protected void _encrypt(byte[] data, ByteArrayOutputStream stream) { + int noBytesProcessed; + byte[] buffer = new byte[data.length]; + + noBytesProcessed = encCipher.processBytes(data, 0, data.length, buffer, 0); + stream.write(buffer, 0, noBytesProcessed); + } + + @Override + protected void _decrypt(byte[] data, ByteArrayOutputStream stream) { + int noBytesProcessed; + byte[] buffer = new byte[data.length]; + + noBytesProcessed = decCipher.processBytes(data, 0, data.length, buffer, 0); + stream.write(buffer, 0, noBytesProcessed); + } +} diff --git a/src/main/java/com/stfl/ss/BlowFishCrypt.java b/src/main/java/com/stfl/ss/BlowFishCrypt.java new file mode 100644 index 0000000..85f9a47 --- /dev/null +++ b/src/main/java/com/stfl/ss/BlowFishCrypt.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2015, Blake + * All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. The name of the author may not be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package com.stfl.ss; + +import org.bouncycastle.crypto.StreamBlockCipher; +import org.bouncycastle.crypto.engines.BlowfishEngine; +import org.bouncycastle.crypto.modes.CFBBlockCipher; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.io.ByteArrayOutputStream; +import java.security.InvalidAlgorithmParameterException; +import java.util.HashMap; +import java.util.Map; + +/** + * Blow fish cipher implementation + */ +public class BlowFishCrypt extends CryptBase { + + public final static String CIPHER_BLOWFISH_CFB = "bf-cfb"; + + public static Map getCiphers() { + Map ciphers = new HashMap<>(); + ciphers.put(CIPHER_BLOWFISH_CFB, BlowFishCrypt.class.getName()); + + return ciphers; + } + + public BlowFishCrypt(String name, String password) { + super(name, password); + } + + @Override + public int getKeyLength() { + return 16; + } + + @Override + protected StreamBlockCipher getCipher(boolean isEncrypted) throws InvalidAlgorithmParameterException { + BlowfishEngine engine = new BlowfishEngine(); + StreamBlockCipher cipher; + + if (_name.equals(CIPHER_BLOWFISH_CFB)) { + cipher = new CFBBlockCipher(engine, getIVLength() * 8); + } + else { + throw new InvalidAlgorithmParameterException(_name); + } + + return cipher; + } + + @Override + public int getIVLength() { + return 8; + } + + @Override + protected SecretKey getKey() { + return new SecretKeySpec(_ssKey.getEncoded(), "AES"); + } + + @Override + protected void _encrypt(byte[] data, ByteArrayOutputStream stream) { + int noBytesProcessed; + byte[] buffer = new byte[data.length]; + + noBytesProcessed = encCipher.processBytes(data, 0, data.length, buffer, 0); + stream.write(buffer, 0, noBytesProcessed); + } + + @Override + protected void _decrypt(byte[] data, ByteArrayOutputStream stream) { + int noBytesProcessed; + byte[] buffer = new byte[data.length]; + + noBytesProcessed = decCipher.processBytes(data, 0, data.length, buffer, 0); + stream.write(buffer, 0, noBytesProcessed); + } +} diff --git a/src/main/java/com/stfl/ss/CamelliaCrypt.java b/src/main/java/com/stfl/ss/CamelliaCrypt.java new file mode 100644 index 0000000..b2faac1 --- /dev/null +++ b/src/main/java/com/stfl/ss/CamelliaCrypt.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2015, Blake + * All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. The name of the author may not be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package com.stfl.ss; + +import org.bouncycastle.crypto.StreamBlockCipher; +import org.bouncycastle.crypto.engines.CamelliaEngine; +import org.bouncycastle.crypto.modes.CFBBlockCipher; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.io.ByteArrayOutputStream; +import java.security.InvalidAlgorithmParameterException; +import java.util.HashMap; +import java.util.Map; + +/** + * Camellia cipher implementation + */ +public class CamelliaCrypt extends CryptBase { + + public final static String CIPHER_CAMELLIA_128_CFB = "camellia-128-cfb"; + public final static String CIPHER_CAMELLIA_192_CFB = "camellia-192-cfb"; + public final static String CIPHER_CAMELLIA_256_CFB = "camellia-256-cfb"; + + public static Map getCiphers() { + Map ciphers = new HashMap<>(); + ciphers.put(CIPHER_CAMELLIA_128_CFB, CamelliaCrypt.class.getName()); + ciphers.put(CIPHER_CAMELLIA_192_CFB, CamelliaCrypt.class.getName()); + ciphers.put(CIPHER_CAMELLIA_256_CFB, CamelliaCrypt.class.getName()); + + return ciphers; + } + + public CamelliaCrypt(String name, String password) { + super(name, password); + } + + @Override + public int getKeyLength() { + if(_name.equals(CIPHER_CAMELLIA_128_CFB)) { + return 16; + } + else if (_name.equals(CIPHER_CAMELLIA_192_CFB)) { + return 24; + } + else if (_name.equals(CIPHER_CAMELLIA_256_CFB)) { + return 32; + } + + return 0; + } + + @Override + protected StreamBlockCipher getCipher(boolean isEncrypted) throws InvalidAlgorithmParameterException { + CamelliaEngine engine = new CamelliaEngine(); + StreamBlockCipher cipher; + + if (_name.equals(CIPHER_CAMELLIA_128_CFB)) { + cipher = new CFBBlockCipher(engine, getIVLength() * 8); + } + else if (_name.equals(CIPHER_CAMELLIA_192_CFB)) { + cipher = new CFBBlockCipher(engine, getIVLength() * 8); + } + else if (_name.equals(CIPHER_CAMELLIA_256_CFB)) { + cipher = new CFBBlockCipher(engine, getIVLength() * 8); + } + else { + throw new InvalidAlgorithmParameterException(_name); + } + + return cipher; + } + + @Override + public int getIVLength() { + return 16; + } + + @Override + protected SecretKey getKey() { + return new SecretKeySpec(_ssKey.getEncoded(), "AES"); + } + + @Override + protected void _encrypt(byte[] data, ByteArrayOutputStream stream) { + int noBytesProcessed; + byte[] buffer = new byte[data.length]; + + noBytesProcessed = encCipher.processBytes(data, 0, data.length, buffer, 0); + stream.write(buffer, 0, noBytesProcessed); + } + + @Override + protected void _decrypt(byte[] data, ByteArrayOutputStream stream) { + int noBytesProcessed; + byte[] buffer = new byte[data.length]; + + noBytesProcessed = decCipher.processBytes(data, 0, data.length, buffer, 0); + stream.write(buffer, 0, noBytesProcessed); + } +} diff --git a/src/main/java/com/stfl/ss/CryptBase.java b/src/main/java/com/stfl/ss/CryptBase.java new file mode 100644 index 0000000..9b5c8f0 --- /dev/null +++ b/src/main/java/com/stfl/ss/CryptBase.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2015, Blake + * All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. The name of the author may not be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package com.stfl.ss; + +import com.stfl.misc.Util; +import org.bouncycastle.crypto.StreamBlockCipher; +import org.bouncycastle.crypto.params.KeyParameter; +import org.bouncycastle.crypto.params.ParametersWithIV; + +import javax.crypto.SecretKey; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.util.Map; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.logging.Logger; + +/** + * Crypt base class implementation + */ +public abstract class CryptBase implements ICrypt { + + protected abstract StreamBlockCipher getCipher(boolean isEncrypted) throws InvalidAlgorithmParameterException; + protected abstract SecretKey getKey(); + protected abstract void _encrypt(byte[] data, ByteArrayOutputStream stream); + protected abstract void _decrypt(byte[] data, ByteArrayOutputStream stream); + + protected final String _name; + protected final SecretKey _key; + protected final ShadowSocksKey _ssKey; + protected final int _ivLength; + protected final int _keyLength; + protected boolean _encryptIVSet; + protected boolean _decryptIVSet; + protected byte[] _encryptIV; + protected byte[] _decryptIV; + protected final Lock encLock = new ReentrantLock(); + protected final Lock decLock = new ReentrantLock(); + protected StreamBlockCipher encCipher; + protected StreamBlockCipher decCipher; + private Logger logger = Logger.getLogger(CryptBase.class.getName()); + + public CryptBase(String name, String password) { + _name = name.toLowerCase(); + _ivLength = getIVLength(); + _keyLength = getKeyLength(); + _ssKey = new ShadowSocksKey(password, _keyLength); + _key = getKey(); + } + + protected void setIV(byte[] iv, boolean isEncrypt) + { + if (_ivLength == 0) { + return; + } + + if (isEncrypt) + { + _encryptIV = new byte[_ivLength]; + System.arraycopy(iv, 0, _encryptIV, 0, _ivLength); + try { + encCipher = getCipher(isEncrypt); + ParametersWithIV parameterIV = new ParametersWithIV(new KeyParameter(_key.getEncoded()), _encryptIV); + encCipher.init(isEncrypt, parameterIV); + } catch (InvalidAlgorithmParameterException e) { + logger.info(e.toString()); + } + } + else + { + _decryptIV = new byte[_ivLength]; + System.arraycopy(iv, 0, _decryptIV, 0, _ivLength); + try { + decCipher = getCipher(isEncrypt); + ParametersWithIV parameterIV = new ParametersWithIV(new KeyParameter(_key.getEncoded()), _decryptIV); + decCipher.init(isEncrypt, parameterIV); + } catch (InvalidAlgorithmParameterException e) { + logger.info(e.toString()); + } + } + } + + @Override + public void encrypt(byte[] data, ByteArrayOutputStream stream) { + synchronized (encLock) { + stream.reset(); + if (!_encryptIVSet) { + _encryptIVSet = true; + byte[] iv = Util.randomBytes(_ivLength); + setIV(iv, true); + try { + stream.write(iv); + } catch (IOException e) { + logger.info(e.toString()); + } + + } + + _encrypt(data, stream); + } + } + + @Override + public void encrypt(byte[] data, int length, ByteArrayOutputStream stream) { + byte[] d = new byte[length]; + System.arraycopy(data, 0, d, 0, length); + encrypt(d, stream); + } + + @Override + public void decrypt(byte[] data, ByteArrayOutputStream stream) { + byte[] temp; + + synchronized (decLock) { + stream.reset(); + if (!_decryptIVSet) { + _decryptIVSet = true; + setIV(data, false); + temp = new byte[data.length - _ivLength]; + System.arraycopy(data, _ivLength, temp, 0, data.length - _ivLength); + } else { + temp = data; + } + + _decrypt(temp, stream); + } + } + + @Override + public void decrypt(byte[] data, int length, ByteArrayOutputStream stream) { + byte[] d = new byte[length]; + System.arraycopy(data, 0, d, 0, length); + decrypt(d, stream); + } +} diff --git a/src/main/java/com/stfl/ss/CryptFactory.java b/src/main/java/com/stfl/ss/CryptFactory.java new file mode 100644 index 0000000..0cd3edb --- /dev/null +++ b/src/main/java/com/stfl/ss/CryptFactory.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2015, Blake + * All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. The name of the author may not be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package com.stfl.ss; + +import java.lang.reflect.Constructor; +import java.util.*; +import java.util.logging.Logger; + +/** + * Crypt factory + */ +public class CryptFactory { + private static final Map crypts = new HashMap() {{ + putAll(AesCrypt.getCiphers()); + putAll(CamelliaCrypt.getCiphers()); + putAll(BlowFishCrypt.getCiphers()); + putAll(SeedCrypt.getCiphers()); + // TODO: other crypts + }}; + private static Logger logger = Logger.getLogger(CryptFactory.class.getName()); + + public static boolean isCipherExisted(String name) { + return (crypts.get(name) != null); + } + public static ICrypt get(String name, String password) { + + try { + String clsName = crypts.get(name); + Class c = Class.forName(clsName); + Class[] oParam = new Class[2]; + oParam[0] = String.class; + oParam[1] = String.class; + + Constructor constructor = c.getConstructor(oParam); + + Object[] paramObjs = new Object[2]; + paramObjs[0] = name; + paramObjs[1] = password; + Object obj = constructor.newInstance(paramObjs); + return (ICrypt)obj; + + } catch (Exception e) { + logger.info(com.stfl.misc.Util.getErrorMessage(e)); + } + + return null; + } + + public static List getSupportedCiphers() { + List sortedKeys=new ArrayList(crypts.keySet()); + Collections.sort(sortedKeys); + return sortedKeys; + } + +} diff --git a/src/main/java/com/stfl/ss/ICrypt.java b/src/main/java/com/stfl/ss/ICrypt.java new file mode 100644 index 0000000..56b4ecf --- /dev/null +++ b/src/main/java/com/stfl/ss/ICrypt.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2015, Blake + * All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. The name of the author may not be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package com.stfl.ss; + +import java.io.ByteArrayOutputStream; +import java.util.Map; + +/** + * Interface of crypt + */ +public interface ICrypt { + void encrypt(byte[] data, ByteArrayOutputStream stream); + void encrypt(byte[] data, int length, ByteArrayOutputStream stream); + void decrypt(byte[] data, ByteArrayOutputStream stream); + void decrypt(byte[] data, int length, ByteArrayOutputStream stream); + int getIVLength(); + int getKeyLength(); +} diff --git a/src/main/java/com/stfl/ss/SeedCrypt.java b/src/main/java/com/stfl/ss/SeedCrypt.java new file mode 100644 index 0000000..b150d59 --- /dev/null +++ b/src/main/java/com/stfl/ss/SeedCrypt.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2015, Blake + * All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. The name of the author may not be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package com.stfl.ss; + +import org.bouncycastle.crypto.StreamBlockCipher; +import org.bouncycastle.crypto.engines.SEEDEngine; +import org.bouncycastle.crypto.modes.CFBBlockCipher; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.io.ByteArrayOutputStream; +import java.security.InvalidAlgorithmParameterException; +import java.util.HashMap; +import java.util.Map; + +/** + * Seed cipher implementation + */ +public class SeedCrypt extends CryptBase { + + public final static String CIPHER_SEED_CFB = "seed-cfb"; + + public static Map getCiphers() { + Map ciphers = new HashMap<>(); + ciphers.put(CIPHER_SEED_CFB, SeedCrypt.class.getName()); + + return ciphers; + } + + public SeedCrypt(String name, String password) { + super(name, password); + } + + @Override + public int getKeyLength() { + return 16; + } + + @Override + protected StreamBlockCipher getCipher(boolean isEncrypted) throws InvalidAlgorithmParameterException { + SEEDEngine engine = new SEEDEngine(); + StreamBlockCipher cipher; + + if (_name.equals(CIPHER_SEED_CFB)) { + cipher = new CFBBlockCipher(engine, getIVLength() * 8); + } + else { + throw new InvalidAlgorithmParameterException(_name); + } + + return cipher; + } + + @Override + public int getIVLength() { + return 16; + } + + @Override + protected SecretKey getKey() { + return new SecretKeySpec(_ssKey.getEncoded(), "AES"); + } + + @Override + protected void _encrypt(byte[] data, ByteArrayOutputStream stream) { + int noBytesProcessed; + byte[] buffer = new byte[data.length]; + + noBytesProcessed = encCipher.processBytes(data, 0, data.length, buffer, 0); + stream.write(buffer, 0, noBytesProcessed); + } + + @Override + protected void _decrypt(byte[] data, ByteArrayOutputStream stream) { + int noBytesProcessed; + byte[] buffer = new byte[data.length]; + + noBytesProcessed = decCipher.processBytes(data, 0, data.length, buffer, 0); + stream.write(buffer, 0, noBytesProcessed); + } +} diff --git a/src/main/java/com/stfl/ss/ShadowSocksKey.java b/src/main/java/com/stfl/ss/ShadowSocksKey.java new file mode 100644 index 0000000..08d4bb1 --- /dev/null +++ b/src/main/java/com/stfl/ss/ShadowSocksKey.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2015, Blake + * All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. The name of the author may not be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package com.stfl.ss; + +import com.stfl.misc.Util; + +import javax.crypto.SecretKey; +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.util.logging.Logger; + +/** + * Shadowsocks key generator + */ +public class ShadowSocksKey implements SecretKey { + + private Logger logger = Logger.getLogger(ShadowSocksKey.class.getName()); + private final static int KEY_LENGTH = 32; + private byte[] _key; + private int _length; + + public ShadowSocksKey(String password) { + _length = KEY_LENGTH; + _key = init(password); + } + + public ShadowSocksKey(String password, int length) { + // TODO: Invalid key length + _length = length; + _key = init(password); + } + + private byte[] init(String password) { + MessageDigest md = null; + byte[] keys = new byte[KEY_LENGTH]; + byte[] temp = null; + byte[] hash = null; + byte[] passwordBytes = null; + int i = 0; + + try { + md = MessageDigest.getInstance("MD5"); + passwordBytes = password.getBytes("UTF-8"); + } + catch (UnsupportedEncodingException e) { + logger.info("ShadowSocksKey: Unsupported string encoding"); + } + catch (Exception e) { + logger.info(Util.getErrorMessage(e)); + return null; + } + + while (i < keys.length) { + if (i == 0) { + hash = md.digest(passwordBytes); + temp = new byte[passwordBytes.length+hash.length]; + } + else { + System.arraycopy(hash, 0, temp, 0, hash.length); + System.arraycopy(passwordBytes, 0, temp, hash.length, passwordBytes.length); + hash = md.digest(temp); + } + System.arraycopy(hash, 0, keys, i, hash.length); + i += hash.length; + } + + if (_length != KEY_LENGTH) { + byte[] keysl = new byte[_length]; + System.arraycopy(keys, 0, keysl, 0, _length); + return keysl; + } + return keys; + } + + @Override + public String getAlgorithm() { + return "shadowsocks"; + } + + @Override + public String getFormat() { + return "RAW"; + } + + @Override + public byte[] getEncoded() { + return _key; + } +} diff --git a/src/main/resources/META-INF/MANIFEST.MF b/src/main/resources/META-INF/MANIFEST.MF new file mode 100644 index 0000000..5a00b74 --- /dev/null +++ b/src/main/resources/META-INF/MANIFEST.MF @@ -0,0 +1,4 @@ +Manifest-Version: 1.0 +Class-Path: bcprov-jdk15on-1.52.jar json-simple-1.1.1.jar +Main-Class: com.stfl.Main +