Files
open-keychain/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainDatabase.java

464 lines
21 KiB
Java
Raw Normal View History

/*
* Copyright (C) 2017 Schürmann & Breitmoser GbR
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
2018-07-02 15:07:11 +02:00
package org.sufficientlysecure.keychain;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import android.arch.persistence.db.SupportSQLiteDatabase;
import android.arch.persistence.db.SupportSQLiteOpenHelper;
import android.arch.persistence.db.SupportSQLiteOpenHelper.Callback;
import android.arch.persistence.db.SupportSQLiteOpenHelper.Configuration;
import android.arch.persistence.db.framework.FrameworkSQLiteOpenHelperFactory;
2014-03-13 23:03:08 +02:00
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteException;
import android.support.annotation.VisibleForTesting;
2014-03-26 00:11:49 +01:00
2018-07-02 15:07:11 +02:00
import org.sufficientlysecure.keychain.daos.LocalSecretKeyStorage;
2014-04-15 22:52:21 +02:00
import org.sufficientlysecure.keychain.provider.KeychainContract.CertsColumns;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeysColumns;
import org.sufficientlysecure.keychain.provider.KeychainContract.UserPacketsColumns;
import org.sufficientlysecure.keychain.util.Preferences;
2018-01-20 02:12:43 +01:00
import timber.log.Timber;
2014-04-29 10:19:10 +02:00
/**
* SQLite Datatypes (from http://www.sqlite.org/datatype3.html)
* - NULL. The value is a NULL value.
* - INTEGER. The value is a signed integer, stored in 1, 2, 3, 4, 6, or 8 bytes depending on the magnitude of the value.
* - REAL. The value is a floating point value, stored as an 8-byte IEEE floating point number.
* - TEXT. The value is a text string, stored using the database encoding (UTF-8, UTF-16BE or UTF-16LE).
* - BLOB. The value is a blob of data, stored exactly as it was input.
*/
public class KeychainDatabase {
private static final String DATABASE_NAME = "openkeychain.db";
private static final int DATABASE_VERSION = 29;
private final SupportSQLiteOpenHelper supportSQLiteOpenHelper;
2018-06-25 15:11:08 +02:00
private static KeychainDatabase sInstance;
public static KeychainDatabase getInstance(Context context) {
sInstance = new KeychainDatabase(context.getApplicationContext());
return sInstance;
}
@VisibleForTesting
public static void resetSingleton() {
sInstance = null;
}
public interface Tables {
String KEY_RINGS_PUBLIC = "keyrings_public";
String KEYS = "keys";
2017-09-22 04:18:14 +02:00
String KEY_SIGNATURES = "key_signatures";
String USER_PACKETS = "user_packets";
2014-03-11 00:09:11 +01:00
String CERTS = "certs";
String API_ALLOWED_KEYS = "api_allowed_keys";
String OVERRIDDEN_WARNINGS = "overridden_warnings";
}
2018-06-25 15:11:08 +02:00
private KeychainDatabase(Context context) {
supportSQLiteOpenHelper =
new FrameworkSQLiteOpenHelperFactory()
.create(Configuration.builder(context).name(DATABASE_NAME).callback(
new Callback(DATABASE_VERSION) {
@Override
public void onCreate(SupportSQLiteDatabase db) {
2018-06-25 15:11:08 +02:00
KeychainDatabase.this.onCreate(db, context);
}
@Override
public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
2018-06-25 15:11:08 +02:00
KeychainDatabase.this.onUpgrade(db, context, oldVersion, newVersion);
}
@Override
public void onDowngrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
2018-06-25 15:11:08 +02:00
KeychainDatabase.this.onDowngrade();
}
@Override
public void onOpen(SupportSQLiteDatabase db) {
super.onOpen(db);
if (!db.isReadOnly()) {
// Enable foreign key constraints
db.execSQL("PRAGMA foreign_keys=ON;");
}
}
}).build());
}
public SupportSQLiteDatabase getReadableDatabase() {
return supportSQLiteOpenHelper.getReadableDatabase();
}
public SupportSQLiteDatabase getWritableDatabase() {
return supportSQLiteOpenHelper.getWritableDatabase();
}
2018-06-25 15:11:08 +02:00
private void onCreate(SupportSQLiteDatabase db, Context context) {
2018-01-20 02:12:43 +01:00
Timber.w("Creating database...");
db.execSQL(KeyRingsPublicModel.CREATE_TABLE);
2018-06-25 15:11:08 +02:00
db.execSQL(KeysModel.CREATE_TABLE);
db.execSQL(UserPacketsModel.CREATE_TABLE);
db.execSQL(CertsModel.CREATE_TABLE);
db.execSQL(KeyMetadataModel.CREATE_TABLE);
db.execSQL(KeySignaturesModel.CREATE_TABLE);
2018-06-25 15:11:08 +02:00
db.execSQL(ApiAppsModel.CREATE_TABLE);
db.execSQL(OverriddenWarningsModel.CREATE_TABLE);
db.execSQL(AutocryptPeersModel.CREATE_TABLE);
db.execSQL(ApiAllowedKeysModel.CREATE_TABLE);
db.execSQL(KeysModel.UNIFIEDKEYVIEW);
2018-01-15 17:01:21 +01:00
db.execSQL("CREATE INDEX keys_by_rank ON keys (" + KeysColumns.RANK + ", " + KeysColumns.MASTER_KEY_ID + ");");
db.execSQL("CREATE INDEX uids_by_rank ON user_packets (" + UserPacketsColumns.RANK + ", "
+ UserPacketsColumns.USER_ID + ", " + UserPacketsColumns.MASTER_KEY_ID + ");");
db.execSQL("CREATE INDEX verified_certs ON certs ("
+ CertsColumns.VERIFIED + ", " + CertsColumns.MASTER_KEY_ID + ");");
2018-01-15 02:55:57 +01:00
db.execSQL("CREATE INDEX uids_by_email ON user_packets ("
+ UserPacketsColumns.EMAIL + ");");
Preferences.getPreferences(context).setKeySignaturesTableInitialized();
}
2018-06-25 15:11:08 +02:00
private void onUpgrade(SupportSQLiteDatabase db, Context context, int oldVersion, int newVersion) {
2018-01-20 02:12:43 +01:00
Timber.d("Upgrading db from " + oldVersion + " to " + newVersion);
2014-09-09 09:26:03 +02:00
2014-08-24 05:12:09 +02:00
switch (oldVersion) {
case 1:
2014-09-09 09:26:03 +02:00
// add has_secret for all who are upgrading from a beta version
2014-08-24 05:12:09 +02:00
try {
db.execSQL("ALTER TABLE keys ADD COLUMN has_secret INTEGER");
2014-09-30 17:50:16 +02:00
} catch (Exception e) {
2014-08-24 05:12:09 +02:00
// never mind, the column probably already existed
}
2014-09-09 09:26:03 +02:00
// fall through
2014-08-24 05:12:09 +02:00
case 2:
2014-09-09 09:26:03 +02:00
// ECC support
2014-08-24 05:12:09 +02:00
try {
db.execSQL("ALTER TABLE keys ADD COLUMN key_curve_oid TEXT");
2014-09-30 17:50:16 +02:00
} catch (Exception e) {
2014-08-24 05:12:09 +02:00
// never mind, the column probably already existed
}
2014-09-09 09:26:03 +02:00
// fall through
case 3:
// better s2k detection, we need consolidate
// fall through
case 4:
try {
db.execSQL("ALTER TABLE keys ADD COLUMN can_authenticate INTEGER");
2014-09-30 17:50:16 +02:00
} catch (Exception e) {
// never mind, the column probably already existed
}
// fall through
2014-09-30 17:50:16 +02:00
case 5:
// do consolidate for 3.0 beta3
// fall through
case 6:
db.execSQL("ALTER TABLE user_ids ADD COLUMN type INTEGER");
db.execSQL("ALTER TABLE user_ids ADD COLUMN attribute_data BLOB");
case 7:
// new table for allowed key ids in API
try {
2018-06-25 15:11:08 +02:00
db.execSQL(ApiAppsModel.CREATE_TABLE);
} catch (Exception e) {
// never mind, the column probably already existed
}
case 8:
// tbale name for user_ids changed to user_packets
db.execSQL("DROP TABLE IF EXISTS certs");
db.execSQL("DROP TABLE IF EXISTS user_ids");
db.execSQL("CREATE TABLE IF NOT EXISTS user_packets("
+ "master_key_id INTEGER, "
+ "type INT, "
+ "user_id TEXT, "
+ "attribute_data BLOB, "
+ "is_primary INTEGER, "
+ "is_revoked INTEGER, "
+ "rank INTEGER, "
+ "PRIMARY KEY(master_key_id, rank), "
+ "FOREIGN KEY(master_key_id) REFERENCES "
+ "keyrings_public(master_key_id) ON DELETE CASCADE"
+ ")");
db.execSQL("CREATE TABLE IF NOT EXISTS certs("
+ "master_key_id INTEGER,"
+ "rank INTEGER, " // rank of certified uid
+ "key_id_certifier INTEGER, " // certifying key
+ "type INTEGER, "
+ "verified INTEGER, "
+ "creation INTEGER, "
+ "data BLOB, "
+ "PRIMARY KEY(master_key_id, rank, "
+ "key_id_certifier), "
+ "FOREIGN KEY(master_key_id) REFERENCES "
+ "keyrings_public(master_key_id) ON DELETE CASCADE,"
+ "FOREIGN KEY(master_key_id, rank) REFERENCES "
+ "user_packets(master_key_id, rank) ON DELETE CASCADE"
+ ")");
case 9:
2015-05-28 22:43:10 +02:00
// do nothing here, just consolidate
case 10:
2015-07-08 03:33:46 +02:00
// fix problems in database, see #1402 for details
// https://github.com/open-keychain/open-keychain/issues/1402
// no longer needed, api_accounts is deprecated
// db.execSQL("DELETE FROM api_accounts WHERE key_id BETWEEN 0 AND 3");
case 11:
db.execSQL("CREATE TABLE IF NOT EXISTS updated_keys ("
+ "master_key_id INTEGER PRIMARY KEY, "
+ "last_updated INTEGER, "
+ "FOREIGN KEY(master_key_id) REFERENCES "
+ "keyrings_public(master_key_id) ON DELETE CASCADE"
+ ")");
case 12:
// do nothing here, just consolidate
case 13:
db.execSQL("CREATE INDEX keys_by_rank ON keys (" + KeysColumns.RANK + ");");
db.execSQL("CREATE INDEX uids_by_rank ON user_packets (" + UserPacketsColumns.RANK + ", "
+ UserPacketsColumns.USER_ID + ", " + UserPacketsColumns.MASTER_KEY_ID + ");");
db.execSQL("CREATE INDEX verified_certs ON certs ("
+ CertsColumns.VERIFIED + ", " + CertsColumns.MASTER_KEY_ID + ");");
case 14:
db.execSQL("ALTER TABLE user_packets ADD COLUMN name TEXT");
db.execSQL("ALTER TABLE user_packets ADD COLUMN email TEXT");
db.execSQL("ALTER TABLE user_packets ADD COLUMN comment TEXT");
case 15:
db.execSQL("CREATE INDEX uids_by_name ON user_packets (name COLLATE NOCASE)");
db.execSQL("CREATE INDEX uids_by_email ON user_packets (email COLLATE NOCASE)");
case 16:
// splitUserId changed: Execute consolidate for new parsing of name, email
case 17:
// splitUserId changed: Execute consolidate for new parsing of name, email
2017-02-03 16:11:29 +05:30
case 18:
db.execSQL("ALTER TABLE keys ADD COLUMN is_secure INTEGER");
2017-02-07 13:15:35 +01:00
case 19:
// emergency fix for crashing consolidate
db.execSQL("UPDATE keys SET is_secure = 1;");
case 20:
db.execSQL(
"CREATE TABLE IF NOT EXISTS overridden_warnings ("
+ "_id INTEGER PRIMARY KEY AUTOINCREMENT, "
+ "identifier TEXT NOT NULL UNIQUE "
+ ")");
case 21:
try {
db.execSQL("ALTER TABLE updated_keys ADD COLUMN seen_on_keyservers INTEGER;");
} catch (SQLiteException e) {
// don't bother, the column probably already existed
}
2016-11-28 08:11:34 +01:00
case 22:
2017-06-13 18:33:21 +02:00
db.execSQL("CREATE TABLE IF NOT EXISTS api_autocrypt_peers ("
+ "package_name TEXT NOT NULL, "
+ "identifier TEXT NOT NULL, "
+ "last_updated INTEGER NOT NULL, "
2017-07-25 23:19:02 +02:00
+ "last_seen_key INTEGER NOT NULL, "
+ "state INTEGER NOT NULL, "
2018-06-25 15:11:08 +02:00
+ "master_key_id INTEGER, "
2017-06-13 18:33:21 +02:00
+ "PRIMARY KEY(package_name, identifier), "
+ "FOREIGN KEY(package_name) REFERENCES api_apps(package_name) ON DELETE CASCADE"
+ ")");
2016-11-28 08:11:34 +01:00
2017-09-22 04:18:14 +02:00
case 23:
db.execSQL("CREATE TABLE IF NOT EXISTS key_signatures ("
+ "master_key_id INTEGER NOT NULL, "
+ "signer_key_id INTEGER NOT NULL, "
+ "PRIMARY KEY(master_key_id, signer_key_id), "
+ "FOREIGN KEY(master_key_id) REFERENCES keyrings_public(master_key_id) ON DELETE CASCADE"
+ ")");
2018-01-08 12:38:57 +01:00
case 24: {
2018-01-30 11:20:23 +01:00
try {
db.beginTransaction();
db.execSQL("ALTER TABLE api_autocrypt_peers RENAME TO tmp");
db.execSQL("CREATE TABLE api_autocrypt_peers ("
+ "package_name TEXT NOT NULL, "
+ "identifier TEXT NOT NULL, "
+ "last_seen INTEGER, "
+ "last_seen_key INTEGER, "
+ "is_mutual INTEGER, "
+ "master_key_id INTEGER, "
+ "gossip_master_key_id INTEGER, "
+ "gossip_last_seen_key INTEGER, "
+ "gossip_origin INTEGER, "
+ "PRIMARY KEY(package_name, identifier), "
+ "FOREIGN KEY(package_name) REFERENCES api_apps (package_name) ON DELETE CASCADE"
+ ")");
// Note: Keys from Autocrypt 0.X with state == "reset" (0) are dropped
db.execSQL("INSERT INTO api_autocrypt_peers " +
"(package_name, identifier, last_seen, gossip_last_seen_key, gossip_master_key_id, gossip_origin) " +
"SELECT package_name, identifier, last_updated, last_seen_key, master_key_id, 0 " +
"FROM tmp WHERE state = 1"); // Autocrypt 0.X, "gossip" -> now origin=autocrypt
db.execSQL("INSERT INTO api_autocrypt_peers " +
"(package_name, identifier, last_seen, gossip_last_seen_key, gossip_master_key_id, gossip_origin) " +
"SELECT package_name, identifier, last_updated, last_seen_key, master_key_id, 20 " +
"FROM tmp WHERE state = 2"); // "selected" keys -> now origin=dedup
db.execSQL("INSERT INTO api_autocrypt_peers " +
"(package_name, identifier, last_seen, last_seen_key, master_key_id, is_mutual) " +
"SELECT package_name, identifier, last_updated, last_seen_key, master_key_id, 0 " +
2018-01-20 22:08:51 +01:00
"FROM tmp WHERE state = 3"); // Autocrypt 0.X, state = "available"
2018-01-30 11:20:23 +01:00
db.execSQL("INSERT INTO api_autocrypt_peers " +
"(package_name, identifier, last_seen, last_seen_key, master_key_id, is_mutual) " +
"SELECT package_name, identifier, last_updated, last_seen_key, master_key_id, 1 " +
2018-01-20 22:08:51 +01:00
"FROM tmp WHERE state = 4"); // from Autocrypt 0.X, state = "mutual"
2018-01-30 11:20:23 +01:00
db.execSQL("DROP TABLE tmp");
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
2018-01-17 15:40:46 +01:00
2018-01-30 11:20:23 +01:00
db.execSQL("CREATE INDEX IF NOT EXISTS uids_by_email ON user_packets (email);");
2018-01-15 17:01:21 +01:00
db.execSQL("DROP INDEX keys_by_rank");
db.execSQL("CREATE INDEX keys_by_rank ON keys(rank, master_key_id);");
}
case 25: {
try {
2018-06-25 15:11:08 +02:00
migrateSecretKeysFromDbToLocalStorage(db, context);
} catch (IOException e) {
throw new IllegalStateException("Error migrating secret keys! This is bad!!");
}
}
case 26:
migrateUpdatedKeysToKeyMetadataTable(db);
case 27:
renameApiAutocryptPeersTable(db);
case 28:
recreateUnifiedKeyView(db);
2018-06-25 15:11:08 +02:00
// drop old table from version 20
db.execSQL("DROP TABLE IF EXISTS api_accounts");
}
}
2014-03-11 00:09:11 +01:00
private void recreateUnifiedKeyView(SupportSQLiteDatabase db) {
2018-06-25 15:11:08 +02:00
// noinspection deprecation
db.execSQL("DROP VIEW IF EXISTS " + KeysModel.UNIFIEDKEYVIEW_VIEW_NAME);
db.execSQL(KeysModel.UNIFIEDKEYVIEW);
}
2018-06-25 15:11:08 +02:00
private void migrateSecretKeysFromDbToLocalStorage(SupportSQLiteDatabase db, Context context) throws IOException {
LocalSecretKeyStorage localSecretKeyStorage = LocalSecretKeyStorage.getInstance(context);
Cursor cursor = db.query("SELECT master_key_id, key_ring_data FROM keyrings_secret");
while (cursor.moveToNext()) {
long masterKeyId = cursor.getLong(0);
byte[] secretKeyBlob = cursor.getBlob(1);
localSecretKeyStorage.writeSecretKey(masterKeyId, secretKeyBlob);
}
cursor.close();
// we'll keep this around for now, but make sure to delete when migration looks ok!!
// db.execSQL("DROP TABLE keyrings_secret");
}
private void migrateUpdatedKeysToKeyMetadataTable(SupportSQLiteDatabase db) {
try {
db.execSQL("ALTER TABLE updated_keys RENAME TO key_metadata;");
} catch (SQLException e) {
if (Constants.DEBUG) {
Timber.e(e, "Ignoring migration exception, this probably happened before");
return;
}
throw e;
}
}
private void renameApiAutocryptPeersTable(SupportSQLiteDatabase db) {
try {
db.execSQL("ALTER TABLE api_autocrypt_peers RENAME TO autocrypt_peers;");
} catch (SQLException e) {
if (Constants.DEBUG) {
Timber.e(e, "Ignoring migration exception, this probably happened before");
return;
}
throw e;
}
}
2018-06-25 15:11:08 +02:00
private void onDowngrade() {
// Downgrade is ok for the debug version, makes it easier to work with branches
if (Constants.DEBUG) {
return;
}
// NOTE: downgrading the database is explicitly not allowed to prevent
// someone from exploiting old bugs to export the database
throw new RuntimeException("Downgrading the database is not allowed!");
}
private static void copy(File in, File out) throws IOException {
2014-08-04 11:19:46 +02:00
FileInputStream is = new FileInputStream(in);
FileOutputStream os = new FileOutputStream(out);
2015-06-17 21:30:25 +02:00
try {
byte[] buf = new byte[512];
while (is.available() > 0) {
int count = is.read(buf, 0, 512);
os.write(buf, 0, count);
}
} finally {
is.close();
os.close();
}
}
2014-08-04 11:19:46 +02:00
public static void debugBackup(Context context, boolean restore) throws IOException {
2014-04-29 14:02:22 +02:00
if (!Constants.DEBUG) {
return;
}
2014-08-04 11:19:46 +02:00
File in;
File out;
if (restore) {
in = context.getDatabasePath("debug_backup.db");
out = context.getDatabasePath(DATABASE_NAME);
2014-08-04 11:19:46 +02:00
} else {
in = context.getDatabasePath(DATABASE_NAME);
2014-08-04 11:19:46 +02:00
out = context.getDatabasePath("debug_backup.db");
// noinspection ResultOfMethodCallIgnored - this is a pure debug feature, anyways
2014-08-04 11:19:46 +02:00
out.createNewFile();
}
2014-04-29 14:02:22 +02:00
if (!in.canRead()) {
throw new IOException("Cannot read " + in.getName());
}
2014-08-04 11:19:46 +02:00
if (!out.canWrite()) {
throw new IOException("Cannot write " + out.getName());
}
copy(in, out);
}
2014-08-04 11:19:46 +02:00
// DANGEROUS, use in test code ONLY!
public void clearDatabase() {
getWritableDatabase().execSQL("delete from " + KeyRingsPublicModel.TABLE_NAME);
getWritableDatabase().execSQL("delete from " + ApiAllowedKeysModel.TABLE_NAME);
getWritableDatabase().execSQL("delete from api_apps");
}
}