Refactoring externalization of SSLSessions (#383)

This is an implementation to #381. This change attempts to provide more
consistency to the session that is returned to the caller by `ConscryptEngine`/`ConscryptFileDescriptorSocket`.

Main changes:

- New interface ConscryptSession adds a few methods currently only defined by ActiveSession
- New interface SessionDecorator that defines getDelegate()
- New class ProvidedSessionDecorator delegates to an external provider of the "current" session. The provider implementations are in ConscryptEngine and ConscryptFileDescriptorSocket.
- New class SessionSnapshot that takes a snapshot of any ConscryptSession.
- Changed ActiveSession and SSLNullSession to implement ConscryptSession.
- Updated ConscryptEngine/ConscryptFileDescriptorSocket to create a SessionSnapshot when closing.

Additional cleanup:

- Split out Java7SessionWrapper into two classes: Java7ExtendedSSLSession and Java8ExtendedSSLSession. The Java 8 version no longer requires reflection and is more consistent with platform-specific code elsewhere. Both classes implement SessionDecorator.
- Renamed SslWrapper->NativeSsl and SslSessionWrapper->NativeSslSession for clarity, since the term "wrapper" was being overloaded.

Fixes #379
diff --git a/android/lint.xml b/android/lint.xml
index f1af8de..bc79476 100644
--- a/android/lint.xml
+++ b/android/lint.xml
@@ -2,7 +2,8 @@
 <lint>
     <!-- ExtendedSSLSession only gets instantiated in new APIs on Android. -->
     <issue id="NewApi">
-        <ignore path="**/org/conscrypt/Java7SessionWrapper.java" />
+        <ignore path="**/org/conscrypt/Java7ExtendedSSLSession.java" />
+        <ignore path="**/org/conscrypt/Java8ExtendedSSLSession.java" />
         <ignore path="**/org/conscrypt/Java8EngineWrapper.java" />
         <ignore path="**/org/conscrypt/Java8EngineSocket.java" />
         <ignore path="**/org/conscrypt/Java8FileDescriptorSocket.java" />
diff --git a/android/src/main/java/org/conscrypt/Platform.java b/android/src/main/java/org/conscrypt/Platform.java
index c2c6c8d..f17ba5e 100644
--- a/android/src/main/java/org/conscrypt/Platform.java
+++ b/android/src/main/java/org/conscrypt/Platform.java
@@ -116,7 +116,7 @@
         }
     }
 
-    /*
+    /**
      * Call Os.setsockoptTimeval via reflection.
      */
     public static void setSocketWriteTimeout(Socket s, long timeoutMillis) throws SocketException {
@@ -849,24 +849,18 @@
         return oid;
     }
 
-    /*
-     * Pre-Java 8 backward compatibility.
+    /**
+     * Provides extended capabilities for the session if supported by the platform.
      */
-
-    public static SSLSession wrapSSLSession(ActiveSession sslSession) {
-        if (Build.VERSION.SDK_INT <= 23) {
-            return sslSession;
+    public static SSLSession wrapSSLSession(ConscryptSession sslSession) {
+        if (Build.VERSION.SDK_INT >= 24) {
+            return new Java8ExtendedSSLSession(sslSession);
+        }
+        if (Build.VERSION.SDK_INT >= 19) {
+            return new Java7ExtendedSSLSession(sslSession);
         }
 
-        return new Java7SessionWrapper(sslSession);
-    }
-
-    public static SSLSession unwrapSSLSession(SSLSession sslSession) {
-        if (Build.VERSION.SDK_INT <= 23) {
-            return sslSession;
-        }
-
-        return Java7SessionWrapper.getDelegate(sslSession);
+        return sslSession;
     }
 
     public static String getOriginalHostNameFromInetAddress(InetAddress addr) {
diff --git a/common/src/main/java/org/conscrypt/AbstractSessionContext.java b/common/src/main/java/org/conscrypt/AbstractSessionContext.java
index 0cba09d..25a6b90 100644
--- a/common/src/main/java/org/conscrypt/AbstractSessionContext.java
+++ b/common/src/main/java/org/conscrypt/AbstractSessionContext.java
@@ -42,11 +42,11 @@
     final long sslCtxNativePointer = NativeCrypto.SSL_CTX_new();
 
     @SuppressWarnings("serial")
-    private final Map<ByteArray, SslSessionWrapper> sessions =
-            new LinkedHashMap<ByteArray, SslSessionWrapper>() {
+    private final Map<ByteArray, NativeSslSession> sessions =
+            new LinkedHashMap<ByteArray, NativeSslSession>() {
                 @Override
                 protected boolean removeEldestEntry(
-                        Map.Entry<ByteArray, SslSessionWrapper> eldest) {
+                        Map.Entry<ByteArray, NativeSslSession> eldest) {
                     // NOTE: does not take into account any session that may have become
                     // invalid.
                     if (maximumSize > 0 && size() > maximumSize) {
@@ -74,13 +74,13 @@
     @Override
     public final Enumeration<byte[]> getIds() {
         // Make a copy of the IDs.
-        final Iterator<SslSessionWrapper> iter;
+        final Iterator<NativeSslSession> iter;
         synchronized (sessions) {
-            iter = Arrays.asList(sessions.values().toArray(new SslSessionWrapper[sessions.size()]))
+            iter = Arrays.asList(sessions.values().toArray(new NativeSslSession[sessions.size()]))
                     .iterator();
         }
         return new Enumeration<byte[]>() {
-            private SslSessionWrapper next;
+            private NativeSslSession next;
 
             @Override
             public boolean hasMoreElements() {
@@ -88,7 +88,7 @@
                     return true;
                 }
                 while (iter.hasNext()) {
-                    SslSessionWrapper session = iter.next();
+                    NativeSslSession session = iter.next();
                     if (session.isValid()) {
                         next = session;
                         return true;
@@ -120,7 +120,7 @@
             throw new NullPointerException("sessionId");
         }
         ByteArray key = new ByteArray(sessionId);
-        SslSessionWrapper session;
+        NativeSslSession session;
         synchronized (sessions) {
             session = sessions.get(key);
         }
@@ -158,9 +158,9 @@
                 NativeCrypto.SSL_CTX_set_timeout(sslCtxNativePointer, Integer.MAX_VALUE);
             }
 
-            Iterator<SslSessionWrapper> i = sessions.values().iterator();
+            Iterator<NativeSslSession> i = sessions.values().iterator();
             while (i.hasNext()) {
-                SslSessionWrapper session = i.next();
+                NativeSslSession session = i.next();
                 // SSLSession's know their context and consult the
                 // timeout as part of their validity condition.
                 if (!session.isValid()) {
@@ -199,7 +199,7 @@
     /**
      * Adds the given session to the cache.
      */
-    final void cacheSession(SslSessionWrapper session) {
+    final void cacheSession(NativeSslSession session) {
         byte[] id = session.getId();
         if (id == null || id.length == 0) {
             return;
@@ -218,13 +218,13 @@
      * Called for server sessions only. Retrieves the session by its ID. Overridden by
      * {@link ServerSessionContext} to
      */
-    final SslSessionWrapper getSessionFromCache(byte[] sessionId) {
+    final NativeSslSession getSessionFromCache(byte[] sessionId) {
         if (sessionId == null) {
             return null;
         }
 
         // First, look in the in-memory cache.
-        SslSessionWrapper session;
+        NativeSslSession session;
         synchronized (sessions) {
             session = sessions.get(new ByteArray(sessionId));
         }
@@ -242,7 +242,7 @@
      *
      * <p>Visible for extension only, not intended to be called directly.
      */
-    abstract void onBeforeAddSession(SslSessionWrapper session);
+    abstract void onBeforeAddSession(NativeSslSession session);
 
     /**
      * Called when a session is about to be removed. Used by {@link ClientSessionContext}
@@ -250,14 +250,14 @@
      *
      * <p>Visible for extension only, not intended to be called directly.
      */
-    abstract void onBeforeRemoveSession(SslSessionWrapper session);
+    abstract void onBeforeRemoveSession(NativeSslSession session);
 
     /**
      * Called for server sessions only. Retrieves the session by ID from the persistent cache.
      *
      * <p>Visible for extension only, not intended to be called directly.
      */
-    abstract SslSessionWrapper getSessionFromPersistentCache(byte[] sessionId);
+    abstract NativeSslSession getSessionFromPersistentCache(byte[] sessionId);
 
     /**
      * Makes sure cache size is < maximumSize.
@@ -267,9 +267,9 @@
             int size = sessions.size();
             if (size > maximumSize) {
                 int removals = size - maximumSize;
-                Iterator<SslSessionWrapper> i = sessions.values().iterator();
+                Iterator<NativeSslSession> i = sessions.values().iterator();
                 while (removals-- > 0) {
-                    SslSessionWrapper session = i.next();
+                    NativeSslSession session = i.next();
                     onBeforeRemoveSession(session);
                     i.remove();
                 }
diff --git a/common/src/main/java/org/conscrypt/ActiveSession.java b/common/src/main/java/org/conscrypt/ActiveSession.java
index 67edfee..e9b695a 100644
--- a/common/src/main/java/org/conscrypt/ActiveSession.java
+++ b/common/src/main/java/org/conscrypt/ActiveSession.java
@@ -27,7 +27,6 @@
 import java.util.List;
 import java.util.Map;
 import javax.net.ssl.SSLPeerUnverifiedException;
-import javax.net.ssl.SSLSession;
 import javax.net.ssl.SSLSessionBindingEvent;
 import javax.net.ssl.SSLSessionBindingListener;
 import javax.net.ssl.SSLSessionContext;
@@ -36,8 +35,8 @@
  * A session that is dedicated a single connection and operates directly on the underlying
  * {@code SSL}.
  */
-final class ActiveSession implements SSLSession {
-    private final SslWrapper ssl;
+final class ActiveSession implements ConscryptSession {
+    private final NativeSsl ssl;
     private AbstractSessionContext sessionContext;
     private byte[] id;
     private long creationTime;
@@ -54,7 +53,7 @@
     // lazy init for memory reasons
     private Map<String, Object> values;
 
-    ActiveSession(SslWrapper ssl, AbstractSessionContext sessionContext) {
+    ActiveSession(NativeSsl ssl, AbstractSessionContext sessionContext) {
         this.ssl = checkNotNull(ssl, "ssl");
         this.sessionContext = checkNotNull(sessionContext, "sessionContext");
     }
@@ -119,8 +118,7 @@
      * @see <a href="https://tools.ietf.org/html/rfc6066">RFC 6066</a>
      * @see <a href="https://tools.ietf.org/html/rfc6961">RFC 6961</a>
      */
-    /* @Override */
-    @SuppressWarnings("MissingOverride") // For Pre-Java9 compatibility.
+    @Override
     public List<byte[]> getStatusResponses() {
         if (peerCertificateOcspData == null) {
             return Collections.<byte[]>emptyList();
@@ -135,14 +133,16 @@
      *
      * @see <a href="https://tools.ietf.org/html/rfc6962">RFC 6962</a>
      */
-    byte[] getPeerSignedCertificateTimestamp() {
+    @Override
+    public byte[] getPeerSignedCertificateTimestamp() {
         if (peerTlsSctData == null) {
             return null;
         }
         return peerTlsSctData.clone();
     }
 
-    String getRequestedServerName() {
+    @Override
+    public String getRequestedServerName() {
         synchronized (ssl) {
             return ssl.getRequestedServerName();
         }
diff --git a/common/src/main/java/org/conscrypt/ClientSessionContext.java b/common/src/main/java/org/conscrypt/ClientSessionContext.java
index 66007f9..761223f 100644
--- a/common/src/main/java/org/conscrypt/ClientSessionContext.java
+++ b/common/src/main/java/org/conscrypt/ClientSessionContext.java
@@ -33,7 +33,7 @@
      * access by holding a lock on sessionsByHostAndPort.
      */
     @SuppressWarnings("serial")
-    private final Map<HostAndPort, SslSessionWrapper> sessionsByHostAndPort = new HashMap<HostAndPort, SslSessionWrapper>();
+    private final Map<HostAndPort, NativeSslSession> sessionsByHostAndPort = new HashMap<HostAndPort, NativeSslSession>();
 
     private SSLClientSessionCache persistentCache;
 
@@ -52,12 +52,12 @@
     /**
      * Gets the suitable session reference from the session cache container.
      */
-    SslSessionWrapper getCachedSession(String hostName, int port, SSLParametersImpl sslParameters) {
+    NativeSslSession getCachedSession(String hostName, int port, SSLParametersImpl sslParameters) {
         if (hostName == null) {
             return null;
         }
 
-        SslSessionWrapper session = getSession(hostName, port);
+        NativeSslSession session = getSession(hostName, port);
         if (session == null) {
             return null;
         }
@@ -100,13 +100,13 @@
      * @param port of server
      * @return cached session or null if none found
      */
-    private SslSessionWrapper getSession(String host, int port) {
+    private NativeSslSession getSession(String host, int port) {
         if (host == null) {
             return null;
         }
 
         HostAndPort key = new HostAndPort(host, port);
-        SslSessionWrapper session;
+        NativeSslSession session;
         synchronized (sessionsByHostAndPort) {
             session = sessionsByHostAndPort.get(key);
         }
@@ -118,7 +118,7 @@
         if (persistentCache != null) {
             byte[] data = persistentCache.getSessionData(host, port);
             if (data != null) {
-                session = SslSessionWrapper.newInstance(this, data, host, port);
+                session = NativeSslSession.newInstance(this, data, host, port);
                 if (session != null && session.isValid()) {
                     synchronized (sessionsByHostAndPort) {
                         sessionsByHostAndPort.put(key, session);
@@ -132,7 +132,7 @@
     }
 
     @Override
-    void onBeforeAddSession(SslSessionWrapper session) {
+    void onBeforeAddSession(NativeSslSession session) {
         String host = session.getPeerHost();
         int port = session.getPeerPort();
         if (host == null) {
@@ -154,7 +154,7 @@
     }
 
     @Override
-    void onBeforeRemoveSession(SslSessionWrapper session) {
+    void onBeforeRemoveSession(NativeSslSession session) {
         String host = session.getPeerHost();
         if (host == null) {
             return;
@@ -167,7 +167,7 @@
     }
 
     @Override
-    SslSessionWrapper getSessionFromPersistentCache(byte[] sessionId) {
+    NativeSslSession getSessionFromPersistentCache(byte[] sessionId) {
         // Not implemented for clients.
         return null;
     }
diff --git a/common/src/main/java/org/conscrypt/ConscryptEngine.java b/common/src/main/java/org/conscrypt/ConscryptEngine.java
index 6c96b47..11c3d1f 100644
--- a/common/src/main/java/org/conscrypt/ConscryptEngine.java
+++ b/common/src/main/java/org/conscrypt/ConscryptEngine.java
@@ -91,7 +91,8 @@
 import javax.net.ssl.X509TrustManager;
 import javax.security.auth.x500.X500Principal;
 import org.conscrypt.NativeRef.SSL_SESSION;
-import org.conscrypt.SslWrapper.BioWrapper;
+import org.conscrypt.NativeSsl.BioWrapper;
+import org.conscrypt.ProvidedSessionDecorator.Provider;
 
 /**
  * Implements the {@link SSLEngine} API using OpenSSL's non-blocking interfaces.
@@ -132,7 +133,7 @@
     /**
      * Wrapper around the underlying SSL object.
      */
-    private final SslWrapper ssl;
+    private final NativeSsl ssl;
 
     /**
      * The BIO used for reading/writing encrypted bytes.
@@ -143,7 +144,23 @@
     /**
      * Set during startHandshake.
      */
-    private final ActiveSession sslSession;
+    private final ActiveSession activeSession;
+
+    /**
+     * A snapshot of the active session when the engine was closed.
+     */
+    private SessionSnapshot closedSession;
+
+    /**
+     * The session object exposed externally from this class.
+     */
+    private final SSLSession externalSession =
+        Platform.wrapSSLSession(new ProvidedSessionDecorator(new Provider() {
+            @Override
+            public ConscryptSession provideSession() {
+                return ConscryptEngine.this.provideSession();
+            }
+        }));
 
     /**
      * Private key for the TLS Channel ID extension. This field is client-side only. Set during
@@ -166,7 +183,7 @@
         peerInfoProvider = PeerInfoProvider.nullProvider();
         this.ssl = newSsl(sslParameters, this);
         this.networkBio = ssl.newBio();
-        sslSession = new ActiveSession(ssl, sslParameters.getSessionContext());
+        activeSession = new ActiveSession(ssl, sslParameters.getSessionContext());
     }
 
     ConscryptEngine(String host, int port, SSLParametersImpl sslParameters) {
@@ -174,7 +191,7 @@
         this.peerInfoProvider = PeerInfoProvider.forHostAndPort(host, port);
         this.ssl = newSsl(sslParameters, this);
         this.networkBio = ssl.newBio();
-        sslSession = new ActiveSession(ssl, sslParameters.getSessionContext());
+        activeSession = new ActiveSession(ssl, sslParameters.getSessionContext());
     }
 
     ConscryptEngine(SSLParametersImpl sslParameters, PeerInfoProvider peerInfoProvider) {
@@ -182,12 +199,12 @@
         this.peerInfoProvider = checkNotNull(peerInfoProvider, "peerInfoProvider");
         this.ssl = newSsl(sslParameters, this);
         this.networkBio = ssl.newBio();
-        sslSession = new ActiveSession(ssl, sslParameters.getSessionContext());
+        activeSession = new ActiveSession(ssl, sslParameters.getSessionContext());
     }
 
-    private static SslWrapper newSsl(SSLParametersImpl sslParameters, ConscryptEngine engine) {
+    private static NativeSsl newSsl(SSLParametersImpl sslParameters, ConscryptEngine engine) {
         try {
-            return SslWrapper.newInstance(sslParameters, engine, engine, engine);
+            return NativeSsl.newInstance(sslParameters, engine, engine, engine);
         } catch (SSLException e) {
             throw new RuntimeException(e);
         }
@@ -387,7 +404,7 @@
                 return;
         }
 
-        state = STATE_HANDSHAKE_STARTED;
+        transitionTo(STATE_HANDSHAKE_STARTED);
 
         boolean releaseResources = true;
         try {
@@ -397,7 +414,7 @@
             // For clients, offer to resume a previously cached session to avoid the
             // full TLS handshake.
             if (getUseClientMode()) {
-                SslSessionWrapper cachedSession = clientSessionContext().getCachedSession(
+                NativeSslSession cachedSession = clientSessionContext().getCachedSession(
                         getHostname(), getPeerPort(), sslParameters);
                 if (cachedSession != null) {
                     cachedSession.offerToResume(ssl);
@@ -430,9 +447,9 @@
                 return;
             }
             if (isOutboundDone()) {
-                state = STATE_CLOSED;
+                transitionTo(STATE_CLOSED);
             } else {
-                state = STATE_CLOSED_INBOUND;
+                transitionTo(STATE_CLOSED_INBOUND);
             }
         }
     }
@@ -448,7 +465,7 @@
                 if (isInboundDone()) {
                     closeAndFreeResources();
                 } else {
-                    state = STATE_CLOSED_OUTBOUND;
+                    transitionTo(STATE_CLOSED_OUTBOUND);
                 }
             } else {
                 // Never started the handshake. Just close now.
@@ -545,18 +562,40 @@
     @Override
     SSLSession handshakeSession() {
         synchronized (ssl) {
-            return state == STATE_HANDSHAKE_STARTED ? sslSession : null;
+            if (state == STATE_HANDSHAKE_STARTED) {
+                return Platform.wrapSSLSession(new ProvidedSessionDecorator(new Provider() {
+                    @Override
+                    public ConscryptSession provideSession() {
+                        return ConscryptEngine.this.provideHandshakeSession();
+                    }
+                }));
+            }
+            return null;
         }
     }
 
     @Override
     public SSLSession getSession() {
+        return externalSession;
+    }
+
+    private ConscryptSession provideSession() {
         synchronized (ssl) {
+            if (state == STATE_CLOSED) {
+                return closedSession != null ? closedSession : SSLNullSession.getNullSession();
+            }
             if (state < STATE_HANDSHAKE_COMPLETED) {
                 // Return an invalid session with invalid cipher suite of "SSL_NULL_WITH_NULL_NULL"
                 return SSLNullSession.getNullSession();
             }
-            return Platform.wrapSSLSession(sslSession);
+            return activeSession;
+        }
+    }
+
+    private ConscryptSession provideHandshakeSession() {
+        synchronized (ssl) {
+            return state == STATE_HANDSHAKE_STARTED ? activeSession
+                : SSLNullSession.getNullSession();
         }
     }
 
@@ -622,7 +661,7 @@
                 throw new IllegalArgumentException(
                         "Can not change mode after handshake: state == " + state);
             }
-            state = STATE_MODE_SET;
+            transitionTo(STATE_MODE_SET);
             sslParameters.setUseClientMode(mode);
         }
     }
@@ -955,7 +994,7 @@
             // The handshake has completed successfully...
 
             // Update the session from the current state of the SSL object.
-            sslSession.onPeerCertificateAvailable(getPeerHost(), getPeerPort());
+            activeSession.onPeerCertificateAvailable(getPeerHost(), getPeerPort());
 
             finishHandshake();
             return FINISHED;
@@ -1528,8 +1567,7 @@
                 case SSL_CB_HANDSHAKE_START: {
                     // For clients, this will allow the NEED_UNWRAP status to be
                     // returned.
-                    state = STATE_HANDSHAKE_STARTED;
-                    handshakeFinished = false;
+                    transitionTo(STATE_HANDSHAKE_STARTED);
                     break;
                 }
                 case SSL_CB_HANDSHAKE_DONE: {
@@ -1538,7 +1576,7 @@
                         throw new IllegalStateException(
                                 "Completed handshake while in mode " + state);
                     }
-                    state = STATE_HANDSHAKE_COMPLETED;
+                    transitionTo(STATE_HANDSHAKE_COMPLETED);
                     break;
                 }
                 default:
@@ -1558,11 +1596,11 @@
             // BoringSSL guarantees will not happen.
             NativeRef.SSL_SESSION ref = new SSL_SESSION(sslSessionNativePtr);
 
-            SslSessionWrapper sessionWrapper = SslSessionWrapper.newInstance(ref, sslSession);
+            NativeSslSession nativeSession = NativeSslSession.newInstance(ref, activeSession);
 
             // Cache the newly established session.
             AbstractSessionContext ctx = sessionContext();
-            ctx.cacheSession(sessionWrapper);
+            ctx.cacheSession(nativeSession);
         } catch (Exception ignored) {
             // Ignore.
         }
@@ -1589,7 +1627,7 @@
             }
 
             // Update the peer information on the session.
-            sslSession.onPeerCertificatesReceived(getPeerHost(), getPeerPort(), peerCertChain);
+            activeSession.onPeerCertificatesReceived(getPeerHost(), getPeerPort(), peerCertChain);
 
             if (getUseClientMode()) {
                 Platform.checkServerTrusted(x509tm, peerCertChain, authMethod, this);
@@ -1620,7 +1658,7 @@
     }
 
     private void closeAndFreeResources() {
-        state = STATE_CLOSED;
+        transitionTo(STATE_CLOSED);
         if (!ssl.isClosed()) {
             ssl.close();
             networkBio.close();
@@ -1747,4 +1785,25 @@
     private AbstractSessionContext sessionContext() {
         return sslParameters.getSessionContext();
     }
+
+    private void transitionTo(int newState) {
+        switch (newState) {
+            case STATE_HANDSHAKE_STARTED: {
+                handshakeFinished = false;
+                break;
+            }
+            case STATE_CLOSED: {
+                if (!ssl.isClosed() && state >= STATE_HANDSHAKE_STARTED && state < STATE_CLOSED ) {
+                    closedSession = new SessionSnapshot(activeSession);
+                }
+                break;
+            }
+            default: {
+                break;
+            }
+        }
+
+        // Update the state
+        this.state = newState;
+    }
 }
diff --git a/common/src/main/java/org/conscrypt/ConscryptFileDescriptorSocket.java b/common/src/main/java/org/conscrypt/ConscryptFileDescriptorSocket.java
index 7864f4e..99f9f98 100644
--- a/common/src/main/java/org/conscrypt/ConscryptFileDescriptorSocket.java
+++ b/common/src/main/java/org/conscrypt/ConscryptFileDescriptorSocket.java
@@ -45,6 +45,7 @@
 import javax.net.ssl.X509TrustManager;
 import javax.security.auth.x500.X500Principal;
 import org.conscrypt.NativeRef.SSL_SESSION;
+import org.conscrypt.ProvidedSessionDecorator.Provider;
 
 /**
  * Implementation of the class OpenSSLSocketImpl based on OpenSSL.
@@ -67,7 +68,7 @@
     /**
      * Wrapper around the underlying SSL object.
      */
-    private final SslWrapper ssl;
+    private final NativeSsl ssl;
 
     /**
      * Protected by synchronizing on ssl. Starts as null, set by
@@ -96,7 +97,21 @@
      */
     private OpenSSLKey channelIdPrivateKey;
 
-    private final ActiveSession sslSession;
+    private final ActiveSession activeSession;
+    /**
+     * A snapshot of the active session when the engine was closed.
+     */
+    private SessionSnapshot closedSession;
+    /**
+     * The session object exposed externally from this class.
+     */
+    private final SSLSession externalSession =
+        Platform.wrapSSLSession(new ProvidedSessionDecorator(new Provider() {
+            @Override
+            public ConscryptSession provideSession() {
+                return ConscryptFileDescriptorSocket.this.provideSession();
+            }
+        }));
 
     private int writeTimeoutMilliseconds = 0;
     private int handshakeTimeoutMilliseconds = -1; // -1 = same as timeout; 0 = infinite
@@ -106,7 +121,7 @@
     ConscryptFileDescriptorSocket(SSLParametersImpl sslParameters) throws IOException {
         this.sslParameters = sslParameters;
         this.ssl = newSsl(sslParameters, this);
-        sslSession = new ActiveSession(ssl, sslParameters.getSessionContext());
+        activeSession = new ActiveSession(ssl, sslParameters.getSessionContext());
     }
 
     ConscryptFileDescriptorSocket(String hostname, int port, SSLParametersImpl sslParameters)
@@ -114,7 +129,7 @@
         super(hostname, port);
         this.sslParameters = sslParameters;
         this.ssl = newSsl(sslParameters, this);
-        sslSession = new ActiveSession(ssl, sslParameters.getSessionContext());
+        activeSession = new ActiveSession(ssl, sslParameters.getSessionContext());
     }
 
     ConscryptFileDescriptorSocket(InetAddress address, int port, SSLParametersImpl sslParameters)
@@ -122,7 +137,7 @@
         super(address, port);
         this.sslParameters = sslParameters;
         this.ssl = newSsl(sslParameters, this);
-        sslSession = new ActiveSession(ssl, sslParameters.getSessionContext());
+        activeSession = new ActiveSession(ssl, sslParameters.getSessionContext());
     }
 
     ConscryptFileDescriptorSocket(String hostname, int port, InetAddress clientAddress,
@@ -130,7 +145,7 @@
         super(hostname, port, clientAddress, clientPort);
         this.sslParameters = sslParameters;
         this.ssl = newSsl(sslParameters, this);
-        sslSession = new ActiveSession(ssl, sslParameters.getSessionContext());
+        activeSession = new ActiveSession(ssl, sslParameters.getSessionContext());
     }
 
     ConscryptFileDescriptorSocket(InetAddress address, int port, InetAddress clientAddress,
@@ -138,7 +153,7 @@
         super(address, port, clientAddress, clientPort);
         this.sslParameters = sslParameters;
         this.ssl = newSsl(sslParameters, this);
-        sslSession = new ActiveSession(ssl, sslParameters.getSessionContext());
+        activeSession = new ActiveSession(ssl, sslParameters.getSessionContext());
     }
 
     ConscryptFileDescriptorSocket(Socket socket, String hostname, int port, boolean autoClose,
@@ -146,13 +161,13 @@
         super(socket, hostname, port, autoClose);
         this.sslParameters = sslParameters;
         this.ssl = newSsl(sslParameters, this);
-        sslSession = new ActiveSession(ssl, sslParameters.getSessionContext());
+        activeSession = new ActiveSession(ssl, sslParameters.getSessionContext());
     }
 
-    private static SslWrapper newSsl(SSLParametersImpl sslParameters,
+    private static NativeSsl newSsl(SSLParametersImpl sslParameters,
             ConscryptFileDescriptorSocket engine) {
         try {
-            return SslWrapper.newInstance(sslParameters, engine, engine, engine);
+            return NativeSsl.newInstance(sslParameters, engine, engine, engine);
         } catch (SSLException e) {
             throw new RuntimeException(e);
         }
@@ -170,7 +185,7 @@
         checkOpen();
         synchronized (ssl) {
             if (state == STATE_NEW) {
-                state = STATE_HANDSHAKE_STARTED;
+                transitionTo(STATE_HANDSHAKE_STARTED);
             } else {
                 // We've either started the handshake already or have been closed.
                 // Do nothing in both cases.
@@ -188,7 +203,7 @@
             // For clients, offer to resume a previously cached session to avoid the
             // full TLS handshake.
             if (getUseClientMode()) {
-                SslSessionWrapper cachedSession = clientSessionContext().getCachedSession(
+                NativeSslSession cachedSession = clientSessionContext().getCachedSession(
                         getHostnameOrIP(), getPort(), sslParameters);
                 if (cachedSession != null) {
                     cachedSession.offerToResume(ssl);
@@ -213,7 +228,7 @@
                 ssl.doHandshake(Platform.getFileDescriptor(socket), getSoTimeout());
 
                 // Update the session from the current state of the SSL object.
-                sslSession.onPeerCertificateAvailable(getHostnameOrIP(), getPort());
+                activeSession.onPeerCertificateAvailable(getHostnameOrIP(), getPort());
             } catch (CertificateException e) {
                 SSLHandshakeException wrapper = new SSLHandshakeException(e.getMessage());
                 wrapper.initCause(e);
@@ -260,9 +275,9 @@
                 releaseResources = (state == STATE_CLOSED);
 
                 if (state == STATE_HANDSHAKE_STARTED) {
-                    state = STATE_READY_HANDSHAKE_CUT_THROUGH;
+                    transitionTo(STATE_READY_HANDSHAKE_CUT_THROUGH);
                 } else {
-                    state = STATE_READY;
+                    transitionTo(STATE_READY);
                 }
 
                 if (!releaseResources) {
@@ -282,7 +297,7 @@
                     //
                     // The state will already be set to closed if we reach this as a result of
                     // an early return or an interruption due to a concurrent call to close().
-                    state = STATE_CLOSED;
+                    transitionTo(STATE_CLOSED);
                     ssl.notifyAll();
                 }
 
@@ -333,7 +348,7 @@
 
             // Now that we've fixed up our state, we can tell waiting threads that
             // we're ready.
-            state = STATE_READY;
+            transitionTo(STATE_READY);
         }
 
         // Let listeners know we are finally done
@@ -357,11 +372,11 @@
             // BoringSSL guarantees will not happen.
             NativeRef.SSL_SESSION ref = new SSL_SESSION(sslSessionNativePtr);
 
-            SslSessionWrapper sessionWrapper = SslSessionWrapper.newInstance(ref, sslSession);
+            NativeSslSession nativeSession = NativeSslSession.newInstance(ref, activeSession);
 
             // Cache the newly established session.
             AbstractSessionContext ctx = sessionContext();
-            ctx.cacheSession(sessionWrapper);
+            ctx.cacheSession(nativeSession);
         } catch (Exception ignored) {
             // Ignore.
         }
@@ -387,7 +402,7 @@
                 throw new CertificateException("No X.509 TrustManager");
             }
             // Update the peer information on the session.
-            sslSession.onPeerCertificatesReceived(getHostnameOrIP(), getPort(), peerCertChain);
+            activeSession.onPeerCertificatesReceived(getHostnameOrIP(), getPort(), peerCertChain);
 
             if (getUseClientMode()) {
                 Platform.checkServerTrusted(x509tm, peerCertChain, authMethod, this);
@@ -635,8 +650,16 @@
 
     @Override
     public final SSLSession getSession() {
+        return externalSession;
+    }
+
+    private ConscryptSession provideSession() {
         boolean handshakeCompleted = false;
         synchronized (ssl) {
+            if (state == STATE_CLOSED) {
+                return closedSession != null ? closedSession : SSLNullSession.getNullSession();
+            }
+
             try {
                 handshakeCompleted = state >= STATE_READY;
                 if (!handshakeCompleted && isConnected()) {
@@ -654,18 +677,33 @@
             return SSLNullSession.getNullSession();
         }
 
-        return Platform.wrapSSLSession(sslSession);
+        return activeSession;
+    }
+
+    private ConscryptSession provideHandshakeSession() {
+        synchronized (ssl) {
+            return state >= STATE_HANDSHAKE_STARTED && state < STATE_READY ? activeSession
+                : SSLNullSession.getNullSession();
+        }
     }
 
     @Override
     final SSLSession getActiveSession() {
-        return sslSession;
+        return activeSession;
     }
 
     @Override
     public final SSLSession getHandshakeSession() {
         synchronized (ssl) {
-            return state >= STATE_HANDSHAKE_STARTED && state < STATE_READY ? sslSession : null;
+            if (state >= STATE_HANDSHAKE_STARTED && state < STATE_READY) {
+                return Platform.wrapSSLSession(new ProvidedSessionDecorator(new Provider() {
+                    @Override
+                    public ConscryptSession provideSession() {
+                        return ConscryptFileDescriptorSocket.this.provideHandshakeSession();
+                    }
+                }));
+            }
+            return null;
         }
     }
 
@@ -911,7 +949,7 @@
             }
 
             int oldState = state;
-            state = STATE_CLOSED;
+            transitionTo(STATE_CLOSED);
 
             if (oldState == STATE_NEW) {
                 // The handshake hasn't been started yet, so there's no OpenSSL related
@@ -1100,4 +1138,21 @@
     private AbstractSessionContext sessionContext() {
         return sslParameters.getSessionContext();
     }
+
+    private void transitionTo(int newState) {
+        switch (newState) {
+            case STATE_CLOSED: {
+                if (!ssl.isClosed() && state >= STATE_HANDSHAKE_STARTED && state < STATE_CLOSED ) {
+                    closedSession = new SessionSnapshot(activeSession);
+                }
+                break;
+            }
+            default: {
+                break;
+            }
+        }
+
+        // Update the state
+        this.state = newState;
+    }
 }
diff --git a/common/src/main/java/org/conscrypt/ConscryptSession.java b/common/src/main/java/org/conscrypt/ConscryptSession.java
new file mode 100644
index 0000000..e59c192
--- /dev/null
+++ b/common/src/main/java/org/conscrypt/ConscryptSession.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.conscrypt;
+
+import java.security.cert.X509Certificate;
+import java.util.List;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSession;
+
+/**
+ * Extends the default interface for {@link SSLSession} to provide additional properties exposed
+ * by Conscrypt.
+ */
+interface ConscryptSession extends SSLSession {
+
+  String getRequestedServerName();
+
+  /**
+   * Returns the OCSP stapled response. Returns a copy of the internal arrays.
+   *
+   * The method signature matches
+   * <a
+   * href="http://download.java.net/java/jdk9/docs/api/javax/net/ssl/ExtendedSSLSession.html#getStatusResponses--">Java
+   * 9</a>.
+   *
+   * @see <a href="https://tools.ietf.org/html/rfc6066">RFC 6066</a>
+   * @see <a href="https://tools.ietf.org/html/rfc6961">RFC 6961</a>
+   */
+  List<byte[]> getStatusResponses();
+
+  /**
+   * Returns the signed certificate timestamp (SCT) received from the peer. Returns a
+   * copy of the internal array.
+   *
+   * @see <a href="https://tools.ietf.org/html/rfc6962">RFC 6962</a>
+   */
+  byte[] getPeerSignedCertificateTimestamp();
+
+  @Override
+  X509Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException;
+}
diff --git a/common/src/main/java/org/conscrypt/Java7ExtendedSSLSession.java b/common/src/main/java/org/conscrypt/Java7ExtendedSSLSession.java
new file mode 100644
index 0000000..8ecb359
--- /dev/null
+++ b/common/src/main/java/org/conscrypt/Java7ExtendedSSLSession.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.conscrypt;
+
+import java.security.Principal;
+import java.security.cert.Certificate;
+import java.util.List;
+import javax.net.ssl.ExtendedSSLSession;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSessionContext;
+import javax.security.cert.X509Certificate;
+
+/**
+ * This is an adapter that wraps the active session with {@link ExtendedSSLSession}, if running
+ * on Java 7+.
+ */
+class Java7ExtendedSSLSession extends ExtendedSSLSession implements SessionDecorator {
+    // TODO: use BoringSSL API to actually fetch the real data
+    private static final String[] LOCAL_SUPPORTED_SIGNATURE_ALGORITHMS = new String[] {
+            "SHA512withRSA", "SHA512withECDSA", "SHA384withRSA", "SHA384withECDSA", "SHA256withRSA",
+            "SHA256withECDSA", "SHA224withRSA", "SHA224withECDSA", "SHA1withRSA", "SHA1withECDSA",
+    };
+    // TODO: use BoringSSL API to actually fetch the real data
+    private static final String[] PEER_SUPPORTED_SIGNATURE_ALGORITHMS =
+            new String[] {"SHA1withRSA", "SHA1withECDSA"};
+    private final ConscryptSession delegate;
+
+    Java7ExtendedSSLSession(ConscryptSession delegate) {
+        this.delegate = delegate;
+    }
+
+    @Override
+    public final ConscryptSession getDelegate() {
+        return delegate;
+    }
+
+    /* @Override */
+    @SuppressWarnings("MissingOverride") // For Android backward-compatibility.
+    public final String[] getLocalSupportedSignatureAlgorithms() {
+        return LOCAL_SUPPORTED_SIGNATURE_ALGORITHMS.clone();
+    }
+
+    /* @Override */
+    @SuppressWarnings("MissingOverride") // For Android backward-compatibility.
+    public final String[] getPeerSupportedSignatureAlgorithms() {
+        return PEER_SUPPORTED_SIGNATURE_ALGORITHMS.clone();
+    }
+
+    @Override
+    public final String getRequestedServerName() {
+        return getDelegate().getRequestedServerName();
+    }
+
+    /**
+     * Provides forward-compatibility with Java 9.
+     */
+    @Override
+    public final List<byte[]> getStatusResponses() {
+        return getDelegate().getStatusResponses();
+    }
+
+    @Override
+    public final byte[] getPeerSignedCertificateTimestamp() {
+        return getDelegate().getPeerSignedCertificateTimestamp();
+    }
+
+    @Override
+    public final byte[] getId() {
+        return getDelegate().getId();
+    }
+
+    @Override
+    public final SSLSessionContext getSessionContext() {
+        return getDelegate().getSessionContext();
+    }
+
+    @Override
+    public final long getCreationTime() {
+        return getDelegate().getCreationTime();
+    }
+
+    @Override
+    public final long getLastAccessedTime() {
+        return getDelegate().getLastAccessedTime();
+    }
+
+    @Override
+    public final void invalidate() {
+        getDelegate().invalidate();
+    }
+
+    @Override
+    public final boolean isValid() {
+        return getDelegate().isValid();
+    }
+
+    @Override
+    public final void putValue(String s, Object o) {
+        getDelegate().putValue(s, o);
+    }
+
+    @Override
+    public final Object getValue(String s) {
+        return getDelegate().getValue(s);
+    }
+
+    @Override
+    public final void removeValue(String s) {
+        getDelegate().removeValue(s);
+    }
+
+    @Override
+    public final String[] getValueNames() {
+        return getDelegate().getValueNames();
+    }
+
+    @Override
+    public java.security.cert.X509Certificate[] getPeerCertificates()
+        throws SSLPeerUnverifiedException {
+        return getDelegate().getPeerCertificates();
+    }
+
+    @Override
+    public final Certificate[] getLocalCertificates() {
+        return getDelegate().getLocalCertificates();
+    }
+
+    @Override
+    public final X509Certificate[] getPeerCertificateChain() throws SSLPeerUnverifiedException {
+        return getDelegate().getPeerCertificateChain();
+    }
+
+    @Override
+    public final Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
+        return getDelegate().getPeerPrincipal();
+    }
+
+    @Override
+    public final Principal getLocalPrincipal() {
+        return getDelegate().getLocalPrincipal();
+    }
+
+    @Override
+    public final String getCipherSuite() {
+        return getDelegate().getCipherSuite();
+    }
+
+    @Override
+    public final String getProtocol() {
+        return getDelegate().getProtocol();
+    }
+
+    @Override
+    public final String getPeerHost() {
+        return getDelegate().getPeerHost();
+    }
+
+    @Override
+    public final int getPeerPort() {
+        return getDelegate().getPeerPort();
+    }
+
+    @Override
+    public final int getPacketBufferSize() {
+        return getDelegate().getPacketBufferSize();
+    }
+
+    @Override
+    public final int getApplicationBufferSize() {
+        return getDelegate().getApplicationBufferSize();
+    }
+}
diff --git a/common/src/main/java/org/conscrypt/Java7SessionWrapper.java b/common/src/main/java/org/conscrypt/Java7SessionWrapper.java
deleted file mode 100644
index 2883f88..0000000
--- a/common/src/main/java/org/conscrypt/Java7SessionWrapper.java
+++ /dev/null
@@ -1,200 +0,0 @@
-/*
- * Copyright 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.conscrypt;
-
-import java.lang.reflect.Constructor;
-import java.lang.reflect.InvocationTargetException;
-import java.security.Principal;
-import java.security.cert.Certificate;
-import java.util.Collections;
-import java.util.List;
-import javax.net.ssl.ExtendedSSLSession;
-import javax.net.ssl.SSLPeerUnverifiedException;
-import javax.net.ssl.SSLSession;
-import javax.net.ssl.SSLSessionContext;
-import javax.security.cert.X509Certificate;
-
-/**
- * This is an adapter that wraps the active session with {@link ExtendedSSLSession}, if running
- * on Java 7+.
- */
-final class Java7SessionWrapper extends ExtendedSSLSession {
-    // TODO: use BoringSSL API to actually fetch the real data
-    private static final String[] LOCAL_SUPPORTED_SIGNATURE_ALGORITHMS = new String[] {
-            "SHA512withRSA", "SHA512withECDSA", "SHA384withRSA", "SHA384withECDSA", "SHA256withRSA",
-            "SHA256withECDSA", "SHA224withRSA", "SHA224withECDSA", "SHA1withRSA", "SHA1withECDSA",
-    };
-    // TODO: use BoringSSL API to actually fetch the real data
-    private static final String[] PEER_SUPPORTED_SIGNATURE_ALGORITHMS =
-            new String[] {"SHA1withRSA", "SHA1withECDSA"};
-    // private static Class<?> EXTENDED_SSL_SESSION_CLASS = getExtendedSslSessionClass();
-    private final ActiveSession delegate;
-
-    Java7SessionWrapper(ActiveSession delegate) {
-        this.delegate = delegate;
-    }
-
-    /**
-     * If the given session is a wrapper, returns the delegate session.
-     */
-    static SSLSession getDelegate(SSLSession session) {
-        if (session instanceof Java7SessionWrapper) {
-            return ((Java7SessionWrapper) session).delegate;
-        }
-        return session;
-    }
-
-    /* @Override */
-    @SuppressWarnings("MissingOverride") // For Android backward-compatibility.
-    public String[] getLocalSupportedSignatureAlgorithms() {
-        return LOCAL_SUPPORTED_SIGNATURE_ALGORITHMS.clone();
-    }
-
-    /* @Override */
-    @SuppressWarnings("MissingOverride") // For Android backward-compatibility.
-    public String[] getPeerSupportedSignatureAlgorithms() {
-        return PEER_SUPPORTED_SIGNATURE_ALGORITHMS.clone();
-    }
-
-    /* @Override */
-    // For Android backward-compatibility.
-    @SuppressWarnings({"MissingOverride", "unchecked", "rawtypes"})
-    public List getRequestedServerNames() {
-        try {
-            String requestedServerName = delegate.getRequestedServerName();
-            if (requestedServerName == null) {
-                return null;
-            }
-
-            Constructor sniHostNameConstructor =
-                    Class.forName("javax.net.ssl.SNIHostName").getConstructor(String.class);
-            return Collections.singletonList(
-                    sniHostNameConstructor.newInstance(requestedServerName));
-
-        } catch (NoSuchMethodException ignore) {
-        } catch (InvocationTargetException ignore) {
-        } catch (IllegalAccessException ignore) {
-        } catch (ClassNotFoundException ignore) {
-        } catch (InstantiationException ignore) {
-        }
-        return null;
-    }
-
-    @Override
-    public byte[] getId() {
-        return delegate.getId();
-    }
-
-    @Override
-    public SSLSessionContext getSessionContext() {
-        return delegate.getSessionContext();
-    }
-
-    @Override
-    public long getCreationTime() {
-        return delegate.getCreationTime();
-    }
-
-    @Override
-    public long getLastAccessedTime() {
-        return delegate.getLastAccessedTime();
-    }
-
-    @Override
-    public void invalidate() {
-        delegate.invalidate();
-    }
-
-    @Override
-    public boolean isValid() {
-        return delegate.isValid();
-    }
-
-    @Override
-    public void putValue(String name, Object value) {
-        delegate.putValue(name, value);
-    }
-
-    @Override
-    public Object getValue(String name) {
-        return delegate.getValue(name);
-    }
-
-    @Override
-    public void removeValue(String name) {
-        delegate.removeValue(name);
-    }
-
-    @Override
-    public String[] getValueNames() {
-        return delegate.getValueNames();
-    }
-
-    @Override
-    public Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException {
-        return delegate.getPeerCertificates();
-    }
-
-    @Override
-    public Certificate[] getLocalCertificates() {
-        return delegate.getLocalCertificates();
-    }
-
-    @Override
-    public X509Certificate[] getPeerCertificateChain() throws SSLPeerUnverifiedException {
-        return delegate.getPeerCertificateChain();
-    }
-
-    @Override
-    public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
-        return delegate.getPeerPrincipal();
-    }
-
-    @Override
-    public Principal getLocalPrincipal() {
-        return delegate.getLocalPrincipal();
-    }
-
-    @Override
-    public String getCipherSuite() {
-        return delegate.getCipherSuite();
-    }
-
-    @Override
-    public String getProtocol() {
-        return delegate.getProtocol();
-    }
-
-    @Override
-    public String getPeerHost() {
-        return delegate.getPeerHost();
-    }
-
-    @Override
-    public int getPeerPort() {
-        return delegate.getPeerPort();
-    }
-
-    @Override
-    public int getPacketBufferSize() {
-        return delegate.getPacketBufferSize();
-    }
-
-    @Override
-    public int getApplicationBufferSize() {
-        return delegate.getApplicationBufferSize();
-    }
-}
diff --git a/common/src/main/java/org/conscrypt/Java8ExtendedSSLSession.java b/common/src/main/java/org/conscrypt/Java8ExtendedSSLSession.java
new file mode 100644
index 0000000..b5f93de
--- /dev/null
+++ b/common/src/main/java/org/conscrypt/Java8ExtendedSSLSession.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.conscrypt;
+
+import java.util.Collections;
+import java.util.List;
+import javax.net.ssl.ExtendedSSLSession;
+import javax.net.ssl.SNIHostName;
+import javax.net.ssl.SNIServerName;
+
+/**
+ * This is an adapter that wraps the active session with {@link ExtendedSSLSession}, if running
+ * on Java 8+.
+ */
+class Java8ExtendedSSLSession extends Java7ExtendedSSLSession {
+
+  public Java8ExtendedSSLSession(ConscryptSession delegate) {
+    super(delegate);
+  }
+
+  @Override
+  public final List<SNIServerName> getRequestedServerNames() {
+      String requestedServerName = getDelegate().getRequestedServerName();
+      if (requestedServerName == null) {
+        return null;
+      }
+
+      return Collections.singletonList((SNIServerName) new SNIHostName(requestedServerName));
+  }
+}
diff --git a/common/src/main/java/org/conscrypt/SslWrapper.java b/common/src/main/java/org/conscrypt/NativeSsl.java
similarity index 98%
rename from common/src/main/java/org/conscrypt/SslWrapper.java
rename to common/src/main/java/org/conscrypt/NativeSsl.java
index 2c5bca8..a76dcca 100644
--- a/common/src/main/java/org/conscrypt/SslWrapper.java
+++ b/common/src/main/java/org/conscrypt/NativeSsl.java
@@ -50,7 +50,7 @@
 /**
  * A utility wrapper that abstracts operations on the underlying native SSL instance.
  */
-final class SslWrapper {
+final class NativeSsl {
     private final SSLParametersImpl parameters;
     private final SSLHandshakeCallbacks handshakeCallbacks;
     private final AliasChooser aliasChooser;
@@ -58,15 +58,7 @@
     private X509Certificate[] localCertificates;
     private volatile long ssl;
 
-    static SslWrapper newInstance(SSLParametersImpl parameters,
-            SSLHandshakeCallbacks handshakeCallbacks, AliasChooser chooser,
-            PSKCallbacks pskCallbacks) throws SSLException {
-        long ctx = parameters.getSessionContext().sslCtxNativePointer;
-        long ssl = NativeCrypto.SSL_new(ctx);
-        return new SslWrapper(ssl, parameters, handshakeCallbacks, chooser, pskCallbacks);
-    }
-
-    private SslWrapper(long ssl, SSLParametersImpl parameters,
+    private NativeSsl(long ssl, SSLParametersImpl parameters,
             SSLHandshakeCallbacks handshakeCallbacks, AliasChooser aliasChooser,
             PSKCallbacks pskCallbacks) {
         this.ssl = ssl;
@@ -76,6 +68,14 @@
         this.pskCallbacks = pskCallbacks;
     }
 
+    static NativeSsl newInstance(SSLParametersImpl parameters,
+            SSLHandshakeCallbacks handshakeCallbacks, AliasChooser chooser,
+            PSKCallbacks pskCallbacks) throws SSLException {
+        long ctx = parameters.getSessionContext().sslCtxNativePointer;
+        long ssl = NativeCrypto.SSL_new(ctx);
+        return new NativeSsl(ssl, parameters, handshakeCallbacks, chooser, pskCallbacks);
+    }
+
     long ssl() {
         return ssl;
     }
diff --git a/common/src/main/java/org/conscrypt/SslSessionWrapper.java b/common/src/main/java/org/conscrypt/NativeSslSession.java
similarity index 93%
rename from common/src/main/java/org/conscrypt/SslSessionWrapper.java
rename to common/src/main/java/org/conscrypt/NativeSslSession.java
index 7ff1a54..1a3ab80 100644
--- a/common/src/main/java/org/conscrypt/SslSessionWrapper.java
+++ b/common/src/main/java/org/conscrypt/NativeSslSession.java
@@ -42,29 +42,29 @@
  *
  * This is abstract only to support mocking for tests.
  */
-abstract class SslSessionWrapper {
-    private static final Logger logger = Logger.getLogger(SslSessionWrapper.class.getName());
+abstract class NativeSslSession {
+    private static final Logger logger = Logger.getLogger(NativeSslSession.class.getName());
 
     /**
      * Creates a new instance. Since BoringSSL does not provide an API to get access to all
      * session information via the SSL_SESSION, we get some values (e.g. peer certs) from
-     * the active session instead (i.e. the SSL object).
+     * the {@link ConscryptSession} instead (i.e. the SSL object).
      */
-    static SslSessionWrapper newInstance(NativeRef.SSL_SESSION ref, ActiveSession activeSession)
+    static NativeSslSession newInstance(NativeRef.SSL_SESSION ref, ConscryptSession session)
             throws SSLPeerUnverifiedException {
-        AbstractSessionContext context = (AbstractSessionContext) activeSession.getSessionContext();
+        AbstractSessionContext context = (AbstractSessionContext) session.getSessionContext();
         if (context instanceof ClientSessionContext) {
-            return new Impl(context, ref, activeSession.getPeerHost(), activeSession.getPeerPort(),
-                    activeSession.getPeerCertificates(), getOcspResponse(activeSession),
-                    activeSession.getPeerSignedCertificateTimestamp());
+            return new Impl(context, ref, session.getPeerHost(), session.getPeerPort(),
+                session.getPeerCertificates(), getOcspResponse(session),
+                session.getPeerSignedCertificateTimestamp());
         }
 
         // Server's will be cached by ID and won't have any of the extra fields.
         return new Impl(context, ref, null, -1, null, null, null);
     }
 
-    private static byte[] getOcspResponse(ActiveSession activeSession) {
-        List<byte[]> ocspResponseList = activeSession.getStatusResponses();
+    private static byte[] getOcspResponse(ConscryptSession session) {
+        List<byte[]> ocspResponseList = session.getStatusResponses();
         if (ocspResponseList.size() >= 1) {
             return ocspResponseList.get(0);
         }
@@ -72,13 +72,13 @@
     }
 
     /**
-     * Creates a new {@link SslSessionWrapper} instance from the provided serialized bytes, which
+     * Creates a new {@link NativeSslSession} instance from the provided serialized bytes, which
      * were generated by {@link #toBytes()}.
      *
      * @return The new instance if successful. If unable to parse the bytes for any reason, returns
      * {@code null}.
      */
-    static SslSessionWrapper newInstance(
+    static NativeSslSession newInstance(
             AbstractSessionContext context, byte[] data, String host, int port) {
         ByteBuffer buf = ByteBuffer.wrap(data);
         try {
@@ -166,7 +166,7 @@
 
     abstract boolean isValid();
 
-    abstract void offerToResume(SslWrapper ssl) throws SSLException;
+    abstract void offerToResume(NativeSsl ssl) throws SSLException;
 
     abstract String getCipherSuite();
 
@@ -209,7 +209,7 @@
     /**
      * The session wrapper implementation.
      */
-    private static final class Impl extends SslSessionWrapper {
+    private static final class Impl extends NativeSslSession {
         private final NativeRef.SSL_SESSION ref;
 
         // BoringSSL offers no API to obtain these values directly from the SSL_SESSION.
@@ -258,7 +258,7 @@
         }
 
         @Override
-        void offerToResume(SslWrapper ssl) throws SSLException {
+        void offerToResume(NativeSsl ssl) throws SSLException {
             ssl.offerToResumeSession(ref.context);
         }
 
diff --git a/common/src/main/java/org/conscrypt/ProvidedSessionDecorator.java b/common/src/main/java/org/conscrypt/ProvidedSessionDecorator.java
new file mode 100644
index 0000000..e0257a0
--- /dev/null
+++ b/common/src/main/java/org/conscrypt/ProvidedSessionDecorator.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.conscrypt;
+
+import java.security.Principal;
+import java.security.cert.Certificate;
+import java.util.List;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSessionContext;
+import javax.security.cert.X509Certificate;
+
+/**
+ * A {@link SessionDecorator} that externalizes the provider of the delegate session. This allows
+ * the underlying session to be changed externally.
+ */
+final class ProvidedSessionDecorator implements SessionDecorator {
+
+  private final Provider provider;
+
+  public ProvidedSessionDecorator(Provider provider) {
+    this.provider = provider;
+  }
+
+  @Override
+  public ConscryptSession getDelegate() {
+    return provider.provideSession();
+  }
+
+  @Override
+  public String getRequestedServerName() {
+    return getDelegate().getRequestedServerName();
+  }
+
+  @Override
+  public List<byte[]> getStatusResponses() {
+    return getDelegate().getStatusResponses();
+  }
+
+  @Override
+  public byte[] getPeerSignedCertificateTimestamp() {
+    return getDelegate().getPeerSignedCertificateTimestamp();
+  }
+
+  @Override
+  public byte[] getId() {
+    return getDelegate().getId();
+  }
+
+  @Override
+  public SSLSessionContext getSessionContext() {
+    return getDelegate().getSessionContext();
+  }
+
+  @Override
+  public long getCreationTime() {
+    return getDelegate().getCreationTime();
+  }
+
+  @Override
+  public long getLastAccessedTime() {
+    return getDelegate().getLastAccessedTime();
+  }
+
+  @Override
+  public void invalidate() {
+    getDelegate().invalidate();
+  }
+
+  @Override
+  public boolean isValid() {
+    return getDelegate().isValid();
+  }
+
+  @Override
+  public void putValue(String s, Object o) {
+    getDelegate().putValue(s, o);
+  }
+
+  @Override
+  public Object getValue(String s) {
+    return getDelegate().getValue(s);
+  }
+
+  @Override
+  public void removeValue(String s) {
+    getDelegate().removeValue(s);
+  }
+
+  @Override
+  public String[] getValueNames() {
+    return getDelegate().getValueNames();
+  }
+
+  @Override
+  public java.security.cert.X509Certificate[] getPeerCertificates()
+      throws SSLPeerUnverifiedException {
+    return getDelegate().getPeerCertificates();
+  }
+
+  @Override
+  public Certificate[] getLocalCertificates() {
+    return getDelegate().getLocalCertificates();
+  }
+
+  @Override
+  public X509Certificate[] getPeerCertificateChain() throws SSLPeerUnverifiedException {
+    return getDelegate().getPeerCertificateChain();
+  }
+
+  @Override
+  public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
+    return getDelegate().getPeerPrincipal();
+  }
+
+  @Override
+  public Principal getLocalPrincipal() {
+    return getDelegate().getLocalPrincipal();
+  }
+
+  @Override
+  public String getCipherSuite() {
+    return getDelegate().getCipherSuite();
+  }
+
+  @Override
+  public String getProtocol() {
+    return getDelegate().getProtocol();
+  }
+
+  @Override
+  public String getPeerHost() {
+    return getDelegate().getPeerHost();
+  }
+
+  @Override
+  public int getPeerPort() {
+    return getDelegate().getPeerPort();
+  }
+
+  @Override
+  public int getPacketBufferSize() {
+    return getDelegate().getPacketBufferSize();
+  }
+
+  @Override
+  public int getApplicationBufferSize() {
+    return getDelegate().getApplicationBufferSize();
+  }
+
+  /**
+   * The provider of the current delegate session.
+   */
+  interface Provider {
+    ConscryptSession provideSession();
+  }
+}
diff --git a/common/src/main/java/org/conscrypt/SSLNullSession.java b/common/src/main/java/org/conscrypt/SSLNullSession.java
index 6289a98..34dc207 100644
--- a/common/src/main/java/org/conscrypt/SSLNullSession.java
+++ b/common/src/main/java/org/conscrypt/SSLNullSession.java
@@ -19,7 +19,10 @@
 
 import java.security.Principal;
 import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import javax.net.ssl.SSLPeerUnverifiedException;
 import javax.net.ssl.SSLSession;
 import javax.net.ssl.SSLSessionBindingEvent;
@@ -32,7 +35,7 @@
  * javax.net.ssl.SSLSocket#getSession()} before {@link javax.net.ssl.SSLSocket#startHandshake()} is
  * called.
  */
-final class SSLNullSession implements SSLSession, Cloneable {
+final class SSLNullSession implements ConscryptSession, Cloneable {
     static final String INVALID_CIPHER = "SSL_NULL_WITH_NULL_NULL";
 
     /*
@@ -48,12 +51,12 @@
     private long creationTime;
     private long lastAccessedTime;
 
-    static SSLSession getNullSession() {
+    static ConscryptSession getNullSession() {
         return DefaultHolder.NULL_SESSION;
     }
 
     static boolean isNullSession(SSLSession session) {
-        return session == DefaultHolder.NULL_SESSION;
+        return SSLUtils.unwrapSession(session) == DefaultHolder.NULL_SESSION;
     }
 
     private SSLNullSession() {
@@ -62,6 +65,21 @@
     }
 
     @Override
+    public String getRequestedServerName() {
+        return null;
+    }
+
+    @Override
+    public List<byte[]> getStatusResponses() {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public byte[] getPeerSignedCertificateTimestamp() {
+        return EmptyArray.BYTE;
+    }
+
+    @Override
     public int getApplicationBufferSize() {
         return NativeConstants.SSL3_RT_MAX_PLAIN_LENGTH;
     }
@@ -108,7 +126,7 @@
     }
 
     @Override
-    public Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException {
+    public X509Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException {
         throw new SSLPeerUnverifiedException("No peer certificate");
     }
 
diff --git a/common/src/main/java/org/conscrypt/SSLUtils.java b/common/src/main/java/org/conscrypt/SSLUtils.java
index 583ebcc..2f6a621 100644
--- a/common/src/main/java/org/conscrypt/SSLUtils.java
+++ b/common/src/main/java/org/conscrypt/SSLUtils.java
@@ -52,6 +52,7 @@
 import javax.net.ssl.SSLException;
 import javax.net.ssl.SSLHandshakeException;
 import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSession;
 import javax.security.cert.CertificateException;
 
 /**
@@ -177,6 +178,17 @@
     /** Key type: Elliptic Curve certificate. */
     private static final String KEY_TYPE_EC = "EC";
 
+    /**
+     * If the given session is a {@link SessionDecorator}, unwraps the session and returns the
+     * underlying (non-decorated) session. Otherwise, returns the provided session.
+     */
+    static SSLSession unwrapSession(SSLSession session) {
+        while (session instanceof SessionDecorator) {
+            session = ((SessionDecorator) session).getDelegate();
+        }
+        return session;
+    }
+
     static X509Certificate[] decodeX509CertificateChain(byte[][] certChain)
             throws java.security.cert.CertificateException {
         CertificateFactory certificateFactory = getCertificateFactory();
diff --git a/common/src/main/java/org/conscrypt/ServerSessionContext.java b/common/src/main/java/org/conscrypt/ServerSessionContext.java
index 331f2b2..4d144f6 100644
--- a/common/src/main/java/org/conscrypt/ServerSessionContext.java
+++ b/common/src/main/java/org/conscrypt/ServerSessionContext.java
@@ -56,11 +56,11 @@
     }
 
     @Override
-    SslSessionWrapper getSessionFromPersistentCache(byte[] sessionId) {
+    NativeSslSession getSessionFromPersistentCache(byte[] sessionId) {
         if (persistentCache != null) {
             byte[] data = persistentCache.getSessionData(sessionId);
             if (data != null) {
-                SslSessionWrapper session = SslSessionWrapper.newInstance(this, data, null, -1);
+                NativeSslSession session = NativeSslSession.newInstance(this, data, null, -1);
                 if (session != null && session.isValid()) {
                     cacheSession(session);
                     return session;
@@ -72,7 +72,7 @@
     }
 
     @Override
-    void onBeforeAddSession(SslSessionWrapper session) {
+    void onBeforeAddSession(NativeSslSession session) {
         // TODO: Do this in background thread.
         if (persistentCache != null) {
             byte[] data = session.toBytes();
@@ -83,7 +83,7 @@
     }
 
     @Override
-    void onBeforeRemoveSession(SslSessionWrapper session) {
+    void onBeforeRemoveSession(NativeSslSession session) {
         // Do nothing.
     }
 }
diff --git a/common/src/main/java/org/conscrypt/SessionDecorator.java b/common/src/main/java/org/conscrypt/SessionDecorator.java
new file mode 100644
index 0000000..e36e1b8
--- /dev/null
+++ b/common/src/main/java/org/conscrypt/SessionDecorator.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.conscrypt;
+
+/**
+ * A session that decorates another {@link ConscryptSession}.
+ */
+interface SessionDecorator extends ConscryptSession {
+
+  /**
+   * Gets the underlying {@link ConscryptSession}.
+   */
+  ConscryptSession getDelegate();
+}
diff --git a/common/src/main/java/org/conscrypt/SessionSnapshot.java b/common/src/main/java/org/conscrypt/SessionSnapshot.java
new file mode 100644
index 0000000..5ae755a
--- /dev/null
+++ b/common/src/main/java/org/conscrypt/SessionSnapshot.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.conscrypt;
+
+import java.security.Principal;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.List;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSessionContext;
+
+/**
+ * A snapshot of the content of another {@link ConscryptSession}. This copies everything over
+ * except for the certificates.
+ */
+final class SessionSnapshot implements ConscryptSession {
+    private final SSLSessionContext sessionContext;
+    private final byte[] id;
+    private final String requestedServerName;
+    private final List<byte[]> statusResponses;
+    private final byte[] peerTlsSctData;
+    private final long creationTime;
+    private final long lastAccessedTime;
+    private final String cipherSuite;
+    private final String protocol;
+    private final String peerHost;
+    private final int peerPort;
+
+    SessionSnapshot(ConscryptSession session) {
+        sessionContext = session.getSessionContext();
+        id = session.getId();
+        requestedServerName = session.getRequestedServerName();
+        statusResponses = session.getStatusResponses();
+        peerTlsSctData = session.getPeerSignedCertificateTimestamp();
+        creationTime = session.getCreationTime();
+        lastAccessedTime = session.getLastAccessedTime();
+        cipherSuite = session.getCipherSuite();
+        protocol = session.getProtocol();
+        peerHost = session.getPeerHost();
+        peerPort = session.getPeerPort();
+    }
+
+    @Override
+    public String getRequestedServerName() {
+        return requestedServerName;
+    }
+
+    @Override
+    public List<byte[]> getStatusResponses() {
+        List<byte[]> ret = new ArrayList<byte[]>(statusResponses.size());
+        for (byte[] resp : statusResponses) {
+            ret.add(resp.clone());
+        }
+        return ret;
+    }
+
+    @Override
+    public byte[] getPeerSignedCertificateTimestamp() {
+        return peerTlsSctData != null ? peerTlsSctData.clone() : null;
+    }
+
+    @Override
+    public byte[] getId() {
+        return id;
+    }
+
+    @Override
+    public SSLSessionContext getSessionContext() {
+        return sessionContext;
+    }
+
+    @Override
+    public long getCreationTime() {
+        return creationTime;
+    }
+
+    @Override
+    public long getLastAccessedTime() {
+        return lastAccessedTime;
+    }
+
+    @Override
+    public void invalidate() {
+        // Do nothing.
+    }
+
+    @Override
+    public boolean isValid() {
+        return false;
+    }
+
+    @Override
+    public void putValue(String s, Object o) {
+        // Do nothing.
+    }
+
+    @Override
+    public Object getValue(String s) {
+        return null;
+    }
+
+    @Override
+    public void removeValue(String s) {
+        // Do nothing.
+    }
+
+    @Override
+    public String[] getValueNames() {
+        return EmptyArray.STRING;
+    }
+
+    @Override
+    public X509Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException {
+        throw new SSLPeerUnverifiedException("No peer certificates");
+    }
+
+    @Override
+    public Certificate[] getLocalCertificates() {
+        return null;
+    }
+
+    @Override
+    public javax.security.cert.X509Certificate[] getPeerCertificateChain()
+        throws SSLPeerUnverifiedException {
+        throw new SSLPeerUnverifiedException("No peer certificates");
+    }
+
+    @Override
+    public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
+        throw new SSLPeerUnverifiedException("No peer certificates");
+    }
+
+    @Override
+    public Principal getLocalPrincipal() {
+        return null;
+    }
+
+    @Override
+    public String getCipherSuite() {
+        return cipherSuite;
+    }
+
+    @Override
+    public String getProtocol() {
+        return protocol;
+    }
+
+    @Override
+    public String getPeerHost() {
+        return peerHost;
+    }
+
+    @Override
+    public int getPeerPort() {
+        return peerPort;
+    }
+
+    @Override
+    public int getPacketBufferSize() {
+        return NativeConstants.SSL3_RT_MAX_PACKET_SIZE;
+    }
+
+    @Override
+    public int getApplicationBufferSize() {
+        return NativeConstants.SSL3_RT_MAX_PLAIN_LENGTH;
+    }
+}
diff --git a/openjdk/src/main/java/org/conscrypt/Java7PlatformUtil.java b/openjdk/src/main/java/org/conscrypt/Java7PlatformUtil.java
index 2e43e19..f2eed33 100644
--- a/openjdk/src/main/java/org/conscrypt/Java7PlatformUtil.java
+++ b/openjdk/src/main/java/org/conscrypt/Java7PlatformUtil.java
@@ -121,12 +121,8 @@
         return file.canExecute();
     }
 
-    static SSLSession wrapSSLSession(ActiveSession sslSession) {
-        return new Java7SessionWrapper(sslSession);
-    }
-
-    static SSLSession unwrapSSLSession(SSLSession sslSession) {
-        return Java7SessionWrapper.getDelegate(sslSession);
+    static SSLSession wrapSSLSession(ConscryptSession sslSession) {
+        return new Java7ExtendedSSLSession(sslSession);
     }
 
     private Java7PlatformUtil() {}
diff --git a/openjdk/src/main/java/org/conscrypt/Java8PlatformUtil.java b/openjdk/src/main/java/org/conscrypt/Java8PlatformUtil.java
index 297d782..ebb8e67 100644
--- a/openjdk/src/main/java/org/conscrypt/Java8PlatformUtil.java
+++ b/openjdk/src/main/java/org/conscrypt/Java8PlatformUtil.java
@@ -24,6 +24,7 @@
 import javax.net.ssl.SNIServerName;
 import javax.net.ssl.SSLEngine;
 import javax.net.ssl.SSLParameters;
+import javax.net.ssl.SSLSession;
 
 /**
  * Utility methods supported on Java 8+.
@@ -88,5 +89,9 @@
         return Java8EngineWrapper.getDelegate(engine);
     }
 
+    static SSLSession wrapSSLSession(ConscryptSession sslSession) {
+        return new Java8ExtendedSSLSession(sslSession);
+    }
+
     private Java8PlatformUtil() {}
 }
diff --git a/openjdk/src/main/java/org/conscrypt/Platform.java b/openjdk/src/main/java/org/conscrypt/Platform.java
index 23ad56c..4ed0dfb 100644
--- a/openjdk/src/main/java/org/conscrypt/Platform.java
+++ b/openjdk/src/main/java/org/conscrypt/Platform.java
@@ -529,21 +529,16 @@
      */
 
     @SuppressWarnings("unused")
-    static SSLSession wrapSSLSession(ActiveSession sslSession) {
+    static SSLSession wrapSSLSession(ConscryptSession sslSession) {
+        if (JAVA_VERSION >= 8) {
+            return Java8PlatformUtil.wrapSSLSession(sslSession);
+        }
         if (JAVA_VERSION >= 7) {
             return Java7PlatformUtil.wrapSSLSession(sslSession);
         }
         return sslSession;
     }
 
-    @SuppressWarnings("unused")
-    static SSLSession unwrapSSLSession(SSLSession sslSession) {
-        if (JAVA_VERSION >= 7) {
-            return Java7PlatformUtil.unwrapSSLSession(sslSession);
-        }
-        return sslSession;
-    }
-
     public static String getOriginalHostNameFromInetAddress(InetAddress addr) {
         try {
             Method getHolder = InetAddress.class.getDeclaredMethod("holder");
diff --git a/openjdk/src/test/java/org/conscrypt/AbstractSessionContextTest.java b/openjdk/src/test/java/org/conscrypt/AbstractSessionContextTest.java
index 1cda6f9..68b7f14 100644
--- a/openjdk/src/test/java/org/conscrypt/AbstractSessionContextTest.java
+++ b/openjdk/src/test/java/org/conscrypt/AbstractSessionContextTest.java
@@ -40,12 +40,16 @@
 
     abstract T newContext();
     abstract int size(T context);
-    abstract SslSessionWrapper getCachedSession(T context, SslSessionWrapper s);
+    private static NativeSslSession[] toArray(NativeSslSession... sessions) {
+        return sessions;
+    }
+
+    abstract NativeSslSession getCachedSession(T context, NativeSslSession s);
 
     @Test
     public void testSimpleAddition() {
-        SslSessionWrapper a = newSession("a");
-        SslSessionWrapper b = newSession("b");
+        NativeSslSession a = newSession("a");
+        NativeSslSession b = newSession("b");
 
         context.cacheSession(a);
         assertSessionContextContents(toArray(a), toArray(b));
@@ -56,10 +60,10 @@
 
     @Test
     public void testTrimToSize() {
-        SslSessionWrapper a = newSession("a");
-        SslSessionWrapper b = newSession("b");
-        SslSessionWrapper c = newSession("c");
-        SslSessionWrapper d = newSession("d");
+        NativeSslSession a = newSession("a");
+        NativeSslSession b = newSession("b");
+        NativeSslSession c = newSession("c");
+        NativeSslSession d = newSession("d");
 
         context.cacheSession(a);
         context.cacheSession(b);
@@ -74,10 +78,10 @@
     @Test
     public void testImplicitRemovalOfOldest() {
         context.setSessionCacheSize(2);
-        SslSessionWrapper a = newSession("a");
-        SslSessionWrapper b = newSession("b");
-        SslSessionWrapper c = newSession("c");
-        SslSessionWrapper d = newSession("d");
+        NativeSslSession a = newSession("a");
+        NativeSslSession b = newSession("b");
+        NativeSslSession c = newSession("c");
+        NativeSslSession d = newSession("d");
 
         context.cacheSession(a);
         assertSessionContextContents(toArray(a), toArray(b, c, d));
@@ -98,7 +102,7 @@
         when(mockCert.getEncoded()).thenReturn(new byte[] {0x05, 0x06, 0x07, 0x10});
 
         byte[] encodedBytes = new byte[] {0x01, 0x02, 0x03};
-        SslSessionWrapper session = new MockSessionBuilder()
+        NativeSslSession session = new MockSessionBuilder()
                 .id(new byte[] {0x11, 0x09, 0x03, 0x20})
                 .host("ssl.example.com")
                 .encodedBytes(encodedBytes)
@@ -113,22 +117,18 @@
     }
 
     private void assertSessionContextContents(
-            SslSessionWrapper[] contains, SslSessionWrapper[] exludes) {
+            NativeSslSession[] contains, NativeSslSession[] exludes) {
         assertEquals(contains.length, size(context));
 
-        for (SslSessionWrapper s : contains) {
+        for (NativeSslSession s : contains) {
             assertSame(s.getPeerHost(), s, getCachedSession(context, s));
         }
-        for (SslSessionWrapper s : exludes) {
+        for (NativeSslSession s : exludes) {
             assertNull(s.getPeerHost(), getCachedSession(context, s));
         }
     }
 
-    private static SslSessionWrapper[] toArray(SslSessionWrapper... sessions) {
-        return sessions;
-    }
-
-    private SslSessionWrapper newSession(String host) {
+    private NativeSslSession newSession(String host) {
         return new MockSessionBuilder().host(host).build();
     }
 }
diff --git a/openjdk/src/test/java/org/conscrypt/ClientSessionContextTest.java b/openjdk/src/test/java/org/conscrypt/ClientSessionContextTest.java
index 709b893..6a20740 100644
--- a/openjdk/src/test/java/org/conscrypt/ClientSessionContextTest.java
+++ b/openjdk/src/test/java/org/conscrypt/ClientSessionContextTest.java
@@ -31,7 +31,7 @@
     }
 
     @Override
-    SslSessionWrapper getCachedSession(ClientSessionContext context, SslSessionWrapper s) {
+    NativeSslSession getCachedSession(ClientSessionContext context, NativeSslSession s) {
         return context.getCachedSession(s.getPeerHost(), DEFAULT_PORT,
                 getDefaultSSLParameters());
     }
diff --git a/openjdk/src/test/java/org/conscrypt/ConscryptEngineTest.java b/openjdk/src/test/java/org/conscrypt/ConscryptEngineTest.java
index d536830..71ccff7 100644
--- a/openjdk/src/test/java/org/conscrypt/ConscryptEngineTest.java
+++ b/openjdk/src/test/java/org/conscrypt/ConscryptEngineTest.java
@@ -44,6 +44,7 @@
 import javax.net.ssl.SSLEngineResult.Status;
 import javax.net.ssl.SSLException;
 import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLSession;
 import libcore.java.security.TestKeyStore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -360,6 +361,20 @@
         exchangeMessage(newMessage(MESSAGE_SIZE), serverEngine, clientEngine);
     }
 
+    @Test
+    public void savedSessionWorksAfterClose() throws Exception {
+        setupEngines(TestKeyStore.getClient(), TestKeyStore.getServer());
+        doHandshake(true);
+
+        SSLSession session = clientEngine.getSession();
+        String cipherSuite = session.getCipherSuite();
+
+        clientEngine.closeOutbound();
+        clientEngine.closeInbound();
+
+        assertEquals(cipherSuite, session.getCipherSuite());
+    }
+
     private void doMutualAuthHandshake(
             TestKeyStore clientKs, TestKeyStore serverKs, ClientAuth clientAuth) throws Exception {
         setupEngines(clientKs, serverKs);
diff --git a/openjdk/src/test/java/org/conscrypt/ConscryptSocketTest.java b/openjdk/src/test/java/org/conscrypt/ConscryptSocketTest.java
index cffb5a4..385c58c 100644
--- a/openjdk/src/test/java/org/conscrypt/ConscryptSocketTest.java
+++ b/openjdk/src/test/java/org/conscrypt/ConscryptSocketTest.java
@@ -48,6 +48,7 @@
 import javax.net.ssl.KeyManager;
 import javax.net.ssl.KeyManagerFactory;
 import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLSession;
 import javax.net.ssl.SSLSocket;
 import javax.net.ssl.SSLSocketFactory;
 import javax.net.ssl.TrustManager;
@@ -610,6 +611,19 @@
         assertFalse(connection.serverHooks.isHandshakeCompleted);
     }
 
+    @Test
+    public void savedSessionWorksAfterClose() throws Exception {
+        TestConnection connection = new TestConnection(new X509Certificate[] {cert, ca}, certKey);
+        connection.doHandshake();
+
+        SSLSession session = connection.client.getSession();
+        String cipherSuite = session.getCipherSuite();
+
+        connection.client.close();
+
+        assertEquals(cipherSuite, session.getCipherSuite());
+    }
+
     private static ServerSocket newServerSocket() throws IOException {
         return new ServerSocket(0, 50, TestUtils.getLoopbackAddress());
     }
diff --git a/openjdk/src/test/java/org/conscrypt/MockSessionBuilder.java b/openjdk/src/test/java/org/conscrypt/MockSessionBuilder.java
index 7cff17a..4883bac 100644
--- a/openjdk/src/test/java/org/conscrypt/MockSessionBuilder.java
+++ b/openjdk/src/test/java/org/conscrypt/MockSessionBuilder.java
@@ -63,8 +63,8 @@
         return this;
     }
 
-    SslSessionWrapper build() {
-        SslSessionWrapper session = mock(SslSessionWrapper.class);
+    NativeSslSession build() {
+        NativeSslSession session = mock(NativeSslSession.class);
         byte[] id = this.id == null ? host.getBytes(UTF_8) : this.id;
         when(session.getId()).thenReturn(id);
         when(session.isValid()).thenReturn(valid);
diff --git a/openjdk/src/test/java/org/conscrypt/SslSessionWrapperTest.java b/openjdk/src/test/java/org/conscrypt/NativeSslSessionTest.java
similarity index 98%
rename from openjdk/src/test/java/org/conscrypt/SslSessionWrapperTest.java
rename to openjdk/src/test/java/org/conscrypt/NativeSslSessionTest.java
index dc600d4..c308d71 100644
--- a/openjdk/src/test/java/org/conscrypt/SslSessionWrapperTest.java
+++ b/openjdk/src/test/java/org/conscrypt/NativeSslSessionTest.java
@@ -28,7 +28,7 @@
 import org.junit.runners.JUnit4;
 
 @RunWith(JUnit4.class)
-public class SslSessionWrapperTest {
+public class NativeSslSessionTest {
     /*
      * Taken from external/boringssl/src/ssl/ssl_test.cc: kOpenSSLSession is a
      * serialized SSL_SESSION.
@@ -433,14 +433,8 @@
         assertValidSession(getType3().build());
     }
 
-    private void assertTruncatedSessionFails(byte[] validSession) {
-        for (int i = 0; i < validSession.length - 1; i++) {
-            byte[] truncatedSession = new byte[i];
-            System.arraycopy(validSession, 0, truncatedSession, 0, i);
-            assertNull("Truncating to " + i + " bytes of " + validSession.length
-                            + " should not succeed",
-                    SslSessionWrapper.newInstance(null, truncatedSession, "www.google.com", 443));
-        }
+    private static void assertValidSession(byte[] data) {
+        assertNotNull(NativeSslSession.newInstance(null, data, "www.google.com", 443));
     }
 
     @Test
@@ -448,12 +442,31 @@
         assertTruncatedSessionFails(getType3().build());
     }
 
-    private static void assertValidSession(byte[] data) {
-        assertNotNull(SslSessionWrapper.newInstance(null, data, "www.google.com", 443));
+    private static void assertInvalidSession(byte[] data) {
+        assertNull(NativeSslSession.newInstance(null, data, "www.google.com", 443));
     }
 
-    private static void assertInvalidSession(byte[] data) {
-        assertNull(SslSessionWrapper.newInstance(null, data, "www.google.com", 443));
+    private static void check_reserializableFromByteArray_roundTrip(
+            byte[] data, byte[] expectedTrailingBytesAfterReserialization) throws Exception {
+        NativeSslSession session =
+                NativeSslSession.newInstance(null, data, "www.example.com", 12345);
+        byte[] sessionBytes = session.toBytes();
+
+        NativeSslSession session2 =
+                NativeSslSession.newInstance(null, sessionBytes, "www.example.com", 12345);
+        byte[] sessionBytes2 = session2.toBytes();
+
+        assertSSLSessionEquals(session, session2);
+        assertByteArrayEquals(sessionBytes, sessionBytes2);
+
+        assertEquals("www.example.com", session.getPeerHost());
+        assertEquals(12345, session.getPeerPort());
+        assertTrue(sessionBytes.length >= data.length);
+
+        byte[] expectedReserializedData = concat(data, expectedTrailingBytesAfterReserialization);
+        // AbstractSessionContext.toBytes() always writes type 3 == OPEN_SSL_WITH_TLS_SCT
+        expectedReserializedData[3] = 3;
+        assertByteArrayEquals(expectedReserializedData, sessionBytes);
     }
 
     @Test
@@ -581,27 +594,13 @@
         check_reserializableFromByteArray_roundTrip(getType3().build(), new byte[0]);
     }
 
-    private static void check_reserializableFromByteArray_roundTrip(
-            byte[] data, byte[] expectedTrailingBytesAfterReserialization) throws Exception {
-        SslSessionWrapper session =
-                SslSessionWrapper.newInstance(null, data, "www.example.com", 12345);
-        byte[] sessionBytes = session.toBytes();
-
-        SslSessionWrapper session2 =
-                SslSessionWrapper.newInstance(null, sessionBytes, "www.example.com", 12345);
-        byte[] sessionBytes2 = session2.toBytes();
-
-        assertSSLSessionEquals(session, session2);
-        assertByteArrayEquals(sessionBytes, sessionBytes2);
-
-        assertEquals("www.example.com", session.getPeerHost());
-        assertEquals(12345, session.getPeerPort());
-        assertTrue(sessionBytes.length >= data.length);
-
-        byte[] expectedReserializedData = concat(data, expectedTrailingBytesAfterReserialization);
-        // AbstractSessionContext.toBytes() always writes type 3 == OPEN_SSL_WITH_TLS_SCT
-        expectedReserializedData[3] = 3;
-        assertByteArrayEquals(expectedReserializedData, sessionBytes);
+    private static void assertSSLSessionEquals(NativeSslSession a, NativeSslSession b)
+            throws Exception {
+        assertEquals(a.getCipherSuite(), b.getCipherSuite());
+        assertByteArrayEquals(a.getId(), b.getId());
+        assertEquals(a.getPeerHost(), b.getPeerHost());
+        assertEquals(a.getPeerPort(), b.getPeerPort());
+        assertEquals(a.getProtocol(), b.getProtocol());
     }
 
     private static byte[] concat(byte[] a, byte[] b) {
@@ -611,13 +610,14 @@
         return result;
     }
 
-    private static void assertSSLSessionEquals(SslSessionWrapper a, SslSessionWrapper b)
-            throws Exception {
-        assertEquals(a.getCipherSuite(), b.getCipherSuite());
-        assertByteArrayEquals(a.getId(), b.getId());
-        assertEquals(a.getPeerHost(), b.getPeerHost());
-        assertEquals(a.getPeerPort(), b.getPeerPort());
-        assertEquals(a.getProtocol(), b.getProtocol());
+    private void assertTruncatedSessionFails(byte[] validSession) {
+        for (int i = 0; i < validSession.length - 1; i++) {
+            byte[] truncatedSession = new byte[i];
+            System.arraycopy(validSession, 0, truncatedSession, 0, i);
+            assertNull("Truncating to " + i + " bytes of " + validSession.length
+                            + " should not succeed",
+                    NativeSslSession.newInstance(null, truncatedSession, "www.google.com", 443));
+        }
     }
 
     private static void assertByteArrayEquals(byte[] expected, byte[] actual) {
diff --git a/openjdk/src/test/java/org/conscrypt/ServerSessionContextTest.java b/openjdk/src/test/java/org/conscrypt/ServerSessionContextTest.java
index 7d1a8ce..1b1ed0b 100644
--- a/openjdk/src/test/java/org/conscrypt/ServerSessionContextTest.java
+++ b/openjdk/src/test/java/org/conscrypt/ServerSessionContextTest.java
@@ -29,7 +29,7 @@
     }
 
     @Override
-    SslSessionWrapper getCachedSession(ServerSessionContext context, SslSessionWrapper s) {
+    NativeSslSession getCachedSession(ServerSessionContext context, NativeSslSession s) {
         return context.getSessionFromCache(s.getId());
     }
 
diff --git a/platform/src/main/java/org/conscrypt/Platform.java b/platform/src/main/java/org/conscrypt/Platform.java
index d16393f..3c76ef8 100644
--- a/platform/src/main/java/org/conscrypt/Platform.java
+++ b/platform/src/main/java/org/conscrypt/Platform.java
@@ -426,16 +426,11 @@
         }
     }
 
-    /*
-     * Pre-Java 8 backward compatibility.
+    /**
+     * Provides extended capabilities for the session if supported by the platform.
      */
-
-    static SSLSession wrapSSLSession(ActiveSession sslSession) {
-        return new Java7SessionWrapper(sslSession);
-    }
-
-    static SSLSession unwrapSSLSession(SSLSession sslSession) {
-        return Java7SessionWrapper.getDelegate(sslSession);
+    static SSLSession wrapSSLSession(ConscryptSession sslSession) {
+        return new Java8ExtendedSSLSession(sslSession);
     }
 
     public static String getOriginalHostNameFromInetAddress(InetAddress addr) {
diff --git a/platform/src/main/java/org/conscrypt/TrustManagerImpl.java b/platform/src/main/java/org/conscrypt/TrustManagerImpl.java
index 95f7f48..7ef4d07 100644
--- a/platform/src/main/java/org/conscrypt/TrustManagerImpl.java
+++ b/platform/src/main/java/org/conscrypt/TrustManagerImpl.java
@@ -420,8 +420,8 @@
 
     private byte[] getOcspDataFromSession(SSLSession session) {
         List<byte[]> ocspResponses = null;
-        if (session instanceof ActiveSession) {
-            ActiveSession opensslSession = (ActiveSession) session;
+        if (session instanceof ConscryptSession) {
+            ConscryptSession opensslSession = (ConscryptSession) session;
             ocspResponses = opensslSession.getStatusResponses();
         } else {
             Method m_getResponses;
@@ -447,8 +447,8 @@
     }
 
     private byte[] getTlsSctDataFromSession(SSLSession session) {
-        if (session instanceof ActiveSession) {
-            ActiveSession opensslSession = (ActiveSession) session;
+        if (session instanceof ConscryptSession) {
+            ConscryptSession opensslSession = (ConscryptSession) session;
             return opensslSession.getPeerSignedCertificateTimestamp();
         }
 
diff --git a/test_logging.properties b/test_logging.properties
index 89be04d..1204c2d 100644
--- a/test_logging.properties
+++ b/test_logging.properties
@@ -8,4 +8,4 @@
 org.conscrypt.handler=java.util.logging.ConsoleHandler
 
 # Avoid nuisance logs in tests.
-org.conscrypt.SslSessionWrapper.level=SEVERE
+org.conscrypt.NativeSslSession.level=SEVERE