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

View File

@ -35,8 +35,8 @@
<PreferenceCategory android:title="@string/preferences_app_protection__communication"> <PreferenceCategory android:title="@string/preferences_app_protection__communication">
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat <org.thoughtcrime.securesms.components.SwitchPreferenceCompat
android:defaultValue="true" android:defaultValue="false"
android:key="pref_blocking_identity_changes" android:key="pref_approve_identity_changes"
android:title="@string/preferences_app_protection__safety_numbers_approval" android:title="@string/preferences_app_protection__safety_numbers_approval"
android:summary="@string/preferences_app_protecting__require_approval_of_new_safety_numbers_when_they_change"/> 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.SpannableString;
import android.text.Spanned; import android.text.Spanned;
import android.text.method.LinkMovementMethod; import android.text.method.LinkMovementMethod;
import android.util.Log;
import android.widget.TextView; import android.widget.TextView;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore; import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.MmsAddressDatabase; import org.thoughtcrime.securesms.database.MmsAddressDatabase;
import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.MmsSmsDatabase; 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.SmsDatabase;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.jobs.IdentityUpdateJob;
import org.thoughtcrime.securesms.jobs.PushDecryptJob; import org.thoughtcrime.securesms.jobs.PushDecryptJob;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory; 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.Base64;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.VerifySpan; import org.thoughtcrime.securesms.util.VerifySpan;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.InvalidNumberException;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos; import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import java.io.IOException; import java.io.IOException;
import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
public class ConfirmIdentityDialog extends AlertDialog { public class ConfirmIdentityDialog extends AlertDialog {
private static final String TAG = ConfirmIdentityDialog.class.getSimpleName(); private static final String TAG = ConfirmIdentityDialog.class.getSimpleName();
@ -102,20 +105,17 @@ public class ConfirmIdentityDialog extends AlertDialog {
{ {
@Override @Override
protected Void doInBackground(Void... params) { protected Void doInBackground(Void... params) {
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(getContext()); synchronized (SESSION_LOCK) {
SignalProtocolAddress mismatchAddress = new SignalProtocolAddress(number, 1);
identityDatabase.saveIdentity(mismatch.getRecipientId(), if (new TextSecureIdentityKeyStore(getContext()).saveIdentity(mismatchAddress, mismatch.getIdentityKey(), true, true)) {
mismatch.getIdentityKey()); new TextSecureSessionStore(getContext()).deleteAllSessions(number);
}
new TextSecureSessionStore(getContext()).deleteAllSessions(number); }
processMessageRecord(messageRecord); processMessageRecord(messageRecord);
processPendingMessageRecords(messageRecord.getThreadId(), mismatch); processPendingMessageRecords(messageRecord.getThreadId(), mismatch);
ApplicationContext.getInstance(getContext())
.getJobManager()
.add(new IdentityUpdateJob(getContext(), mismatch.getRecipientId()));
return null; 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.SmsDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob; import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.mms.AttachmentManager; import org.thoughtcrime.securesms.mms.AttachmentManager;
import org.thoughtcrime.securesms.mms.AttachmentManager.MediaType; import org.thoughtcrime.securesms.mms.AttachmentManager.MediaType;
import org.thoughtcrime.securesms.mms.AudioSlide; import org.thoughtcrime.securesms.mms.AudioSlide;
@ -260,6 +261,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
initializeDraft(); initializeDraft();
} }
}); });
initializeProfiles();
} }
@Override @Override
@ -315,6 +317,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
MessageNotifier.setVisibleThread(threadId); MessageNotifier.setVisibleThread(threadId);
markThreadAsRead(); markThreadAsRead();
markIdentitySeen();
Log.w(TAG, "onResume() Finished: " + (System.currentTimeMillis() - getIntent().getLongExtra(TIMING_EXTRA, 0))); Log.w(TAG, "onResume() Finished: " + (System.currentTimeMillis() - getIntent().getLongExtra(TIMING_EXTRA, 0)));
} }
@ -1142,6 +1145,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
recipients.addListener(this); recipients.addListener(this);
} }
private void initializeProfiles() {
ApplicationContext.getInstance(this)
.getJobManager()
.add(new RetrieveProfileJob(this, recipients));
}
@Override @Override
public void onModified(final Recipients recipients) { public void onModified(final Recipients recipients) {
titleView.post(new Runnable() { titleView.post(new Runnable() {
@ -1434,6 +1443,17 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}.execute(threadId); }.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) { protected void sendComplete(long threadId) {
boolean refreshFragment = (threadId != this.threadId); boolean refreshFragment = (threadId != this.threadId);
this.threadId = 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.WebRtcCallControls;
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallScreen; import org.thoughtcrime.securesms.components.webrtc.WebRtcCallScreen;
import org.thoughtcrime.securesms.components.webrtc.WebRtcIncomingCallOverlay; import org.thoughtcrime.securesms.components.webrtc.WebRtcIncomingCallOverlay;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
import org.thoughtcrime.securesms.database.IdentityDatabase; import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
import org.thoughtcrime.securesms.events.WebRtcViewModel; import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess; import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
import org.thoughtcrime.securesms.recipients.Recipient; 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.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.ViewUtil;
import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.SignalProtocolAddress;
import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
public class WebRtcCallActivity extends Activity { public class WebRtcCallActivity extends Activity {
@ -254,8 +257,11 @@ public class WebRtcCallActivity extends Activity {
callScreen.setAcceptIdentityListener(new View.OnClickListener() { callScreen.setAcceptIdentityListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(WebRtcCallActivity.this); synchronized (SESSION_LOCK) {
identityDatabase.saveIdentity(recipient.getRecipientId(), theirIdentity); 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 intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
intent.putExtra(WebRtcCallService.EXTRA_REMOTE_NUMBER, recipient.getNumber()); intent.putExtra(WebRtcCallService.EXTRA_REMOTE_NUMBER, recipient.getNumber());

View File

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

View File

@ -1,21 +1,31 @@
package org.thoughtcrime.securesms.crypto.storage; package org.thoughtcrime.securesms.crypto.storage;
import android.content.Context; import android.content.Context;
import android.util.Log;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.SessionUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory; 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.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.SignalProtocolAddress; import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.state.IdentityKeyStore; import org.whispersystems.libsignal.state.IdentityKeyStore;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.concurrent.TimeUnit;
public class TextSecureIdentityKeyStore implements IdentityKeyStore { 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; private final Context context;
public TextSecureIdentityKeyStore(Context context) { public TextSecureIdentityKeyStore(Context context) {
@ -32,31 +42,96 @@ public class TextSecureIdentityKeyStore implements IdentityKeyStore {
return TextSecurePreferences.getLocalRegistrationId(context); return TextSecurePreferences.getLocalRegistrationId(context);
} }
@Override public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey,
public void saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) { boolean blockingApproval, boolean nonBlockingApproval)
long recipientId = RecipientFactory.getRecipientsFromString(context, address.getName(), true).getPrimaryRecipient().getRecipientId(); {
DatabaseFactory.getIdentityDatabase(context).saveIdentity(recipientId, identityKey); 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 if (!identityRecord.isPresent()) {
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey) { Log.w(TAG, "Saving new identity...");
long recipientId = RecipientFactory.getRecipientsFromString(context, address.getName(), true).getPrimaryRecipient().getRecipientId(); identityDatabase.saveIdentity(recipientId, identityKey, true, System.currentTimeMillis(), blockingApproval, nonBlockingApproval);
boolean trusted = DatabaseFactory.getIdentityDatabase(context) return false;
.isValidIdentity(recipientId, identityKey); }
if (trusted) { if (!identityRecord.get().getIdentityKey().equals(identityKey)) {
return true; Log.w(TAG, "Replacing existing identity...");
} else if (!TextSecurePreferences.isBlockingIdentityUpdates(context)) { identityDatabase.saveIdentity(recipientId, identityKey, false, System.currentTimeMillis(), blockingApproval, nonBlockingApproval);
saveIdentity(address, identityKey); IdentityUtil.markIdentityUpdate(context, recipients.getPrimaryRecipient());
new TextSecureSessionStore(context).deleteAllSessions(address.getName()); return true;
}
ApplicationContext.getInstance(context) if (isBlockingApprovalRequired(identityRecord.get()) || isNonBlockingApprovalRequired(identityRecord.get())) {
.getJobManager() Log.w(TAG, "Setting approval status...");
.add(new IdentityUpdateJob(context, recipientId)); identityDatabase.setApproval(recipientId, blockingApproval, nonBlockingApproval);
return false;
}
return true;
} else {
return false; 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_DOCUMENTS = 32;
private static final int INTRODUCED_FAST_PREFLIGHT = 33; private static final int INTRODUCED_FAST_PREFLIGHT = 33;
private static final int INTRODUCED_VOICE_NOTES = 34; 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 String DATABASE_NAME = "messages.db";
private static final Object lock = new Object(); 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"); 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.setTransactionSuccessful();
db.endTransaction(); db.endTransaction();
} }

View File

@ -24,74 +24,73 @@ import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri; import android.net.Uri;
import android.util.Log; import android.util.Log;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.Base64;
import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException; import java.io.IOException;
public class IdentityDatabase extends Database { 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 Uri CHANGE_URI = Uri.parse("content://textsecure/identities");
private static final String ID = "_id";
public static final String RECIPIENT = "recipient"; private static final String TABLE_NAME = "identities";
public static final String IDENTITY_KEY = "key"; 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 + public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME +
" (" + ID + " INTEGER PRIMARY KEY, " + " (" + ID + " INTEGER PRIMARY KEY, " +
RECIPIENT + " INTEGER UNIQUE, " + 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); super(context, databaseHelper);
} }
public Cursor getIdentities() { public Optional<IdentityRecord> getIdentity(long recipientId) {
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)
{
SQLiteDatabase database = databaseHelper.getReadableDatabase(); SQLiteDatabase database = databaseHelper.getReadableDatabase();
Cursor cursor = null; Cursor cursor = null;
try { try {
cursor = database.query(TABLE_NAME, null, RECIPIENT + " = ?", cursor = database.query(TABLE_NAME, null, RECIPIENT + " = ?",
new String[] {recipientId+""}, null, null,null); new String[] {recipientId + ""}, null, null, null);
if (cursor != null && cursor.moveToFirst()) { if (cursor != null && cursor.moveToFirst()) {
String serializedIdentity = cursor.getString(cursor.getColumnIndexOrThrow(IDENTITY_KEY)); String serializedIdentity = cursor.getString(cursor.getColumnIndexOrThrow(IDENTITY_KEY));
IdentityKey ourIdentity = new IdentityKey(Base64.decode(serializedIdentity), 0); 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); return Optional.of(new IdentityRecord(identity, firstUse, timestamp, seen, blockingApproval, nonblockingApproval));
} else {
return true;
} }
} catch (IOException e) { } catch (InvalidKeyException | IOException e) {
Log.w("IdentityDatabase", e); throw new AssertionError(e);
return false;
} catch (InvalidKeyException e) {
Log.w("IdentityDatabase", e);
return false;
} finally { } finally {
if (cursor != null) { if (cursor != null) cursor.close();
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(); SQLiteDatabase database = databaseHelper.getWritableDatabase();
String identityKeyString = Base64.encodeBytes(identityKey.serialize()); String identityKeyString = Base64.encodeBytes(identityKey.serialize());
@ -99,65 +98,85 @@ public class IdentityDatabase extends Database {
ContentValues contentValues = new ContentValues(); ContentValues contentValues = new ContentValues();
contentValues.put(RECIPIENT, recipientId); contentValues.put(RECIPIENT, recipientId);
contentValues.put(IDENTITY_KEY, identityKeyString); 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); database.replace(TABLE_NAME, null, contentValues);
context.getContentResolver().notifyChange(CHANGE_URI, null); context.getContentResolver().notifyChange(CHANGE_URI, null);
} }
public void deleteIdentity(long id) { public void setApproval(long recipientId, boolean blockingApproval, boolean nonBlockingApproval) {
SQLiteDatabase database = databaseHelper.getWritableDatabase(); 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); context.getContentResolver().notifyChange(CHANGE_URI, null);
} }
public Reader readerFor(Cursor cursor) { public void setSeen(long recipientId) {
return new Reader(cursor); 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 { public static class IdentityRecord {
private final Cursor cursor;
public Reader(Cursor cursor) { private final IdentityKey identitykey;
this.cursor = cursor; private final boolean firstUse;
} private final long timestamp;
private final long seen;
private final boolean blockingApproval;
private final boolean nonblockingApproval;
public Identity getCurrent() { private IdentityRecord(IdentityKey identitykey, boolean firstUse, long timestamp,
long recipientId = cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT)); long seen, boolean blockingApproval, boolean nonblockingApproval)
Recipients recipients = RecipientFactory.getRecipientsForIds(context, new long[]{recipientId}, true); {
this.identitykey = identitykey;
try { this.firstUse = firstUse;
String identityKeyString = cursor.getString(cursor.getColumnIndexOrThrow(IDENTITY_KEY)); this.timestamp = timestamp;
IdentityKey identityKey = new IdentityKey(Base64.decode(identityKeyString), 0); this.seen = seen;
this.blockingApproval = blockingApproval;
return new Identity(recipients, identityKey); this.nonblockingApproval = nonblockingApproval;
} 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;
} }
public IdentityKey getIdentityKey() { 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 = { 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 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) { public ThreadDatabase(Context context, SQLiteOpenHelper databaseHelper) {
@ -339,7 +339,7 @@ public class ThreadDatabase extends Database {
public Cursor getConversationList() { public Cursor getConversationList() {
SQLiteDatabase db = databaseHelper.getReadableDatabase(); 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); setNotifyConverationListListeners(cursor);
@ -348,7 +348,7 @@ public class ThreadDatabase extends Database {
public Cursor getArchivedConversationList() { public Cursor getArchivedConversationList() {
SQLiteDatabase db = databaseHelper.getReadableDatabase(); 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); 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.RefreshAttributesJob;
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob; import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
import org.thoughtcrime.securesms.jobs.RequestGroupInfoJob; import org.thoughtcrime.securesms.jobs.RequestGroupInfoJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.jobs.RotateSignedPreKeyJob; import org.thoughtcrime.securesms.jobs.RotateSignedPreKeyJob;
import org.thoughtcrime.securesms.push.SecurityEventListener; import org.thoughtcrime.securesms.push.SecurityEventListener;
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess; import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
@ -59,7 +60,8 @@ import dagger.Provides;
PushGroupUpdateJob.class, PushGroupUpdateJob.class,
AvatarDownloadJob.class, AvatarDownloadJob.class,
RotateSignedPreKeyJob.class, RotateSignedPreKeyJob.class,
WebRtcCallService.class}) WebRtcCallService.class,
RetrieveProfileJob.class})
public class SignalCommunicationModule { public class SignalCommunicationModule {
private final Context context; 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) { if (isVisible) {
List<MarkedMessageInfo> messageIds = threads.setRead(threadId, false); List<MarkedMessageInfo> messageIds = threads.setRead(threadId, false);
MarkReadReceiver.process(context, messageIds); MarkReadReceiver.process(context, messageIds);
if (recipients != null && recipients.getPrimaryRecipient() != null) {
DatabaseFactory.getIdentityDatabase(context)
.setSeen(recipients.getPrimaryRecipient().getRecipientId());
}
} }
if (!TextSecurePreferences.isNotificationsEnabled(context) || if (!TextSecurePreferences.isNotificationsEnabled(context) ||

View File

@ -260,7 +260,7 @@ public class RegistrationService extends Service {
TextSecurePreferences.setWebsocketRegistered(this, true); 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); DirectoryHelper.refreshDirectory(this, accountManager, number);
DirectoryRefreshListener.schedule(this); DirectoryRefreshListener.schedule(this);

View File

@ -28,6 +28,7 @@ import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.WebRtcCallActivity; import org.thoughtcrime.securesms.WebRtcCallActivity;
import org.thoughtcrime.securesms.contacts.ContactAccessor; import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.dependencies.InjectableType; import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.dependencies.SignalCommunicationModule.SignalMessageSenderFactory; import org.thoughtcrime.securesms.dependencies.SignalCommunicationModule.SignalMessageSenderFactory;
import org.thoughtcrime.securesms.events.WebRtcViewModel; import org.thoughtcrime.securesms.events.WebRtcViewModel;
@ -68,6 +69,7 @@ import org.webrtc.SurfaceViewRenderer;
import org.webrtc.VideoRenderer; import org.webrtc.VideoRenderer;
import org.webrtc.VideoTrack; import org.webrtc.VideoTrack;
import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
@ -337,6 +339,12 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
return; return;
} }
if (isUnseenIdentity(this.recipient)) {
insertMissedCall(this.recipient, true);
terminate();
return;
}
timeoutExecutor.schedule(new TimeoutRunnable(this.callId), 2, TimeUnit.MINUTES); timeoutExecutor.schedule(new TimeoutRunnable(this.callId), 2, TimeUnit.MINUTES);
initializeVideo(); initializeVideo();
@ -942,6 +950,28 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
else return result; 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) { private long getCallId(Intent intent) {
return intent.getLongExtra(EXTRA_CALL_ID, -1); return intent.getLongExtra(EXTRA_CALL_ID, -1);
} }

View File

@ -3,10 +3,22 @@ package org.thoughtcrime.securesms.util;
import android.content.Context; import android.content.Context;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.support.annotation.UiThread; import android.support.annotation.UiThread;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore; 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.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.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture; import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import org.whispersystems.libsignal.IdentityKey; 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.SessionRecord;
import org.whispersystems.libsignal.state.SessionStore; import org.whispersystems.libsignal.state.SessionStore;
import org.whispersystems.libsignal.util.guava.Optional; 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.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
public class IdentityUtil { public class IdentityUtil {
private static final String TAG = IdentityUtil.class.getSimpleName();
@UiThread @UiThread
public static ListenableFuture<Optional<IdentityKey>> getRemoteIdentityKey(final Context context, public static ListenableFuture<Optional<IdentityKey>> getRemoteIdentityKey(final Context context,
final MasterSecret masterSecret, final MasterSecret masterSecret,
@ -48,4 +64,38 @@ public class IdentityUtil {
return future; 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 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 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 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"; 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"; 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); return getBooleanPreference(context, MULTI_DEVICE_PROVISIONED_PREF, false);
} }
public static boolean isBlockingIdentityUpdates(Context context) { public static boolean isSendingIdentityApprovalRequired(Context context) {
return getBooleanPreference(context, BLOCKING_IDENTITY_CHANGES_PREF, true); return getBooleanPreference(context, APPROVAL_IDENTITY_CHANGES_PREF, false);
} }
public static void setBlockingIdentityUpdates(Context context, boolean value) { public static void setSendingIdentityApprovalRequired(Context context, boolean value) {
setBooleanPreference(context, BLOCKING_IDENTITY_CHANGES_PREF, value); setBooleanPreference(context, APPROVAL_IDENTITY_CHANGES_PREF, value);
} }
public static void setSignedPreKeyFailureCount(Context context, int value) { public static void setSignedPreKeyFailureCount(Context context, int value) {