Reputation: 61
I'm developing an application on Android with Android Studio and I want to use FTP to send files to a server. I need to support session reuse since the server is hosted by an hosting service provider and they obviously have session reuse enabled.
I found this reflection hack in this post used by many to make make this possible:
// adapted from:
// https://trac.cyberduck.io/browser/trunk/ftp/src/main/java/ch/cyberduck/core/ftp/FTPClient.java
@Override
protected void _prepareDataSocket_(final Socket socket) throws IOException {
if (socket instanceof SSLSocket) {
// Control socket is SSL
final SSLSession session = ((SSLSocket) _socket_).getSession();
if (session.isValid()) {
final SSLSessionContext context = session.getSessionContext();
try {
final Field sessionHostPortCache = context.getClass().getDeclaredField("sessionHostPortCache");
sessionHostPortCache.setAccessible(true);
final Object cache = sessionHostPortCache.get(context);
final Method method = cache.getClass().getDeclaredMethod("put", Object.class, Object.class);
method.setAccessible(true);
method.invoke(cache, String
.format("%s:%s", socket.getInetAddress().getHostName(), String.valueOf(socket.getPort()))
.toLowerCase(Locale.ROOT), session);
method.invoke(cache, String
.format("%s:%s", socket.getInetAddress().getHostAddress(), String.valueOf(socket.getPort()))
.toLowerCase(Locale.ROOT), session);
} catch (NoSuchFieldException e) {
throw new IOException(e);
} catch (Exception e) {
throw new IOException(e);
}
} else {
throw new IOException("Invalid SSL Session");
}
}
}
Here's the code that uses SSLSessionReuseFTPSClient:
System.setProperty("jdk.tls.useExtendedMasterSecret", "false");
String host = "xxxxxxxx";
String user = "xxxxxxxx";
String password = "xxxxxxxx";
String directory = "xxxxxxxx";
ProtocolCommandListener listener = new MyProtocolCommandListener(host);
SSLSessionReuseFTPSClient client = new SSLSessionReuseFTPSClient("TLS", false);
client.addProtocolCommandListener(listener);
try {
client.connect(host);
client.execPBSZ(0);
client.execPROT("P");
if (client.login(user, password)) {
Log.w("myApp", "Logged in as " + user + " on " + host + ".");
}
if (client.changeWorkingDirectory(directory)) {
Log.w("myApp", "Working directory changed to " + directory + ".");
}
client.enterLocalPassiveMode();
InputStream input = new FileInputStream(file);
if (client.storeFile(file.getName(), input)) {
Log.w("myApp", "File " + file.getName() + " sent to " + host + ".");
} else {
Log.w("myApp", "Couldn't send file " + file.getName() + " to " + host + ".");
Log.w("myApp", "Reply: " + client.getReplyString());
}
client.logout();
client.disconnect();
} catch (Exception e) {
e.printStackTrace();
}
I first tried in Eclipse, and it worked. Then I tried to implement it in my Android application, but I get this error:
java.io.IOException: java.lang.NoSuchFieldException: No field sessionHostPortCache in class Lcom/android/org/conscrypt/ClientSessionContext; (declaration of 'com.android.org.conscrypt.ClientSessionContext' appears in /system/framework/conscrypt.jar)
I noticed that, when I execute the code in Eclipse and print the context
class name, I get this: sun.security.ssl.SSLSessionContextImpl
, but in Android Studio, I get: com.android.org.conscrypt.ClientSessionContext
.
I've been searching for almost two days straight and I'm just not experienced enough to know what is up. Why is com.android.org.conscrypt.ClientSessionContext
use instead of sun.security.ssl.SSLSessionContextImpl
? I check the java.security file and from what I see, sun.security.ssl.SSLSessionContextImpl
should be used.
If someone could help me with this, I would be insanely grateful.
Finally, here's some information that could be useful :
Android Studio 3.6.2
commons-net-3.6
openjdk version "1.8.0_212-release"
OpenJDK Runtime Environment (build 1.8.0_212-release-1586-b04)
OpenJDK 64-Bit Server VM (build 25.212-b04, mixed mode)
Thank you !
Upvotes: 4
Views: 1651
Reputation: 765
I ran into the same issue in an android project with api 33. Trying to get the field: sessionsContext.getClass().getDeclaredField("sessionsByHostAndPort") leads to:
Accessing hidden field (blocked, reflection, denied)
W/Accessing hidden field Lcom/android/org/conscrypt/ClientSessionContext;
->sessionsByHostAndPort:Ljava/util/Map; (blocked, reflection, denied)
W/System.err: java.lang.NoSuchFieldException:
No field sessionsByHostAndPort in class Lcom/android/org/conscrypt/ClientSessionContext;
(declaration of 'com.android.org.conscrypt.ClientSessionContext'
appears in /apex/com.android.conscrypt/javalib/conscrypt.jar)
W/System.err: at java.lang.Class.getDeclaredField(Native Method)
W/System.err: at org.apache.commons.net.ftp.FTPSClient._prepareDataSocket_(FTPSClient.java:409)
W/System.err: at org.apache.commons.net.ftp.FTPSClient._openDataConnection_(FTPSClient.java:310)
W/System.err: at org.apache.commons.net.ftp.FTPClient._openDataConnection_(FTPClient.java:638)
W/System.err: at org.apache.commons.net.ftp.FTPClient.initiateListParsing(FTPClient.java:1988)
W/System.err: at org.apache.commons.net.ftp.FTPClient.initiateListParsing(FTPClient.java:2084)
W/System.err: at org.apache.commons.net.ftp.FTPClient.listFiles(FTPClient.java:2282)
W/System.err: at org.apache.commons.net.ftp.FTPClient.listFiles(FTPClient.java:2248)
W/System.err: at de.medialux.powerftp.activities.RemoteExplorerActivityFTP$23.doInBackground(RemoteExplorerActivityFTP.java:2186)
W/System.err: at de.medialux.powerftp.activities.RemoteExplorerActivityFTP$23.doInBackground(RemoteExplorerActivityFTP.java:1978)
W/System.err: at android.os.AsyncTask$3.call(AsyncTask.java:394)
W/System.err: at java.util.concurrent.FutureTask.run(FutureTask.java:264)
W/System.err: at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:305)
W/System.err: at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1137)
W/System.err: at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637)
W/System.err: at java.lang.Thread.run(Thread.java:1012)
Since API level 28 Android forbids access to some hidden API functions (see: https://developer.android.com/distribute/best-practices/develop/restrictions-non-sdk-interfaces). Lots of required functions used by the PoC are black listed and threw exceptions while trying to access them via the Reflection API.
After some time of research I found the 'com.github.ChickenHook:RestrictionBypass' library.
https://github.com/ChickenHook/RestrictionBypass
After adding the library to the project in app gradle file the access to the fields in ClientSessionContext are granted.
Upvotes: 3
Reputation: 79
Based on the same idea of the Java cyberduck's solution, I overrided the "prepareDataSocket" method of the FTPSClient to make it works on Android. I test it in Android 9.0 and in Android 5.1.1 and it works fine. The code was:
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSessionContext;
import javax.net.ssl.SSLSocket;
import org.apache.commons.net.ftp.FTPSClient;
public class TLSAndroidFTPSClient extends FTPSClient
{
@Override
protected void _prepareDataSocket_(final Socket socket) throws IOException
{
if (socket instanceof SSLSocket)
{
final SSLSession sessionAux = ((SSLSocket) _socket_).getSession();
if(sessionAux.isValid())
{
final SSLSessionContext sessionsContext = sessionAux.getSessionContext();
try
{
// lets find the sessions in the context' cache
final Field fieldSessionsInContext =sessionsContext.getClass().getDeclaredField("sessionsByHostAndPort");
fieldSessionsInContext.setAccessible(true);
final Object sessionsInContext = fieldSessionsInContext.get(sessionsContext);
// lets find the session of our conexion
int portNumb=sessionAux.getPeerPort();
Set keys=((HashMap)sessionsInContext).keySet();
if(keys.size()==0)
throw new IOException("Invalid SSL Session");
final Field fieldPort=((keys.toArray())[0]).getClass().getDeclaredField("port");
fieldPort.setAccessible(true);
int i=0;
while(i<keys.size() && ((int)fieldPort.get((keys.toArray())[i]))!=portNumb)
i++;
if(i<keys.size()) // it was found
{
Object ourKey=(keys.toArray())[i];
// building two objects like our key but with the new port and the host Name and host address
final Constructor construc =ourKey.getClass().getDeclaredConstructor(String.class, int.class);
construc.setAccessible(true);
Object copy1Key=construc.newInstance(socket.getInetAddress().getHostName(),socket.getPort());
Object copy2Key=construc.newInstance(socket.getInetAddress().getHostAddress(),socket.getPort());
// getting our session
Object ourSession=((HashMap)sessionsInContext).get(ourKey);
// Lets add the pairs copy1Key-ourSession & copy2Key-ourSession to the context'cache
final Method method = sessionsInContext.getClass().getDeclaredMethod("put", Object.class, Object.class);
method.setAccessible(true);
method.invoke(sessionsInContext,copy1Key,ourSession);
method.invoke(sessionsInContext,copy2Key,ourSession);
}
else
throw new IOException("Invalid SSL Session");
} catch (NoSuchFieldException e) {
throw new IOException(e);
} catch (Exception e) {
throw new IOException(e);
}
} else {
throw new IOException("Invalid SSL Session");
}
}
}
}
Upvotes: 6