Return full result set from external provider autocrypt_status query
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -50,7 +50,8 @@ 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 (?, ?);
|
||||
|
||||
selectAutocryptKeyStatus:
|
||||
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,
|
||||
@@ -60,5 +61,10 @@ SELECT autocryptPeer.*,
|
||||
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;
|
||||
LEFT JOIN keys AS gossip_key ON (gossip_key.master_key_id = gossip_master_key_id AND gossip_key.rank = 0);
|
||||
|
||||
selectAutocryptKeyStatus:
|
||||
SELECT * FROM autocryptKeyStatus WHERE package_name = ?1 AND identifier IN ?2;
|
||||
|
||||
selectAutocryptKeyStatusLike:
|
||||
SELECT * FROM autocryptKeyStatus WHERE package_name = ?1 AND identifier LIKE ?2;
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user