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

@ -55,12 +55,10 @@ import android.widget.Toast;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.SwitchCompat;
import androidx.core.view.OneShotPreDrawListener;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentTransaction;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
import androidx.transition.TransitionManager;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.concurrent.SignalExecutors;
@ -70,9 +68,9 @@ import org.thoughtcrime.securesms.components.camera.CameraView;
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
import org.thoughtcrime.securesms.database.model.IdentityRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.MultiDeviceVerifiedUpdateJob;
import org.thoughtcrime.securesms.permissions.Permissions;
@ -119,7 +117,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
private final VerifyScanFragment scanFragment = new VerifyScanFragment();
public static Intent newIntent(@NonNull Context context,
@NonNull IdentityDatabase.IdentityRecord identityRecord)
@NonNull IdentityRecord identityRecord)
{
return newIntent(context,
identityRecord.getRecipientId(),
@ -128,7 +126,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
}
public static Intent newIntent(@NonNull Context context,
@NonNull IdentityDatabase.IdentityRecord identityRecord,
@NonNull IdentityRecord identityRecord,
boolean verified)
{
return newIntent(context,
@ -642,16 +640,15 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
try (SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
if (verified) {
Log.i(TAG, "Saving identity: " + recipientId);
DatabaseFactory.getIdentityDatabase(getActivity())
.saveIdentity(recipientId,
remoteIdentity,
VerifiedStatus.VERIFIED, false,
System.currentTimeMillis(), true);
ApplicationDependencies.getIdentityStore()
.saveIdentityWithoutSideEffects(recipientId,
remoteIdentity,
VerifiedStatus.VERIFIED,
false,
System.currentTimeMillis(),
true);
} else {
DatabaseFactory.getIdentityDatabase(getActivity())
.setVerified(recipientId,
remoteIdentity,
VerifiedStatus.DEFAULT);
ApplicationDependencies.getIdentityStore().setVerified(recipientId, remoteIdentity, VerifiedStatus.DEFAULT);
}
ApplicationDependencies.getJobManager()

View file

@ -9,9 +9,11 @@ import androidx.appcompat.app.AlertDialog;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.database.model.IdentityRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.whispersystems.signalservice.api.SignalSessionLock;
@ -40,12 +42,12 @@ public class UntrustedSendDialog extends AlertDialog.Builder implements DialogIn
@Override
public void onClick(DialogInterface dialog, int which) {
final IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(getContext());
final TextSecureIdentityKeyStore identityStore = ApplicationDependencies.getIdentityStore();
SimpleTask.run(() -> {
try(SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
for (IdentityRecord identityRecord : untrustedRecords) {
identityDatabase.setApproval(identityRecord.getRecipientId(), true);
identityStore.setApproval(identityRecord.getRecipientId(), true);
}
}

View file

@ -16,7 +16,7 @@ import androidx.annotation.RequiresApi;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.database.model.IdentityRecord;
import java.util.List;

View file

@ -11,7 +11,9 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.database.model.IdentityRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.whispersystems.signalservice.api.SignalSessionLock;
import java.util.List;
@ -39,27 +41,16 @@ public class UnverifiedSendDialog extends AlertDialog.Builder implements DialogI
@Override
public void onClick(DialogInterface dialog, int which) {
final IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(getContext());
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
try(SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
for (IdentityRecord identityRecord : untrustedRecords) {
identityDatabase.setVerified(identityRecord.getRecipientId(),
identityRecord.getIdentityKey(),
IdentityDatabase.VerifiedStatus.DEFAULT);
}
SimpleTask.run(() -> {
try(SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
for (IdentityRecord identityRecord : untrustedRecords) {
ApplicationDependencies.getIdentityStore().setVerified(identityRecord.getRecipientId(),
identityRecord.getIdentityKey(),
IdentityDatabase.VerifiedStatus.DEFAULT);
}
return null;
}
@Override
protected void onPostExecute(Void result) {
resendListener.onResendMessage();
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
return null;
}, nothing -> resendListener.onResendMessage());
}
public interface ResendListener {

View file

@ -11,8 +11,8 @@ import org.signal.storageservice.protos.groups.local.DecryptedPendingMember
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.GroupDatabase
import org.thoughtcrime.securesms.database.IdentityDatabase
import org.thoughtcrime.securesms.database.MediaDatabase
import org.thoughtcrime.securesms.database.model.IdentityRecord
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.GroupManager
@ -64,13 +64,9 @@ class ConversationSettingsRepository(
SignalExecutors.BOUNDED.execute { consumer(DatabaseFactory.getGroupDatabase(context).activeGroupCount > 0) }
}
fun getIdentity(recipientId: RecipientId, consumer: (IdentityDatabase.IdentityRecord?) -> Unit) {
fun getIdentity(recipientId: RecipientId, consumer: (IdentityRecord?) -> Unit) {
SignalExecutors.BOUNDED.execute {
consumer(
DatabaseFactory.getIdentityDatabase(context)
.getIdentity(recipientId)
.orNull()
)
consumer(ApplicationDependencies.getIdentityStore().getIdentityRecord(recipientId).orNull())
}
}

View file

@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.components.settings.conversation
import android.database.Cursor
import org.thoughtcrime.securesms.components.settings.conversation.preferences.ButtonStripPreference
import org.thoughtcrime.securesms.components.settings.conversation.preferences.LegacyGroupPreference
import org.thoughtcrime.securesms.database.IdentityDatabase
import org.thoughtcrime.securesms.database.model.IdentityRecord
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry
import org.thoughtcrime.securesms.recipients.Recipient
@ -43,7 +43,7 @@ sealed class SpecificSettingsState {
abstract val isLoaded: Boolean
data class RecipientSettingsState(
val identityRecord: IdentityDatabase.IdentityRecord? = null,
val identityRecord: IdentityRecord? = null,
val allGroupsInCommon: List<Recipient> = listOf(),
val groupsInCommon: List<Recipient> = listOf(),
val selfHasGroups: Boolean = false,

View file

@ -42,8 +42,7 @@ class WebRtcCallRepository {
@WorkerThread
void getIdentityRecords(@NonNull Recipient recipient, @NonNull Consumer<IdentityRecordList> consumer) {
SignalExecutors.BOUNDED.execute(() -> {
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
List<Recipient> recipients;
List<Recipient> recipients;
if (recipient.isGroup()) {
recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(recipient.requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
@ -51,7 +50,7 @@ class WebRtcCallRepository {
recipients = Collections.singletonList(recipient);
}
consumer.accept(identityDatabase.getIdentities(recipients));
consumer.accept(ApplicationDependencies.getIdentityStore().getIdentityRecords(recipients));
});
}
}

View file

@ -20,6 +20,7 @@ import org.signal.core.util.ThreadUtil;
import org.thoughtcrime.securesms.components.sensors.DeviceOrientationMonitor;
import org.thoughtcrime.securesms.components.sensors.Orientation;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.model.IdentityRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.CallParticipantId;
@ -440,7 +441,7 @@ public class WebRtcCallViewModel extends ViewModel {
if (recipient.isGroup()) {
repository.getIdentityRecords(recipient, identityRecords -> {
if (identityRecords.isUntrusted(false) || identityRecords.isUnverified(false)) {
List<IdentityDatabase.IdentityRecord> records = identityRecords.getUnverifiedRecords();
List<IdentityRecord> records = identityRecords.getUnverifiedRecords();
records.addAll(identityRecords.getUntrustedRecords());
events.postValue(new Event.ShowGroupCallSafetyNumberChange(records));
} else {
@ -475,13 +476,13 @@ public class WebRtcCallViewModel extends ViewModel {
}
public static class ShowGroupCallSafetyNumberChange extends Event {
private final List<IdentityDatabase.IdentityRecord> identityRecords;
private final List<IdentityRecord> identityRecords;
public ShowGroupCallSafetyNumberChange(@NonNull List<IdentityDatabase.IdentityRecord> identityRecords) {
public ShowGroupCallSafetyNumberChange(@NonNull List<IdentityRecord> identityRecords) {
this.identityRecords = identityRecords;
}
public @NonNull List<IdentityDatabase.IdentityRecord> getIdentityRecords() {
public @NonNull List<IdentityRecord> getIdentityRecords() {
return identityRecords;
}
}

View file

@ -159,7 +159,7 @@ import org.thoughtcrime.securesms.database.DraftDatabase.Draft;
import org.thoughtcrime.securesms.database.DraftDatabase.Drafts;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase;
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.database.MentionUtil;
import org.thoughtcrime.securesms.database.MentionUtil.UpdatedBodyAndMentions;
@ -1957,8 +1957,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
new AsyncTask<Recipient, Void, Pair<IdentityRecordList, String>>() {
@Override
protected @NonNull Pair<IdentityRecordList, String> doInBackground(Recipient... params) {
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(ConversationActivity.this);
List<Recipient> recipients;
List<Recipient> recipients;
if (params[0].isGroup()) {
recipients = DatabaseFactory.getGroupDatabase(ConversationActivity.this)
@ -1968,7 +1967,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
}
long startTime = System.currentTimeMillis();
IdentityRecordList identityRecordList = identityDatabase.getIdentities(recipients);
IdentityRecordList identityRecordList = ApplicationDependencies.getIdentityStore().getIdentityRecords(recipients);
Log.i(TAG, String.format(Locale.US, "Loaded %d identities in %d ms", recipients.size(), System.currentTimeMillis() - startTime));
@ -3954,27 +3953,16 @@ public class ConversationActivity extends PassphraseRequiredActivity
private class UnverifiedDismissedListener implements UnverifiedBannerView.DismissListener {
@Override
public void onDismissed(final List<IdentityRecord> unverifiedIdentities) {
final IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(ConversationActivity.this);
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
try (SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
for (IdentityRecord identityRecord : unverifiedIdentities) {
identityDatabase.setVerified(identityRecord.getRecipientId(),
identityRecord.getIdentityKey(),
VerifiedStatus.DEFAULT);
}
SimpleTask.run(() -> {
try (SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
for (IdentityRecord identityRecord : unverifiedIdentities) {
ApplicationDependencies.getIdentityStore().setVerified(identityRecord.getRecipientId(),
identityRecord.getIdentityKey(),
VerifiedStatus.DEFAULT);
}
return null;
}
@Override
protected void onPostExecute(Void result) {
initializeIdentityRecords();
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
return null;
}, nothing -> initializeIdentityRecords());
}
}

View file

@ -1654,7 +1654,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
.setView(R.layout.safety_number_changed_learn_more_dialog)
.setPositiveButton(R.string.ConversationFragment_verify, (d, w) -> {
SimpleTask.run(getLifecycle(), () -> {
return DatabaseFactory.getIdentityDatabase(requireContext()).getIdentity(recipient.getId());
return ApplicationDependencies.getIdentityStore().getIdentityRecord(recipient.getId());
}, identityRecord -> {
if (identityRecord.isPresent()) {
startActivity(VerifyIdentityActivity.newIntent(requireContext(), identityRecord.get()));

View file

@ -30,7 +30,7 @@ import org.thoughtcrime.securesms.conversation.colors.Colorizer;
import org.thoughtcrime.securesms.conversation.mutiselect.Multiselect;
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart;
import org.thoughtcrime.securesms.conversation.ui.error.EnableCallNotificationSettingsDialog;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.database.model.IdentityRecord;
import org.thoughtcrime.securesms.database.model.GroupCallUpdateDetailsUtil;
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord;
import org.thoughtcrime.securesms.database.model.LiveUpdateMessage;

View file

@ -27,7 +27,7 @@ import org.thoughtcrime.securesms.components.ContactFilterView
import org.thoughtcrime.securesms.components.FixedRoundedCornerBottomSheetDialogFragment
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog
import org.thoughtcrime.securesms.database.IdentityDatabase
import org.thoughtcrime.securesms.database.model.IdentityRecord
import org.thoughtcrime.securesms.keyboard.findListener
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.RecipientId
@ -215,7 +215,7 @@ class MultiselectForwardFragment :
.show()
}
private fun displaySafetyNumberConfirmation(identityRecords: List<IdentityDatabase.IdentityRecord>) {
private fun displaySafetyNumberConfirmation(identityRecords: List<IdentityRecord>) {
SafetyNumberChangeDialog.show(childFragmentManager, identityRecords)
}

View file

@ -5,9 +5,10 @@ import androidx.core.util.Consumer
import io.reactivex.rxjava3.core.Single
import org.signal.core.util.concurrent.SignalExecutors
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.IdentityDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.identity.IdentityRecordList
import org.thoughtcrime.securesms.database.model.IdentityRecord
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.sharing.MultiShareArgs
@ -26,11 +27,10 @@ class MultiselectForwardRepository(context: Context) {
val onAllMessagesFailed: () -> Unit
)
fun checkForBadIdentityRecords(shareContacts: List<ShareContact>, consumer: Consumer<List<IdentityDatabase.IdentityRecord>>) {
fun checkForBadIdentityRecords(shareContacts: List<ShareContact>, consumer: Consumer<List<IdentityRecord>>) {
SignalExecutors.BOUNDED.execute {
val identityDatabase: IdentityDatabase = DatabaseFactory.getIdentityDatabase(context)
val recipients: List<Recipient> = shareContacts.map { Recipient.resolved(it.recipientId.get()) }
val identityRecordList: IdentityRecordList = identityDatabase.getIdentities(recipients)
val identityRecordList: IdentityRecordList = ApplicationDependencies.getIdentityStore().getIdentityRecords(recipients)
consumer.accept(identityRecordList.untrustedRecords)
}

View file

@ -1,6 +1,6 @@
package org.thoughtcrime.securesms.conversation.mutiselect.forward
import org.thoughtcrime.securesms.database.IdentityDatabase
import org.thoughtcrime.securesms.database.model.IdentityRecord
import org.thoughtcrime.securesms.sharing.ShareContact
data class MultiselectForwardState(
@ -11,7 +11,7 @@ data class MultiselectForwardState(
object Selection : Stage()
object FirstConfirmation : Stage()
object LoadingIdentities : Stage()
data class SafetyConfirmation(val identities: List<IdentityDatabase.IdentityRecord>) : Stage()
data class SafetyConfirmation(val identities: List<IdentityRecord>) : Stage()
object SendPending : Stage()
object SomeFailed : Stage()
object AllFailed : Stage()

View file

@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.conversation.ui.error;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.database.model.IdentityRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
/**

View file

@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.components.FromTextView;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.model.IdentityRecord;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.adapter.AlwaysChangedDiffUtil;
@ -80,6 +81,6 @@ final class SafetyNumberChangeAdapter extends ListAdapter<ChangedRecipient, Safe
}
interface Callbacks {
void onViewIdentityRecord(@NonNull IdentityDatabase.IdentityRecord identityRecord);
void onViewIdentityRecord(@NonNull IdentityRecord identityRecord);
}
}

View file

@ -29,6 +29,7 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.VerifyIdentityActivity;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.model.IdentityRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.recipients.RecipientId;
@ -62,9 +63,9 @@ public final class SafetyNumberChangeDialog extends DialogFragment implements Sa
fragment.show(fragmentManager, SAFETY_NUMBER_DIALOG);
}
public static void show(@NonNull FragmentManager fragmentManager, @NonNull List<IdentityDatabase.IdentityRecord> identityRecords) {
public static void show(@NonNull FragmentManager fragmentManager, @NonNull List<IdentityRecord> identityRecords) {
List<String> ids = Stream.of(identityRecords)
.filterNot(IdentityDatabase.IdentityRecord::isFirstUse)
.filterNot(IdentityRecord::isFirstUse)
.map(record -> record.getRecipientId().serialize())
.distinct()
.toList();
@ -102,9 +103,9 @@ public final class SafetyNumberChangeDialog extends DialogFragment implements Sa
fragment.show(fragmentManager, SAFETY_NUMBER_DIALOG);
}
public static void showForGroupCall(@NonNull FragmentManager fragmentManager, @NonNull List<IdentityDatabase.IdentityRecord> identityRecords) {
public static void showForGroupCall(@NonNull FragmentManager fragmentManager, @NonNull List<IdentityRecord> identityRecords) {
List<String> ids = Stream.of(identityRecords)
.filterNot(IdentityDatabase.IdentityRecord::isFirstUse)
.filterNot(IdentityRecord::isFirstUse)
.map(record -> record.getRecipientId().serialize())
.distinct()
.toList();
@ -255,7 +256,7 @@ public final class SafetyNumberChangeDialog extends DialogFragment implements Sa
}
@Override
public void onViewIdentityRecord(@NonNull IdentityDatabase.IdentityRecord identityRecord) {
public void onViewIdentityRecord(@NonNull IdentityRecord identityRecord) {
startActivity(VerifyIdentityActivity.newIntent(requireContext(), identityRecord));
}

View file

@ -17,7 +17,7 @@ import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.database.model.IdentityRecord;
import org.thoughtcrime.securesms.database.MessageDatabase;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
@ -68,7 +68,7 @@ final class SafetyNumberChangeRepository {
List<Recipient> recipients = Stream.of(recipientIds).map(Recipient::resolved).toList();
List<ChangedRecipient> changedRecipients = Stream.of(DatabaseFactory.getIdentityDatabase(context).getIdentities(recipients).getIdentityRecords())
List<ChangedRecipient> changedRecipients = Stream.of(ApplicationDependencies.getIdentityStore().getIdentityRecords(recipients).getIdentityRecords())
.map(record -> new ChangedRecipient(Recipient.resolved(record.getRecipientId()), record))
.toList();
@ -96,7 +96,7 @@ final class SafetyNumberChangeRepository {
@WorkerThread
private TrustAndVerifyResult trustOrVerifyChangedRecipientsInternal(@NonNull List<ChangedRecipient> changedRecipients) {
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
TextSecureIdentityKeyStore identityStore = ApplicationDependencies.getIdentityStore();
try(SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
for (ChangedRecipient changedRecipient : changedRecipients) {
@ -104,12 +104,12 @@ final class SafetyNumberChangeRepository {
if (changedRecipient.isUnverified()) {
Log.d(TAG, "Setting " + identityRecord.getRecipientId() + " as verified");
identityDatabase.setVerified(identityRecord.getRecipientId(),
identityRecord.getIdentityKey(),
IdentityDatabase.VerifiedStatus.DEFAULT);
ApplicationDependencies.getIdentityStore().setVerified(identityRecord.getRecipientId(),
identityRecord.getIdentityKey(),
IdentityDatabase.VerifiedStatus.DEFAULT);
} else {
Log.d(TAG, "Setting " + identityRecord.getRecipientId() + " as approved");
identityDatabase.setApproval(identityRecord.getRecipientId(), true);
identityStore.setApproval(identityRecord.getRecipientId(), true);
}
}
}

View file

@ -3,17 +3,23 @@ package org.thoughtcrime.securesms.crypto.storage;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.SessionUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
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.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.LRUCache;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
@ -21,19 +27,29 @@ import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.state.IdentityKeyStore;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
public class TextSecureIdentityKeyStore implements IdentityKeyStore {
private static final int TIMESTAMP_THRESHOLD_SECONDS = 5;
private static final String TAG = Log.tag(TextSecureIdentityKeyStore.class);
private static final Object LOCK = new Object();
private static final Object LOCK = new Object();
private static final int TIMESTAMP_THRESHOLD_SECONDS = 5;
private final Context context;
private final Cache cache;
public TextSecureIdentityKeyStore(Context context) {
this(context, DatabaseFactory.getIdentityDatabase(context));
}
TextSecureIdentityKeyStore(@NonNull Context context, @NonNull IdentityDatabase identityDatabase) {
this.context = context;
this.cache = new Cache(identityDatabase);
}
@Override
@ -46,40 +62,44 @@ public class TextSecureIdentityKeyStore implements IdentityKeyStore {
return TextSecurePreferences.getLocalRegistrationId(context);
}
@Override
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
return saveIdentity(address, identityKey, false) == SaveResult.UPDATE;
}
public @NonNull SaveResult saveIdentity(SignalProtocolAddress address, IdentityKey identityKey, boolean nonBlockingApproval) {
synchronized (LOCK) {
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
RecipientId recipientId = RecipientId.fromExternalPush(address.getName());
Optional<IdentityRecord> identityRecord = identityDatabase.getIdentity(recipientId);
IdentityStoreRecord identityRecord = cache.get(address.getName());
RecipientId recipientId = RecipientId.fromExternalPush(address.getName());
if (!identityRecord.isPresent()) {
if (identityRecord == null) {
Log.i(TAG, "Saving new identity...");
identityDatabase.saveIdentity(recipientId, identityKey, VerifiedStatus.DEFAULT, true, System.currentTimeMillis(), nonBlockingApproval);
cache.save(address.getName(), recipientId, identityKey, VerifiedStatus.DEFAULT, true, System.currentTimeMillis(), nonBlockingApproval);
return SaveResult.NEW;
}
if (!identityRecord.get().getIdentityKey().equals(identityKey)) {
Log.i(TAG, "Replacing existing identity... Existing: " + identityRecord.get().getIdentityKey().hashCode() + " New: " + identityKey.hashCode());
if (!identityRecord.getIdentityKey().equals(identityKey)) {
Log.i(TAG, "Replacing existing identity... Existing: " + identityRecord.getIdentityKey().hashCode() + " New: " + identityKey.hashCode());
VerifiedStatus verifiedStatus;
if (identityRecord.get().getVerifiedStatus() == VerifiedStatus.VERIFIED ||
identityRecord.get().getVerifiedStatus() == VerifiedStatus.UNVERIFIED)
if (identityRecord.getVerifiedStatus() == VerifiedStatus.VERIFIED ||
identityRecord.getVerifiedStatus() == VerifiedStatus.UNVERIFIED)
{
verifiedStatus = VerifiedStatus.UNVERIFIED;
} else {
verifiedStatus = VerifiedStatus.DEFAULT;
}
identityDatabase.saveIdentity(recipientId, identityKey, verifiedStatus, false, System.currentTimeMillis(), nonBlockingApproval);
cache.save(address.getName(), recipientId, identityKey, verifiedStatus, false, System.currentTimeMillis(), nonBlockingApproval);
IdentityUtil.markIdentityUpdate(context, recipientId);
SessionUtil.archiveSiblingSessions(address);
DatabaseFactory.getSenderKeySharedDatabase(context).deleteAllFor(recipientId);
return SaveResult.UPDATE;
}
if (isNonBlockingApprovalRequired(identityRecord.get())) {
if (isNonBlockingApprovalRequired(identityRecord)) {
Log.i(TAG, "Setting approval status...");
identityDatabase.setApproval(recipientId, nonBlockingApproval);
cache.setApproval(address.getName(), recipientId, identityRecord, nonBlockingApproval);
return SaveResult.NON_BLOCKING_APPROVAL_REQUIRED;
}
@ -87,73 +107,132 @@ public class TextSecureIdentityKeyStore implements IdentityKeyStore {
}
}
@Override
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
return saveIdentity(address, identityKey, false) == SaveResult.UPDATE;
public void saveIdentityWithoutSideEffects(@NonNull RecipientId recipientId,
IdentityKey identityKey,
VerifiedStatus verifiedStatus,
boolean firstUse,
long timestamp,
boolean nonBlockingApproval)
{
Recipient recipient = Recipient.resolved(recipientId);
if (recipient.hasServiceIdentifier()) {
cache.save(recipient.requireServiceId(), recipientId, identityKey, verifiedStatus, firstUse, timestamp, nonBlockingApproval);
} else {
Log.w(TAG, "[saveIdentity] No serviceId for " + recipient.getId());
}
}
@Override
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) {
synchronized (LOCK) {
if (DatabaseFactory.getRecipientDatabase(context).containsPhoneOrUuid(address.getName())) {
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
RecipientId ourRecipientId = Recipient.self().getId();
RecipientId theirRecipientId = RecipientId.fromExternalPush(address.getName());
boolean isSelf = address.getName().equals(TextSecurePreferences.getLocalUuid(context).toString()) ||
address.getName().equals(TextSecurePreferences.getLocalNumber(context));
if (ourRecipientId.equals(theirRecipientId)) {
return identityKey.equals(IdentityKeyUtil.getIdentityKey(context));
}
if (isSelf) {
return identityKey.equals(IdentityKeyUtil.getIdentityKey(context));
}
switch (direction) {
case SENDING: return isTrustedForSending(identityKey, identityDatabase.getIdentity(theirRecipientId));
case RECEIVING: return true;
default: throw new AssertionError("Unknown direction: " + direction);
}
} else {
Log.w(TAG, "Tried to check if identity is trusted for " + address.getName() + ", but no matching recipient existed!");
switch (direction) {
case SENDING: return false;
case RECEIVING: return true;
default: throw new AssertionError("Unknown direction: " + direction);
}
}
IdentityStoreRecord record = cache.get(address.getName());
switch (direction) {
case SENDING:
return isTrustedForSending(identityKey, record);
case RECEIVING:
return true;
default:
throw new AssertionError("Unknown direction: " + direction);
}
}
@Override
public IdentityKey getIdentity(SignalProtocolAddress address) {
if (DatabaseFactory.getRecipientDatabase(context).containsPhoneOrUuid(address.getName())) {
RecipientId recipientId = RecipientId.fromExternalPush(address.getName());
Optional<IdentityRecord> record = DatabaseFactory.getIdentityDatabase(context).getIdentity(recipientId);
IdentityStoreRecord record = cache.get(address.getName());
return record != null ? record.getIdentityKey() : null;
}
if (record.isPresent()) {
return record.get().getIdentityKey();
} else {
return null;
}
public @NonNull Optional<IdentityRecord> getIdentityRecord(@NonNull RecipientId recipientId) {
Recipient recipient = Recipient.resolved(recipientId);
if (recipient.hasServiceIdentifier()) {
IdentityStoreRecord record = cache.get(recipient.requireServiceId());
return Optional.fromNullable(record).transform(r -> r.toIdentityRecord(recipientId));
} else {
Log.w(TAG, "Tried to get identity for " + address.getName() + ", but no matching recipient existed!");
return null;
Log.w(TAG, "[getIdentityRecord] No serviceId for " + recipient.getId());
return Optional.absent();
}
}
private boolean isTrustedForSending(IdentityKey identityKey, Optional<IdentityRecord> identityRecord) {
if (!identityRecord.isPresent()) {
public @NonNull IdentityRecordList getIdentityRecords(@NonNull List<Recipient> recipients) {
List<String> addressNames = recipients.stream()
.filter(Recipient::hasServiceIdentifier)
.map(Recipient::requireServiceId)
.collect(Collectors.toList());
if (addressNames.isEmpty()) {
return IdentityRecordList.EMPTY;
}
List<IdentityRecord> records = new ArrayList<>(recipients.size());
for (Recipient recipient : recipients) {
if (recipient.hasServiceIdentifier()) {
IdentityStoreRecord record = cache.get(recipient.requireServiceId());
if (record != null) {
records.add(record.toIdentityRecord(recipient.getId()));
}
} else {
Log.w(TAG, "[getIdentityRecords] No serviceId for " + recipient.getId());
}
}
return new IdentityRecordList(records);
}
public void setApproval(@NonNull RecipientId recipientId, boolean nonBlockingApproval) {
Recipient recipient = Recipient.resolved(recipientId);
if (recipient.hasServiceIdentifier()) {
cache.setApproval(recipient.requireServiceId(), recipientId, nonBlockingApproval);
} else {
Log.w(TAG, "[setApproval] No serviceId for " + recipient.getId());
}
}
public void setVerified(@NonNull RecipientId recipientId, IdentityKey identityKey, VerifiedStatus verifiedStatus) {
Recipient recipient = Recipient.resolved(recipientId);
if (recipient.hasServiceIdentifier()) {
cache.setVerified(recipient.requireServiceId(), recipientId, identityKey, verifiedStatus);
} else {
Log.w(TAG, "[setVerified] No serviceId for " + recipient.getId());
}
}
public void delete(@NonNull String addressName) {
cache.delete(addressName);
}
public void invalidate(@NonNull String addressName) {
cache.invalidate(addressName);
}
private boolean isTrustedForSending(@NonNull IdentityKey identityKey, @Nullable IdentityStoreRecord identityRecord) {
if (identityRecord == null) {
Log.w(TAG, "Nothing here, returning true...");
return true;
}
if (!identityKey.equals(identityRecord.get().getIdentityKey())) {
Log.w(TAG, "Identity keys don't match... service: " + identityKey.hashCode() + " database: " + identityRecord.get().getIdentityKey().hashCode());
if (!identityKey.equals(identityRecord.getIdentityKey())) {
Log.w(TAG, "Identity keys don't match... service: " + identityKey.hashCode() + " database: " + identityRecord.getIdentityKey().hashCode());
return false;
}
if (identityRecord.get().getVerifiedStatus() == VerifiedStatus.UNVERIFIED) {
if (identityRecord.getVerifiedStatus() == VerifiedStatus.UNVERIFIED) {
Log.w(TAG, "Needs unverified approval!");
return false;
}
if (isNonBlockingApprovalRequired(identityRecord.get())) {
if (isNonBlockingApprovalRequired(identityRecord)) {
Log.w(TAG, "Needs non-blocking approval!");
return false;
}
@ -161,10 +240,78 @@ public class TextSecureIdentityKeyStore implements IdentityKeyStore {
return true;
}
private boolean isNonBlockingApprovalRequired(IdentityRecord identityRecord) {
return !identityRecord.isFirstUse() &&
System.currentTimeMillis() - identityRecord.getTimestamp() < TimeUnit.SECONDS.toMillis(TIMESTAMP_THRESHOLD_SECONDS) &&
!identityRecord.isApprovedNonBlocking();
private boolean isNonBlockingApprovalRequired(IdentityStoreRecord record) {
return !record.getFirstUse() &&
!record.getNonblockingApproval() &&
System.currentTimeMillis() - record.getTimestamp() < TimeUnit.SECONDS.toMillis(TIMESTAMP_THRESHOLD_SECONDS);
}
private static final class Cache {
private final Map<String, IdentityStoreRecord> cache;
private final IdentityDatabase identityDatabase;
Cache(@NonNull IdentityDatabase identityDatabase) {
this.identityDatabase = identityDatabase;
this.cache = new LRUCache<>(200);
}
public synchronized @Nullable IdentityStoreRecord get(@NonNull String addressName) {
if (cache.containsKey(addressName)) {
return cache.get(addressName);
} else {
IdentityStoreRecord record = identityDatabase.getIdentityStoreRecord(addressName);
cache.put(addressName, record);
return record;
}
}
public synchronized void save(@NonNull String addressName, @NonNull RecipientId recipientId, @NonNull IdentityKey identityKey, @NonNull VerifiedStatus verifiedStatus, boolean firstUse, long timestamp, boolean nonBlockingApproval) {
identityDatabase.saveIdentity(addressName, recipientId, identityKey, verifiedStatus, firstUse, timestamp, nonBlockingApproval);
cache.put(addressName, new IdentityStoreRecord(addressName, identityKey, verifiedStatus, firstUse, timestamp, nonBlockingApproval));
}
public synchronized void setApproval(@NonNull String addressName, @NonNull RecipientId recipientId, boolean nonblockingApproval) {
setApproval(addressName, recipientId, cache.get(addressName), nonblockingApproval);
}
public synchronized void setApproval(@NonNull String addressName, @NonNull RecipientId recipientId, @Nullable IdentityStoreRecord record, boolean nonblockingApproval) {
identityDatabase.setApproval(addressName, recipientId, nonblockingApproval);
if (record != null) {
cache.put(record.getAddressName(),
new IdentityStoreRecord(record.getAddressName(),
record.getIdentityKey(),
record.getVerifiedStatus(),
record.getFirstUse(),
record.getTimestamp(),
nonblockingApproval));
}
}
public synchronized void setVerified(@NonNull String addressName, @NonNull RecipientId recipientId, @NonNull IdentityKey identityKey, @NonNull VerifiedStatus verifiedStatus) {
identityDatabase.setVerified(addressName, recipientId, identityKey, verifiedStatus);
IdentityStoreRecord record = cache.get(addressName);
if (record != null) {
cache.put(addressName,
new IdentityStoreRecord(record.getAddressName(),
record.getIdentityKey(),
verifiedStatus,
record.getFirstUse(),
record.getTimestamp(),
record.getNonblockingApproval()));
}
}
public synchronized void delete(@NonNull String addressName) {
identityDatabase.delete(addressName);
cache.remove(addressName);
}
public synchronized void invalidate(@NonNull String addressName) {
cache.remove(addressName);
}
}
public enum SaveResult {

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

View file

@ -18,6 +18,7 @@ import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.model.IdentityRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
@ -144,10 +145,10 @@ public class MultiDeviceContactUpdateJob extends BaseJob {
return;
}
Optional<IdentityDatabase.IdentityRecord> identityRecord = DatabaseFactory.getIdentityDatabase(context).getIdentity(recipient.getId());
Optional<VerifiedMessage> verifiedMessage = getVerifiedMessage(recipient, identityRecord);
Map<RecipientId, Integer> inboxPositions = DatabaseFactory.getThreadDatabase(context).getInboxPositions();
Set<RecipientId> archived = DatabaseFactory.getThreadDatabase(context).getArchivedRecipients();
Optional<IdentityRecord> identityRecord = ApplicationDependencies.getIdentityStore().getIdentityRecord(recipient.getId());
Optional<VerifiedMessage> verifiedMessage = getVerifiedMessage(recipient, identityRecord);
Map<RecipientId, Integer> inboxPositions = DatabaseFactory.getThreadDatabase(context).getInboxPositions();
Set<RecipientId> archived = DatabaseFactory.getThreadDatabase(context).getArchivedRecipients();
out.write(new DeviceContact(RecipientUtil.toSignalServiceAddress(context, recipient),
Optional.fromNullable(recipient.isGroup() || recipient.isSystemContact() ? recipient.getDisplayName(context) : null),
@ -203,13 +204,13 @@ public class MultiDeviceContactUpdateJob extends BaseJob {
Set<RecipientId> archived = DatabaseFactory.getThreadDatabase(context).getArchivedRecipients();
for (Recipient recipient : recipients) {
Optional<IdentityDatabase.IdentityRecord> identity = DatabaseFactory.getIdentityDatabase(context).getIdentity(recipient.getId());
Optional<VerifiedMessage> verified = getVerifiedMessage(recipient, identity);
Optional<String> name = Optional.fromNullable(recipient.isSystemContact() ? recipient.getDisplayName(context) : recipient.getGroupName(context));
Optional<ProfileKey> profileKey = ProfileKeyUtil.profileKeyOptional(recipient.getProfileKey());
boolean blocked = recipient.isBlocked();
Optional<Integer> expireTimer = recipient.getExpiresInSeconds() > 0 ? Optional.of(recipient.getExpiresInSeconds()) : Optional.absent();
Optional<Integer> inboxPosition = Optional.fromNullable(inboxPositions.get(recipient.getId()));
Optional<IdentityRecord> identity = ApplicationDependencies.getIdentityStore().getIdentityRecord(recipient.getId());
Optional<VerifiedMessage> verified = getVerifiedMessage(recipient, identity);
Optional<String> name = Optional.fromNullable(recipient.isSystemContact() ? recipient.getDisplayName(context) : recipient.getGroupName(context));
Optional<ProfileKey> profileKey = ProfileKeyUtil.profileKeyOptional(recipient.getProfileKey());
boolean blocked = recipient.isBlocked();
Optional<Integer> expireTimer = recipient.getExpiresInSeconds() > 0 ? Optional.of(recipient.getExpiresInSeconds()) : Optional.absent();
Optional<Integer> inboxPosition = Optional.fromNullable(inboxPositions.get(recipient.getId()));
out.write(new DeviceContact(RecipientUtil.toSignalServiceAddress(context, recipient),
name,
@ -386,7 +387,7 @@ public class MultiDeviceContactUpdateJob extends BaseJob {
}
}
private Optional<VerifiedMessage> getVerifiedMessage(Recipient recipient, Optional<IdentityDatabase.IdentityRecord> identity)
private Optional<VerifiedMessage> getVerifiedMessage(Recipient recipient, Optional<IdentityRecord> identity)
throws InvalidNumberException, IOException
{
if (!identity.isPresent()) return Optional.absent();

View file

@ -365,10 +365,7 @@ public class RetrieveProfileJob extends BaseJob {
IdentityKey identityKey = new IdentityKey(Base64.decode(identityKeyValue), 0);
if (!DatabaseFactory.getIdentityDatabase(context)
.getIdentity(recipient.getId())
.isPresent())
{
if (!ApplicationDependencies.getIdentityStore().getIdentityRecord(recipient.getId()).isPresent()) {
Log.w(TAG, "Still first use...");
return;
}

View file

@ -552,7 +552,7 @@ public final class MessageContentProcessor {
database.markAsMissedCall(smsMessageId.get(), message.getType() == OfferMessage.Type.VIDEO_CALL);
} else {
RemotePeer remotePeer = new RemotePeer(senderRecipient.getId());
byte[] remoteIdentityKey = DatabaseFactory.getIdentityDatabase(context).getIdentity(senderRecipient.getId()).transform(record -> record.getIdentityKey().serialize()).orNull();
byte[] remoteIdentityKey = ApplicationDependencies.getIdentityStore().getIdentityRecord(senderRecipient.getId()).transform(record -> record.getIdentityKey().serialize()).orNull();
ApplicationDependencies.getSignalCallManager()
.receivedOffer(new WebRtcData.CallMetadata(remotePeer, new CallId(message.getId()), content.getSenderDevice()),
@ -570,7 +570,7 @@ public final class MessageContentProcessor {
{
log(String.valueOf(content), "handleCallAnswerMessage...");
RemotePeer remotePeer = new RemotePeer(senderRecipient.getId());
byte[] remoteIdentityKey = DatabaseFactory.getIdentityDatabase(context).getIdentity(senderRecipient.getId()).transform(record -> record.getIdentityKey().serialize()).orNull();
byte[] remoteIdentityKey = ApplicationDependencies.getIdentityStore().getIdentityRecord(senderRecipient.getId()).transform(record -> record.getIdentityKey().serialize()).orNull();
ApplicationDependencies.getSignalCallManager()
.receivedAnswer(new WebRtcData.CallMetadata(remotePeer, new CallId(message.getId()), content.getSenderDevice()),

View file

@ -12,6 +12,8 @@ import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.model.IdentityRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupChangeException;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupManager;
@ -51,11 +53,9 @@ final class RecipientDialogRepository {
return groupId;
}
void getIdentity(@NonNull Consumer<IdentityDatabase.IdentityRecord> callback) {
void getIdentity(@NonNull Consumer<IdentityRecord> callback) {
SignalExecutors.BOUNDED.execute(
() -> callback.accept(DatabaseFactory.getIdentityDatabase(context)
.getIdentity(recipientId)
.orNull()));
() -> callback.accept(ApplicationDependencies.getIdentityStore().getIdentityRecord(recipientId).orNull()));
}
void getRecipient(@NonNull RecipientCallback recipientCallback) {

View file

@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.VerifyIdentityActivity;
import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsActivity;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.model.IdentityRecord;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.LiveGroup;
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
@ -37,13 +38,13 @@ import java.util.Objects;
final class RecipientDialogViewModel extends ViewModel {
private final Context context;
private final RecipientDialogRepository recipientDialogRepository;
private final LiveData<Recipient> recipient;
private final MutableLiveData<IdentityDatabase.IdentityRecord> identity;
private final LiveData<AdminActionStatus> adminActionStatus;
private final LiveData<Boolean> canAddToAGroup;
private final MutableLiveData<Boolean> adminActionBusy;
private final Context context;
private final RecipientDialogRepository recipientDialogRepository;
private final LiveData<Recipient> recipient;
private final MutableLiveData<IdentityRecord> identity;
private final LiveData<AdminActionStatus> adminActionStatus;
private final LiveData<Boolean> canAddToAGroup;
private final MutableLiveData<Boolean> adminActionBusy;
private RecipientDialogViewModel(@NonNull Context context,
@NonNull RecipientDialogRepository recipientDialogRepository)
@ -101,7 +102,7 @@ final class RecipientDialogViewModel extends ViewModel {
return adminActionStatus;
}
LiveData<IdentityDatabase.IdentityRecord> getIdentity() {
LiveData<IdentityRecord> getIdentity() {
return identity;
}
@ -133,7 +134,7 @@ final class RecipientDialogViewModel extends ViewModel {
recipientDialogRepository.getRecipient(recipient -> BlockUnblockDialog.showUnblockFor(activity, activity.getLifecycle(), recipient, () -> RecipientUtil.unblock(context, recipient)));
}
void onViewSafetyNumberClicked(@NonNull Activity activity, @NonNull IdentityDatabase.IdentityRecord identityRecord) {
void onViewSafetyNumberClicked(@NonNull Activity activity, @NonNull IdentityRecord identityRecord) {
activity.startActivity(VerifyIdentityActivity.newIntent(activity, identityRecord));
}

View file

@ -157,10 +157,13 @@ public final class RegistrationRepository {
TextSecurePreferences.setFcmDisabled(context, registrationData.isNotFcm());
TextSecurePreferences.setWebsocketRegistered(context, true);
DatabaseFactory.getIdentityDatabase(context)
.saveIdentity(selfId,
identityKey.getPublicKey(), IdentityDatabase.VerifiedStatus.VERIFIED,
true, System.currentTimeMillis(), true);
ApplicationDependencies.getIdentityStore()
.saveIdentityWithoutSideEffects(selfId,
identityKey.getPublicKey(),
IdentityDatabase.VerifiedStatus.VERIFIED,
true,
System.currentTimeMillis(),
true);
TextSecurePreferences.setPushRegistered(context, true);
TextSecurePreferences.setPushServerPassword(context, registrationData.getPassword());

View file

@ -10,11 +10,12 @@ import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.database.model.IdentityRecord;
import org.thoughtcrime.securesms.database.MessageDatabase;
import org.thoughtcrime.securesms.database.MessageDatabase.InsertResult;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
@ -52,8 +53,7 @@ public final class IdentityUtil {
final RecipientId recipientId = recipient.getId();
SimpleTask.run(SignalExecutors.BOUNDED,
() -> DatabaseFactory.getIdentityDatabase(context)
.getIdentity(recipientId),
() -> ApplicationDependencies.getIdentityStore().getIdentityRecord(recipientId),
future::set);
return future;
@ -161,9 +161,9 @@ public final class IdentityUtil {
public static void processVerifiedMessage(Context context, VerifiedMessage verifiedMessage) {
try(SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
Recipient recipient = Recipient.externalPush(context, verifiedMessage.getDestination());
Optional<IdentityRecord> identityRecord = identityDatabase.getIdentity(recipient.getId());
TextSecureIdentityKeyStore identityStore = ApplicationDependencies.getIdentityStore();
Recipient recipient = Recipient.externalPush(context, verifiedMessage.getDestination());
Optional<IdentityRecord> identityRecord = identityStore.getIdentityRecord(recipient.getId());
if (!identityRecord.isPresent() && verifiedMessage.getVerified() == VerifiedMessage.VerifiedState.DEFAULT) {
Log.w(TAG, "No existing record for default status");
@ -175,7 +175,7 @@ public final class IdentityUtil {
identityRecord.get().getIdentityKey().equals(verifiedMessage.getIdentityKey()) &&
identityRecord.get().getVerifiedStatus() != IdentityDatabase.VerifiedStatus.DEFAULT)
{
identityDatabase.setVerified(recipient.getId(), identityRecord.get().getIdentityKey(), IdentityDatabase.VerifiedStatus.DEFAULT);
identityStore.setVerified(recipient.getId(), identityRecord.get().getIdentityKey(), IdentityDatabase.VerifiedStatus.DEFAULT);
markIdentityVerified(context, recipient, false, true);
}
@ -185,7 +185,7 @@ public final class IdentityUtil {
(identityRecord.isPresent() && identityRecord.get().getVerifiedStatus() != IdentityDatabase.VerifiedStatus.VERIFIED)))
{
saveIdentity(verifiedMessage.getDestination().getIdentifier(), verifiedMessage.getIdentityKey());
identityDatabase.setVerified(recipient.getId(), verifiedMessage.getIdentityKey(), IdentityDatabase.VerifiedStatus.VERIFIED);
identityStore.setVerified(recipient.getId(), verifiedMessage.getIdentityKey(), IdentityDatabase.VerifiedStatus.VERIFIED);
markIdentityVerified(context, recipient, true, true);
}
}

View file

@ -0,0 +1,66 @@
package org.thoughtcrime.securesms.crypto.storage
import android.content.Context
import junit.framework.Assert.assertEquals
import org.junit.Test
import org.mockito.Mockito.`when`
import org.mockito.Mockito.mock
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.thoughtcrime.securesms.database.IdentityDatabase
import org.thoughtcrime.securesms.database.model.IdentityStoreRecord
import org.whispersystems.libsignal.IdentityKey
import org.whispersystems.libsignal.SignalProtocolAddress
import org.whispersystems.libsignal.ecc.ECPublicKey
class TextSecureIdentityKeyStoreTest {
companion object {
private const val ADDRESS = "address1"
}
@Test
fun `getIdentity() hits disk on first retrieve but not the second`() {
val mockDb = mock(IdentityDatabase::class.java)
val subject = TextSecureIdentityKeyStore(mock(Context::class.java), mockDb)
val identityKey = IdentityKey(ECPublicKey.fromPublicKeyBytes(ByteArray(32)))
val record = mockRecord(ADDRESS, identityKey)
`when`(mockDb.getIdentityStoreRecord(ADDRESS)).thenReturn(record)
assertEquals(identityKey, subject.getIdentity(SignalProtocolAddress(ADDRESS, 1)))
verify(mockDb, times(1)).getIdentityStoreRecord(ADDRESS)
assertEquals(identityKey, subject.getIdentity(SignalProtocolAddress(ADDRESS, 1)))
verify(mockDb, times(1)).getIdentityStoreRecord(ADDRESS)
}
@Test
fun `invalidate() evicts cache entry`() {
val mockDb = mock(IdentityDatabase::class.java)
val subject = TextSecureIdentityKeyStore(mock(Context::class.java), mockDb)
val identityKey = IdentityKey(ECPublicKey.fromPublicKeyBytes(ByteArray(32)))
val record = mockRecord(ADDRESS, identityKey)
`when`(mockDb.getIdentityStoreRecord(ADDRESS)).thenReturn(record)
assertEquals(identityKey, subject.getIdentity(SignalProtocolAddress(ADDRESS, 1)))
verify(mockDb, times(1)).getIdentityStoreRecord(ADDRESS)
subject.invalidate(ADDRESS)
assertEquals(identityKey, subject.getIdentity(SignalProtocolAddress(ADDRESS, 1)))
verify(mockDb, times(2)).getIdentityStoreRecord(ADDRESS)
}
private fun mockRecord(addressName: String, identityKey: IdentityKey): IdentityStoreRecord {
return IdentityStoreRecord(
addressName = addressName,
identityKey = identityKey,
verifiedStatus = IdentityDatabase.VerifiedStatus.DEFAULT,
firstUse = false,
timestamp = 1,
nonblockingApproval = true
)
}
}