Add a write-through cache to the identity store.

This commit is contained in:
Greyson Parrelli
2021-09-01 09:41:49 -04:00
parent 50dfe7bc25
commit 7ac83625d3
32 changed files with 469 additions and 388 deletions

View File

@@ -26,8 +26,9 @@ import androidx.annotation.Nullable;
import org.greenrobot.eventbus.EventBus;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.identity.IdentityRecordList;
import org.thoughtcrime.securesms.database.model.IdentityRecord;
import org.thoughtcrime.securesms.database.model.IdentityStoreRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.Base64;
@@ -39,9 +40,6 @@ import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
public class IdentityDatabase extends Database {
@@ -91,43 +89,6 @@ public class IdentityDatabase extends Database {
super(context, databaseHelper);
}
public Cursor getIdentities() {
SQLiteDatabase database = databaseHelper.getSignalReadableDatabase();
return database.query(TABLE_NAME, null, null, null, null, null, null);
}
public @Nullable IdentityReader readerFor(@Nullable Cursor cursor) {
if (cursor == null) return null;
return new IdentityReader(cursor);
}
public Optional<IdentityRecord> getIdentity(@NonNull String addressName) {
SQLiteDatabase database = databaseHelper.getSignalReadableDatabase();
String query = ADDRESS + " = ?";
String[] args = SqlUtil.buildArgs(addressName);
try (Cursor cursor = database.query(TABLE_NAME, null, query, args, null, null, null)) {
if (cursor.moveToFirst()) {
return Optional.of(getIdentityRecord(cursor));
}
} catch (InvalidKeyException | IOException e) {
throw new AssertionError(e);
}
return Optional.absent();
}
public Optional<IdentityRecord> getIdentity(@NonNull RecipientId recipientId) {
Recipient recipient = Recipient.resolved(recipientId);
if (recipient.hasServiceIdentifier()) {
return getIdentity(recipient.requireServiceId());
} else {
Log.w(TAG, "Recipient has no service identifier!");
return Optional.absent();
}
}
public @Nullable IdentityStoreRecord getIdentityStoreRecord(@NonNull String addressName) {
SQLiteDatabase database = databaseHelper.getSignalReadableDatabase();
String query = ADDRESS + " = ?";
@@ -170,34 +131,6 @@ public class IdentityDatabase extends Database {
return null;
}
public @NonNull IdentityRecordList getIdentities(@NonNull List<Recipient> recipients) {
List<String> addressNames = recipients.stream()
.filter(Recipient::hasServiceIdentifier)
.map(Recipient::requireServiceId)
.collect(Collectors.toList());
if (addressNames.isEmpty()) {
return IdentityRecordList.EMPTY;
}
SQLiteDatabase database = databaseHelper.getSignalReadableDatabase();
SqlUtil.Query query = SqlUtil.buildCollectionQuery(ADDRESS, addressNames);
List<IdentityRecord> records = new LinkedList<>();
try (Cursor cursor = database.query(TABLE_NAME, null, query.getWhere(), query.getWhereArgs(), null, null, null)) {
while (cursor.moveToNext()) {
try {
records.add(getIdentityRecord(cursor));
} catch (InvalidKeyException | IOException e) {
throw new AssertionError(e);
}
}
}
return new IdentityRecordList(records);
}
public void saveIdentity(@NonNull String addressName,
@NonNull RecipientId recipientId,
IdentityKey identityKey,
@@ -206,25 +139,10 @@ public class IdentityDatabase extends Database {
long timestamp,
boolean nonBlockingApproval)
{
saveIdentityInternal(addressName, identityKey, verifiedStatus, firstUse, timestamp, nonBlockingApproval);
saveIdentityInternal(addressName, recipientId, identityKey, verifiedStatus, firstUse, timestamp, nonBlockingApproval);
DatabaseFactory.getRecipientDatabase(context).markNeedsSync(recipientId);
}
public void saveIdentity(@NonNull RecipientId recipientId,
IdentityKey identityKey,
VerifiedStatus verifiedStatus,
boolean firstUse,
long timestamp,
boolean nonBlockingApproval)
{
saveIdentityInternal(Recipient.resolved(recipientId).requireServiceId(), identityKey, verifiedStatus, firstUse, timestamp, nonBlockingApproval);
DatabaseFactory.getRecipientDatabase(context).markNeedsSync(recipientId);
}
public void setApproval(@NonNull RecipientId recipientId, boolean nonBlockingApproval) {
setApproval(Recipient.resolved(recipientId).requireServiceId(), recipientId, nonBlockingApproval);
}
public void setApproval(@NonNull String addressName, @NonNull RecipientId recipientId, boolean nonBlockingApproval) {
SQLiteDatabase database = databaseHelper.getSignalWritableDatabase();
@@ -236,11 +154,11 @@ public class IdentityDatabase extends Database {
DatabaseFactory.getRecipientDatabase(context).markNeedsSync(recipientId);
}
public void setVerified(@NonNull RecipientId recipientId, IdentityKey identityKey, VerifiedStatus verifiedStatus) {
public void setVerified(@NonNull String addressName, @NonNull RecipientId recipientId, IdentityKey identityKey, VerifiedStatus verifiedStatus) {
SQLiteDatabase database = databaseHelper.getSignalWritableDatabase();
String query = ADDRESS + " = ? AND " + IDENTITY_KEY + " = ?";
String[] args = SqlUtil.buildArgs(Recipient.resolved(recipientId).requireServiceId(), Base64.encodeBytes(identityKey.serialize()));
String[] args = SqlUtil.buildArgs(addressName, Base64.encodeBytes(identityKey.serialize()));
ContentValues contentValues = new ContentValues(1);
contentValues.put(VERIFIED, verifiedStatus.toInt());
@@ -248,25 +166,27 @@ public class IdentityDatabase extends Database {
int updated = database.update(TABLE_NAME, contentValues, query, args);
if (updated > 0) {
Optional<IdentityRecord> record = getIdentity(recipientId);
Optional<IdentityRecord> record = getIdentityRecord(addressName);
if (record.isPresent()) EventBus.getDefault().post(record.get());
DatabaseFactory.getRecipientDatabase(context).markNeedsSync(recipientId);
}
}
public void updateIdentityAfterSync(@NonNull String addressName, IdentityKey identityKey, VerifiedStatus verifiedStatus) {
boolean hadEntry = getIdentity(addressName).isPresent();
public void updateIdentityAfterSync(@NonNull String addressName, @NonNull RecipientId recipientId, IdentityKey identityKey, VerifiedStatus verifiedStatus) {
boolean hadEntry = getIdentityRecord(addressName).isPresent();
boolean keyMatches = hasMatchingKey(addressName, identityKey);
boolean statusMatches = keyMatches && hasMatchingStatus(addressName, identityKey, verifiedStatus);
if (!keyMatches || !statusMatches) {
saveIdentityInternal(addressName, identityKey, verifiedStatus, !hadEntry, System.currentTimeMillis(), true);
saveIdentityInternal(addressName, recipientId, identityKey, verifiedStatus, !hadEntry, System.currentTimeMillis(), true);
Optional<IdentityRecord> record = getIdentity(addressName);
Optional<IdentityRecord> record = getIdentityRecord(addressName);
if (record.isPresent()) {
EventBus.getDefault().post(record.get());
}
ApplicationDependencies.getIdentityStore().invalidate(addressName);
}
if (hadEntry && !keyMatches) {
@@ -274,6 +194,26 @@ public class IdentityDatabase extends Database {
}
}
public void delete(@NonNull String addressName) {
databaseHelper.getSignalWritableDatabase().delete(IdentityDatabase.TABLE_NAME, IdentityDatabase.ADDRESS + " = ?", SqlUtil.buildArgs(addressName));
}
private Optional<IdentityRecord> getIdentityRecord(@NonNull String addressName) {
SQLiteDatabase database = databaseHelper.getSignalReadableDatabase();
String query = ADDRESS + " = ?";
String[] args = SqlUtil.buildArgs(addressName);
try (Cursor cursor = database.query(TABLE_NAME, null, query, args, null, null, null)) {
if (cursor.moveToFirst()) {
return Optional.of(getIdentityRecord(cursor));
}
} catch (InvalidKeyException | IOException e) {
throw new AssertionError(e);
}
return Optional.absent();
}
private boolean hasMatchingKey(@NonNull String addressName, IdentityKey identityKey) {
SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
String query = ADDRESS + " = ? AND " + IDENTITY_KEY + " = ?";
@@ -307,6 +247,7 @@ public class IdentityDatabase extends Database {
}
private void saveIdentityInternal(@NonNull String addressName,
@NonNull RecipientId recipientId,
IdentityKey identityKey,
VerifiedStatus verifiedStatus,
boolean firstUse,
@@ -326,86 +267,6 @@ public class IdentityDatabase extends Database {
database.replace(TABLE_NAME, null, contentValues);
EventBus.getDefault().post(new IdentityRecord(RecipientId.fromExternalPush(addressName), identityKey, verifiedStatus, firstUse, timestamp, nonBlockingApproval));
EventBus.getDefault().post(new IdentityRecord(recipientId, identityKey, verifiedStatus, firstUse, timestamp, nonBlockingApproval));
}
public static class IdentityRecord {
private final RecipientId recipientId;
private final IdentityKey identitykey;
private final VerifiedStatus verifiedStatus;
private final boolean firstUse;
private final long timestamp;
private final boolean nonblockingApproval;
private IdentityRecord(@NonNull RecipientId recipientId,
IdentityKey identitykey,
VerifiedStatus verifiedStatus,
boolean firstUse,
long timestamp,
boolean nonblockingApproval)
{
this.recipientId = recipientId;
this.identitykey = identitykey;
this.verifiedStatus = verifiedStatus;
this.firstUse = firstUse;
this.timestamp = timestamp;
this.nonblockingApproval = nonblockingApproval;
}
public RecipientId getRecipientId() {
return recipientId;
}
public IdentityKey getIdentityKey() {
return identitykey;
}
public long getTimestamp() {
return timestamp;
}
public VerifiedStatus getVerifiedStatus() {
return verifiedStatus;
}
public boolean isApprovedNonBlocking() {
return nonblockingApproval;
}
public boolean isFirstUse() {
return firstUse;
}
@Override
public @NonNull String toString() {
return "{recipientId: " + recipientId + ", identityKey: " + identitykey + ", verifiedStatus: " + verifiedStatus + ", firstUse: " + firstUse + "}";
}
}
public static class IdentityReader {
private final Cursor cursor;
IdentityReader(@NonNull Cursor cursor) {
this.cursor = cursor;
}
public @Nullable IdentityRecord getNext() {
if (cursor.moveToNext()) {
try {
return getIdentityRecord(cursor);
} catch (IOException | InvalidKeyException e) {
throw new AssertionError(e);
}
}
return null;
}
public void close() {
cursor.close();
}
}
}

View File

@@ -28,7 +28,8 @@ import org.thoughtcrime.securesms.conversation.colors.AvatarColor;
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
import org.thoughtcrime.securesms.conversation.colors.ChatColorsMapper;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
import org.thoughtcrime.securesms.database.model.IdentityRecord;
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
@@ -802,7 +803,7 @@ public class RecipientDatabase extends Database {
try {
IdentityKey identityKey = new IdentityKey(insert.getIdentityKey().get(), 0);
DatabaseFactory.getIdentityDatabase(context).updateIdentityAfterSync(insert.getAddress().getIdentifier(), identityKey, StorageSyncModels.remoteToLocalIdentityStatus(insert.getIdentityState()));
DatabaseFactory.getIdentityDatabase(context).updateIdentityAfterSync(insert.getAddress().getIdentifier(), recipientId, identityKey, StorageSyncModels.remoteToLocalIdentityStatus(insert.getIdentityState()));
} catch (InvalidKeyException e) {
Log.w(TAG, "Failed to process identity key during insert! Skipping.", e);
}
@@ -812,9 +813,9 @@ public class RecipientDatabase extends Database {
}
public void applyStorageSyncContactUpdate(@NonNull StorageRecordUpdate<SignalContactRecord> update) {
SQLiteDatabase db = databaseHelper.getSignalWritableDatabase();
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
ContentValues values = getValuesForStorageContact(update.getNew(), false);
SQLiteDatabase db = databaseHelper.getSignalWritableDatabase();
TextSecureIdentityKeyStore identityStore = ApplicationDependencies.getIdentityStore();
ContentValues values = getValuesForStorageContact(update.getNew(), false);
try {
int updateCount = db.update(TABLE_NAME, values, STORAGE_SERVICE_ID + " = ?", new String[]{Base64.encodeBytes(update.getOld().getId().getRaw())});
@@ -842,14 +843,14 @@ public class RecipientDatabase extends Database {
}
try {
Optional<IdentityRecord> oldIdentityRecord = identityDatabase.getIdentity(recipientId);
Optional<IdentityRecord> oldIdentityRecord = identityStore.getIdentityRecord(recipientId);
if (update.getNew().getIdentityKey().isPresent() && update.getNew().getAddress().hasValidUuid()) {
IdentityKey identityKey = new IdentityKey(update.getNew().getIdentityKey().get(), 0);
DatabaseFactory.getIdentityDatabase(context).updateIdentityAfterSync(update.getNew().getAddress().getIdentifier(), identityKey, StorageSyncModels.remoteToLocalIdentityStatus(update.getNew().getIdentityState()));
DatabaseFactory.getIdentityDatabase(context).updateIdentityAfterSync(update.getNew().getAddress().getIdentifier(), recipientId, identityKey, StorageSyncModels.remoteToLocalIdentityStatus(update.getNew().getIdentityState()));
}
Optional<IdentityRecord> newIdentityRecord = identityDatabase.getIdentity(recipientId);
Optional<IdentityRecord> newIdentityRecord = identityStore.getIdentityRecord(recipientId);
if ((newIdentityRecord.isPresent() && newIdentityRecord.get().getVerifiedStatus() == VerifiedStatus.VERIFIED) &&
(!oldIdentityRecord.isPresent() || oldIdentityRecord.get().getVerifiedStatus() != VerifiedStatus.VERIFIED))
@@ -2901,7 +2902,7 @@ public class RecipientDatabase extends Database {
db.update(TABLE_NAME, uuidValues, ID_WHERE, SqlUtil.buildArgs(byUuid));
// Identities
db.delete(IdentityDatabase.TABLE_NAME, IdentityDatabase.ADDRESS + " = ?", SqlUtil.buildArgs(byE164));
ApplicationDependencies.getIdentityStore().delete(e164Settings.e164);
// Group Receipts
ContentValues groupReceiptValues = new ContentValues();

View File

@@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.database.identity;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.database.model.IdentityRecord;
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
import org.thoughtcrime.securesms.recipients.Recipient;

View File

@@ -0,0 +1,16 @@
package org.thoughtcrime.securesms.database.model
import org.thoughtcrime.securesms.database.IdentityDatabase
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.libsignal.IdentityKey
data class IdentityRecord(
val recipientId: RecipientId,
val identityKey: IdentityKey,
val verifiedStatus: IdentityDatabase.VerifiedStatus,
@get:JvmName("isFirstUse")
val firstUse: Boolean,
val timestamp: Long,
@get:JvmName("isApprovedNonBlocking")
val nonblockingApproval: Boolean
)

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.database.model
import org.thoughtcrime.securesms.database.IdentityDatabase
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.libsignal.IdentityKey
data class IdentityStoreRecord(
@@ -10,4 +11,15 @@ data class IdentityStoreRecord(
val firstUse: Boolean,
val timestamp: Long,
val nonblockingApproval: Boolean
)
) {
fun toIdentityRecord(recipientId: RecipientId): IdentityRecord {
return IdentityRecord(
recipientId = recipientId,
identityKey = identityKey,
verifiedStatus = verifiedStatus,
firstUse = firstUse,
timestamp = timestamp,
nonblockingApproval = nonblockingApproval
)
}
}