• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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