Merge pull request #2127 from open-keychain/autocrypt

Autocrypt
This commit is contained in:
Dominik Schürmann
2017-07-25 22:58:59 +02:00
committed by GitHub
47 changed files with 2658 additions and 283 deletions

View File

@@ -864,6 +864,11 @@
android:exported="false"
android:theme="@style/Theme.Keychain.Transparent"
android:label="@string/app_name" />
<activity
android:name=".remote.ui.dialog.RemoteDeduplicateActivity"
android:exported="false"
android:theme="@style/Theme.Keychain.Transparent"
android:label="@string/app_name" />
<activity
android:name=".remote.ui.RemoteSelectPubKeyActivity"
android:exported="false"

View File

@@ -62,6 +62,10 @@ public abstract class CanonicalizedKeyRing extends KeyRing {
return getRing().getPublicKey().getFingerprint();
}
public byte[] getRawPrimaryUserId() throws PgpKeyNotFoundException {
return getPublicKey().getRawPrimaryUserId();
}
public String getPrimaryUserId() throws PgpKeyNotFoundException {
return getPublicKey().getPrimaryUserId();
}
@@ -136,6 +140,15 @@ public abstract class CanonicalizedKeyRing extends KeyRing {
}
}
public long getSigningId() throws PgpKeyNotFoundException {
for(CanonicalizedPublicKey key : publicKeyIterator()) {
if (key.canSign() && key.isValid()) {
return key.getKeyId();
}
}
throw new PgpKeyNotFoundException("No valid signing key found!");
}
public void encode(OutputStream stream) throws IOException {
getRing().encode(stream);
}

View File

@@ -18,6 +18,13 @@
package org.sufficientlysecure.keychain.pgp;
import java.io.IOException;
import java.util.Iterator;
import java.util.NoSuchElementException;
import android.support.annotation.Nullable;
import org.bouncycastle.openpgp.PGPObjectFactory;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
@@ -27,8 +34,6 @@ import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
import org.sufficientlysecure.keychain.util.IterableIterator;
import java.io.IOException;
import java.util.Iterator;
public class CanonicalizedPublicKeyRing extends CanonicalizedKeyRing {
@@ -84,6 +89,69 @@ public class CanonicalizedPublicKeyRing extends CanonicalizedKeyRing {
});
}
/** Returns a minimized version of this key.
*
* The minimized version includes:
* - the master key
* - the current best signing key (if any)
* - one encryption key (if any)
* - the user id that matches the userIdToKeep parameter, or the primary user id if none matches
* each with their most recent binding certificates
*/
public CanonicalizedPublicKeyRing minimize(@Nullable String userIdToKeep) throws IOException, PgpKeyNotFoundException {
CanonicalizedPublicKey masterKey = getPublicKey();
PGPPublicKey masterPubKey = masterKey.getPublicKey();
boolean userIdStrippedOk = false;
if (userIdToKeep != null) {
try {
masterPubKey = PGPPublicKeyUtils.keepOnlyUserId(masterPubKey, userIdToKeep);
userIdStrippedOk = true;
} catch (NoSuchElementException e) {
// will be handled because userIdStrippedOk is false
}
}
if (!userIdStrippedOk) {
byte[] rawPrimaryUserId = getRawPrimaryUserId();
masterPubKey = PGPPublicKeyUtils.keepOnlyRawUserId(masterPubKey, rawPrimaryUserId);
}
masterPubKey = PGPPublicKeyUtils.keepOnlySelfCertsForUserIds(masterPubKey);
masterPubKey = PGPPublicKeyUtils.removeAllUserAttributes(masterPubKey);
masterPubKey = PGPPublicKeyUtils.removeAllDirectKeyCerts(masterPubKey);
PGPPublicKeyRing resultRing = new PGPPublicKeyRing(masterPubKey.getEncoded(), new JcaKeyFingerprintCalculator());
Long encryptId;
try {
encryptId = getEncryptId();
// only add if this key doesn't coincide with master key
if (encryptId != getMasterKeyId()) {
CanonicalizedPublicKey encryptKey = getPublicKey(encryptId);
PGPPublicKey encryptPubKey = encryptKey.getPublicKey();
resultRing = PGPPublicKeyRing.insertPublicKey(resultRing, encryptPubKey);
}
} catch (PgpKeyNotFoundException e) {
// no encryption key: can't be reasonably minimized
return null;
}
try {
long signingId = getSigningId();
// only add if this key doesn't coincide with master or encryption key
if (signingId != encryptId && signingId != getMasterKeyId()) {
CanonicalizedPublicKey signingKey = getPublicKey(signingId);
PGPPublicKey signingPubKey = signingKey.getPublicKey();
resultRing = PGPPublicKeyRing.insertPublicKey(resultRing, signingPubKey);
}
} catch (PgpKeyNotFoundException e) {
// no signing key: can't be reasonably minimized
return null;
}
return new CanonicalizedPublicKeyRing(resultRing, getVerified());
}
/** Create a dummy secret ring from this key */
public UncachedKeyRing createDivertSecretRing (byte[] cardAid, long[] subKeyIds) {
PGPSecretKeyRing secRing = PGPSecretKeyRing.constructDummyFromPublic(getRing(), cardAid);

View File

@@ -125,32 +125,37 @@ public class OpenPgpSignatureResultBuilder {
}
setSignatureKeyCertified(signingRing.getVerified() > 0);
ArrayList<String> allUserIds = signingRing.getUnorderedUserIds();
ArrayList<String> confirmedUserIds;
try {
ArrayList<String> allUserIds = signingRing.getUnorderedUserIds();
ArrayList<String> 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<String> allUserIds, ArrayList<String> 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<String> confirmedUserIds) {
for (String rawUserId : confirmedUserIds) {
UserId userId = OpenPgpUtils.splitUserId(rawUserId);

View File

@@ -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<byte[]> 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<byte[]> 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<byte[]> 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<PGPSignature> 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<PGPUserAttributeSubpacketVector> it = masterPubKey.getUserAttributes();
while (it.hasNext()) {
masterPubKey = PGPPublicKey.removeCertification(masterPubKey, it.next());
}
return masterPubKey;
}
static PGPPublicKey removeAllDirectKeyCerts(PGPPublicKey masterPubKey) {
Iterator<PGPSignature> it = masterPubKey.getSignaturesOfType(PGPSignature.DIRECT_KEY);
while (it.hasNext()) {
masterPubKey = PGPPublicKey.removeCertification(masterPubKey, it.next());
}
return masterPubKey;
}
}

View File

@@ -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;

View File

@@ -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;
}
/**

View File

@@ -0,0 +1,156 @@
/*
* Copyright (C) 2017 Vincent Breitmoser <v.breitmoser@mugenguild.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.provider;
import 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);
}
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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() {
}
}

View File

@@ -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
*
* <pre>
* trust_ids/by_key_id/_
*
* </pre>
*/
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<String, String> 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<String, String> 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);

