From 2ca7f2ccad949b8fe0ffe97ac1cf05e171e939fb Mon Sep 17 00:00:00 2001 From: Stefan Rossbach Date: Mon, 27 Jul 2020 23:05:38 +0200 Subject: [PATCH] [INTERNAL][CORE] Adds Socks5 proxy connection encryption This patch will now encrypt non mediated Socks 5 connection because the SMACK API do not offer it. The encryption is currently done with RSA and RC4 although RC4 should be replaced. This patch includes some randomization that makes sniffing RC4 sessions harder. --- .../saros/net/stream/SecureByteStream.java | 239 ++++++++++++++++++ .../saros/net/stream/Socks5StreamService.java | 8 +- core/test/junit/saros/SarosCoreTestSuite.java | 1 + .../net/stream/SecureByteStreamTest.java | 122 +++++++++ .../junit/saros/net/stream/TestSuite.java | 11 + 5 files changed, 377 insertions(+), 4 deletions(-) create mode 100644 core/src/saros/net/stream/SecureByteStream.java create mode 100644 core/test/junit/saros/net/stream/SecureByteStreamTest.java create mode 100644 core/test/junit/saros/net/stream/TestSuite.java diff --git a/core/src/saros/net/stream/SecureByteStream.java b/core/src/saros/net/stream/SecureByteStream.java new file mode 100644 index 0000000000..f764329065 --- /dev/null +++ b/core/src/saros/net/stream/SecureByteStream.java @@ -0,0 +1,239 @@ +package saros.net.stream; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.Random; +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.KeyGenerator; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +public class SecureByteStream implements ByteStream { + + private static final int READ_TIMEOUT = 60 * 1000; + + private static final int RSA_KEY_SIZE = 1024; // bits + private static final int MAX_BUFFER_LENGTH = 1024 * 16; // bytes + + private static final int RC4_KEY_SIZE = 128; // bits + + final CipherInputStream in; + final CipherOutputStream out; + final ByteStream delegate; + + private SecureByteStream( + final ByteStream delegate, final CipherOutputStream out, final CipherInputStream in) { + this.delegate = delegate; + this.out = out; + this.in = in; + } + + @Override + public InputStream getInputStream() throws IOException { + return in; + } + + @Override + public OutputStream getOutputStream() throws IOException { + return out; + } + + @Override + public void close() throws IOException { + delegate.close(); + } + + @Override + public int getReadTimeout() throws IOException { + return delegate.getReadTimeout(); + } + + @Override + public void setReadTimeout(final int timeout) throws IOException { + delegate.setReadTimeout(timeout); + } + + public static ByteStream wrap(final ByteStream stream, final boolean isClient) + throws IOException { + if (stream instanceof SecureByteStream) return stream; + + return isClient ? doClientHandshake(stream) : doServerHandshake(stream); + } + + private static SecureByteStream doServerHandshake(final ByteStream delegate) throws IOException { + + final int currentReadTimeout = delegate.getReadTimeout(); + + final DataOutputStream out = new DataOutputStream(delegate.getOutputStream()); + + final DataInputStream in = new DataInputStream(delegate.getInputStream()); + + try { + + delegate.setReadTimeout(READ_TIMEOUT); + + final KeyPairGenerator keyGenerator = KeyPairGenerator.getInstance("RSA"); + + keyGenerator.initialize(RSA_KEY_SIZE); + + final KeyPair keyPair = keyGenerator.generateKeyPair(); + + byte[] publicKeyData = keyPair.getPublic().getEncoded(); + + out.writeInt(publicKeyData.length); + out.write(publicKeyData); + out.flush(); + + final int rsaEncodedRC4KeyDataLength = in.readInt(); + + checkBufferSize(rsaEncodedRC4KeyDataLength, "rsaEncodedRC4KeyData"); + + final byte[] rsaEncodedRC4KeyData = new byte[rsaEncodedRC4KeyDataLength]; + + in.readFully(rsaEncodedRC4KeyData); + + final Cipher rsaCipher = Cipher.getInstance("RSA"); + + rsaCipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate()); + + final byte[] rc4KeyData = rsaCipher.doFinal(rsaEncodedRC4KeyData); + + final SecretKey rc4SecretKey = new SecretKeySpec(rc4KeyData, "RC4"); + + final Cipher rc4CipherOut = Cipher.getInstance("RC4"); + final Cipher rc4CipherIn = Cipher.getInstance("RC4"); + + rc4CipherOut.init(Cipher.ENCRYPT_MODE, rc4SecretKey); + rc4CipherIn.init(Cipher.DECRYPT_MODE, rc4SecretKey); + + final CipherOutputStream rc4CipherOutputStream = + new CipherOutputStream(delegate.getOutputStream(), rc4CipherOut); + + final CipherInputStream rc4CipherInputStream = + new CipherInputStream(delegate.getInputStream(), rc4CipherIn); + + // send garbage to avoid RC4 attacks/sniffing + + final Random random = new Random(); + + final int garbageDataSize = random.nextInt(64) + 64; + + final byte[] garbageData = new byte[garbageDataSize]; + + final DataOutputStream encryptedOutputStream = new DataOutputStream(rc4CipherOutputStream); + random.nextBytes(garbageData); + + encryptedOutputStream.writeInt(garbageDataSize); + encryptedOutputStream.write(garbageData); + encryptedOutputStream.flush(); + + return new SecureByteStream(delegate, rc4CipherOutputStream, rc4CipherInputStream); + + } catch (NoSuchAlgorithmException + | InvalidKeyException + | NoSuchPaddingException + | IllegalBlockSizeException + | BadPaddingException e) { + throw new IOException(e); + } finally { + delegate.setReadTimeout(currentReadTimeout); + } + } + + private static SecureByteStream doClientHandshake(final ByteStream delegate) throws IOException { + final int currentReadTimeout = delegate.getReadTimeout(); + + final DataOutputStream out = new DataOutputStream(delegate.getOutputStream()); + + final DataInputStream in = new DataInputStream(delegate.getInputStream()); + + try { + + delegate.setReadTimeout(READ_TIMEOUT); + + final int rsaPublicKeyDataLength = in.readInt(); + + checkBufferSize(rsaPublicKeyDataLength, "rsaPublicKeyData"); + + final byte[] rsaPublicKeyData = new byte[rsaPublicKeyDataLength]; + + in.readFully(rsaPublicKeyData); + + final PublicKey rsaPublicKey = + KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(rsaPublicKeyData)); + + final KeyGenerator keyGenerator = KeyGenerator.getInstance("RC4"); + keyGenerator.init(RC4_KEY_SIZE); + + final SecretKey rc4SecretKey = keyGenerator.generateKey(); + + byte[] rc4SecretKeyData = rc4SecretKey.getEncoded(); + + final Cipher rsaCipher = Cipher.getInstance("RSA"); + + rsaCipher.init(Cipher.ENCRYPT_MODE, rsaPublicKey); + + final byte[] encodedRC4SecretKeyData = rsaCipher.doFinal(rc4SecretKeyData); + + out.writeInt(encodedRC4SecretKeyData.length); + out.write(encodedRC4SecretKeyData); + out.flush(); + + final Cipher rc4CipherOut = Cipher.getInstance("RC4"); + final Cipher rc4CipherIn = Cipher.getInstance("RC4"); + + rc4CipherOut.init(Cipher.ENCRYPT_MODE, rc4SecretKey); + rc4CipherIn.init(Cipher.DECRYPT_MODE, rc4SecretKey); + + final CipherOutputStream rc4CipherOutputStream = + new CipherOutputStream(delegate.getOutputStream(), rc4CipherOut); + + final CipherInputStream rc4CipherInputStream = + new CipherInputStream(delegate.getInputStream(), rc4CipherIn); + + final DataInputStream decryptedInputStream = new DataInputStream(rc4CipherInputStream); + + final int garbageDataSize = decryptedInputStream.readInt(); + + checkBufferSize(garbageDataSize, "garbageData"); + + final byte[] garbageData = new byte[garbageDataSize]; + + decryptedInputStream.readFully(garbageData); + + return new SecureByteStream(delegate, rc4CipherOutputStream, rc4CipherInputStream); + + } catch (NoSuchAlgorithmException + | NoSuchPaddingException + | InvalidKeySpecException + | InvalidKeyException + | IllegalBlockSizeException + | BadPaddingException e) { + throw new IOException(e); + } finally { + delegate.setReadTimeout(currentReadTimeout); + } + } + + private static void checkBufferSize(final int size, final String bufferName) throws IOException { + if (size < 0 || size > MAX_BUFFER_LENGTH) + throw new IOException( + "invalid " + bufferName + " buffer length: 0 >= " + size + " < " + MAX_BUFFER_LENGTH); + } +} diff --git a/core/src/saros/net/stream/Socks5StreamService.java b/core/src/saros/net/stream/Socks5StreamService.java index a23ac82336..7a86a4ec4b 100644 --- a/core/src/saros/net/stream/Socks5StreamService.java +++ b/core/src/saros/net/stream/Socks5StreamService.java @@ -401,7 +401,7 @@ private IByteStreamConnection acceptNewRequest(BytestreamRequest request) localAddress, new JID(peer), connectionIdentifier, - new XMPPByteStreamAdapter(inSession), + SecureByteStream.wrap(new XMPPByteStreamAdapter(inSession), false), StreamMode.SOCKS5_DIRECT, listener); } else { @@ -430,7 +430,7 @@ private IByteStreamConnection acceptNewRequest(BytestreamRequest request) localAddress, new JID(peer), connectionIdentifier, - new XMPPByteStreamAdapter(outSession), + SecureByteStream.wrap(new XMPPByteStreamAdapter(outSession), true), StreamMode.SOCKS5_DIRECT, listener); } @@ -512,7 +512,7 @@ private IByteStreamConnection establishBinaryChannel(String connectionIdentifier localAddress, new JID(peer), connectionIdentifier, - new XMPPByteStreamAdapter(outSession), + SecureByteStream.wrap(new XMPPByteStreamAdapter(outSession), true), StreamMode.SOCKS5_DIRECT, listener); } @@ -588,7 +588,7 @@ private IByteStreamConnection establishBinaryChannel(String connectionIdentifier localAddress, new JID(peer), connectionIdentifier, - new XMPPByteStreamAdapter(inSession), + SecureByteStream.wrap(new XMPPByteStreamAdapter(inSession), false), StreamMode.SOCKS5_DIRECT, listener); } diff --git a/core/test/junit/saros/SarosCoreTestSuite.java b/core/test/junit/saros/SarosCoreTestSuite.java index 53d31a4346..7fe9434889 100644 --- a/core/test/junit/saros/SarosCoreTestSuite.java +++ b/core/test/junit/saros/SarosCoreTestSuite.java @@ -19,6 +19,7 @@ saros.negotiation.TestSuite.class, saros.net.TestSuite.class, saros.net.internal.TestSuite.class, + saros.net.stream.TestSuite.class, saros.preferences.TestSuite.class, saros.session.TestSuite.class, saros.session.internal.TestSuite.class, diff --git a/core/test/junit/saros/net/stream/SecureByteStreamTest.java b/core/test/junit/saros/net/stream/SecureByteStreamTest.java new file mode 100644 index 0000000000..f45141a5ba --- /dev/null +++ b/core/test/junit/saros/net/stream/SecureByteStreamTest.java @@ -0,0 +1,122 @@ +package saros.net.stream; + +import static org.junit.Assert.assertEquals; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.util.Arrays; +import org.junit.Test; +import saros.test.util.TestThread; + +public class SecureByteStreamTest { + + private static byte[] secretData = + new String( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis knostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.") + .getBytes(); + + private static class PipedByteStream implements ByteStream { + + private class RecordingOutputStream extends OutputStream { + + private final ByteArrayOutputStream recordedData = new ByteArrayOutputStream(); + + private boolean record = false; + + @Override + public void write(int b) throws IOException { + if (record) recordedData.write(b); + + out.write(b); + } + } + + private final PipedInputStream in = new PipedInputStream(); + private final PipedOutputStream out = new PipedOutputStream(); + + private final RecordingOutputStream recordingOut = new RecordingOutputStream(); + + public void connect(PipedByteStream other) throws IOException { + other.in.connect(out); + other.out.connect(in); + } + + @Override + public InputStream getInputStream() throws IOException { + return in; + } + + @Override + public OutputStream getOutputStream() throws IOException { + return recordingOut; + } + + @Override + public void close() throws IOException { + // NOP + } + + @Override + public int getReadTimeout() throws IOException { + return 0; + } + + @Override + public void setReadTimeout(int timeout) throws IOException { + // NOP + } + } + + @Test + public void testEncryption() throws Exception { + + PipedByteStream a = new PipedByteStream(); + PipedByteStream b = new PipedByteStream(); + + a.connect(b); + + // the whole assertion stuff must be done in a thread + // if a threads terminates that was accessing the pipe then the pipe will become unusable + TestThread t0 = new TestThread((TestThread.Runnable) () -> testOut(a)); + TestThread t1 = new TestThread((TestThread.Runnable) () -> testIn(b)); + + t0.start(); + t1.start(); + + t0.join(60000); + t1.join(60000); + + t0.verify(); + t1.verify(); + } + + private void testOut(PipedByteStream s) throws IOException { + ByteStream b = SecureByteStream.wrap(s, false); + + s.recordingOut.record = true; + + b.getOutputStream().write(secretData); + b.getOutputStream().flush(); + + byte[] recordedData = s.recordingOut.recordedData.toByteArray(); + + // as we use a symmetric cipher compare the length + if (secretData.length != recordedData.length || Arrays.equals(secretData, recordedData)) + throw new AssertionError( + "data was not encrypted: " + + Arrays.toString(secretData) + + " matches " + + Arrays.toString(recordedData)); + } + + private void testIn(PipedByteStream s) throws IOException { + ByteStream b = SecureByteStream.wrap(s, true); + + for (int i = 0; i < secretData.length; i++) + assertEquals(secretData[i], b.getInputStream().read()); + } +} diff --git a/core/test/junit/saros/net/stream/TestSuite.java b/core/test/junit/saros/net/stream/TestSuite.java new file mode 100644 index 0000000000..63aa8b3f60 --- /dev/null +++ b/core/test/junit/saros/net/stream/TestSuite.java @@ -0,0 +1,11 @@ +package saros.net.stream; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite; + +@RunWith(Suite.class) +@Suite.SuiteClasses({SecureByteStreamTest.class}) +public class TestSuite { + // the class remains completely empty, + // being used only as a holder for the above annotations +}