/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mozilla.gecko.db; import java.util.HashMap; import org.mozilla.gecko.CrashHandler; import org.mozilla.gecko.GeckoApp; import org.mozilla.gecko.GeckoAppShell; import org.mozilla.gecko.GeckoMessageReceiver; import org.mozilla.gecko.NSSBridge; import org.mozilla.gecko.db.BrowserContract.DeletedPasswords; import org.mozilla.gecko.db.BrowserContract.GeckoDisabledHosts; import org.mozilla.gecko.db.BrowserContract.Passwords; import org.mozilla.gecko.mozglue.GeckoLoader; import org.mozilla.gecko.sqlite.MatrixBlobCursor; import org.mozilla.gecko.sqlite.SQLiteBridge; import org.mozilla.gecko.sync.Utils; import android.content.ContentValues; import android.content.Intent; import android.content.UriMatcher; import android.database.Cursor; import android.net.Uri; import android.text.TextUtils; import android.util.Log; public class PasswordsProvider extends SQLiteBridgeContentProvider { static final String TABLE_PASSWORDS = "moz_logins"; static final String TABLE_DELETED_PASSWORDS = "moz_deleted_logins"; static final String TABLE_DISABLED_HOSTS = "moz_disabledHosts"; private static final String TELEMETRY_TAG = "SQLITEBRIDGE_PROVIDER_PASSWORDS"; private static final int PASSWORDS = 100; private static final int DELETED_PASSWORDS = 101; private static final int DISABLED_HOSTS = 102; static final String DEFAULT_PASSWORDS_SORT_ORDER = Passwords.HOSTNAME + " ASC"; static final String DEFAULT_DELETED_PASSWORDS_SORT_ORDER = DeletedPasswords.TIME_DELETED + " ASC"; private static final UriMatcher URI_MATCHER; private static final HashMap PASSWORDS_PROJECTION_MAP; private static final HashMap DELETED_PASSWORDS_PROJECTION_MAP; private static final HashMap DISABLED_HOSTS_PROJECTION_MAP; // this should be kept in sync with the version in toolkit/components/passwordmgr/storage-mozStorage.js private static final int DB_VERSION = 6; private static final String DB_FILENAME = "signons.sqlite"; private static final String WHERE_GUID_IS_NULL = BrowserContract.DeletedPasswords.GUID + " IS NULL"; private static final String WHERE_GUID_IS_VALUE = BrowserContract.DeletedPasswords.GUID + " = ?"; private static final String LOG_TAG = "GeckoPasswordsProvider"; private CrashHandler mCrashHandler; static { URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH); // content://org.mozilla.gecko.providers.browser/passwords/# URI_MATCHER.addURI(BrowserContract.PASSWORDS_AUTHORITY, "passwords", PASSWORDS); PASSWORDS_PROJECTION_MAP = new HashMap(); PASSWORDS_PROJECTION_MAP.put(Passwords.ID, Passwords.ID); PASSWORDS_PROJECTION_MAP.put(Passwords.HOSTNAME, Passwords.HOSTNAME); PASSWORDS_PROJECTION_MAP.put(Passwords.HTTP_REALM, Passwords.HTTP_REALM); PASSWORDS_PROJECTION_MAP.put(Passwords.FORM_SUBMIT_URL, Passwords.FORM_SUBMIT_URL); PASSWORDS_PROJECTION_MAP.put(Passwords.USERNAME_FIELD, Passwords.USERNAME_FIELD); PASSWORDS_PROJECTION_MAP.put(Passwords.PASSWORD_FIELD, Passwords.PASSWORD_FIELD); PASSWORDS_PROJECTION_MAP.put(Passwords.ENCRYPTED_USERNAME, Passwords.ENCRYPTED_USERNAME); PASSWORDS_PROJECTION_MAP.put(Passwords.ENCRYPTED_PASSWORD, Passwords.ENCRYPTED_PASSWORD); PASSWORDS_PROJECTION_MAP.put(Passwords.GUID, Passwords.GUID); PASSWORDS_PROJECTION_MAP.put(Passwords.ENC_TYPE, Passwords.ENC_TYPE); PASSWORDS_PROJECTION_MAP.put(Passwords.TIME_CREATED, Passwords.TIME_CREATED); PASSWORDS_PROJECTION_MAP.put(Passwords.TIME_LAST_USED, Passwords.TIME_LAST_USED); PASSWORDS_PROJECTION_MAP.put(Passwords.TIME_PASSWORD_CHANGED, Passwords.TIME_PASSWORD_CHANGED); PASSWORDS_PROJECTION_MAP.put(Passwords.TIMES_USED, Passwords.TIMES_USED); URI_MATCHER.addURI(BrowserContract.PASSWORDS_AUTHORITY, "deleted-passwords", DELETED_PASSWORDS); DELETED_PASSWORDS_PROJECTION_MAP = new HashMap(); DELETED_PASSWORDS_PROJECTION_MAP.put(DeletedPasswords.ID, DeletedPasswords.ID); DELETED_PASSWORDS_PROJECTION_MAP.put(DeletedPasswords.GUID, DeletedPasswords.GUID); DELETED_PASSWORDS_PROJECTION_MAP.put(DeletedPasswords.TIME_DELETED, DeletedPasswords.TIME_DELETED); URI_MATCHER.addURI(BrowserContract.PASSWORDS_AUTHORITY, "disabled-hosts", DISABLED_HOSTS); DISABLED_HOSTS_PROJECTION_MAP = new HashMap(); DISABLED_HOSTS_PROJECTION_MAP.put(GeckoDisabledHosts.HOSTNAME, GeckoDisabledHosts.HOSTNAME); } public PasswordsProvider() { super(LOG_TAG); } @Override public boolean onCreate() { mCrashHandler = CrashHandler.createDefaultCrashHandler(getContext()); // We don't use .loadMozGlue because we're in a different process, // and we just want to reuse code rather than use the loader lock etc. GeckoLoader.doLoadLibrary(getContext(), "mozglue"); return super.onCreate(); } @Override public void shutdown() { super.shutdown(); if (mCrashHandler != null) { mCrashHandler.unregister(); mCrashHandler = null; } } @Override protected String getDBName() { return DB_FILENAME; } @Override protected String getTelemetryPrefix() { return TELEMETRY_TAG; } @Override protected int getDBVersion() { return DB_VERSION; } @Override public String getType(Uri uri) { final int match = URI_MATCHER.match(uri); switch (match) { case PASSWORDS: return Passwords.CONTENT_TYPE; case DELETED_PASSWORDS: return DeletedPasswords.CONTENT_TYPE; case DISABLED_HOSTS: return GeckoDisabledHosts.CONTENT_TYPE; default: throw new UnsupportedOperationException("Unknown type " + uri); } } @Override public String getTable(Uri uri) { final int match = URI_MATCHER.match(uri); switch (match) { case DELETED_PASSWORDS: return TABLE_DELETED_PASSWORDS; case PASSWORDS: return TABLE_PASSWORDS; case DISABLED_HOSTS: return TABLE_DISABLED_HOSTS; default: throw new UnsupportedOperationException("Unknown table " + uri); } } @Override public String getSortOrder(Uri uri, String aRequested) { if (!TextUtils.isEmpty(aRequested)) { return aRequested; } final int match = URI_MATCHER.match(uri); switch (match) { case DELETED_PASSWORDS: return DEFAULT_DELETED_PASSWORDS_SORT_ORDER; case PASSWORDS: return DEFAULT_PASSWORDS_SORT_ORDER; case DISABLED_HOSTS: return null; default: throw new UnsupportedOperationException("Unknown URI " + uri); } } @Override public void setupDefaults(Uri uri, ContentValues values) throws IllegalArgumentException { int match = URI_MATCHER.match(uri); long now = System.currentTimeMillis(); switch (match) { case DELETED_PASSWORDS: values.put(DeletedPasswords.TIME_DELETED, now); // Deleted passwords must contain a guid if (!values.containsKey(Passwords.GUID)) { throw new IllegalArgumentException("Must provide a GUID for a deleted password"); } break; case PASSWORDS: values.put(Passwords.TIME_CREATED, now); // Generate GUID for new password. Don't override specified GUIDs. if (!values.containsKey(Passwords.GUID)) { String guid = Utils.generateGuid(); values.put(Passwords.GUID, guid); } String nowString = Long.toString(now); DBUtils.replaceKey(values, null, Passwords.HOSTNAME, ""); DBUtils.replaceKey(values, null, Passwords.HTTP_REALM, ""); DBUtils.replaceKey(values, null, Passwords.FORM_SUBMIT_URL, ""); DBUtils.replaceKey(values, null, Passwords.USERNAME_FIELD, ""); DBUtils.replaceKey(values, null, Passwords.PASSWORD_FIELD, ""); DBUtils.replaceKey(values, null, Passwords.ENCRYPTED_USERNAME, ""); DBUtils.replaceKey(values, null, Passwords.ENCRYPTED_PASSWORD, ""); DBUtils.replaceKey(values, null, Passwords.ENC_TYPE, "0"); DBUtils.replaceKey(values, null, Passwords.TIME_LAST_USED, nowString); DBUtils.replaceKey(values, null, Passwords.TIME_PASSWORD_CHANGED, nowString); DBUtils.replaceKey(values, null, Passwords.TIMES_USED, "0"); break; case DISABLED_HOSTS: if (!values.containsKey(GeckoDisabledHosts.HOSTNAME)) { throw new IllegalArgumentException("Must provide a hostname for a disabled host"); } break; default: throw new UnsupportedOperationException("Unknown URI " + uri); } } @Override public void initGecko() { // We're not in the main process. The receiver of this Intent can // communicate with Gecko in the main process. Intent initIntent = new Intent(getContext(), GeckoMessageReceiver.class); initIntent.setAction(GeckoApp.ACTION_INIT_PW); mContext.sendBroadcast(initIntent); } private String doCrypto(String initialValue, Uri uri, Boolean encrypt) { String profilePath = null; if (uri != null) { profilePath = uri.getQueryParameter(BrowserContract.PARAM_PROFILE_PATH); } String result = ""; try { if (encrypt) { if (profilePath != null) { result = NSSBridge.encrypt(mContext, profilePath, initialValue); } else { result = NSSBridge.encrypt(mContext, initialValue); } } else { if (profilePath != null) { result = NSSBridge.decrypt(mContext, profilePath, initialValue); } else { result = NSSBridge.decrypt(mContext, initialValue); } } } catch (Exception ex) { Log.e(LOG_TAG, "Error in NSSBridge"); throw new RuntimeException(ex); } return result; } @Override public void onPreInsert(ContentValues values, Uri uri, SQLiteBridge db) { if (values.containsKey(Passwords.GUID)) { String guid = values.getAsString(Passwords.GUID); if (guid == null) { db.delete(TABLE_DELETED_PASSWORDS, WHERE_GUID_IS_NULL, null); return; } String[] args = new String[] { guid }; db.delete(TABLE_DELETED_PASSWORDS, WHERE_GUID_IS_VALUE, args); } if (values.containsKey(Passwords.ENCRYPTED_PASSWORD)) { String res = doCrypto(values.getAsString(Passwords.ENCRYPTED_PASSWORD), uri, true); values.put(Passwords.ENCRYPTED_PASSWORD, res); values.put(Passwords.ENC_TYPE, Passwords.ENCTYPE_SDR); } if (values.containsKey(Passwords.ENCRYPTED_USERNAME)) { String res = doCrypto(values.getAsString(Passwords.ENCRYPTED_USERNAME), uri, true); values.put(Passwords.ENCRYPTED_USERNAME, res); values.put(Passwords.ENC_TYPE, Passwords.ENCTYPE_SDR); } } @Override public void onPreUpdate(ContentValues values, Uri uri, SQLiteBridge db) { if (values.containsKey(Passwords.ENCRYPTED_PASSWORD)) { String res = doCrypto(values.getAsString(Passwords.ENCRYPTED_PASSWORD), uri, true); values.put(Passwords.ENCRYPTED_PASSWORD, res); values.put(Passwords.ENC_TYPE, Passwords.ENCTYPE_SDR); } if (values.containsKey(Passwords.ENCRYPTED_USERNAME)) { String res = doCrypto(values.getAsString(Passwords.ENCRYPTED_USERNAME), uri, true); values.put(Passwords.ENCRYPTED_USERNAME, res); values.put(Passwords.ENC_TYPE, Passwords.ENCTYPE_SDR); } } @Override public void onPostQuery(Cursor cursor, Uri uri, SQLiteBridge db) { int passwordIndex = -1; int usernameIndex = -1; String profilePath = null; try { passwordIndex = cursor.getColumnIndexOrThrow(Passwords.ENCRYPTED_PASSWORD); } catch (Exception ex) { } try { usernameIndex = cursor.getColumnIndexOrThrow(Passwords.ENCRYPTED_USERNAME); } catch (Exception ex) { } if (passwordIndex > -1 || usernameIndex > -1) { MatrixBlobCursor m = (MatrixBlobCursor)cursor; if (cursor.moveToFirst()) { do { if (passwordIndex > -1) { String decrypted = doCrypto(cursor.getString(passwordIndex), uri, false);; m.set(passwordIndex, decrypted); } if (usernameIndex > -1) { String decrypted = doCrypto(cursor.getString(usernameIndex), uri, false); m.set(usernameIndex, decrypted); } } while (cursor.moveToNext()); } } } }