I don't believe this can be done via the public API. You can get the session ID but there is no public interface to obtain the key.
However, I was able to use a combination of reflection and native code to get access to the underlying OpenSSL struct, which does contain both the session ID and the master key. So it is possible but it is not at all safe as the hidden member and libraries are not guaranteed to remain the same. In fact, it looks like the struct layout has changed on the OpenSSL master branch so the parsing code below will need updating if/when that is pulled into Android.
I used URL.openConnection()
instead of DefaultHttpClient
for making an HTTPS connection as the latter is now deprecated. Here's the class that calls URL.openConnection()
and replaces the default SSLSocketFactory
(nothing that interesting here):
public class MyConnection implements Runnable {
@Override
public void run() {
try {
// Create the connection.
URL url = new URL("https://www.google.com");
HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
// Replace the default SSLSocketFactory with our own.
MySSLSocketFactory sslSocketFactory = new MySSLSocketFactory();
urlConnection.setSSLSocketFactory(sslSocketFactory);
// Establish the TLS connection.
int statusCode = urlConnection.getResponseCode();
Log.i("MyConnection", String.format("status %d", statusCode));
// Get SSL details from the captured socket.
sslSocketFactory.getSessionInfo();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Here is the custom SSLSocketFactory
, where most of the magic is. All it does is forward overridden methods to the real SSLSocketFactory
, caching the SSLSocket
instance that gets created. There are also two new (non-overridden) methods - a native method shown further below and getSessionInfo()
which uses reflection on the SSLSocket
to get the native OpenSSL ssl_session_st
pointer and parses (and logs) fields of interest. Note that you could get the session ID using the supported SSLSession.getId()
; it's getting the key that requires being sneaky.
// Use Decorator pattern to capture the SSL socket from the default SSLSocketFactory.
class MySSLSocketFactory extends SSLSocketFactory {
// Load NDK shared library.
static {
System.loadLibrary("my_native_helper");
}
// All overridden methods will be forwarded to the real SSLSocketFactory.
// The only addition is that the SSLSocket returned by createSocket() is
// cached.
SSLSocketFactory realFactory_ = HttpsURLConnection.getDefaultSSLSocketFactory();
SSLSocket s_;
// This native method copies data from a native pointer into a ByteBuffer.
native void readNative(long pointer, ByteBuffer dst);
// Use the cached SSLSocket to access native OpenSSL session data.
void getSessionInfo() throws NoSuchFieldException, IllegalAccessException {
// Get the protected OpenSSL ssl_session_st pointer. Note that this
// is not part of the API and could change across Android versions.
// See https://android.googlesource.com/platform/external/conscrypt/+/lollipop-mr1-dev/src/main/java/org/conscrypt/OpenSSLSessionImpl.java
SSLSession session = s_.getSession();
Field field = session.getClass().getDeclaredField("sslSessionNativePointer");
field.setAccessible(true);
long sessionPointer = field.getLong(session);
// Read as many bytes as we need from the native pointer.
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(104);
byteBuffer.order(ByteOrder.nativeOrder());
readNative(sessionPointer, byteBuffer);
// Parse the OpenSSL ssl_session_st. Note that the layout of this structure
// may change with OpenSSL versions and different compilers/platforms (e.g.
// 32-bit vs. 64-bit).
// See https://github.com/openssl/openssl/blob/OpenSSL_1_0_0-stable/ssl/ssl.h#L451
IntBuffer intBuffer = byteBuffer.asIntBuffer();
Log.i("MyConnection", String.format("SSL version %04x", intBuffer.get(0)));
int master_key_length = intBuffer.get(4);
String master_key = "";
for (int i = 0; i < master_key_length; ++i)
master_key += String.format("%02x", byteBuffer.get(20 + i));
Log.i("MyConnection", String.format("Master key %s", master_key));
int session_id_length = intBuffer.get(17);
String session_id = "";
for (int i = 0; i < session_id_length; ++i)
session_id += String.format("%02x", byteBuffer.get(72 + i));
Log.i("MyConnection", String.format("Session ID %s", session_id));
}
@Override
public String[] getDefaultCipherSuites() {
return realFactory_.getDefaultCipherSuites();
}
@Override
public String[] getSupportedCipherSuites() {
return realFactory_.getSupportedCipherSuites();
}
@Override
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
s_ = (SSLSocket)realFactory_.createSocket(s, host, port, autoClose);
return s_;
}
@Override
public Socket createSocket(String host, int port) throws IOException {
s_ = (SSLSocket)realFactory_.createSocket(host, port);
return s_;
}
@Override
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
s_ = (SSLSocket)realFactory_.createSocket(host, port, localHost, localPort);
return s_;
}
@Override
public Socket createSocket(InetAddress host, int port) throws IOException {
s_ = (SSLSocket)realFactory_.createSocket(host, port);
return s_;
}
@Override
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
s_ = (SSLSocket)realFactory_.createSocket(address, port, localAddress, localPort);
return s_;
}
}
Finally, here is the native C code that enables reading memory into a ByteBuffer from a native pointer. This needs to be built using the Android NDK and loaded as shown at the top of MySSLSocketFactory
.
#include <jni.h>
#include <string.h>
JNIEXPORT
void JNICALL Java_com_example_mysocketfactory_MySSLSocketFactory_readNative(
JNIEnv *env, jobject o,
jlong pointer, jobject buffer) {
const char *p = (const char *)pointer;
memcpy(
(*env)->GetDirectBufferAddress(env, buffer),
p,
(*env)->GetDirectBufferCapacity(env, buffer));
}
That's it. When MyConnection.run()
is invoked on my KitKat device, the log shows:
I/MyConnection﹕ status 200
I/MyConnection﹕ SSL version 0301
I/MyConnection﹕ Master key 81ef39c5f8f7f796a34b307ff453511378fd081d14c37eb2e912fa829edf280e0fa7a499c370fdc156b8499758373d67
I/MyConnection﹕ Session ID b9ee4ae0c7738909430d47e9b0d6d60420d34a17d08181f21996e55a463aa5cf
I did make a brief attempt with DefaultHttpClient
but abandoned it when I couldn't figure out how to access the default SchemeRegistry. I think it can be done by specifying a ClientConnectionManager
when constructing the DefaultHttpClient
but I didn't feel like pursuing a deprecated path any further. If you want to try then you would likely use a similar approach to intercept the SSLSessionImpl
instance handling the connection. This class has a master_secret
member so native code would not be required, only reflection (this code path does not use OpenSSL).