Rename folder structure from OpenPGP Keychain to OpenKeychain
This commit is contained in:
@@ -0,0 +1,297 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.provider;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.provider.BaseColumns;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
|
||||
public class KeychainContract {
|
||||
|
||||
interface KeyRingsColumns {
|
||||
String MASTER_KEY_ID = "master_key_id"; // not a database id
|
||||
String KEY_RING_DATA = "key_ring_data"; // PGPPublicKeyRing / PGPSecretKeyRing blob
|
||||
}
|
||||
|
||||
interface KeysColumns {
|
||||
String MASTER_KEY_ID = "master_key_id"; // not a database id
|
||||
String RANK = "rank";
|
||||
|
||||
String KEY_ID = "key_id"; // not a database id
|
||||
String ALGORITHM = "algorithm";
|
||||
String FINGERPRINT = "fingerprint";
|
||||
|
||||
String KEY_SIZE = "key_size";
|
||||
String CAN_SIGN = "can_sign";
|
||||
String CAN_ENCRYPT = "can_encrypt";
|
||||
String CAN_CERTIFY = "can_certify";
|
||||
String IS_REVOKED = "is_revoked";
|
||||
|
||||
String CREATION = "creation";
|
||||
String EXPIRY = "expiry";
|
||||
}
|
||||
|
||||
interface UserIdsColumns {
|
||||
String MASTER_KEY_ID = "master_key_id"; // foreign key to key_rings._ID
|
||||
String USER_ID = "user_id"; // not a database id
|
||||
String RANK = "rank"; // ONLY used for sorting! no key, no nothing!
|
||||
String IS_PRIMARY = "is_primary";
|
||||
String IS_REVOKED = "is_revoked";
|
||||
}
|
||||
|
||||
interface CertsColumns {
|
||||
String MASTER_KEY_ID = "master_key_id";
|
||||
String RANK = "rank";
|
||||
String KEY_ID_CERTIFIER = "key_id_certifier";
|
||||
String TYPE = "type";
|
||||
String VERIFIED = "verified";
|
||||
String CREATION = "creation";
|
||||
String DATA = "data";
|
||||
}
|
||||
|
||||
interface ApiAppsColumns {
|
||||
String PACKAGE_NAME = "package_name";
|
||||
String PACKAGE_SIGNATURE = "package_signature";
|
||||
}
|
||||
|
||||
interface ApiAppsAccountsColumns {
|
||||
String ACCOUNT_NAME = "account_name";
|
||||
String KEY_ID = "key_id"; // not a database id
|
||||
String ENCRYPTION_ALGORITHM = "encryption_algorithm";
|
||||
String HASH_ALORITHM = "hash_algorithm";
|
||||
String COMPRESSION = "compression";
|
||||
String PACKAGE_NAME = "package_name"; // foreign key to api_apps.package_name
|
||||
}
|
||||
|
||||
public static final class KeyTypes {
|
||||
public static final int PUBLIC = 0;
|
||||
public static final int SECRET = 1;
|
||||
}
|
||||
|
||||
public static final String CONTENT_AUTHORITY = Constants.PACKAGE_NAME + ".provider";
|
||||
|
||||
private static final Uri BASE_CONTENT_URI_INTERNAL = Uri
|
||||
.parse("content://" + CONTENT_AUTHORITY);
|
||||
|
||||
public static final String BASE_KEY_RINGS = "key_rings";
|
||||
public static final String BASE_DATA = "data";
|
||||
|
||||
public static final String PATH_UNIFIED = "unified";
|
||||
|
||||
public static final String PATH_FIND = "find";
|
||||
public static final String PATH_BY_EMAIL = "email";
|
||||
public static final String PATH_BY_SUBKEY = "subkey";
|
||||
|
||||
public static final String PATH_PUBLIC = "public";
|
||||
public static final String PATH_SECRET = "secret";
|
||||
public static final String PATH_USER_IDS = "user_ids";
|
||||
public static final String PATH_KEYS = "keys";
|
||||
public static final String PATH_CERTS = "certs";
|
||||
|
||||
public static final String BASE_API_APPS = "api_apps";
|
||||
public static final String PATH_ACCOUNTS = "accounts";
|
||||
|
||||
public static class KeyRings implements BaseColumns, KeysColumns, UserIdsColumns {
|
||||
public static final String MASTER_KEY_ID = KeysColumns.MASTER_KEY_ID;
|
||||
public static final String IS_REVOKED = KeysColumns.IS_REVOKED;
|
||||
public static final String VERIFIED = CertsColumns.VERIFIED;
|
||||
public static final String HAS_SECRET = "has_secret";
|
||||
|
||||
public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon()
|
||||
.appendPath(BASE_KEY_RINGS).build();
|
||||
|
||||
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.sufficientlysecure.openkeychain.key_ring";
|
||||
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.sufficientlysecure.openkeychain.key_ring";
|
||||
|
||||
public static Uri buildUnifiedKeyRingsUri() {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_UNIFIED).build();
|
||||
}
|
||||
|
||||
public static Uri buildGenericKeyRingUri(String masterKeyId) {
|
||||
return CONTENT_URI.buildUpon().appendPath(masterKeyId).build();
|
||||
}
|
||||
public static Uri buildUnifiedKeyRingUri(String masterKeyId) {
|
||||
return CONTENT_URI.buildUpon().appendPath(masterKeyId).appendPath(PATH_UNIFIED).build();
|
||||
}
|
||||
public static Uri buildUnifiedKeyRingUri(Uri uri) {
|
||||
return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)).appendPath(PATH_UNIFIED).build();
|
||||
}
|
||||
|
||||
public static Uri buildUnifiedKeyRingsFindByEmailUri(String email) {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_FIND).appendPath(PATH_BY_EMAIL).appendPath(email).build();
|
||||
}
|
||||
public static Uri buildUnifiedKeyRingsFindBySubkeyUri(String subkey) {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_FIND).appendPath(PATH_BY_SUBKEY).appendPath(subkey).build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class KeyRingData implements KeyRingsColumns, BaseColumns {
|
||||
public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon()
|
||||
.appendPath(BASE_KEY_RINGS).build();
|
||||
|
||||
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.sufficientlysecure.openkeychain.key_ring_data";
|
||||
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.sufficientlysecure.openkeychain.key_ring_data";
|
||||
|
||||
public static Uri buildPublicKeyRingUri() {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).build();
|
||||
}
|
||||
public static Uri buildPublicKeyRingUri(String masterKeyId) {
|
||||
return CONTENT_URI.buildUpon().appendPath(masterKeyId).appendPath(PATH_PUBLIC).build();
|
||||
}
|
||||
public static Uri buildPublicKeyRingUri(Uri uri) {
|
||||
return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)).appendPath(PATH_PUBLIC).build();
|
||||
}
|
||||
|
||||
public static Uri buildSecretKeyRingUri() {
|
||||
return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).build();
|
||||
}
|
||||
public static Uri buildSecretKeyRingUri(String masterKeyId) {
|
||||
return CONTENT_URI.buildUpon().appendPath(masterKeyId).appendPath(PATH_SECRET).build();
|
||||
}
|
||||
public static Uri buildSecretKeyRingUri(Uri uri) {
|
||||
return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)).appendPath(PATH_SECRET).build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Keys implements KeysColumns, BaseColumns {
|
||||
public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon()
|
||||
.appendPath(BASE_KEY_RINGS).build();
|
||||
|
||||
/**
|
||||
* Use if multiple items get returned
|
||||
*/
|
||||
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.sufficientlysecure.openkeychain.key";
|
||||
|
||||
/**
|
||||
* Use if a single item is returned
|
||||
*/
|
||||
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.sufficientlysecure.openkeychain.key";
|
||||
|
||||
public static Uri buildKeysUri(String masterKeyId) {
|
||||
return CONTENT_URI.buildUpon().appendPath(masterKeyId).appendPath(PATH_KEYS).build();
|
||||
}
|
||||
public static Uri buildKeysUri(Uri uri) {
|
||||
return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)).appendPath(PATH_KEYS).build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class UserIds implements UserIdsColumns, BaseColumns {
|
||||
public static final String VERIFIED = "verified";
|
||||
public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon()
|
||||
.appendPath(BASE_KEY_RINGS).build();
|
||||
|
||||
/**
|
||||
* Use if multiple items get returned
|
||||
*/
|
||||
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.sufficientlysecure.openkeychain.user_id";
|
||||
|
||||
/**
|
||||
* Use if a single item is returned
|
||||
*/
|
||||
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.sufficientlysecure.openkeychain.user_id";
|
||||
|
||||
public static Uri buildUserIdsUri(String masterKeyId) {
|
||||
return CONTENT_URI.buildUpon().appendPath(masterKeyId).appendPath(PATH_USER_IDS).build();
|
||||
}
|
||||
public static Uri buildUserIdsUri(Uri uri) {
|
||||
return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)).appendPath(PATH_USER_IDS).build();
|
||||
}
|
||||
}
|
||||
|
||||
public static class ApiApps implements ApiAppsColumns, BaseColumns {
|
||||
public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon()
|
||||
.appendPath(BASE_API_APPS).build();
|
||||
|
||||
/**
|
||||
* Use if multiple items get returned
|
||||
*/
|
||||
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.sufficientlysecure.openkeychain.api_apps";
|
||||
|
||||
/**
|
||||
* Use if a single item is returned
|
||||
*/
|
||||
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.sufficientlysecure.openkeychain.api_app";
|
||||
|
||||
public static Uri buildByPackageNameUri(String packageName) {
|
||||
return CONTENT_URI.buildUpon().appendEncodedPath(packageName).build();
|
||||
}
|
||||
}
|
||||
|
||||
public static class ApiAccounts implements ApiAppsAccountsColumns, BaseColumns {
|
||||
public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon()
|
||||
.appendPath(BASE_API_APPS).build();
|
||||
|
||||
/**
|
||||
* Use if multiple items get returned
|
||||
*/
|
||||
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.sufficientlysecure.openkeychain.api_app.accounts";
|
||||
|
||||
/**
|
||||
* Use if a single item is returned
|
||||
*/
|
||||
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.sufficientlysecure.openkeychain.api_app.account";
|
||||
|
||||
public static Uri buildBaseUri(String packageName) {
|
||||
return CONTENT_URI.buildUpon().appendEncodedPath(packageName).appendPath(PATH_ACCOUNTS)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static Uri buildByPackageAndAccountUri(String packageName, String accountName) {
|
||||
return CONTENT_URI.buildUpon().appendEncodedPath(packageName).appendPath(PATH_ACCOUNTS)
|
||||
.appendEncodedPath(accountName).build();
|
||||
}
|
||||
}
|
||||
|
||||
public static class Certs implements CertsColumns, BaseColumns {
|
||||
public static final String USER_ID = UserIdsColumns.USER_ID;
|
||||
public static final String SIGNER_UID = "signer_user_id";
|
||||
|
||||
public static final int VERIFIED_SECRET = 1;
|
||||
public static final int VERIFIED_SELF = 2;
|
||||
|
||||
public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon()
|
||||
.appendPath(BASE_KEY_RINGS).build();
|
||||
|
||||
public static Uri buildCertsUri(String masterKeyId) {
|
||||
return CONTENT_URI.buildUpon().appendPath(masterKeyId).appendPath(PATH_CERTS).build();
|
||||
}
|
||||
public static Uri buildCertsSpecificUri(String masterKeyId, String rank, String certifier) {
|
||||
return CONTENT_URI.buildUpon().appendPath(masterKeyId).appendPath(PATH_CERTS).appendPath(rank).appendPath(certifier).build();
|
||||
}
|
||||
public static Uri buildCertsUri(Uri uri) {
|
||||
return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)).appendPath(PATH_CERTS).build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class DataStream {
|
||||
public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon()
|
||||
.appendPath(BASE_DATA).build();
|
||||
|
||||
public static Uri buildDataStreamUri(String streamFilename) {
|
||||
return CONTENT_URI.buildUpon().appendPath(streamFilename).build();
|
||||
}
|
||||
}
|
||||
|
||||
private KeychainContract() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.provider;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.provider.BaseColumns;
|
||||
|
||||
import org.spongycastle.openpgp.PGPKeyRing;
|
||||
import org.spongycastle.openpgp.PGPPublicKeyRing;
|
||||
import org.spongycastle.openpgp.PGPSecretKeyRing;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpConversionHelper;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAppsColumns;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAppsAccountsColumns;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingsColumns;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeysColumns;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.UserIdsColumns;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.CertsColumns;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class KeychainDatabase extends SQLiteOpenHelper {
|
||||
private static final String DATABASE_NAME = "openkeychain.db";
|
||||
private static final int DATABASE_VERSION = 1;
|
||||
static Boolean apgHack = false;
|
||||
|
||||
public interface Tables {
|
||||
String KEY_RINGS_PUBLIC = "keyrings_public";
|
||||
String KEY_RINGS_SECRET = "keyrings_secret";
|
||||
String KEYS = "keys";
|
||||
String USER_IDS = "user_ids";
|
||||
String CERTS = "certs";
|
||||
String API_APPS = "api_apps";
|
||||
String API_ACCOUNTS = "api_accounts";
|
||||
}
|
||||
|
||||
private static final String CREATE_KEYRINGS_PUBLIC =
|
||||
"CREATE TABLE IF NOT EXISTS keyrings_public ("
|
||||
+ KeyRingsColumns.MASTER_KEY_ID + " INTEGER PRIMARY KEY,"
|
||||
+ KeyRingsColumns.KEY_RING_DATA + " BLOB"
|
||||
+ ")";
|
||||
|
||||
private static final String CREATE_KEYRINGS_SECRET =
|
||||
"CREATE TABLE IF NOT EXISTS keyrings_secret ("
|
||||
+ KeyRingsColumns.MASTER_KEY_ID + " INTEGER PRIMARY KEY,"
|
||||
+ KeyRingsColumns.KEY_RING_DATA + " BLOB,"
|
||||
+ "FOREIGN KEY(" + KeyRingsColumns.MASTER_KEY_ID + ") "
|
||||
+ "REFERENCES keyrings_public(" + KeyRingsColumns.MASTER_KEY_ID + ") ON DELETE CASCADE"
|
||||
+ ")";
|
||||
|
||||
private static final String CREATE_KEYS =
|
||||
"CREATE TABLE IF NOT EXISTS " + Tables.KEYS + " ("
|
||||
+ KeysColumns.MASTER_KEY_ID + " INTEGER, "
|
||||
+ KeysColumns.RANK + " INTEGER, "
|
||||
|
||||
+ KeysColumns.KEY_ID + " INTEGER, "
|
||||
+ KeysColumns.KEY_SIZE + " INTEGER, "
|
||||
+ KeysColumns.ALGORITHM + " INTEGER, "
|
||||
+ KeysColumns.FINGERPRINT + " BLOB, "
|
||||
|
||||
+ KeysColumns.CAN_CERTIFY + " BOOLEAN, "
|
||||
+ KeysColumns.CAN_SIGN + " BOOLEAN, "
|
||||
+ KeysColumns.CAN_ENCRYPT + " BOOLEAN, "
|
||||
+ KeysColumns.IS_REVOKED + " BOOLEAN, "
|
||||
|
||||
+ KeysColumns.CREATION + " INTEGER, "
|
||||
+ KeysColumns.EXPIRY + " INTEGER, "
|
||||
|
||||
+ "PRIMARY KEY(" + KeysColumns.MASTER_KEY_ID + ", " + KeysColumns.RANK + "),"
|
||||
+ "FOREIGN KEY(" + KeysColumns.MASTER_KEY_ID + ") REFERENCES "
|
||||
+ Tables.KEY_RINGS_PUBLIC + "(" + KeyRingsColumns.MASTER_KEY_ID + ") ON DELETE CASCADE"
|
||||
+ ")";
|
||||
|
||||
private static final String CREATE_USER_IDS =
|
||||
"CREATE TABLE IF NOT EXISTS " + Tables.USER_IDS + "("
|
||||
+ UserIdsColumns.MASTER_KEY_ID + " INTEGER, "
|
||||
+ UserIdsColumns.USER_ID + " CHARMANDER, "
|
||||
|
||||
+ UserIdsColumns.IS_PRIMARY + " BOOLEAN, "
|
||||
+ UserIdsColumns.IS_REVOKED + " BOOLEAN, "
|
||||
+ UserIdsColumns.RANK+ " INTEGER, "
|
||||
|
||||
+ "PRIMARY KEY(" + UserIdsColumns.MASTER_KEY_ID + ", " + UserIdsColumns.USER_ID + "), "
|
||||
+ "UNIQUE (" + UserIdsColumns.MASTER_KEY_ID + ", " + UserIdsColumns.RANK + "), "
|
||||
+ "FOREIGN KEY(" + UserIdsColumns.MASTER_KEY_ID + ") REFERENCES "
|
||||
+ Tables.KEY_RINGS_PUBLIC + "(" + KeyRingsColumns.MASTER_KEY_ID + ") ON DELETE CASCADE"
|
||||
+ ")";
|
||||
|
||||
private static final String CREATE_CERTS =
|
||||
"CREATE TABLE IF NOT EXISTS " + Tables.CERTS + "("
|
||||
+ CertsColumns.MASTER_KEY_ID + " INTEGER,"
|
||||
+ CertsColumns.RANK + " INTEGER, " // rank of certified uid
|
||||
|
||||
+ CertsColumns.KEY_ID_CERTIFIER + " INTEGER, " // certifying key
|
||||
+ CertsColumns.TYPE + " INTEGER, "
|
||||
+ CertsColumns.VERIFIED + " INTEGER, "
|
||||
+ CertsColumns.CREATION + " INTEGER, "
|
||||
|
||||
+ CertsColumns.DATA + " BLOB, "
|
||||
|
||||
+ "PRIMARY KEY(" + CertsColumns.MASTER_KEY_ID + ", " + CertsColumns.RANK + ", "
|
||||
+ CertsColumns.KEY_ID_CERTIFIER + "), "
|
||||
+ "FOREIGN KEY(" + CertsColumns.MASTER_KEY_ID + ") REFERENCES "
|
||||
+ Tables.KEY_RINGS_PUBLIC + "(" + KeyRingsColumns.MASTER_KEY_ID + ") ON DELETE CASCADE,"
|
||||
+ "FOREIGN KEY(" + CertsColumns.MASTER_KEY_ID + ", " + CertsColumns.RANK + ") REFERENCES "
|
||||
+ Tables.USER_IDS + "(" + UserIdsColumns.MASTER_KEY_ID + ", " + UserIdsColumns.RANK + ") ON DELETE CASCADE"
|
||||
+ ")";
|
||||
|
||||
private static final String CREATE_API_APPS = "CREATE TABLE IF NOT EXISTS " + Tables.API_APPS
|
||||
+ " (" + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
+ ApiAppsColumns.PACKAGE_NAME + " TEXT NOT NULL UNIQUE, "
|
||||
+ ApiAppsColumns.PACKAGE_SIGNATURE + " BLOB)";
|
||||
|
||||
private static final String CREATE_API_APPS_ACCOUNTS = "CREATE TABLE IF NOT EXISTS " + Tables.API_ACCOUNTS
|
||||
+ " (" + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
+ ApiAppsAccountsColumns.ACCOUNT_NAME + " TEXT NOT NULL, "
|
||||
+ ApiAppsAccountsColumns.KEY_ID + " INT64, "
|
||||
+ ApiAppsAccountsColumns.ENCRYPTION_ALGORITHM + " INTEGER, "
|
||||
+ ApiAppsAccountsColumns.HASH_ALORITHM + " INTEGER, "
|
||||
+ ApiAppsAccountsColumns.COMPRESSION + " INTEGER, "
|
||||
+ ApiAppsAccountsColumns.PACKAGE_NAME + " TEXT NOT NULL, "
|
||||
+ "UNIQUE(" + ApiAppsAccountsColumns.ACCOUNT_NAME + ", "
|
||||
+ ApiAppsAccountsColumns.PACKAGE_NAME + "), "
|
||||
+ "FOREIGN KEY(" + ApiAppsAccountsColumns.PACKAGE_NAME + ") REFERENCES "
|
||||
+ Tables.API_APPS + "(" + ApiAppsColumns.PACKAGE_NAME + ") ON DELETE CASCADE)";
|
||||
|
||||
KeychainDatabase(Context context) {
|
||||
super(context, DATABASE_NAME, null, DATABASE_VERSION);
|
||||
|
||||
// make sure this is only done once, on the first instance!
|
||||
boolean iAmIt = false;
|
||||
synchronized(apgHack) {
|
||||
if(!apgHack) {
|
||||
iAmIt = true;
|
||||
apgHack = true;
|
||||
}
|
||||
}
|
||||
// if it's us, do the import
|
||||
if(iAmIt)
|
||||
checkAndImportApg(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
Log.w(Constants.TAG, "Creating database...");
|
||||
|
||||
db.execSQL(CREATE_KEYRINGS_PUBLIC);
|
||||
db.execSQL(CREATE_KEYRINGS_SECRET);
|
||||
db.execSQL(CREATE_KEYS);
|
||||
db.execSQL(CREATE_USER_IDS);
|
||||
db.execSQL(CREATE_CERTS);
|
||||
db.execSQL(CREATE_API_APPS);
|
||||
db.execSQL(CREATE_API_APPS_ACCOUNTS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpen(SQLiteDatabase db) {
|
||||
super.onOpen(db);
|
||||
if (!db.isReadOnly()) {
|
||||
// Enable foreign key constraints
|
||||
db.execSQL("PRAGMA foreign_keys=ON;");
|
||||
// TODO remove, once we remove the "always migrate" debug stuff
|
||||
// db.execSQL("DROP TABLE certs;");
|
||||
// db.execSQL("DROP TABLE user_ids;");
|
||||
db.execSQL(CREATE_USER_IDS);
|
||||
db.execSQL(CREATE_CERTS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int old, int nu) {
|
||||
// don't care (this is version 1)
|
||||
}
|
||||
|
||||
/** This method tries to import data from a provided database.
|
||||
*
|
||||
* The sole assumptions made on this db are that there is a key_rings table
|
||||
* with a key_ring_data, a master_key_id and a type column, the latter of
|
||||
* which should be 1 for secret keys and 0 for public keys.
|
||||
*/
|
||||
public void checkAndImportApg(Context context) {
|
||||
|
||||
boolean hasApgDb = false; {
|
||||
// It's the Java way =(
|
||||
String[] dbs = context.databaseList();
|
||||
for(String db : dbs) {
|
||||
if(db.equals("apg.db")) {
|
||||
hasApgDb = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!hasApgDb)
|
||||
return;
|
||||
|
||||
Log.d(Constants.TAG, "apg.db exists! Importing...");
|
||||
|
||||
SQLiteDatabase db = new SQLiteOpenHelper(context, "apg.db", null, 1) {
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
// should never happen
|
||||
assert false;
|
||||
}
|
||||
@Override
|
||||
public void onDowngrade(SQLiteDatabase db, int old, int nu) {
|
||||
// don't care
|
||||
}
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int old, int nu) {
|
||||
// don't care either
|
||||
}
|
||||
}.getReadableDatabase();
|
||||
|
||||
// kill current!
|
||||
{ // TODO don't kill current.
|
||||
Log.d(Constants.TAG, "Truncating db...");
|
||||
SQLiteDatabase d = getWritableDatabase();
|
||||
d.execSQL("DELETE FROM keyrings_public");
|
||||
d.close();
|
||||
Log.d(Constants.TAG, "Ok.");
|
||||
}
|
||||
|
||||
Cursor c = null;
|
||||
try {
|
||||
// we insert in two steps: first, all public keys that have secret keys
|
||||
c = db.rawQuery("SELECT key_ring_data FROM key_rings WHERE type = 1 OR EXISTS ("
|
||||
+ " SELECT 1 FROM key_rings d2 WHERE key_rings.master_key_id = d2.master_key_id"
|
||||
+ " AND d2.type = 1) ORDER BY type ASC", null);
|
||||
Log.d(Constants.TAG, "Importing " + c.getCount() + " secret keyrings from apg.db...");
|
||||
for(int i = 0; i < c.getCount(); i++) {
|
||||
c.moveToPosition(i);
|
||||
byte[] data = c.getBlob(0);
|
||||
PGPKeyRing ring = PgpConversionHelper.BytesToPGPKeyRing(data);
|
||||
if(ring instanceof PGPPublicKeyRing)
|
||||
ProviderHelper.saveKeyRing(context, (PGPPublicKeyRing) ring);
|
||||
else if(ring instanceof PGPSecretKeyRing)
|
||||
ProviderHelper.saveKeyRing(context, (PGPSecretKeyRing) ring);
|
||||
else {
|
||||
Log.e(Constants.TAG, "Unknown blob data type!");
|
||||
}
|
||||
}
|
||||
|
||||
// afterwards, insert all keys, starting with public keys that have secret keys, then
|
||||
// secret keys, then all others. this order is necessary to ensure all certifications
|
||||
// are recognized properly.
|
||||
c = db.rawQuery("SELECT key_ring_data FROM key_rings ORDER BY (type = 0 AND EXISTS ("
|
||||
+ " SELECT 1 FROM key_rings d2 WHERE key_rings.master_key_id = d2.master_key_id AND"
|
||||
+ " d2.type = 1)) DESC, type DESC", null);
|
||||
// import from old database
|
||||
Log.d(Constants.TAG, "Importing " + c.getCount() + " keyrings from apg.db...");
|
||||
for(int i = 0; i < c.getCount(); i++) {
|
||||
c.moveToPosition(i);
|
||||
byte[] data = c.getBlob(0);
|
||||
PGPKeyRing ring = PgpConversionHelper.BytesToPGPKeyRing(data);
|
||||
if(ring instanceof PGPPublicKeyRing)
|
||||
ProviderHelper.saveKeyRing(context, (PGPPublicKeyRing) ring);
|
||||
else if(ring instanceof PGPSecretKeyRing)
|
||||
ProviderHelper.saveKeyRing(context, (PGPSecretKeyRing) ring);
|
||||
else {
|
||||
Log.e(Constants.TAG, "Unknown blob data type!");
|
||||
}
|
||||
}
|
||||
|
||||
} catch(IOException e) {
|
||||
Log.e(Constants.TAG, "Error importing apg db!", e);
|
||||
return;
|
||||
} finally {
|
||||
if(c != null)
|
||||
c.close();
|
||||
if(db != null)
|
||||
db.close();
|
||||
}
|
||||
|
||||
// TODO delete old db, if we are sure this works
|
||||
// context.deleteDatabase("apg.db");
|
||||
Log.d(Constants.TAG, "All done, (not) deleting apg.db");
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,752 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
* Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.provider;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentValues;
|
||||
import android.content.UriMatcher;
|
||||
import android.database.Cursor;
|
||||
import android.database.DatabaseUtils;
|
||||
import android.database.sqlite.SQLiteConstraintException;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteQueryBuilder;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAccounts;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingData;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.Keys;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.Certs;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
|
||||
public class KeychainProvider extends ContentProvider {
|
||||
|
||||
private static final int KEY_RINGS_UNIFIED = 101;
|
||||
private static final int KEY_RINGS_PUBLIC = 102;
|
||||
private static final int KEY_RINGS_SECRET = 103;
|
||||
|
||||
private static final int KEY_RING_UNIFIED = 200;
|
||||
private static final int KEY_RING_KEYS = 201;
|
||||
private static final int KEY_RING_USER_IDS = 202;
|
||||
private static final int KEY_RING_PUBLIC = 203;
|
||||
private static final int KEY_RING_SECRET = 204;
|
||||
private static final int KEY_RING_CERTS = 205;
|
||||
private static final int KEY_RING_CERTS_SPECIFIC = 206;
|
||||
|
||||
private static final int API_APPS = 301;
|
||||
private static final int API_APPS_BY_PACKAGE_NAME = 303;
|
||||
private static final int API_ACCOUNTS = 304;
|
||||
private static final int API_ACCOUNTS_BY_ACCOUNT_NAME = 306;
|
||||
|
||||
private static final int KEY_RINGS_FIND_BY_EMAIL = 400;
|
||||
private static final int KEY_RINGS_FIND_BY_SUBKEY = 401;
|
||||
|
||||
// private static final int DATA_STREAM = 501;
|
||||
|
||||
protected UriMatcher mUriMatcher;
|
||||
|
||||
/**
|
||||
* Build and return a {@link UriMatcher} that catches all {@link Uri} variations supported by
|
||||
* this {@link ContentProvider}.
|
||||
*/
|
||||
protected UriMatcher buildUriMatcher() {
|
||||
final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
|
||||
|
||||
String authority = KeychainContract.CONTENT_AUTHORITY;
|
||||
|
||||
/**
|
||||
* list key_rings
|
||||
*
|
||||
* <pre>
|
||||
* key_rings/unified
|
||||
* key_rings/public
|
||||
* </pre>
|
||||
*/
|
||||
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS
|
||||
+ "/" + KeychainContract.PATH_UNIFIED,
|
||||
KEY_RINGS_UNIFIED);
|
||||
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS
|
||||
+ "/" + KeychainContract.PATH_PUBLIC,
|
||||
KEY_RINGS_PUBLIC);
|
||||
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS
|
||||
+ "/" + KeychainContract.PATH_SECRET,
|
||||
KEY_RINGS_SECRET);
|
||||
|
||||
/**
|
||||
* find by criteria other than master key id
|
||||
*
|
||||
* key_rings/find/email/_
|
||||
* key_rings/find/subkey/_
|
||||
*
|
||||
*/
|
||||
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
|
||||
+ KeychainContract.PATH_FIND + "/" + KeychainContract.PATH_BY_EMAIL + "/*",
|
||||
KEY_RINGS_FIND_BY_EMAIL);
|
||||
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
|
||||
+ KeychainContract.PATH_FIND + "/" + KeychainContract.PATH_BY_SUBKEY + "/*",
|
||||
KEY_RINGS_FIND_BY_SUBKEY);
|
||||
|
||||
/**
|
||||
* list key_ring specifics
|
||||
*
|
||||
* <pre>
|
||||
* key_rings/_/unified
|
||||
* key_rings/_/keys
|
||||
* key_rings/_/user_ids
|
||||
* key_rings/_/public
|
||||
* key_rings/_/secret
|
||||
* key_rings/_/certs
|
||||
* key_rings/_/certs/_/_
|
||||
* </pre>
|
||||
*/
|
||||
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/"
|
||||
+ KeychainContract.PATH_UNIFIED,
|
||||
KEY_RING_UNIFIED);
|
||||
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/"
|
||||
+ KeychainContract.PATH_KEYS,
|
||||
KEY_RING_KEYS);
|
||||
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/"
|
||||
+ KeychainContract.PATH_USER_IDS,
|
||||
KEY_RING_USER_IDS);
|
||||
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/"
|
||||
+ KeychainContract.PATH_PUBLIC,
|
||||
KEY_RING_PUBLIC);
|
||||
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/"
|
||||
+ KeychainContract.PATH_SECRET,
|
||||
KEY_RING_SECRET);
|
||||
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/"
|
||||
+ KeychainContract.PATH_CERTS,
|
||||
KEY_RING_CERTS);
|
||||
matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/"
|
||||
+ KeychainContract.PATH_CERTS + "/*/*",
|
||||
KEY_RING_CERTS_SPECIFIC);
|
||||
|
||||
/**
|
||||
* API apps
|
||||
*
|
||||
* <pre>
|
||||
* api_apps
|
||||
* api_apps/_ (package name)
|
||||
*
|
||||
* api_apps/_/accounts
|
||||
* api_apps/_/accounts/_ (account name)
|
||||
* </pre>
|
||||
*/
|
||||
matcher.addURI(authority, KeychainContract.BASE_API_APPS, API_APPS);
|
||||
matcher.addURI(authority, KeychainContract.BASE_API_APPS + "/*", API_APPS_BY_PACKAGE_NAME);
|
||||
|
||||
matcher.addURI(authority, KeychainContract.BASE_API_APPS + "/*/"
|
||||
+ KeychainContract.PATH_ACCOUNTS, API_ACCOUNTS);
|
||||
matcher.addURI(authority, KeychainContract.BASE_API_APPS + "/*/"
|
||||
+ KeychainContract.PATH_ACCOUNTS + "/*", API_ACCOUNTS_BY_ACCOUNT_NAME);
|
||||
|
||||
/**
|
||||
* data stream
|
||||
*
|
||||
* <pre>
|
||||
* data / _
|
||||
* </pre>
|
||||
*/
|
||||
// matcher.addURI(authority, KeychainContract.BASE_DATA + "/*", DATA_STREAM);
|
||||
|
||||
return matcher;
|
||||
}
|
||||
|
||||
private KeychainDatabase mKeychainDatabase;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
mUriMatcher = buildUriMatcher();
|
||||
return true;
|
||||
}
|
||||
|
||||
public KeychainDatabase getDb() {
|
||||
if(mKeychainDatabase == null)
|
||||
mKeychainDatabase = new KeychainDatabase(getContext());
|
||||
return mKeychainDatabase;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public String getType(Uri uri) {
|
||||
final int match = mUriMatcher.match(uri);
|
||||
switch (match) {
|
||||
case KEY_RING_PUBLIC:
|
||||
return KeyRings.CONTENT_ITEM_TYPE;
|
||||
|
||||
case KEY_RING_KEYS:
|
||||
return Keys.CONTENT_TYPE;
|
||||
|
||||
case KEY_RING_USER_IDS:
|
||||
return UserIds.CONTENT_TYPE;
|
||||
|
||||
case KEY_RING_SECRET:
|
||||
return KeyRings.CONTENT_ITEM_TYPE;
|
||||
|
||||
case API_APPS:
|
||||
return ApiApps.CONTENT_TYPE;
|
||||
|
||||
case API_APPS_BY_PACKAGE_NAME:
|
||||
return ApiApps.CONTENT_ITEM_TYPE;
|
||||
|
||||
case API_ACCOUNTS:
|
||||
return ApiAccounts.CONTENT_TYPE;
|
||||
|
||||
case API_ACCOUNTS_BY_ACCOUNT_NAME:
|
||||
return ApiAccounts.CONTENT_ITEM_TYPE;
|
||||
|
||||
default:
|
||||
throw new UnsupportedOperationException("Unknown uri: " + uri);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
|
||||
String sortOrder) {
|
||||
Log.v(Constants.TAG, "query(uri=" + uri + ", proj=" + Arrays.toString(projection) + ")");
|
||||
|
||||
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
|
||||
|
||||
int match = mUriMatcher.match(uri);
|
||||
|
||||
// all query() parameters, for good measure
|
||||
String groupBy = null, having = null;
|
||||
|
||||
switch (match) {
|
||||
case KEY_RING_UNIFIED:
|
||||
case KEY_RINGS_UNIFIED:
|
||||
case KEY_RINGS_FIND_BY_EMAIL:
|
||||
case KEY_RINGS_FIND_BY_SUBKEY: {
|
||||
HashMap<String, String> projectionMap = new HashMap<String, String>();
|
||||
projectionMap.put(KeyRings._ID, Tables.KEYS + ".oid AS _id");
|
||||
projectionMap.put(KeyRings.MASTER_KEY_ID, Tables.KEYS + "." + Keys.MASTER_KEY_ID);
|
||||
projectionMap.put(KeyRings.KEY_ID, Keys.KEY_ID);
|
||||
projectionMap.put(KeyRings.KEY_SIZE, Keys.KEY_SIZE);
|
||||
projectionMap.put(KeyRings.IS_REVOKED, Tables.KEYS + "." + Keys.IS_REVOKED);
|
||||
projectionMap.put(KeyRings.CAN_CERTIFY, Keys.CAN_CERTIFY);
|
||||
projectionMap.put(KeyRings.CAN_ENCRYPT, Keys.CAN_ENCRYPT);
|
||||
projectionMap.put(KeyRings.CAN_SIGN, Keys.CAN_SIGN);
|
||||
projectionMap.put(KeyRings.CREATION, Tables.KEYS + "." + Keys.CREATION);
|
||||
projectionMap.put(KeyRings.EXPIRY, Keys.EXPIRY);
|
||||
projectionMap.put(KeyRings.ALGORITHM, Keys.ALGORITHM);
|
||||
projectionMap.put(KeyRings.FINGERPRINT, Keys.FINGERPRINT);
|
||||
projectionMap.put(KeyRings.USER_ID, UserIds.USER_ID);
|
||||
projectionMap.put(KeyRings.VERIFIED, KeyRings.VERIFIED);
|
||||
projectionMap.put(KeyRings.HAS_SECRET, "(" + Tables.KEY_RINGS_SECRET + "." + KeyRings.MASTER_KEY_ID + " IS NOT NULL) AS " + KeyRings.HAS_SECRET);
|
||||
qb.setProjectionMap(projectionMap);
|
||||
|
||||
qb.setTables(
|
||||
Tables.KEYS
|
||||
+ " INNER JOIN " + Tables.USER_IDS + " ON ("
|
||||
+ Tables.KEYS + "." + Keys.MASTER_KEY_ID
|
||||
+ " = "
|
||||
+ Tables.USER_IDS + "." + UserIds.MASTER_KEY_ID
|
||||
+ " AND " + Tables.USER_IDS + "." + UserIds.RANK + " = 0"
|
||||
+ ") LEFT JOIN " + Tables.KEY_RINGS_SECRET + " ON ("
|
||||
+ Tables.KEYS + "." + Keys.MASTER_KEY_ID
|
||||
+ " = "
|
||||
+ Tables.KEY_RINGS_SECRET + "." + KeyRings.MASTER_KEY_ID
|
||||
+ ") LEFT JOIN " + Tables.CERTS + " ON ("
|
||||
+ Tables.KEYS + "." + Keys.MASTER_KEY_ID
|
||||
+ " = "
|
||||
+ Tables.CERTS + "." + KeyRings.MASTER_KEY_ID
|
||||
+ " AND " + Tables.CERTS + "." + Certs.VERIFIED
|
||||
+ " = " + Certs.VERIFIED_SECRET
|
||||
+ ")"
|
||||
);
|
||||
qb.appendWhere(Tables.KEYS + "." + Keys.RANK + " = 0");
|
||||
// in case there are multiple verifying certificates
|
||||
groupBy = Tables.KEYS + "." + Keys.MASTER_KEY_ID;
|
||||
|
||||
switch(match) {
|
||||
case KEY_RING_UNIFIED: {
|
||||
qb.appendWhere(" AND " + Tables.KEYS + "." + Keys.MASTER_KEY_ID + " = ");
|
||||
qb.appendWhereEscapeString(uri.getPathSegments().get(1));
|
||||
break;
|
||||
}
|
||||
case KEY_RINGS_FIND_BY_SUBKEY: {
|
||||
try {
|
||||
String subkey = Long.valueOf(uri.getLastPathSegment()).toString();
|
||||
qb.appendWhere(" AND EXISTS ("
|
||||
+ " SELECT 1 FROM " + Tables.KEYS + " AS tmp"
|
||||
+ " WHERE tmp." + UserIds.MASTER_KEY_ID
|
||||
+ " = " + Tables.KEYS + "." + Keys.MASTER_KEY_ID
|
||||
+ " AND tmp." + Keys.KEY_ID + " = " + subkey + ""
|
||||
+ ")");
|
||||
} catch(NumberFormatException e) {
|
||||
Log.e(Constants.TAG, "Malformed find by subkey query!", e);
|
||||
qb.appendWhere(" AND 0");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case KEY_RINGS_FIND_BY_EMAIL: {
|
||||
String chunks[] = uri.getLastPathSegment().split(" *, *");
|
||||
boolean gotCondition = false;
|
||||
String emailWhere = "";
|
||||
// JAVA ♥
|
||||
for (int i = 0; i < chunks.length; ++i) {
|
||||
if (chunks[i].length() == 0) {
|
||||
continue;
|
||||
}
|
||||
if (i != 0) {
|
||||
emailWhere += " OR ";
|
||||
}
|
||||
emailWhere += "tmp." + UserIds.USER_ID + " LIKE ";
|
||||
// match '*<email>', so it has to be at the *end* of the user id
|
||||
emailWhere += DatabaseUtils.sqlEscapeString("%<" + chunks[i] + ">");
|
||||
gotCondition = true;
|
||||
}
|
||||
if(gotCondition) {
|
||||
qb.appendWhere(" AND EXISTS ("
|
||||
+ " SELECT 1 FROM " + Tables.USER_IDS + " AS tmp"
|
||||
+ " WHERE tmp." + UserIds.MASTER_KEY_ID
|
||||
+ " = " + Tables.KEYS + "." + Keys.MASTER_KEY_ID
|
||||
+ " AND (" + emailWhere + ")"
|
||||
+ ")");
|
||||
} else {
|
||||
// TODO better way to do this?
|
||||
Log.e(Constants.TAG, "Malformed find by email query!");
|
||||
qb.appendWhere(" AND 0");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(sortOrder)) {
|
||||
sortOrder =
|
||||
Tables.KEY_RINGS_SECRET + "." + KeyRings.MASTER_KEY_ID + " IS NULL ASC, "
|
||||
+ Tables.USER_IDS + "." + UserIds.USER_ID + " ASC";
|
||||
}
|
||||
|
||||
// uri to watch is all /key_rings/
|
||||
uri = KeyRings.CONTENT_URI;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case KEY_RING_KEYS: {
|
||||
HashMap<String, String> projectionMap = new HashMap<String, String>();
|
||||
projectionMap.put(Keys._ID, Tables.KEYS + ".oid AS _id");
|
||||
projectionMap.put(Keys.MASTER_KEY_ID, Tables.KEYS + "." + Keys.MASTER_KEY_ID);
|
||||
projectionMap.put(Keys.RANK, Tables.KEYS + "." + Keys.RANK);
|
||||
projectionMap.put(Keys.KEY_ID, Keys.KEY_ID);
|
||||
projectionMap.put(Keys.KEY_SIZE, Keys.KEY_SIZE);
|
||||
projectionMap.put(Keys.IS_REVOKED, Keys.IS_REVOKED);
|
||||
projectionMap.put(Keys.CAN_CERTIFY, Keys.CAN_CERTIFY);
|
||||
projectionMap.put(Keys.CAN_ENCRYPT, Keys.CAN_ENCRYPT);
|
||||
projectionMap.put(Keys.CAN_SIGN, Keys.CAN_SIGN);
|
||||
projectionMap.put(Keys.CREATION, Keys.CREATION);
|
||||
projectionMap.put(Keys.EXPIRY, Keys.EXPIRY);
|
||||
projectionMap.put(Keys.ALGORITHM, Keys.ALGORITHM);
|
||||
projectionMap.put(Keys.FINGERPRINT, Keys.FINGERPRINT);
|
||||
qb.setProjectionMap(projectionMap);
|
||||
|
||||
qb.setTables(Tables.KEYS);
|
||||
qb.appendWhere(Keys.MASTER_KEY_ID + " = ");
|
||||
qb.appendWhereEscapeString(uri.getPathSegments().get(1));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case KEY_RING_USER_IDS: {
|
||||
HashMap<String, String> projectionMap = new HashMap<String, String>();
|
||||
projectionMap.put(UserIds._ID, Tables.USER_IDS + ".oid AS _id");
|
||||
projectionMap.put(UserIds.MASTER_KEY_ID, Tables.USER_IDS + "." + UserIds.MASTER_KEY_ID);
|
||||
projectionMap.put(UserIds.USER_ID, Tables.USER_IDS + "." + UserIds.USER_ID);
|
||||
projectionMap.put(UserIds.RANK, Tables.USER_IDS + "." + UserIds.RANK);
|
||||
projectionMap.put(UserIds.IS_PRIMARY, Tables.USER_IDS + "." + UserIds.IS_PRIMARY);
|
||||
projectionMap.put(UserIds.IS_REVOKED, Tables.USER_IDS + "." + UserIds.IS_REVOKED);
|
||||
// we take the minimum (>0) here, where "1" is "verified by known secret key"
|
||||
projectionMap.put(UserIds.VERIFIED, "MIN(" + Certs.VERIFIED + ") AS " + UserIds.VERIFIED);
|
||||
qb.setProjectionMap(projectionMap);
|
||||
|
||||
qb.setTables(Tables.USER_IDS
|
||||
+ " LEFT JOIN " + Tables.CERTS + " ON ("
|
||||
+ Tables.USER_IDS + "." + UserIds.MASTER_KEY_ID + " = "
|
||||
+ Tables.CERTS + "." + Certs.MASTER_KEY_ID
|
||||
+ " AND " + Tables.USER_IDS + "." + UserIds.RANK + " = "
|
||||
+ Tables.CERTS + "." + Certs.RANK
|
||||
+ " AND " + Tables.CERTS + "." + Certs.VERIFIED + " > 0"
|
||||
+ ")");
|
||||
groupBy = Tables.USER_IDS + "." + UserIds.RANK;
|
||||
|
||||
qb.appendWhere(Tables.USER_IDS + "." + UserIds.MASTER_KEY_ID + " = ");
|
||||
qb.appendWhereEscapeString(uri.getPathSegments().get(1));
|
||||
|
||||
if (TextUtils.isEmpty(sortOrder)) {
|
||||
sortOrder = Tables.USER_IDS + "." + UserIds.RANK + " ASC";
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
case KEY_RINGS_PUBLIC:
|
||||
case KEY_RING_PUBLIC: {
|
||||
HashMap<String, String> projectionMap = new HashMap<String, String>();
|
||||
projectionMap.put(KeyRingData._ID, Tables.KEY_RINGS_PUBLIC + ".oid AS _id");
|
||||
projectionMap.put(KeyRingData.MASTER_KEY_ID, KeyRingData.MASTER_KEY_ID);
|
||||
projectionMap.put(KeyRingData.KEY_RING_DATA, KeyRingData.KEY_RING_DATA);
|
||||
qb.setProjectionMap(projectionMap);
|
||||
|
||||
qb.setTables(Tables.KEY_RINGS_PUBLIC);
|
||||
|
||||
if(match == KEY_RING_PUBLIC) {
|
||||
qb.appendWhere(KeyRings.MASTER_KEY_ID + " = ");
|
||||
qb.appendWhereEscapeString(uri.getPathSegments().get(1));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case KEY_RINGS_SECRET:
|
||||
case KEY_RING_SECRET: {
|
||||
HashMap<String, String> projectionMap = new HashMap<String, String>();
|
||||
projectionMap.put(KeyRingData._ID, Tables.KEY_RINGS_SECRET + ".oid AS _id");
|
||||
projectionMap.put(KeyRingData.MASTER_KEY_ID, KeyRingData.MASTER_KEY_ID);
|
||||
projectionMap.put(KeyRingData.KEY_RING_DATA, KeyRingData.KEY_RING_DATA);
|
||||
qb.setProjectionMap(projectionMap);
|
||||
|
||||
qb.setTables(Tables.KEY_RINGS_SECRET);
|
||||
|
||||
if(match == KEY_RING_SECRET) {
|
||||
qb.appendWhere(KeyRings.MASTER_KEY_ID + " = ");
|
||||
qb.appendWhereEscapeString(uri.getPathSegments().get(1));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case KEY_RING_CERTS:
|
||||
case KEY_RING_CERTS_SPECIFIC: {
|
||||
HashMap<String, String> projectionMap = new HashMap<String, String>();
|
||||
projectionMap.put(Certs._ID, Tables.CERTS + ".oid AS " + Certs._ID);
|
||||
projectionMap.put(Certs.MASTER_KEY_ID, Tables.CERTS + "." + Certs.MASTER_KEY_ID);
|
||||
projectionMap.put(Certs.RANK, Tables.CERTS + "." + Certs.RANK);
|
||||
projectionMap.put(Certs.VERIFIED, Tables.CERTS + "." + Certs.VERIFIED);
|
||||
projectionMap.put(Certs.TYPE, Tables.CERTS + "." + Certs.TYPE);
|
||||
projectionMap.put(Certs.CREATION, Tables.CERTS + "." + Certs.CREATION);
|
||||
projectionMap.put(Certs.KEY_ID_CERTIFIER, Tables.CERTS + "." + Certs.KEY_ID_CERTIFIER);
|
||||
projectionMap.put(Certs.DATA, Tables.CERTS + "." + Certs.DATA);
|
||||
projectionMap.put(Certs.USER_ID, Tables.USER_IDS + "." + UserIds.USER_ID);
|
||||
projectionMap.put(Certs.SIGNER_UID, "signer." + UserIds.USER_ID + " AS " + Certs.SIGNER_UID);
|
||||
qb.setProjectionMap(projectionMap);
|
||||
|
||||
qb.setTables(Tables.CERTS
|
||||
+ " JOIN " + Tables.USER_IDS + " ON ("
|
||||
+ Tables.CERTS + "." + Certs.MASTER_KEY_ID + " = "
|
||||
+ Tables.USER_IDS + "." + UserIds.MASTER_KEY_ID
|
||||
+ " AND "
|
||||
+ Tables.CERTS + "." + Certs.RANK + " = "
|
||||
+ Tables.USER_IDS + "." + UserIds.RANK
|
||||
+ ") LEFT JOIN " + Tables.USER_IDS + " AS signer ON ("
|
||||
+ Tables.CERTS + "." + Certs.KEY_ID_CERTIFIER + " = "
|
||||
+ "signer." + UserIds.MASTER_KEY_ID
|
||||
+ " AND "
|
||||
+ "signer." + Keys.RANK + " = 0"
|
||||
+ ")");
|
||||
|
||||
groupBy = Tables.CERTS + "." + Certs.RANK + ", "
|
||||
+ Tables.CERTS + "." + Certs.KEY_ID_CERTIFIER;
|
||||
|
||||
qb.appendWhere(Tables.CERTS + "." + Certs.MASTER_KEY_ID + " = ");
|
||||
qb.appendWhereEscapeString(uri.getPathSegments().get(1));
|
||||
if(match == KEY_RING_CERTS_SPECIFIC) {
|
||||
qb.appendWhere(" AND " + Tables.CERTS + "." + Certs.RANK + " = ");
|
||||
qb.appendWhereEscapeString(uri.getPathSegments().get(3));
|
||||
qb.appendWhere(" AND " + Tables.CERTS + "." + Certs.KEY_ID_CERTIFIER+ " = ");
|
||||
qb.appendWhereEscapeString(uri.getPathSegments().get(4));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case API_APPS:
|
||||
qb.setTables(Tables.API_APPS);
|
||||
|
||||
break;
|
||||
case API_APPS_BY_PACKAGE_NAME:
|
||||
qb.setTables(Tables.API_APPS);
|
||||
qb.appendWhere(ApiApps.PACKAGE_NAME + " = ");
|
||||
qb.appendWhereEscapeString(uri.getLastPathSegment());
|
||||
|
||||
break;
|
||||
case API_ACCOUNTS:
|
||||
qb.setTables(Tables.API_ACCOUNTS);
|
||||
qb.appendWhere(Tables.API_ACCOUNTS + "." + ApiAccounts.PACKAGE_NAME + " = ");
|
||||
qb.appendWhereEscapeString(uri.getPathSegments().get(1));
|
||||
|
||||
break;
|
||||
case API_ACCOUNTS_BY_ACCOUNT_NAME:
|
||||
qb.setTables(Tables.API_ACCOUNTS);
|
||||
qb.appendWhere(Tables.API_ACCOUNTS + "." + ApiAccounts.PACKAGE_NAME + " = ");
|
||||
qb.appendWhereEscapeString(uri.getPathSegments().get(1));
|
||||
|
||||
qb.appendWhere(" AND " + Tables.API_ACCOUNTS + "." + ApiAccounts.ACCOUNT_NAME + " = ");
|
||||
qb.appendWhereEscapeString(uri.getLastPathSegment());
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown URI " + uri + " (" + match + ")");
|
||||
|
||||
}
|
||||
|
||||
// If no sort order is specified use the default
|
||||
String orderBy;
|
||||
if (TextUtils.isEmpty(sortOrder)) {
|
||||
orderBy = null;
|
||||
} else {
|
||||
orderBy = sortOrder;
|
||||
}
|
||||
|
||||
SQLiteDatabase db = getDb().getReadableDatabase();
|
||||
Cursor c = qb.query(db, projection, selection, selectionArgs, groupBy, having, orderBy);
|
||||
|
||||
// Tell the cursor what uri to watch, so it knows when its source data changes
|
||||
c.setNotificationUri(getContext().getContentResolver(), uri);
|
||||
|
||||
if (Constants.DEBUG) {
|
||||
Log.d(Constants.TAG,
|
||||
"Query: "
|
||||
+ qb.buildQuery(projection, selection, selectionArgs, null, null,
|
||||
orderBy, null));
|
||||
Log.d(Constants.TAG, "Cursor: " + DatabaseUtils.dumpCursorToString(c));
|
||||
}
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Uri insert(Uri uri, ContentValues values) {
|
||||
Log.d(Constants.TAG, "insert(uri=" + uri + ", values=" + values.toString() + ")");
|
||||
|
||||
final SQLiteDatabase db = getDb().getWritableDatabase();
|
||||
|
||||
Uri rowUri = null;
|
||||
Long keyId = null;
|
||||
try {
|
||||
final int match = mUriMatcher.match(uri);
|
||||
|
||||
switch (match) {
|
||||
case KEY_RING_PUBLIC:
|
||||
db.insertOrThrow(Tables.KEY_RINGS_PUBLIC, null, values);
|
||||
keyId = values.getAsLong(KeyRings.MASTER_KEY_ID);
|
||||
break;
|
||||
|
||||
case KEY_RING_SECRET:
|
||||
db.insertOrThrow(Tables.KEY_RINGS_SECRET, null, values);
|
||||
keyId = values.getAsLong(KeyRings.MASTER_KEY_ID);
|
||||
break;
|
||||
|
||||
case KEY_RING_KEYS:
|
||||
Log.d(Constants.TAG, "keys");
|
||||
db.insertOrThrow(Tables.KEYS, null, values);
|
||||
keyId = values.getAsLong(Keys.MASTER_KEY_ID);
|
||||
break;
|
||||
|
||||
case KEY_RING_USER_IDS:
|
||||
db.insertOrThrow(Tables.USER_IDS, null, values);
|
||||
keyId = values.getAsLong(UserIds.MASTER_KEY_ID);
|
||||
break;
|
||||
|
||||
case KEY_RING_CERTS:
|
||||
// we replace here, keeping only the latest signature
|
||||
// TODO this would be better handled in saveKeyRing directly!
|
||||
db.replaceOrThrow(Tables.CERTS, null, values);
|
||||
keyId = values.getAsLong(Certs.MASTER_KEY_ID);
|
||||
break;
|
||||
|
||||
case API_APPS:
|
||||
db.insertOrThrow(Tables.API_APPS, null, values);
|
||||
break;
|
||||
|
||||
case API_ACCOUNTS:
|
||||
// set foreign key automatically based on given uri
|
||||
// e.g., api_apps/com.example.app/accounts/
|
||||
String packageName = uri.getPathSegments().get(1);
|
||||
values.put(ApiAccounts.PACKAGE_NAME, packageName);
|
||||
|
||||
Log.d(Constants.TAG, "provider packageName: " + packageName);
|
||||
|
||||
db.insertOrThrow(Tables.API_ACCOUNTS, null, values);
|
||||
// TODO: this is wrong:
|
||||
// rowUri = ApiAccounts.buildIdUri(Long.toString(rowId));
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new UnsupportedOperationException("Unknown uri: " + uri);
|
||||
}
|
||||
|
||||
if(keyId != null) {
|
||||
uri = KeyRings.buildGenericKeyRingUri(keyId.toString());
|
||||
rowUri = uri;
|
||||
}
|
||||
|
||||
// notify of changes in db
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
|
||||
} catch (SQLiteConstraintException e) {
|
||||
Log.e(Constants.TAG, "Constraint exception on insert! Entry already existing?", e);
|
||||
}
|
||||
|
||||
return rowUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public int delete(Uri uri, String additionalSelection, String[] selectionArgs) {
|
||||
Log.v(Constants.TAG, "delete(uri=" + uri + ")");
|
||||
|
||||
final SQLiteDatabase db = getDb().getWritableDatabase();
|
||||
|
||||
int count;
|
||||
final int match = mUriMatcher.match(uri);
|
||||
|
||||
switch (match) {
|
||||
case KEY_RING_PUBLIC: {
|
||||
@SuppressWarnings("ConstantConditions") // ensured by uriMatcher above
|
||||
String selection = KeyRings.MASTER_KEY_ID + " = " + uri.getPathSegments().get(1);
|
||||
if (!TextUtils.isEmpty(additionalSelection)) {
|
||||
selection += " AND (" + additionalSelection + ")";
|
||||
}
|
||||
// corresponding keys and userIds are deleted by ON DELETE CASCADE
|
||||
count = db.delete(Tables.KEY_RINGS_PUBLIC, selection, selectionArgs);
|
||||
uri = KeyRings.buildGenericKeyRingUri(uri.getPathSegments().get(1));
|
||||
break;
|
||||
}
|
||||
case KEY_RING_SECRET: {
|
||||
@SuppressWarnings("ConstantConditions") // ensured by uriMatcher above
|
||||
String selection = KeyRings.MASTER_KEY_ID + " = " + uri.getPathSegments().get(1);
|
||||
if (!TextUtils.isEmpty(additionalSelection)) {
|
||||
selection += " AND (" + additionalSelection + ")";
|
||||
}
|
||||
count = db.delete(Tables.KEY_RINGS_SECRET, selection, selectionArgs);
|
||||
uri = KeyRings.buildGenericKeyRingUri(uri.getPathSegments().get(1));
|
||||
break;
|
||||
}
|
||||
|
||||
case API_APPS_BY_PACKAGE_NAME:
|
||||
count = db.delete(Tables.API_APPS, buildDefaultApiAppsSelection(uri, additionalSelection),
|
||||
selectionArgs);
|
||||
break;
|
||||
case API_ACCOUNTS_BY_ACCOUNT_NAME:
|
||||
count = db.delete(Tables.API_ACCOUNTS, buildDefaultApiAccountsSelection(uri, additionalSelection),
|
||||
selectionArgs);
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedOperationException("Unknown uri: " + uri);
|
||||
}
|
||||
|
||||
// notify of changes in db
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
|
||||
Log.v(Constants.TAG, "update(uri=" + uri + ", values=" + values.toString() + ")");
|
||||
|
||||
final SQLiteDatabase db = mKeychainDatabase.getWritableDatabase();
|
||||
|
||||
String defaultSelection = null;
|
||||
int count = 0;
|
||||
try {
|
||||
final int match = mUriMatcher.match(uri);
|
||||
switch (match) {
|
||||
case API_APPS_BY_PACKAGE_NAME:
|
||||
count = db.update(Tables.API_APPS, values,
|
||||
buildDefaultApiAppsSelection(uri, selection), selectionArgs);
|
||||
break;
|
||||
case API_ACCOUNTS_BY_ACCOUNT_NAME:
|
||||
count = db.update(Tables.API_ACCOUNTS, values,
|
||||
buildDefaultApiAccountsSelection(uri, selection), selectionArgs);
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedOperationException("Unknown uri: " + uri);
|
||||
}
|
||||
|
||||
// notify of changes in db
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
|
||||
} catch (SQLiteConstraintException e) {
|
||||
Log.e(Constants.TAG, "Constraint exception on update! Entry already existing?");
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build default selection statement for API apps. If no extra selection is specified only build
|
||||
* where clause with rowId
|
||||
*
|
||||
* @param uri
|
||||
* @param selection
|
||||
* @return
|
||||
*/
|
||||
private String buildDefaultApiAppsSelection(Uri uri, String selection) {
|
||||
String packageName = DatabaseUtils.sqlEscapeString(uri.getLastPathSegment());
|
||||
|
||||
String andSelection = "";
|
||||
if (!TextUtils.isEmpty(selection)) {
|
||||
andSelection = " AND (" + selection + ")";
|
||||
}
|
||||
|
||||
return ApiApps.PACKAGE_NAME + "=" + packageName + andSelection;
|
||||
}
|
||||
|
||||
private String buildDefaultApiAccountsSelection(Uri uri, String selection) {
|
||||
String packageName = DatabaseUtils.sqlEscapeString(uri.getPathSegments().get(1));
|
||||
String accountName = DatabaseUtils.sqlEscapeString(uri.getLastPathSegment());
|
||||
|
||||
String andSelection = "";
|
||||
if (!TextUtils.isEmpty(selection)) {
|
||||
andSelection = " AND (" + selection + ")";
|
||||
}
|
||||
|
||||
return ApiAccounts.PACKAGE_NAME + "=" + packageName + " AND "
|
||||
+ ApiAccounts.ACCOUNT_NAME + "=" + accountName
|
||||
+ andSelection;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.provider;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.provider.BaseColumns;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
|
||||
public class KeychainServiceBlobContract {
|
||||
|
||||
interface BlobsColumns {
|
||||
String KEY = "key";
|
||||
}
|
||||
|
||||
public static final String CONTENT_AUTHORITY = Constants.PACKAGE_NAME + ".blobs";
|
||||
|
||||
private static final Uri BASE_CONTENT_URI = Uri.parse("content://" + CONTENT_AUTHORITY);
|
||||
|
||||
public static class Blobs implements BlobsColumns, BaseColumns {
|
||||
public static final Uri CONTENT_URI = BASE_CONTENT_URI;
|
||||
}
|
||||
|
||||
private KeychainServiceBlobContract() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
* Copyright (C) 2011 Markus Doits <markus.doits@googlemail.com>
|
||||
*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.provider;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.provider.BaseColumns;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainServiceBlobContract.BlobsColumns;
|
||||
|
||||
public class KeychainServiceBlobDatabase extends SQLiteOpenHelper {
|
||||
private static final String DATABASE_NAME = "openkeychain_blob.db";
|
||||
private static final int DATABASE_VERSION = 2;
|
||||
|
||||
public static final String TABLE = "data";
|
||||
|
||||
public KeychainServiceBlobDatabase(Context context) {
|
||||
super(context, DATABASE_NAME, null, DATABASE_VERSION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
db.execSQL("CREATE TABLE " + TABLE + " ( " + BaseColumns._ID
|
||||
+ " INTEGER PRIMARY KEY AUTOINCREMENT, " + BlobsColumns.KEY + " TEXT NOT NULL)");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
// no upgrade necessary yet
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
* Copyright (C) 2011 Markus Doits <markus.doits@googlemail.com>
|
||||
*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.provider;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentUris;
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.BaseColumns;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainServiceBlobContract.Blobs;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainServiceBlobContract.BlobsColumns;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class KeychainServiceBlobProvider extends ContentProvider {
|
||||
private static final String STORE_PATH = Constants.Path.APP_DIR + "/KeychainBlobs";
|
||||
|
||||
private KeychainServiceBlobDatabase mBlobDatabase = null;
|
||||
|
||||
public KeychainServiceBlobProvider() {
|
||||
File dir = new File(STORE_PATH);
|
||||
dir.mkdirs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
mBlobDatabase = new KeychainServiceBlobDatabase(getContext());
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Uri insert(Uri uri, ContentValues ignored) {
|
||||
// ContentValues are actually ignored, because we want to store a blob with no more
|
||||
// information but have to create an record with the password generated here first
|
||||
ContentValues vals = new ContentValues();
|
||||
|
||||
// Insert a random key in the database. This has to provided by the caller when updating or
|
||||
// getting the blob
|
||||
String password = UUID.randomUUID().toString();
|
||||
vals.put(BlobsColumns.KEY, password);
|
||||
|
||||
SQLiteDatabase db = mBlobDatabase.getWritableDatabase();
|
||||
long newRowId = db.insert(KeychainServiceBlobDatabase.TABLE, null, vals);
|
||||
Uri insertedUri = ContentUris.withAppendedId(Blobs.CONTENT_URI, newRowId);
|
||||
|
||||
return Uri.withAppendedPath(insertedUri, password);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public ParcelFileDescriptor openFile(Uri uri, String mode) throws SecurityException,
|
||||
FileNotFoundException {
|
||||
Log.d(Constants.TAG, "openFile() called with uri: " + uri.toString() + " and mode: " + mode);
|
||||
|
||||
List<String> segments = uri.getPathSegments();
|
||||
if (segments.size() < 2) {
|
||||
throw new SecurityException("Password not found in URI");
|
||||
}
|
||||
String id = segments.get(0);
|
||||
String key = segments.get(1);
|
||||
|
||||
Log.d(Constants.TAG, "Got id: " + id + " and key: " + key);
|
||||
|
||||
// get the data
|
||||
SQLiteDatabase db = mBlobDatabase.getReadableDatabase();
|
||||
Cursor result = db.query(KeychainServiceBlobDatabase.TABLE, new String[]{BaseColumns._ID},
|
||||
BaseColumns._ID + " = ? and " + BlobsColumns.KEY + " = ?",
|
||||
new String[]{id, key}, null, null, null);
|
||||
|
||||
if (result.getCount() == 0) {
|
||||
// either the key is wrong or no id exists
|
||||
throw new FileNotFoundException("No file found with that ID and/or password");
|
||||
}
|
||||
|
||||
File targetFile = new File(STORE_PATH, id);
|
||||
if (mode.equals("w")) {
|
||||
Log.d(Constants.TAG, "Try to open file w");
|
||||
if (!targetFile.exists()) {
|
||||
try {
|
||||
targetFile.createNewFile();
|
||||
} catch (IOException e) {
|
||||
Log.e(Constants.TAG, "Got IEOException on creating new file", e);
|
||||
throw new FileNotFoundException("Could not create file to write to");
|
||||
}
|
||||
}
|
||||
return ParcelFileDescriptor.open(targetFile, ParcelFileDescriptor.MODE_WRITE_ONLY
|
||||
| ParcelFileDescriptor.MODE_TRUNCATE);
|
||||
} else if (mode.equals("r")) {
|
||||
Log.d(Constants.TAG, "Try to open file r");
|
||||
if (!targetFile.exists()) {
|
||||
throw new FileNotFoundException("Error: Could not find the file requested");
|
||||
}
|
||||
return ParcelFileDescriptor.open(targetFile, ParcelFileDescriptor.MODE_READ_ONLY);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public String getType(Uri uri) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
|
||||
String sortOrder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public int delete(Uri uri, String selection, String[] selectionArgs) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,673 @@
|
||||
/*
|
||||
* Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.provider;
|
||||
|
||||
import android.content.ContentProviderOperation;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.OperationApplicationException;
|
||||
import android.database.Cursor;
|
||||
import android.database.DatabaseUtils;
|
||||
import android.net.Uri;
|
||||
import android.os.RemoteException;
|
||||
|
||||
import org.spongycastle.bcpg.ArmoredOutputStream;
|
||||
import org.spongycastle.openpgp.PGPException;
|
||||
import org.spongycastle.openpgp.PGPKeyRing;
|
||||
import org.spongycastle.openpgp.PGPPublicKey;
|
||||
import org.spongycastle.openpgp.PGPPublicKeyRing;
|
||||
import org.spongycastle.openpgp.PGPSecretKeyRing;
|
||||
import org.spongycastle.openpgp.PGPSignature;
|
||||
import org.spongycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpConversionHelper;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpHelper;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.Certs;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingData;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.Keys;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
|
||||
import org.sufficientlysecure.keychain.remote.AccountSettings;
|
||||
import org.sufficientlysecure.keychain.remote.AppSettings;
|
||||
import org.sufficientlysecure.keychain.util.IterableIterator;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.SignatureException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class ProviderHelper {
|
||||
|
||||
// If we ever switch to api level 11, we can ditch this whole mess!
|
||||
public static final int FIELD_TYPE_NULL = 1;
|
||||
// this is called integer to stay coherent with the constants in Cursor (api level 11)
|
||||
public static final int FIELD_TYPE_INTEGER = 2;
|
||||
public static final int FIELD_TYPE_FLOAT = 3;
|
||||
public static final int FIELD_TYPE_STRING = 4;
|
||||
public static final int FIELD_TYPE_BLOB = 5;
|
||||
|
||||
public static Object getGenericData(Context context, Uri uri, String column, int type) {
|
||||
return getGenericData(context, uri, new String[] { column }, new int[] { type }).get(column);
|
||||
}
|
||||
|
||||
public static HashMap<String,Object> getGenericData(Context context, Uri uri, String[] proj, int[] types) {
|
||||
Cursor cursor = context.getContentResolver().query(uri, proj, null, null, null);
|
||||
|
||||
HashMap<String, Object> result = new HashMap<String, Object>(proj.length);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
int pos = 0;
|
||||
for(String p : proj) {
|
||||
switch(types[pos]) {
|
||||
case FIELD_TYPE_NULL: result.put(p, cursor.isNull(pos)); break;
|
||||
case FIELD_TYPE_INTEGER: result.put(p, cursor.getLong(pos)); break;
|
||||
case FIELD_TYPE_FLOAT: result.put(p, cursor.getFloat(pos)); break;
|
||||
case FIELD_TYPE_STRING: result.put(p, cursor.getString(pos)); break;
|
||||
case FIELD_TYPE_BLOB: result.put(p, cursor.getBlob(pos)); break;
|
||||
}
|
||||
pos += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static Object getUnifiedData(Context context, long masterKeyId, String column, int type) {
|
||||
return getUnifiedData(context, masterKeyId, new String[] { column }, new int[] { type }).get(column);
|
||||
}
|
||||
|
||||
public static HashMap<String,Object> getUnifiedData(Context context, long masterKeyId, String[] proj, int[] types) {
|
||||
return getGenericData(context, KeyRings.buildUnifiedKeyRingUri(Long.toString(masterKeyId)), proj, types);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the master key id related to a given query. The id will either be extracted from the
|
||||
* query, which should work for all specific /key_rings/ queries, or will be queried if it can't.
|
||||
*/
|
||||
public static long getMasterKeyId(Context context, Uri queryUri) {
|
||||
// try extracting from the uri first
|
||||
String firstSegment = queryUri.getPathSegments().get(1);
|
||||
if(!firstSegment.equals("find")) try {
|
||||
return Long.parseLong(firstSegment);
|
||||
} catch(NumberFormatException e) {
|
||||
// didn't work? oh well.
|
||||
Log.d(Constants.TAG, "Couldn't get masterKeyId from URI, querying...");
|
||||
}
|
||||
Object data = getGenericData(context, queryUri, KeyRings.MASTER_KEY_ID, FIELD_TYPE_INTEGER);
|
||||
if(data != null)
|
||||
return (Long) data;
|
||||
// TODO better error handling?
|
||||
return 0L;
|
||||
}
|
||||
|
||||
public static Map<Long, PGPKeyRing> getPGPKeyRings(Context context, Uri queryUri) {
|
||||
Cursor cursor = context.getContentResolver().query(queryUri,
|
||||
new String[]{KeyRingData.MASTER_KEY_ID, KeyRingData.KEY_RING_DATA },
|
||||
null, null, null);
|
||||
|
||||
Map<Long, PGPKeyRing> result = new HashMap<Long, PGPKeyRing>(cursor.getCount());
|
||||
if (cursor != null && cursor.moveToFirst()) do {
|
||||
long masterKeyId = cursor.getLong(0);
|
||||
byte[] data = cursor.getBlob(1);
|
||||
if (data != null) {
|
||||
result.put(masterKeyId, PgpConversionHelper.BytesToPGPKeyRing(data));
|
||||
}
|
||||
} while(cursor.moveToNext());
|
||||
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
public static PGPKeyRing getPGPKeyRing(Context context, Uri queryUri) {
|
||||
Map<Long, PGPKeyRing> result = getPGPKeyRings(context, queryUri);
|
||||
if(result.isEmpty())
|
||||
return null;
|
||||
return result.values().iterator().next();
|
||||
}
|
||||
|
||||
public static PGPPublicKeyRing getPGPPublicKeyRingWithKeyId(Context context, long keyId) {
|
||||
Uri uri = KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(Long.toString(keyId));
|
||||
long masterKeyId = getMasterKeyId(context, uri);
|
||||
if(masterKeyId != 0)
|
||||
return getPGPPublicKeyRing(context, masterKeyId);
|
||||
return null;
|
||||
}
|
||||
public static PGPSecretKeyRing getPGPSecretKeyRingWithKeyId(Context context, long keyId) {
|
||||
Uri uri = KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(Long.toString(keyId));
|
||||
long masterKeyId = getMasterKeyId(context, uri);
|
||||
if(masterKeyId != 0)
|
||||
return getPGPSecretKeyRing(context, masterKeyId);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the actual PGPPublicKeyRing object from the database blob based on the masterKeyId
|
||||
*/
|
||||
public static PGPPublicKeyRing getPGPPublicKeyRing(Context context,
|
||||
long masterKeyId) {
|
||||
Uri queryUri = KeyRingData.buildPublicKeyRingUri(Long.toString(masterKeyId));
|
||||
return (PGPPublicKeyRing) getPGPKeyRing(context, queryUri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the actual PGPSecretKeyRing object from the database blob based on the maserKeyId
|
||||
*/
|
||||
public static PGPSecretKeyRing getPGPSecretKeyRing(Context context,
|
||||
long masterKeyId) {
|
||||
Uri queryUri = KeyRingData.buildSecretKeyRingUri(Long.toString(masterKeyId));
|
||||
return (PGPSecretKeyRing) getPGPKeyRing(context, queryUri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves PGPPublicKeyRing with its keys and userIds in DB
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static void saveKeyRing(Context context, PGPPublicKeyRing keyRing) throws IOException {
|
||||
PGPPublicKey masterKey = keyRing.getPublicKey();
|
||||
long masterKeyId = masterKey.getKeyID();
|
||||
|
||||
// IF there is a secret key, preserve it!
|
||||
PGPSecretKeyRing secretRing = ProviderHelper.getPGPSecretKeyRing(context, masterKeyId);
|
||||
|
||||
// delete old version of this keyRing, which also deletes all keys and userIds on cascade
|
||||
try {
|
||||
context.getContentResolver().delete(KeyRingData.buildPublicKeyRingUri(Long.toString(masterKeyId)), null, null);
|
||||
} catch (UnsupportedOperationException e) {
|
||||
Log.e(Constants.TAG, "Key could not be deleted! Maybe we are creating a new one!", e);
|
||||
}
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
// use exactly the same _ID again to replace key in-place.
|
||||
// NOTE: If we would not use the same _ID again,
|
||||
// getting back to the ViewKeyActivity would result in Nullpointer,
|
||||
// because the currently loaded key would be gone from the database
|
||||
values.put(KeyRingData.MASTER_KEY_ID, masterKeyId);
|
||||
values.put(KeyRingData.KEY_RING_DATA, keyRing.getEncoded());
|
||||
|
||||
// insert new version of this keyRing
|
||||
Uri uri = KeyRingData.buildPublicKeyRingUri(Long.toString(masterKeyId));
|
||||
Uri insertedUri = context.getContentResolver().insert(uri, values);
|
||||
|
||||
// save all keys and userIds included in keyRing object in database
|
||||
ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
|
||||
|
||||
int rank = 0;
|
||||
for (PGPPublicKey key : new IterableIterator<PGPPublicKey>(keyRing.getPublicKeys())) {
|
||||
operations.add(buildPublicKeyOperations(context, masterKeyId, key, rank));
|
||||
++rank;
|
||||
}
|
||||
|
||||
// get a list of owned secret keys, for verification filtering
|
||||
Map<Long, PGPKeyRing> allKeyRings = getPGPKeyRings(context, KeyRingData.buildSecretKeyRingUri());
|
||||
// special case: available secret keys verify themselves!
|
||||
if(secretRing != null)
|
||||
allKeyRings.put(secretRing.getSecretKey().getKeyID(), secretRing);
|
||||
|
||||
// classify and order user ids. primary are moved to the front, revoked to the back,
|
||||
// otherwise the order in the keyfile is preserved.
|
||||
List<UserIdItem> uids = new ArrayList<UserIdItem>();
|
||||
|
||||
for (String userId : new IterableIterator<String>(masterKey.getUserIDs())) {
|
||||
UserIdItem item = new UserIdItem();
|
||||
uids.add(item);
|
||||
item.userId = userId;
|
||||
|
||||
// look through signatures for this specific key
|
||||
for (PGPSignature cert : new IterableIterator<PGPSignature>(
|
||||
masterKey.getSignaturesForID(userId))) {
|
||||
long certId = cert.getKeyID();
|
||||
try {
|
||||
// self signature
|
||||
if(certId == masterKeyId) {
|
||||
cert.init(new JcaPGPContentVerifierBuilderProvider().setProvider(
|
||||
Constants.BOUNCY_CASTLE_PROVIDER_NAME), masterKey);
|
||||
if(!cert.verifyCertification(userId, masterKey)) {
|
||||
// not verified?! dang! TODO notify user? this is kinda serious...
|
||||
Log.e(Constants.TAG, "Could not verify self signature for " + userId + "!");
|
||||
continue;
|
||||
}
|
||||
// is this the first, or a more recent certificate?
|
||||
if(item.selfCert == null ||
|
||||
item.selfCert.getCreationTime().before(cert.getCreationTime())) {
|
||||
item.selfCert = cert;
|
||||
item.isPrimary = cert.getHashedSubPackets().isPrimaryUserID();
|
||||
item.isRevoked =
|
||||
cert.getSignatureType() == PGPSignature.CERTIFICATION_REVOCATION;
|
||||
}
|
||||
}
|
||||
// verify signatures from known private keys
|
||||
if(allKeyRings.containsKey(certId)) {
|
||||
// mark them as verified
|
||||
cert.init(new JcaPGPContentVerifierBuilderProvider().setProvider(
|
||||
Constants.BOUNCY_CASTLE_PROVIDER_NAME),
|
||||
allKeyRings.get(certId).getPublicKey());
|
||||
if(cert.verifyCertification(userId, masterKey)) {
|
||||
item.trustedCerts.add(cert);
|
||||
}
|
||||
}
|
||||
} catch(SignatureException e) {
|
||||
Log.e(Constants.TAG, "Signature verification failed! "
|
||||
+ PgpKeyHelper.convertKeyIdToHex(masterKey.getKeyID())
|
||||
+ " from "
|
||||
+ PgpKeyHelper.convertKeyIdToHex(cert.getKeyID()), e);
|
||||
} catch(PGPException e) {
|
||||
Log.e(Constants.TAG, "Signature verification failed! "
|
||||
+ PgpKeyHelper.convertKeyIdToHex(masterKey.getKeyID())
|
||||
+ " from "
|
||||
+ PgpKeyHelper.convertKeyIdToHex(cert.getKeyID()), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// primary before regular before revoked (see UserIdItem.compareTo)
|
||||
// this is a stable sort, so the order of keys is otherwise preserved.
|
||||
Collections.sort(uids);
|
||||
// iterate and put into db
|
||||
for(int userIdRank = 0; userIdRank < uids.size(); userIdRank++) {
|
||||
UserIdItem item = uids.get(userIdRank);
|
||||
operations.add(buildUserIdOperations(masterKeyId, item, userIdRank));
|
||||
// no self cert is bad, but allowed by the rfc...
|
||||
if(item.selfCert != null) {
|
||||
operations.add(buildCertOperations(
|
||||
masterKeyId, userIdRank, item.selfCert, Certs.VERIFIED_SELF));
|
||||
}
|
||||
// don't bother with trusted certs if the uid is revoked, anyways
|
||||
if(item.isRevoked) {
|
||||
continue;
|
||||
}
|
||||
for(int i = 0; i < item.trustedCerts.size(); i++) {
|
||||
operations.add(buildCertOperations(
|
||||
masterKeyId, userIdRank, item.trustedCerts.get(i), Certs.VERIFIED_SECRET));
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
context.getContentResolver().applyBatch(KeychainContract.CONTENT_AUTHORITY, operations);
|
||||
} catch (RemoteException e) {
|
||||
Log.e(Constants.TAG, "applyBatch failed!", e);
|
||||
} catch (OperationApplicationException e) {
|
||||
Log.e(Constants.TAG, "applyBatch failed!", e);
|
||||
}
|
||||
|
||||
// Save the saved keyring (if any)
|
||||
if(secretRing != null) {
|
||||
saveKeyRing(context, secretRing);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class UserIdItem implements Comparable<UserIdItem> {
|
||||
String userId;
|
||||
boolean isPrimary = false;
|
||||
boolean isRevoked = false;
|
||||
PGPSignature selfCert;
|
||||
List<PGPSignature> trustedCerts = new ArrayList<PGPSignature>();
|
||||
|
||||
@Override
|
||||
public int compareTo(UserIdItem o) {
|
||||
// if one key is primary but the other isn't, the primary one always comes first
|
||||
if(isPrimary != o.isPrimary)
|
||||
return isPrimary ? -1 : 1;
|
||||
// revoked keys always come last!
|
||||
if(isRevoked != o.isRevoked)
|
||||
return isRevoked ? 1 : -1;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a PGPSecretKeyRing in the DB. This will only work if a corresponding public keyring
|
||||
* is already in the database!
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static void saveKeyRing(Context context, PGPSecretKeyRing keyRing) throws IOException {
|
||||
long masterKeyId = keyRing.getPublicKey().getKeyID();
|
||||
|
||||
// save secret keyring
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(KeyRingData.MASTER_KEY_ID, masterKeyId);
|
||||
values.put(KeyRingData.KEY_RING_DATA, keyRing.getEncoded());
|
||||
// insert new version of this keyRing
|
||||
Uri uri = KeyRingData.buildSecretKeyRingUri(Long.toString(masterKeyId));
|
||||
context.getContentResolver().insert(uri, values);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves (or updates) a pair of public and secret KeyRings in the database
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static void saveKeyRing(Context context, PGPPublicKeyRing pubRing, PGPSecretKeyRing privRing) throws IOException {
|
||||
long masterKeyId = pubRing.getPublicKey().getKeyID();
|
||||
|
||||
// delete secret keyring (so it isn't unnecessarily saved by public-saveKeyRing below)
|
||||
context.getContentResolver().delete(KeyRingData.buildSecretKeyRingUri(Long.toString(masterKeyId)), null, null);
|
||||
|
||||
// save public keyring
|
||||
saveKeyRing(context, pubRing);
|
||||
saveKeyRing(context, privRing);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build ContentProviderOperation to add PGPPublicKey to database corresponding to a keyRing
|
||||
*/
|
||||
private static ContentProviderOperation buildPublicKeyOperations(Context context,
|
||||
long masterKeyId, PGPPublicKey key, int rank) throws IOException {
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(Keys.MASTER_KEY_ID, masterKeyId);
|
||||
values.put(Keys.RANK, rank);
|
||||
|
||||
values.put(Keys.KEY_ID, key.getKeyID());
|
||||
values.put(Keys.KEY_SIZE, key.getBitStrength());
|
||||
values.put(Keys.ALGORITHM, key.getAlgorithm());
|
||||
values.put(Keys.FINGERPRINT, key.getFingerprint());
|
||||
|
||||
values.put(Keys.CAN_CERTIFY, (PgpKeyHelper.isCertificationKey(key)));
|
||||
values.put(Keys.CAN_SIGN, (PgpKeyHelper.isSigningKey(key)));
|
||||
values.put(Keys.CAN_ENCRYPT, PgpKeyHelper.isEncryptionKey(key));
|
||||
values.put(Keys.IS_REVOKED, key.isRevoked());
|
||||
|
||||
values.put(Keys.CREATION, PgpKeyHelper.getCreationDate(key).getTime() / 1000);
|
||||
Date expiryDate = PgpKeyHelper.getExpiryDate(key);
|
||||
if (expiryDate != null) {
|
||||
values.put(Keys.EXPIRY, expiryDate.getTime() / 1000);
|
||||
}
|
||||
|
||||
Uri uri = Keys.buildKeysUri(Long.toString(masterKeyId));
|
||||
|
||||
return ContentProviderOperation.newInsert(uri).withValues(values).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build ContentProviderOperation to add PGPPublicKey to database corresponding to a keyRing
|
||||
*/
|
||||
private static ContentProviderOperation buildCertOperations(long masterKeyId,
|
||||
int rank,
|
||||
PGPSignature cert,
|
||||
int verified)
|
||||
throws IOException {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(Certs.MASTER_KEY_ID, masterKeyId);
|
||||
values.put(Certs.RANK, rank);
|
||||
values.put(Certs.KEY_ID_CERTIFIER, cert.getKeyID());
|
||||
values.put(Certs.TYPE, cert.getSignatureType());
|
||||
values.put(Certs.CREATION, cert.getCreationTime().getTime() / 1000);
|
||||
values.put(Certs.VERIFIED, verified);
|
||||
values.put(Certs.DATA, cert.getEncoded());
|
||||
|
||||
Uri uri = Certs.buildCertsUri(Long.toString(masterKeyId));
|
||||
|
||||
return ContentProviderOperation.newInsert(uri).withValues(values).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build ContentProviderOperation to add PublicUserIds to database corresponding to a keyRing
|
||||
*/
|
||||
private static ContentProviderOperation buildUserIdOperations(long masterKeyId, UserIdItem item,
|
||||
int rank) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(UserIds.MASTER_KEY_ID, masterKeyId);
|
||||
values.put(UserIds.USER_ID, item.userId);
|
||||
values.put(UserIds.IS_PRIMARY, item.isPrimary);
|
||||
values.put(UserIds.IS_REVOKED, item.isRevoked);
|
||||
values.put(UserIds.RANK, rank);
|
||||
|
||||
Uri uri = UserIds.buildUserIdsUri(Long.toString(masterKeyId));
|
||||
|
||||
return ContentProviderOperation.newInsert(uri).withValues(values).build();
|
||||
}
|
||||
|
||||
public static ArrayList<String> getKeyRingsAsArmoredString(Context context, long[] masterKeyIds) {
|
||||
ArrayList<String> output = new ArrayList<String>();
|
||||
|
||||
if (masterKeyIds != null && masterKeyIds.length > 0) {
|
||||
|
||||
Cursor cursor = getCursorWithSelectedKeyringMasterKeyIds(context, masterKeyIds);
|
||||
|
||||
if (cursor != null) {
|
||||
int masterIdCol = cursor.getColumnIndex(KeyRingData.MASTER_KEY_ID);
|
||||
int dataCol = cursor.getColumnIndex(KeyRingData.KEY_RING_DATA);
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
Log.d(Constants.TAG, "masterKeyId: " + cursor.getLong(masterIdCol));
|
||||
|
||||
// get actual keyring data blob and write it to ByteArrayOutputStream
|
||||
try {
|
||||
Object keyRing = null;
|
||||
byte[] data = cursor.getBlob(dataCol);
|
||||
if (data != null) {
|
||||
keyRing = PgpConversionHelper.BytesToPGPKeyRing(data);
|
||||
}
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
ArmoredOutputStream aos = new ArmoredOutputStream(bos);
|
||||
aos.setHeader("Version", PgpHelper.getFullVersion(context));
|
||||
|
||||
if (keyRing instanceof PGPSecretKeyRing) {
|
||||
aos.write(((PGPSecretKeyRing) keyRing).getEncoded());
|
||||
} else if (keyRing instanceof PGPPublicKeyRing) {
|
||||
aos.write(((PGPPublicKeyRing) keyRing).getEncoded());
|
||||
}
|
||||
aos.close();
|
||||
|
||||
String armoredKey = bos.toString("UTF-8");
|
||||
|
||||
Log.d(Constants.TAG, "armoredKey:" + armoredKey);
|
||||
|
||||
output.add(armoredKey);
|
||||
} catch (IOException e) {
|
||||
Log.e(Constants.TAG, "IOException", e);
|
||||
}
|
||||
} while (cursor.moveToNext());
|
||||
}
|
||||
}
|
||||
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
} else {
|
||||
Log.e(Constants.TAG, "No master keys given!");
|
||||
}
|
||||
|
||||
if (output.size() > 0) {
|
||||
return output;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Cursor getCursorWithSelectedKeyringMasterKeyIds(Context context, long[] masterKeyIds) {
|
||||
Cursor cursor = null;
|
||||
if (masterKeyIds != null && masterKeyIds.length > 0) {
|
||||
|
||||
String inMasterKeyList = KeyRingData.MASTER_KEY_ID + " IN (";
|
||||
for (int i = 0; i < masterKeyIds.length; ++i) {
|
||||
if (i != 0) {
|
||||
inMasterKeyList += ", ";
|
||||
}
|
||||
inMasterKeyList += DatabaseUtils.sqlEscapeString("" + masterKeyIds[i]);
|
||||
}
|
||||
inMasterKeyList += ")";
|
||||
|
||||
cursor = context.getContentResolver().query(KeyRingData.buildPublicKeyRingUri(), new String[] {
|
||||
KeyRingData._ID, KeyRingData.MASTER_KEY_ID, KeyRingData.KEY_RING_DATA
|
||||
}, inMasterKeyList, null, null);
|
||||
}
|
||||
|
||||
return cursor;
|
||||
}
|
||||
|
||||
public static ArrayList<String> getRegisteredApiApps(Context context) {
|
||||
Cursor cursor = context.getContentResolver().query(ApiApps.CONTENT_URI, null, null, null,
|
||||
null);
|
||||
|
||||
ArrayList<String> packageNames = new ArrayList<String>();
|
||||
if (cursor != null) {
|
||||
int packageNameCol = cursor.getColumnIndex(ApiApps.PACKAGE_NAME);
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
packageNames.add(cursor.getString(packageNameCol));
|
||||
} while (cursor.moveToNext());
|
||||
}
|
||||
}
|
||||
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
return packageNames;
|
||||
}
|
||||
|
||||
private static ContentValues contentValueForApiApps(AppSettings appSettings) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(ApiApps.PACKAGE_NAME, appSettings.getPackageName());
|
||||
values.put(ApiApps.PACKAGE_SIGNATURE, appSettings.getPackageSignature());
|
||||
return values;
|
||||
}
|
||||
|
||||
private static ContentValues contentValueForApiAccounts(AccountSettings accSettings) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(KeychainContract.ApiAccounts.ACCOUNT_NAME, accSettings.getAccountName());
|
||||
values.put(KeychainContract.ApiAccounts.KEY_ID, accSettings.getKeyId());
|
||||
values.put(KeychainContract.ApiAccounts.COMPRESSION, accSettings.getCompression());
|
||||
values.put(KeychainContract.ApiAccounts.ENCRYPTION_ALGORITHM, accSettings.getEncryptionAlgorithm());
|
||||
values.put(KeychainContract.ApiAccounts.HASH_ALORITHM, accSettings.getHashAlgorithm());
|
||||
return values;
|
||||
}
|
||||
|
||||
public static void insertApiApp(Context context, AppSettings appSettings) {
|
||||
context.getContentResolver().insert(KeychainContract.ApiApps.CONTENT_URI,
|
||||
contentValueForApiApps(appSettings));
|
||||
}
|
||||
|
||||
public static void insertApiAccount(Context context, Uri uri, AccountSettings accSettings) {
|
||||
context.getContentResolver().insert(uri, contentValueForApiAccounts(accSettings));
|
||||
}
|
||||
|
||||
public static void updateApiApp(Context context, AppSettings appSettings, Uri uri) {
|
||||
if (context.getContentResolver().update(uri, contentValueForApiApps(appSettings), null,
|
||||
null) <= 0) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
}
|
||||
|
||||
public static void updateApiAccount(Context context, AccountSettings accSettings, Uri uri) {
|
||||
if (context.getContentResolver().update(uri, contentValueForApiAccounts(accSettings), null,
|
||||
null) <= 0) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Must be an uri pointing to an account
|
||||
*
|
||||
* @param context
|
||||
* @param uri
|
||||
* @return
|
||||
*/
|
||||
public static AppSettings getApiAppSettings(Context context, Uri uri) {
|
||||
AppSettings settings = null;
|
||||
|
||||
Cursor cur = context.getContentResolver().query(uri, null, null, null, null);
|
||||
if (cur != null && cur.moveToFirst()) {
|
||||
settings = new AppSettings();
|
||||
settings.setPackageName(cur.getString(
|
||||
cur.getColumnIndex(KeychainContract.ApiApps.PACKAGE_NAME)));
|
||||
settings.setPackageSignature(cur.getBlob(
|
||||
cur.getColumnIndex(KeychainContract.ApiApps.PACKAGE_SIGNATURE)));
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
public static AccountSettings getApiAccountSettings(Context context, Uri accountUri) {
|
||||
AccountSettings settings = null;
|
||||
|
||||
Cursor cur = context.getContentResolver().query(accountUri, null, null, null, null);
|
||||
if (cur != null && cur.moveToFirst()) {
|
||||
settings = new AccountSettings();
|
||||
|
||||
settings.setAccountName(cur.getString(
|
||||
cur.getColumnIndex(KeychainContract.ApiAccounts.ACCOUNT_NAME)));
|
||||
settings.setKeyId(cur.getLong(
|
||||
cur.getColumnIndex(KeychainContract.ApiAccounts.KEY_ID)));
|
||||
settings.setCompression(cur.getInt(
|
||||
cur.getColumnIndexOrThrow(KeychainContract.ApiAccounts.COMPRESSION)));
|
||||
settings.setHashAlgorithm(cur.getInt(
|
||||
cur.getColumnIndexOrThrow(KeychainContract.ApiAccounts.HASH_ALORITHM)));
|
||||
settings.setEncryptionAlgorithm(cur.getInt(
|
||||
cur.getColumnIndexOrThrow(KeychainContract.ApiAccounts.ENCRYPTION_ALGORITHM)));
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
public static Set<Long> getAllKeyIdsForApp(Context context, Uri uri) {
|
||||
Set<Long> keyIds = new HashSet<Long>();
|
||||
|
||||
Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
|
||||
if (cursor != null) {
|
||||
int keyIdColumn = cursor.getColumnIndex(KeychainContract.ApiAccounts.KEY_ID);
|
||||
while (cursor.moveToNext()) {
|
||||
keyIds.add(cursor.getLong(keyIdColumn));
|
||||
}
|
||||
}
|
||||
|
||||
return keyIds;
|
||||
}
|
||||
|
||||
public static byte[] getApiAppSignature(Context context, String packageName) {
|
||||
Uri queryUri = ApiApps.buildByPackageNameUri(packageName);
|
||||
|
||||
String[] projection = new String[]{ApiApps.PACKAGE_SIGNATURE};
|
||||
|
||||
ContentResolver cr = context.getContentResolver();
|
||||
Cursor cursor = cr.query(queryUri, projection, null, null, null);
|
||||
|
||||
byte[] signature = null;
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
int signatureCol = 0;
|
||||
|
||||
signature = cursor.getBlob(signatureCol);
|
||||
}
|
||||
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
return signature;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user