diff --git a/build.gradle b/build.gradle index 23fda1e4af..32de8f73f0 100644 --- a/build.gradle +++ b/build.gradle @@ -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', diff --git a/res/xml/preferences_app_protection.xml b/res/xml/preferences_app_protection.xml index 30f6fc4409..a58469ba13 100644 --- a/res/xml/preferences_app_protection.xml +++ b/res/xml/preferences_app_protection.xml @@ -35,8 +35,8 @@ diff --git a/src/org/thoughtcrime/securesms/ConfirmIdentityDialog.java b/src/org/thoughtcrime/securesms/ConfirmIdentityDialog.java index 98e3eef272..8be370c238 100644 --- a/src/org/thoughtcrime/securesms/ConfirmIdentityDialog.java +++ b/src/org/thoughtcrime/securesms/ConfirmIdentityDialog.java @@ -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; } diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java index 6483d6a6af..e92015b7fe 100644 --- a/src/org/thoughtcrime/securesms/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationActivity.java @@ -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() { + @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; diff --git a/src/org/thoughtcrime/securesms/WebRtcCallActivity.java b/src/org/thoughtcrime/securesms/WebRtcCallActivity.java index ead6a030b3..854785011c 100644 --- a/src/org/thoughtcrime/securesms/WebRtcCallActivity.java +++ b/src/org/thoughtcrime/securesms/WebRtcCallActivity.java @@ -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()); diff --git a/src/org/thoughtcrime/securesms/crypto/storage/SignalProtocolStoreImpl.java b/src/org/thoughtcrime/securesms/crypto/storage/SignalProtocolStoreImpl.java index b4b65c790f..6d1ee8a96b 100644 --- a/src/org/thoughtcrime/securesms/crypto/storage/SignalProtocolStoreImpl.java +++ b/src/org/thoughtcrime/securesms/crypto/storage/SignalProtocolStoreImpl.java @@ -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 diff --git a/src/org/thoughtcrime/securesms/crypto/storage/TextSecureIdentityKeyStore.java b/src/org/thoughtcrime/securesms/crypto/storage/TextSecureIdentityKeyStore.java index 597e3d6278..1e0524585e 100644 --- a/src/org/thoughtcrime/securesms/crypto/storage/TextSecureIdentityKeyStore.java +++ b/src/org/thoughtcrime/securesms/crypto/storage/TextSecureIdentityKeyStore.java @@ -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 = 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) { + 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(); + } } diff --git a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java index 8be4317011..c79197fca2 100644 --- a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java +++ b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java @@ -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(); } diff --git a/src/org/thoughtcrime/securesms/database/IdentityDatabase.java b/src/org/thoughtcrime/securesms/database/IdentityDatabase.java index 571f0f65b7..60d9223569 100644 --- a/src/org/thoughtcrime/securesms/database/IdentityDatabase.java +++ b/src/org/thoughtcrime/securesms/database/IdentityDatabase.java @@ -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 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; + } + } } diff --git a/src/org/thoughtcrime/securesms/database/ThreadDatabase.java b/src/org/thoughtcrime/securesms/database/ThreadDatabase.java index 73d23c638e..667d485e46 100644 --- a/src/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/src/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -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); diff --git a/src/org/thoughtcrime/securesms/database/loaders/IdentityLoader.java b/src/org/thoughtcrime/securesms/database/loaders/IdentityLoader.java deleted file mode 100644 index e906f8e6be..0000000000 --- a/src/org/thoughtcrime/securesms/database/loaders/IdentityLoader.java +++ /dev/null @@ -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(); - } - -} diff --git a/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java b/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java index 2f989b35f4..f432b0ef90 100644 --- a/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java +++ b/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java @@ -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; diff --git a/src/org/thoughtcrime/securesms/jobs/IdentityUpdateJob.java b/src/org/thoughtcrime/securesms/jobs/IdentityUpdateJob.java deleted file mode 100644 index 15895a83b7..0000000000 --- a/src/org/thoughtcrime/securesms/jobs/IdentityUpdateJob.java +++ /dev/null @@ -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.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() { - - } -} diff --git a/src/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java b/src/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java new file mode 100644 index 0000000000..01390be98d --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java @@ -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)); + } +} diff --git a/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java b/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java index 1169bb3998..3e3085c44d 100644 --- a/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java +++ b/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java @@ -208,6 +208,11 @@ public class MessageNotifier { if (isVisible) { List 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) || diff --git a/src/org/thoughtcrime/securesms/service/RegistrationService.java b/src/org/thoughtcrime/securesms/service/RegistrationService.java index b476e7aaa7..5309daf444 100644 --- a/src/org/thoughtcrime/securesms/service/RegistrationService.java +++ b/src/org/thoughtcrime/securesms/service/RegistrationService.java @@ -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); diff --git a/src/org/thoughtcrime/securesms/service/WebRtcCallService.java b/src/org/thoughtcrime/securesms/service/WebRtcCallService.java index 119ee0aecd..bcc5617b19 100644 --- a/src/org/thoughtcrime/securesms/service/WebRtcCallService.java +++ b/src/org/thoughtcrime/securesms/service/WebRtcCallService.java @@ -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 = 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); } diff --git a/src/org/thoughtcrime/securesms/util/IdentityUtil.java b/src/org/thoughtcrime/securesms/util/IdentityUtil.java index cb91a92d4a..6dd750281d 100644 --- a/src/org/thoughtcrime/securesms/util/IdentityUtil.java +++ b/src/org/thoughtcrime/securesms/util/IdentityUtil.java @@ -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> 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.absent(), 0); + IncomingIdentityUpdateMessage individualUpdate = new IncomingIdentityUpdateMessage(incoming); + Optional insertResult = smsDatabase.insertMessageInbox(individualUpdate); + + if (insertResult.isPresent()) { + MessageNotifier.updateNotification(context, null, insertResult.get().getThreadId()); + } + } } diff --git a/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java b/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java index fb341eee74..429334ae01 100644 --- a/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java +++ b/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java @@ -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) {