Return full result set from external provider autocrypt_status query

This commit is contained in:
Vincent Breitmoser
2024-02-26 15:02:07 +01:00
parent 290ccf049e
commit b209835285
7 changed files with 159 additions and 74 deletions

View File

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

View File

@@ -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<SelectAutocryptKeyStatus> getAutocryptKeyStatus(String packageName,
public List<AutocryptKeyStatus> getAutocryptKeyStatus(String packageName,
String[] autocryptIds) {
return autocryptPeersQueries.selectAutocryptKeyStatus(packageName,
Arrays.asList(autocryptIds)).executeAsList();
}
public List<AutocryptKeyStatus> getAutocryptKeyStatusLike(String packageName,
String query) {
return autocryptPeersQueries.selectAutocryptKeyStatusLike(packageName, query).executeAsList();
}
private void ensureAutocryptPeerExists(String packageName, String autocryptId) {
autocryptPeersQueries.insertPeer(packageName, autocryptId);
}

View File

@@ -34,11 +34,6 @@ public class UserIdDao extends AbstractDao {
.executeAsList();
}
public UidStatus getUidStatusByEmailLike(String emailLike) {
return getDatabase().getUserPacketsQueries().selectUserIdStatusByEmailLike(emailLike)
.executeAsOneOrNull();
}
public Map<String, UidStatus> getUidStatusByEmail(String... emails) {
Query<UidStatus> q = getDatabase().getUserPacketsQueries()
.selectUserIdStatusByEmail(Arrays.asList(emails));
@@ -54,6 +49,21 @@ public class UserIdDao extends AbstractDao {
return result;
}
public Map<String, UidStatus> getUidStatusByEmailLike(String query) {
Query<UidStatus> q = getDatabase().getUserPacketsQueries()
.selectUserIdStatusByEmailLike(query);
Map<String, UidStatus> 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<Long> getLongArrayAsList(long[] longList) {
Long[] longs = new Long[longList.length];
int i = 0;

View File

@@ -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<String,AutocryptRecommendationResult> determineAutocryptRecommendations(String... autocryptIds) {
Map<String,AutocryptRecommendationResult> 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<String,AutocryptRecommendationResult> determineAutocryptRecommendationsLike(String query) {
Map<String,AutocryptRecommendationResult> 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());

View File

@@ -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<String> 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<String, UidStatus> uidStatuses;
Map<String, AutocryptRecommendationResult> 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<String> 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<String, UidStatus> uidStatuses = queriesUidResult ?
loadUidStatusMap(selectionArgs, isWildcardSelector) :
Collections.emptyMap();
Map<String, AutocryptRecommendationResult> 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<String, UidStatus> uidStatuses,
Map<String, AutocryptRecommendationResult> autocryptStates) {
Map<String, AutocryptRecommendationResult> 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<String, org.sufficientlysecure.keychain.UidStatus> 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<String, AutocryptRecommendationResult> loadAutocryptRecommendationMap(
String[] selectionArgs, String callingPackageName) {
AutocryptInteractor autocryptInteractor =
AutocryptInteractor.getInstance(getContext(), callingPackageName);
return autocryptInteractor.determineAutocryptRecommendations(selectionArgs);
}
private int getPeerStateValue(AutocryptState autocryptState) {
switch (autocryptState) {
case DISABLE:

View File

@@ -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;
SELECT * FROM autocryptKeyStatus WHERE package_name = ?1 AND identifier IN ?2;
selectAutocryptKeyStatusLike:
SELECT * FROM autocryptKeyStatus WHERE package_name = ?1 AND identifier LIKE ?2;

View File

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