diff --git a/OpenKeychain/src/main/AndroidManifest.xml b/OpenKeychain/src/main/AndroidManifest.xml index fd0576100..7b4f3ee28 100644 --- a/OpenKeychain/src/main/AndroidManifest.xml +++ b/OpenKeychain/src/main/AndroidManifest.xml @@ -864,6 +864,11 @@ android:exported="false" android:theme="@style/Theme.Keychain.Transparent" android:label="@string/app_name" /> + 0); + ArrayList allUserIds = signingRing.getUnorderedUserIds(); + ArrayList confirmedUserIds; try { - ArrayList allUserIds = signingRing.getUnorderedUserIds(); - ArrayList confirmedUserIds = mKeyRepository.getConfirmedUserIds(signingRing.getMasterKeyId()); - setUserIds(allUserIds, confirmedUserIds); - - if (mSenderAddress != null) { - if (userIdListContainsAddress(mSenderAddress, confirmedUserIds)) { - mSenderStatusResult = SenderStatusResult.USER_ID_CONFIRMED; - } else if (userIdListContainsAddress(mSenderAddress, allUserIds)) { - mSenderStatusResult = SenderStatusResult.USER_ID_UNCONFIRMED; - } else { - mSenderStatusResult = SenderStatusResult.USER_ID_MISSING; - } - } else { - mSenderStatusResult = SenderStatusResult.UNKNOWN; - } - + confirmedUserIds = mKeyRepository.getConfirmedUserIds(signingRing.getMasterKeyId()); } catch (NotFoundException e) { throw new IllegalStateException("Key didn't exist anymore for user id query!", e); } + setUserIds(allUserIds, confirmedUserIds); + + mSenderStatusResult = processSenderStatusResult(allUserIds, confirmedUserIds); // either master key is expired/revoked or this specific subkey is expired/revoked setKeyExpired(signingRing.isExpired() || signingKey.isExpired()); setKeyRevoked(signingRing.isRevoked() || signingKey.isRevoked()); } + private SenderStatusResult processSenderStatusResult( + ArrayList allUserIds, ArrayList confirmedUserIds) { + if (mSenderAddress == null) { + return SenderStatusResult.UNKNOWN; + } + + if (userIdListContainsAddress(mSenderAddress, confirmedUserIds)) { + return SenderStatusResult.USER_ID_CONFIRMED; + } else if (userIdListContainsAddress(mSenderAddress, allUserIds)) { + return SenderStatusResult.USER_ID_UNCONFIRMED; + } else { + return SenderStatusResult.USER_ID_MISSING; + } + } + private static boolean userIdListContainsAddress(String senderAddress, ArrayList confirmedUserIds) { for (String rawUserId : confirmedUserIds) { UserId userId = OpenPgpUtils.splitUserId(rawUserId); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PGPPublicKeyUtils.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PGPPublicKeyUtils.java new file mode 100644 index 000000000..a3909aff8 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PGPPublicKeyUtils.java @@ -0,0 +1,99 @@ +package org.sufficientlysecure.keychain.pgp; + + +import java.util.Arrays; +import java.util.Iterator; +import java.util.NoSuchElementException; + +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVector; +import org.sufficientlysecure.keychain.util.Utf8Util; + + +@SuppressWarnings("unchecked") // BouncyCastle doesn't do generics here :( +class PGPPublicKeyUtils { + + static PGPPublicKey keepOnlyRawUserId(PGPPublicKey masterPublicKey, byte[] rawUserIdToKeep) { + boolean elementToKeepFound = false; + + Iterator it = masterPublicKey.getRawUserIDs(); + while (it.hasNext()) { + byte[] rawUserId = it.next(); + if (Arrays.equals(rawUserId, rawUserIdToKeep)) { + elementToKeepFound = true; + } else { + masterPublicKey = PGPPublicKey.removeCertification(masterPublicKey, rawUserId); + } + } + + if (!elementToKeepFound) { + throw new NoSuchElementException(); + } + return masterPublicKey; + } + + static PGPPublicKey keepOnlyUserId(PGPPublicKey masterPublicKey, String userIdToKeep) { + boolean elementToKeepFound = false; + + Iterator it = masterPublicKey.getRawUserIDs(); + while (it.hasNext()) { + byte[] rawUserId = it.next(); + String userId = Utf8Util.fromUTF8ByteArrayReplaceBadEncoding(rawUserId); + if (userId.contains(userIdToKeep)) { + elementToKeepFound = true; + } else { + masterPublicKey = PGPPublicKey.removeCertification(masterPublicKey, rawUserId); + } + } + + if (!elementToKeepFound) { + throw new NoSuchElementException(); + } + return masterPublicKey; + } + + static PGPPublicKey keepOnlySelfCertsForUserIds(PGPPublicKey masterPubKey) { + long masterKeyId = masterPubKey.getKeyID(); + + Iterator it = masterPubKey.getRawUserIDs(); + while (it.hasNext()) { + byte[] rawUserId = it.next(); + masterPubKey = keepOnlySelfCertsForRawUserId(masterPubKey, masterKeyId, rawUserId); + } + + return masterPubKey; + } + + private static PGPPublicKey keepOnlySelfCertsForRawUserId( + PGPPublicKey masterPubKey, long masterKeyId, byte[] rawUserId) { + Iterator it = masterPubKey.getSignaturesForID(rawUserId); + while (it.hasNext()) { + PGPSignature sig = it.next(); + if (sig.getKeyID() != masterKeyId) { + masterPubKey = PGPPublicKey.removeCertification(masterPubKey, rawUserId, sig); + } + } + return masterPubKey; + } + + static PGPPublicKey removeAllUserAttributes(PGPPublicKey masterPubKey) { + Iterator it = masterPubKey.getUserAttributes(); + + while (it.hasNext()) { + masterPubKey = PGPPublicKey.removeCertification(masterPubKey, it.next()); + } + + return masterPubKey; + } + + static PGPPublicKey removeAllDirectKeyCerts(PGPPublicKey masterPubKey) { + Iterator it = masterPubKey.getSignaturesOfType(PGPSignature.DIRECT_KEY); + + while (it.hasNext()) { + masterPubKey = PGPPublicKey.removeCertification(masterPubKey, it.next()); + } + + return masterPubKey; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyInputParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyInputParcel.java index 767bf1e9d..ae538637a 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyInputParcel.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyInputParcel.java @@ -19,8 +19,6 @@ package org.sufficientlysecure.keychain.pgp; -import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.List; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java index 77ce0dc90..4305d8a12 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java @@ -105,6 +105,24 @@ public class UncachedPublicKey { * */ public String getPrimaryUserId() { + byte[] found = getRawPrimaryUserId(); + if (found != null) { + return Utf8Util.fromUTF8ByteArrayReplaceBadEncoding(found); + } else { + return null; + } + } + + /** Returns the primary user id, as indicated by the public key's self certificates. + * + * This is an expensive operation, since potentially a lot of certificates (and revocations) + * have to be checked, and even then the result is NOT guaranteed to be constant through a + * canonicalization operation. + * + * Returns null if there is no primary user id (as indicated by certificates) + * + */ + public byte[] getRawPrimaryUserId() { byte[] found = null; PGPSignature foundSig = null; // noinspection unchecked @@ -161,11 +179,7 @@ public class UncachedPublicKey { } } } - if (found != null) { - return Utf8Util.fromUTF8ByteArrayReplaceBadEncoding(found); - } else { - return null; - } + return found; } /** diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/AutocryptPeerDataAccessObject.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/AutocryptPeerDataAccessObject.java new file mode 100644 index 000000000..768ec6321 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/AutocryptPeerDataAccessObject.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2017 Vincent Breitmoser + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.provider; + + +import java.util.Date; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; + +import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAutocryptPeer; + + +public class AutocryptPeerDataAccessObject { + private final SimpleContentResolverInterface mQueryInterface; + private final String packageName; + + + public AutocryptPeerDataAccessObject(Context context, String packageName) { + this.packageName = packageName; + + final ContentResolver contentResolver = context.getContentResolver(); + mQueryInterface = new SimpleContentResolverInterface() { + @Override + public Cursor query(Uri contentUri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + return contentResolver.query(contentUri, projection, selection, selectionArgs, sortOrder); + } + + @Override + public Uri insert(Uri contentUri, ContentValues values) { + return contentResolver.insert(contentUri, values); + } + + @Override + public int update(Uri contentUri, ContentValues values, String where, String[] selectionArgs) { + return contentResolver.update(contentUri, values, where, selectionArgs); + } + + @Override + public int delete(Uri contentUri, String where, String[] selectionArgs) { + return contentResolver.delete(contentUri, where, selectionArgs); + } + }; + } + + public AutocryptPeerDataAccessObject(SimpleContentResolverInterface queryInterface, String packageName) { + mQueryInterface = queryInterface; + this.packageName = packageName; + } + + public Long getMasterKeyIdForAutocryptPeer(String autocryptId) { + Cursor cursor = mQueryInterface.query( + ApiAutocryptPeer.buildByPackageNameAndAutocryptId(packageName, autocryptId), null, null, null, null); + + try { + if (cursor != null && cursor.moveToFirst()) { + int masterKeyIdColumn = cursor.getColumnIndex(ApiAutocryptPeer.MASTER_KEY_ID); + return cursor.getLong(masterKeyIdColumn); + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + + return null; + } + + public Date getLastSeen(String autocryptId) { + Cursor cursor = mQueryInterface.query(ApiAutocryptPeer.buildByPackageNameAndAutocryptId(packageName, autocryptId), + null, null, null, null); + + try { + if (cursor != null && cursor.moveToFirst()) { + long lastUpdated = cursor.getColumnIndex(ApiAutocryptPeer.LAST_SEEN); + return new Date(lastUpdated); + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + return null; + } + + public Date getLastSeenKey(String autocryptId) { + Cursor cursor = mQueryInterface.query(ApiAutocryptPeer.buildByPackageNameAndAutocryptId(packageName, autocryptId), + null, null, null, null); + + try { + if (cursor != null && cursor.moveToFirst()) { + long lastUpdated = cursor.getColumnIndex(ApiAutocryptPeer.LAST_SEEN_KEY); + return new Date(lastUpdated); + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + return null; + } + + public void updateToResetState(String autocryptId, Date effectiveDate) { + updateAutocryptState(autocryptId, effectiveDate, null, ApiAutocryptPeer.RESET); + } + + public void updateToSelectedState(String autocryptId, long masterKeyId) { + updateAutocryptState(autocryptId, new Date(), masterKeyId, ApiAutocryptPeer.SELECTED); + } + + public void updateToGossipState(String autocryptId, Date effectiveDate, long masterKeyId) { + updateAutocryptState(autocryptId, effectiveDate, masterKeyId, ApiAutocryptPeer.GOSSIP); + } + + public void updateToMutualState(String autocryptId, Date effectiveDate, long masterKeyId) { + updateAutocryptState(autocryptId, effectiveDate, masterKeyId, ApiAutocryptPeer.MUTUAL); + } + + public void updateToAvailableState(String autocryptId, Date effectiveDate, long masterKeyId) { + updateAutocryptState(autocryptId, effectiveDate, masterKeyId, ApiAutocryptPeer.AVAILABLE); + } + + private void updateAutocryptState(String autocryptId, Date date, Long masterKeyId, int status) { + ContentValues cv = new ContentValues(); + cv.put(ApiAutocryptPeer.MASTER_KEY_ID, masterKeyId); + cv.put(ApiAutocryptPeer.LAST_SEEN, date.getTime()); + if (masterKeyId != null) { + cv.put(ApiAutocryptPeer.LAST_SEEN_KEY, masterKeyId); + } + cv.put(ApiAutocryptPeer.STATE, status); + mQueryInterface.update(ApiAutocryptPeer.buildByPackageNameAndAutocryptId(packageName, autocryptId), cv, null, null); + } + + public void delete(String autocryptId) { + mQueryInterface.delete(ApiAutocryptPeer.buildByPackageNameAndAutocryptId(packageName, autocryptId), null, null); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeyWritableRepository.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeyWritableRepository.java index abea30df4..162ad24ed 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeyWritableRepository.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeyWritableRepository.java @@ -62,6 +62,7 @@ import org.sufficientlysecure.keychain.pgp.UncachedPublicKey; import org.sufficientlysecure.keychain.pgp.WrappedSignature; import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; +import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAutocryptPeer; import org.sufficientlysecure.keychain.provider.KeychainContract.Certs; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingData; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; @@ -602,6 +603,7 @@ public class KeyWritableRepository extends KeyRepository { android.util.Log.e(Constants.TAG, "Could not delete file!", e); return false; } + mContentResolver.delete(ApiAutocryptPeer.buildByMasterKeyId(masterKeyId),null, null); int deletedRows = mContentResolver.delete(KeyRingData.buildPublicKeyRingUri(masterKeyId), null, null); return deletedRows > 0; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java index 79bef905c..ec975a232 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java @@ -95,6 +95,15 @@ public class KeychainContract { String IDENTIFIER = "identifier"; } + interface ApiAutocryptPeerColumns { + String PACKAGE_NAME = "package_name"; + String IDENTIFIER = "identifier"; + String LAST_SEEN = "last_updated"; + String LAST_SEEN_KEY = "last_seen_key"; + String STATE = "state"; + String MASTER_KEY_ID = "master_key_id"; + } + public static final String CONTENT_AUTHORITY = Constants.PROVIDER_AUTHORITY; private static final Uri BASE_CONTENT_URI_INTERNAL = Uri @@ -121,6 +130,11 @@ public class KeychainContract { public static final String BASE_API_APPS = "api_apps"; public static final String PATH_ALLOWED_KEYS = "allowed_keys"; + public static final String PATH_BY_PACKAGE_NAME = "by_package_name"; + public static final String PATH_BY_KEY_ID = "by_key_id"; + + public static final String BASE_AUTOCRYPT_PEERS = "autocrypt_peers"; + public static class KeyRings implements BaseColumns, KeysColumns, UserPacketsColumns { public static final String MASTER_KEY_ID = KeysColumns.MASTER_KEY_ID; public static final String IS_REVOKED = KeysColumns.IS_REVOKED; @@ -133,6 +147,7 @@ public class KeychainContract { public static final String HAS_CERTIFY = "has_certify"; public static final String HAS_AUTHENTICATE = "has_authenticate"; public static final String HAS_DUPLICATE_USER_ID = "has_duplicate_user_id"; + public static final String API_KNOWN_TO_PACKAGE_NAMES = "known_to_apps"; public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon() .appendPath(BASE_KEY_RINGS).build(); @@ -332,6 +347,29 @@ public class KeychainContract { } } + public static class ApiAutocryptPeer implements ApiAutocryptPeerColumns, BaseColumns { + public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon() + .appendPath(BASE_AUTOCRYPT_PEERS).build(); + + public static final int RESET = 0; + public static final int GOSSIP = 1; + public static final int SELECTED = 2; + public static final int AVAILABLE = 3; + public static final int MUTUAL = 4; + + public static Uri buildByKeyUri(Uri uri) { + return CONTENT_URI.buildUpon().appendPath(PATH_BY_KEY_ID).appendPath(uri.getPathSegments().get(1)).build(); + } + + public static Uri buildByPackageNameAndAutocryptId(String packageName, String autocryptPeer) { + return CONTENT_URI.buildUpon().appendPath(PATH_BY_PACKAGE_NAME).appendPath(packageName).appendPath(autocryptPeer).build(); + } + + public static Uri buildByMasterKeyId(long masterKeyId) { + return CONTENT_URI.buildUpon().appendPath(PATH_BY_KEY_ID).appendPath(Long.toString(masterKeyId)).build(); + } + } + public static class Certs implements CertsColumns, BaseColumns { public static final String USER_ID = UserPacketsColumns.USER_ID; public static final String NAME = UserPacketsColumns.NAME; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java index 8c45264ab..2833805cb 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java @@ -33,6 +33,7 @@ import android.provider.BaseColumns; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAppsAllowedKeysColumns; import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAppsColumns; +import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAutocryptPeerColumns; import org.sufficientlysecure.keychain.provider.KeychainContract.CertsColumns; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingsColumns; import org.sufficientlysecure.keychain.provider.KeychainContract.KeysColumns; @@ -52,7 +53,7 @@ import org.sufficientlysecure.keychain.util.Log; */ public class KeychainDatabase extends SQLiteOpenHelper { private static final String DATABASE_NAME = "openkeychain.db"; - private static final int DATABASE_VERSION = 22; + private static final int DATABASE_VERSION = 23; private Context mContext; public interface Tables { @@ -65,6 +66,7 @@ public class KeychainDatabase extends SQLiteOpenHelper { String API_APPS = "api_apps"; String API_ALLOWED_KEYS = "api_allowed_keys"; String OVERRIDDEN_WARNINGS = "overridden_warnings"; + String API_AUTOCRYPT_PEERS = "api_autocrypt_peers"; } private static final String CREATE_KEYRINGS_PUBLIC = @@ -156,6 +158,20 @@ public class KeychainDatabase extends SQLiteOpenHelper { + Tables.KEY_RINGS_PUBLIC + "(" + KeyRingsColumns.MASTER_KEY_ID + ") ON DELETE CASCADE" + ")"; + private static final String CREATE_API_AUTOCRYPT_PEERS = + "CREATE TABLE IF NOT EXISTS " + Tables.API_AUTOCRYPT_PEERS + " (" + + ApiAutocryptPeerColumns.PACKAGE_NAME + " TEXT NOT NULL, " + + ApiAutocryptPeerColumns.IDENTIFIER + " TEXT NOT NULL, " + + ApiAutocryptPeerColumns.LAST_SEEN + " INTEGER NOT NULL, " + + ApiAutocryptPeerColumns.LAST_SEEN_KEY + " INTEGER NOT NULL, " + + ApiAutocryptPeerColumns.STATE + " INTEGER NOT NULL, " + + ApiAutocryptPeerColumns.MASTER_KEY_ID + " INTEGER NULL, " + + "PRIMARY KEY(" + ApiAutocryptPeerColumns.PACKAGE_NAME + ", " + + ApiAutocryptPeerColumns.IDENTIFIER + "), " + + "FOREIGN KEY(" + ApiAutocryptPeerColumns.PACKAGE_NAME + ") REFERENCES " + + Tables.API_APPS + "(" + ApiAppsColumns.PACKAGE_NAME + ") ON DELETE CASCADE" + + ")"; + private static final String CREATE_API_APPS = "CREATE TABLE IF NOT EXISTS " + Tables.API_APPS + " (" + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " @@ -199,6 +215,7 @@ public class KeychainDatabase extends SQLiteOpenHelper { db.execSQL(CREATE_API_APPS); db.execSQL(CREATE_API_APPS_ALLOWED_KEYS); db.execSQL(CREATE_OVERRIDDEN_WARNINGS); + db.execSQL(CREATE_API_AUTOCRYPT_PEERS); db.execSQL("CREATE INDEX keys_by_rank ON keys (" + KeysColumns.RANK + ");"); db.execSQL("CREATE INDEX uids_by_rank ON user_packets (" + UserPacketsColumns.RANK + ", " @@ -318,8 +335,17 @@ public class KeychainDatabase extends SQLiteOpenHelper { case 21: db.execSQL("ALTER TABLE updated_keys ADD COLUMN seen_on_keyservers INTEGER;"); - if (oldVersion == 18 || oldVersion == 19 || oldVersion == 20 || oldVersion == 21) { - // no consolidate for now, often crashes! + case 22: + db.execSQL("CREATE TABLE IF NOT EXISTS api_autocrypt_peers (" + + "package_name TEXT NOT NULL, " + + "identifier TEXT NOT NULL, " + + "last_updated INTEGER NOT NULL, " + + "master_key_id INTEGER NOT NULL, " + + "PRIMARY KEY(package_name, identifier), " + + "FOREIGN KEY(package_name) REFERENCES api_apps(package_name) ON DELETE CASCADE" + + ")"); + + if (oldVersion == 18 || oldVersion == 19 || oldVersion == 20 || oldVersion == 21 || oldVersion == 22) { return; } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainExternalContract.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainExternalContract.java index f4d9d657b..8010abb36 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainExternalContract.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainExternalContract.java @@ -22,20 +22,22 @@ import android.net.Uri; import android.provider.BaseColumns; import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAutocryptPeer; public class KeychainExternalContract { - public static final int KEY_STATUS_UNVERIFIED = 1; - public static final int KEY_STATUS_VERIFIED = 2; // this is in KeychainExternalContract already, but we want to be double // sure this isn't mixed up with the internal one! public static final String CONTENT_AUTHORITY_EXTERNAL = Constants.PROVIDER_AUTHORITY + ".exported"; - private static final Uri BASE_CONTENT_URI_EXTERNAL = Uri .parse("content://" + CONTENT_AUTHORITY_EXTERNAL); - public static final String BASE_EMAIL_STATUS = "email_status"; + public static final String BASE_AUTOCRYPT_STATUS = "autocrypt_status"; + + public static final int KEY_STATUS_UNAVAILABLE = 0; + public static final int KEY_STATUS_UNVERIFIED = 1; + public static final int KEY_STATUS_VERIFIED = 2; public static class EmailStatus implements BaseColumns { public static final String EMAIL_ADDRESS = "email_address"; @@ -50,7 +52,33 @@ public class KeychainExternalContract { = "vnd.android.cursor.dir/vnd.org.sufficientlysecure.keychain.provider.email_status"; } - private KeychainExternalContract() { + public static class AutocryptStatus implements BaseColumns { + public static final String ADDRESS = "address"; + + public static final String UID_ADDRESS = "uid_address"; + public static final String UID_KEY_STATUS = "uid_key_status"; + public static final String UID_MASTER_KEY_ID = "uid_master_key_id"; + public static final String UID_CANDIDATES = "uid_candidates"; + + public static final String AUTOCRYPT_MASTER_KEY_ID = "autocrypt_master_key_id"; + public static final String AUTOCRYPT_KEY_STATUS = "autocrypt_key_status"; + public static final String AUTOCRYPT_PEER_STATE = "autocrypt_peer_state"; + public static final String AUTOCRYPT_LAST_SEEN = "autocrypt_last_seen"; + public static final String AUTOCRYPT_LAST_SEEN_KEY = "autocrypt_last_seen_key"; + + public static final int AUTOCRYPT_PEER_RESET = ApiAutocryptPeer.RESET; + public static final int AUTOCRYPT_PEER_GOSSIP = ApiAutocryptPeer.GOSSIP; + public static final int AUTOCRYPT_PEER_SELECTED = ApiAutocryptPeer.SELECTED; + public static final int AUTOCRYPT_PEER_AVAILABLE = ApiAutocryptPeer.AVAILABLE; + public static final int AUTOCRYPT_PEER_MUTUAL = ApiAutocryptPeer.MUTUAL; + + public static final Uri CONTENT_URI = BASE_CONTENT_URI_EXTERNAL.buildUpon() + .appendPath(BASE_AUTOCRYPT_STATUS).build(); + + public static final String CONTENT_TYPE = + "vnd.android.cursor.dir/vnd.org.sufficientlysecure.keychain.provider.email_status"; } + private KeychainExternalContract() { + } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java index 2e8177d88..921436b1b 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java @@ -19,7 +19,14 @@ package org.sufficientlysecure.keychain.provider; + +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.List; + import android.content.ContentProvider; +import android.content.ContentResolver; import android.content.ContentValues; import android.content.UriMatcher; import android.database.Cursor; @@ -35,6 +42,7 @@ import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute; import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAllowedKeys; import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps; +import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAutocryptPeer; import org.sufficientlysecure.keychain.provider.KeychainContract.Certs; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingData; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; @@ -45,11 +53,6 @@ import org.sufficientlysecure.keychain.provider.KeychainContract.UserPacketsColu import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables; import org.sufficientlysecure.keychain.util.Log; -import java.util.Arrays; -import java.util.Date; -import java.util.HashMap; -import java.util.List; - public class KeychainProvider extends ContentProvider { private static final int KEY_RINGS_UNIFIED = 101; @@ -78,6 +81,10 @@ public class KeychainProvider extends ContentProvider { private static final int UPDATED_KEYS = 500; private static final int UPDATED_KEYS_SPECIFIC = 501; + private static final int AUTOCRYPT_PEERS_BY_MASTER_KEY_ID = 601; + private static final int AUTOCRYPT_PEERS_BY_PACKAGE_NAME = 602; + private static final int AUTOCRYPT_PEERS_BY_PACKAGE_NAME_AND_TRUST_ID = 603; + protected UriMatcher mUriMatcher; /** @@ -190,6 +197,22 @@ public class KeychainProvider extends ContentProvider { matcher.addURI(authority, KeychainContract.BASE_API_APPS + "/*/" + KeychainContract.PATH_ALLOWED_KEYS, API_ALLOWED_KEYS); + /** + * Trust Identity access + * + *
+         * trust_ids/by_key_id/_
+         *
+         * 
+ */ + matcher.addURI(authority, KeychainContract.BASE_AUTOCRYPT_PEERS + "/" + + KeychainContract.PATH_BY_KEY_ID + "/*", AUTOCRYPT_PEERS_BY_MASTER_KEY_ID); + matcher.addURI(authority, KeychainContract.BASE_AUTOCRYPT_PEERS + "/" + + KeychainContract.PATH_BY_PACKAGE_NAME + "/*", AUTOCRYPT_PEERS_BY_PACKAGE_NAME); + matcher.addURI(authority, KeychainContract.BASE_AUTOCRYPT_PEERS + "/" + + KeychainContract.PATH_BY_PACKAGE_NAME + "/*/*", AUTOCRYPT_PEERS_BY_PACKAGE_NAME_AND_TRUST_ID); + + /** * to access table containing last updated dates of keys */ @@ -321,6 +344,9 @@ public class KeychainProvider extends ContentProvider { projectionMap.put(KeyRings.IS_EXPIRED, "(" + Tables.KEYS + "." + Keys.EXPIRY + " IS NOT NULL AND " + Tables.KEYS + "." + Keys.EXPIRY + " < " + new Date().getTime() / 1000 + ") AS " + KeyRings.IS_EXPIRED); + projectionMap.put(KeyRings.API_KNOWN_TO_PACKAGE_NAMES, + "GROUP_CONCAT(aTI." + ApiAutocryptPeer.PACKAGE_NAME + ") AS " + + KeyRings.API_KNOWN_TO_PACKAGE_NAMES); qb.setProjectionMap(projectionMap); if (projection == null) { @@ -389,6 +415,11 @@ public class KeychainProvider extends ContentProvider { + " AND ( kC." + Keys.EXPIRY + " IS NULL OR kC." + Keys.EXPIRY + " >= " + new Date().getTime() / 1000 + " )" + ")" : "") + + (plist.contains(KeyRings.API_KNOWN_TO_PACKAGE_NAMES) ? + " LEFT JOIN " + Tables.API_AUTOCRYPT_PEERS + " AS aTI ON (" + +"aTI." + Keys.MASTER_KEY_ID + + " = " + Tables.KEYS + "." + Keys.MASTER_KEY_ID + + ")" : "") ); qb.appendWhere(Tables.KEYS + "." + Keys.RANK + " = 0"); // in case there are multiple verifying certificates @@ -635,6 +666,45 @@ public class KeychainProvider extends ContentProvider { break; } + case AUTOCRYPT_PEERS_BY_MASTER_KEY_ID: + case AUTOCRYPT_PEERS_BY_PACKAGE_NAME: + case AUTOCRYPT_PEERS_BY_PACKAGE_NAME_AND_TRUST_ID: { + if (selection != null || selectionArgs != null) { + throw new UnsupportedOperationException(); + } + + HashMap projectionMap = new HashMap<>(); + projectionMap.put(ApiAutocryptPeer._ID, "oid AS " + ApiAutocryptPeer._ID); + projectionMap.put(ApiAutocryptPeer.PACKAGE_NAME, ApiAutocryptPeer.PACKAGE_NAME); + projectionMap.put(ApiAutocryptPeer.IDENTIFIER, ApiAutocryptPeer.IDENTIFIER); + projectionMap.put(ApiAutocryptPeer.MASTER_KEY_ID, ApiAutocryptPeer.MASTER_KEY_ID); + projectionMap.put(ApiAutocryptPeer.LAST_SEEN, ApiAutocryptPeer.LAST_SEEN); + qb.setProjectionMap(projectionMap); + + qb.setTables(Tables.API_AUTOCRYPT_PEERS); + + if (match == AUTOCRYPT_PEERS_BY_MASTER_KEY_ID) { + long masterKeyId = Long.parseLong(uri.getLastPathSegment()); + + selection = Tables.API_AUTOCRYPT_PEERS + "." + ApiAutocryptPeer.MASTER_KEY_ID + " = ?"; + selectionArgs = new String[] { Long.toString(masterKeyId) }; + } else if (match == AUTOCRYPT_PEERS_BY_PACKAGE_NAME) { + String packageName = uri.getPathSegments().get(2); + + selection = Tables.API_AUTOCRYPT_PEERS + "." + ApiAutocryptPeer.PACKAGE_NAME + " = ?"; + selectionArgs = new String[] { packageName }; + } else { // AUTOCRYPT_PEERS_BY_PACKAGE_NAME_AND_TRUST_ID + String packageName = uri.getPathSegments().get(2); + String autocryptPeer = uri.getPathSegments().get(3); + + selection = Tables.API_AUTOCRYPT_PEERS + "." + ApiAutocryptPeer.PACKAGE_NAME + " = ? AND " + + Tables.API_AUTOCRYPT_PEERS + "." + ApiAutocryptPeer.IDENTIFIER + " = ?"; + selectionArgs = new String[] { packageName, autocryptPeer }; + } + + break; + } + case UPDATED_KEYS: case UPDATED_KEYS_SPECIFIC: { HashMap projectionMap = new HashMap<>(); @@ -835,6 +905,7 @@ public class KeychainProvider extends ContentProvider { int count; final int match = mUriMatcher.match(uri); + ContentResolver contentResolver = getContext().getContentResolver(); switch (match) { // dangerous case KEY_RINGS_UNIFIED: { @@ -849,7 +920,7 @@ public class KeychainProvider extends ContentProvider { } // corresponding keys and userIds are deleted by ON DELETE CASCADE count = db.delete(Tables.KEY_RINGS_PUBLIC, selection, selectionArgs); - uri = KeyRings.buildGenericKeyRingUri(uri.getPathSegments().get(1)); + contentResolver.notifyChange(KeyRings.buildGenericKeyRingUri(uri.getPathSegments().get(1)), null); break; } case KEY_RING_SECRET: { @@ -859,10 +930,43 @@ public class KeychainProvider extends ContentProvider { selection += " AND (" + additionalSelection + ")"; } count = db.delete(Tables.KEY_RINGS_SECRET, selection, selectionArgs); - uri = KeyRings.buildGenericKeyRingUri(uri.getPathSegments().get(1)); + contentResolver.notifyChange(KeyRings.buildGenericKeyRingUri(uri.getPathSegments().get(1)), null); break; } + case AUTOCRYPT_PEERS_BY_PACKAGE_NAME_AND_TRUST_ID: { + String packageName = uri.getPathSegments().get(2); + String autocryptPeer = uri.getPathSegments().get(3); + + String selection = ApiAutocryptPeer.PACKAGE_NAME + " = ? AND " + ApiAutocryptPeer.IDENTIFIER + " = ?"; + selectionArgs = new String[] { packageName, autocryptPeer }; + + Cursor cursor = db.query(Tables.API_AUTOCRYPT_PEERS, new String[] { ApiAutocryptPeer.MASTER_KEY_ID }, + selection, selectionArgs, null, null, null); + Long masterKeyId = null; + if (cursor != null && cursor.moveToNext() && !cursor.isNull(0)) { + masterKeyId = cursor.getLong(0); + } + + count = db.delete(Tables.API_AUTOCRYPT_PEERS, selection, selectionArgs); + + if (masterKeyId != null) { + contentResolver.notifyChange(KeyRings.buildGenericKeyRingUri(masterKeyId), null); + } + contentResolver.notifyChange( + ApiAutocryptPeer.buildByPackageNameAndAutocryptId(packageName, autocryptPeer), null); + break; + } + + case AUTOCRYPT_PEERS_BY_MASTER_KEY_ID: + String selection = ApiAutocryptPeer.MASTER_KEY_ID + " = " + uri.getLastPathSegment(); + if (!TextUtils.isEmpty(additionalSelection)) { + selection += " AND (" + additionalSelection + ")"; + } + count = db.delete(Tables.API_AUTOCRYPT_PEERS, selection, selectionArgs); + contentResolver.notifyChange(KeyRings.buildGenericKeyRingUri(uri.getLastPathSegment()), null); + break; + case API_APPS_BY_PACKAGE_NAME: { count = db.delete(Tables.API_APPS, buildDefaultApiAppsSelection(uri, additionalSelection), selectionArgs); @@ -878,9 +982,6 @@ public class KeychainProvider extends ContentProvider { } } - // notify of changes in db - getContext().getContentResolver().notifyChange(uri, null); - return count; } @@ -892,6 +993,7 @@ public class KeychainProvider extends ContentProvider { Log.v(Constants.TAG, "update(uri=" + uri + ", values=" + values.toString() + ")"); final SQLiteDatabase db = getDb().getWritableDatabase(); + ContentResolver contentResolver = getContext().getContentResolver(); int count = 0; try { @@ -929,13 +1031,48 @@ public class KeychainProvider extends ContentProvider { db.update(Tables.UPDATED_KEYS, values, null, null); break; } + case AUTOCRYPT_PEERS_BY_PACKAGE_NAME_AND_TRUST_ID: { + Long masterKeyId = values.getAsLong(ApiAutocryptPeer.MASTER_KEY_ID); + if (masterKeyId == null) { + throw new IllegalArgumentException("master_key_id must be a non-null value!"); + } + + ContentValues actualValues = new ContentValues(); + String packageName = uri.getPathSegments().get(2); + actualValues.put(ApiAutocryptPeer.PACKAGE_NAME, packageName); + actualValues.put(ApiAutocryptPeer.IDENTIFIER, uri.getLastPathSegment()); + actualValues.put(ApiAutocryptPeer.MASTER_KEY_ID, masterKeyId); + + Long newLastSeen = values.getAsLong(ApiAutocryptPeer.LAST_SEEN); + if (newLastSeen != null) { + actualValues.put(ApiAutocryptPeer.LAST_SEEN, newLastSeen); + } + + if (values.containsKey(ApiAutocryptPeer.LAST_SEEN_KEY)) { + actualValues.put(ApiAutocryptPeer.LAST_SEEN_KEY, + values.getAsLong(ApiAutocryptPeer.LAST_SEEN_KEY)); + } + if (values.containsKey(ApiAutocryptPeer.STATE)) { + actualValues.put(ApiAutocryptPeer.STATE, + values.getAsLong(ApiAutocryptPeer.STATE)); + } + + contentResolver.notifyChange(KeyRings.buildGenericKeyRingUri(masterKeyId), null); + + try { + db.replace(Tables.API_AUTOCRYPT_PEERS, null, actualValues); + } finally { + db.close(); + } + break; + } default: { throw new UnsupportedOperationException("Unknown uri: " + uri); } } // notify of changes in db - getContext().getContentResolver().notifyChange(uri, null); + contentResolver.notifyChange(uri, null); } catch (SQLiteConstraintException e) { Log.d(Constants.TAG, "Constraint exception on update! Entry already existing?", e); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPendingIntentFactory.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPendingIntentFactory.java index 18b1c0de2..ff5351b2b 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPendingIntentFactory.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPendingIntentFactory.java @@ -28,6 +28,7 @@ import android.os.Build; import org.sufficientlysecure.keychain.pgp.DecryptVerifySecurityProblem; import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.remote.ui.RemoteBackupActivity; +import org.sufficientlysecure.keychain.remote.ui.dialog.RemoteDeduplicateActivity; import org.sufficientlysecure.keychain.remote.ui.RemoteErrorActivity; import org.sufficientlysecure.keychain.remote.ui.RemoteImportKeysActivity; import org.sufficientlysecure.keychain.remote.ui.RemotePassphraseDialogActivity; @@ -97,6 +98,16 @@ public class ApiPendingIntentFactory { return createInternal(data, intent); } + PendingIntent createDeduplicatePendingIntent(String packageName, Intent data, ArrayList duplicateEmails) { + Intent intent = new Intent(mContext, RemoteDeduplicateActivity.class); + + intent.putExtra(RemoteDeduplicateActivity.EXTRA_PACKAGE_NAME, packageName); + intent.putExtra(RemoteDeduplicateActivity.EXTRA_DUPLICATE_EMAILS, duplicateEmails); + + return createInternal(data, intent); + } + + PendingIntent createImportFromKeyserverPendingIntent(Intent data, long masterKeyId) { Intent intent = new Intent(mContext, RemoteImportKeysActivity.class); intent.setAction(RemoteImportKeysActivity.ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN_RESULT); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/KeychainExternalProvider.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/KeychainExternalProvider.java index 29d874f9a..785ba4ace 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/KeychainExternalProvider.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/KeychainExternalProvider.java @@ -17,8 +17,10 @@ package org.sufficientlysecure.keychain.remote; + import java.security.AccessControlException; import java.util.Arrays; +import java.util.Date; import java.util.HashMap; import java.util.List; @@ -27,6 +29,7 @@ import android.content.ContentValues; import android.content.Context; import android.content.UriMatcher; import android.database.Cursor; +import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteQueryBuilder; import android.net.Uri; @@ -39,19 +42,25 @@ import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.provider.ApiDataAccessObject; import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps; +import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAutocryptPeer; import org.sufficientlysecure.keychain.provider.KeychainContract.Certs; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.provider.KeychainContract.Keys; import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets; import org.sufficientlysecure.keychain.provider.KeychainDatabase; import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables; import org.sufficientlysecure.keychain.provider.KeychainExternalContract; +import org.sufficientlysecure.keychain.provider.KeychainExternalContract.AutocryptStatus; import org.sufficientlysecure.keychain.provider.KeychainExternalContract.EmailStatus; import org.sufficientlysecure.keychain.provider.SimpleContentResolverInterface; import org.sufficientlysecure.keychain.util.Log; public class KeychainExternalProvider extends ContentProvider implements SimpleContentResolverInterface { private static final int EMAIL_STATUS = 101; - private static final int EMAIL_STATUS_INTERNAL = 102; + + private static final int AUTOCRYPT_STATUS = 201; + private static final int AUTOCRYPT_STATUS_INTERNAL = 202; + private static final int API_APPS = 301; private static final int API_APPS_BY_PACKAGE_NAME = 302; @@ -72,7 +81,7 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC String authority = KeychainExternalContract.CONTENT_AUTHORITY_EXTERNAL; - /** + /* * list email_status * *
@@ -80,7 +89,9 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC
          * 
*/ matcher.addURI(authority, KeychainExternalContract.BASE_EMAIL_STATUS, EMAIL_STATUS); - matcher.addURI(authority, KeychainExternalContract.BASE_EMAIL_STATUS + "/*", EMAIL_STATUS_INTERNAL); + + matcher.addURI(authority, KeychainExternalContract.BASE_AUTOCRYPT_STATUS, AUTOCRYPT_STATUS); + matcher.addURI(authority, KeychainExternalContract.BASE_AUTOCRYPT_STATUS + "/*", AUTOCRYPT_STATUS_INTERNAL); // can only query status of calling app - for internal use only! matcher.addURI(KeychainContract.CONTENT_AUTHORITY, KeychainContract.BASE_API_APPS + "/*", API_APPS_BY_PACKAGE_NAME); @@ -140,16 +151,8 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC String callingPackageName = mApiPermissionHelper.getCurrentCallingPackage(); switch (match) { - case EMAIL_STATUS_INTERNAL: - if (!BuildConfig.APPLICATION_ID.equals(callingPackageName)) { - throw new AccessControlException("This URI can only be called internally!"); - } - - // override package name to use any external - // callingPackageName = uri.getLastPathSegment(); - case EMAIL_STATUS: { - boolean callerIsAllowed = (match == EMAIL_STATUS_INTERNAL) || mApiPermissionHelper.isAllowedIgnoreErrors(); + boolean callerIsAllowed = mApiPermissionHelper.isAllowedIgnoreErrors(); if (!callerIsAllowed) { throw new AccessControlException("An application must register before use of KeychainExternalProvider!"); } @@ -215,6 +218,105 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC break; } + case AUTOCRYPT_STATUS_INTERNAL: + if (!BuildConfig.APPLICATION_ID.equals(callingPackageName)) { + throw new AccessControlException("This URI can only be called internally!"); + } + + // override package name to use any external + callingPackageName = uri.getLastPathSegment(); + + case AUTOCRYPT_STATUS: { + boolean callerIsAllowed = (match == AUTOCRYPT_STATUS_INTERNAL) || mApiPermissionHelper.isAllowedIgnoreErrors(); + if (!callerIsAllowed) { + throw new AccessControlException("An application must register before use of KeychainExternalProvider!"); + } + + db.execSQL("CREATE TEMPORARY TABLE " + TEMP_TABLE_QUERIED_ADDRESSES + " (" + TEMP_TABLE_COLUMN_ADDRES + " TEXT);"); + ContentValues cv = new ContentValues(); + for (String address : selectionArgs) { + cv.put(TEMP_TABLE_COLUMN_ADDRES, address); + db.insert(TEMP_TABLE_QUERIED_ADDRESSES, null, cv); + } + + HashMap projectionMap = new HashMap<>(); + projectionMap.put(AutocryptStatus._ID, "email AS _id"); + projectionMap.put(AutocryptStatus.ADDRESS, // this is actually the queried address + TEMP_TABLE_QUERIED_ADDRESSES + "." + TEMP_TABLE_COLUMN_ADDRES + " AS " + AutocryptStatus.ADDRESS); + + projectionMap.put(AutocryptStatus.UID_ADDRESS, + Tables.USER_PACKETS + "." + UserPackets.USER_ID + " AS " + AutocryptStatus.UID_ADDRESS); + // we take the minimum (>0) here, where "1" is "verified by known secret key", "2" is "self-certified" + projectionMap.put(AutocryptStatus.UID_KEY_STATUS, "CASE ( MIN (certs_user_id." + Certs.VERIFIED + " ) ) " + // remap to keep this provider contract independent from our internal representation + + " WHEN " + Certs.VERIFIED_SELF + " THEN " + KeychainExternalContract.KEY_STATUS_UNVERIFIED + + " WHEN " + Certs.VERIFIED_SECRET + " THEN " + KeychainExternalContract.KEY_STATUS_VERIFIED + + " WHEN NULL THEN NULL" + + " END AS " + AutocryptStatus.UID_KEY_STATUS); + projectionMap.put(AutocryptStatus.UID_MASTER_KEY_ID, + Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID + " AS " + AutocryptStatus.UID_MASTER_KEY_ID); + projectionMap.put(AutocryptStatus.UID_CANDIDATES, + "COUNT(DISTINCT " + Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID + + ") AS " + AutocryptStatus.UID_CANDIDATES); + + projectionMap.put(AutocryptStatus.AUTOCRYPT_KEY_STATUS, "CASE ( MIN (certs_autocrypt_peer." + Certs.VERIFIED + " ) ) " + // remap to keep this provider contract independent from our internal representation + + " WHEN " + Certs.VERIFIED_SELF + " THEN " + KeychainExternalContract.KEY_STATUS_UNVERIFIED + + " WHEN " + Certs.VERIFIED_SECRET + " THEN " + KeychainExternalContract.KEY_STATUS_VERIFIED + + " WHEN NULL THEN NULL" + + " END AS " + AutocryptStatus.AUTOCRYPT_KEY_STATUS); + projectionMap.put(AutocryptStatus.AUTOCRYPT_MASTER_KEY_ID, + Tables.API_AUTOCRYPT_PEERS + "." + ApiAutocryptPeer.MASTER_KEY_ID + " AS " + AutocryptStatus.AUTOCRYPT_MASTER_KEY_ID); + projectionMap.put(AutocryptStatus.AUTOCRYPT_PEER_STATE, Tables.API_AUTOCRYPT_PEERS + "." + + ApiAutocryptPeer.STATE + " AS " + AutocryptStatus.AUTOCRYPT_LAST_SEEN_KEY); + projectionMap.put(AutocryptStatus.AUTOCRYPT_LAST_SEEN, Tables.API_AUTOCRYPT_PEERS + "." + + ApiAutocryptPeer.LAST_SEEN + " AS " + AutocryptStatus.AUTOCRYPT_LAST_SEEN); + projectionMap.put(AutocryptStatus.AUTOCRYPT_LAST_SEEN_KEY, Tables.API_AUTOCRYPT_PEERS + "." + + ApiAutocryptPeer.LAST_SEEN_KEY + " AS " + AutocryptStatus.AUTOCRYPT_LAST_SEEN_KEY); + qb.setProjectionMap(projectionMap); + + if (projection == null) { + throw new IllegalArgumentException("Please provide a projection!"); + } + + qb.setTables( + TEMP_TABLE_QUERIED_ADDRESSES + + " LEFT JOIN " + Tables.USER_PACKETS + " ON (" + + Tables.USER_PACKETS + "." + UserPackets.USER_ID + " IS NOT NULL" + + " AND " + Tables.USER_PACKETS + "." + UserPackets.EMAIL + " LIKE " + TEMP_TABLE_QUERIED_ADDRESSES + "." + TEMP_TABLE_COLUMN_ADDRES + + ")" + + " LEFT JOIN " + Tables.CERTS + " AS certs_user_id ON (" + + Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID + " = certs_user_id." + Certs.MASTER_KEY_ID + + " AND " + Tables.USER_PACKETS + "." + UserPackets.RANK + " = certs_user_id." + Certs.RANK + + ")" + + " LEFT JOIN " + Tables.API_AUTOCRYPT_PEERS + " ON (" + + Tables.API_AUTOCRYPT_PEERS + "." + ApiAutocryptPeer.IDENTIFIER + " LIKE queried_addresses.address" + + " AND " + Tables.API_AUTOCRYPT_PEERS + "." + ApiAutocryptPeer.PACKAGE_NAME + " = \"" + callingPackageName + "\"" + + ")" + + " LEFT JOIN " + Tables.CERTS + " AS certs_autocrypt_peer ON (" + + Tables.API_AUTOCRYPT_PEERS + "." + ApiAutocryptPeer.MASTER_KEY_ID + " = certs_autocrypt_peer." + Certs.MASTER_KEY_ID + + ")" + ); + // in case there are multiple verifying certificates + groupBy = TEMP_TABLE_QUERIED_ADDRESSES + "." + TEMP_TABLE_COLUMN_ADDRES; + + // can't have an expired master key for the uid candidate + qb.appendWhere("(EXISTS (SELECT * FROM " + Tables.KEYS + " WHERE " + + Tables.KEYS + "." + Keys.KEY_ID + " = " + Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID + + " AND " + Tables.KEYS + "." + Keys.IS_REVOKED + " = 0" + + " AND NOT " + "(" + Tables.KEYS + "." + Keys.EXPIRY + " IS NOT NULL AND " + Tables.KEYS + "." + Keys.EXPIRY + + " < " + new Date().getTime() / 1000 + ")" + + ")) OR " + Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID + " IS NULL"); + + if (TextUtils.isEmpty(sortOrder)) { + sortOrder = AutocryptStatus.ADDRESS; + } + + // uri to watch is all /key_rings/ + uri = KeyRings.CONTENT_URI; + break; + } + case API_APPS_BY_PACKAGE_NAME: { String requestedPackageName = uri.getLastPathSegment(); checkIfPackageBelongsToCaller(getContext(), requestedPackageName); @@ -244,6 +346,9 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC if (cursor != null) { // Tell the cursor what uri to watch, so it knows when its source data changes cursor.setNotificationUri(getContext().getContentResolver(), uri); + if (Constants.DEBUG_LOG_DB_QUERIES) { + DatabaseUtils.dumpCursor(cursor); + } } Log.d(Constants.TAG, @@ -277,7 +382,7 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC } @Override - public int delete(@NonNull Uri uri, String additionalSelection, String[] selectionArgs) { + public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) { throw new UnsupportedOperationException(); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java index 42d0b1490..7fa062915 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java @@ -41,11 +41,14 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.openintents.openpgp.AutocryptPeerUpdate; +import org.openintents.openpgp.AutocryptPeerUpdate.PreferEncrypt; import org.openintents.openpgp.IOpenPgpService; import org.openintents.openpgp.OpenPgpDecryptionResult; import org.openintents.openpgp.OpenPgpError; import org.openintents.openpgp.OpenPgpMetadata; import org.openintents.openpgp.OpenPgpSignatureResult; +import org.openintents.openpgp.OpenPgpSignatureResult.AutocryptPeerResult; import org.openintents.openpgp.util.OpenPgpApi; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.operations.BackupOperation; @@ -62,12 +65,17 @@ import org.sufficientlysecure.keychain.pgp.PgpSignEncryptData; import org.sufficientlysecure.keychain.pgp.PgpSignEncryptOperation; import org.sufficientlysecure.keychain.pgp.Progressable; import org.sufficientlysecure.keychain.pgp.SecurityProblem; +import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; +import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; import org.sufficientlysecure.keychain.provider.ApiDataAccessObject; +import org.sufficientlysecure.keychain.provider.AutocryptPeerDataAccessObject; import org.sufficientlysecure.keychain.provider.KeyRepository; +import org.sufficientlysecure.keychain.provider.KeyWritableRepository; import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.OverriddenWarningsRepository; +import org.sufficientlysecure.keychain.remote.OpenPgpServiceKeyIdExtractor.AutocryptState; import org.sufficientlysecure.keychain.remote.OpenPgpServiceKeyIdExtractor.KeyIdResult; import org.sufficientlysecure.keychain.remote.OpenPgpServiceKeyIdExtractor.KeyIdResultStatus; import org.sufficientlysecure.keychain.service.BackupKeyringParcel; @@ -82,9 +90,10 @@ public class OpenPgpService extends Service { public static final int API_VERSION_WITHOUT_SIGNATURE_ONLY_FLAG = 8; public static final int API_VERSION_WITH_DECRYPTION_RESULT = 8; public static final int API_VERSION_WITH_RESULT_NO_SIGNATURE = 8; + public static final int API_VERSION_WITH_AUTOCRYPT = 12; public static final List SUPPORTED_VERSIONS = - Collections.unmodifiableList(Arrays.asList(7, 8, 9, 10, 11)); + Collections.unmodifiableList(Arrays.asList(7, 8, 9, 10, 11, 12)); private ApiPermissionHelper mApiPermissionHelper; private KeyRepository mKeyRepository; @@ -95,10 +104,9 @@ public class OpenPgpService extends Service { @Override public void onCreate() { super.onCreate(); - mApiPermissionHelper = new ApiPermissionHelper(this, new ApiDataAccessObject(this)); mKeyRepository = KeyRepository.createDatabaseInteractor(this); mApiDao = new ApiDataAccessObject(this); - + mApiPermissionHelper = new ApiPermissionHelper(this, mApiDao); mApiPendingIntentFactory = new ApiPendingIntentFactory(getBaseContext()); mKeyIdExtractor = OpenPgpServiceKeyIdExtractor.getInstance(getContentResolver(), mApiPendingIntentFactory); } @@ -192,7 +200,7 @@ public class OpenPgpService extends Service { } private Intent encryptAndSignImpl(Intent data, InputStream inputStream, - OutputStream outputStream, boolean sign) { + OutputStream outputStream, boolean sign, boolean isQueryAutocryptStatus) { try { boolean asciiArmor = data.getBooleanExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); String originalFilename = data.getStringExtra(OpenPgpApi.EXTRA_ORIGINAL_FILENAME); @@ -231,11 +239,10 @@ public class OpenPgpService extends Service { KeyIdResult keyIdResult = mKeyIdExtractor.returnKeyIdsFromIntent(data, false, mApiPermissionHelper.getCurrentCallingPackage()); - boolean isDryRun = data.getBooleanExtra(OpenPgpApi.EXTRA_DRY_RUN, false); boolean isOpportunistic = data.getBooleanExtra(OpenPgpApi.EXTRA_OPPORTUNISTIC_ENCRYPTION, false); KeyIdResultStatus keyIdResultStatus = keyIdResult.getStatus(); - if (isDryRun) { - return getDryRunStatusResult(keyIdResult); + if (isQueryAutocryptStatus) { + return getAutocryptStatusResult(keyIdResult); } if (keyIdResult.hasKeySelectionPendingIntent()) { @@ -296,39 +303,56 @@ public class OpenPgpService extends Service { } @NonNull - private Intent getDryRunStatusResult(KeyIdResult keyIdResult) { - switch (keyIdResult.getStatus()) { - case MISSING: { - Intent result = new Intent(); - result.putExtra(OpenPgpApi.RESULT_ERROR, - new OpenPgpError(OpenPgpError.OPPORTUNISTIC_MISSING_KEYS, "missing keys in opportunistic mode")); - result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR); - return result; + private Intent getAutocryptStatusResult(KeyIdResult keyIdResult) { + Intent result = new Intent(); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS); + + AutocryptState combinedAutocryptState = keyIdResult.getCombinedAutocryptState(); + if (combinedAutocryptState == null) { + switch (keyIdResult.getStatus()) { + case NO_KEYS: + case NO_KEYS_ERROR: + case MISSING: { + result.putExtra(OpenPgpApi.RESULT_AUTOCRYPT_STATUS, OpenPgpApi.AUTOCRYPT_STATUS_UNAVAILABLE); + break; + } + case DUPLICATE: { + if (keyIdResult.hasKeySelectionPendingIntent()) { + result.putExtra(OpenPgpApi.RESULT_INTENT, keyIdResult.getKeySelectionPendingIntent()); + } + result.putExtra(OpenPgpApi.RESULT_AUTOCRYPT_STATUS, OpenPgpApi.AUTOCRYPT_STATUS_DISCOURAGE); + break; + } + case OK: { + result.putExtra(OpenPgpApi.RESULT_AUTOCRYPT_STATUS, OpenPgpApi.AUTOCRYPT_STATUS_DISCOURAGE); + break; + } } - case NO_KEYS: - case NO_KEYS_ERROR: { - Intent result = new Intent(); - result.putExtra(OpenPgpApi.RESULT_ERROR, - new OpenPgpError(OpenPgpError.NO_USER_IDS, "empty recipient list")); - result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR); - return result; + return result; + } + + switch (combinedAutocryptState) { + case EXTERNAL: + case SELECTED: + case GOSSIP: + case RESET: { + result.putExtra(OpenPgpApi.RESULT_AUTOCRYPT_STATUS, OpenPgpApi.AUTOCRYPT_STATUS_DISCOURAGE); + break; } - case DUPLICATE: { - Intent result = new Intent(); - result.putExtra(OpenPgpApi.RESULT_KEYS_CONFIRMED, false); - result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS); - return result; + case AVAILABLE: { + result.putExtra(OpenPgpApi.RESULT_AUTOCRYPT_STATUS, OpenPgpApi.AUTOCRYPT_STATUS_AVAILABLE); + break; } - case OK: { - Intent result = new Intent(); - result.putExtra(OpenPgpApi.RESULT_KEYS_CONFIRMED, keyIdResult.isAllKeysConfirmed()); - result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS); - return result; + case MUTUAL: { + result.putExtra(OpenPgpApi.RESULT_AUTOCRYPT_STATUS, OpenPgpApi.AUTOCRYPT_STATUS_MUTUAL); + break; } default: { throw new IllegalStateException("unhandled case!"); } } + + return result; } private Intent decryptAndVerifyImpl(Intent data, InputStream inputStream, OutputStream outputStream, @@ -361,6 +385,10 @@ public class OpenPgpService extends Service { byte[] detachedSignature = data.getByteArrayExtra(OpenPgpApi.EXTRA_DETACHED_SIGNATURE); String senderAddress = data.getStringExtra(OpenPgpApi.EXTRA_SENDER_ADDRESS); + AutocryptPeerDataAccessObject autocryptPeerentityDao = new AutocryptPeerDataAccessObject( + getBaseContext(), mApiPermissionHelper.getCurrentCallingPackage()); + updateAutocryptPeerStateFromIntent(data, autocryptPeerentityDao); + PgpDecryptVerifyOperation op = new PgpDecryptVerifyOperation(this, mKeyRepository, progressable); long inputLength = data.getLongExtra(OpenPgpApi.EXTRA_DATA_LENGTH, InputData.UNKNOWN_FILESIZE); @@ -436,7 +464,7 @@ public class OpenPgpService extends Service { if (prioritySecurityProblem.isIdentifiable()) { String identifier = prioritySecurityProblem.getIdentifier(); boolean isOverridden = OverriddenWarningsRepository.createOverriddenWarningsRepository(this) - .isWarningOverridden(identifier); + .isWarningOverridden(identifier); result.putExtra(OpenPgpApi.RESULT_OVERRIDE_CRYPTO_WARNING, isOverridden); } } @@ -446,6 +474,53 @@ public class OpenPgpService extends Service { mApiPendingIntentFactory.createSecurityProblemIntent(packageName, securityProblem, supportOverride)); } + private String updateAutocryptPeerStateFromIntent(Intent data, AutocryptPeerDataAccessObject autocryptPeerDao) + throws PgpGeneralException, IOException { + String autocryptPeerId = data.getStringExtra(OpenPgpApi.EXTRA_AUTOCRYPT_PEER_ID); + AutocryptPeerUpdate autocryptPeerUpdate = data.getParcelableExtra(OpenPgpApi.EXTRA_AUTOCRYPT_PEER_UPDATE); + if (autocryptPeerUpdate == null) { + return null; + } + + Long newMasterKeyId; + if (autocryptPeerUpdate.hasKeyData()) { + UncachedKeyRing uncachedKeyRing = UncachedKeyRing.decodeFromData(autocryptPeerUpdate.getKeyData()); + if (uncachedKeyRing.isSecret()) { + Log.e(Constants.TAG, "Found secret key in autocrypt id! - Ignoring"); + return null; + } + // this will merge if the key already exists - no worries! + KeyWritableRepository.createDatabaseReadWriteInteractor(this).savePublicKeyRing(uncachedKeyRing); + newMasterKeyId = uncachedKeyRing.getMasterKeyId(); + } else { + newMasterKeyId = null; + } + + Date lastSeen = autocryptPeerDao.getLastSeen(autocryptPeerId); + Date effectiveDate = autocryptPeerUpdate.getEffectiveDate(); + if (newMasterKeyId == null) { + if (effectiveDate.after(lastSeen)) { + autocryptPeerDao.updateToResetState(autocryptPeerId, effectiveDate); + } + return autocryptPeerId; + } + + Date lastSeenKey = autocryptPeerDao.getLastSeenKey(autocryptPeerId); + if (lastSeenKey != null && effectiveDate.before(lastSeenKey)) { + return autocryptPeerId; + } + + if (lastSeen == null || effectiveDate.after(lastSeen)) { + if (autocryptPeerUpdate.getPreferEncrypt() == PreferEncrypt.MUTUAL) { + autocryptPeerDao.updateToMutualState(autocryptPeerId, effectiveDate, newMasterKeyId); + } else { + autocryptPeerDao.updateToAvailableState(autocryptPeerId, effectiveDate, newMasterKeyId); + } + } + + return autocryptPeerId; + } + private void processDecryptionResultForResultIntent(int targetApiVersion, Intent result, OpenPgpDecryptionResult decryptionResult) { if (targetApiVersion < API_VERSION_WITH_DECRYPTION_RESULT) { @@ -528,9 +603,41 @@ public class OpenPgpService extends Service { } } + String autocryptPeerentity = data.getStringExtra(OpenPgpApi.EXTRA_AUTOCRYPT_PEER_ID); + if (autocryptPeerentity != null) { + if (targetApiVersion < API_VERSION_WITH_AUTOCRYPT) { + throw new IllegalStateException("API version conflict, autocrypt is supported v12 and up!"); + } + signatureResult = processAutocryptPeerInfoToSignatureResult(signatureResult, autocryptPeerentity); + } + result.putExtra(OpenPgpApi.RESULT_SIGNATURE, signatureResult); } + private OpenPgpSignatureResult processAutocryptPeerInfoToSignatureResult(OpenPgpSignatureResult signatureResult, + String autocryptPeerentity) { + boolean hasValidSignature = + signatureResult.getResult() == OpenPgpSignatureResult.RESULT_VALID_KEY_CONFIRMED || + signatureResult.getResult() == OpenPgpSignatureResult.RESULT_VALID_KEY_UNCONFIRMED; + if (!hasValidSignature) { + return signatureResult; + } + + AutocryptPeerDataAccessObject autocryptPeerentityDao = new AutocryptPeerDataAccessObject(getBaseContext(), + mApiPermissionHelper.getCurrentCallingPackage()); + Long autocryptPeerMasterKeyId = autocryptPeerentityDao.getMasterKeyIdForAutocryptPeer(autocryptPeerentity); + + long masterKeyId = signatureResult.getKeyId(); + if (autocryptPeerMasterKeyId == null) { + autocryptPeerentityDao.updateToGossipState(autocryptPeerentity, new Date(), masterKeyId); + return signatureResult.withAutocryptPeerResult(AutocryptPeerResult.NEW); + } else if (masterKeyId == autocryptPeerMasterKeyId) { + return signatureResult.withAutocryptPeerResult(AutocryptPeerResult.OK); + } else { + return signatureResult.withAutocryptPeerResult(AutocryptPeerResult.MISMATCH); + } + } + private Intent getKeyImpl(Intent data, OutputStream outputStream) { try { long masterKeyId = data.getLongExtra(OpenPgpApi.EXTRA_KEY_ID, 0); @@ -544,6 +651,11 @@ public class OpenPgpService extends Service { Intent result = new Intent(); result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS); + if (data.getBooleanExtra(OpenPgpApi.EXTRA_MINIMIZE, false)) { + String userIdToKeep = data.getStringExtra(OpenPgpApi.EXTRA_MINIMIZE_USER_ID); + keyRing = keyRing.minimize(userIdToKeep); + } + boolean requestedKeyData = outputStream != null; if (requestedKeyData) { boolean requestAsciiArmor = data.getBooleanExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, false); @@ -669,6 +781,27 @@ public class OpenPgpService extends Service { } } + private Intent updateAutocryptPeerImpl(Intent data) { + try { + if (!data.hasExtra(OpenPgpApi.EXTRA_AUTOCRYPT_PEER_ID) || + !data.hasExtra(OpenPgpApi.EXTRA_AUTOCRYPT_PEER_UPDATE)) { + throw new IllegalArgumentException("need to specify both autocrypt_peer_id and autocrypt_peer_update!"); + } + + AutocryptPeerDataAccessObject autocryptPeerentityDao = new AutocryptPeerDataAccessObject(getBaseContext(), + mApiPermissionHelper.getCurrentCallingPackage()); + + updateAutocryptPeerStateFromIntent(data, autocryptPeerentityDao); + + Intent result = new Intent(); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS); + return result; + } catch (Exception e) { + Log.d(Constants.TAG, "exception in updateAutocryptPeerImpl", e); + return createErrorResultIntent(OpenPgpError.GENERIC_ERROR, e.getMessage()); + } + } + private Intent checkPermissionImpl(@NonNull Intent data) { Intent permissionIntent = mApiPermissionHelper.isAllowedOrReturnIntent(data); if (permissionIntent != null) { @@ -816,11 +949,13 @@ public class OpenPgpService extends Service { case OpenPgpApi.ACTION_DETACHED_SIGN: { return signImpl(data, inputStream, outputStream, false); } - case OpenPgpApi.ACTION_ENCRYPT: { - return encryptAndSignImpl(data, inputStream, outputStream, false); + case OpenPgpApi.ACTION_QUERY_AUTOCRYPT_STATUS: { + return encryptAndSignImpl(data, inputStream, outputStream, false, true); } + case OpenPgpApi.ACTION_ENCRYPT: case OpenPgpApi.ACTION_SIGN_AND_ENCRYPT: { - return encryptAndSignImpl(data, inputStream, outputStream, true); + boolean enableSign = action.equals(OpenPgpApi.ACTION_SIGN_AND_ENCRYPT); + return encryptAndSignImpl(data, inputStream, outputStream, enableSign, false); } case OpenPgpApi.ACTION_DECRYPT_VERIFY: { return decryptAndVerifyImpl(data, inputStream, outputStream, false, progressable); @@ -840,6 +975,9 @@ public class OpenPgpService extends Service { case OpenPgpApi.ACTION_BACKUP: { return backupImpl(data, outputStream); } + case OpenPgpApi.ACTION_UPDATE_AUTOCRYPT_PEER: { + return updateAutocryptPeerImpl(data); + } default: { return null; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpServiceKeyIdExtractor.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpServiceKeyIdExtractor.java index db0e5e03b..92eba893d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpServiceKeyIdExtractor.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpServiceKeyIdExtractor.java @@ -4,7 +4,6 @@ package org.sufficientlysecure.keychain.remote; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; -import java.util.Map.Entry; import android.app.PendingIntent; import android.content.ContentResolver; @@ -16,22 +15,31 @@ import android.support.annotation.VisibleForTesting; import org.openintents.openpgp.util.OpenPgpApi; import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAutocryptPeer; import org.sufficientlysecure.keychain.provider.KeychainExternalContract; -import org.sufficientlysecure.keychain.provider.KeychainExternalContract.EmailStatus; +import org.sufficientlysecure.keychain.provider.KeychainExternalContract.AutocryptStatus; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.util.Log; class OpenPgpServiceKeyIdExtractor { @VisibleForTesting - static final String[] PROJECTION_KEY_SEARCH = { - "email_address", - "master_key_id", - "email_status", + static final String[] PROJECTION_MAIL_STATUS = { + AutocryptStatus.ADDRESS, + AutocryptStatus.UID_MASTER_KEY_ID, + AutocryptStatus.UID_KEY_STATUS, + AutocryptStatus.UID_CANDIDATES, + AutocryptStatus.AUTOCRYPT_MASTER_KEY_ID, + AutocryptStatus.AUTOCRYPT_KEY_STATUS, + AutocryptStatus.AUTOCRYPT_PEER_STATE }; private static final int INDEX_EMAIL_ADDRESS = 0; private static final int INDEX_MASTER_KEY_ID = 1; - private static final int INDEX_EMAIL_STATUS = 2; + private static final int INDEX_USER_ID_STATUS = 2; + private static final int INDEX_USER_ID_CANDIDATES = 3; + private static final int INDEX_AUTOCRYPT_MASTER_KEY_ID = 4; + private static final int INDEX_AUTOCRYPT_KEY_STATUS = 5; + private static final int INDEX_AUTOCRYPT_PEER_STATE = 6; private final ApiPendingIntentFactory apiPendingIntentFactory; @@ -58,7 +66,7 @@ class OpenPgpServiceKeyIdExtractor { for (long keyId : data.getLongArrayExtra(OpenPgpApi.EXTRA_KEY_IDS_SELECTED)) { encryptKeyIds.add(keyId); } - result = createKeysOkResult(encryptKeyIds, false); + result = createKeysOkResult(encryptKeyIds, false, null); } else if (data.hasExtra(OpenPgpApi.EXTRA_USER_IDS) || askIfNoUserIdsProvided) { String[] userIds = data.getStringArrayExtra(OpenPgpApi.EXTRA_USER_IDS); result = returnKeyIdsFromEmails(data, userIds, callingPackageName); @@ -85,32 +93,54 @@ class OpenPgpServiceKeyIdExtractor { HashSet keyIds = new HashSet<>(); ArrayList missingEmails = new ArrayList<>(); ArrayList duplicateEmails = new ArrayList<>(); + AutocryptState combinedAutocryptState = null; if (hasAddresses) { - HashMap keyRows = getStatusMapForQueriedAddresses(encryptionAddresses, callingPackageName); + HashMap userIdEntries = getStatusMapForQueriedAddresses( + encryptionAddresses, callingPackageName); boolean anyKeyNotVerified = false; - for (Entry entry : keyRows.entrySet()) { - String queriedAddress = entry.getKey(); - UserIdStatus userIdStatus = entry.getValue(); + for (String queriedAddress : encryptionAddresses) { + AddressQueryResult addressQueryResult = userIdEntries.get(queriedAddress); + if (addressQueryResult == null) { + throw new IllegalStateException("No result for address - shouldn't happen!"); + } + + if (addressQueryResult.autocryptMasterKeyId != null) { + keyIds.add(addressQueryResult.autocryptMasterKeyId); + + if (addressQueryResult.autocryptKeyStatus != KeychainExternalContract.KEY_STATUS_VERIFIED) { + anyKeyNotVerified = true; + } + + if (combinedAutocryptState == null) { + combinedAutocryptState = addressQueryResult.autocryptState; + } else { + combinedAutocryptState = combinedAutocryptState.combineWith(addressQueryResult.autocryptState); + } - if (userIdStatus.masterKeyId == null) { - missingEmails.add(queriedAddress); continue; } - keyIds.add(userIdStatus.masterKeyId); + if (addressQueryResult.uidMasterKeyId != null) { + keyIds.add(addressQueryResult.uidMasterKeyId); + combinedAutocryptState = AutocryptState.EXTERNAL; - if (userIdStatus.hasDuplicate) { - duplicateEmails.add(queriedAddress); + if (addressQueryResult.uidHasMultipleCandidates) { + duplicateEmails.add(queriedAddress); + } + + if (addressQueryResult.uidKeyStatus != KeychainExternalContract.KEY_STATUS_VERIFIED) { + anyKeyNotVerified = true; + } + + continue; } - if (!userIdStatus.verified) { - anyKeyNotVerified = true; - } + missingEmails.add(queriedAddress); } - if (keyRows.size() != encryptionAddresses.length) { + if (userIdEntries.size() != encryptionAddresses.length) { Log.e(Constants.TAG, "Number of rows doesn't match number of retrieved rows! Probably a bug?"); } @@ -122,14 +152,14 @@ class OpenPgpServiceKeyIdExtractor { } if (!duplicateEmails.isEmpty()) { - return createDuplicateKeysResult(data, keyIds, missingEmails, duplicateEmails); + return createDuplicateKeysResult(data, callingPackageName, duplicateEmails); } if (keyIds.isEmpty()) { return createNoKeysResult(data, keyIds, missingEmails, duplicateEmails); } - return createKeysOkResult(keyIds, allKeysConfirmed); + return createKeysOkResult(keyIds, allKeysConfirmed, combinedAutocryptState); } /** This method queries the KeychainExternalProvider for all addresses given in encryptionUserIds. @@ -138,10 +168,10 @@ class OpenPgpServiceKeyIdExtractor { * verification status exist, the first one is returned and marked as having a duplicate. */ @NonNull - private HashMap getStatusMapForQueriedAddresses(String[] encryptionUserIds, String callingPackageName) { - HashMap keyRows = new HashMap<>(); - Uri queryUri = EmailStatus.CONTENT_URI.buildUpon().appendPath(callingPackageName).build(); - Cursor cursor = contentResolver.query(queryUri, PROJECTION_KEY_SEARCH, null, encryptionUserIds, null); + private HashMap getStatusMapForQueriedAddresses(String[] encryptionUserIds, String callingPackageName) { + HashMap keyRows = new HashMap<>(); + Uri queryUri = AutocryptStatus.CONTENT_URI.buildUpon().appendPath(callingPackageName).build(); + Cursor cursor = contentResolver.query(queryUri, PROJECTION_MAIL_STATUS, null, encryptionUserIds, null); if (cursor == null) { throw new IllegalStateException("Internal error, received null cursor!"); } @@ -149,24 +179,21 @@ class OpenPgpServiceKeyIdExtractor { try { while (cursor.moveToNext()) { String queryAddress = cursor.getString(INDEX_EMAIL_ADDRESS); - Long masterKeyId = cursor.isNull(INDEX_MASTER_KEY_ID) ? null : cursor.getLong(INDEX_MASTER_KEY_ID); - boolean isVerified = cursor.getInt(INDEX_EMAIL_STATUS) == KeychainExternalContract.KEY_STATUS_VERIFIED; - UserIdStatus userIdStatus = new UserIdStatus(masterKeyId, isVerified); + Long uidMasterKeyId = + cursor.isNull(INDEX_MASTER_KEY_ID) ? null : cursor.getLong(INDEX_MASTER_KEY_ID); + int uidKeyStatus = cursor.getInt(INDEX_USER_ID_STATUS); + boolean uidHasMultipleCandidates = cursor.getInt(INDEX_USER_ID_CANDIDATES) > 1; - boolean seenBefore = keyRows.containsKey(queryAddress); - if (!seenBefore) { - keyRows.put(queryAddress, userIdStatus); - continue; - } + Long autocryptMasterKeyId = + cursor.isNull(INDEX_AUTOCRYPT_MASTER_KEY_ID) ? null : cursor.getLong(INDEX_AUTOCRYPT_MASTER_KEY_ID); + int autocryptKeyStatus = cursor.getInt(INDEX_AUTOCRYPT_KEY_STATUS); + int autocryptPeerStatus = cursor.getInt(INDEX_AUTOCRYPT_PEER_STATE); - UserIdStatus previousUserIdStatus = keyRows.get(queryAddress); - if (previousUserIdStatus.masterKeyId == null) { - keyRows.put(queryAddress, userIdStatus); - } else if (!previousUserIdStatus.verified && userIdStatus.verified) { - keyRows.put(queryAddress, userIdStatus); - } else if (previousUserIdStatus.verified == userIdStatus.verified) { - previousUserIdStatus.hasDuplicate = true; - } + AddressQueryResult status = new AddressQueryResult( + uidMasterKeyId, uidKeyStatus, uidHasMultipleCandidates, autocryptMasterKeyId, + autocryptKeyStatus, AutocryptState.fromDbValue(autocryptPeerStatus)); + + keyRows.put(queryAddress, status); } } finally { cursor.close(); @@ -174,14 +201,65 @@ class OpenPgpServiceKeyIdExtractor { return keyRows; } - private static class UserIdStatus { - private final Long masterKeyId; - private final boolean verified; - private boolean hasDuplicate; + private static class AddressQueryResult { + private final Long uidMasterKeyId; + private final int uidKeyStatus; + private boolean uidHasMultipleCandidates; + private final Long autocryptMasterKeyId; + private final int autocryptKeyStatus; + private final AutocryptState autocryptState; - UserIdStatus(Long masterKeyId, boolean verified) { - this.masterKeyId = masterKeyId; - this.verified = verified; + AddressQueryResult(Long uidMasterKeyId, int uidKeyStatus, boolean uidHasMultipleCandidates, Long autocryptMasterKeyId, + int autocryptKeyStatus, AutocryptState autocryptState) { + this.uidMasterKeyId = uidMasterKeyId; + this.uidKeyStatus = uidKeyStatus; + this.uidHasMultipleCandidates = uidHasMultipleCandidates; + this.autocryptMasterKeyId = autocryptMasterKeyId; + this.autocryptKeyStatus = autocryptKeyStatus; + this.autocryptState = autocryptState; + } + } + + enum AutocryptState { + EXTERNAL, RESET, GOSSIP, SELECTED, AVAILABLE, MUTUAL; + + static AutocryptState fromDbValue(int state) { + switch (state) { + case ApiAutocryptPeer.RESET: + return RESET; + case ApiAutocryptPeer.AVAILABLE: + return AVAILABLE; + case ApiAutocryptPeer.SELECTED: + return SELECTED; + case ApiAutocryptPeer.GOSSIP: + return GOSSIP; + case ApiAutocryptPeer.MUTUAL: + return MUTUAL; + default: + throw new IllegalStateException(); + } + } + + public AutocryptState combineWith(AutocryptState other) { + if (this == EXTERNAL || other == EXTERNAL) { + return EXTERNAL; + } + if (this == RESET || other == RESET) { + return RESET; + } + if (this == GOSSIP || other == GOSSIP) { + return GOSSIP; + } + if (this == SELECTED || other == SELECTED) { + return SELECTED; + } + if (this == AVAILABLE || other == AVAILABLE) { + return AVAILABLE; + } + if (this == MUTUAL && other == MUTUAL) { + return MUTUAL; + } + throw new IllegalStateException("Bug: autocrypt states can't be combined!"); } } @@ -191,6 +269,7 @@ class OpenPgpServiceKeyIdExtractor { private final HashSet mExplicitKeyIds; private final KeyIdResultStatus mStatus; private final boolean mAllKeysConfirmed; + private final AutocryptState mCombinedAutocryptState; private KeyIdResult(PendingIntent keySelectionPendingIntent, KeyIdResultStatus keyIdResultStatus) { mKeySelectionPendingIntent = keySelectionPendingIntent; @@ -198,13 +277,17 @@ class OpenPgpServiceKeyIdExtractor { mAllKeysConfirmed = false; mStatus = keyIdResultStatus; mExplicitKeyIds = null; + mCombinedAutocryptState = null; } - private KeyIdResult(HashSet keyIds, boolean allKeysConfirmed, KeyIdResultStatus keyIdResultStatus) { + + private KeyIdResult(HashSet keyIds, boolean allKeysConfirmed, KeyIdResultStatus keyIdResultStatus, + AutocryptState combinedAutocryptState) { mKeySelectionPendingIntent = null; mUserKeyIds = keyIds; mAllKeysConfirmed = allKeysConfirmed; mStatus = keyIdResultStatus; mExplicitKeyIds = null; + mCombinedAutocryptState = combinedAutocryptState; } private KeyIdResult(KeyIdResult keyIdResult, HashSet explicitKeyIds) { @@ -213,6 +296,7 @@ class OpenPgpServiceKeyIdExtractor { mAllKeysConfirmed = keyIdResult.mAllKeysConfirmed; mStatus = keyIdResult.mStatus; mExplicitKeyIds = explicitKeyIds; + mCombinedAutocryptState = keyIdResult.mCombinedAutocryptState; } boolean hasKeySelectionPendingIntent() { @@ -254,14 +338,19 @@ class OpenPgpServiceKeyIdExtractor { KeyIdResultStatus getStatus() { return mStatus; } + + public AutocryptState getCombinedAutocryptState() { + return mCombinedAutocryptState; + } } enum KeyIdResultStatus { OK, MISSING, DUPLICATE, NO_KEYS, NO_KEYS_ERROR } - private KeyIdResult createKeysOkResult(HashSet encryptKeyIds, boolean allKeysConfirmed) { - return new KeyIdResult(encryptKeyIds, allKeysConfirmed, KeyIdResultStatus.OK); + private KeyIdResult createKeysOkResult(HashSet encryptKeyIds, boolean allKeysConfirmed, + AutocryptState combinedAutocryptState) { + return new KeyIdResult(encryptKeyIds, allKeysConfirmed, KeyIdResultStatus.OK, combinedAutocryptState); } private KeyIdResult createNoKeysResult(Intent data, @@ -273,11 +362,9 @@ class OpenPgpServiceKeyIdExtractor { return new KeyIdResult(selectKeyPendingIntent, KeyIdResultStatus.NO_KEYS); } - private KeyIdResult createDuplicateKeysResult(Intent data, - HashSet selectedKeyIds, ArrayList missingEmails, ArrayList duplicateEmails) { - long[] keyIdsArray = KeyFormattingUtils.getUnboxedLongArray(selectedKeyIds); - PendingIntent selectKeyPendingIntent = apiPendingIntentFactory.createSelectPublicKeyPendingIntent( - data, keyIdsArray, missingEmails, duplicateEmails, false); + private KeyIdResult createDuplicateKeysResult(Intent data, String packageName, ArrayList duplicateEmails) { + PendingIntent selectKeyPendingIntent = apiPendingIntentFactory.createDeduplicatePendingIntent( + packageName, data, duplicateEmails); return new KeyIdResult(selectKeyPendingIntent, KeyIdResultStatus.DUPLICATE); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/KeyLoader.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/KeyLoader.java new file mode 100644 index 000000000..53055b40a --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/KeyLoader.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2017 Vincent Breitmoser + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.remote.ui.dialog; + + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.support.annotation.Nullable; +import android.support.v4.content.AsyncTaskLoader; + +import com.google.auto.value.AutoValue; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables; +import org.sufficientlysecure.keychain.remote.ui.dialog.KeyLoader.KeyInfo; + + +public class KeyLoader extends AsyncTaskLoader> { + // These are the rows that we will retrieve. + private String[] QUERY_PROJECTION = new String[]{ + KeyRings._ID, + KeyRings.MASTER_KEY_ID, + KeyRings.CREATION, + KeyRings.HAS_ENCRYPT, + KeyRings.VERIFIED, + KeyRings.NAME, + KeyRings.EMAIL, + KeyRings.COMMENT, + KeyRings.IS_EXPIRED, + KeyRings.IS_REVOKED, + }; + private static final int INDEX_MASTER_KEY_ID = 1; + private static final int INDEX_CREATION = 2; + private static final int INDEX_HAS_ENCRYPT = 3; + private static final int INDEX_VERIFIED = 4; + private static final int INDEX_NAME = 5; + private static final int INDEX_EMAIL = 6; + private static final int INDEX_COMMENT = 7; + + private static final String QUERY_WHERE = Tables.KEYS + "." + KeyRings.IS_REVOKED + + " = 0 AND " + KeyRings.IS_EXPIRED + " = 0"; + private static final String QUERY_ORDER = Tables.KEYS + "." + KeyRings.CREATION + " DESC"; + + private final ContentResolver contentResolver; + private final String emailAddress; + + private List cachedResult; + + + KeyLoader(Context context, ContentResolver contentResolver, String emailAddress) { + super(context); + + this.contentResolver = contentResolver; + this.emailAddress = emailAddress; + } + + @Override + public List loadInBackground() { + ArrayList keyInfos = new ArrayList<>(); + + Cursor cursor = contentResolver.query(KeyRings.buildUnifiedKeyRingsFindByEmailUri(emailAddress), + QUERY_PROJECTION, QUERY_WHERE, null, QUERY_ORDER); + if (cursor == null) { + return null; + } + + while (cursor.moveToNext()) { + KeyInfo keyInfo = KeyInfo.fromCursor(cursor); + keyInfos.add(keyInfo); + } + + return Collections.unmodifiableList(keyInfos); + } + + @Override + public void deliverResult(List keySubkeyStatus) { + cachedResult = keySubkeyStatus; + + if (isStarted()) { + super.deliverResult(keySubkeyStatus); + } + } + + @Override + protected void onStartLoading() { + if (cachedResult != null) { + deliverResult(cachedResult); + } + + if (takeContentChanged() || cachedResult == null) { + forceLoad(); + } + } + + @Override + protected void onStopLoading() { + super.onStopLoading(); + + cachedResult = null; + } + + @AutoValue + public abstract static class KeyInfo { + public abstract long getMasterKeyId(); + public abstract long getCreationDate(); + public abstract boolean getHasEncrypt(); + public abstract boolean getIsVerified(); + + @Nullable + public abstract String getName(); + @Nullable + public abstract String getEmail(); + @Nullable + public abstract String getComment(); + + static KeyInfo fromCursor(Cursor cursor) { + long masterKeyId = cursor.getLong(INDEX_MASTER_KEY_ID); + long creationDate = cursor.getLong(INDEX_CREATION) * 1000L; + boolean hasEncrypt = cursor.getInt(INDEX_HAS_ENCRYPT) != 0; + boolean isVerified = cursor.getInt(INDEX_VERIFIED) == 2; + + String name = cursor.getString(INDEX_NAME); + String email = cursor.getString(INDEX_EMAIL); + String comment = cursor.getString(INDEX_COMMENT); + + return new AutoValue_KeyLoader_KeyInfo( + masterKeyId, creationDate, hasEncrypt, isVerified, name, email, comment); + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteDeduplicateActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteDeduplicateActivity.java new file mode 100644 index 000000000..3872ce352 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteDeduplicateActivity.java @@ -0,0 +1,331 @@ +/* + * Copyright (C) 2017 Vincent Breitmoser + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.remote.ui.dialog; + + +import java.util.List; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Drawable.ConstantState; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.content.res.ResourcesCompat; +import android.support.v4.graphics.drawable.DrawableCompat; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.RecyclerView.Adapter; +import android.text.format.DateUtils; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.TextView; + +import com.mikepenz.materialdrawer.util.KeyboardUtil; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.remote.ui.dialog.KeyLoader.KeyInfo; +import org.sufficientlysecure.keychain.remote.ui.dialog.RemoteDeduplicatePresenter.RemoteDeduplicateView; +import org.sufficientlysecure.keychain.ui.dialog.CustomAlertDialogBuilder; +import org.sufficientlysecure.keychain.ui.util.ThemeChanger; +import org.sufficientlysecure.keychain.ui.util.recyclerview.DividerItemDecoration; +import org.sufficientlysecure.keychain.ui.util.recyclerview.RecyclerItemClickListener; + + +public class RemoteDeduplicateActivity extends FragmentActivity { + public static final String EXTRA_PACKAGE_NAME = "package_name"; + public static final String EXTRA_DUPLICATE_EMAILS = "duplicate_emails"; + + public static final int LOADER_ID_KEYS = 0; + + + private RemoteDeduplicatePresenter presenter; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + presenter = new RemoteDeduplicatePresenter(getBaseContext(), LOADER_ID_KEYS); + + KeyboardUtil.hideKeyboard(this); + + if (savedInstanceState == null) { + RemoteDeduplicateDialogFragment frag = new RemoteDeduplicateDialogFragment(); + frag.show(getSupportFragmentManager(), "requestKeyDialog"); + } + } + + @Override + protected void onStart() { + super.onStart(); + + Intent intent = getIntent(); + List dupAddresses = intent.getStringArrayListExtra(EXTRA_DUPLICATE_EMAILS); + String duplicateAddress = dupAddresses.get(0); + String packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME); + + presenter.setupFromIntentData(packageName, duplicateAddress); + presenter.startLoaders(getSupportLoaderManager()); + } + + public static class RemoteDeduplicateDialogFragment extends DialogFragment { + private RemoteDeduplicatePresenter presenter; + private RemoteDeduplicateView mvpView; + + private Button buttonSelect; + private Button buttonCancel; + private RecyclerView keyChoiceList; + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Activity activity = getActivity(); + + ContextThemeWrapper theme = ThemeChanger.getDialogThemeWrapper(activity); + CustomAlertDialogBuilder alert = new CustomAlertDialogBuilder(theme); + + LayoutInflater layoutInflater = LayoutInflater.from(theme); + @SuppressLint("InflateParams") + View view = layoutInflater.inflate(R.layout.api_remote_deduplicate, null, false); + alert.setView(view); + + buttonSelect = (Button) view.findViewById(R.id.button_select); + buttonCancel = (Button) view.findViewById(R.id.button_cancel); + + keyChoiceList = (RecyclerView) view.findViewById(R.id.duplicate_key_list); + keyChoiceList.setLayoutManager(new LinearLayoutManager(activity)); + keyChoiceList.addItemDecoration(new DividerItemDecoration(activity, DividerItemDecoration.VERTICAL_LIST)); + + setupListenersForPresenter(); + mvpView = createMvpView(view, layoutInflater); + + return alert.create(); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + presenter = ((RemoteDeduplicateActivity) getActivity()).presenter; + presenter.setView(mvpView); + } + + @Override + public void onCancel(DialogInterface dialog) { + super.onCancel(dialog); + + if (presenter != null) { + presenter.onCancel(); + } + } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + + if (presenter != null) { + presenter.setView(null); + presenter = null; + } + } + + @NonNull + private RemoteDeduplicateView createMvpView(View view, LayoutInflater layoutInflater) { + final ImageView iconClientApp = (ImageView) view.findViewById(R.id.icon_client_app); + final KeyChoiceAdapter keyChoiceAdapter = new KeyChoiceAdapter(layoutInflater, getResources()); + final TextView addressText = (TextView) view.findViewById(R.id.select_key_item_name); + keyChoiceList.setAdapter(keyChoiceAdapter); + + return new RemoteDeduplicateView() { + @Override + public void finish() { + FragmentActivity activity = getActivity(); + if (activity == null) { + return; + } + + activity.setResult(RESULT_OK, null); + activity.finish(); + } + + @Override + public void finishAsCancelled() { + FragmentActivity activity = getActivity(); + if (activity == null) { + return; + } + + activity.setResult(RESULT_CANCELED); + activity.finish(); + } + + @Override + public void setTitleClientIcon(Drawable drawable) { + iconClientApp.setImageDrawable(drawable); + keyChoiceAdapter.setSelectionDrawable(drawable); + } + + @Override + public void setAddressText(String text) { + addressText.setText(text); + } + + @Override + public void setKeyListData(List data) { + keyChoiceAdapter.setData(data); + } + + @Override + public void setActiveItem(Integer position) { + keyChoiceAdapter.setActiveItem(position); + } + + @Override + public void setEnableSelectButton(boolean enabled) { + buttonSelect.setEnabled(enabled); + } + }; + } + + private void setupListenersForPresenter() { + buttonSelect.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + presenter.onClickSelect(); + } + }); + + buttonCancel.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + presenter.onClickCancel(); + } + }); + + keyChoiceList.addOnItemTouchListener(new RecyclerItemClickListener(getContext(), + new RecyclerItemClickListener.OnItemClickListener() { + @Override + public void onItemClick(View view, int position) { + presenter.onKeyItemClick(position); + } + })); + } + } + + private static class KeyChoiceAdapter extends Adapter { + private final LayoutInflater layoutInflater; + private final Resources resources; + private List data; + private Drawable iconUnselected; + private Drawable iconSelected; + private Integer activeItem; + + KeyChoiceAdapter(LayoutInflater layoutInflater, Resources resources) { + this.layoutInflater = layoutInflater; + this.resources = resources; + } + + @Override + public KeyChoiceViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View keyChoiceItemView = layoutInflater.inflate(R.layout.duplicate_key_item, parent, false); + return new KeyChoiceViewHolder(keyChoiceItemView); + } + + @Override + public void onBindViewHolder(KeyChoiceViewHolder holder, int position) { + KeyInfo keyInfo = data.get(position); + Drawable icon = (activeItem != null && position == activeItem) ? iconSelected : iconUnselected; + holder.bind(keyInfo, icon); + } + + @Override + public int getItemCount() { + return data != null ? data.size() : 0; + } + + public void setData(List data) { + this.data = data; + notifyDataSetChanged(); + } + + void setSelectionDrawable(Drawable drawable) { + ConstantState constantState = drawable.getConstantState(); + if (constantState == null) { + return; + } + + iconSelected = constantState.newDrawable(resources); + + iconUnselected = constantState.newDrawable(resources); + DrawableCompat.setTint(iconUnselected.mutate(), ResourcesCompat.getColor(resources, R.color.md_grey_300, null)); + + notifyDataSetChanged(); + } + + void setActiveItem(Integer newActiveItem) { + Integer prevActiveItem = this.activeItem; + this.activeItem = newActiveItem; + + if (prevActiveItem != null) { + notifyItemChanged(prevActiveItem); + } + if (newActiveItem != null) { + notifyItemChanged(newActiveItem); + } + } + } + + private static class KeyChoiceViewHolder extends RecyclerView.ViewHolder { + private final TextView vName; + private final TextView vCreation; + private final ImageView vIcon; + + KeyChoiceViewHolder(View itemView) { + super(itemView); + + vName = (TextView) itemView.findViewById(R.id.key_list_item_name); + vCreation = (TextView) itemView.findViewById(R.id.key_list_item_creation); + vIcon = (ImageView) itemView.findViewById(R.id.key_list_item_icon); + } + + void bind(KeyInfo keyInfo, Drawable selectionIcon) { + vName.setText(keyInfo.getName()); + + Context context = vCreation.getContext(); + String dateTime = DateUtils.formatDateTime(context, keyInfo.getCreationDate(), + DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME | + DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_ABBREV_MONTH); + vCreation.setText(context.getString(R.string.label_key_created, dateTime)); + + vIcon.setImageDrawable(selectionIcon); + } + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteDeduplicatePresenter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteDeduplicatePresenter.java new file mode 100644 index 000000000..5697dc9b3 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/dialog/RemoteDeduplicatePresenter.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2017 Vincent Breitmoser + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.remote.ui.dialog; + + +import java.util.List; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.support.v4.app.LoaderManager; +import android.support.v4.app.LoaderManager.LoaderCallbacks; +import android.support.v4.content.Loader; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.provider.AutocryptPeerDataAccessObject; +import org.sufficientlysecure.keychain.remote.ui.dialog.KeyLoader.KeyInfo; +import org.sufficientlysecure.keychain.util.Log; + + +class RemoteDeduplicatePresenter implements LoaderCallbacks> { + private final PackageManager packageManager; + private final Context context; + private final int loaderId; + + + private AutocryptPeerDataAccessObject autocryptPeerDao; + private String duplicateAddress; + + private RemoteDeduplicateView view; + private Integer selectedItem; + private List keyInfoData; + + + RemoteDeduplicatePresenter(Context context, int loaderId) { + this.context = context; + + packageManager = context.getPackageManager(); + + this.loaderId = loaderId; + } + + public void setView(RemoteDeduplicateView view) { + this.view = view; + } + + void setupFromIntentData(String packageName, String duplicateAddress) { + try { + setPackageInfo(packageName); + } catch (NameNotFoundException e) { + Log.e(Constants.TAG, "Unable to find info of calling app!"); + view.finishAsCancelled(); + return; + } + + autocryptPeerDao = new AutocryptPeerDataAccessObject(context, packageName); + + this.duplicateAddress = duplicateAddress; + view.setAddressText(duplicateAddress); + } + + private void setPackageInfo(String packageName) throws NameNotFoundException { + ApplicationInfo applicationInfo = packageManager.getApplicationInfo(packageName, 0); + Drawable appIcon = packageManager.getApplicationIcon(applicationInfo); + + view.setTitleClientIcon(appIcon); + } + + void startLoaders(LoaderManager loaderManager) { + loaderManager.restartLoader(loaderId, null, this); + } + + @Override + public Loader> onCreateLoader(int id, Bundle args) { + return new KeyLoader(context, context.getContentResolver(), duplicateAddress); + } + + @Override + public void onLoadFinished(Loader> loader, List data) { + this.keyInfoData = data; + view.setKeyListData(data); + } + + @Override + public void onLoaderReset(Loader loader) { + view.setKeyListData(null); + } + + void onClickSelect() { + if (keyInfoData == null) { + Log.e(Constants.TAG, "got click on select with no data…?"); + return; + } + if (selectedItem == null) { + Log.e(Constants.TAG, "got click on select with no selection…?"); + return; + } + + long masterKeyId = keyInfoData.get(selectedItem).getMasterKeyId(); + autocryptPeerDao.updateToSelectedState(duplicateAddress, masterKeyId); + + view.finish(); + } + + void onClickCancel() { + view.finishAsCancelled(); + } + + public void onCancel() { + view.finishAsCancelled(); + } + + void onKeyItemClick(int position) { + if (selectedItem != null && position == selectedItem) { + selectedItem = null; + } else { + selectedItem = position; + } + view.setActiveItem(selectedItem); + view.setEnableSelectButton(selectedItem != null); + } + + interface RemoteDeduplicateView { + void finish(); + void finishAsCancelled(); + + void setAddressText(String text); + void setTitleClientIcon(Drawable drawable); + + void setKeyListData(List data); + void setActiveItem(Integer position); + void setEnableSelectButton(boolean enabled); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/CertSectionedListAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/CertSectionedListAdapter.java index e0a4df9c1..abc730292 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/CertSectionedListAdapter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/CertSectionedListAdapter.java @@ -32,6 +32,7 @@ import org.sufficientlysecure.keychain.pgp.KeyRing; import org.sufficientlysecure.keychain.pgp.WrappedSignature; import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainDatabase; +import org.sufficientlysecure.keychain.ui.adapter.CertSectionedListAdapter.CertCursor; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.ui.util.adapter.CursorAdapter; import org.sufficientlysecure.keychain.ui.util.adapter.SectionCursorAdapter; @@ -39,7 +40,7 @@ import org.sufficientlysecure.keychain.ui.util.adapter.SectionCursorAdapter; import java.util.ArrayList; import java.util.Arrays; -public class CertSectionedListAdapter extends SectionCursorAdapter { private CertListListener mListener; @@ -162,11 +163,11 @@ public class CertSectionedListAdapter extends SectionCursorAdapter projection = new ArrayList<>(); - projection.addAll(Arrays.asList(AbstractCursor.PROJECTION)); + projection.addAll(Arrays.asList(SimpleCursor.PROJECTION)); projection.addAll(Arrays.asList( KeychainContract.Certs.MASTER_KEY_ID, KeychainContract.Certs.VERIFIED, diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/IdentityAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/IdentityAdapter.java index d7b7fea8d..52ef50b5c 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/IdentityAdapter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/IdentityAdapter.java @@ -28,6 +28,7 @@ import android.os.Build.VERSION_CODES; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; +import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; @@ -38,6 +39,7 @@ import org.sufficientlysecure.keychain.provider.KeychainContract.Certs; import org.sufficientlysecure.keychain.ui.adapter.IdentityAdapter.ViewHolder; import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityLoader.IdentityInfo; import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityLoader.LinkedIdInfo; +import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityLoader.TrustIdInfo; import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityLoader.UserIdInfo; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.State; @@ -52,15 +54,17 @@ public class IdentityAdapter extends RecyclerView.Adapter { private final Context context; private final LayoutInflater layoutInflater; private final boolean isSecret; + private final IdentityClickListener identityClickListener; private List data; - public IdentityAdapter(Context context, boolean isSecret) { + public IdentityAdapter(Context context, boolean isSecret, IdentityClickListener identityClickListener) { super(); this.layoutInflater = LayoutInflater.from(context); this.context = context; this.isSecret = isSecret; + this.identityClickListener = identityClickListener; } public void setData(List data) { @@ -75,7 +79,11 @@ public class IdentityAdapter extends RecyclerView.Adapter { int viewType = getItemViewType(position); if (viewType == VIEW_TYPE_USER_ID) { - ((UserIdViewHolder) holder).bind((UserIdInfo) info); + if (info instanceof TrustIdInfo) { + ((UserIdViewHolder) holder).bind((TrustIdInfo) info); + } else { + ((UserIdViewHolder) holder).bind((UserIdInfo) info); + } } else if (viewType == VIEW_TYPE_LINKED_ID) { ((LinkedIdViewHolder) holder).bind(context, (LinkedIdInfo) info, isSecret); } else { @@ -86,7 +94,8 @@ public class IdentityAdapter extends RecyclerView.Adapter { @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == VIEW_TYPE_USER_ID) { - return new UserIdViewHolder(layoutInflater.inflate(R.layout.view_key_identity_user_id, parent, false)); + return new UserIdViewHolder( + layoutInflater.inflate(R.layout.view_key_identity_user_id, parent, false), identityClickListener); } else if (viewType == VIEW_TYPE_LINKED_ID) { return new LinkedIdViewHolder(layoutInflater.inflate(R.layout.linked_id_item, parent, false)); } else { @@ -97,7 +106,7 @@ public class IdentityAdapter extends RecyclerView.Adapter { @Override public int getItemViewType(int position) { IdentityInfo info = data.get(position); - if (info instanceof UserIdInfo) { + if (info instanceof UserIdInfo || info instanceof TrustIdInfo) { return VIEW_TYPE_USER_ID; } else if (info instanceof LinkedIdInfo) { return VIEW_TYPE_LINKED_ID; @@ -189,16 +198,59 @@ public class IdentityAdapter extends RecyclerView.Adapter { private final TextView vName; private final TextView vAddress; private final TextView vComment; + private final ImageView vIcon; + private final ImageView vMore; - private UserIdViewHolder(View view) { + private UserIdViewHolder(View view, final IdentityClickListener identityClickListener) { super(view); vName = (TextView) view.findViewById(R.id.user_id_item_name); vAddress = (TextView) view.findViewById(R.id.user_id_item_address); vComment = (TextView) view.findViewById(R.id.user_id_item_comment); + + vIcon = (ImageView) view.findViewById(R.id.trust_id_app_icon); + vMore = (ImageView) view.findViewById(R.id.user_id_item_more); + + view.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + identityClickListener.onClickIdentity(getAdapterPosition()); + } + }); + + vMore.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + identityClickListener.onClickIdentityMore(getAdapterPosition(), v); + } + }); + } + + public void bind(TrustIdInfo info) { + if (info.getUserIdInfo() != null) { + bindUserIdInfo(info.getUserIdInfo()); + } else { + vName.setVisibility(View.GONE); + vComment.setVisibility(View.GONE); + + vAddress.setText(info.getTrustId()); + vAddress.setTypeface(null, Typeface.NORMAL); + } + + vIcon.setImageDrawable(info.getAppIcon()); + vMore.setVisibility(View.VISIBLE); + + itemView.setClickable(info.getTrustIdIntent() != null); } public void bind(UserIdInfo info) { + bindUserIdInfo(info); + + vIcon.setVisibility(View.GONE); + vMore.setVisibility(View.GONE); + } + + private void bindUserIdInfo(UserIdInfo info) { if (info.getName() != null) { vName.setText(info.getName()); } else { @@ -224,8 +276,12 @@ public class IdentityAdapter extends RecyclerView.Adapter { vName.setTypeface(null, Typeface.NORMAL); vAddress.setTypeface(null, Typeface.NORMAL); } - } } + + public interface IdentityClickListener { + void onClickIdentity(int position); + void onClickIdentityMore(int position, View anchor); + } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeySectionedListAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeySectionedListAdapter.java index da7612143..53ad5f416 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeySectionedListAdapter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeySectionedListAdapter.java @@ -19,10 +19,13 @@ package org.sufficientlysecure.keychain.ui.adapter; import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; import android.database.Cursor; import android.database.MatrixCursor; import android.database.MergeCursor; import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; import android.support.v4.content.ContextCompat; import android.text.TextUtils; import android.text.format.DateUtils; @@ -38,6 +41,7 @@ import com.futuremind.recyclerviewfastscroll.SectionTitleProvider; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.ui.util.FormattingUtils; import org.sufficientlysecure.keychain.ui.util.Highlighter; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; @@ -47,6 +51,8 @@ import org.sufficientlysecure.keychain.util.Log; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; import java.util.List; public class KeySectionedListAdapter extends SectionCursorAdapter packageNames = keyItem.getTrustIdPackages(); + + if (!keyItem.isSecret() && !packageNames.isEmpty()) { + String packageName = packageNames.get(0); + Drawable drawable = getDrawableForPackageName(packageName); + if (drawable != null) { + mTrustIdIcon.setImageDrawable(drawable); + mTrustIdIcon.setVisibility(View.VISIBLE); + } else { + mTrustIdIcon.setVisibility(View.GONE); + } + } else { + mTrustIdIcon.setVisibility(View.GONE); + } } } @@ -562,7 +586,8 @@ public class KeySectionedListAdapter extends SectionCursorAdapter 0; } + + public List getTrustIdPackages() { + int index = getColumnIndexOrThrow(KeyRings.API_KNOWN_TO_PACKAGE_NAMES); + String packageNames = getString(index); + if (packageNames == null) { + return Collections.EMPTY_LIST; + } + return Arrays.asList(packageNames.split(",")); + } } public interface KeyListListener { @@ -614,4 +648,24 @@ public class KeySectionedListAdapter extends SectionCursorAdapter appIconCache = new HashMap<>(); + + private Drawable getDrawableForPackageName(String packageName) { + if (appIconCache.containsKey(packageName)) { + return appIconCache.get(packageName); + } + + PackageManager pm = getContext().getPackageManager(); + try { + ApplicationInfo ai = pm.getApplicationInfo(packageName, 0); + + Drawable appIcon = pm.getApplicationIcon(ai); + appIconCache.put(packageName, appIcon); + + return appIcon; + } catch (PackageManager.NameNotFoundException e) { + return null; + } + } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/ViewKeyFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/ViewKeyFragment.java index a5e6af591..fb7335373 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/ViewKeyFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/ViewKeyFragment.java @@ -25,7 +25,11 @@ import android.os.Handler; import android.support.v4.app.DialogFragment; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentTransaction; +import android.support.v7.widget.PopupMenu; +import android.support.v7.widget.PopupMenu.OnDismissListener; +import android.support.v7.widget.PopupMenu.OnMenuItemClickListener; import android.view.LayoutInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; @@ -44,7 +48,7 @@ import org.sufficientlysecure.keychain.ui.keyview.view.KeyserverStatusView; import org.sufficientlysecure.keychain.ui.keyview.view.SystemContactCardView; -public class ViewKeyFragment extends LoaderFragment implements ViewKeyMvpView { +public class ViewKeyFragment extends LoaderFragment implements ViewKeyMvpView, OnMenuItemClickListener { public static final String ARG_MASTER_KEY_ID = "master_key_id"; public static final String ARG_IS_SECRET = "is_secret"; @@ -67,6 +71,8 @@ public class ViewKeyFragment extends LoaderFragment implements ViewKeyMvpView { KeyHealthPresenter mKeyHealthPresenter; KeyserverStatusPresenter mKeyserverStatusPresenter; + private Integer displayedContextMenuPosition; + /** * Creates new instance of this fragment */ @@ -161,4 +167,37 @@ public class ViewKeyFragment extends LoaderFragment implements ViewKeyMvpView { } }); } + + @Override + public void showContextMenu(int position, View anchor) { + displayedContextMenuPosition = position; + + PopupMenu menu = new PopupMenu(getContext(), anchor); + menu.inflate(R.menu.identity_context_menu); + menu.setOnMenuItemClickListener(this); + menu.setOnDismissListener(new OnDismissListener() { + @Override + public void onDismiss(PopupMenu popupMenu) { + displayedContextMenuPosition = null; + } + }); + menu.show(); + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + if (displayedContextMenuPosition == null) { + return false; + } + + switch (item.getItemId()) { + case R.id.autocrypt_forget: + int position = displayedContextMenuPosition; + displayedContextMenuPosition = null; + mIdentitiesPresenter.onClickForgetIdentity(position); + return true; + } + + return false; + } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/IdentityLoader.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/IdentityLoader.java index a2edbaffe..a14287811 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/IdentityLoader.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/loader/IdentityLoader.java @@ -25,18 +25,26 @@ import java.util.List; import android.content.ContentResolver; import android.content.Context; +import android.content.Intent; +import android.content.pm.ResolveInfo; import android.database.Cursor; +import android.graphics.drawable.Drawable; import android.support.annotation.Nullable; import android.support.v4.content.AsyncTaskLoader; import android.util.Log; import com.google.auto.value.AutoValue; +import org.openintents.openpgp.util.OpenPgpApi; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.linked.LinkedAttribute; import org.sufficientlysecure.keychain.linked.UriAttribute; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAutocryptPeer; +import org.sufficientlysecure.keychain.provider.KeychainContract.Certs; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets; import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityLoader.IdentityInfo; +import org.sufficientlysecure.keychain.ui.util.PackageIconGetter; public class IdentityLoader extends AsyncTaskLoader> { @@ -68,6 +76,7 @@ public class IdentityLoader extends AsyncTaskLoader> { private static final String USER_IDS_WHERE = UserPackets.IS_REVOKED + " = 0"; private final ContentResolver contentResolver; + private final PackageIconGetter packageIconGetter; private final long masterKeyId; private final boolean showLinkedIds; @@ -82,6 +91,9 @@ public class IdentityLoader extends AsyncTaskLoader> { this.masterKeyId = masterKeyId; this.showLinkedIds = showLinkedIds; + this.identityObserver = new ForceLoadContentObserver(); + this.packageIconGetter = PackageIconGetter.getInstance(context); + this.identityObserver = new ForceLoadContentObserver(); } @@ -93,10 +105,76 @@ public class IdentityLoader extends AsyncTaskLoader> { loadLinkedIds(identities); } loadUserIds(identities); + correlateOrAddTrustIds(identities); return Collections.unmodifiableList(identities); } + private static final String[] TRUST_IDS_PROJECTION = new String[] { + ApiAutocryptPeer._ID, + ApiAutocryptPeer.PACKAGE_NAME, + ApiAutocryptPeer.IDENTIFIER, + }; + private static final int INDEX_PACKAGE_NAME = 1; + private static final int INDEX_TRUST_ID = 2; + + private void correlateOrAddTrustIds(ArrayList identities) { + Cursor cursor = contentResolver.query(ApiAutocryptPeer.buildByMasterKeyId(masterKeyId), + TRUST_IDS_PROJECTION, null, null, null); + if (cursor == null) { + Log.e(Constants.TAG, "Error loading trust ids!"); + return; + } + + try { + while (cursor.moveToNext()) { + String packageName = cursor.getString(INDEX_PACKAGE_NAME); + String autocryptPeer = cursor.getString(INDEX_TRUST_ID); + + Drawable drawable = packageIconGetter.getDrawableForPackageName(packageName); + Intent autocryptPeerIntent = getTrustIdActivityIntentIfResolvable(packageName, autocryptPeer); + + UserIdInfo associatedUserIdInfo = findUserIdMatchingTrustId(identities, autocryptPeer); + if (associatedUserIdInfo != null) { + int position = identities.indexOf(associatedUserIdInfo); + TrustIdInfo autocryptPeerInfo = TrustIdInfo.create(associatedUserIdInfo, autocryptPeer, packageName, drawable, autocryptPeerIntent); + identities.set(position, autocryptPeerInfo); + } else { + TrustIdInfo autocryptPeerInfo = TrustIdInfo.create(autocryptPeer, packageName, drawable, autocryptPeerIntent); + identities.add(autocryptPeerInfo); + } + } + } finally { + cursor.close(); + } + } + + private Intent getTrustIdActivityIntentIfResolvable(String packageName, String autocryptPeer) { + Intent intent = new Intent(); + intent.setAction(packageName + ".AUTOCRYPT_PEER_ACTION"); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(OpenPgpApi.EXTRA_AUTOCRYPT_PEER_ID, autocryptPeer); + + List resolveInfos = getContext().getPackageManager().queryIntentActivities(intent, 0); + if (resolveInfos != null && !resolveInfos.isEmpty()) { + return intent; + } else { + return null; + } + } + + private static UserIdInfo findUserIdMatchingTrustId(List identities, String autocryptPeer) { + for (IdentityInfo identityInfo : identities) { + if (identityInfo instanceof UserIdInfo) { + UserIdInfo userIdInfo = (UserIdInfo) identityInfo; + if (autocryptPeer.equals(userIdInfo.getEmail())) { + return userIdInfo; + } + } + } + return null; + } + private void loadLinkedIds(ArrayList identities) { Cursor cursor = contentResolver.query(UserPackets.buildLinkedIdsUri(masterKeyId), USER_PACKETS_PROJECTION, USER_IDS_WHERE, null, null); @@ -222,4 +300,32 @@ public class IdentityLoader extends AsyncTaskLoader> { return new AutoValue_IdentityLoader_LinkedIdInfo(rank, verified, isPrimary, uriAttribute); } } + + @AutoValue + public abstract static class TrustIdInfo implements IdentityInfo { + public abstract int getRank(); + public abstract int getVerified(); + public abstract boolean isPrimary(); + + public abstract String getTrustId(); + public abstract String getPackageName(); + @Nullable + public abstract Drawable getAppIcon(); + @Nullable + public abstract UserIdInfo getUserIdInfo(); + @Nullable + public abstract Intent getTrustIdIntent(); + + static TrustIdInfo create(UserIdInfo userIdInfo, String autocryptPeer, String packageName, + Drawable appIcon, Intent autocryptPeerIntent) { + return new AutoValue_IdentityLoader_TrustIdInfo(userIdInfo.getRank(), userIdInfo.getVerified(), + userIdInfo.isPrimary(), autocryptPeer, packageName, appIcon, userIdInfo, autocryptPeerIntent); + } + + static TrustIdInfo create(String autocryptPeer, String packageName, Drawable appIcon, Intent autocryptPeerIntent) { + return new AutoValue_IdentityLoader_TrustIdInfo( + 0, Certs.VERIFIED_SELF, false, autocryptPeer, packageName, appIcon, null, autocryptPeerIntent); + } + } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/presenter/IdentitiesPresenter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/presenter/IdentitiesPresenter.java index 0356b42d0..0680e09fe 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/presenter/IdentitiesPresenter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/presenter/IdentitiesPresenter.java @@ -28,18 +28,22 @@ import android.os.Bundle; import android.support.v4.app.LoaderManager; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.Loader; +import android.view.View; import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.provider.AutocryptPeerDataAccessObject; import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets; import org.sufficientlysecure.keychain.ui.EditIdentitiesActivity; import org.sufficientlysecure.keychain.ui.adapter.IdentityAdapter; +import org.sufficientlysecure.keychain.ui.adapter.IdentityAdapter.IdentityClickListener; import org.sufficientlysecure.keychain.ui.dialog.UserIdInfoDialogFragment; import org.sufficientlysecure.keychain.ui.keyview.LinkedIdViewFragment; import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityLoader; import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityLoader.IdentityInfo; import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityLoader.LinkedIdInfo; +import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityLoader.TrustIdInfo; import org.sufficientlysecure.keychain.ui.keyview.loader.IdentityLoader.UserIdInfo; import org.sufficientlysecure.keychain.ui.linked.LinkedIdWizard; import org.sufficientlysecure.keychain.util.Log; @@ -67,17 +71,23 @@ public class IdentitiesPresenter implements LoaderCallbacks> this.masterKeyId = masterKeyId; this.isSecret = isSecret; - identitiesAdapter = new IdentityAdapter(context, isSecret); + identitiesAdapter = new IdentityAdapter(context, isSecret, new IdentityClickListener() { + @Override + public void onClickIdentity(int position) { + showIdentityInfo(position); + } + + @Override + public void onClickIdentityMore(int position, View anchor) { + showIdentityContextMenu(position, anchor); + + } + }); view.setIdentitiesAdapter(identitiesAdapter); view.setEditIdentitiesButtonVisible(isSecret); view.setIdentitiesCardListener(new IdentitiesCardListener() { - @Override - public void onIdentityItemClick(int position) { - showIdentityInfo(position); - } - @Override public void onClickEditIdentities() { editIdentities(); @@ -117,9 +127,18 @@ public class IdentitiesPresenter implements LoaderCallbacks> showLinkedId((LinkedIdInfo) info); } else if (info instanceof UserIdInfo) { showUserIdInfo((UserIdInfo) info); + } else if (info instanceof TrustIdInfo) { + Intent autocryptPeerIntent = ((TrustIdInfo) info).getTrustIdIntent(); + if (autocryptPeerIntent != null) { + viewKeyMvpView.startActivity(autocryptPeerIntent); + } } } + private void showIdentityContextMenu(int position, View anchor) { + viewKeyMvpView.showContextMenu(position, anchor); + } + private void showLinkedId(final LinkedIdInfo info) { final LinkedIdViewFragment frag; try { @@ -154,6 +173,18 @@ public class IdentitiesPresenter implements LoaderCallbacks> context.startActivity(intent); } + public void onClickForgetIdentity(int position) { + TrustIdInfo info = (TrustIdInfo) identitiesAdapter.getInfo(position); + if (info == null) { + Log.e(Constants.TAG, "got a 'forget' click on a bad trust id"); + return; + } + + AutocryptPeerDataAccessObject autocryptPeerDao = + new AutocryptPeerDataAccessObject(context, info.getPackageName()); + autocryptPeerDao.delete(info.getTrustId()); + } + public interface IdentitiesMvpView { void setIdentitiesAdapter(IdentityAdapter userIdsAdapter); void setIdentitiesCardListener(IdentitiesCardListener identitiesCardListener); @@ -161,8 +192,6 @@ public class IdentitiesPresenter implements LoaderCallbacks> } public interface IdentitiesCardListener { - void onIdentityItemClick(int position); - void onClickEditIdentities(); void onClickAddIdentity(); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/presenter/ViewKeyMvpView.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/presenter/ViewKeyMvpView.java index 98399a52c..3a4fa7f59 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/presenter/ViewKeyMvpView.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/presenter/ViewKeyMvpView.java @@ -4,12 +4,16 @@ package org.sufficientlysecure.keychain.ui.keyview.presenter; import android.content.Intent; import android.support.v4.app.DialogFragment; import android.support.v4.app.Fragment; +import android.view.View; public interface ViewKeyMvpView { void switchToFragment(Fragment frag, String backStackName); + void startActivity(Intent intent); void startActivityAndShowResultSnackbar(Intent intent); void showDialogFragment(DialogFragment dialogFragment, final String tag); void setContentShown(boolean show, boolean animate); + + void showContextMenu(int position, View anchor); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/view/IdentitiesCardView.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/view/IdentitiesCardView.java index d30f93093..0ddc6b160 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/view/IdentitiesCardView.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/keyview/view/IdentitiesCardView.java @@ -32,7 +32,6 @@ import org.sufficientlysecure.keychain.ui.adapter.IdentityAdapter; import org.sufficientlysecure.keychain.ui.keyview.presenter.IdentitiesPresenter.IdentitiesCardListener; import org.sufficientlysecure.keychain.ui.keyview.presenter.IdentitiesPresenter.IdentitiesMvpView; import org.sufficientlysecure.keychain.ui.util.recyclerview.DividerItemDecoration; -import org.sufficientlysecure.keychain.ui.util.recyclerview.RecyclerItemClickListener; public class IdentitiesCardView extends CardView implements IdentitiesMvpView { @@ -59,16 +58,6 @@ public class IdentitiesCardView extends CardView implements IdentitiesMvpView { } }); - vIdentities.addOnItemTouchListener(new RecyclerItemClickListener(context, - new RecyclerItemClickListener.OnItemClickListener() { - @Override - public void onItemClick(View view, int position) { - if (identitiesCardListener != null) { - identitiesCardListener.onIdentityItemClick(position); - } - } - })); - Button linkedIdsAddButton = (Button) view.findViewById(R.id.view_key_card_linked_ids_add); linkedIdsAddButton.setOnClickListener(new View.OnClickListener() { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/PackageIconGetter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/PackageIconGetter.java new file mode 100644 index 000000000..0ea8c7b7c --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/PackageIconGetter.java @@ -0,0 +1,44 @@ +package org.sufficientlysecure.keychain.ui.util; + + +import java.util.HashMap; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; + + +public class PackageIconGetter { + private final PackageManager packageManager; + private final HashMap appIconCache = new HashMap<>(); + + + public static PackageIconGetter getInstance(Context context) { + PackageManager pm = context.getPackageManager(); + + return new PackageIconGetter(pm); + } + + private PackageIconGetter(PackageManager packageManager) { + this.packageManager = packageManager; + } + + public Drawable getDrawableForPackageName(String packageName) { + if (appIconCache.containsKey(packageName)) { + return appIconCache.get(packageName); + } + + try { + ApplicationInfo ai = packageManager.getApplicationInfo(packageName, 0); + + Drawable appIcon = packageManager.getApplicationIcon(ai); + appIconCache.put(packageName, appIcon); + + return appIcon; + } catch (PackageManager.NameNotFoundException e) { + return null; + } + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/CursorAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/CursorAdapter.java index 75b1c9ff0..06fa523d0 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/CursorAdapter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/CursorAdapter.java @@ -27,6 +27,7 @@ import android.support.v7.widget.RecyclerView; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.ui.util.adapter.CursorAdapter.SimpleCursor; import org.sufficientlysecure.keychain.util.Log; import java.lang.reflect.Constructor; @@ -35,7 +36,7 @@ import java.util.Arrays; import java.util.Date; import java.util.HashMap; -public abstract class CursorAdapter +public abstract class CursorAdapter extends RecyclerView.Adapter { public static final String TAG = "CursorAdapter"; @@ -58,7 +59,7 @@ public abstract class CursorAdapternot + * {@link #changeCursor(SimpleCursor)}, the returned old Cursor is not * closed. * * @param newCursor The new cursor to be used. @@ -312,10 +313,10 @@ public abstract class CursorAdapter T wrap(Cursor cursor, Class type) { + public static T wrap(Cursor cursor, Class type) { if (cursor != null) { try { Constructor constructor = type.getConstructor(Cursor.class); @@ -335,7 +336,7 @@ public abstract class CursorAdapter(cursor.getColumnCount() * 4 / 3, 0.75f); } @@ -376,12 +377,12 @@ public abstract class CursorAdapter arr = new ArrayList<>(); - arr.addAll(Arrays.asList(AbstractCursor.PROJECTION)); + arr.addAll(Arrays.asList(SimpleCursor.PROJECTION)); arr.addAll(Arrays.asList( KeychainContract.KeyRings.MASTER_KEY_ID, KeychainContract.KeyRings.USER_ID, @@ -392,7 +393,8 @@ public abstract class CursorAdapter section type. * @param the view holder extending {@code BaseViewHolder} that is bound to the cursor data. * @param the view holder extending {@code BaseViewHolder<>} that is bound to the section data. */ -public abstract class SectionCursorAdapter extends CursorAdapter { public static final String TAG = "SectionCursorAdapter"; diff --git a/OpenKeychain/src/main/res/drawable/ic_chat_black_24dp.xml b/OpenKeychain/src/main/res/drawable/ic_chat_black_24dp.xml new file mode 100644 index 000000000..e3489bdea --- /dev/null +++ b/OpenKeychain/src/main/res/drawable/ic_chat_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/OpenKeychain/src/main/res/layout/api_remote_deduplicate.xml b/OpenKeychain/src/main/res/layout/api_remote_deduplicate.xml new file mode 100644 index 000000000..b02f0f053 --- /dev/null +++ b/OpenKeychain/src/main/res/layout/api_remote_deduplicate.xml @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +