diff options
21 files changed, 1163 insertions, 112 deletions
diff --git a/OpenPGP-Keychain/src/main/AndroidManifest.xml b/OpenPGP-Keychain/src/main/AndroidManifest.xml index b1fbcf555..bd45def24 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 08ac16ec4..fa9fcfccd 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; @@ -104,6 +106,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 436c26700..586eb0776 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 @@ -412,7 +412,6 @@ public class PgpKeyHelper { algorithmStr = "RSA"; break; } - case PGPPublicKey.DSA: { algorithmStr = "DSA"; break; @@ -429,7 +428,10 @@ public class PgpKeyHelper { break; } } - return algorithmStr + ", " + keySize + " bit"; + if(keySize > 0) + return algorithmStr + ", " + keySize + " bit"; + else + return algorithmStr; } public static String getFingerPrint(Context context, long keyId) { 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 5f2354c24..404c128a1 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 @@ -53,6 +53,16 @@ public class KeychainContract { String RANK = "rank"; } + interface CertsColumns { + String KEY_RING_ROW_ID = "key_ring_row_id"; // verified id, foreign key to key_rings._ID + String RANK = "rank"; // rank of verified key + String KEY_ID = "key_id"; // verified id, not a database id + String KEY_ID_CERTIFIER = "key_id_certifier"; // verifying id, not a database id + String CREATION = "creation"; + String VERIFIED = "verified"; + String KEY_DATA = "key_data"; // certification blob + } + interface ApiAppsColumns { String PACKAGE_NAME = "package_name"; String PACKAGE_SIGNATURE = "package_signature"; @@ -80,6 +90,8 @@ public class KeychainContract { public static final String PATH_BY_MASTER_KEY_ID = "master_key_id"; public static final String PATH_BY_KEY_ID = "key_id"; + public static final String PATH_BY_KEY_ROW_ID = "key_row_id"; + public static final String PATH_BY_CERTIFIER_ID = "certifier_id"; public static final String PATH_BY_EMAILS = "emails"; public static final String PATH_BY_LIKE_EMAIL = "like_email"; @@ -89,6 +101,8 @@ public class KeychainContract { public static final String BASE_API_APPS = "api_apps"; public static final String PATH_BY_PACKAGE_NAME = "package_name"; + public static final String BASE_CERTS = "certs"; + public static class KeyRings implements KeyRingsColumns, BaseColumns { public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon() .appendPath(BASE_KEY_RINGS).build(); @@ -274,6 +288,32 @@ public class KeychainContract { } } + public static class Certs implements CertsColumns, BaseColumns { + public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon() + .appendPath(BASE_CERTS).build(); + + // do we even need this one...? just using it as default for database insert notifications~ + public static Uri buildCertsUri(String rowId) { + return CONTENT_URI.buildUpon().appendPath(rowId).build(); + } + + public static Uri buildCertsByKeyRowIdUri(String keyRingRowId) { + return CONTENT_URI.buildUpon().appendPath(PATH_BY_KEY_ROW_ID) + .appendPath(keyRingRowId).build(); + } + + public static Uri buildCertsByKeyIdUri(String keyId) { + return CONTENT_URI.buildUpon().appendPath(PATH_BY_KEY_ID).appendPath(keyId) + .build(); + } + + public static Uri buildCertsByCertifierKeyIdUri(String keyId) { + return CONTENT_URI.buildUpon().appendPath(PATH_BY_CERTIFIER_ID).appendPath(keyId) + .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 031a7d5ae..cc6a1f1e1 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 @@ -26,17 +26,19 @@ 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; public class KeychainDatabase extends SQLiteOpenHelper { private static final String DATABASE_NAME = "apg.db"; - private static final int DATABASE_VERSION = 7; + private static final int DATABASE_VERSION = 8; public interface Tables { String KEY_RINGS = "key_rings"; String KEYS = "keys"; String USER_IDS = "user_ids"; String API_APPS = "api_apps"; + String CERTS = "certs"; } private static final String CREATE_KEY_RINGS = "CREATE TABLE IF NOT EXISTS " + Tables.KEY_RINGS @@ -82,6 +84,18 @@ public class KeychainDatabase extends SQLiteOpenHelper { + ApiAppsColumns.HASH_ALORITHM + " INTEGER, " + ApiAppsColumns.COMPRESSION + " INTEGER)"; + private static final String CREATE_CERTS = "CREATE TABLE IF NOT EXISTS " + Tables.CERTS + + " (" + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + CertsColumns.KEY_RING_ROW_ID + " INTEGER NOT NULL " + + " REFERENCES " + Tables.KEY_RINGS + "(" + BaseColumns._ID + ") ON DELETE CASCADE, " + + CertsColumns.KEY_ID + " INTEGER, " // certified key + + CertsColumns.RANK + " INTEGER, " // key rank of certified uid + + CertsColumns.KEY_ID_CERTIFIER + " INTEGER, " // certifying key + + CertsColumns.CREATION + " INTEGER, " + + CertsColumns.VERIFIED + " INTEGER, " + + CertsColumns.KEY_DATA + " BLOB)"; + + KeychainDatabase(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } @@ -94,6 +108,7 @@ public class KeychainDatabase extends SQLiteOpenHelper { db.execSQL(CREATE_KEYS); db.execSQL(CREATE_USER_IDS); db.execSQL(CREATE_API_APPS); + db.execSQL(CREATE_CERTS); } @Override @@ -133,6 +148,11 @@ public class KeychainDatabase extends SQLiteOpenHelper { db.execSQL("ALTER TABLE " + Tables.KEYS + " ADD COLUMN " + KeysColumns.FINGERPRINT + " BLOB;"); break; + case 7: + // new table: certs + db.execSQL(CREATE_CERTS); + + break; default: break; 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 b963ceb39..f34423a71 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 @@ -76,6 +76,14 @@ public class KeychainProvider extends ContentProvider { private static final int UNIFIED_KEY_RING = 401; + private static final int CERTS = 401; + private static final int CERTS_BY_KEY_ID = 402; + private static final int CERTS_BY_ROW_ID = 403; + private static final int CERTS_BY_KEY_ROW_ID = 404; + private static final int CERTS_BY_KEY_ROW_ID_ALL = 405; + private static final int CERTS_BY_CERTIFIER_ID = 406; + private static final int CERTS_BY_KEY_ROW_ID_HAS_SECRET = 407; + // private static final int DATA_STREAM = 401; protected UriMatcher mUriMatcher; @@ -236,6 +244,27 @@ public class KeychainProvider extends ContentProvider { + KeychainContract.PATH_BY_PACKAGE_NAME + "/*", API_APPS_BY_PACKAGE_NAME); /** + /** + * certifications + * + * <pre> + * </pre> + * + */ + matcher.addURI(authority, KeychainContract.BASE_CERTS, CERTS); + matcher.addURI(authority, KeychainContract.BASE_CERTS + "/#", CERTS_BY_ROW_ID); + matcher.addURI(authority, KeychainContract.BASE_CERTS + "/" + + KeychainContract.PATH_BY_KEY_ROW_ID + "/#", CERTS_BY_KEY_ROW_ID); + matcher.addURI(authority, KeychainContract.BASE_CERTS + "/" + + KeychainContract.PATH_BY_KEY_ROW_ID + "/#/all", CERTS_BY_KEY_ROW_ID_ALL); + matcher.addURI(authority, KeychainContract.BASE_CERTS + "/" + + KeychainContract.PATH_BY_KEY_ROW_ID + "/#/has_secret", CERTS_BY_KEY_ROW_ID_HAS_SECRET); + matcher.addURI(authority, KeychainContract.BASE_CERTS + "/" + + KeychainContract.PATH_BY_KEY_ID + "/#", CERTS_BY_KEY_ID); + matcher.addURI(authority, KeychainContract.BASE_CERTS + "/" + + KeychainContract.PATH_BY_CERTIFIER_ID + "/#", CERTS_BY_CERTIFIER_ID); + + /** * data stream * * <pre> @@ -425,8 +454,28 @@ public class KeychainProvider extends ContentProvider { projectionMap.put(KeyRingsColumns.MASTER_KEY_ID, Tables.KEY_RINGS + "." + KeyRingsColumns.MASTER_KEY_ID); + // this is the count of known secret keys who certified this uid + projectionMap.put("verified", "COUNT(" + Tables.KEYS + "." + Keys._ID + ") AS verified"); + return projectionMap; } + private HashMap<String, String> getProjectionMapForCerts() { + + HashMap<String, String> pmap = new HashMap<String, String>(); + pmap.put(Certs._ID, Tables.CERTS + "." + Certs._ID); + pmap.put(Certs.KEY_ID, Tables.CERTS + "." + Certs.KEY_ID); + pmap.put(Certs.RANK, Tables.CERTS + "." + Certs.RANK); + pmap.put(Certs.CREATION, Tables.CERTS + "." + Certs.CREATION); + pmap.put(Certs.KEY_ID_CERTIFIER, Tables.CERTS + "." + Certs.KEY_ID_CERTIFIER); + pmap.put(Certs.KEY_DATA, Tables.CERTS + "." + Certs.KEY_DATA); + pmap.put(Certs.VERIFIED, Tables.CERTS + "." + Certs.VERIFIED); + // verified key data + pmap.put(UserIds.USER_ID, Tables.USER_IDS + "." + UserIds.USER_ID); + // verifying key data + pmap.put("signer_uid", "signer." + UserIds.USER_ID + " AS signer_uid"); + + return pmap; + } /** * Builds default query for keyRings: KeyRings table is joined with UserIds and Keys @@ -632,21 +681,37 @@ public class KeychainProvider extends ContentProvider { break; case PUBLIC_KEY_RING_BY_MASTER_KEY_ID_USER_ID: - qb.setTables(Tables.USER_IDS + " INNER JOIN " + Tables.KEY_RINGS + " ON " + "(" - + Tables.KEY_RINGS + "." + BaseColumns._ID + " = " + Tables.USER_IDS + "." - + KeysColumns.KEY_RING_ROW_ID + " )"); - qb.appendWhere(Tables.KEY_RINGS + "." + KeyRingsColumns.MASTER_KEY_ID + " = "); - qb.appendWhereEscapeString(uri.getPathSegments().get(3)); + case PUBLIC_KEY_RING_USER_ID: + case SECRET_KEY_RING_USER_ID: + qb.setTables(Tables.USER_IDS + + " INNER JOIN " + Tables.KEY_RINGS + " ON (" + + Tables.KEY_RINGS + "." + BaseColumns._ID + " = " + + Tables.USER_IDS + "." + KeysColumns.KEY_RING_ROW_ID + + ") LEFT JOIN " + Tables.CERTS + " ON (" + + Tables.USER_IDS + "." + UserIds.KEY_RING_ROW_ID + " = " + + Tables.CERTS + "." + Certs.KEY_RING_ROW_ID + + " AND " + Tables.USER_IDS + "." + UserIds.RANK + " = " + + Tables.CERTS + "." + Certs.RANK + + ") LEFT JOIN " + Tables.KEYS + " ON (" + + Tables.KEYS + "." + Keys.KEY_ID + " = " + + Tables.CERTS + "." + Certs.KEY_ID_CERTIFIER + // might introduce a "trusted" flag later? for now, we simply assume + // every private key's signature is good. + + " AND " + Tables.KEYS + "." + Keys.TYPE + + " == " + KeyTypes.SECRET + + ")"); + + groupBy = Tables.USER_IDS + "." + UserIds.RANK; qb.setProjectionMap(getProjectionMapForUserIds()); - break; - - case PUBLIC_KEY_RING_USER_ID: - case SECRET_KEY_RING_USER_ID: - qb.setTables(Tables.USER_IDS); - qb.appendWhere(UserIdsColumns.KEY_RING_ROW_ID + " = "); - qb.appendWhereEscapeString(uri.getPathSegments().get(2)); + if(match == PUBLIC_KEY_RING_BY_MASTER_KEY_ID_USER_ID) { + qb.appendWhere(Tables.KEY_RINGS + "." + KeyRingsColumns.MASTER_KEY_ID + " = "); + qb.appendWhereEscapeString(uri.getPathSegments().get(3)); + } else { + qb.appendWhere(Tables.USER_IDS + "." + UserIdsColumns.KEY_RING_ROW_ID + " = "); + qb.appendWhereEscapeString(uri.getPathSegments().get(2)); + } break; @@ -661,6 +726,47 @@ public class KeychainProvider extends ContentProvider { break; + case CERTS_BY_ROW_ID: + case CERTS_BY_KEY_ROW_ID_ALL: + case CERTS_BY_KEY_ROW_ID_HAS_SECRET: + case CERTS_BY_KEY_ROW_ID: + qb.setTables(Tables.CERTS + + " JOIN " + Tables.USER_IDS + " ON (" + + Tables.CERTS + "." + Certs.KEY_RING_ROW_ID + " = " + + Tables.USER_IDS + "." + UserIds.KEY_RING_ROW_ID + + " AND " + + Tables.CERTS + "." + Certs.RANK + " = " + + Tables.USER_IDS + "." + UserIds.RANK + // noooooooot sure about this~ database design + + ")" + (match == CERTS_BY_KEY_ROW_ID_ALL ? " LEFT" : "") + + " JOIN " + Tables.KEYS + " ON (" + + Tables.CERTS + "." + Certs.KEY_ID_CERTIFIER + " = " + + Tables.KEYS + "." + Keys.KEY_ID + + (match == CERTS_BY_KEY_ROW_ID_HAS_SECRET ? + " AND " + Tables.KEYS + "." + Keys.TYPE + " = " + KeyTypes.SECRET : "" + ) + + ") LEFT JOIN " + Tables.USER_IDS + " AS signer ON (" + + Tables.KEYS + "." + Keys.KEY_RING_ROW_ID + " = " + + "signer." + UserIds.KEY_RING_ROW_ID + + " AND " + + "signer." + Keys.RANK + " = 0" + + ")"); + + qb.setProjectionMap(getProjectionMapForCerts()); + + groupBy = Tables.CERTS + "." + Certs.RANK + ", " + + Tables.CERTS + "." + Certs.KEY_ID_CERTIFIER; + + if(match == CERTS_BY_ROW_ID) { + qb.appendWhere(Tables.CERTS + "." + Certs._ID + " = "); + qb.appendWhereEscapeString(uri.getPathSegments().get(1)); + } else { + qb.appendWhere(Tables.CERTS + "." + Certs.KEY_RING_ROW_ID + " = "); + qb.appendWhereEscapeString(uri.getPathSegments().get(2)); + } + + break; + case API_APPS: qb.setTables(Tables.API_APPS); @@ -771,6 +877,13 @@ public class KeychainProvider extends ContentProvider { rowUri = ApiApps.buildIdUri(Long.toString(rowId)); break; + case CERTS_BY_KEY_ROW_ID: + rowId = db.insertOrThrow(Tables.CERTS, null, values); + // kinda useless.. should this be buildCertsByKeyRowIdUri? + // rowUri = Certs.buildCertsUri(Long.toString(rowId)); + rowUri = uri; + + break; default: throw new UnsupportedOperationException("Unknown uri: " + uri); } 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 db2c57207..1d249c67e 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,10 @@ package org.sufficientlysecure.keychain.provider; +import java.security.SignatureException; +import org.spongycastle.bcpg.ArmoredOutputStream; +import org.spongycastle.bcpg.UserAttributePacket; +import org.spongycastle.bcpg.UserAttributeSubpacket; import android.content.*; import android.database.Cursor; import android.database.DatabaseUtils; @@ -24,6 +28,7 @@ import android.net.Uri; import android.os.RemoteException; import org.spongycastle.bcpg.ArmoredOutputStream; import org.spongycastle.openpgp.*; +import org.spongycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.pgp.PgpConversionHelper; import org.sufficientlysecure.keychain.pgp.PgpHelper; @@ -32,6 +37,7 @@ import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps; 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.provider.KeychainDatabase.Tables; import org.sufficientlysecure.keychain.service.remote.AppSettings; import org.sufficientlysecure.keychain.util.IterableIterator; @@ -41,31 +47,39 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Date; +import java.util.HashMap; +import java.util.Map; + public class ProviderHelper { /** * Private helper method to get PGPKeyRing from database */ - 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, KeyRings.KEY_RING_DATA}, null, null, null); + new String[]{KeyRings._ID, KeyRings.MASTER_KEY_ID, KeyRings.KEY_RING_DATA}, null, null, null); - PGPKeyRing keyRing = null; - if (cursor != null && cursor.moveToFirst()) { + Map<Long, PGPKeyRing> result = new HashMap<Long, PGPKeyRing>(cursor.getCount()); + if (cursor != null && cursor.moveToFirst()) do { int keyRingDataCol = cursor.getColumnIndex(KeyRings.KEY_RING_DATA); + int masterKeyIdCol = cursor.getColumnIndex(KeyRings.MASTER_KEY_ID); byte[] data = cursor.getBlob(keyRingDataCol); if (data != null) { - keyRing = PgpConversionHelper.BytesToPGPKeyRing(data); + result.put(cursor.getLong(masterKeyIdCol), PgpConversionHelper.BytesToPGPKeyRing(data)); } - } + + } while(cursor.moveToNext()); if (cursor != null) { cursor.close(); } - return keyRing; + return result; + } + public static PGPKeyRing getPGPKeyRing(Context context, Uri queryUri) { + return getPGPKeyRings(context, queryUri).values().iterator().next(); } /** @@ -192,21 +206,50 @@ public class ProviderHelper { ++rank; } + // get a list of owned secret keys, for verification filtering + Map<Long, PGPKeyRing> allKeyRings = getPGPKeyRings(context, KeyRings.buildSecretKeyRingsUri()); + int userIdRank = 0; for (String userId : new IterableIterator<String>(masterKey.getUserIDs())) { operations.add(buildPublicUserIdOperations(context, keyRingRowId, userId, userIdRank)); - ++userIdRank; - } - 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)); - } + // look through signatures for this specific key + for (PGPSignature cert : new IterableIterator<PGPSignature>( + masterKey.getSignaturesForID(userId))) { + long certId = cert.getKeyID(); + boolean verified = false; + // do verify signatures from our own private keys + if(allKeyRings.containsKey(certId)) try { + // mark them as verified + cert.init( + new JcaPGPContentVerifierBuilderProvider().setProvider( + Constants.BOUNCY_CASTLE_PROVIDER_NAME), + allKeyRings.get(certId).getPublicKey()); + verified = cert.verifyCertification(userId, masterKey); + Log.d(Constants.TAG, "Verified sig for " + userId + " " + verified + " from " + + PgpKeyHelper.convertKeyIdToHex(cert.getKeyID()) + ); + } 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); + } + Log.d(Constants.TAG, "sig for " + userId + " from " + + PgpKeyHelper.convertKeyIdToHex(cert.getKeyID()) + ); + // regardless of verification, save the certification + operations.add(buildPublicCertOperations( + context, keyRingRowId, userIdRank, masterKey.getKeyID(), cert, verified)); + } + ++userIdRank; + } try { context.getContentResolver().applyBatch(KeychainContract.CONTENT_AUTHORITY, operations); @@ -313,6 +356,30 @@ public class ProviderHelper { } /** + * Build ContentProviderOperation to add PGPPublicKey to database corresponding to a keyRing + */ + private static ContentProviderOperation buildPublicCertOperations(Context context, + long keyRingRowId, + int rank, + long keyId, + PGPSignature cert, + boolean verified) + throws IOException { + ContentValues values = new ContentValues(); + values.put(Certs.KEY_RING_ROW_ID, keyRingRowId); + values.put(Certs.RANK, rank); + values.put(Certs.KEY_ID, keyId); + values.put(Certs.KEY_ID_CERTIFIER, cert.getKeyID()); + values.put(Certs.CREATION, cert.getCreationTime().getTime() / 1000); + values.put(Certs.VERIFIED, verified); + values.put(Certs.KEY_DATA, cert.getEncoded()); + + Uri uri = Certs.buildCertsByKeyRowIdUri(Long.toString(keyRingRowId)); + + return ContentProviderOperation.newInsert(uri).withValues(values).build(); + } + + /** * Build ContentProviderOperation to add PublicUserIds to database corresponding to a keyRing */ private static ContentProviderOperation buildPublicUserIdOperations(Context context, 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 d8df5ced3..f2e6c4bd9 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 @@ -43,6 +43,7 @@ import org.sufficientlysecure.keychain.helper.Preferences; import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.provider.KeychainDatabase; import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.service.KeychainIntentService; import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler; @@ -164,7 +165,7 @@ public class CertifyKeyActivity extends ActionBarActivity implements KeychainContract.KeyRings._ID, KeychainContract.KeyRings.MASTER_KEY_ID, KeychainContract.Keys.FINGERPRINT, - KeychainContract.UserIds.USER_ID + KeychainContract.UserIds.USER_ID, }; static final int INDEX_MASTER_KEY_ID = 1; static final int INDEX_FINGERPRINT = 2; @@ -174,10 +175,11 @@ public class CertifyKeyActivity extends ActionBarActivity implements new String[]{ KeychainContract.UserIds._ID, KeychainContract.UserIds.USER_ID, - KeychainContract.UserIds.RANK + KeychainContract.UserIds.RANK, + "verified" }; static final String USER_IDS_SORT_ORDER = - KeychainContract.UserIds.RANK + " ASC"; + KeychainDatabase.Tables.USER_IDS + "." + KeychainContract.UserIds.RANK + " ASC"; @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { 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 5ac59965d..a08f4bc74 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 @@ -480,8 +480,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) { @@ -519,8 +519,10 @@ public class KeyListFragment extends Fragment public void onClick(View view) { Intent editIntent = new Intent(getActivity(), EditKeyActivity.class); editIntent.setData( - KeychainContract.KeyRings - .buildSecretKeyRingsByMasterKeyIdUri(Long.toString(id))); + KeychainContract.KeyRings.buildSecretKeyRingsByMasterKeyIdUri( + Long.toString(id) + ) + ); editIntent.setAction(EditKeyActivity.ACTION_EDIT_KEY); startActivityForResult(editIntent, 0); } @@ -595,8 +597,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..611864d8e --- /dev/null +++ b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewCertActivity.java @@ -0,0 +1,200 @@ +/* + * 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.widget.TextView; + +import org.spongycastle.openpgp.PGPSignature; +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.util.Log; + +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[] { + KeychainContract.Certs._ID, + KeychainContract.Certs.KEY_ID, + KeychainContract.UserIds.USER_ID, + KeychainContract.Certs.CREATION, + KeychainContract.Certs.KEY_ID_CERTIFIER, + "signer_uid", + KeychainContract.Certs.KEY_DATA + }; + private static final int INDEX_KEY_ID = 1; + private static final int INDEX_USER_ID = 2; + private static final int INDEX_CREATION = 3; + private static final int INDEX_KEY_ID_CERTIFIER = 4; + private static final int INDEX_UID_CERTIFIER = 5; + private static final int INDEX_KEY_DATA = 6; + + private Uri mDataUri; + + private long mSignerKeyId; + + private TextView mSigneeKey, mSigneeUid, mRank, mAlgorithm, mType, mCreation, mExpiry; + private TextView mSignerKey, mSignerUid; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + ActionBar actionBar = getSupportActionBar(); + actionBar.setDisplayHomeAsUpEnabled(true); + + setContentView(R.layout.view_cert_activity); + + 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); + mCreation = (TextView) findViewById(R.id.creation); + mExpiry = (TextView) findViewById(R.id.expiry); + + mSignerKey = (TextView) findViewById(R.id.signer_key_id); + mSignerUid = (TextView) findViewById(R.id.signer_uid); + + 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_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_UID_CERTIFIER); + if(signerUid != null) + mSignerUid.setText(signerUid); + else + mSignerUid.setText(R.string.unknown_uid); + + byte[] sigData = data.getBlob(INDEX_KEY_DATA); + PGPSignature sig = PgpConversionHelper.BytesToPGPSignature(sigData); + if(sig != null) { + String algorithmStr = PgpKeyHelper.getAlgorithmInfo(sig.getKeyAlgorithm(), 0); + mAlgorithm.setText(algorithmStr); + + switch(sig.getSignatureType()) { + case PGPSignature.DEFAULT_CERTIFICATION: + mType.setText(R.string.sig_type_default); break; + case PGPSignature.NO_CERTIFICATION: + mType.setText(R.string.sig_type_none); break; + case PGPSignature.CASUAL_CERTIFICATION: + mType.setText(R.string.sig_type_casual); break; + case PGPSignature.POSITIVE_CERTIFICATION: + mType.setText(R.string.sig_type_positive); break; + } + + long expiry = sig.getHashedSubPackets().getSignatureExpirationTime(); + if(expiry == 0) + mExpiry.setText("never"); + else { + Date expiryDate = new Date(creationDate.getTime() + expiry * 1000); + mExpiry.setText(DateFormat.getDateFormat(getApplicationContext()).format(expiryDate)); + } + } + } + } + + @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_verify: + // TODO verification routine + return true; + case R.id.menu_view_cert_view_signer: + // can't do this before the data is initialized + // TODO notify user of this, maybe offer download? + if(mSignerKeyId == 0) + return true; + 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); + } + viewIntent.setData(KeychainContract.KeyRings.buildPublicKeyRingsByMasterKeyIdUri( + Long.toString(mSignerKeyId)) + ); + startActivity(viewIntent); + return true; + } + return super.onOptionsItemSelected(item); + } + +} diff --git a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java index 93bb83003..592c4890b 100644 --- a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java +++ b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java @@ -83,13 +83,11 @@ public class ViewKeyActivity extends ActionBarActivity { selectedTab = intent.getExtras().getInt(EXTRA_SELECTED_TAB); } - { - // normalize mDataUri to a "by row id" query, to ensure it works with any - // given valid /public/ query - long rowId = ProviderHelper.getRowId(this, getIntent().getData()); - // TODO: handle (rowId == 0) with something else than a crash - mDataUri = KeychainContract.KeyRings.buildPublicKeyRingsUri(Long.toString(rowId)); - } + // normalize mDataUri to a "by row id" query, to ensure it works with any + // given valid /public/ query + long rowId = ProviderHelper.getRowId(this, getIntent().getData()); + // TODO: handle (rowId == 0) with something else than a crash + mDataUri = KeychainContract.KeyRings.buildPublicKeyRingsUri(Long.toString(rowId)); Bundle mainBundle = new Bundle(); mainBundle.putParcelable(ViewKeyMainFragment.ARG_DATA_URI, mDataUri); @@ -97,7 +95,7 @@ public class ViewKeyActivity extends ActionBarActivity { ViewKeyMainFragment.class, mainBundle, (selectedTab == 0)); Bundle certBundle = new Bundle(); - certBundle.putParcelable(ViewKeyCertsFragment.ARG_DATA_URI, mDataUri); + certBundle.putLong(ViewKeyCertsFragment.ARG_KEYRING_ROW_ID, rowId); mTabsAdapter.addTab(actionBar.newTab().setText(getString(R.string.key_view_tab_certs)), ViewKeyCertsFragment.class, certBundle, (selectedTab == 1)); } 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..c65e9e691 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,74 +17,320 @@ 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.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; +import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.provider.KeychainDatabase; 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[] { + KeychainContract.Certs._ID, + KeychainContract.Certs.VERIFIED, + KeychainContract.Certs.RANK, + KeychainContract.Certs.KEY_ID_CERTIFIER, + KeychainContract.UserIds.USER_ID, + "signer_uid" + }; + + // sort by our user id, + static final String SORT_ORDER = + KeychainDatabase.Tables.USER_IDS + "." + KeychainContract.UserIds.RANK + " ASC, " + + KeychainDatabase.Tables.CERTS + "." + KeychainContract.Certs.VERIFIED + " DESC, " + + "signer_uid ASC"; -public class ViewKeyCertsFragment extends Fragment { + public static final String ARG_KEYRING_ROW_ID = "row_id"; - public static final String ARG_DATA_URI = "uri"; + private StickyListHeadersListView mStickyList; + private Spinner mSpinner; - private BootstrapButton mActionCertify; + private CertListAdapter mAdapter; + private boolean mUnknownShown = false; - private Uri mDataUri; + private Uri mBaseUri, mDataUri; @Override 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; } + private void changeShowState(int type) { + switch(type) { + case 0: + mDataUri = mBaseUri.buildUpon().appendPath("has_secret").build(); + break; + case 1: + mDataUri = mBaseUri; + break; + case 2: + mDataUri = mBaseUri.buildUpon().appendPath("all").build(); + break; + } + getLoaderManager().restartLoader(0, null, this); + } + @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - Uri dataUri = getArguments().getParcelable(ARG_DATA_URI); - if (dataUri == null) { + mSpinner = (Spinner) getActivity().findViewById(R.id.spinner); + ArrayAdapter<String> adapter = new ArrayAdapter<String>(getActivity(), + android.R.layout.simple_spinner_item, new String[] { + getResources().getString(R.string.certs_list_known_secret), + getResources().getString(R.string.certs_list_known), + getResources().getString(R.string.certs_list_all) + } ); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + mSpinner.setAdapter(adapter); + mSpinner.setSelection(1); + mSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) { + changeShowState(i); + } + + @Override + public void onNothingSelected(AdapterView<?> adapterView) { + } + }); + + mStickyList = (StickyListHeadersListView) getActivity().findViewById(R.id.list); + + if (!getArguments().containsKey(ARG_KEYRING_ROW_ID)) { Log.e(Constants.TAG, "Data missing. Should be Uri of key!"); getActivity().finish(); return; } - loadData(dataUri); + long rowId = getArguments().getLong(ARG_KEYRING_ROW_ID); + mBaseUri = KeychainContract.Certs.buildCertsByKeyRowIdUri(Long.toString(rowId)); + + 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); + + // Prepare the loader. Either re-connect with an existing one, + // or start a new one. + mDataUri = mBaseUri; + 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) { + Intent viewIntent = null; + viewIntent = new Intent(getActivity(), ViewCertActivity.class); + viewIntent.setData(KeychainContract.Certs.buildCertsUri(Long.toString(id))); + 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 mIndexCertId; + private int mIndexUserId, mIndexRank; + private int mIndexSignerKeyId, mIndexSignerUserId; + private int mIndexVerified; + + 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); + mIndexCertId = cursor.getColumnIndexOrThrow(KeychainContract.Certs._ID); + mIndexUserId = cursor.getColumnIndexOrThrow(KeychainContract.UserIds.USER_ID); + mIndexRank = cursor.getColumnIndexOrThrow(KeychainContract.UserIds.RANK); + mIndexVerified = cursor.getColumnIndexOrThrow(KeychainContract.Certs.VERIFIED); + mIndexSignerKeyId = cursor.getColumnIndexOrThrow(KeychainContract.Certs.KEY_ID_CERTIFIER); + mIndexSignerUserId = cursor.getColumnIndexOrThrow("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); + String signStatus = cursor.getInt(mIndexVerified) > 0 ? "ok" : "unknown"; + + wSignerUserId.setText(signerUserId); + wSignerKeyId.setText(signerKeyId); + wSignStatus.setText(signStatus); + + } + + @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 a828a4405..8f8c13c29 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 @@ -38,6 +38,7 @@ import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.helper.OtherHelper; import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.provider.KeychainDatabase; import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.ui.adapter.ViewKeyKeysAdapter; import org.sufficientlysecure.keychain.ui.adapter.ViewKeyUserIdsAdapter; @@ -197,22 +198,26 @@ public class ViewKeyMainFragment extends Fragment implements static final int KEYRING_INDEX_MASTER_KEY_ID = 1; static final int KEYRING_INDEX_USER_ID = 2; - static final String[] USER_IDS_PROJECTION = - new String[]{ - KeychainContract.UserIds._ID, - KeychainContract.UserIds.USER_ID, - KeychainContract.UserIds.RANK, - }; + static final String[] USER_IDS_PROJECTION = new String[]{ + KeychainContract.UserIds._ID, + KeychainContract.UserIds.USER_ID, + KeychainContract.UserIds.RANK, + "verified", + }; + // not the main user id + static final String USER_IDS_SELECTION = + KeychainDatabase.Tables.USER_IDS + "." + KeychainContract.UserIds.RANK + " > 0 "; static final String USER_IDS_SORT_ORDER = - KeychainContract.UserIds.RANK + " COLLATE LOCALIZED ASC"; + KeychainDatabase.Tables.USER_IDS + "." + KeychainContract.UserIds.RANK + " COLLATE LOCALIZED ASC"; - static final String[] KEYS_PROJECTION = - new String[]{KeychainContract.Keys._ID, KeychainContract.Keys.KEY_ID, + static final String[] KEYS_PROJECTION = new String[]{ + KeychainContract.Keys._ID, KeychainContract.Keys.KEY_ID, KeychainContract.Keys.IS_MASTER_KEY, KeychainContract.Keys.ALGORITHM, KeychainContract.Keys.KEY_SIZE, KeychainContract.Keys.CAN_CERTIFY, KeychainContract.Keys.CAN_SIGN, KeychainContract.Keys.CAN_ENCRYPT, KeychainContract.Keys.CREATION, KeychainContract.Keys.EXPIRY, - KeychainContract.Keys.FINGERPRINT}; + KeychainContract.Keys.FINGERPRINT + }; static final String KEYS_SORT_ORDER = KeychainContract.Keys.RANK + " ASC"; static final int KEYS_INDEX_ID = 0; static final int KEYS_INDEX_KEY_ID = 1; @@ -353,7 +358,6 @@ public class ViewKeyMainFragment extends Fragment implements } } - private void encryptToContact(Uri dataUri) { long keyId = ProviderHelper.getMasterKeyId(getActivity(), dataUri); 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 61572b9ce..2778ed08c 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 @@ -25,6 +25,7 @@ import android.view.View; import android.view.ViewGroup; 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; @@ -36,6 +37,7 @@ public class ViewKeyUserIdsAdapter extends CursorAdapter { private LayoutInflater mInflater; private int mIndexUserId, mIndexRank; + private int mVerifiedId; final private ArrayList<Boolean> mCheckStates; @@ -61,8 +63,11 @@ 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 == 0); + } } } @@ -79,6 +84,7 @@ public class ViewKeyUserIdsAdapter extends CursorAdapter { if (cursor != null) { mIndexUserId = cursor.getColumnIndexOrThrow(UserIds.USER_ID); mIndexRank = cursor.getColumnIndexOrThrow(UserIds.RANK); + mVerifiedId = cursor.getColumnIndexOrThrow("verified"); } } @@ -88,6 +94,7 @@ 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))); @@ -99,13 +106,20 @@ public class ViewKeyUserIdsAdapter extends CursorAdapter { } vAddress.setText(userId[1]); + int verified = cursor.getInt(mVerifiedId); + // TODO introduce own resource for this :) + if(verified > 0) + vVerified.setImageResource(android.R.drawable.presence_online); + else + vVerified.setImageResource(android.R.drawable.presence_invisible); + // don't care further if checkboxes aren't shown if(mCheckStates == null) return; 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 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..3c6c09429 --- /dev/null +++ b/OpenPGP-Keychain/src/main/res/layout/view_cert_activity.xml @@ -0,0 +1,189 @@ +<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"> + + <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> + + <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> + + <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="Expires" /> + + <TextView + android:id="@+id/expiry" + 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 299471c66..b2bfe1700 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,45 @@ <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 + <Spinner 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!" /> + android:id="@+id/spinner" + android:layout_gravity="center_horizontal" + android:gravity="center_horizontal" + android:layout_centerHorizontal="true" + android:layout_marginTop="2dp" + android:layout_marginBottom="2dp" /> + + <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/spinner" /> <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_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="60dp" - android:padding="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 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..de481c1cc --- /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="17sp" + 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..aa14e6d2f 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 @@ -24,7 +24,8 @@ <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 +41,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..dbdb2bfbe --- /dev/null +++ b/OpenPGP-Keychain/src/main/res/menu/view_cert.xml @@ -0,0 +1,13 @@ +<?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_verify" + app:showAsAction="never" + android:title="Verify signature" /> + <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/strings.xml b/OpenPGP-Keychain/src/main/res/values/strings.xml index 265bcc6db..350c57002 100644 --- a/OpenPGP-Keychain/src/main/res/values/strings.xml +++ b/OpenPGP-Keychain/src/main/res/values/strings.xml @@ -465,7 +465,23 @@ <string name="label_secret_key">Secret Key</string> <string name="secret_key_yes">available</string> <string name="secret_key_no">unavailable</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="sig_type_default">default</string> + <string name="sig_type_none">none</string> + <string name="sig_type_casual">casual</string> + <string name="sig_type_positive">positive</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> </resources> |