mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-23 18:15:22 +00:00
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:
parent
ca701df1e4
commit
d507756821
10
build.gradle
10
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',
|
||||
|
@ -35,8 +35,8 @@
|
||||
|
||||
<PreferenceCategory android:title="@string/preferences_app_protection__communication">
|
||||
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="pref_blocking_identity_changes"
|
||||
android:defaultValue="false"
|
||||
android:key="pref_approve_identity_changes"
|
||||
android:title="@string/preferences_app_protection__safety_numbers_approval"
|
||||
android:summary="@string/preferences_app_protecting__require_approval_of_new_safety_numbers_when_they_change"/>
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -105,6 +105,7 @@ import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase.Recipient
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||
import org.thoughtcrime.securesms.mms.AttachmentManager;
|
||||
import org.thoughtcrime.securesms.mms.AttachmentManager.MediaType;
|
||||
import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||
@ -260,6 +261,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
initializeDraft();
|
||||
}
|
||||
});
|
||||
initializeProfiles();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -315,6 +317,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
MessageNotifier.setVisibleThread(threadId);
|
||||
markThreadAsRead();
|
||||
markIdentitySeen();
|
||||
|
||||
Log.w(TAG, "onResume() Finished: " + (System.currentTimeMillis() - getIntent().getLongExtra(TIMING_EXTRA, 0)));
|
||||
}
|
||||
@ -1142,6 +1145,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
recipients.addListener(this);
|
||||
}
|
||||
|
||||
private void initializeProfiles() {
|
||||
ApplicationContext.getInstance(this)
|
||||
.getJobManager()
|
||||
.add(new RetrieveProfileJob(this, recipients));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onModified(final Recipients recipients) {
|
||||
titleView.post(new Runnable() {
|
||||
@ -1434,6 +1443,17 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
}.execute(threadId);
|
||||
}
|
||||
|
||||
private void markIdentitySeen() {
|
||||
new AsyncTask<Recipient, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Recipient... params) {
|
||||
DatabaseFactory.getIdentityDatabase(ConversationActivity.this)
|
||||
.setSeen(params[0].getRecipientId());
|
||||
return null;
|
||||
}
|
||||
}.execute(recipients.getPrimaryRecipient());
|
||||
}
|
||||
|
||||
protected void sendComplete(long threadId) {
|
||||
boolean refreshFragment = (threadId != this.threadId);
|
||||
this.threadId = threadId;
|
||||
|
@ -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());
|
||||
|
@ -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
|
||||
|
@ -1,21 +1,31 @@
|
||||
package org.thoughtcrime.securesms.crypto.storage;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.SessionUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.jobs.IdentityUpdateJob;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientFactory;
|
||||
import org.thoughtcrime.securesms.recipients.Recipients;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.IdentityKeyPair;
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
import org.whispersystems.libsignal.state.IdentityKeyStore;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class TextSecureIdentityKeyStore implements IdentityKeyStore {
|
||||
|
||||
private static final int TIMESTAMP_THRESHOLD_SECONDS = 5;
|
||||
|
||||
private static final String TAG = TextSecureIdentityKeyStore.class.getSimpleName();
|
||||
private static final Object LOCK = new Object();
|
||||
|
||||
private final Context context;
|
||||
|
||||
public TextSecureIdentityKeyStore(Context context) {
|
||||
@ -32,31 +42,96 @@ public class TextSecureIdentityKeyStore implements IdentityKeyStore {
|
||||
return TextSecurePreferences.getLocalRegistrationId(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
|
||||
long recipientId = RecipientFactory.getRecipientsFromString(context, address.getName(), true).getPrimaryRecipient().getRecipientId();
|
||||
DatabaseFactory.getIdentityDatabase(context).saveIdentity(recipientId, identityKey);
|
||||
}
|
||||
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey,
|
||||
boolean blockingApproval, boolean nonBlockingApproval)
|
||||
{
|
||||
synchronized (LOCK) {
|
||||
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
|
||||
Recipients recipients = RecipientFactory.getRecipientsFromString(context, address.getName(), true);
|
||||
long recipientId = recipients.getPrimaryRecipient().getRecipientId();
|
||||
Optional<IdentityRecord> identityRecord = identityDatabase.getIdentity(recipientId);
|
||||
|
||||
@Override
|
||||
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
|
||||
long recipientId = RecipientFactory.getRecipientsFromString(context, address.getName(), true).getPrimaryRecipient().getRecipientId();
|
||||
boolean trusted = DatabaseFactory.getIdentityDatabase(context)
|
||||
.isValidIdentity(recipientId, identityKey);
|
||||
if (!identityRecord.isPresent()) {
|
||||
Log.w(TAG, "Saving new identity...");
|
||||
identityDatabase.saveIdentity(recipientId, identityKey, true, System.currentTimeMillis(), blockingApproval, nonBlockingApproval);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (trusted) {
|
||||
return true;
|
||||
} else if (!TextSecurePreferences.isBlockingIdentityUpdates(context)) {
|
||||
saveIdentity(address, identityKey);
|
||||
new TextSecureSessionStore(context).deleteAllSessions(address.getName());
|
||||
if (!identityRecord.get().getIdentityKey().equals(identityKey)) {
|
||||
Log.w(TAG, "Replacing existing identity...");
|
||||
identityDatabase.saveIdentity(recipientId, identityKey, false, System.currentTimeMillis(), blockingApproval, nonBlockingApproval);
|
||||
IdentityUtil.markIdentityUpdate(context, recipients.getPrimaryRecipient());
|
||||
return true;
|
||||
}
|
||||
|
||||
ApplicationContext.getInstance(context)
|
||||
.getJobManager()
|
||||
.add(new IdentityUpdateJob(context, recipientId));
|
||||
if (isBlockingApprovalRequired(identityRecord.get()) || isNonBlockingApprovalRequired(identityRecord.get())) {
|
||||
Log.w(TAG, "Setting approval status...");
|
||||
identityDatabase.setApproval(recipientId, blockingApproval, nonBlockingApproval);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
|
||||
return saveIdentity(address, identityKey, !TextSecurePreferences.isSendingIdentityApprovalRequired(context), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) {
|
||||
synchronized (LOCK) {
|
||||
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
|
||||
long recipientId = RecipientFactory.getRecipientsFromString(context, address.getName(), true).getPrimaryRecipient().getRecipientId();
|
||||
String ourNumber = TextSecurePreferences.getLocalNumber(context);
|
||||
long ourRecipientId = RecipientFactory.getRecipientsFromString(context, ourNumber, true).getPrimaryRecipient().getRecipientId();
|
||||
|
||||
if (ourRecipientId == recipientId || ourNumber.equals(address.getName())) {
|
||||
return identityKey.equals(IdentityKeyUtil.getIdentityKey(context));
|
||||
}
|
||||
|
||||
switch (direction) {
|
||||
case SENDING: return isTrustedForSending(identityKey, identityDatabase.getIdentity(recipientId));
|
||||
case RECEIVING: return true;
|
||||
default: throw new AssertionError("Unknown direction: " + direction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isTrustedForSending(IdentityKey identityKey, Optional<IdentityRecord> identityRecord) {
|
||||
if (!identityRecord.isPresent()) {
|
||||
Log.w(TAG, "Nothing here, returning true...");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!identityKey.equals(identityRecord.get().getIdentityKey())) {
|
||||
Log.w(TAG, "Identity keys don't match...");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isBlockingApprovalRequired(identityRecord.get())) {
|
||||
Log.w(TAG, "Needs blocking approval!");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isNonBlockingApprovalRequired(identityRecord.get())) {
|
||||
Log.w(TAG, "Needs non-blocking approval!");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isBlockingApprovalRequired(IdentityRecord identityRecord) {
|
||||
return !identityRecord.isFirstUse() &&
|
||||
TextSecurePreferences.isSendingIdentityApprovalRequired(context) &&
|
||||
!identityRecord.isApprovedBlocking();
|
||||
}
|
||||
|
||||
private boolean isNonBlockingApprovalRequired(IdentityRecord identityRecord) {
|
||||
return !identityRecord.isFirstUse() &&
|
||||
System.currentTimeMillis() - identityRecord.getTimestamp() < TimeUnit.SECONDS.toMillis(TIMESTAMP_THRESHOLD_SECONDS) &&
|
||||
!identityRecord.isApprovedNonBlocking();
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -24,74 +24,73 @@ import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.RecipientFactory;
|
||||
import org.thoughtcrime.securesms.recipients.Recipients;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class IdentityDatabase extends Database {
|
||||
|
||||
private static final Uri CHANGE_URI = Uri.parse("content://textsecure/identities");
|
||||
private static final String TAG = IdentityDatabase.class.getSimpleName();
|
||||
|
||||
private static final String TABLE_NAME = "identities";
|
||||
private static final String ID = "_id";
|
||||
public static final String RECIPIENT = "recipient";
|
||||
public static final String IDENTITY_KEY = "key";
|
||||
private static final Uri CHANGE_URI = Uri.parse("content://textsecure/identities");
|
||||
|
||||
private static final String TABLE_NAME = "identities";
|
||||
private static final String ID = "_id";
|
||||
private static final String RECIPIENT = "recipient";
|
||||
private static final String IDENTITY_KEY = "key";
|
||||
private static final String TIMESTAMP = "timestamp";
|
||||
private static final String FIRST_USE = "first_use";
|
||||
private static final String SEEN = "seen";
|
||||
private static final String BLOCKING_APPROVAL = "blocking_approval";
|
||||
private static final String NONBLOCKING_APPROVAL = "nonblocking_approval";
|
||||
|
||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME +
|
||||
" (" + ID + " INTEGER PRIMARY KEY, " +
|
||||
RECIPIENT + " INTEGER UNIQUE, " +
|
||||
IDENTITY_KEY + " TEXT);";
|
||||
IDENTITY_KEY + " TEXT, " +
|
||||
FIRST_USE + " INTEGER DEFAULT 0, " +
|
||||
TIMESTAMP + " INTEGER DEFAULT 0, " +
|
||||
SEEN + " INTEGER DEFAULT 0, " +
|
||||
BLOCKING_APPROVAL + " INTEGER DEFAULT 0, " +
|
||||
NONBLOCKING_APPROVAL + " INTEGER DEFAULT 0);";
|
||||
|
||||
public IdentityDatabase(Context context, SQLiteOpenHelper databaseHelper) {
|
||||
IdentityDatabase(Context context, SQLiteOpenHelper databaseHelper) {
|
||||
super(context, databaseHelper);
|
||||
}
|
||||
|
||||
public Cursor getIdentities() {
|
||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
Cursor cursor = database.query(TABLE_NAME, null, null, null, null, null, null);
|
||||
|
||||
if (cursor != null)
|
||||
cursor.setNotificationUri(context.getContentResolver(), CHANGE_URI);
|
||||
|
||||
return cursor;
|
||||
}
|
||||
|
||||
public boolean isValidIdentity(long recipientId,
|
||||
IdentityKey theirIdentity)
|
||||
{
|
||||
public Optional<IdentityRecord> getIdentity(long recipientId) {
|
||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
Cursor cursor = null;
|
||||
|
||||
try {
|
||||
cursor = database.query(TABLE_NAME, null, RECIPIENT + " = ?",
|
||||
new String[] {recipientId+""}, null, null,null);
|
||||
new String[] {recipientId + ""}, null, null, null);
|
||||
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
String serializedIdentity = cursor.getString(cursor.getColumnIndexOrThrow(IDENTITY_KEY));
|
||||
IdentityKey ourIdentity = new IdentityKey(Base64.decode(serializedIdentity), 0);
|
||||
String serializedIdentity = cursor.getString(cursor.getColumnIndexOrThrow(IDENTITY_KEY));
|
||||
long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP));
|
||||
long seen = cursor.getLong(cursor.getColumnIndexOrThrow(SEEN));
|
||||
boolean blockingApproval = cursor.getInt(cursor.getColumnIndexOrThrow(BLOCKING_APPROVAL)) == 1;
|
||||
boolean nonblockingApproval = cursor.getInt(cursor.getColumnIndexOrThrow(NONBLOCKING_APPROVAL)) == 1;
|
||||
boolean firstUse = cursor.getInt(cursor.getColumnIndexOrThrow(FIRST_USE)) == 1;
|
||||
IdentityKey identity = new IdentityKey(Base64.decode(serializedIdentity), 0);
|
||||
|
||||
return ourIdentity.equals(theirIdentity);
|
||||
} else {
|
||||
return true;
|
||||
return Optional.of(new IdentityRecord(identity, firstUse, timestamp, seen, blockingApproval, nonblockingApproval));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w("IdentityDatabase", e);
|
||||
return false;
|
||||
} catch (InvalidKeyException e) {
|
||||
Log.w("IdentityDatabase", e);
|
||||
return false;
|
||||
} catch (InvalidKeyException | IOException e) {
|
||||
throw new AssertionError(e);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
if (cursor != null) cursor.close();
|
||||
}
|
||||
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
public void saveIdentity(long recipientId, IdentityKey identityKey)
|
||||
public void saveIdentity(long recipientId, IdentityKey identityKey, boolean firstUse,
|
||||
long timestamp, boolean blockingApproval, boolean nonBlockingApproval)
|
||||
{
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
String identityKeyString = Base64.encodeBytes(identityKey.serialize());
|
||||
@ -99,65 +98,85 @@ public class IdentityDatabase extends Database {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(RECIPIENT, recipientId);
|
||||
contentValues.put(IDENTITY_KEY, identityKeyString);
|
||||
contentValues.put(TIMESTAMP, timestamp);
|
||||
contentValues.put(BLOCKING_APPROVAL, blockingApproval ? 1 : 0);
|
||||
contentValues.put(NONBLOCKING_APPROVAL, nonBlockingApproval ? 1 : 0);
|
||||
contentValues.put(FIRST_USE, firstUse ? 1 : 0);
|
||||
contentValues.put(SEEN, 0);
|
||||
|
||||
database.replace(TABLE_NAME, null, contentValues);
|
||||
|
||||
context.getContentResolver().notifyChange(CHANGE_URI, null);
|
||||
}
|
||||
|
||||
public void deleteIdentity(long id) {
|
||||
public void setApproval(long recipientId, boolean blockingApproval, boolean nonBlockingApproval) {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
database.delete(TABLE_NAME, ID_WHERE, new String[] {id+""});
|
||||
|
||||
ContentValues contentValues = new ContentValues(2);
|
||||
contentValues.put(BLOCKING_APPROVAL, blockingApproval);
|
||||
contentValues.put(NONBLOCKING_APPROVAL, nonBlockingApproval);
|
||||
|
||||
database.update(TABLE_NAME, contentValues, RECIPIENT + " = ?",
|
||||
new String[] {String.valueOf(recipientId)});
|
||||
|
||||
context.getContentResolver().notifyChange(CHANGE_URI, null);
|
||||
}
|
||||
|
||||
public Reader readerFor(Cursor cursor) {
|
||||
return new Reader(cursor);
|
||||
public void setSeen(long recipientId) {
|
||||
Log.w(TAG, "Setting seen to current time: " + recipientId);
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
|
||||
ContentValues contentValues = new ContentValues(1);
|
||||
contentValues.put(SEEN, System.currentTimeMillis());
|
||||
|
||||
database.update(TABLE_NAME, contentValues, RECIPIENT + " = ? AND " + SEEN + " = 0",
|
||||
new String[] {String.valueOf(recipientId)});
|
||||
}
|
||||
|
||||
public class Reader {
|
||||
private final Cursor cursor;
|
||||
public static class IdentityRecord {
|
||||
|
||||
public Reader(Cursor cursor) {
|
||||
this.cursor = cursor;
|
||||
}
|
||||
private final IdentityKey identitykey;
|
||||
private final boolean firstUse;
|
||||
private final long timestamp;
|
||||
private final long seen;
|
||||
private final boolean blockingApproval;
|
||||
private final boolean nonblockingApproval;
|
||||
|
||||
public Identity getCurrent() {
|
||||
long recipientId = cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT));
|
||||
Recipients recipients = RecipientFactory.getRecipientsForIds(context, new long[]{recipientId}, true);
|
||||
|
||||
try {
|
||||
String identityKeyString = cursor.getString(cursor.getColumnIndexOrThrow(IDENTITY_KEY));
|
||||
IdentityKey identityKey = new IdentityKey(Base64.decode(identityKeyString), 0);
|
||||
|
||||
return new Identity(recipients, identityKey);
|
||||
} catch (IOException e) {
|
||||
Log.w("IdentityDatabase", e);
|
||||
return new Identity(recipients, null);
|
||||
} catch (InvalidKeyException e) {
|
||||
Log.w("IdentityDatabase", e);
|
||||
return new Identity(recipients, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class Identity {
|
||||
private final Recipients recipients;
|
||||
private final IdentityKey identityKey;
|
||||
|
||||
public Identity(Recipients recipients, IdentityKey identityKey) {
|
||||
this.recipients = recipients;
|
||||
this.identityKey = identityKey;
|
||||
}
|
||||
|
||||
public Recipients getRecipients() {
|
||||
return recipients;
|
||||
private IdentityRecord(IdentityKey identitykey, boolean firstUse, long timestamp,
|
||||
long seen, boolean blockingApproval, boolean nonblockingApproval)
|
||||
{
|
||||
this.identitykey = identitykey;
|
||||
this.firstUse = firstUse;
|
||||
this.timestamp = timestamp;
|
||||
this.seen = seen;
|
||||
this.blockingApproval = blockingApproval;
|
||||
this.nonblockingApproval = nonblockingApproval;
|
||||
}
|
||||
|
||||
public IdentityKey getIdentityKey() {
|
||||
return identityKey;
|
||||
return identitykey;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public long getSeen() {
|
||||
return seen;
|
||||
}
|
||||
|
||||
public boolean isApprovedBlocking() {
|
||||
return blockingApproval;
|
||||
}
|
||||
|
||||
public boolean isApprovedNonBlocking() {
|
||||
return nonblockingApproval;
|
||||
}
|
||||
|
||||
public boolean isFirstUse() {
|
||||
return firstUse;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
|
@ -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() {
|
||||
|
||||
}
|
||||
}
|
119
src/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java
Normal file
119
src/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java
Normal 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));
|
||||
}
|
||||
}
|
@ -208,6 +208,11 @@ public class MessageNotifier {
|
||||
if (isVisible) {
|
||||
List<MarkedMessageInfo> messageIds = threads.setRead(threadId, false);
|
||||
MarkReadReceiver.process(context, messageIds);
|
||||
|
||||
if (recipients != null && recipients.getPrimaryRecipient() != null) {
|
||||
DatabaseFactory.getIdentityDatabase(context)
|
||||
.setSeen(recipients.getPrimaryRecipient().getRecipientId());
|
||||
}
|
||||
}
|
||||
|
||||
if (!TextSecurePreferences.isNotificationsEnabled(context) ||
|
||||
|
@ -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);
|
||||
|
@ -28,6 +28,7 @@ import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.WebRtcCallActivity;
|
||||
import org.thoughtcrime.securesms.contacts.ContactAccessor;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||
import org.thoughtcrime.securesms.dependencies.SignalCommunicationModule.SignalMessageSenderFactory;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
@ -68,6 +69,7 @@ import org.webrtc.SurfaceViewRenderer;
|
||||
import org.webrtc.VideoRenderer;
|
||||
import org.webrtc.VideoTrack;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
@ -337,6 +339,12 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
|
||||
return;
|
||||
}
|
||||
|
||||
if (isUnseenIdentity(this.recipient)) {
|
||||
insertMissedCall(this.recipient, true);
|
||||
terminate();
|
||||
return;
|
||||
}
|
||||
|
||||
timeoutExecutor.schedule(new TimeoutRunnable(this.callId), 2, TimeUnit.MINUTES);
|
||||
|
||||
initializeVideo();
|
||||
@ -942,6 +950,28 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
|
||||
else return result;
|
||||
}
|
||||
|
||||
private boolean isUnseenIdentity(@NonNull Recipient recipient) {
|
||||
Log.w(TAG, "Checking for unseen identity: " + recipient.getRecipientId());
|
||||
|
||||
Optional<IdentityRecord> identityRecord = DatabaseFactory.getIdentityDatabase(this).getIdentity(recipient.getRecipientId());
|
||||
|
||||
if (!identityRecord.isPresent()) {
|
||||
throw new AssertionError("Should have an identity record at this point.");
|
||||
}
|
||||
|
||||
if (identityRecord.get().isFirstUse()) {
|
||||
Log.w(TAG, "Identity is first use...");
|
||||
return false;
|
||||
}
|
||||
|
||||
Log.w(TAG, "Last seen: " + identityRecord.get().getSeen() + " vs timestamp: " + identityRecord.get().getTimestamp());
|
||||
if (identityRecord.get().getSeen() >= identityRecord.get().getTimestamp()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private long getCallId(Intent intent) {
|
||||
return intent.getLongExtra(EXTRA_CALL_ID, -1);
|
||||
}
|
||||
|
@ -3,10 +3,22 @@ package org.thoughtcrime.securesms.util;
|
||||
import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
import android.support.annotation.UiThread;
|
||||
import android.util.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientFactory;
|
||||
import org.thoughtcrime.securesms.recipients.Recipients;
|
||||
import org.thoughtcrime.securesms.sms.IncomingIdentityUpdateMessage;
|
||||
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
@ -14,10 +26,14 @@ import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
import org.whispersystems.libsignal.state.SessionRecord;
|
||||
import org.whispersystems.libsignal.state.SessionStore;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.util.InvalidNumberException;
|
||||
|
||||
public class IdentityUtil {
|
||||
|
||||
private static final String TAG = IdentityUtil.class.getSimpleName();
|
||||
|
||||
@UiThread
|
||||
public static ListenableFuture<Optional<IdentityKey>> getRemoteIdentityKey(final Context context,
|
||||
final MasterSecret masterSecret,
|
||||
@ -48,4 +64,38 @@ public class IdentityUtil {
|
||||
return future;
|
||||
}
|
||||
|
||||
public static void markIdentityUpdate(Context context, Recipient recipient) {
|
||||
long time = System.currentTimeMillis();
|
||||
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
|
||||
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
GroupDatabase.Reader reader = groupDatabase.getGroups();
|
||||
|
||||
String number = recipient.getNumber();
|
||||
|
||||
try {
|
||||
number = Util.canonicalizeNumber(context, number);
|
||||
} catch (InvalidNumberException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
GroupDatabase.GroupRecord groupRecord;
|
||||
|
||||
while ((groupRecord = reader.getNext()) != null) {
|
||||
if (groupRecord.getMembers().contains(number) && groupRecord.isActive()) {
|
||||
SignalServiceGroup group = new SignalServiceGroup(groupRecord.getId());
|
||||
IncomingTextMessage incoming = new IncomingTextMessage(number, 1, time, null, Optional.of(group), 0);
|
||||
IncomingIdentityUpdateMessage groupUpdate = new IncomingIdentityUpdateMessage(incoming);
|
||||
|
||||
smsDatabase.insertMessageInbox(groupUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
IncomingTextMessage incoming = new IncomingTextMessage(number, 1, time, null, Optional.<SignalServiceGroup>absent(), 0);
|
||||
IncomingIdentityUpdateMessage individualUpdate = new IncomingIdentityUpdateMessage(incoming);
|
||||
Optional<InsertResult> insertResult = smsDatabase.insertMessageInbox(individualUpdate);
|
||||
|
||||
if (insertResult.isPresent()) {
|
||||
MessageNotifier.updateNotification(context, null, insertResult.get().getThreadId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user