From b209835285cef452352dc03233c28bc55e9b90c1 Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Mon, 26 Feb 2024 15:02:07 +0100 Subject: [PATCH] Return full result set from external provider autocrypt_status query --- .../keychain/KeychainDatabase.java | 18 +++- .../keychain/daos/AutocryptPeerDao.java | 9 +- .../keychain/daos/UserIdDao.java | 20 +++-- .../keychain/remote/AutocryptInteractor.java | 21 +++-- .../remote/KeychainExternalProvider.java | 83 ++++++++----------- .../keychain/AutocryptPeers.sq | 28 ++++--- .../remote/KeychainExternalProviderTest.java | 54 ++++++++++++ 7 files changed, 159 insertions(+), 74 deletions(-) diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainDatabase.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainDatabase.java index 2574ae90d..828f850e9 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainDatabase.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainDatabase.java @@ -48,7 +48,7 @@ import timber.log.Timber; */ public class KeychainDatabase { private static final String DATABASE_NAME = "openkeychain.db"; - private static final int DATABASE_VERSION = 35; + private static final int DATABASE_VERSION = 36; private final SupportSQLiteOpenHelper supportSQLiteOpenHelper; private final Database sqldelightDatabase; @@ -141,6 +141,7 @@ public class KeychainDatabase { } switch (oldVersion) { case 34: + case 35: // nothing } // recreate the unified key view on any upgrade @@ -191,7 +192,20 @@ public class KeychainDatabase { FROM validKeys WHERE rank = 0; """); - + db.execSQL("DROP VIEW IF EXISTS autocryptKeyStatus"); + db.execSQL(""" + CREATE VIEW autocryptKeyStatus AS + SELECT autocryptPeer.*, + (CASE WHEN ac_key.expiry IS NULL THEN 0 WHEN ac_key.expiry > strftime('%s', 'now') THEN 0 ELSE 1 END) AS key_is_expired_int, + (CASE WHEN gossip_key.expiry IS NULL THEN 0 WHEN gossip_key.expiry > strftime('%s', 'now') THEN 0 ELSE 1 END) AS gossip_key_is_expired_int, + ac_key.is_revoked AS key_is_revoked, + gossip_key.is_revoked AS gossip_key_is_revoked, + EXISTS (SELECT * FROM certs WHERE certs.master_key_id = autocryptPeer.master_key_id AND verified = 1) AS key_is_verified, + EXISTS (SELECT * FROM certs WHERE certs.master_key_id = autocryptPeer.gossip_master_key_id AND verified = 1) AS gossip_key_is_verified + FROM autocrypt_peers AS autocryptPeer + LEFT JOIN keys AS ac_key ON (ac_key.master_key_id = autocryptPeer.master_key_id AND ac_key.rank = 0) + LEFT JOIN keys AS gossip_key ON (gossip_key.master_key_id = gossip_master_key_id AND gossip_key.rank = 0); + """); } private void onDowngrade() { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/AutocryptPeerDao.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/AutocryptPeerDao.java index 4e6e57391..c0d90fda7 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/AutocryptPeerDao.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/AutocryptPeerDao.java @@ -26,10 +26,10 @@ import java.util.List; import android.content.Context; import androidx.annotation.Nullable; +import org.sufficientlysecure.keychain.AutocryptKeyStatus; import org.sufficientlysecure.keychain.AutocryptPeersQueries; import org.sufficientlysecure.keychain.Autocrypt_peers; import org.sufficientlysecure.keychain.KeychainDatabase; -import org.sufficientlysecure.keychain.SelectAutocryptKeyStatus; import org.sufficientlysecure.keychain.SelectMasterKeyIdByIdentifier; import org.sufficientlysecure.keychain.model.GossipOrigin; @@ -71,12 +71,17 @@ public class AutocryptPeerDao extends AbstractDao { .executeAsList(); } - public List getAutocryptKeyStatus(String packageName, + public List getAutocryptKeyStatus(String packageName, String[] autocryptIds) { return autocryptPeersQueries.selectAutocryptKeyStatus(packageName, Arrays.asList(autocryptIds)).executeAsList(); } + public List getAutocryptKeyStatusLike(String packageName, + String query) { + return autocryptPeersQueries.selectAutocryptKeyStatusLike(packageName, query).executeAsList(); + } + private void ensureAutocryptPeerExists(String packageName, String autocryptId) { autocryptPeersQueries.insertPeer(packageName, autocryptId); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/UserIdDao.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/UserIdDao.java index b582445f4..61bc47670 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/UserIdDao.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/daos/UserIdDao.java @@ -34,11 +34,6 @@ public class UserIdDao extends AbstractDao { .executeAsList(); } - public UidStatus getUidStatusByEmailLike(String emailLike) { - return getDatabase().getUserPacketsQueries().selectUserIdStatusByEmailLike(emailLike) - .executeAsOneOrNull(); - } - public Map getUidStatusByEmail(String... emails) { Query q = getDatabase().getUserPacketsQueries() .selectUserIdStatusByEmail(Arrays.asList(emails)); @@ -54,6 +49,21 @@ public class UserIdDao extends AbstractDao { return result; } + public Map getUidStatusByEmailLike(String query) { + Query q = getDatabase().getUserPacketsQueries() + .selectUserIdStatusByEmailLike(query); + Map result = new HashMap<>(); + try (SqlCursor cursor = q.execute()) { + while (cursor.next()) { + UidStatus item = q.getMapper().invoke(cursor); + result.put(item.getEmail(), item); + } + } catch (IOException e) { + // oops + } + return result; + } + private List getLongArrayAsList(long[] longList) { Long[] longs = new Long[longList.length]; int i = 0; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/AutocryptInteractor.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/AutocryptInteractor.java index c5867858e..2c3322d19 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/AutocryptInteractor.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/AutocryptInteractor.java @@ -13,9 +13,9 @@ import android.text.format.DateUtils; import androidx.annotation.Nullable; import org.openintents.openpgp.AutocryptPeerUpdate; import org.openintents.openpgp.AutocryptPeerUpdate.PreferEncrypt; +import org.sufficientlysecure.keychain.AutocryptKeyStatus; import org.sufficientlysecure.keychain.Autocrypt_peers; import org.sufficientlysecure.keychain.Constants; -import org.sufficientlysecure.keychain.SelectAutocryptKeyStatus; import org.sufficientlysecure.keychain.daos.AutocryptPeerDao; import org.sufficientlysecure.keychain.daos.KeyWritableRepository; import org.sufficientlysecure.keychain.model.GossipOrigin; @@ -150,7 +150,18 @@ public class AutocryptInteractor { public Map determineAutocryptRecommendations(String... autocryptIds) { Map result = new HashMap<>(autocryptIds.length); - for (SelectAutocryptKeyStatus autocryptKeyStatus : autocryptPeerDao.getAutocryptKeyStatus(packageName, autocryptIds)) { + for (AutocryptKeyStatus autocryptKeyStatus : autocryptPeerDao.getAutocryptKeyStatus(packageName, autocryptIds)) { + AutocryptRecommendationResult peerResult = determineAutocryptRecommendation(autocryptKeyStatus); + result.put(peerResult.peerId, peerResult); + } + + return Collections.unmodifiableMap(result); + } + + public Map determineAutocryptRecommendationsLike(String query) { + Map result = new HashMap<>(); + + for (AutocryptKeyStatus autocryptKeyStatus : autocryptPeerDao.getAutocryptKeyStatusLike(packageName, query)) { AutocryptRecommendationResult peerResult = determineAutocryptRecommendation(autocryptKeyStatus); result.put(peerResult.peerId, peerResult); } @@ -162,7 +173,7 @@ public class AutocryptInteractor { * See https://autocrypt.org/level1.html#recommendations-for-single-recipient-messages */ private AutocryptRecommendationResult determineAutocryptRecommendation( - SelectAutocryptKeyStatus autocryptKeyStatus) { + AutocryptKeyStatus autocryptKeyStatus) { AutocryptRecommendationResult keyRecommendation = determineAutocryptKeyRecommendation(autocryptKeyStatus); if (keyRecommendation != null) return keyRecommendation; @@ -174,7 +185,7 @@ public class AutocryptInteractor { @Nullable private AutocryptRecommendationResult determineAutocryptKeyRecommendation( - SelectAutocryptKeyStatus autocryptKeyStatus) { + AutocryptKeyStatus autocryptKeyStatus) { Long masterKeyId = autocryptKeyStatus.getMaster_key_id(); boolean hasKey = masterKeyId != null; boolean isRevoked = Boolean.TRUE.equals(autocryptKeyStatus.getKey_is_revoked()); @@ -202,7 +213,7 @@ public class AutocryptInteractor { @Nullable private AutocryptRecommendationResult determineAutocryptGossipRecommendation( - SelectAutocryptKeyStatus autocryptKeyStatus) { + AutocryptKeyStatus autocryptKeyStatus) { boolean gossipHasKey = autocryptKeyStatus.getGossip_master_key_id() != null; boolean gossipIsRevoked = Boolean.TRUE.equals(autocryptKeyStatus.getGossip_key_is_revoked()); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/KeychainExternalProvider.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/KeychainExternalProvider.java index d23f88aac..5ab59968d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/KeychainExternalProvider.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/KeychainExternalProvider.java @@ -20,9 +20,10 @@ package org.sufficientlysecure.keychain.remote; import java.security.AccessControlException; import java.util.Arrays; -import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import android.content.ContentProvider; import android.content.ContentValues; @@ -128,30 +129,31 @@ public class KeychainExternalProvider extends ContentProvider { List plist = Arrays.asList(projection); boolean isWildcardSelector = selectionArgs.length == 1 && selectionArgs[0].contains("%"); - boolean queriesUidResult = plist.contains(AutocryptStatus.UID_KEY_STATUS) || - plist.contains(AutocryptStatus.UID_ADDRESS) || - plist.contains(AutocryptStatus.UID_MASTER_KEY_ID) || - plist.contains(AutocryptStatus.UID_CANDIDATES); - boolean queriesAutocryptResult = - plist.contains(AutocryptStatus.AUTOCRYPT_PEER_STATE) || - plist.contains(AutocryptStatus.AUTOCRYPT_MASTER_KEY_ID) || - plist.contains(AutocryptStatus.AUTOCRYPT_KEY_STATUS); - if (isWildcardSelector && queriesAutocryptResult) { - throw new UnsupportedOperationException( - "Cannot wildcard-query autocrypt results!"); + + UserIdDao userIdDao = UserIdDao.getInstance(getContext()); + AutocryptInteractor autocryptInteractor = + AutocryptInteractor.getInstance(getContext(), callingPackageName); + + Map uidStatuses; + Map autocryptStates; + String[] emails; + if (isWildcardSelector) { + uidStatuses = userIdDao.getUidStatusByEmailLike(selectionArgs[0]); + autocryptStates = autocryptInteractor.determineAutocryptRecommendationsLike(selectionArgs[0]); + // If this was a wildcard query, use the found email addresses in the result set. + Set emailsSet = new HashSet<>(); + emailsSet.addAll(uidStatuses.keySet()); + emailsSet.addAll(autocryptStates.keySet()); + emails = emailsSet.toArray(new String[0]); + } else { + uidStatuses = userIdDao.getUidStatusByEmail(selectionArgs); + autocryptStates = autocryptInteractor.determineAutocryptRecommendations(selectionArgs); + // Otherwise, map exactly the selection args to results. + emails = selectionArgs; } - Map uidStatuses = queriesUidResult ? - loadUidStatusMap(selectionArgs, isWildcardSelector) : - Collections.emptyMap(); - Map autocryptStates = - queriesAutocryptResult ? - loadAutocryptRecommendationMap(selectionArgs, callingPackageName) : - Collections.emptyMap(); - - MatrixCursor cursor = - mapResultsToProjectedMatrixCursor(projection, selectionArgs, uidStatuses, - autocryptStates); + MatrixCursor cursor = mapResultsToProjectedMatrixCursor( + projection, emails, uidStatuses, autocryptStates); uri = DatabaseNotifyManager.getNotifyUriAllKeys(); cursor.setNotificationUri(context.getContentResolver(), uri); @@ -166,20 +168,22 @@ public class KeychainExternalProvider extends ContentProvider { } @NonNull - private MatrixCursor mapResultsToProjectedMatrixCursor(String[] projection, - String[] selectionArgs, + private MatrixCursor mapResultsToProjectedMatrixCursor( + String[] projection, + String[] addresses, Map uidStatuses, - Map autocryptStates) { + Map autocryptStates + ) { MatrixCursor cursor = new MatrixCursor(projection); - for (String selectionArg : selectionArgs) { - AutocryptRecommendationResult autocryptResult = autocryptStates.get(selectionArg); - UidStatus uidStatus = uidStatuses.get(selectionArg); + for (String address : addresses) { + AutocryptRecommendationResult autocryptResult = autocryptStates.get(address); + UidStatus uidStatus = uidStatuses.get(address); Object[] row = new Object[projection.length]; for (int i = 0; i < projection.length; i++) { if (AutocryptStatus.ADDRESS.equals(projection[i]) || AutocryptStatus._ID.equals(projection[i])) { - row[i] = selectionArg; + row[i] = address; } else { row[i] = columnNameToRowContent(projection[i], autocryptResult, uidStatus); } @@ -244,25 +248,6 @@ public class KeychainExternalProvider extends ContentProvider { } } - private Map loadUidStatusMap( - String[] selectionArgs, boolean isWildcardSelector) { - UserIdDao userIdDao = UserIdDao.getInstance(getContext()); - if (isWildcardSelector) { - org.sufficientlysecure.keychain.UidStatus uidStatus = - userIdDao.getUidStatusByEmailLike(selectionArgs[0]); - return Collections.singletonMap(selectionArgs[0], uidStatus); - } else { - return userIdDao.getUidStatusByEmail(selectionArgs); - } - } - - private Map loadAutocryptRecommendationMap( - String[] selectionArgs, String callingPackageName) { - AutocryptInteractor autocryptInteractor = - AutocryptInteractor.getInstance(getContext(), callingPackageName); - return autocryptInteractor.determineAutocryptRecommendations(selectionArgs); - } - private int getPeerStateValue(AutocryptState autocryptState) { switch (autocryptState) { case DISABLE: diff --git a/OpenKeychain/src/main/sqldelight/org/sufficientlysecure/keychain/AutocryptPeers.sq b/OpenKeychain/src/main/sqldelight/org/sufficientlysecure/keychain/AutocryptPeers.sq index a4fd7ebd9..44d3121c5 100644 --- a/OpenKeychain/src/main/sqldelight/org/sufficientlysecure/keychain/AutocryptPeers.sq +++ b/OpenKeychain/src/main/sqldelight/org/sufficientlysecure/keychain/AutocryptPeers.sq @@ -50,15 +50,21 @@ UPDATE autocrypt_peers SET gossip_last_seen_key = ?3, gossip_master_key_id = ?4, insertPeer: INSERT OR IGNORE INTO autocrypt_peers (package_name, identifier) VALUES (?, ?); +autocryptKeyStatus: +CREATE VIEW autocryptKeyStatus AS + SELECT autocryptPeer.*, + (CASE WHEN ac_key.expiry IS NULL THEN 0 WHEN ac_key.expiry > strftime('%s', 'now') THEN 0 ELSE 1 END) AS key_is_expired_int, + (CASE WHEN gossip_key.expiry IS NULL THEN 0 WHEN gossip_key.expiry > strftime('%s', 'now') THEN 0 ELSE 1 END) AS gossip_key_is_expired_int, + ac_key.is_revoked AS key_is_revoked, + gossip_key.is_revoked AS gossip_key_is_revoked, + EXISTS (SELECT * FROM certs WHERE certs.master_key_id = autocryptPeer.master_key_id AND verified = 1) AS key_is_verified, + EXISTS (SELECT * FROM certs WHERE certs.master_key_id = autocryptPeer.gossip_master_key_id AND verified = 1) AS gossip_key_is_verified + FROM autocrypt_peers AS autocryptPeer + LEFT JOIN keys AS ac_key ON (ac_key.master_key_id = autocryptPeer.master_key_id AND ac_key.rank = 0) + LEFT JOIN keys AS gossip_key ON (gossip_key.master_key_id = gossip_master_key_id AND gossip_key.rank = 0); + selectAutocryptKeyStatus: -SELECT autocryptPeer.*, - (CASE WHEN ac_key.expiry IS NULL THEN 0 WHEN ac_key.expiry > strftime('%s', 'now') THEN 0 ELSE 1 END) AS key_is_expired_int, - (CASE WHEN gossip_key.expiry IS NULL THEN 0 WHEN gossip_key.expiry > strftime('%s', 'now') THEN 0 ELSE 1 END) AS gossip_key_is_expired_int, - ac_key.is_revoked AS key_is_revoked, - gossip_key.is_revoked AS gossip_key_is_revoked, - EXISTS (SELECT * FROM certs WHERE certs.master_key_id = autocryptPeer.master_key_id AND verified = 1) AS key_is_verified, - EXISTS (SELECT * FROM certs WHERE certs.master_key_id = autocryptPeer.gossip_master_key_id AND verified = 1) AS gossip_key_is_verified - FROM autocrypt_peers AS autocryptPeer - LEFT JOIN keys AS ac_key ON (ac_key.master_key_id = autocryptPeer.master_key_id AND ac_key.rank = 0) - LEFT JOIN keys AS gossip_key ON (gossip_key.master_key_id = gossip_master_key_id AND gossip_key.rank = 0) - WHERE package_name = ?1 AND identifier IN ?2; \ No newline at end of file +SELECT * FROM autocryptKeyStatus WHERE package_name = ?1 AND identifier IN ?2; + +selectAutocryptKeyStatusLike: +SELECT * FROM autocryptKeyStatus WHERE package_name = ?1 AND identifier LIKE ?2; \ No newline at end of file diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/remote/KeychainExternalProviderTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/remote/KeychainExternalProviderTest.java index 8a406dbf5..2c258be10 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/remote/KeychainExternalProviderTest.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/remote/KeychainExternalProviderTest.java @@ -264,6 +264,60 @@ public class KeychainExternalProviderTest { assertFalse(cursor.moveToNext()); } + @Test + public void testAutocryptStatus_autocryptPeer_wildcard() throws Exception { + insertSecretKeyringFrom("/test-keys/testring.sec"); + insertPublicKeyringFrom("/test-keys/testring.pub"); + + autocryptPeerDao.insertOrUpdateLastSeen(PACKAGE_NAME, "tid", new Date()); + autocryptPeerDao.updateKey(PACKAGE_NAME, AUTOCRYPT_PEER, new Date(), KEY_ID_PUBLIC, false); + + Cursor cursor = contentResolver.query( + AutocryptStatus.CONTENT_URI, new String[] { + AutocryptStatus.ADDRESS, AutocryptStatus.UID_KEY_STATUS, AutocryptStatus.UID_ADDRESS, + AutocryptStatus.AUTOCRYPT_PEER_STATE, + AutocryptStatus.AUTOCRYPT_KEY_STATUS, AutocryptStatus.AUTOCRYPT_MASTER_KEY_ID + }, + null, new String [] { "twi@%" }, null + ); + + assertNotNull(cursor); + assertTrue(cursor.moveToFirst()); + assertEquals("twi@openkeychain.org", cursor.getString(0)); + assertFalse(cursor.moveToNext()); + } + + @Test + public void testAutocryptStatus_autocryptPeer_wildcard2() throws Exception { + insertSecretKeyringFrom("/test-keys/testring.sec"); + insertPublicKeyringFrom("/test-keys/testring.pub"); + + autocryptPeerDao.insertOrUpdateLastSeen(PACKAGE_NAME, "tid1", new Date()); + autocryptPeerDao.insertOrUpdateLastSeen(PACKAGE_NAME, "tid2", new Date()); + autocryptPeerDao.updateKey(PACKAGE_NAME, "tid1", new Date(), KEY_ID_PUBLIC, false); + autocryptPeerDao.updateKey(PACKAGE_NAME, "tid2", new Date(), KEY_ID_PUBLIC, false); + + Cursor cursor = contentResolver.query( + AutocryptStatus.CONTENT_URI, new String[] { + AutocryptStatus.ADDRESS, AutocryptStatus.UID_KEY_STATUS, + AutocryptStatus.AUTOCRYPT_PEER_STATE, + AutocryptStatus.AUTOCRYPT_KEY_STATUS, AutocryptStatus.AUTOCRYPT_MASTER_KEY_ID + }, + null, new String [] { "ti%" }, null + ); + + assertNotNull(cursor); + assertTrue(cursor.moveToFirst()); + assertEquals("tid1", cursor.getString(0)); + assertTrue(cursor.isNull(1)); + assertEquals(KEY_ID_PUBLIC, cursor.getLong(4)); + assertTrue(cursor.moveToNext()); + assertEquals("tid2", cursor.getString(0)); + assertTrue(cursor.isNull(1)); + assertEquals(KEY_ID_PUBLIC, cursor.getLong(4)); + assertFalse(cursor.moveToNext()); + } + /* @Test public void testAutocryptStatus_stateSelected() throws Exception {