View File

@@ -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<String> 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);

View File

@@ -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
*
* <pre>
@@ -80,7 +89,9 @@ public class KeychainExternalProvider extends ContentProvider implements SimpleC
* </pre>
*/
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<String, String> 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();
}

View File

@@ -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<Integer> 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;
}

View File

@@ -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<Long> keyIds = new HashSet<>();
ArrayList<String> missingEmails = new ArrayList<>();
ArrayList<String> duplicateEmails = new ArrayList<>();
AutocryptState combinedAutocryptState = null;
if (hasAddresses) {
HashMap<String, UserIdStatus> keyRows = getStatusMapForQueriedAddresses(encryptionAddresses, callingPackageName);
HashMap<String, AddressQueryResult> userIdEntries = getStatusMapForQueriedAddresses(
encryptionAddresses, callingPackageName);
boolean anyKeyNotVerified = false;
for (Entry<String, UserIdStatus> 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<String, UserIdStatus> getStatusMapForQueriedAddresses(String[] encryptionUserIds, String callingPackageName) {
HashMap<String,UserIdStatus> 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<String, AddressQueryResult> getStatusMapForQueriedAddresses(String[] encryptionUserIds, String callingPackageName) {
HashMap<String,AddressQueryResult> 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<Long> 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<Long> keyIds, boolean allKeysConfirmed, KeyIdResultStatus keyIdResultStatus) {
private KeyIdResult(HashSet<Long> keyIds, boolean allKeysConfirmed, KeyIdResultStatus keyIdResultStatus,
AutocryptState combinedAutocryptState) {
mKeySelectionPendingIntent = null;
mUserKeyIds = keyIds;
mAllKeysConfirmed = allKeysConfirmed;
mStatus = keyIdResultStatus;
mExplicitKeyIds = null;
mCombinedAutocryptState = combinedAutocryptState;
}
private KeyIdResult(KeyIdResult keyIdResult, HashSet<Long> 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<Long> encryptKeyIds, boolean allKeysConfirmed) {
return new KeyIdResult(encryptKeyIds, allKeysConfirmed, KeyIdResultStatus.OK);
private KeyIdResult createKeysOkResult(HashSet<Long> 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<Long> selectedKeyIds, ArrayList<String> missingEmails, ArrayList<String> duplicateEmails) {
long[] keyIdsArray = KeyFormattingUtils.getUnboxedLongArray(selectedKeyIds);
PendingIntent selectKeyPendingIntent = apiPendingIntentFactory.createSelectPublicKeyPendingIntent(
data, keyIdsArray, missingEmails, duplicateEmails, false);
private KeyIdResult createDuplicateKeysResult(Intent data, String packageName, ArrayList<String> duplicateEmails) {
PendingIntent selectKeyPendingIntent = apiPendingIntentFactory.createDeduplicatePendingIntent(
packageName, data, duplicateEmails);
return new KeyIdResult(selectKeyPendingIntent, KeyIdResultStatus.DUPLICATE);
}

View File

@@ -0,0 +1,149 @@
/*
* Copyright (C) 2017 Vincent Breitmoser <v.breitmoser@mugenguild.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.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<List<KeyInfo>> {
// 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<KeyInfo> cachedResult;
KeyLoader(Context context, ContentResolver contentResolver, String emailAddress) {
super(context);
this.contentResolver = contentResolver;
this.emailAddress = emailAddress;
}
@Override
public List<KeyInfo> loadInBackground() {
ArrayList<KeyInfo> 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<KeyInfo> 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);
}
}
}

View File

@@ -0,0 +1,331 @@
/*
* Copyright (C) 2017 Vincent Breitmoser <v.breitmoser@mugenguild.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.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<String> 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<KeyInfo> 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<KeyChoiceViewHolder> {
private final LayoutInflater layoutInflater;
private final Resources resources;
private List<KeyInfo> 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<KeyInfo> 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);
}
}
}

View File

@@ -0,0 +1,152 @@
/*
* Copyright (C) 2017 Vincent Breitmoser <v.breitmoser@mugenguild.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.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<List<KeyInfo>> {
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<KeyInfo> 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<List<KeyInfo>> onCreateLoader(int id, Bundle args) {
return new KeyLoader(context, context.getContentResolver(), duplicateAddress);
}
@Override
public void onLoadFinished(Loader<List<KeyInfo>> loader, List<KeyInfo> 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<KeyInfo> data);
void setActiveItem(Integer position);
void setEnableSelectButton(boolean enabled);
}
}

View File

@@ -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<CertSectionedListAdapter.CertCursor, String,
public class CertSectionedListAdapter extends SectionCursorAdapter<CertCursor, String,
CertSectionedListAdapter.CertItemViewHolder, CertSectionedListAdapter.CertSectionViewHolder> {
private CertListListener mListener;
@@ -162,11 +163,11 @@ public class CertSectionedListAdapter extends SectionCursorAdapter<CertSectioned
}
}
public static class CertCursor extends CursorAdapter.AbstractCursor {
public static class CertCursor extends CursorAdapter.SimpleCursor {
public static final String[] CERTS_PROJECTION;
static {
ArrayList<String> 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,

View File

@@ -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<ViewHolder> {
private final Context context;
private final LayoutInflater layoutInflater;
private final boolean isSecret;
private final IdentityClickListener identityClickListener;
private List<IdentityInfo> 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<IdentityInfo> data) {
@@ -75,7 +79,11 @@ public class IdentityAdapter extends RecyclerView.Adapter<ViewHolder> {
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<ViewHolder> {
@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<ViewHolder> {
@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<ViewHolder> {
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<ViewHolder> {
vName.setTypeface(null, Typeface.NORMAL);
vAddress.setTypeface(null, Typeface.NORMAL);
}
}
}
public interface IdentityClickListener {
void onClickIdentity(int position);
void onClickIdentityMore(int position, View anchor);
}
}

View File

@@ -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<KeySectionedListAdapter.KeyListCursor, Character,
@@ -348,12 +354,13 @@ public class KeySectionedListAdapter extends SectionCursorAdapter<KeySectionedLi
public class KeyItemViewHolder extends SectionCursorAdapter.ViewHolder
implements View.OnClickListener, View.OnLongClickListener {
private TextView mMainUserId;
private TextView mMainUserIdRest;
private TextView mCreationDate;
private ImageView mStatus;
private View mSlinger;
private ImageButton mSlingerButton;
private final ImageView mTrustIdIcon;
private final TextView mMainUserId;
private final TextView mMainUserIdRest;
private final TextView mCreationDate;
private final ImageView mStatus;
private final View mSlinger;
private final ImageButton mSlingerButton;
KeyItemViewHolder(View itemView) {
super(itemView);
@@ -364,6 +371,7 @@ public class KeySectionedListAdapter extends SectionCursorAdapter<KeySectionedLi
mSlinger = itemView.findViewById(R.id.key_list_item_slinger_view);
mSlingerButton = (ImageButton) itemView.findViewById(R.id.key_list_item_slinger_button);
mCreationDate = (TextView) itemView.findViewById(R.id.key_list_item_creation);
mTrustIdIcon = (ImageView) itemView.findViewById(R.id.key_list_item_tid_icon);
itemView.setClickable(true);
itemView.setLongClickable(true);
@@ -485,7 +493,23 @@ public class KeySectionedListAdapter extends SectionCursorAdapter<KeySectionedLi
} else {
mCreationDate.setVisibility(View.GONE);
}
}
{ // set icons
List<String> 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<KeySectionedLi
KeychainContract.KeyRings.VERIFIED,
KeychainContract.KeyRings.HAS_ANY_SECRET,
KeychainContract.KeyRings.FINGERPRINT,
KeychainContract.KeyRings.HAS_ENCRYPT
KeychainContract.KeyRings.HAS_ENCRYPT,
KeychainContract.KeyRings.API_KNOWN_TO_PACKAGE_NAMES
));
PROJECTION = arr.toArray(new String[arr.size()]);
@@ -603,6 +628,15 @@ public class KeySectionedListAdapter extends SectionCursorAdapter<KeySectionedLi
int index = getColumnIndexOrThrow(KeychainContract.KeyRings.VERIFIED);
return getInt(index) > 0;
}
public List<String> 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<KeySectionedLi
void onSelectionStateChanged(int selectedCount);
}
private HashMap<String, Drawable> 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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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<List<IdentityInfo>> {
@@ -68,6 +76,7 @@ public class IdentityLoader extends AsyncTaskLoader<List<IdentityInfo>> {
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<List<IdentityInfo>> {
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<List<IdentityInfo>> {
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<IdentityInfo> 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<ResolveInfo> resolveInfos = getContext().getPackageManager().queryIntentActivities(intent, 0);
if (resolveInfos != null && !resolveInfos.isEmpty()) {
return intent;
} else {
return null;
}
}
private static UserIdInfo findUserIdMatchingTrustId(List<IdentityInfo> 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<IdentityInfo> 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<List<IdentityInfo>> {
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);
}
}
}

View File

@@ -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<List<IdentityInfo>>
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<List<IdentityInfo>>
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<List<IdentityInfo>>
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<List<IdentityInfo>>
}
public interface IdentitiesCardListener {
void onIdentityItemClick(int position);
void onClickEditIdentities();
void onClickAddIdentity();
}

View File

@@ -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);
}

View File

@@ -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() {

View File

@@ -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<String, Drawable> 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;
}
}
}

View File

@@ -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<C extends CursorAdapter.AbstractCursor, VH extends RecyclerView.ViewHolder>
public abstract class CursorAdapter<C extends SimpleCursor, VH extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter<VH> {
public static final String TAG = "CursorAdapter";
@@ -58,7 +59,7 @@ public abstract class CursorAdapter<C extends CursorAdapter.AbstractCursor, VH e
/**
* Constructor that allows control over auto-requery. It is recommended
* you not use this, but instead {@link #CursorAdapter(Context, AbstractCursor, int)}.
* you not use this, but instead {@link #CursorAdapter(Context, SimpleCursor, int)}.
* When using this constructor, {@link #FLAG_REGISTER_CONTENT_OBSERVER}
* will always be set.
*
@@ -223,7 +224,7 @@ public abstract class CursorAdapter<C extends CursorAdapter.AbstractCursor, VH e
/**
* Swap in a new Cursor, returning the old Cursor. Unlike
* {@link #changeCursor(AbstractCursor)}, the returned old Cursor is <em>not</em>
* {@link #changeCursor(SimpleCursor)}, the returned old Cursor is <em>not</em>
* closed.
*
* @param newCursor The new cursor to be used.
@@ -312,10 +313,10 @@ public abstract class CursorAdapter<C extends CursorAdapter.AbstractCursor, VH e
}
}
public static abstract class AbstractCursor extends CursorWrapper {
public static class SimpleCursor extends CursorWrapper {
public static final String[] PROJECTION = {"_id"};
public static <T extends AbstractCursor> T wrap(Cursor cursor, Class<T> type) {
public static <T extends SimpleCursor> T wrap(Cursor cursor, Class<T> type) {
if (cursor != null) {
try {
Constructor<T> constructor = type.getConstructor(Cursor.class);
@@ -335,7 +336,7 @@ public abstract class CursorAdapter<C extends CursorAdapter.AbstractCursor, VH e
*
* @param cursor The underlying cursor to wrap.
*/
protected AbstractCursor(Cursor cursor) {
public SimpleCursor(Cursor cursor) {
super(cursor);
mColumnIndices = new HashMap<>(cursor.getColumnCount() * 4 / 3, 0.75f);
}
@@ -376,12 +377,12 @@ public abstract class CursorAdapter<C extends CursorAdapter.AbstractCursor, VH e
}
}
public static class KeyCursor extends AbstractCursor {
public static class KeyCursor extends SimpleCursor {
public static final String[] PROJECTION;
static {
ArrayList<String> 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<C extends CursorAdapter.AbstractCursor, VH e
KeychainContract.KeyRings.CREATION,
KeychainContract.KeyRings.NAME,
KeychainContract.KeyRings.EMAIL,
KeychainContract.KeyRings.COMMENT
KeychainContract.KeyRings.COMMENT,
KeychainContract.KeyRings.API_KNOWN_TO_PACKAGE_NAMES
));
PROJECTION = arr.toArray(new String[arr.size()]);

View File

@@ -19,7 +19,6 @@
package org.sufficientlysecure.keychain.ui.util.adapter;
import android.content.Context;
import android.database.Cursor;
import android.support.v4.util.SparseArrayCompat;
import android.support.v7.widget.RecyclerView;
import android.view.View;
@@ -27,18 +26,16 @@ import android.view.ViewGroup;
import com.tonicartos.superslim.LayoutManager;
import org.sufficientlysecure.keychain.ui.util.adapter.CursorAdapter.SimpleCursor;
import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.Log;
import java.util.Objects;
/**
* @param <T> section type.
* @param <VH> the view holder extending {@code BaseViewHolder<Cursor>} that is bound to the cursor data.
* @param <SH> the view holder extending {@code BaseViewHolder<<T>>} that is bound to the section data.
*/
public abstract class SectionCursorAdapter<C extends CursorAdapter.AbstractCursor, T, VH extends SectionCursorAdapter.ViewHolder,
public abstract class SectionCursorAdapter<C extends SimpleCursor, T, VH extends SectionCursorAdapter.ViewHolder,
SH extends SectionCursorAdapter.ViewHolder> extends CursorAdapter<C, RecyclerView.ViewHolder> {
public static final String TAG = "SectionCursorAdapter";

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM6,9h12v2L6,11L6,9zM14,14L6,14v-2h8v2zM18,8L6,8L6,6h12v2z"/>
</vector>

View File

@@ -0,0 +1,138 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:layout_marginTop="24dp"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:elevation="4dp"
android:background="?attr/colorPrimary"
android:gravity="center_horizontal"
tools:targetApi="lollipop">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher"
/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="24dp"
android:layout_marginRight="24dp"
android:src="@drawable/link_24dp"
/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/icon_client_app"
tools:src="@drawable/apps_k9"
/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical">
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:overScrollMode="ifContentScrolls"
tools:ignore="UselessParent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingLeft="24dp"
android:paddingRight="24dp"
android:paddingTop="24dp"
android:paddingBottom="16dp"
>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:textAppearanceLarge"
android:text="Multiple candidate keys"
android:id="@+id/dialog_title"
/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:orientation="horizontal"
>
<TextView
android:id="@+id/select_key_item_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="valodim@mugenguild.com"
android:textAppearance="?android:attr/textAppearanceMedium" />
</LinearLayout>
<TextView
android:id="@+id/select_identity_key_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginBottom="20dp"
android:gravity="center_vertical"
android:textAppearance="?android:attr/textAppearanceSmall"
android:text="There are multiple keys that could be used to encrypt to this address. Please choose from the list:"
/>
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/duplicate_key_list"
/>
</LinearLayout>
</ScrollView>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end"
android:padding="8dp"
style="?buttonBarStyle">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/button_cancel"
android:id="@+id/button_cancel"
style="?buttonBarButtonStyle"
/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Select"
android:id="@+id/button_select"
android:enabled="false"
style="?buttonBarButtonStyle" />
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="6dp"
android:paddingBottom="6dp"
android:gravity="center_vertical"
android:orientation="horizontal"
tools:layout_marginTop="30dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginRight="12dp"
android:layout_marginEnd="12dp"
android:id="@+id/key_list_item_icon"
tools:tint="@color/md_grey_600"
tools:src="@drawable/apps_k9"
/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/key_list_item_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="@string/label_main_user_id"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:id="@+id/key_list_item_creation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:textAppearance="?android:attr/textAppearanceSmall"
tools:visibility="visible"
tools:text="Created on 10/10/2010 10:00" />
</LinearLayout>
</LinearLayout>

View File

@@ -2,7 +2,6 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/listPreferredItemHeight"
@@ -11,7 +10,15 @@
android:orientation="horizontal"
android:descendantFocusability="blocksDescendants"
android:background="@drawable/list_item_ripple"
android:focusable="false">
android:focusable="false"
tools:layout_marginTop="30dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_margin="4dp"
android:id="@+id/key_list_item_tid_icon"
tools:src="@drawable/apps_k9" />
<LinearLayout
android:id="@+id/key_list_item_data"
@@ -24,8 +31,7 @@
android:paddingLeft="8dp"
android:paddingRight="4dp"
android:paddingTop="4dp"
android:paddingBottom="4dp"
tools:visibility="gone">
android:paddingBottom="4dp">
<TextView
android:id="@+id/key_list_item_name"
@@ -60,7 +66,8 @@
android:layout_width="wrap_content"
android:layout_height="?android:attr/listPreferredItemHeight"
android:layout_gravity="center_vertical"
android:orientation="horizontal">
android:orientation="horizontal"
tools:visibility="gone">
<View
android:layout_width="1dip"

View File

@@ -6,6 +6,7 @@
android:minHeight="?android:attr/listPreferredItemHeight"
android:orientation="horizontal"
android:singleLine="true"
android:background="?selectableItemBackground"
tools:showIn="@layout/linked_id_view_fragment">
<ImageView

View File

@@ -3,7 +3,8 @@
xmlns:tools="http://schemas.android.com/tools"
tools:parentTag="LinearLayout"
tools:layout_width="match_parent"
tools:layout_height="match_parent">
tools:layout_height="match_parent"
tools:orientation="vertical">
<LinearLayout
android:orientation="horizontal"

View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/subkey_status_card_content" />
</LinearLayout>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<ImageView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_margin="4dp"
tools:src="@drawable/apps_k9" />

View File

@@ -22,7 +22,8 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:orientation="vertical"
android:animateLayoutChanges="true">
<TextView
style="@style/CardViewHeader"

View File

@@ -3,31 +3,61 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center_vertical"
android:minHeight="?android:attr/listPreferredItemHeight"
android:paddingLeft="8dp"
android:paddingTop="4dp"
android:paddingBottom="4dp">
android:orientation="horizontal"
android:maxLines="1"
android:padding="8dp"
android:background="?selectableItemBackground"
>
<TextView
android:id="@+id/user_id_item_address"
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center_vertical"
android:padding="8dp"
android:id="@+id/trust_id_app_icon"
tools:src="@drawable/apps_k9"/>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_gravity="center_vertical">
<TextView
android:id="@+id/user_id_item_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="alice@example.com"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:id="@+id/user_id_item_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="Alice"
android:textAppearance="?android:attr/textAppearanceSmall" />
<TextView
android:id="@+id/user_id_item_comment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="comment"
tools:visibility="gone"
android:textAppearance="?android:attr/textAppearanceSmall" />
</LinearLayout>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="alice@example.com"
android:textAppearance="?android:attr/textAppearanceMedium" />
android:layout_gravity="center_vertical"
android:padding="8dp"
android:id="@+id/user_id_item_more"
android:background="?selectableItemBackground"
android:src="@drawable/ic_more_vert_black_24dp"
android:visibility="gone"
tools:visibility="visible"
/>
<TextView
android:id="@+id/user_id_item_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="Alice"
android:textAppearance="?android:attr/textAppearanceSmall" />
<TextView
android:id="@+id/user_id_item_comment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="comment"
android:textAppearance="?android:attr/textAppearanceSmall" />
</LinearLayout>

View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="?selectableItemBackground"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?android:attr/listPreferredItemHeight"
android:orientation="horizontal"
android:maxLines="1"
android:padding="8dp"
>
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center_vertical"
android:padding="8dp"
android:id="@+id/trust_id_app_icon"
tools:src="@drawable/apps_k9"/>
<TextView
android:layout_height="wrap_content"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:id="@+id/trust_id_name"
android:textAppearance="?android:attr/textAppearanceMedium"
tools:text="alice@example.com"
/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:padding="8dp"
android:id="@+id/user_id_item_more"
android:background="?selectableItemBackground"
android:src="@drawable/ic_chat_black_24dp"
android:visibility="gone"
tools:visibility="visible"
/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/trust_id_button_bar"
android:visibility="gone"
tools:visibility="visible"
style="?android:buttonBarStyle">
<Button
android:layout_width="wrap_content"
android:layout_height="36dp"
android:id="@+id/view_key_card_user_ids_edit"
android:text="Forget"
android:textColor="@color/card_view_button"
style="?android:attr/buttonBarButtonStyle"
/>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/autocrypt_forget"
android:title="@string/identity_context_forget"
android:icon="@drawable/ic_delete_grey_24dp"
/>
</menu>

View File

@@ -1886,4 +1886,6 @@
<string name="dialog_insecure_button_view_key">View Key</string>
<string name="dialog_insecure_button_ok">Got it</string>
<string name="identity_context_forget">Forget</string>
</resources>

View File

@@ -3,6 +3,7 @@ package org.sufficientlysecure.keychain.remote;
import java.security.AccessControlException;
import java.util.Collections;
import java.util.Date;
import android.content.ContentResolver;
import android.content.pm.PackageInfo;
@@ -14,13 +15,17 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.res.builder.RobolectricPackageManager;
import org.robolectric.shadows.ShadowLog;
import org.sufficientlysecure.keychain.KeychainTestRunner;
import org.sufficientlysecure.keychain.operations.CertifyOperation;
import org.sufficientlysecure.keychain.operations.results.CertifyResult;
import org.sufficientlysecure.keychain.operations.results.SaveKeyringResult;
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
import org.sufficientlysecure.keychain.provider.ApiDataAccessObject;
import org.sufficientlysecure.keychain.provider.AutocryptPeerDataAccessObject;
import org.sufficientlysecure.keychain.provider.KeyWritableRepository;
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.KeyRepositorySaveTest;
import org.sufficientlysecure.keychain.service.CertifyActionsParcel;
@@ -43,6 +48,7 @@ public class KeychainExternalProviderTest {
static final String USER_ID_SEC_1 = "twi <twi-sec@openkeychain.org>";
static final long KEY_ID_SECRET = 0x5D4DA4423C39122FL;
static final long KEY_ID_PUBLIC = 0x9A282CE2AB44A382L;
public static final String AUTOCRYPT_PEER = "tid";
KeyWritableRepository databaseInteractor =
@@ -50,10 +56,13 @@ public class KeychainExternalProviderTest {
ContentResolver contentResolver = RuntimeEnvironment.application.getContentResolver();
ApiPermissionHelper apiPermissionHelper;
ApiDataAccessObject apiDao;
AutocryptPeerDataAccessObject autocryptPeerDao;
@Before
public void setUp() throws Exception {
ShadowLog.stream = System.out;
RobolectricPackageManager rpm = (RobolectricPackageManager) RuntimeEnvironment.getPackageManager();
rpm.setPackagesForUid(0, PACKAGE_NAME);
PackageInfo packageInfo = new PackageInfo();
@@ -63,6 +72,7 @@ public class KeychainExternalProviderTest {
apiDao = new ApiDataAccessObject(RuntimeEnvironment.application);
apiPermissionHelper = new ApiPermissionHelper(RuntimeEnvironment.application, apiDao);
autocryptPeerDao = new AutocryptPeerDataAccessObject(RuntimeEnvironment.application, PACKAGE_NAME);
apiDao.insertApiApp(new AppSettings(PACKAGE_NAME, PACKAGE_SIGNATURE));
}
@@ -78,15 +88,6 @@ public class KeychainExternalProviderTest {
);
}
@Test(expected = AccessControlException.class)
public void testPermission__withExplicitPackage() throws Exception {
contentResolver.query(
EmailStatus.CONTENT_URI.buildUpon().appendPath("fake_pkg").build(),
new String[] { EmailStatus.EMAIL_ADDRESS, EmailStatus.EMAIL_ADDRESS, EmailStatus.USER_ID },
null, new String [] { }, null
);
}
@Test(expected = AccessControlException.class)
public void testPermission__withWrongPackageCert() throws Exception {
apiDao.deleteApiApp(PACKAGE_NAME);
@@ -100,7 +101,7 @@ public class KeychainExternalProviderTest {
}
@Test
public void testQuery__withNonExistentAddress() throws Exception {
public void testEmailStatus_withNonExistentAddress() throws Exception {
Cursor cursor = contentResolver.query(
EmailStatus.CONTENT_URI, new String[] {
EmailStatus.EMAIL_ADDRESS, EmailStatus.USER_ID_STATUS, EmailStatus.USER_ID },
@@ -110,12 +111,12 @@ public class KeychainExternalProviderTest {
assertNotNull(cursor);
assertTrue(cursor.moveToFirst());
assertEquals(MAIL_ADDRESS_1, cursor.getString(0));
assertEquals(0, cursor.getInt(1));
assertEquals(KeychainExternalContract.KEY_STATUS_UNAVAILABLE, cursor.getInt(1));
assertTrue(cursor.isNull(2));
}
@Test
public void testQuery() throws Exception {
public void testEmailStatus() throws Exception {
insertPublicKeyringFrom("/test-keys/testring.pub");
Cursor cursor = contentResolver.query(
@@ -127,13 +128,13 @@ public class KeychainExternalProviderTest {
assertNotNull(cursor);
assertTrue(cursor.moveToFirst());
assertEquals(MAIL_ADDRESS_1, cursor.getString(0));
assertEquals(1, cursor.getInt(1));
assertEquals(KeychainExternalContract.KEY_STATUS_UNVERIFIED, cursor.getInt(1));
assertEquals("twi <twi@openkeychain.org>", cursor.getString(2));
assertFalse(cursor.moveToNext());
}
@Test
public void testQuery__multiple() throws Exception {
public void testEmailStatus_multiple() throws Exception {
insertPublicKeyringFrom("/test-keys/testring.pub");
Cursor cursor = contentResolver.query(
@@ -145,17 +146,17 @@ public class KeychainExternalProviderTest {
assertNotNull(cursor);
assertTrue(cursor.moveToNext());
assertEquals(MAIL_ADDRESS_2, cursor.getString(0));
assertEquals(0, cursor.getInt(1));
assertEquals(KeychainExternalContract.KEY_STATUS_UNAVAILABLE, cursor.getInt(1));
assertTrue(cursor.isNull(2));
assertTrue(cursor.moveToNext());
assertEquals(MAIL_ADDRESS_1, cursor.getString(0));
assertEquals(1, cursor.getInt(1));
assertEquals(KeychainExternalContract.KEY_STATUS_UNVERIFIED, cursor.getInt(1));
assertEquals("twi <twi@openkeychain.org>", cursor.getString(2));
assertFalse(cursor.moveToNext());
}
@Test
public void testQuery__withSecretKey() throws Exception {
public void testEmailStatus_withSecretKey() throws Exception {
insertSecretKeyringFrom("/test-keys/testring.sec");
Cursor cursor = contentResolver.query(
@@ -173,7 +174,7 @@ public class KeychainExternalProviderTest {
}
@Test
public void testQuery__withConfirmedKey() throws Exception {
public void testEmailStatus_withConfirmedKey() throws Exception {
insertSecretKeyringFrom("/test-keys/testring.sec");
insertPublicKeyringFrom("/test-keys/testring.pub");
@@ -181,18 +182,197 @@ public class KeychainExternalProviderTest {
Cursor cursor = contentResolver.query(
EmailStatus.CONTENT_URI, new String[] {
EmailStatus.EMAIL_ADDRESS, EmailStatus.USER_ID_STATUS, EmailStatus.USER_ID },
EmailStatus.EMAIL_ADDRESS, EmailStatus.USER_ID, EmailStatus.USER_ID_STATUS},
null, new String [] { MAIL_ADDRESS_1 }, null
);
assertNotNull(cursor);
assertTrue(cursor.moveToFirst());
assertEquals(MAIL_ADDRESS_1, cursor.getString(0));
assertEquals(USER_ID_1, cursor.getString(2));
assertEquals(2, cursor.getInt(1));
assertEquals(USER_ID_1, cursor.getString(1));
assertEquals(KeychainExternalContract.KEY_STATUS_VERIFIED, cursor.getInt(2));
assertFalse(cursor.moveToNext());
}
@Test
public void testAutocryptStatus_autocryptPeer_withUnconfirmedKey() throws Exception {
insertSecretKeyringFrom("/test-keys/testring.sec");
insertPublicKeyringFrom("/test-keys/testring.pub");
autocryptPeerDao.updateToAvailableState("tid", new Date(), KEY_ID_PUBLIC);
Cursor cursor = contentResolver.query(
AutocryptStatus.CONTENT_URI, new String[] {
AutocryptStatus.ADDRESS, AutocryptStatus.UID_KEY_STATUS, AutocryptStatus.UID_ADDRESS,
AutocryptStatus.AUTOCRYPT_KEY_STATUS, AutocryptStatus.AUTOCRYPT_MASTER_KEY_ID,
AutocryptStatus.AUTOCRYPT_PEER_STATE
},
null, new String [] { AUTOCRYPT_PEER }, null
);
assertNotNull(cursor);
assertTrue(cursor.moveToFirst());
assertEquals("tid", cursor.getString(0));
assertTrue(cursor.isNull(1));
assertEquals(null, cursor.getString(2));
assertEquals(KeychainExternalContract.KEY_STATUS_UNVERIFIED, cursor.getInt(3));
assertEquals(KEY_ID_PUBLIC, cursor.getLong(4));
assertEquals(AutocryptStatus.AUTOCRYPT_PEER_AVAILABLE, cursor.getInt(5));
assertFalse(cursor.moveToNext());
}
@Test
public void testAutocryptStatus_available_withConfirmedKey() throws Exception {
insertSecretKeyringFrom("/test-keys/testring.sec");
insertPublicKeyringFrom("/test-keys/testring.pub");
autocryptPeerDao.updateToAvailableState("tid", new Date(), KEY_ID_PUBLIC);
certifyKey(KEY_ID_SECRET, KEY_ID_PUBLIC, USER_ID_1);
Cursor cursor = contentResolver.query(
AutocryptStatus.CONTENT_URI, new String[] {
AutocryptStatus.ADDRESS, AutocryptStatus.UID_KEY_STATUS, AutocryptStatus.UID_ADDRESS,
AutocryptStatus.AUTOCRYPT_KEY_STATUS, AutocryptStatus.AUTOCRYPT_PEER_STATE },
null, new String [] { AUTOCRYPT_PEER }, null
);
assertNotNull(cursor);
assertTrue(cursor.moveToFirst());
assertEquals("tid", cursor.getString(0));
assertEquals(KeychainExternalContract.KEY_STATUS_UNAVAILABLE, cursor.getInt(1));
assertEquals(null, cursor.getString(2));
assertEquals(KeychainExternalContract.KEY_STATUS_VERIFIED, cursor.getInt(3));
assertEquals(AutocryptStatus.AUTOCRYPT_PEER_AVAILABLE, cursor.getInt(4));
assertFalse(cursor.moveToNext());
}
@Test
public void testAutocryptStatus_noData() throws Exception {
Cursor cursor = contentResolver.query(
AutocryptStatus.CONTENT_URI, new String[] {
AutocryptStatus.ADDRESS, AutocryptStatus.UID_KEY_STATUS, AutocryptStatus.UID_ADDRESS,
AutocryptStatus.AUTOCRYPT_KEY_STATUS, AutocryptStatus.AUTOCRYPT_PEER_STATE },
null, new String [] { AUTOCRYPT_PEER }, null
);
assertNotNull(cursor);
assertTrue(cursor.moveToFirst());
assertEquals("tid", cursor.getString(0));
assertEquals(KeychainExternalContract.KEY_STATUS_UNAVAILABLE, cursor.getInt(1));
assertEquals(null, cursor.getString(2));
assertEquals(KeychainExternalContract.KEY_STATUS_UNAVAILABLE, cursor.getInt(3));
assertEquals(AutocryptStatus.AUTOCRYPT_PEER_RESET, cursor.getInt(4));
assertFalse(cursor.moveToNext());
}
@Test
public void testAutocryptStatus_afterDelete() throws Exception {
insertSecretKeyringFrom("/test-keys/testring.sec");
insertPublicKeyringFrom("/test-keys/testring.pub");
autocryptPeerDao.updateToGossipState("tid", new Date(), KEY_ID_PUBLIC);
autocryptPeerDao.delete("tid");
Cursor cursor = contentResolver.query(
AutocryptStatus.CONTENT_URI, new String[] {
AutocryptStatus.ADDRESS, AutocryptStatus.UID_KEY_STATUS, AutocryptStatus.UID_ADDRESS,
AutocryptStatus.AUTOCRYPT_KEY_STATUS, AutocryptStatus.AUTOCRYPT_PEER_STATE },
null, new String [] { AUTOCRYPT_PEER }, null
);
assertNotNull(cursor);
assertTrue(cursor.moveToFirst());
assertEquals("tid", cursor.getString(0));
assertEquals(KeychainExternalContract.KEY_STATUS_UNAVAILABLE, cursor.getInt(1));
assertEquals(null, cursor.getString(2));
assertEquals(KeychainExternalContract.KEY_STATUS_UNAVAILABLE, cursor.getInt(3));
assertEquals(AutocryptStatus.AUTOCRYPT_PEER_RESET, cursor.getInt(4));
assertFalse(cursor.moveToNext());
}
@Test
public void testAutocryptStatus_stateGossip() throws Exception {
insertSecretKeyringFrom("/test-keys/testring.sec");
insertPublicKeyringFrom("/test-keys/testring.pub");
autocryptPeerDao.updateToGossipState("tid", new Date(), KEY_ID_PUBLIC);
certifyKey(KEY_ID_SECRET, KEY_ID_PUBLIC, USER_ID_1);
Cursor cursor = contentResolver.query(
AutocryptStatus.CONTENT_URI, new String[] {
AutocryptStatus.ADDRESS, AutocryptStatus.UID_KEY_STATUS, AutocryptStatus.UID_ADDRESS,
AutocryptStatus.AUTOCRYPT_KEY_STATUS, AutocryptStatus.AUTOCRYPT_PEER_STATE },
null, new String [] { AUTOCRYPT_PEER }, null
);
assertNotNull(cursor);
assertTrue(cursor.moveToFirst());
assertEquals("tid", cursor.getString(0));
assertEquals(KeychainExternalContract.KEY_STATUS_UNAVAILABLE, cursor.getInt(1));
assertEquals(null, cursor.getString(2));
assertEquals(KeychainExternalContract.KEY_STATUS_VERIFIED, cursor.getInt(3));
assertEquals(AutocryptStatus.AUTOCRYPT_PEER_GOSSIP, cursor.getInt(4));
assertFalse(cursor.moveToNext());
}
@Test
public void testAutocryptStatus_stateSelected() throws Exception {
insertSecretKeyringFrom("/test-keys/testring.sec");
insertPublicKeyringFrom("/test-keys/testring.pub");
autocryptPeerDao.updateToSelectedState("tid", KEY_ID_PUBLIC);
certifyKey(KEY_ID_SECRET, KEY_ID_PUBLIC, USER_ID_1);
Cursor cursor = contentResolver.query(
AutocryptStatus.CONTENT_URI, new String[] {
AutocryptStatus.ADDRESS, AutocryptStatus.UID_KEY_STATUS, AutocryptStatus.UID_ADDRESS,
AutocryptStatus.AUTOCRYPT_KEY_STATUS, AutocryptStatus.AUTOCRYPT_PEER_STATE },
null, new String [] { AUTOCRYPT_PEER }, null
);
assertNotNull(cursor);
assertTrue(cursor.moveToFirst());
assertEquals("tid", cursor.getString(0));
assertEquals(KeychainExternalContract.KEY_STATUS_UNAVAILABLE, cursor.getInt(1));
assertEquals(null, cursor.getString(2));
assertEquals(KeychainExternalContract.KEY_STATUS_VERIFIED, cursor.getInt(3));
assertEquals(AutocryptStatus.AUTOCRYPT_PEER_SELECTED, cursor.getInt(4));
assertFalse(cursor.moveToNext());
}
@Test
public void testAutocryptStatus_withConfirmedKey() throws Exception {
insertSecretKeyringFrom("/test-keys/testring.sec");
insertPublicKeyringFrom("/test-keys/testring.pub");
certifyKey(KEY_ID_SECRET, KEY_ID_PUBLIC, USER_ID_1);
Cursor cursor = contentResolver.query(
AutocryptStatus.CONTENT_URI, new String[] {
AutocryptStatus.ADDRESS, AutocryptStatus.UID_KEY_STATUS, AutocryptStatus.UID_ADDRESS,
AutocryptStatus.AUTOCRYPT_PEER_STATE },
null, new String [] { MAIL_ADDRESS_1 }, null
);
assertNotNull(cursor);
assertTrue(cursor.moveToFirst());
assertEquals(MAIL_ADDRESS_1, cursor.getString(0));
assertEquals(KeychainExternalContract.KEY_STATUS_VERIFIED, cursor.getInt(1));
assertEquals(USER_ID_1, cursor.getString(2));
assertEquals(AutocryptStatus.AUTOCRYPT_PEER_RESET, cursor.getInt(3));
assertFalse(cursor.moveToNext());
}
@Test(expected = AccessControlException.class)
public void testPermission__withExplicitPackage() throws Exception {
contentResolver.query(
AutocryptStatus.CONTENT_URI.buildUpon().appendPath("fake_pkg").build(),
new String[] { AutocryptStatus.ADDRESS },
null, new String [] { }, null
);
}
private void certifyKey(long secretMasterKeyId, long publicMasterKeyId, String userId) {
CertifyActionsParcel.Builder certifyActionsParcel = CertifyActionsParcel.builder(secretMasterKeyId);
certifyActionsParcel.addAction(

View File

@@ -74,7 +74,7 @@ public class OpenPgpServiceKeyIdExtractorTest {
assertArrayEqualsSorted(KEY_IDS, keyIdResult.getKeyIds());
}
@Test
@Test(expected = IllegalStateException.class)
public void returnKeyIdsFromIntent__withUserIds__withEmptyQueryResult() throws Exception {
Intent intent = new Intent();
intent.putExtra(OpenPgpApi.EXTRA_USER_IDS, USER_IDS);
@@ -82,15 +82,10 @@ public class OpenPgpServiceKeyIdExtractorTest {
setupContentResolverResult();
PendingIntent pendingIntent = mock(PendingIntent.class);
setupPendingIntentFactoryResult(pendingIntent);
setupSelectPubkeyPendingIntentFactoryResult(pendingIntent);
KeyIdResult keyIdResult = openPgpServiceKeyIdExtractor.returnKeyIdsFromIntent(intent, false,
BuildConfig.APPLICATION_ID);
assertEquals(KeyIdResultStatus.NO_KEYS, keyIdResult.getStatus());
assertTrue(keyIdResult.hasKeySelectionPendingIntent());
openPgpServiceKeyIdExtractor.returnKeyIdsFromIntent(intent, false, BuildConfig.APPLICATION_ID);
}
@Test
@@ -99,7 +94,7 @@ public class OpenPgpServiceKeyIdExtractorTest {
intent.putExtra(OpenPgpApi.EXTRA_USER_IDS, new String[] { });
PendingIntent pendingIntent = mock(PendingIntent.class);
setupPendingIntentFactoryResult(pendingIntent);
setupSelectPubkeyPendingIntentFactoryResult(pendingIntent);
KeyIdResult keyIdResult = openPgpServiceKeyIdExtractor.returnKeyIdsFromIntent(intent, false,
@@ -117,7 +112,7 @@ public class OpenPgpServiceKeyIdExtractorTest {
intent.putExtra(OpenPgpApi.EXTRA_USER_IDS, new String[0]);
PendingIntent pendingIntent = mock(PendingIntent.class);
setupPendingIntentFactoryResult(pendingIntent);
setupSelectPubkeyPendingIntentFactoryResult(pendingIntent);
KeyIdResult keyIdResult = openPgpServiceKeyIdExtractor.returnKeyIdsFromIntent(intent, false,
BuildConfig.APPLICATION_ID);
@@ -135,7 +130,7 @@ public class OpenPgpServiceKeyIdExtractorTest {
setupContentResolverResult();
PendingIntent pendingIntent = mock(PendingIntent.class);
setupPendingIntentFactoryResult(pendingIntent);
setupSelectPubkeyPendingIntentFactoryResult(pendingIntent);
KeyIdResult keyIdResult = openPgpServiceKeyIdExtractor.returnKeyIdsFromIntent(intent, true,
@@ -152,7 +147,7 @@ public class OpenPgpServiceKeyIdExtractorTest {
Intent intent = new Intent();
intent.putExtra(OpenPgpApi.EXTRA_USER_IDS, USER_IDS);
setupContentResolverResult(USER_IDS, new Long[] { 123L, 234L }, new int[] { 0, 0 });
setupContentResolverResult(USER_IDS, new Long[] { 123L, 234L }, new int[] { 0, 0 }, new int[] { 1, 1, 1 });
KeyIdResult keyIdResult = openPgpServiceKeyIdExtractor.returnKeyIdsFromIntent(intent, false,
@@ -170,11 +165,11 @@ public class OpenPgpServiceKeyIdExtractorTest {
intent.putExtra(OpenPgpApi.EXTRA_USER_IDS, USER_IDS);
setupContentResolverResult(new String[] {
USER_IDS[0], USER_IDS[0], USER_IDS[1]
}, new Long[] { 123L, 345L, 234L }, new int[] { 0, 0, 0 });
USER_IDS[0], USER_IDS[1]
}, new Long[] { 123L, 234L }, new int[] { 0, 0 }, new int[] { 2, 1 });
PendingIntent pendingIntent = mock(PendingIntent.class);
setupPendingIntentFactoryResult(pendingIntent);
setupDeduplicatePendingIntentFactoryResult(pendingIntent);
KeyIdResult keyIdResult = openPgpServiceKeyIdExtractor.returnKeyIdsFromIntent(intent, false,
@@ -190,10 +185,10 @@ public class OpenPgpServiceKeyIdExtractorTest {
Intent intent = new Intent();
intent.putExtra(OpenPgpApi.EXTRA_USER_IDS, USER_IDS);
setupContentResolverResult(USER_IDS, new Long[] { null, 234L }, new int[] { 0, 0 });
setupContentResolverResult(USER_IDS, new Long[] { null, 234L }, new int[] { 0, 0 }, new int[] { 0, 1 });
PendingIntent pendingIntent = mock(PendingIntent.class);
setupPendingIntentFactoryResult(pendingIntent);
setupSelectPubkeyPendingIntentFactoryResult(pendingIntent);
KeyIdResult keyIdResult = openPgpServiceKeyIdExtractor.returnKeyIdsFromIntent(intent, false,
@@ -205,16 +200,16 @@ public class OpenPgpServiceKeyIdExtractorTest {
}
private void setupContentResolverResult() {
MatrixCursor resultCursor = new MatrixCursor(OpenPgpServiceKeyIdExtractor.PROJECTION_KEY_SEARCH);
MatrixCursor resultCursor = new MatrixCursor(OpenPgpServiceKeyIdExtractor.PROJECTION_MAIL_STATUS);
when(contentResolver.query(
any(Uri.class), any(String[].class), any(String.class), any(String[].class), any(String.class)))
.thenReturn(resultCursor);
}
private void setupContentResolverResult(String[] userIds, Long[] resultKeyIds, int[] verified) {
MatrixCursor resultCursor = new MatrixCursor(OpenPgpServiceKeyIdExtractor.PROJECTION_KEY_SEARCH);
private void setupContentResolverResult(String[] userIds, Long[] resultKeyIds, int[] verified, int[] candidates) {
MatrixCursor resultCursor = new MatrixCursor(OpenPgpServiceKeyIdExtractor.PROJECTION_MAIL_STATUS);
for (int i = 0; i < userIds.length; i++) {
resultCursor.addRow(new Object[] { userIds[i], resultKeyIds[i], verified[i] });
resultCursor.addRow(new Object[] { userIds[i], resultKeyIds[i], verified[i], candidates[i], null, null, null });
}
when(contentResolver.query(
@@ -222,12 +217,16 @@ public class OpenPgpServiceKeyIdExtractorTest {
.thenReturn(resultCursor);
}
private void setupPendingIntentFactoryResult(PendingIntent pendingIntent) {
private void setupSelectPubkeyPendingIntentFactoryResult(PendingIntent pendingIntent) {
when(apiPendingIntentFactory.createSelectPublicKeyPendingIntent(
any(Intent.class), any(long[].class), any(ArrayList.class), any(ArrayList.class), any(Boolean.class)))
.thenReturn(pendingIntent);
}
private void setupDeduplicatePendingIntentFactoryResult(PendingIntent pendingIntent) {
when(apiPendingIntentFactory.createDeduplicatePendingIntent(
any(String.class), any(Intent.class), any(ArrayList.class))).thenReturn(pendingIntent);
}
private static void assertArrayEqualsSorted(long[] a, long[] b) {
long[] tmpA = Arrays.copyOf(a, a.length);