// Copyright 2009 The Android Open Source Project
package android.core;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
import android.content.ContentValues;
import android.content.Context;
import org.apache.commons.codec.binary.Base64;
import org.apache.harmony.xnet.provider.jsse.SSLClientSessionCache;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.net.ssl.SSLSession;
/**
* Hook into harmony SSL cache to persist the SSL sessions.
*
* Current implementation is suitable for saving a small number of hosts -
* like google services. It can be extended with expiration and more features
* to support more hosts.
*
* {@hide}
*/
public class DatabaseSessionCache implements SSLClientSessionCache {
private static final String TAG = "SslSessionCache";
static DatabaseHelper sDefaultDatabaseHelper;
private DatabaseHelper mDatabaseHelper;
/**
* Table where sessions are stored.
*/
public static final String SSL_CACHE_TABLE = "ssl_sessions";
private static final String SSL_CACHE_ID = "_id";
/**
* Key is host:port - port is not optional.
*/
private static final String SSL_CACHE_HOSTPORT = "hostport";
/**
* Base64-encoded DER value of the session.
*/
private static final String SSL_CACHE_SESSION = "session";
/**
* Time when the record was added - should be close to the time
* of the initial session negotiation.
*/
private static final String SSL_CACHE_TIME_SEC = "time_sec";
public static final String DATABASE_NAME = "ssl_sessions.db";
public static final int DATABASE_VERSION = 1;
/** public for testing
*/
public static final int SSL_CACHE_ID_COL = 0;
public static final int SSL_CACHE_HOSTPORT_COL = 1;
public static final int SSL_CACHE_SESSION_COL = 2;
public static final int SSL_CACHE_TIME_SEC_COL = 3;
private static final String SAVE_ON_ADD = "save_on_add";
static boolean sHookInitializationDone = false;
public static final int MAX_CACHE_SIZE = 256;
private static final Map<String, byte[]> mExternalCache =
new LinkedHashMap<String, byte[]>(MAX_CACHE_SIZE, 0.75f, true) {
@Override
public boolean removeEldestEntry(
Map.Entry<String, byte[]> eldest) {
boolean shouldDelete = this.size() > MAX_CACHE_SIZE;
// TODO: delete from DB
return shouldDelete;
}
};
static boolean mNeedsCacheLoad = true;
public static final String[] PROJECTION = new String[] {
SSL_CACHE_ID,
SSL_CACHE_HOSTPORT,
SSL_CACHE_SESSION,
SSL_CACHE_TIME_SEC
};
/**
* This class needs to be installed as a hook, if the security property
* is set. Getting the right classloader may be fun since we don't use
* Provider to get its classloader, but in android this is in same
* loader with AndroidHttpClient.
*
* This constructor will use the default database. You must
* call init() before to specify the context used for the database and
* check settings.
*/
public DatabaseSessionCache() {
Log.v(TAG, "Instance created.");
// May be null if caching is disabled - no sessions will be persisted.
this.mDatabaseHelper = sDefaultDatabaseHelper;
}
/**
* Create a SslSessionCache instance, using the specified context to
* initialize the database.
*
* This constructor will use the default database - created the first
* time.
*
* @param activityContext
*/
public DatabaseSessionCache(Context activityContext) {
// Static init - only one initialization will happen.
// Each SslSessionCache is using the same DB.
init(activityContext);
// May be null if caching is disabled - no sessions will be persisted.
this.mDatabaseHelper = sDefaultDatabaseHelper;
}
/**
* Create a SslSessionCache that uses a specific database.
*
* @param database
*/
public DatabaseSessionCache(DatabaseHelper database) {
this.mDatabaseHelper = database;
}
// public static boolean enabled(Context androidContext) {
// String sslCache = Settings.Gservices.getString(androidContext.getContentResolver(),
// Settings.Gservices.SSL_SESSION_CACHE);
//
// if (Log.isLoggable(TAG, Log.DEBUG)) {
// Log.d(TAG, "enabled " + sslCache + " " + androidContext.getPackageName());
// }
//
// return SAVE_ON_ADD.equals(sslCache);
// }
/**
* You must call this method to enable SSL session caching for an app.
*/
public synchronized static void init(Context activityContext) {
// It is possible that multiple provider will try to install this hook.
// We want a single db per VM.
if (sHookInitializationDone) {
return;
}
// // More values can be added in future to provide different
// // behaviours, like 'batch save'.
// if (enabled(activityContext)) {
Context appContext = activityContext.getApplicationContext();
sDefaultDatabaseHelper = new DatabaseHelper(appContext);
// Set default SSLSocketFactory
// The property is defined in the javadocs for javax.net.SSLSocketFactory
// (no constant defined there)
// This should cover all code using SSLSocketFactory.getDefault(),
// including native http client and apache httpclient.
// MCS is using its own custom factory - will need special code.
// Security.setProperty("ssl.SocketFactory.provider",
// SslSocketFactoryWithCache.class.getName());
// }
// Won't try again.
sHookInitializationDone = true;
}
public void putSessionData(SSLSession session, byte[] der) {
if (mDatabaseHelper == null) {
return;
}
if (mExternalCache.size() > MAX_CACHE_SIZE) {
// remove oldest.
Cursor byTime = mDatabaseHelper.getWritableDatabase().query(SSL_CACHE_TABLE,
PROJECTION, null, null, null, null, SSL_CACHE_TIME_SEC);
byTime.moveToFirst();
// TODO: can I do byTime.deleteRow() ?
String hostPort = byTime.getString(SSL_CACHE_HOSTPORT_COL);
mDatabaseHelper.getWritableDatabase().delete(SSL_CACHE_TABLE,
SSL_CACHE_HOSTPORT + "= ?" , new String[] { hostPort });
}
// Serialize native session to standard DER encoding
long t0 = System.currentTimeMillis();
String b64 = new String(Base64.encodeBase64(der));
String key = session.getPeerHost() + ":" + session.getPeerPort();
ContentValues values = new ContentValues();
values.put(SSL_CACHE_HOSTPORT, key);
values.put(SSL_CACHE_SESSION, b64);
values.put(SSL_CACHE_TIME_SEC, System.currentTimeMillis() / 1000);
synchronized (this.getClass()) {
mExternalCache.put(key, der);
try {
mDatabaseHelper.getWritableDatabase().insert(SSL_CACHE_TABLE, null /*nullColumnHack */ , values);
} catch(SQLException ex) {
// Ignore - nothing we can do to recover, and caller shouldn't
// be affected.
Log.w(TAG, "Ignoring SQL exception when caching session", ex);
}
}
if (Log.isLoggable(TAG, Log.DEBUG)) {
long t1 = System.currentTimeMillis();
Log.d(TAG, "New SSL session " + session.getPeerHost() +
" DER len: " + der.length + " " + (t1 - t0));
}
}
public byte[] getSessionData(String host, int port) {
// Current (simple) implementation does a single lookup to DB, then saves
// all entries to the cache.
// This works for google services - i.e. small number of certs.
// If we extend this to all processes - we should hold a separate cache
// or do lookups to DB each time.
if (mDatabaseHelper == null) {
return null;
}
synchronized(this.getClass()) {
if (mNeedsCacheLoad) {
// Don't try to load again, if something is wrong on the first
// request it'll likely be wrong each time.
mNeedsCacheLoad = false;
long t0 = System.currentTimeMillis();
Cursor cur = null;
try {
cur = mDatabaseHelper.getReadableDatabase().query(SSL_CACHE_TABLE, PROJECTION, null,
null, null, null, null);
if (cur.moveToFirst()) {
do {
String hostPort = cur.getString(SSL_CACHE_HOSTPORT_COL);
String value = cur.getString(SSL_CACHE_SESSION_COL);
if (hostPort == null || value == null) {
continue;
}
// TODO: blob support ?
byte[] der = Base64.decodeBase64(value.getBytes());
mExternalCache.put(hostPort, der);
} while (cur.moveToNext());
}
} catch (SQLException ex) {
Log.d(TAG, "Error loading SSL cached entries ", ex);
} finally {
if (cur != null) {
cur.close();
}
if (Log.isLoggable(TAG, Log.DEBUG)) {
long t1 = System.currentTimeMillis();
Log.d(TAG, "LOADED CACHED SSL " + (t1 - t0) + " ms");
}
}
}
String key = host + ":" + port;
return mExternalCache.get(key);
}
}
public byte[] getSessionData(byte[] id) {
// We support client side only - the cache will do nothing on client.
return null;
}
/** Visible for testing.
*/
public static class DatabaseHelper extends SQLiteOpenHelper {
public DatabaseHelper(Context context) {
super(context, DATABASE_NAME, null /* factory */, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("CREATE TABLE " + SSL_CACHE_TABLE + " (" +
SSL_CACHE_ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
SSL_CACHE_HOSTPORT + " TEXT UNIQUE ON CONFLICT REPLACE," +
SSL_CACHE_SESSION + " TEXT," +
SSL_CACHE_TIME_SEC + " INTEGER" +
");");
db.execSQL("CREATE INDEX ssl_sessions_idx1 ON ssl_sessions (" +
SSL_CACHE_HOSTPORT + ");");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + SSL_CACHE_TABLE );
onCreate(db);
}
}
} |