diff options
23 files changed, 1331 insertions, 146 deletions
diff --git a/OpenPGP-Keychain/src/main/AndroidManifest.xml b/OpenPGP-Keychain/src/main/AndroidManifest.xml index 835d708b5..de17f9b20 100644 --- a/OpenPGP-Keychain/src/main/AndroidManifest.xml +++ b/OpenPGP-Keychain/src/main/AndroidManifest.xml @@ -95,6 +95,15 @@ android:value=".ui.KeyListActivity" /> </activity> <activity + android:name=".ui.ViewCertActivity" + android:configChanges="orientation|screenSize|keyboardHidden|keyboard" + android:label="View Certificate Details" + android:parentActivityName=".ui.ViewKeyActivity"> + <meta-data + android:name="android.support.PARENT_ACTIVITY" + android:value=".ui.ViewKeyActivity" /> + </activity> + <activity android:name=".ui.SelectPublicKeyActivity" android:configChanges="orientation|screenSize|keyboardHidden|keyboard" android:label="@string/title_select_recipients" diff --git a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpConversionHelper.java b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpConversionHelper.java index 7c25c2c2a..c6c62d649 100644 --- a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpConversionHelper.java +++ b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpConversionHelper.java @@ -21,6 +21,8 @@ import org.spongycastle.openpgp.PGPKeyRing; import org.spongycastle.openpgp.PGPObjectFactory; import org.spongycastle.openpgp.PGPSecretKey; import org.spongycastle.openpgp.PGPSecretKeyRing; +import org.spongycastle.openpgp.PGPSignature; +import org.spongycastle.openpgp.PGPSignatureList; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.util.Log; @@ -123,6 +125,28 @@ public class PgpConversionHelper { } /** + * Convert from byte[] to PGPSignature + * + * @param sigBytes + * @return + */ + public static PGPSignature BytesToPGPSignature(byte[] sigBytes) { + PGPObjectFactory factory = new PGPObjectFactory(sigBytes); + PGPSignatureList signatures = null; + try { + if ((signatures = (PGPSignatureList) factory.nextObject()) == null || signatures.isEmpty()) { + Log.e(Constants.TAG, "No signatures given!"); + return null; + } + } catch (IOException e) { + Log.e(Constants.TAG, "Error while converting to PGPSignature!", e); + return null; + } + + return signatures.get(0); + } + + /** * Convert from ArrayList<PGPSecretKey> to byte[] * * @param keys diff --git a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyHelper.java b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyHelper.java index 9a97bc717..4c786f555 100644 --- a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyHelper.java +++ b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyHelper.java @@ -454,7 +454,6 @@ public class PgpKeyHelper { algorithmStr = "RSA"; break; } - case PGPPublicKey.DSA: { algorithmStr = "DSA"; break; @@ -471,7 +470,10 @@ public class PgpKeyHelper { break; } } - return algorithmStr + ", " + keySize + " bit"; + if(keySize > 0) + return algorithmStr + ", " + keySize + " bit"; + else + return algorithmStr; } /** diff --git a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java index 9eeb57222..cad40d4a0 100644 --- a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java +++ b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java @@ -52,6 +52,17 @@ public class KeychainContract { 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 { @@ -91,12 +102,15 @@ public class KeychainContract { 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 = "master_key_id"; + 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() @@ -145,6 +159,9 @@ public class KeychainContract { 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(); } @@ -178,6 +195,7 @@ public class KeychainContract { } 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(); @@ -243,6 +261,28 @@ public class KeychainContract { } } + 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(); diff --git a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java index 36e2a7962..51f8d8e8e 100644 --- a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java +++ b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java @@ -30,14 +30,13 @@ 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.ApiAppsColumns; 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; -import java.util.Arrays; public class KeychainDatabase extends SQLiteOpenHelper { private static final String DATABASE_NAME = "openkeychain.db"; @@ -49,6 +48,7 @@ public class KeychainDatabase extends SQLiteOpenHelper { 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"; } @@ -96,13 +96,35 @@ public class KeychainDatabase extends SQLiteOpenHelper { + UserIdsColumns.USER_ID + " CHARMANDER, " + UserIdsColumns.IS_PRIMARY + " BOOLEAN, " + + UserIdsColumns.IS_REVOKED + " BOOLEAN, " + UserIdsColumns.RANK+ " INTEGER, " - + "PRIMARY KEY(" + UserIdsColumns.MASTER_KEY_ID + ", " + UserIdsColumns.USER_ID + ")," + + "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, " @@ -145,6 +167,7 @@ public class KeychainDatabase extends SQLiteOpenHelper { 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); } @@ -155,6 +178,11 @@ public class KeychainDatabase extends SQLiteOpenHelper { 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); } } @@ -166,8 +194,8 @@ public class KeychainDatabase extends SQLiteOpenHelper { /** 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 and a type column, the latter of which should be bigger - * for secret keys. + * 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) { @@ -212,8 +240,32 @@ public class KeychainDatabase extends SQLiteOpenHelper { Log.d(Constants.TAG, "Ok."); } - Cursor c = db.rawQuery("SELECT key_ring_data FROM key_rings ORDER BY type ASC", null); + 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++) { diff --git a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java index 1dd6ab08f..9b9e4991d 100644 --- a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java +++ b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java @@ -34,6 +34,7 @@ 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; @@ -45,12 +46,15 @@ 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; @@ -87,6 +91,9 @@ public class KeychainProvider extends ContentProvider { 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 @@ -111,6 +118,8 @@ public class KeychainProvider extends ContentProvider { * key_rings/_/user_ids * key_rings/_/public * key_rings/_/secret + * key_rings/_/certs + * key_rings/_/certs/_/_ * </pre> */ matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/" @@ -128,6 +137,12 @@ public class KeychainProvider extends ContentProvider { 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 @@ -238,15 +253,16 @@ public class KeychainProvider extends ContentProvider { 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, Keys.IS_REVOKED); + 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, Keys.CREATION); + 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); @@ -261,9 +277,17 @@ public class KeychainProvider extends ContentProvider { + 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: { @@ -358,18 +382,30 @@ public class KeychainProvider extends ContentProvider { 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, UserIds.MASTER_KEY_ID); - projectionMap.put(UserIds.USER_ID, UserIds.USER_ID); - projectionMap.put(UserIds.RANK, UserIds.RANK); - projectionMap.put(UserIds.IS_PRIMARY, UserIds.IS_PRIMARY); + 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); - qb.appendWhere(UserIds.MASTER_KEY_ID + " = "); + 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 = UserIds.RANK + " ASC"; + sortOrder = Tables.USER_IDS + "." + UserIds.RANK + " ASC"; } break; @@ -394,6 +430,7 @@ public class KeychainProvider extends ContentProvider { 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"); @@ -402,8 +439,55 @@ public class KeychainProvider extends ContentProvider { qb.setProjectionMap(projectionMap); qb.setTables(Tables.KEY_RINGS_SECRET); - qb.appendWhere(KeyRings.MASTER_KEY_ID + " = "); + + 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; } @@ -499,6 +583,13 @@ public class KeychainProvider extends ContentProvider { 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; diff --git a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java index 3d1d663c7..e9179f864 100644 --- a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java +++ b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java @@ -17,6 +17,8 @@ package org.sufficientlysecure.keychain.provider; +import java.security.SignatureException; +import org.spongycastle.bcpg.ArmoredOutputStream; import android.content.ContentProviderOperation; import android.content.ContentResolver; import android.content.ContentValues; @@ -27,11 +29,13 @@ import android.database.DatabaseUtils; import android.net.Uri; import android.os.RemoteException; -import org.spongycastle.bcpg.ArmoredOutputStream; +import org.spongycastle.bcpg.SignatureSubpacketTags; +import org.spongycastle.bcpg.sig.SignatureExpirationTime; +import org.spongycastle.openpgp.PGPException; +import org.spongycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider; import org.spongycastle.openpgp.PGPKeyRing; import org.spongycastle.openpgp.PGPPublicKey; import org.spongycastle.openpgp.PGPPublicKeyRing; -import org.spongycastle.openpgp.PGPSecretKey; import org.spongycastle.openpgp.PGPSecretKeyRing; import org.spongycastle.openpgp.PGPSignature; import org.sufficientlysecure.keychain.Constants; @@ -43,6 +47,7 @@ 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.provider.KeychainContract.Certs; import org.sufficientlysecure.keychain.remote.AccountSettings; import org.sufficientlysecure.keychain.remote.AppSettings; import org.sufficientlysecure.keychain.util.IterableIterator; @@ -51,8 +56,11 @@ import org.sufficientlysecure.keychain.util.Log; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.Date; import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.HashSet; import java.util.Set; @@ -123,25 +131,31 @@ public class ProviderHelper { return 0L; } - public static PGPKeyRing getPGPKeyRing(Context context, Uri queryUri) { + public static Map<Long, PGPKeyRing> getPGPKeyRings(Context context, Uri queryUri) { Cursor cursor = context.getContentResolver().query(queryUri, - new String[]{KeyRings._ID, KeyRingData.KEY_RING_DATA}, null, null, null); + new String[]{KeyRingData.MASTER_KEY_ID, KeyRingData.KEY_RING_DATA }, + null, null, null); - PGPKeyRing keyRing = null; - if (cursor != null && cursor.moveToFirst()) { - int keyRingDataCol = cursor.getColumnIndex(KeyRingData.KEY_RING_DATA); - - byte[] data = cursor.getBlob(keyRingDataCol); + 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) { - keyRing = PgpConversionHelper.BytesToPGPKeyRing(data); + result.put(masterKeyId, PgpConversionHelper.BytesToPGPKeyRing(data)); } - } + } while(cursor.moveToNext()); if (cursor != null) { cursor.close(); } - return keyRing; + 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) { @@ -216,19 +230,88 @@ public class ProviderHelper { ++rank; } - int userIdRank = 0; + // 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())) { - operations.add(buildUserIdOperations(context, masterKeyId, userId, userIdRank)); - ++userIdRank; + 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); + } + } } - for (PGPSignature certification : - new IterableIterator<PGPSignature>( - masterKey.getSignaturesOfType(PGPSignature.POSITIVE_CERTIFICATION))) { - // TODO: how to do this?? - // we need to verify the signatures again and again when they are displayed... -// if (certification.verify -// operations.add(buildPublicKeyOperations(context, keyRingRowId, key, rank)); + // 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 { @@ -246,6 +329,25 @@ public class ProviderHelper { } + 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! @@ -311,21 +413,37 @@ public class ProviderHelper { } /** - * Build ContentProviderOperation to add PGPSecretKey to database corresponding to a keyRing + * Build ContentProviderOperation to add PGPPublicKey to database corresponding to a keyRing */ - private static ContentProviderOperation buildSecretKeyOperations(Context context, - long masterKeyId, PGPSecretKey key, int rank) throws IOException { - return buildPublicKeyOperations(context, masterKeyId, key.getPublicKey(), rank); + 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(Context context, - long masterKeyId, String userId, int rank) { + 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, userId); + 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)); diff --git a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyActivity.java b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyActivity.java index fb8726ace..7027c114e 100644 --- a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyActivity.java +++ b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyActivity.java @@ -46,7 +46,8 @@ import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.helper.Preferences; import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; -import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds; import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.service.KeychainIntentService; import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler; @@ -147,42 +148,37 @@ public class CertifyKeyActivity extends ActionBarActivity implements mUserIdsAdapter = new ViewKeyUserIdsAdapter(this, null, 0, true); mUserIds.setAdapter(mUserIdsAdapter); + mUserIds.setOnItemClickListener(mUserIdsAdapter); getSupportLoaderManager().initLoader(LOADER_ID_KEYRING, null, this); getSupportLoaderManager().initLoader(LOADER_ID_USER_IDS, null, this); } + static final String USER_IDS_SELECTION = UserIds.IS_REVOKED + " = 0"; + static final String[] KEYRING_PROJECTION = new String[] { - KeychainContract.KeyRings._ID, - KeychainContract.Keys.MASTER_KEY_ID, - KeychainContract.Keys.FINGERPRINT, - KeychainContract.UserIds.USER_ID + KeyRings._ID, + KeyRings.MASTER_KEY_ID, + KeyRings.FINGERPRINT, + KeyRings.USER_ID, }; static final int INDEX_MASTER_KEY_ID = 1; static final int INDEX_FINGERPRINT = 2; static final int INDEX_USER_ID = 3; - static final String[] USER_IDS_PROJECTION = - new String[]{ - KeychainContract.UserIds._ID, - KeychainContract.UserIds.USER_ID, - KeychainContract.UserIds.RANK - }; - static final String USER_IDS_SORT_ORDER = - KeychainContract.UserIds.RANK + " ASC"; - @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { switch(id) { case LOADER_ID_KEYRING: { - Uri uri = KeychainContract.KeyRings.buildUnifiedKeyRingUri(mDataUri); + Uri uri = KeyRings.buildUnifiedKeyRingUri(mDataUri); return new CursorLoader(this, uri, KEYRING_PROJECTION, null, null, null); } case LOADER_ID_USER_IDS: { - Uri uri = KeychainContract.UserIds.buildUserIdsUri(mDataUri); - return new CursorLoader(this, uri, USER_IDS_PROJECTION, null, null, USER_IDS_SORT_ORDER); + Uri uri = UserIds.buildUserIdsUri(mDataUri); + return new CursorLoader(this, uri, + ViewKeyUserIdsAdapter.USER_IDS_PROJECTION, USER_IDS_SELECTION, null, null); } } return null; diff --git a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java index 425f5f036..3e2c96464 100644 --- a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java +++ b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java @@ -50,6 +50,7 @@ import android.widget.AbsListView.MultiChoiceModeListener; import android.widget.AdapterView; import android.widget.Button; import android.widget.FrameLayout; +import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; import com.beardedhen.androidbootstrap.BootstrapButton; @@ -251,13 +252,15 @@ public class KeyListFragment extends Fragment KeyRings.MASTER_KEY_ID, KeyRings.USER_ID, KeyRings.IS_REVOKED, + KeyRings.VERIFIED, KeyRings.HAS_SECRET }; static final int INDEX_MASTER_KEY_ID = 1; static final int INDEX_USER_ID = 2; static final int INDEX_IS_REVOKED = 3; - static final int INDEX_HAS_SECRET = 4; + static final int INDEX_VERIFIED = 4; + static final int INDEX_HAS_SECRET = 5; @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { @@ -461,8 +464,8 @@ public class KeyListFragment extends Fragment /** * Bind cursor data to the item list view * <p/> - * NOTE: CursorAdapter already implements the ViewHolder pattern in its getView() method. Thus - * no ViewHolder is required here. + * NOTE: CursorAdapter already implements the ViewHolder pattern in its getView() method. + * Thus no ViewHolder is required here. */ @Override public void bindView(View view, Context context, Cursor cursor) { @@ -491,12 +494,14 @@ public class KeyListFragment extends Fragment FrameLayout statusLayout = (FrameLayout) view.findViewById(R.id.status_layout); Button button = (Button) view.findViewById(R.id.edit); TextView revoked = (TextView) view.findViewById(R.id.revoked); + ImageView verified = (ImageView) view.findViewById(R.id.verified); if (cursor.getInt(KeyListFragment.INDEX_HAS_SECRET) != 0) { // this is a secret key - show the edit button statusDivider.setVisibility(View.VISIBLE); statusLayout.setVisibility(View.VISIBLE); revoked.setVisibility(View.GONE); + verified.setVisibility(View.GONE); button.setVisibility(View.VISIBLE); final long id = cursor.getLong(INDEX_MASTER_KEY_ID); @@ -514,8 +519,16 @@ public class KeyListFragment extends Fragment button.setVisibility(View.GONE); boolean isRevoked = cursor.getInt(INDEX_IS_REVOKED) > 0; - statusLayout.setVisibility(isRevoked ? View.VISIBLE : View.GONE); - revoked.setVisibility(isRevoked ? View.VISIBLE : View.GONE); + if(isRevoked) { + statusLayout.setVisibility(isRevoked ? View.VISIBLE : View.GONE); + revoked.setVisibility(isRevoked ? View.VISIBLE : View.GONE); + verified.setVisibility(View.GONE); + } else { + boolean isVerified = cursor.getInt(INDEX_VERIFIED) > 0; + statusLayout.setVisibility(isVerified ? View.VISIBLE : View.GONE); + revoked.setVisibility(View.GONE); + verified.setVisibility(isVerified ? View.VISIBLE : View.GONE); + } } } @@ -587,8 +600,7 @@ public class KeyListFragment extends Fragment String userId = mCursor.getString(KeyListFragment.INDEX_USER_ID); String headerText = convertView.getResources().getString(R.string.user_id_no_name); if (userId != null && userId.length() > 0) { - headerText = "" + - mCursor.getString(KeyListFragment.INDEX_USER_ID).subSequence(0, 1).charAt(0); + headerText = "" + userId.subSequence(0, 1).charAt(0); } holder.mText.setText(headerText); holder.mCount.setVisibility(View.GONE); diff --git a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewCertActivity.java b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewCertActivity.java new file mode 100644 index 000000000..b130519ba --- /dev/null +++ b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewCertActivity.java @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2011 Senecaso + * + * 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.ui; + +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.support.v7.app.ActionBar; +import android.support.v7.app.ActionBarActivity; +import android.text.format.DateFormat; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.TextView; + +import org.spongycastle.bcpg.SignatureSubpacket; +import org.spongycastle.bcpg.SignatureSubpacketTags; +import org.spongycastle.bcpg.sig.RevocationReason; +import org.spongycastle.openpgp.PGPException; +import org.spongycastle.openpgp.PGPKeyRing; +import org.spongycastle.openpgp.PGPSignature; +import org.spongycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.pgp.PgpConversionHelper; +import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; +import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.provider.KeychainContract.Certs; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.util.Log; + +import java.security.SignatureException; +import java.util.Date; + +/** + * Swag + */ +public class ViewCertActivity extends ActionBarActivity + implements LoaderManager.LoaderCallbacks<Cursor> { + + // These are the rows that we will retrieve. + static final String[] PROJECTION = new String[] { + Certs.MASTER_KEY_ID, + Certs.USER_ID, + Certs.TYPE, + Certs.CREATION, + Certs.KEY_ID_CERTIFIER, + Certs.SIGNER_UID, + Certs.DATA, + }; + private static final int INDEX_MASTER_KEY_ID = 0; + private static final int INDEX_USER_ID = 1; + private static final int INDEX_TYPE = 2; + private static final int INDEX_CREATION = 3; + private static final int INDEX_KEY_ID_CERTIFIER = 4; + private static final int INDEX_SIGNER_UID = 5; + private static final int INDEX_DATA = 6; + + private Uri mDataUri; + + private long mSignerKeyId; + + private TextView mSigneeKey, mSigneeUid, mAlgorithm, mType, mRReason, mCreation; + private TextView mSignerKey, mSignerUid, mStatus; + private View mRowReason; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + ActionBar actionBar = getSupportActionBar(); + actionBar.setDisplayHomeAsUpEnabled(true); + + setContentView(R.layout.view_cert_activity); + + mStatus = (TextView) findViewById(R.id.status); + mSigneeKey = (TextView) findViewById(R.id.signee_key); + mSigneeUid = (TextView) findViewById(R.id.signee_uid); + mAlgorithm = (TextView) findViewById(R.id.algorithm); + mType = (TextView) findViewById(R.id.signature_type); + mRReason = (TextView) findViewById(R.id.reason); + mCreation = (TextView) findViewById(R.id.creation); + + mSignerKey = (TextView) findViewById(R.id.signer_key_id); + mSignerUid = (TextView) findViewById(R.id.signer_uid); + + mRowReason = findViewById(R.id.row_reason); + + mDataUri = getIntent().getData(); + if (mDataUri == null) { + Log.e(Constants.TAG, "Intent data missing. Should be Uri of key!"); + finish(); + return; + } + + getSupportLoaderManager().initLoader(0, null, this); + + } + + @Override + public Loader<Cursor> onCreateLoader(int id, Bundle args) { + // Now create and return a CursorLoader that will take care of + // creating a Cursor for the data being displayed. + return new CursorLoader(this, mDataUri, PROJECTION, null, null, null); + } + + @Override + public void onLoadFinished(Loader<Cursor> loader, Cursor data) { + if(data.moveToFirst()) { + String signeeKey = "0x" + PgpKeyHelper.convertKeyIdToHex(data.getLong(INDEX_MASTER_KEY_ID)); + mSigneeKey.setText(signeeKey); + + String signeeUid = data.getString(INDEX_USER_ID); + mSigneeUid.setText(signeeUid); + + Date creationDate = new Date(data.getLong(INDEX_CREATION) * 1000); + mCreation.setText(DateFormat.getDateFormat(getApplicationContext()).format(creationDate)); + + mSignerKeyId = data.getLong(INDEX_KEY_ID_CERTIFIER); + String signerKey = "0x" + PgpKeyHelper.convertKeyIdToHex(mSignerKeyId); + mSignerKey.setText(signerKey); + + String signerUid = data.getString(INDEX_SIGNER_UID); + if(signerUid != null) + mSignerUid.setText(signerUid); + else + mSignerUid.setText(R.string.unknown_uid); + + PGPSignature sig = PgpConversionHelper.BytesToPGPSignature(data.getBlob(INDEX_DATA)); + PGPKeyRing signeeRing = ProviderHelper.getPGPKeyRing(this, + KeychainContract.KeyRingData.buildPublicKeyRingUri(Long.toString(data.getLong(INDEX_MASTER_KEY_ID)))); + PGPKeyRing signerRing = ProviderHelper.getPGPKeyRing(this, + KeychainContract.KeyRingData.buildPublicKeyRingUri(Long.toString(sig.getKeyID()))); + if(signerRing != null) try { + sig.init(new JcaPGPContentVerifierBuilderProvider().setProvider( + Constants.BOUNCY_CASTLE_PROVIDER_NAME), signeeRing.getPublicKey()); + if (sig.verifyCertification(signeeUid, signerRing.getPublicKey())) { + mStatus.setText("ok"); + mStatus.setTextColor(getResources().getColor(R.color.bbutton_success)); + } else { + mStatus.setText("failed!"); + mStatus.setTextColor(getResources().getColor(R.color.alert)); + } + } catch(SignatureException e) { + mStatus.setText("error!"); + mStatus.setTextColor(getResources().getColor(R.color.alert)); + } catch(PGPException e) { + mStatus.setText("error!"); + mStatus.setTextColor(getResources().getColor(R.color.alert)); + } else { + mStatus.setText("key unavailable"); + mStatus.setTextColor(getResources().getColor(R.color.black)); + } + + String algorithmStr = PgpKeyHelper.getAlgorithmInfo(sig.getKeyAlgorithm(), 0); + mAlgorithm.setText(algorithmStr); + + mRowReason.setVisibility(View.GONE); + switch(data.getInt(INDEX_TYPE)) { + case PGPSignature.DEFAULT_CERTIFICATION: + mType.setText(R.string.cert_default); break; + case PGPSignature.NO_CERTIFICATION: + mType.setText(R.string.cert_none); break; + case PGPSignature.CASUAL_CERTIFICATION: + mType.setText(R.string.cert_casual); break; + case PGPSignature.POSITIVE_CERTIFICATION: + mType.setText(R.string.cert_positive); break; + case PGPSignature.CERTIFICATION_REVOCATION: { + mType.setText(R.string.cert_revoke); + if(sig.getHashedSubPackets().hasSubpacket(SignatureSubpacketTags.REVOCATION_REASON)) { + SignatureSubpacket p = sig.getHashedSubPackets().getSubpacket( + SignatureSubpacketTags.REVOCATION_REASON); + // For some reason, this is missing in SignatureSubpacketInputStream:146 + if(!(p instanceof RevocationReason)) { + p = new RevocationReason(false, p.getData()); + } + String reason = ((RevocationReason) p).getRevocationDescription(); + mRReason.setText(reason); + mRowReason.setVisibility(View.VISIBLE); + } + break; + } + } + } + } + + @Override + public void onLoaderReset(Loader<Cursor> loader) { + } + + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + getMenuInflater().inflate(R.menu.view_cert, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_view_cert_view_signer: + // can't do this before the data is initialized + Intent viewIntent = null; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { + viewIntent = new Intent(this, ViewKeyActivity.class); + } else { + viewIntent = new Intent(this, ViewKeyActivityJB.class); + } + // + long signerMasterKeyId = ProviderHelper.getMasterKeyId(this, + KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(Long.toString(mSignerKeyId)) + ); + // TODO notify user of this, maybe offer download? + if(mSignerKeyId == 0L) + return true; + viewIntent.setData(KeyRings.buildGenericKeyRingUri( + Long.toString(signerMasterKeyId)) + ); + startActivity(viewIntent); + return true; + } + return super.onOptionsItemSelected(item); + } + +} diff --git a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyCertsFragment.java b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyCertsFragment.java index 02c04c11d..a872443af 100644 --- a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyCertsFragment.java +++ b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyCertsFragment.java @@ -17,24 +17,61 @@ package org.sufficientlysecure.keychain.ui; +import android.content.Context; import android.content.Intent; +import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.support.v4.app.Fragment; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.support.v4.widget.CursorAdapter; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import com.beardedhen.androidbootstrap.BootstrapButton; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Spinner; +import android.widget.TextView; + +import org.spongycastle.openpgp.PGPSignature; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; +import org.sufficientlysecure.keychain.provider.KeychainContract.Certs; +import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables; import org.sufficientlysecure.keychain.util.Log; +import se.emilsjolander.stickylistheaders.ApiLevelTooLowException; +import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter; +import se.emilsjolander.stickylistheaders.StickyListHeadersListView; + + +public class ViewKeyCertsFragment extends Fragment + implements LoaderManager.LoaderCallbacks<Cursor>, AdapterView.OnItemClickListener { + + // These are the rows that we will retrieve. + static final String[] PROJECTION = new String[] { + Certs._ID, + Certs.MASTER_KEY_ID, + Certs.VERIFIED, + Certs.TYPE, + Certs.RANK, + Certs.KEY_ID_CERTIFIER, + Certs.USER_ID, + Certs.SIGNER_UID + }; -public class ViewKeyCertsFragment extends Fragment { + // sort by our user id, + static final String SORT_ORDER = + Tables.CERTS + "." + Certs.RANK + " ASC, " + + Certs.VERIFIED + " DESC, " + Certs.TYPE + " DESC, " + Certs.SIGNER_UID + " ASC"; - public static final String ARG_DATA_URI = "uri"; + public static final String ARG_DATA_URI = "data_uri"; - private BootstrapButton mActionCertify; + private StickyListHeadersListView mStickyList; + private CertListAdapter mAdapter; private Uri mDataUri; @@ -42,8 +79,6 @@ public class ViewKeyCertsFragment extends Fragment { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.view_key_certs_fragment, container, false); - mActionCertify = (BootstrapButton) view.findViewById(R.id.action_certify); - return view; } @@ -51,40 +86,231 @@ public class ViewKeyCertsFragment extends Fragment { public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - Uri dataUri = getArguments().getParcelable(ARG_DATA_URI); - if (dataUri == null) { + mStickyList = (StickyListHeadersListView) getActivity().findViewById(R.id.list); + + if (!getArguments().containsKey(ARG_DATA_URI)) { Log.e(Constants.TAG, "Data missing. Should be Uri of key!"); getActivity().finish(); return; } - loadData(dataUri); + Uri uri = getArguments().getParcelable(ARG_DATA_URI); + mDataUri = Certs.buildCertsUri(uri); + + mStickyList.setAreHeadersSticky(true); + mStickyList.setDrawingListUnderStickyHeader(false); + mStickyList.setFastScrollEnabled(true); + mStickyList.setOnItemClickListener(this); + + try { + mStickyList.setFastScrollAlwaysVisible(true); + } catch (ApiLevelTooLowException e) { + } + + mStickyList.setEmptyView(getActivity().findViewById(R.id.empty)); + + // TODO this view is made visible if no data is available + // mStickyList.setEmptyView(getActivity().findViewById(R.id.empty)); + + + // Create an empty adapter we will use to display the loaded data. + mAdapter = new CertListAdapter(getActivity(), null); + mStickyList.setAdapter(mAdapter); + + getLoaderManager().initLoader(0, null, this); + } - private void loadData(Uri dataUri) { - if (dataUri.equals(mDataUri)) { - Log.d(Constants.TAG, "Same URI, no need to load the data again!"); - return; + @Override + public Loader<Cursor> onCreateLoader(int id, Bundle args) { + // Now create and return a CursorLoader that will take care of + // creating a Cursor for the data being displayed. + return new CursorLoader(getActivity(), mDataUri, PROJECTION, null, null, SORT_ORDER); + } + + @Override + public void onLoadFinished(Loader<Cursor> loader, Cursor data) { + // Swap the new cursor in. (The framework will take care of closing the + // old cursor once we return.) + mAdapter.swapCursor(data); + + mStickyList.setAdapter(mAdapter); + } + + /** + * On click on item, start key view activity + */ + @Override + public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) { + if(view.getTag(R.id.tag_mki) != null) { + long masterKeyId = (Long) view.getTag(R.id.tag_mki); + long rank = (Long) view.getTag(R.id.tag_rank); + long certifierId = (Long) view.getTag(R.id.tag_certifierId); + + Intent viewIntent = new Intent(getActivity(), ViewCertActivity.class); + viewIntent.setData(Certs.buildCertsSpecificUri( + Long.toString(masterKeyId), Long.toString(rank), Long.toString(certifierId))); + startActivity(viewIntent); + } + } + + @Override + public void onLoaderReset(Loader<Cursor> loader) { + // This is called when the last Cursor provided to onLoadFinished() + // above is about to be closed. We need to make sure we are no + // longer using it. + mAdapter.swapCursor(null); + } + + /** + * Implements StickyListHeadersAdapter from library + */ + private class CertListAdapter extends CursorAdapter implements StickyListHeadersAdapter { + private LayoutInflater mInflater; + private int mIndexMasterKeyId, mIndexUserId, mIndexRank; + private int mIndexSignerKeyId, mIndexSignerUserId; + private int mIndexVerified, mIndexType; + + public CertListAdapter(Context context, Cursor c) { + super(context, c, 0); + + mInflater = LayoutInflater.from(context); + initIndex(c); } - mDataUri = dataUri; + @Override + public Cursor swapCursor(Cursor newCursor) { + initIndex(newCursor); - Log.i(Constants.TAG, "mDataUri: " + mDataUri.toString()); + return super.swapCursor(newCursor); + } - mActionCertify.setOnClickListener(new View.OnClickListener() { + /** + * Get column indexes for performance reasons just once in constructor and swapCursor. For a + * performance comparison see http://stackoverflow.com/a/17999582 + * + * @param cursor + */ + private void initIndex(Cursor cursor) { + if (cursor != null) { - @Override - public void onClick(View v) { - certifyKey(mDataUri); + mIndexMasterKeyId = cursor.getColumnIndexOrThrow(Certs.MASTER_KEY_ID); + mIndexUserId = cursor.getColumnIndexOrThrow(Certs.USER_ID); + mIndexRank = cursor.getColumnIndexOrThrow(Certs.RANK); + mIndexType = cursor.getColumnIndexOrThrow(Certs.TYPE); + mIndexVerified = cursor.getColumnIndexOrThrow(Certs.VERIFIED); + mIndexSignerKeyId = cursor.getColumnIndexOrThrow(Certs.KEY_ID_CERTIFIER); + mIndexSignerUserId = cursor.getColumnIndexOrThrow(Certs.SIGNER_UID); } - }); + } - } + /** + * Bind cursor data to the item list view + * <p/> + * NOTE: CursorAdapter already implements the ViewHolder pattern in its getView() method. + * Thus no ViewHolder is required here. + */ + @Override + public void bindView(View view, Context context, Cursor cursor) { + + // set name and stuff, common to both key types + TextView wSignerKeyId = (TextView) view.findViewById(R.id.signerKeyId); + TextView wSignerUserId = (TextView) view.findViewById(R.id.signerUserId); + TextView wSignStatus = (TextView) view.findViewById(R.id.signStatus); + + String signerKeyId = PgpKeyHelper.convertKeyIdToHex(cursor.getLong(mIndexSignerKeyId)); + String signerUserId = cursor.getString(mIndexSignerUserId); + switch(cursor.getInt(mIndexType)) { + case PGPSignature.DEFAULT_CERTIFICATION: // 0x10 + wSignStatus.setText(R.string.cert_default); break; + case PGPSignature.NO_CERTIFICATION: // 0x11 + wSignStatus.setText(R.string.cert_none); break; + case PGPSignature.CASUAL_CERTIFICATION: // 0x12 + wSignStatus.setText(R.string.cert_casual); break; + case PGPSignature.POSITIVE_CERTIFICATION: // 0x13 + wSignStatus.setText(R.string.cert_positive); break; + case PGPSignature.CERTIFICATION_REVOCATION: // 0x30 + wSignStatus.setText(R.string.cert_revoke); break; + } + + wSignerUserId.setText(signerUserId); + wSignerKeyId.setText(signerKeyId); + + view.setTag(R.id.tag_mki, cursor.getLong(mIndexMasterKeyId)); + view.setTag(R.id.tag_rank, cursor.getLong(mIndexRank)); + view.setTag(R.id.tag_certifierId, cursor.getLong(mIndexSignerKeyId)); + + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return mInflater.inflate(R.layout.view_key_certs_item, parent, false); + } + + /** + * Creates a new header view and binds the section headers to it. It uses the ViewHolder + * pattern. Most functionality is similar to getView() from Android's CursorAdapter. + * <p/> + * NOTE: The variables mDataValid and mCursor are available due to the super class + * CursorAdapter. + */ + @Override + public View getHeaderView(int position, View convertView, ViewGroup parent) { + HeaderViewHolder holder; + if (convertView == null) { + holder = new HeaderViewHolder(); + convertView = mInflater.inflate(R.layout.view_key_certs_header, parent, false); + holder.text = (TextView) convertView.findViewById(R.id.stickylist_header_text); + holder.count = (TextView) convertView.findViewById(R.id.certs_num); + convertView.setTag(holder); + } else { + holder = (HeaderViewHolder) convertView.getTag(); + } + + if (!mDataValid) { + // no data available at this point + Log.d(Constants.TAG, "getHeaderView: No data available at this point!"); + return convertView; + } + + if (!mCursor.moveToPosition(position)) { + throw new IllegalStateException("couldn't move cursor to position " + position); + } + + // set header text as first char in user id + String userId = mCursor.getString(mIndexUserId); + holder.text.setText(userId); + holder.count.setVisibility(View.GONE); + return convertView; + } + + /** + * Header IDs should be static, position=1 should always return the same Id that is. + */ + @Override + public long getHeaderId(int position) { + if (!mDataValid) { + // no data available at this point + Log.d(Constants.TAG, "getHeaderView: No data available at this point!"); + return -1; + } + + if (!mCursor.moveToPosition(position)) { + throw new IllegalStateException("couldn't move cursor to position " + position); + } + + // otherwise, return the first character of the name as ID + return mCursor.getInt(mIndexRank); + + // sort by the first four characters (should be enough I guess?) + // return ByteBuffer.wrap(userId.getBytes()).asLongBuffer().get(0); + } + + class HeaderViewHolder { + TextView text; + TextView count; + } - private void certifyKey(Uri dataUri) { - Intent signIntent = new Intent(getActivity(), CertifyKeyActivity.class); - signIntent.setData(dataUri); - startActivity(signIntent); } } diff --git a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyMainFragment.java b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyMainFragment.java index 010124862..6e96a338a 100644 --- a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyMainFragment.java +++ b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyMainFragment.java @@ -171,10 +171,6 @@ public class ViewKeyMainFragment extends Fragment implements static final int INDEX_UNIFIED_CREATION = 7; static final int INDEX_UNIFIED_EXPIRY = 8; - static final String[] USER_IDS_PROJECTION = new String[] { - UserIds._ID, UserIds.USER_ID, UserIds.RANK, - }; - static final String[] KEYS_PROJECTION = new String[] { Keys._ID, Keys.KEY_ID, Keys.RANK, Keys.ALGORITHM, Keys.KEY_SIZE, @@ -191,7 +187,7 @@ public class ViewKeyMainFragment extends Fragment implements } case LOADER_ID_USER_IDS: { Uri baseUri = UserIds.buildUserIdsUri(mDataUri); - return new CursorLoader(getActivity(), baseUri, USER_IDS_PROJECTION, null, null, null); + return new CursorLoader(getActivity(), baseUri, ViewKeyUserIdsAdapter.USER_IDS_PROJECTION, null, null, null); } case LOADER_ID_KEYS: { Uri baseUri = Keys.buildKeysUri(mDataUri); diff --git a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ViewKeyUserIdsAdapter.java b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ViewKeyUserIdsAdapter.java index fcaa6a940..09137f745 100644 --- a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ViewKeyUserIdsAdapter.java +++ b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ViewKeyUserIdsAdapter.java @@ -23,22 +23,31 @@ import android.support.v4.widget.CursorAdapter; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.AdapterView; import android.widget.CheckBox; import android.widget.CompoundButton; +import android.widget.ImageView; import android.widget.TextView; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds; +import org.sufficientlysecure.keychain.provider.KeychainContract.Certs; import java.util.ArrayList; -public class ViewKeyUserIdsAdapter extends CursorAdapter { +public class ViewKeyUserIdsAdapter extends CursorAdapter implements AdapterView.OnItemClickListener { private LayoutInflater mInflater; private int mIndexUserId, mIndexRank; + private int mVerifiedId, mIsRevoked, mIsPrimary; private final ArrayList<Boolean> mCheckStates; + public static final String[] USER_IDS_PROJECTION = new String[] { + UserIds._ID, UserIds.USER_ID, UserIds.RANK, + UserIds.VERIFIED, UserIds.IS_PRIMARY, UserIds.IS_REVOKED + }; + public ViewKeyUserIdsAdapter(Context context, Cursor c, int flags, boolean showCheckBoxes) { super(context, c, flags); @@ -61,8 +70,10 @@ public class ViewKeyUserIdsAdapter extends CursorAdapter { int count = newCursor.getCount(); mCheckStates.ensureCapacity(count); // initialize to true (use case knowledge: we usually want to sign all uids) - for (int i = 0; i < count; i++) { - mCheckStates.add(true); + for(int i = 0; i < count; i++) { + newCursor.moveToPosition(i); + int verified = newCursor.getInt(mVerifiedId); + mCheckStates.add(verified != Certs.VERIFIED_SECRET); } } } @@ -80,6 +91,9 @@ public class ViewKeyUserIdsAdapter extends CursorAdapter { if (cursor != null) { mIndexUserId = cursor.getColumnIndexOrThrow(UserIds.USER_ID); mIndexRank = cursor.getColumnIndexOrThrow(UserIds.RANK); + mVerifiedId = cursor.getColumnIndexOrThrow(UserIds.VERIFIED); + mIsRevoked = cursor.getColumnIndexOrThrow(UserIds.IS_REVOKED); + mIsPrimary = cursor.getColumnIndexOrThrow(UserIds.IS_PRIMARY); } } @@ -89,8 +103,13 @@ public class ViewKeyUserIdsAdapter extends CursorAdapter { TextView vRank = (TextView) view.findViewById(R.id.rank); TextView vUserId = (TextView) view.findViewById(R.id.userId); TextView vAddress = (TextView) view.findViewById(R.id.address); + ImageView vVerified = (ImageView) view.findViewById(R.id.certified); - vRank.setText(Integer.toString(cursor.getInt(mIndexRank))); + if(cursor.getInt(mIsPrimary) > 0) { + vRank.setText("+"); + } else { + vRank.setText(Integer.toString(cursor.getInt(mIndexRank))); + } String[] userId = PgpKeyHelper.splitUserId(cursor.getString(mIndexUserId)); if (userId[0] != null) { @@ -100,6 +119,20 @@ public class ViewKeyUserIdsAdapter extends CursorAdapter { } vAddress.setText(userId[1]); + if(cursor.getInt(mIsRevoked) > 0) { + vRank.setText(" "); + vVerified.setImageResource(android.R.drawable.presence_away); + } else { + int verified = cursor.getInt(mVerifiedId); + // TODO introduce own resources for this :) + if(verified == Certs.VERIFIED_SECRET) + vVerified.setImageResource(android.R.drawable.presence_online); + else if(verified == Certs.VERIFIED_SELF) + vVerified.setImageResource(android.R.drawable.presence_invisible); + else + vVerified.setImageResource(android.R.drawable.presence_busy); + } + // don't care further if checkboxes aren't shown if (mCheckStates == null) { return; @@ -107,7 +140,7 @@ public class ViewKeyUserIdsAdapter extends CursorAdapter { final CheckBox vCheckBox = (CheckBox) view.findViewById(R.id.checkBox); final int position = cursor.getPosition(); - vCheckBox.setClickable(false); + vCheckBox.setOnCheckedChangeListener(null); vCheckBox.setChecked(mCheckStates.get(position)); vCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override @@ -115,15 +148,17 @@ public class ViewKeyUserIdsAdapter extends CursorAdapter { mCheckStates.set(position, b); } }); - view.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - vCheckBox.toggle(); - } - }); + vCheckBox.setClickable(false); } + public void onItemClick(AdapterView<?> adapter, View view, int position, long id) { + CheckBox box = ((CheckBox) view.findViewById(R.id.checkBox)); + if(box != null) { + box.toggle(); + } + } + public ArrayList<String> getSelectedUserIds() { ArrayList<String> result = new ArrayList<String>(); for (int i = 0; i < mCheckStates.size(); i++) { diff --git a/OpenPGP-Keychain/src/main/res/layout/certify_key_activity.xml b/OpenPGP-Keychain/src/main/res/layout/certify_key_activity.xml index 6cd140739..3fa0468de 100644 --- a/OpenPGP-Keychain/src/main/res/layout/certify_key_activity.xml +++ b/OpenPGP-Keychain/src/main/res/layout/certify_key_activity.xml @@ -2,7 +2,7 @@ <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:bootstrapbutton="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" - android:layout_width="match_parent" + android:layout_width="wrap_content" android:layout_height="match_parent" > <LinearLayout @@ -114,7 +114,8 @@ <org.sufficientlysecure.keychain.ui.widget.FixedListView android:id="@+id/user_ids" android:layout_width="match_parent" - android:layout_height="wrap_content" /> + android:layout_height="wrap_content" + android:descendantFocusability="blocksDescendants" /> <TextView style="@style/SectionHeader" diff --git a/OpenPGP-Keychain/src/main/res/layout/key_list_item.xml b/OpenPGP-Keychain/src/main/res/layout/key_list_item.xml index 0abae8bbb..84ad9f9b5 100644 --- a/OpenPGP-Keychain/src/main/res/layout/key_list_item.xml +++ b/OpenPGP-Keychain/src/main/res/layout/key_list_item.xml @@ -73,6 +73,14 @@ android:text="@string/revoked" android:textColor="#e00" android:layout_gravity="center" /> + + <ImageView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/verified" + android:layout_gravity="center" + android:src="@android:drawable/presence_online" + android:paddingLeft="25dp" /> </FrameLayout> </LinearLayout> diff --git a/OpenPGP-Keychain/src/main/res/layout/view_cert_activity.xml b/OpenPGP-Keychain/src/main/res/layout/view_cert_activity.xml new file mode 100644 index 000000000..95b8ffc8d --- /dev/null +++ b/OpenPGP-Keychain/src/main/res/layout/view_cert_activity.xml @@ -0,0 +1,210 @@ +<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:bootstrapbutton="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <!-- focusable and related properties to workaround http://stackoverflow.com/q/16182331--> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:focusable="true" + android:focusableInTouchMode="true" + android:descendantFocusability="beforeDescendants" + android:orientation="vertical" + android:paddingLeft="16dp" + android:paddingRight="16dp"> + + <LinearLayout + android:orientation="horizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center" + android:layout_marginTop="14dp"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Verification Status" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="ok" + android:id="@+id/status" + android:layout_marginLeft="30dp" /> + </LinearLayout> + + <TextView + style="@style/SectionHeader" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="4dp" + android:layout_marginTop="14dp" + android:text="@string/section_cert" /> + + <TableLayout + android:layout_width="wrap_content" + android:layout_height="0dp" + android:layout_weight="1" + android:stretchColumns="1"> + + <TableRow> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="top" + android:paddingRight="10dip" + android:text="@string/label_key_id" /> + + <TextView + android:id="@+id/signee_key" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:paddingRight="5dip" /> + </TableRow> + + <TableRow> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="top" + android:paddingRight="10dip" + android:text="@string/label_user_id" /> + + <TextView + android:id="@+id/signee_uid" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:paddingRight="5dip" /> + </TableRow> + + <TableRow + android:layout_width="fill_parent" + android:layout_height="fill_parent"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="top" + android:paddingRight="10dip" + android:text="@string/label_algorithm" /> + + <TextView + android:id="@+id/algorithm" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:paddingRight="5dip" /> + </TableRow> + + <TableRow + android:layout_width="fill_parent" + android:layout_height="fill_parent"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="top" + android:paddingRight="10dip" + android:text="Type" /> + + <TextView + android:id="@+id/signature_type" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:paddingRight="5dip" /> + </TableRow> + + <TableRow + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:id="@+id/row_reason"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="top" + android:paddingRight="10dip" + android:text="Revocation Reason" /> + + <TextView + android:id="@+id/reason" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:paddingRight="5dip" /> + </TableRow> + + <TableRow> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="top" + android:paddingRight="10dip" + android:text="Creation" /> + + <TextView + android:id="@+id/creation" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:paddingRight="5dip" /> + </TableRow> + + </TableLayout> + + <TextView + style="@style/SectionHeader" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="4dp" + android:layout_marginTop="14dp" + android:text="@string/section_signer_id" /> + + <TableLayout + android:layout_width="wrap_content" + android:layout_height="0dp" + android:layout_weight="1" + android:stretchColumns="1"> + + <TableRow> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:paddingRight="10dip" + android:text="@string/label_key_id" /> + + <TextView + android:id="@+id/signer_key_id" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingRight="5dip" + android:text="" + android:typeface="monospace" /> + </TableRow> + + <TableRow> + + <TextView + android:id="@+id/label_algorithm" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:paddingRight="10dip" + android:text="@string/label_email" /> + + <TextView + android:id="@+id/signer_uid" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingRight="5dip" + android:text="" /> + </TableRow> + + </TableLayout> + + </LinearLayout> + +</ScrollView> diff --git a/OpenPGP-Keychain/src/main/res/layout/view_key_certs_fragment.xml b/OpenPGP-Keychain/src/main/res/layout/view_key_certs_fragment.xml index 9c0ecb15d..33042c541 100644 --- a/OpenPGP-Keychain/src/main/res/layout/view_key_certs_fragment.xml +++ b/OpenPGP-Keychain/src/main/res/layout/view_key_certs_fragment.xml @@ -1,38 +1,34 @@ <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:bootstrapbutton="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" - android:layout_height="match_parent"> + android:layout_height="match_parent" + android:fillViewport="true"> - <LinearLayout + <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" - android:orientation="vertical" - android:paddingLeft="16dp" - android:paddingRight="16dp"> + android:orientation="vertical"> - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginBottom="4dp" - android:layout_marginTop="14dp" - android:text="Display of existing certifications is a planned feature for a later release of OpenPGP Keychain. Stay tuned for updates!" /> + <view + android:layout_width="match_parent" + android:layout_height="match_parent" + class="se.emilsjolander.stickylistheaders.StickyListHeadersListView" + android:id="@+id/list" + android:paddingRight="32dp" + android:paddingLeft="16dp" + android:layout_alignParentStart="false" + android:layout_alignParentEnd="false" + android:layout_below="@+id/list" /> <TextView - style="@style/SectionHeader" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginTop="14dp" - android:text="@string/section_actions" /> + android:textAppearance="?android:attr/textAppearanceMedium" + android:text="@string/empty_certs" + android:id="@+id/empty" + android:visibility="gone" + android:layout_centerInParent="true" + android:paddingBottom="32dp" /> - <com.beardedhen.androidbootstrap.BootstrapButton - android:id="@+id/action_certify" - android:layout_width="match_parent" - android:layout_height="50dp" - android:layout_marginTop="4dp" - android:layout_marginBottom="4dp" - android:text="@string/key_view_action_certify" - bootstrapbutton:bb_icon_left="fa-pencil" - bootstrapbutton:bb_type="info" /> - </LinearLayout> + </RelativeLayout> -</ScrollView>
\ No newline at end of file +</ScrollView> diff --git a/OpenPGP-Keychain/src/main/res/layout/view_key_certs_header.xml b/OpenPGP-Keychain/src/main/res/layout/view_key_certs_header.xml new file mode 100644 index 000000000..04e7b8097 --- /dev/null +++ b/OpenPGP-Keychain/src/main/res/layout/view_key_certs_header.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" > + + <org.sufficientlysecure.keychain.ui.widget.UnderlineTextView + android:id="@+id/stickylist_header_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="start|left" + android:padding="8dp" + android:textColor="@color/emphasis" + android:textSize="14sp" + android:textStyle="bold" + android:text="header text" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="?android:attr/textAppearanceSmall" + android:text="certification count" + android:id="@+id/certs_num" + android:layout_centerVertical="true" + android:layout_alignParentRight="true" + android:layout_alignParentEnd="true" + android:layout_marginRight="10px" + android:visibility="visible" + android:textColor="@android:color/darker_gray" /> + +</RelativeLayout>
\ No newline at end of file diff --git a/OpenPGP-Keychain/src/main/res/layout/view_key_certs_item.xml b/OpenPGP-Keychain/src/main/res/layout/view_key_certs_item.xml new file mode 100644 index 000000000..de7570818 --- /dev/null +++ b/OpenPGP-Keychain/src/main/res/layout/view_key_certs_item.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="?android:attr/listPreferredItemHeight" + android:gravity="center_vertical" + android:paddingLeft="8dp" + android:paddingTop="4dp" + android:paddingBottom="4dp" + android:singleLine="true" + android:descendantFocusability="blocksDescendants" + android:focusable="false"> + + <TextView + android:id="@+id/signerKeyId" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="signer key id" + android:textAppearance="?android:attr/textAppearanceMedium" + android:layout_alignParentTop="true" + android:layout_alignParentLeft="true" + android:layout_alignParentStart="true" /> + + <TextView + android:id="@+id/signerUserId" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="<user@example.com>" + android:textAppearance="?android:attr/textAppearanceSmall" + android:layout_below="@+id/signerKeyId" + android:layout_alignParentLeft="true" + android:layout_alignParentStart="true" /> + + <TextView + android:id="@+id/signStatus" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="?android:attr/textAppearanceSmall" + android:text="status" + android:visibility="visible" + android:layout_above="@+id/signerUserId" + android:layout_alignParentRight="true" + android:layout_alignParentEnd="true" + android:layout_marginRight="10dp" /> + +</RelativeLayout> diff --git a/OpenPGP-Keychain/src/main/res/layout/view_key_userids_item.xml b/OpenPGP-Keychain/src/main/res/layout/view_key_userids_item.xml index 2e8cfeb04..e47f591cb 100644 --- a/OpenPGP-Keychain/src/main/res/layout/view_key_userids_item.xml +++ b/OpenPGP-Keychain/src/main/res/layout/view_key_userids_item.xml @@ -9,7 +9,9 @@ <CheckBox android:layout_width="wrap_content" android:layout_height="wrap_content" - android:id="@+id/checkBox" /> + android:id="@+id/checkBox" + android:clickable="false" + android:focusable="false" /> <TextView android:id="@+id/rank" @@ -19,12 +21,14 @@ android:text="0" android:paddingLeft="10dp" android:paddingRight="10dp" - android:layout_gravity="center_vertical" /> + android:layout_gravity="center_vertical" + android:width="30sp" /> <LinearLayout android:orientation="vertical" android:layout_width="fill_parent" - android:layout_height="wrap_content"> + android:layout_height="wrap_content" + android:layout_weight="1"> <TextView android:id="@+id/userId" @@ -40,6 +44,15 @@ android:text="@string/label_email" android:textAppearance="?android:attr/textAppearanceSmall" android:paddingLeft="10dp" /> + </LinearLayout> + <ImageView + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:id="@+id/certified" + android:src="@android:drawable/presence_invisible" + android:layout_marginLeft="5dp" + android:layout_marginRight="5dp" /> + </LinearLayout> diff --git a/OpenPGP-Keychain/src/main/res/menu/view_cert.xml b/OpenPGP-Keychain/src/main/res/menu/view_cert.xml new file mode 100644 index 000000000..8c8e455c7 --- /dev/null +++ b/OpenPGP-Keychain/src/main/res/menu/view_cert.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <item + android:id="@+id/menu_view_cert_view_signer" + app:showAsAction="never" + android:title="View signing key" /> +</menu>
\ No newline at end of file diff --git a/OpenPGP-Keychain/src/main/res/values/ids.xml b/OpenPGP-Keychain/src/main/res/values/ids.xml new file mode 100644 index 000000000..2004c0397 --- /dev/null +++ b/OpenPGP-Keychain/src/main/res/values/ids.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <item name="tag_mki" type="id" /> + <item name="tag_rank" type="id" /> + <item name="tag_certifierId" type="id" /> +</resources>
\ No newline at end of file diff --git a/OpenPGP-Keychain/src/main/res/values/strings.xml b/OpenPGP-Keychain/src/main/res/values/strings.xml index a57063441..6c6d05103 100644 --- a/OpenPGP-Keychain/src/main/res/values/strings.xml +++ b/OpenPGP-Keychain/src/main/res/values/strings.xml @@ -483,11 +483,29 @@ <string name="label_secret_key">Secret Key</string> <string name="secret_key_yes">available</string> <string name="secret_key_no">unavailable</string> - <string name="section_uids_to_sign">User IDs to sign</string> - <string name="progress_re_adding_certs">Reapplying certificates</string> <!-- hints --> <string name="encrypt_content_edit_text_hint">Write message here to encrypt and/or sign…</string> <string name="decrypt_content_edit_text_hint">Enter ciphertext here to decrypt and/or verify…</string> + <!-- unsorted --> + <string name="show_unknown_signatures">Show unknown signatures</string> + <string name="section_signer_id">Signer</string> + <string name="section_cert">Certificate Details</string> + <string name="label_user_id">User ID</string> + <string name="label_subkey_rank">Subkey Rank</string> + <string name="unknown_uid"><![CDATA[<unknown>]]></string> + <string name="empty_certs">No certificates for this key</string> + <string name="section_uids_to_sign">User IDs to sign</string> + <string name="progress_re_adding_certs">Reapplying certificates</string> + <string name="certs_list_known_secret">Show by known secret keys</string> + <string name="certs_list_known">Show by known public keys</string> + <string name="certs_list_all">Show all certificates</string> + <string name="cert_default">default</string> + <string name="cert_none">none</string> + <string name="cert_casual">casual</string> + <string name="cert_positive">positive</string> + <string name="cert_revoke">revoke</string> + <string name="never">never</string> + </resources> |