1 // Copyright 2009 The Android Open Source Project 2 3 package com.android.internal.net; 4 5 import android.content.ContentValues; 6 import android.content.Context; 7 import android.database.Cursor; 8 import android.database.SQLException; 9 import android.database.sqlite.SQLiteDatabase; 10 import android.database.sqlite.SQLiteOpenHelper; 11 import android.util.Log; 12 13 import org.apache.commons.codec.binary.Base64; 14 import org.apache.harmony.xnet.provider.jsse.SSLClientSessionCache; 15 16 import java.util.HashMap; 17 import java.util.Map; 18 19 import javax.net.ssl.SSLSession; 20 21 /** 22 * Hook into harmony SSL cache to persist the SSL sessions. 23 * 24 * Current implementation is suitable for saving a small number of hosts - 25 * like google services. It can be extended with expiration and more features 26 * to support more hosts. 27 * 28 * {@hide} 29 */ 30 public class DbSSLSessionCache implements SSLClientSessionCache { 31 private static final String TAG = "DbSSLSessionCache"; 32 33 /** 34 * Table where sessions are stored. 35 */ 36 public static final String SSL_CACHE_TABLE = "ssl_sessions"; 37 38 private static final String SSL_CACHE_ID = "_id"; 39 40 /** 41 * Key is host:port - port is not optional. 42 */ 43 private static final String SSL_CACHE_HOSTPORT = "hostport"; 44 45 /** 46 * Base64-encoded DER value of the session. 47 */ 48 private static final String SSL_CACHE_SESSION = "session"; 49 50 /** 51 * Time when the record was added - should be close to the time 52 * of the initial session negotiation. 53 */ 54 private static final String SSL_CACHE_TIME_SEC = "time_sec"; 55 56 public static final String DATABASE_NAME = "ssl_sessions.db"; 57 58 public static final int DATABASE_VERSION = 2; 59 60 /** public for testing 61 */ 62 public static final int SSL_CACHE_ID_COL = 0; 63 public static final int SSL_CACHE_HOSTPORT_COL = 1; 64 public static final int SSL_CACHE_SESSION_COL = 2; 65 public static final int SSL_CACHE_TIME_SEC_COL = 3; 66 67 public static final int MAX_CACHE_SIZE = 256; 68 69 private final Map<String, byte[]> mExternalCache = 70 new HashMap<String, byte[]>(); 71 72 73 private DatabaseHelper mDatabaseHelper; 74 75 private boolean mNeedsCacheLoad = true; 76 77 public static final String[] PROJECTION = new String[] { 78 SSL_CACHE_ID, 79 SSL_CACHE_HOSTPORT, 80 SSL_CACHE_SESSION, 81 SSL_CACHE_TIME_SEC 82 }; 83 84 private static final Map<String,DbSSLSessionCache> sInstances = 85 new HashMap<String,DbSSLSessionCache>(); 86 87 /** 88 * Returns a singleton instance of the DbSSLSessionCache that should be used for this 89 * context's package. 90 * 91 * @param context The context that should be used for getting/creating the singleton instance. 92 * @return The singleton instance for the context's package. 93 */ getInstanceForPackage(Context context)94 public static synchronized DbSSLSessionCache getInstanceForPackage(Context context) { 95 String packageName = context.getPackageName(); 96 if (sInstances.containsKey(packageName)) { 97 return sInstances.get(packageName); 98 } 99 DbSSLSessionCache cache = new DbSSLSessionCache(context); 100 sInstances.put(packageName, cache); 101 return cache; 102 } 103 104 /** 105 * Create a SslSessionCache instance, using the specified context to 106 * initialize the database. 107 * 108 * This constructor will use the default database - created for the application 109 * context. 110 * 111 * @param activityContext 112 */ DbSSLSessionCache(Context activityContext)113 private DbSSLSessionCache(Context activityContext) { 114 Context appContext = activityContext.getApplicationContext(); 115 mDatabaseHelper = new DatabaseHelper(appContext); 116 } 117 118 /** 119 * Create a SslSessionCache that uses a specific database. 120 * 121 * 122 * @param database 123 */ DbSSLSessionCache(DatabaseHelper database)124 public DbSSLSessionCache(DatabaseHelper database) { 125 this.mDatabaseHelper = database; 126 } 127 putSessionData(SSLSession session, byte[] der)128 public void putSessionData(SSLSession session, byte[] der) { 129 if (mDatabaseHelper == null) { 130 return; 131 } 132 synchronized (this.getClass()) { 133 SQLiteDatabase db = mDatabaseHelper.getWritableDatabase(); 134 if (mExternalCache.size() == MAX_CACHE_SIZE) { 135 // remove oldest. 136 // TODO: check if the new one is in cached already ( i.e. update ). 137 Cursor byTime = mDatabaseHelper.getReadableDatabase().query(SSL_CACHE_TABLE, 138 PROJECTION, null, null, null, null, SSL_CACHE_TIME_SEC); 139 if (byTime.moveToFirst()) { 140 // TODO: can I do byTime.deleteRow() ? 141 String hostPort = byTime.getString(SSL_CACHE_HOSTPORT_COL); 142 db.delete(SSL_CACHE_TABLE, 143 SSL_CACHE_HOSTPORT + "= ?" , new String[] { hostPort }); 144 mExternalCache.remove(hostPort); 145 } else { 146 Log.w(TAG, "No rows found"); 147 // something is wrong, clear it 148 clear(); 149 } 150 } 151 // Serialize native session to standard DER encoding 152 long t0 = System.currentTimeMillis(); 153 154 String b64 = new String(Base64.encodeBase64(der)); 155 String key = session.getPeerHost() + ":" + session.getPeerPort(); 156 157 ContentValues values = new ContentValues(); 158 values.put(SSL_CACHE_HOSTPORT, key); 159 values.put(SSL_CACHE_SESSION, b64); 160 values.put(SSL_CACHE_TIME_SEC, System.currentTimeMillis() / 1000); 161 162 mExternalCache.put(key, der); 163 164 try { 165 db.insert(SSL_CACHE_TABLE, null /*nullColumnHack */ , values); 166 } catch(SQLException ex) { 167 // Ignore - nothing we can do to recover, and caller shouldn't 168 // be affected. 169 Log.w(TAG, "Ignoring SQL exception when caching session", ex); 170 } 171 if (Log.isLoggable(TAG, Log.DEBUG)) { 172 long t1 = System.currentTimeMillis(); 173 Log.d(TAG, "New SSL session " + session.getPeerHost() + 174 " DER len: " + der.length + " " + (t1 - t0)); 175 } 176 } 177 178 } 179 getSessionData(String host, int port)180 public byte[] getSessionData(String host, int port) { 181 // Current (simple) implementation does a single lookup to DB, then saves 182 // all entries to the cache. 183 184 // This works for google services - i.e. small number of certs. 185 // If we extend this to all processes - we should hold a separate cache 186 // or do lookups to DB each time. 187 if (mDatabaseHelper == null) { 188 return null; 189 } 190 synchronized(this.getClass()) { 191 if (mNeedsCacheLoad) { 192 // Don't try to load again, if something is wrong on the first 193 // request it'll likely be wrong each time. 194 mNeedsCacheLoad = false; 195 long t0 = System.currentTimeMillis(); 196 197 Cursor cur = null; 198 try { 199 cur = mDatabaseHelper.getReadableDatabase().query(SSL_CACHE_TABLE, 200 PROJECTION, null, null, null, null, null); 201 if (cur.moveToFirst()) { 202 do { 203 String hostPort = cur.getString(SSL_CACHE_HOSTPORT_COL); 204 String value = cur.getString(SSL_CACHE_SESSION_COL); 205 206 if (hostPort == null || value == null) { 207 continue; 208 } 209 // TODO: blob support ? 210 byte[] der = Base64.decodeBase64(value.getBytes()); 211 mExternalCache.put(hostPort, der); 212 } while (cur.moveToNext()); 213 214 } 215 } catch (SQLException ex) { 216 Log.d(TAG, "Error loading SSL cached entries ", ex); 217 } finally { 218 if (cur != null) { 219 cur.close(); 220 } 221 if (Log.isLoggable(TAG, Log.DEBUG)) { 222 long t1 = System.currentTimeMillis(); 223 Log.d(TAG, "LOADED CACHED SSL " + (t1 - t0) + " ms"); 224 } 225 } 226 } 227 228 String key = host + ":" + port; 229 230 return mExternalCache.get(key); 231 } 232 } 233 234 /** 235 * Reset the database and internal state. 236 * Used for testing or to free space. 237 */ clear()238 public void clear() { 239 synchronized(this) { 240 try { 241 mExternalCache.clear(); 242 mNeedsCacheLoad = true; 243 mDatabaseHelper.getWritableDatabase().delete(SSL_CACHE_TABLE, 244 null, null); 245 } catch (SQLException ex) { 246 Log.d(TAG, "Error removing SSL cached entries ", ex); 247 // ignore - nothing we can do about it 248 } 249 } 250 } 251 getSessionData(byte[] id)252 public byte[] getSessionData(byte[] id) { 253 // We support client side only - the cache will do nothing for 254 // server-side sessions. 255 return null; 256 } 257 258 /** Visible for testing. 259 */ 260 public static class DatabaseHelper extends SQLiteOpenHelper { 261 DatabaseHelper(Context context)262 public DatabaseHelper(Context context) { 263 super(context, DATABASE_NAME, null /* factory */, DATABASE_VERSION); 264 } 265 266 @Override onCreate(SQLiteDatabase db)267 public void onCreate(SQLiteDatabase db) { 268 db.execSQL("CREATE TABLE " + SSL_CACHE_TABLE + " (" + 269 SSL_CACHE_ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + 270 SSL_CACHE_HOSTPORT + " TEXT UNIQUE ON CONFLICT REPLACE," + 271 SSL_CACHE_SESSION + " TEXT," + 272 SSL_CACHE_TIME_SEC + " INTEGER" + 273 ");"); 274 275 // No index - we load on startup, index would slow down inserts. 276 // If we want to scale this to lots of rows - we could use 277 // index, but then we'll hit DB a bit too often ( including 278 // negative hits ) 279 } 280 281 @Override onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)282 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 283 db.execSQL("DROP TABLE IF EXISTS " + SSL_CACHE_TABLE ); 284 onCreate(db); 285 } 286 287 } 288 289 } 290