Some identity key handling changes

1) Prefetch identity keys when possible

2) Always accept prefetched keys or keys from incoming messages

3) Block sending only if it's a recent change, or if always
   block is enabled

// FREEBIE
master
Moxie Marlinspike 2017-05-19 18:01:40 -07:00
parent ca701df1e4
commit d507756821
19 changed files with 476 additions and 248 deletions

View File

@ -61,7 +61,7 @@ dependencies {
compile 'org.whispersystems:jobmanager:1.0.2'
compile 'org.whispersystems:libpastelog:1.0.7'
compile 'org.whispersystems:signal-service-android:2.5.6'
compile 'org.whispersystems:signal-service-android:2.5.7'
compile 'org.whispersystems:webrtc-android:M57-S2'
compile "me.leolin:ShortcutBadger:1.1.16"
@ -135,7 +135,7 @@ dependencyVerification {
'com.google.android.exoplayer:exoplayer:955085aa611a8f7cf6c61b88ae03d1a392f4ad94c9bfbc153f3dedb9ffb14718',
'org.whispersystems:jobmanager:506f679fc2fcf7bb6d10f00f41d6f6ea0abf75c70dc95b913398661ad538a181',
'org.whispersystems:libpastelog:bb331d9a98240fc139101128ba836c1edec3c40e000597cdbb29ebf4cbf34d88',
'org.whispersystems:signal-service-android:d19edb9faaa59cf9b3550942a030c8fd73d939003cda16955ed6c71209dd4d29',
'org.whispersystems:signal-service-android:ef8e97ceef05909713dd5247f52d114cb0a30c3f48e79486d2e583ee4dbb89d5',
'org.whispersystems:webrtc-android:9d11e39d4b3823713e5b1486226e0ce09f989d6f47f52da1815e406c186701d5',
'me.leolin:ShortcutBadger:e3cb3e7625892129b0c92dd5e4bc649faffdd526d5af26d9c45ee31ff8851774',
'se.emilsjolander:stickylistheaders:a08ca948aa6b220f09d82f16bbbac395f6b78897e9eeac6a9f0b0ba755928eeb',
@ -169,8 +169,8 @@ dependencyVerification {
'com.google.android.gms:play-services-base:0ca636a8fc9a5af45e607cdcd61783bf5d561cbbb0f862021ce69606eee5ad49',
'com.google.android.gms:play-services-basement:95dd882c5ffba15b9a99de3fefb05d3a01946623af67454ca00055d222f85a8d',
'com.google.android.gms:play-services-iid:54e919f9957b8b7820da7ee9b83471d00d0cac1cf08ddea8b5b41aea80bb1a70',
'org.whispersystems:signal-service-java:a410adf969fc80119f0e04b2c0d4fcec0f9fcca11a098b3782c02925b61dfbad',
'org.whispersystems:signal-protocol-android:1b4b9d557c8eaf861797ff683990d482d4aa8e9f23d9b17ff0cc67a02f38cb19',
'org.whispersystems:signal-service-java:640e374e8bc5d4d2c33f0e0e51b1a88283c0f97a5ea89f8c81aaa957afd78f5c',
'org.whispersystems:signal-protocol-android:b6921cd5e2f237eb523cc8e0c301b022fb5109e4c8e4dca7bb873da6efbaa939',
'com.nineoldandroids:library:68025a14e3e7673d6ad2f95e4b46d78d7d068343aa99256b686fe59de1b3163a',
'javax.inject:javax.inject:91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff',
'com.madgag.spongycastle:core:8d6240b974b0aca4d3da9c7dd44d42339d8a374358aca5fc98e50a995764511f',
@ -181,7 +181,7 @@ dependencyVerification {
'com.fasterxml.jackson.core:jackson-databind:835097bcdd11f5bc8a08378c70d4c8054dfa4b911691cc2752063c75534d198d',
'com.squareup.okhttp3:okhttp:a992938d7203ca557cd7a116f002e8c427ec9cdae7ea852441abb8aec891f948',
'org.whispersystems:curve25519-android:bf6c34223d45d2f2813a8efcab9923caf99115115c760c9acea680bcb42d23c0',
'org.whispersystems:signal-protocol-java:a835cd0609cf116a74651bd0aa748db9392bba48c2d2af787757b8a1b50d131c',
'org.whispersystems:signal-protocol-java:e184dee4c8c1900ce152f2cc9d539c97a0e42dd5f06663cd8e26b069a289ff61',
'com.fasterxml.jackson.core:jackson-annotations:0ca408c24202a7626ec8b861e99d85eca5e38b73311dd6dd12e3e9deecc3fe94',
'com.fasterxml.jackson.core:jackson-core:cbf4604784b4de226262845447a1ad3bb38a6728cebe86562e2c5afada8be2c0',
'com.squareup.okio:okio:8c5436cadfab36bbd97db5f5c43b7bfdb5bf2f5f894ec8709b1929f14bdd010c',

View File

@ -35,8 +35,8 @@
<PreferenceCategory android:title="@string/preferences_app_protection__communication">
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat
android:defaultValue="true"
android:key="pref_blocking_identity_changes"
android:defaultValue="false"
android:key="pref_approve_identity_changes"
android:title="@string/preferences_app_protection__safety_numbers_approval"
android:summary="@string/preferences_app_protecting__require_approval_of_new_safety_numbers_when_they_change"/>

View File

@ -8,12 +8,13 @@ import android.support.v7.app.AlertDialog;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.util.Log;
import android.widget.TextView;
import org.thoughtcrime.securesms.crypto.MasterSecret;
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.IdentityDatabase;
import org.thoughtcrime.securesms.database.MmsAddressDatabase;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
@ -21,7 +22,6 @@ import org.thoughtcrime.securesms.database.PushDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.jobs.IdentityUpdateJob;
import org.thoughtcrime.securesms.jobs.PushDecryptJob;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
@ -30,12 +30,15 @@ import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.VerifySpan;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import java.io.IOException;
import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
public class ConfirmIdentityDialog extends AlertDialog {
private static final String TAG = ConfirmIdentityDialog.class.getSimpleName();
@ -102,20 +105,17 @@ public class ConfirmIdentityDialog extends AlertDialog {
{
@Override
protected Void doInBackground(Void... params) {
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(getContext());
synchronized (SESSION_LOCK) {
SignalProtocolAddress mismatchAddress = new SignalProtocolAddress(number, 1);
identityDatabase.saveIdentity(mismatch.getRecipientId(),
mismatch.getIdentityKey());
new TextSecureSessionStore(getContext()).deleteAllSessions(number);
if (new TextSecureIdentityKeyStore(getContext()).saveIdentity(mismatchAddress, mismatch.getIdentityKey(), true, true)) {
new TextSecureSessionStore(getContext()).deleteAllSessions(number);
}
}
processMessageRecord(messageRecord);
processPendingMessageRecords(messageRecord.getThreadId(), mismatch);
ApplicationContext.getInstance(getContext())
.getJobManager()
.add(new IdentityUpdateJob(getContext(), mismatch.getRecipientId()));
return null;
}

View File

@ -105,6 +105,7 @@ import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase.Recipient
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.mms.AttachmentManager;
import org.thoughtcrime.securesms.mms.AttachmentManager.MediaType;
import org.thoughtcrime.securesms.mms.AudioSlide;
@ -260,6 +261,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
initializeDraft();
}
});
initializeProfiles();
}
@Override
@ -315,6 +317,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
MessageNotifier.setVisibleThread(threadId);
markThreadAsRead();
markIdentitySeen();
Log.w(TAG, "onResume() Finished: " + (System.currentTimeMillis() - getIntent().getLongExtra(TIMING_EXTRA, 0)));
}
@ -1142,6 +1145,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
recipients.addListener(this);
}
private void initializeProfiles() {
ApplicationContext.getInstance(this)
.getJobManager()
.add(new RetrieveProfileJob(this, recipients));
}
@Override
public void onModified(final Recipients recipients) {
titleView.post(new Runnable() {
@ -1434,6 +1443,17 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}.execute(threadId);
}
private void markIdentitySeen() {
new AsyncTask<Recipient, Void, Void>() {
@Override
protected Void doInBackground(Recipient... params) {
DatabaseFactory.getIdentityDatabase(ConversationActivity.this)
.setSeen(params[0].getRecipientId());
return null;
}
}.execute(recipients.getPrimaryRecipient());
}
protected void sendComplete(long threadId) {
boolean refreshFragment = (threadId != this.threadId);
this.threadId = threadId;

View File

@ -38,8 +38,8 @@ import org.greenrobot.eventbus.ThreadMode;
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallControls;
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallScreen;
import org.thoughtcrime.securesms.components.webrtc.WebRtcIncomingCallOverlay;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
import org.thoughtcrime.securesms.recipients.Recipient;
@ -49,6 +49,9 @@ import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.SignalProtocolAddress;
import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
public class WebRtcCallActivity extends Activity {
@ -254,8 +257,11 @@ public class WebRtcCallActivity extends Activity {
callScreen.setAcceptIdentityListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(WebRtcCallActivity.this);
identityDatabase.saveIdentity(recipient.getRecipientId(), theirIdentity);
synchronized (SESSION_LOCK) {
if (new TextSecureIdentityKeyStore(WebRtcCallActivity.this).saveIdentity(new SignalProtocolAddress(recipient.getNumber(), 1), theirIdentity, true, true)) {
new TextSecureSessionStore(WebRtcCallActivity.this).deleteAllSessions(recipient.getNumber());
}
}
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
intent.putExtra(WebRtcCallService.EXTRA_REMOTE_NUMBER, recipient.getNumber());

View File

@ -42,13 +42,13 @@ public class SignalProtocolStoreImpl implements SignalProtocolStore {
}
@Override
public void saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
identityKeyStore.saveIdentity(address, identityKey);
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
return identityKeyStore.saveIdentity(address, identityKey);
}
@Override
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
return identityKeyStore.isTrustedIdentity(address, identityKey);
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) {
return identityKeyStore.isTrustedIdentity(address, identityKey, direction);
}
@Override

View File

@ -1,21 +1,31 @@
package org.thoughtcrime.securesms.crypto.storage;
import android.content.Context;
import android.util.Log;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.SessionUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.jobs.IdentityUpdateJob;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.state.IdentityKeyStore;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.concurrent.TimeUnit;
public class TextSecureIdentityKeyStore implements IdentityKeyStore {
private static final int TIMESTAMP_THRESHOLD_SECONDS = 5;
private static final String TAG = TextSecureIdentityKeyStore.class.getSimpleName();
private static final Object LOCK = new Object();
private final Context context;
public TextSecureIdentityKeyStore(Context context) {
@ -32,31 +42,96 @@ public class TextSecureIdentityKeyStore implements IdentityKeyStore {
return TextSecurePreferences.getLocalRegistrationId(context);
}
@Override
public void saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
long recipientId = RecipientFactory.getRecipientsFromString(context, address.getName(), true).getPrimaryRecipient().getRecipientId();
DatabaseFactory.getIdentityDatabase(context).saveIdentity(recipientId, identityKey);
}
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey,
boolean blockingApproval, boolean nonBlockingApproval)
{
synchronized (LOCK) {
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
Recipients recipients = RecipientFactory.getRecipientsFromString(context, address.getName(), true);
long recipientId = recipients.getPrimaryRecipient().getRecipientId();
Optional<IdentityRecord> identityRecord = identityDatabase.getIdentity(recipientId);
@Override
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
long recipientId = RecipientFactory.getRecipientsFromString(context, address.getName(), true).getPrimaryRecipient().getRecipientId();
boolean trusted = DatabaseFactory.getIdentityDatabase(context)
.isValidIdentity(recipientId, identityKey);
if (!identityRecord.isPresent()) {
Log.w(TAG, "Saving new identity...");
identityDatabase.saveIdentity(recipientId, identityKey, true, System.currentTimeMillis(), blockingApproval, nonBlockingApproval);
return false;
}
if (trusted) {
return true;
} else if (!TextSecurePreferences.isBlockingIdentityUpdates(context)) {
saveIdentity(address, identityKey);
new TextSecureSessionStore(context).deleteAllSessions(address.getName());
if (!identityRecord.get().getIdentityKey().equals(identityKey)) {
Log.w(TAG, "Replacing existing identity...");
identityDatabase.saveIdentity(recipientId, identityKey, false, System.currentTimeMillis(), blockingApproval, nonBlockingApproval);
IdentityUtil.markIdentityUpdate(context, recipients.getPrimaryRecipient());
return true;
}
ApplicationContext.getInstance(context)
.getJobManager()
.add(new IdentityUpdateJob(context, recipientId));
if (isBlockingApprovalRequired(identityRecord.get()) || isNonBlockingApprovalRequired(identityRecord.get())) {
Log.w(TAG, "Setting approval status...");
identityDatabase.setApproval(recipientId, blockingApproval, nonBlockingApproval);
return false;
}
return true;
} else {
return false;
}
}
@Override
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
return saveIdentity(address, identityKey, !TextSecurePreferences.isSendingIdentityApprovalRequired(context), false);
}
@Override
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) {
synchronized (LOCK) {
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
long recipientId = RecipientFactory.getRecipientsFromString(context, address.getName(), true).getPrimaryRecipient().getRecipientId();
String ourNumber = TextSecurePreferences.getLocalNumber(context);
long ourRecipientId = RecipientFactory.getRecipientsFromString(context, ourNumber, true).getPrimaryRecipient().getRecipientId();
if (ourRecipientId == recipientId || ourNumber.equals(address.getName())) {
return identityKey.equals(IdentityKeyUtil.getIdentityKey(context));
}
switch (direction) {
case SENDING: return isTrustedForSending(identityKey, identityDatabase.getIdentity(recipientId));
case RECEIVING: return true;
default: throw new AssertionError("Unknown direction: " + direction);
}
}
}
private boolean isTrustedForSending(IdentityKey identityKey, Optional<IdentityRecord> identityRecord) {
if (!identityRecord.isPresent()) {
Log.w(TAG, "Nothing here, returning true...");
return true;
}
if (!identityKey.equals(identityRecord.get().getIdentityKey())) {
Log.w(TAG, "Identity keys don't match...");
return false;
}
if (isBlockingApprovalRequired(identityRecord.get())) {
Log.w(TAG, "Needs blocking approval!");
return false;
}
if (isNonBlockingApprovalRequired(identityRecord.get())) {
Log.w(TAG, "Needs non-blocking approval!");
return false;
}
return true;
}
private boolean isBlockingApprovalRequired(IdentityRecord identityRecord) {
return !identityRecord.isFirstUse() &&
TextSecurePreferences.isSendingIdentityApprovalRequired(context) &&
!identityRecord.isApprovedBlocking();
}
private boolean isNonBlockingApprovalRequired(IdentityRecord identityRecord) {
return !identityRecord.isFirstUse() &&
System.currentTimeMillis() - identityRecord.getTimestamp() < TimeUnit.SECONDS.toMillis(TIMESTAMP_THRESHOLD_SECONDS) &&
!identityRecord.isApprovedNonBlocking();
}
}

View File

@ -78,7 +78,8 @@ public class DatabaseFactory {
private static final int INTRODUCED_DOCUMENTS = 32;
private static final int INTRODUCED_FAST_PREFLIGHT = 33;
private static final int INTRODUCED_VOICE_NOTES = 34;
private static final int DATABASE_VERSION = 34;
private static final int INTRODUCED_IDENTITY_TIMESTAMP = 35;
private static final int DATABASE_VERSION = 35;
private static final String DATABASE_NAME = "messages.db";
private static final Object lock = new Object();
@ -867,6 +868,17 @@ public class DatabaseFactory {
db.execSQL("ALTER TABLE part ADD COLUMN voice_note INTEGER DEFAULT 0");
}
if (oldVersion < INTRODUCED_IDENTITY_TIMESTAMP) {
db.execSQL("ALTER TABLE identities ADD COLUMN timestamp INTEGER DEFAULT 0");
db.execSQL("ALTER TABLE identities ADD COLUMN first_use INTEGER DEFAULT 0");
db.execSQL("ALTER TABLE identities ADD COLUMN seen INTEGER DEFAULT 0");
db.execSQL("ALTER TABLE identities ADD COLUMN blocking_approval INTEGER DEFAULT 0");
db.execSQL("ALTER TABLE identities ADD COLUMN nonblocking_approval INTEGER DEFAULT 0");
db.execSQL("DROP INDEX archived_count_index");
db.execSQL("CREATE INDEX IF NOT EXISTS archived_count_index ON thread (archived, message_count)");
}
db.setTransactionSuccessful();
db.endTransaction();
}

View File

@ -24,74 +24,73 @@ import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.util.Log;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.Base64;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
public class IdentityDatabase extends Database {
private static final Uri CHANGE_URI = Uri.parse("content://textsecure/identities");
private static final String TAG = IdentityDatabase.class.getSimpleName();
private static final String TABLE_NAME = "identities";
private static final String ID = "_id";
public static final String RECIPIENT = "recipient";
public static final String IDENTITY_KEY = "key";
private static final Uri CHANGE_URI = Uri.parse("content://textsecure/identities");
private static final String TABLE_NAME = "identities";
private static final String ID = "_id";
private static final String RECIPIENT = "recipient";
private static final String IDENTITY_KEY = "key";
private static final String TIMESTAMP = "timestamp";
private static final String FIRST_USE = "first_use";
private static final String SEEN = "seen";
private static final String BLOCKING_APPROVAL = "blocking_approval";
private static final String NONBLOCKING_APPROVAL = "nonblocking_approval";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME +
" (" + ID + " INTEGER PRIMARY KEY, " +
RECIPIENT + " INTEGER UNIQUE, " +
IDENTITY_KEY + " TEXT);";
IDENTITY_KEY + " TEXT, " +
FIRST_USE + " INTEGER DEFAULT 0, " +
TIMESTAMP + " INTEGER DEFAULT 0, " +
SEEN + " INTEGER DEFAULT 0, " +
BLOCKING_APPROVAL + " INTEGER DEFAULT 0, " +
NONBLOCKING_APPROVAL + " INTEGER DEFAULT 0);";
public IdentityDatabase(Context context, SQLiteOpenHelper databaseHelper) {
IdentityDatabase(Context context, SQLiteOpenHelper databaseHelper) {
super(context, databaseHelper);
}
public Cursor getIdentities() {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
Cursor cursor = database.query(TABLE_NAME, null, null, null, null, null, null);
if (cursor != null)
cursor.setNotificationUri(context.getContentResolver(), CHANGE_URI);
return cursor;
}
public boolean isValidIdentity(long recipientId,
IdentityKey theirIdentity)
{
public Optional<IdentityRecord> getIdentity(long recipientId) {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
Cursor cursor = null;
try {
cursor = database.query(TABLE_NAME, null, RECIPIENT + " = ?",
new String[] {recipientId+""}, null, null,null);
new String[] {recipientId + ""}, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
String serializedIdentity = cursor.getString(cursor.getColumnIndexOrThrow(IDENTITY_KEY));
IdentityKey ourIdentity = new IdentityKey(Base64.decode(serializedIdentity), 0);
String serializedIdentity = cursor.getString(cursor.getColumnIndexOrThrow(IDENTITY_KEY));
long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP));
long seen = cursor.getLong(cursor.getColumnIndexOrThrow(SEEN));
boolean blockingApproval = cursor.getInt(cursor.getColumnIndexOrThrow(BLOCKING_APPROVAL)) == 1;
boolean nonblockingApproval = cursor.getInt(cursor.getColumnIndexOrThrow(NONBLOCKING_APPROVAL)) == 1;
boolean firstUse = cursor.getInt(cursor.getColumnIndexOrThrow(FIRST_USE)) == 1;
IdentityKey identity = new IdentityKey(Base64.decode(serializedIdentity), 0);
return ourIdentity.equals(theirIdentity);
} else {
return true;
return Optional.of(new IdentityRecord(identity, firstUse, timestamp, seen, blockingApproval, nonblockingApproval));
}
} catch (IOException e) {
Log.w("IdentityDatabase", e);
return false;
} catch (InvalidKeyException e) {
Log.w("IdentityDatabase", e);
return false;
} catch (InvalidKeyException | IOException e) {
throw new AssertionError(e);
} finally {
if (cursor != null) {
cursor.close();
}
if (cursor != null) cursor.close();
}
return Optional.absent();
}
public void saveIdentity(long recipientId, IdentityKey identityKey)
public void saveIdentity(long recipientId, IdentityKey identityKey, boolean firstUse,
long timestamp, boolean blockingApproval, boolean nonBlockingApproval)
{
SQLiteDatabase database = databaseHelper.getWritableDatabase();
String identityKeyString = Base64.encodeBytes(identityKey.serialize());
@ -99,65 +98,85 @@ public class IdentityDatabase extends Database {
ContentValues contentValues = new ContentValues();
contentValues.put(RECIPIENT, recipientId);
contentValues.put(IDENTITY_KEY, identityKeyString);
contentValues.put(TIMESTAMP, timestamp);
contentValues.put(BLOCKING_APPROVAL, blockingApproval ? 1 : 0);
contentValues.put(NONBLOCKING_APPROVAL, nonBlockingApproval ? 1 : 0);
contentValues.put(FIRST_USE, firstUse ? 1 : 0);
contentValues.put(SEEN, 0);
database.replace(TABLE_NAME, null, contentValues);
context.getContentResolver().notifyChange(CHANGE_URI, null);
}
public void deleteIdentity(long id) {
public void setApproval(long recipientId, boolean blockingApproval, boolean nonBlockingApproval) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
database.delete(TABLE_NAME, ID_WHERE, new String[] {id+""});
ContentValues contentValues = new ContentValues(2);
contentValues.put(BLOCKING_APPROVAL, blockingApproval);
contentValues.put(NONBLOCKING_APPROVAL, nonBlockingApproval);
database.update(TABLE_NAME, contentValues, RECIPIENT + " = ?",
new String[] {String.valueOf(recipientId)});
context.getContentResolver().notifyChange(CHANGE_URI, null);
}
public Reader readerFor(Cursor cursor) {
return new Reader(cursor);
public void setSeen(long recipientId) {
Log.w(TAG, "Setting seen to current time: " + recipientId);
SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues contentValues = new ContentValues(1);
contentValues.put(SEEN, System.currentTimeMillis());
database.update(TABLE_NAME, contentValues, RECIPIENT + " = ? AND " + SEEN + " = 0",
new String[] {String.valueOf(recipientId)});
}
public class Reader {
private final Cursor cursor;
public static class IdentityRecord {
public Reader(Cursor cursor) {
this.cursor = cursor;
}
private final IdentityKey identitykey;
private final boolean firstUse;
private final long timestamp;
private final long seen;
private final boolean blockingApproval;
private final boolean nonblockingApproval;
public Identity getCurrent() {
long recipientId = cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT));
Recipients recipients = RecipientFactory.getRecipientsForIds(context, new long[]{recipientId}, true);
try {
String identityKeyString = cursor.getString(cursor.getColumnIndexOrThrow(IDENTITY_KEY));
IdentityKey identityKey = new IdentityKey(Base64.decode(identityKeyString), 0);
return new Identity(recipients, identityKey);
} catch (IOException e) {
Log.w("IdentityDatabase", e);
return new Identity(recipients, null);
} catch (InvalidKeyException e) {
Log.w("IdentityDatabase", e);
return new Identity(recipients, null);
}
}
}
public static class Identity {
private final Recipients recipients;
private final IdentityKey identityKey;
public Identity(Recipients recipients, IdentityKey identityKey) {
this.recipients = recipients;
this.identityKey = identityKey;
}
public Recipients getRecipients() {
return recipients;
private IdentityRecord(IdentityKey identitykey, boolean firstUse, long timestamp,
long seen, boolean blockingApproval, boolean nonblockingApproval)
{
this.identitykey = identitykey;
this.firstUse = firstUse;
this.timestamp = timestamp;
this.seen = seen;
this.blockingApproval = blockingApproval;
this.nonblockingApproval = nonblockingApproval;
}
public IdentityKey getIdentityKey() {
return identityKey;
return identitykey;
}
public long getTimestamp() {
return timestamp;
}
public long getSeen() {
return seen;
}
public boolean isApprovedBlocking() {
return blockingApproval;
}
public boolean isApprovedNonBlocking() {
return nonblockingApproval;
}
public boolean isFirstUse() {
return firstUse;
}
}
}

View File

@ -83,7 +83,7 @@ public class ThreadDatabase extends Database {
public static final String[] CREATE_INDEXS = {
"CREATE INDEX IF NOT EXISTS thread_recipient_ids_index ON " + TABLE_NAME + " (" + RECIPIENT_IDS + ");",
"CREATE INDEX IF NOT EXISTS archived_index ON " + TABLE_NAME + " (" + ARCHIVED + ");",
"CREATE INDEX IF NOT EXISTS archived_count_index ON " + TABLE_NAME + " (" + ARCHIVED + ", " + MESSAGE_COUNT + ");",
};
public ThreadDatabase(Context context, SQLiteOpenHelper databaseHelper) {
@ -339,7 +339,7 @@ public class ThreadDatabase extends Database {
public Cursor getConversationList() {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, null, ARCHIVED + " = ?", new String[] {"0"}, null, null, DATE + " DESC");
Cursor cursor = db.query(TABLE_NAME, null, ARCHIVED + " = ? AND " + MESSAGE_COUNT + " != 0", new String[] {"0"}, null, null, DATE + " DESC");
setNotifyConverationListListeners(cursor);
@ -348,7 +348,7 @@ public class ThreadDatabase extends Database {
public Cursor getArchivedConversationList() {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, null, ARCHIVED + " = ?", new String[] {"1"}, null, null, DATE + " DESC");
Cursor cursor = db.query(TABLE_NAME, null, ARCHIVED + " = ? AND " + MESSAGE_COUNT + " != 0", new String[] {"1"}, null, null, DATE + " DESC");
setNotifyConverationListListeners(cursor);

View File

@ -1,23 +0,0 @@
package org.thoughtcrime.securesms.database.loaders;
import android.content.Context;
import android.database.Cursor;
import android.support.v4.content.CursorLoader;
import org.thoughtcrime.securesms.database.DatabaseFactory;
public class IdentityLoader extends CursorLoader {
private final Context context;
public IdentityLoader(Context context) {
super(context);
this.context = context.getApplicationContext();
}
@Override
public Cursor loadInBackground() {
return DatabaseFactory.getIdentityDatabase(context).getIdentities();
}
}

View File

@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.jobs.PushTextSendJob;
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob;
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
import org.thoughtcrime.securesms.jobs.RequestGroupInfoJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.jobs.RotateSignedPreKeyJob;
import org.thoughtcrime.securesms.push.SecurityEventListener;
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
@ -59,7 +60,8 @@ import dagger.Provides;
PushGroupUpdateJob.class,
AvatarDownloadJob.class,
RotateSignedPreKeyJob.class,
WebRtcCallService.class})
WebRtcCallService.class,
RetrieveProfileJob.class})
public class SignalCommunicationModule {
private final Context context;

View File

@ -1,87 +0,0 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.sms.IncomingIdentityUpdateMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.jobqueue.JobParameters;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
public class IdentityUpdateJob extends MasterSecretJob {
private static final String TAG = IdentityUpdateJob.class.getSimpleName();
private final long recipientId;
public IdentityUpdateJob(Context context, long recipientId) {
super(context, JobParameters.newBuilder()
.withGroupId("IdentityUpdateJob")
.withPersistence()
.create());
this.recipientId = recipientId;
}
@Override
public void onRun(MasterSecret masterSecret) {
Recipient recipient = RecipientFactory.getRecipientForId(context, recipientId, true);
Recipients recipients = RecipientFactory.getRecipientsFor(context, recipient, true);
long time = System.currentTimeMillis();
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
GroupDatabase.Reader reader = groupDatabase.getGroups();
String number = recipient.getNumber();
try {
number = Util.canonicalizeNumber(context, number);
} catch (InvalidNumberException e) {
Log.w(TAG, e);
}
GroupDatabase.GroupRecord groupRecord;
while ((groupRecord = reader.getNext()) != null) {
if (groupRecord.getMembers().contains(number) && groupRecord.isActive()) {
SignalServiceGroup group = new SignalServiceGroup(groupRecord.getId());
IncomingTextMessage incoming = new IncomingTextMessage(number, 1, time, null, Optional.of(group), 0);
IncomingIdentityUpdateMessage groupUpdate = new IncomingIdentityUpdateMessage(incoming);
smsDatabase.insertMessageInbox(groupUpdate);
}
}
if (threadDatabase.getThreadIdIfExistsFor(recipients) != -1) {
IncomingTextMessage incoming = new IncomingTextMessage(number, 1, time, null, Optional.<SignalServiceGroup>absent(), 0);
IncomingIdentityUpdateMessage individualUpdate = new IncomingIdentityUpdateMessage(incoming);
smsDatabase.insertMessageInbox(individualUpdate);
}
}
@Override
public boolean onShouldRetryThrowable(Exception exception) {
return false;
}
@Override
public void onAdded() {
}
@Override
public void onCanceled() {
}
}

View File

@ -0,0 +1,119 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.support.annotation.NonNull;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.service.MessageRetrievalService;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.whispersystems.jobqueue.JobParameters;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.state.IdentityKeyStore;
import org.whispersystems.signalservice.api.SignalServiceMessagePipe;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.SignalServiceProfile;
import java.io.IOException;
import javax.inject.Inject;
import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
public class RetrieveProfileJob extends ContextJob implements InjectableType {
private static final String TAG = RetrieveProfileJob.class.getSimpleName();
@Inject transient SignalServiceMessageReceiver receiver;
private final long[] recipientIds;
public RetrieveProfileJob(Context context, Recipients recipients) {
super(context, JobParameters.newBuilder()
.withRetryCount(3)
.create());
this.recipientIds = recipients.getIds();
}
@Override
public void onAdded() {}
@Override
public void onRun() throws IOException, InvalidKeyException {
Recipients recipients = RecipientFactory.getRecipientsForIds(context, recipientIds, true);
for (Recipient recipient : recipients) {
if (recipient.isGroupRecipient()) handleGroupRecipient(recipient);
else handleIndividualRecipient(recipient);
}
}
@Override
public boolean onShouldRetry(Exception e) {
return false;
}
@Override
public void onCanceled() {}
private void handleIndividualRecipient(Recipient recipient)
throws IOException, InvalidKeyException
{
SignalServiceProfile profile = retrieveProfile(recipient.getNumber());
IdentityKey identityKey = new IdentityKey(Base64.decode(profile.getIdentityKey()), 0);
if (!DatabaseFactory.getIdentityDatabase(context)
.getIdentity(recipient.getRecipientId())
.isPresent())
{
Log.w(TAG, "Still first use...");
return;
}
synchronized (SESSION_LOCK) {
IdentityKeyStore identityKeyStore = new TextSecureIdentityKeyStore(context);
if (identityKeyStore.saveIdentity(new SignalProtocolAddress(recipient.getNumber(), 1), identityKey)) {
Log.w(TAG, "Deleting all sessions...");
new TextSecureSessionStore(getContext()).deleteAllSessions(recipient.getNumber());
}
}
}
private void handleGroupRecipient(Recipient group)
throws IOException, InvalidKeyException
{
byte[] groupId = GroupUtil.getDecodedId(group.getNumber());
Recipients recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupId, false);
for (Recipient recipient : recipients) {
handleIndividualRecipient(recipient);
}
}
private SignalServiceProfile retrieveProfile(@NonNull String number) throws IOException {
SignalServiceMessagePipe pipe = MessageRetrievalService.getPipe();
if (pipe != null) {
try {
return pipe.getProfile(new SignalServiceAddress(number));
} catch (IOException e) {
Log.w(TAG, e);
}
}
return receiver.retrieveProfile(new SignalServiceAddress(number));
}
}

View File

@ -208,6 +208,11 @@ public class MessageNotifier {
if (isVisible) {
List<MarkedMessageInfo> messageIds = threads.setRead(threadId, false);
MarkReadReceiver.process(context, messageIds);
if (recipients != null && recipients.getPrimaryRecipient() != null) {
DatabaseFactory.getIdentityDatabase(context)
.setSeen(recipients.getPrimaryRecipient().getRecipientId());
}
}
if (!TextSecurePreferences.isNotificationsEnabled(context) ||

View File

@ -260,7 +260,7 @@ public class RegistrationService extends Service {
TextSecurePreferences.setWebsocketRegistered(this, true);
DatabaseFactory.getIdentityDatabase(this).saveIdentity(self.getRecipientId(), identityKey.getPublicKey());
DatabaseFactory.getIdentityDatabase(this).saveIdentity(self.getRecipientId(), identityKey.getPublicKey(), true, System.currentTimeMillis(), true, true);
DirectoryHelper.refreshDirectory(this, accountManager, number);
DirectoryRefreshListener.schedule(this);

View File

@ -28,6 +28,7 @@ import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.WebRtcCallActivity;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.dependencies.SignalCommunicationModule.SignalMessageSenderFactory;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
@ -68,6 +69,7 @@ import org.webrtc.SurfaceViewRenderer;
import org.webrtc.VideoRenderer;
import org.webrtc.VideoTrack;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
@ -337,6 +339,12 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
return;
}
if (isUnseenIdentity(this.recipient)) {
insertMissedCall(this.recipient, true);
terminate();
return;
}
timeoutExecutor.schedule(new TimeoutRunnable(this.callId), 2, TimeUnit.MINUTES);
initializeVideo();
@ -942,6 +950,28 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
else return result;
}
private boolean isUnseenIdentity(@NonNull Recipient recipient) {
Log.w(TAG, "Checking for unseen identity: " + recipient.getRecipientId());
Optional<IdentityRecord> identityRecord = DatabaseFactory.getIdentityDatabase(this).getIdentity(recipient.getRecipientId());
if (!identityRecord.isPresent()) {
throw new AssertionError("Should have an identity record at this point.");
}
if (identityRecord.get().isFirstUse()) {
Log.w(TAG, "Identity is first use...");
return false;
}
Log.w(TAG, "Last seen: " + identityRecord.get().getSeen() + " vs timestamp: " + identityRecord.get().getTimestamp());
if (identityRecord.get().getSeen() >= identityRecord.get().getTimestamp()) {
return false;
}
return true;
}
private long getCallId(Intent intent) {
return intent.getLongExtra(EXTRA_CALL_ID, -1);
}

View File

@ -3,10 +3,22 @@ package org.thoughtcrime.securesms.util;
import android.content.Context;
import android.os.AsyncTask;
import android.support.annotation.UiThread;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.MessagingDatabase;
import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.sms.IncomingIdentityUpdateMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import org.whispersystems.libsignal.IdentityKey;
@ -14,10 +26,14 @@ import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.state.SessionRecord;
import org.whispersystems.libsignal.state.SessionStore;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
public class IdentityUtil {
private static final String TAG = IdentityUtil.class.getSimpleName();
@UiThread
public static ListenableFuture<Optional<IdentityKey>> getRemoteIdentityKey(final Context context,
final MasterSecret masterSecret,
@ -48,4 +64,38 @@ public class IdentityUtil {
return future;
}
public static void markIdentityUpdate(Context context, Recipient recipient) {
long time = System.currentTimeMillis();
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
GroupDatabase.Reader reader = groupDatabase.getGroups();
String number = recipient.getNumber();
try {
number = Util.canonicalizeNumber(context, number);
} catch (InvalidNumberException e) {
Log.w(TAG, e);
}
GroupDatabase.GroupRecord groupRecord;
while ((groupRecord = reader.getNext()) != null) {
if (groupRecord.getMembers().contains(number) && groupRecord.isActive()) {
SignalServiceGroup group = new SignalServiceGroup(groupRecord.getId());
IncomingTextMessage incoming = new IncomingTextMessage(number, 1, time, null, Optional.of(group), 0);
IncomingIdentityUpdateMessage groupUpdate = new IncomingIdentityUpdateMessage(incoming);
smsDatabase.insertMessageInbox(groupUpdate);
}
}
IncomingTextMessage incoming = new IncomingTextMessage(number, 1, time, null, Optional.<SignalServiceGroup>absent(), 0);
IncomingIdentityUpdateMessage individualUpdate = new IncomingIdentityUpdateMessage(incoming);
Optional<InsertResult> insertResult = smsDatabase.insertMessageInbox(individualUpdate);
if (insertResult.isPresent()) {
MessageNotifier.updateNotification(context, null, insertResult.get().getThreadId());
}
}
}

View File

@ -79,7 +79,7 @@ public class TextSecurePreferences {
private static final String UPDATE_APK_DIGEST = "pref_update_apk_digest";
private static final String SIGNED_PREKEY_ROTATION_TIME_PREF = "pref_signed_pre_key_rotation_time";
private static final String IN_THREAD_NOTIFICATION_PREF = "pref_key_inthread_notifications";
private static final String BLOCKING_IDENTITY_CHANGES_PREF = "pref_blocking_identity_changes";
private static final String APPROVAL_IDENTITY_CHANGES_PREF = "pref_approve_identity_changes";
private static final String SHOW_INVITE_REMINDER_PREF = "pref_show_invite_reminder";
public static final String MESSAGE_BODY_TEXT_SIZE_PREF = "pref_message_body_text_size";
@ -156,12 +156,12 @@ public class TextSecurePreferences {
return getBooleanPreference(context, MULTI_DEVICE_PROVISIONED_PREF, false);
}
public static boolean isBlockingIdentityUpdates(Context context) {
return getBooleanPreference(context, BLOCKING_IDENTITY_CHANGES_PREF, true);
public static boolean isSendingIdentityApprovalRequired(Context context) {
return getBooleanPreference(context, APPROVAL_IDENTITY_CHANGES_PREF, false);
}
public static void setBlockingIdentityUpdates(Context context, boolean value) {
setBooleanPreference(context, BLOCKING_IDENTITY_CHANGES_PREF, value);
public static void setSendingIdentityApprovalRequired(Context context, boolean value) {
setBooleanPreference(context, APPROVAL_IDENTITY_CHANGES_PREF, value);
}
public static void setSignedPreKeyFailureCount(Context context, int value) {