Merge pull request #31 from loki-project/multi-device

Multi Device Support
This commit is contained in:
gmbnt 2019-10-08 14:29:51 +11:00 committed by GitHub
commit 7116f2502a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 1007 additions and 364 deletions

View File

@ -20,7 +20,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="40dp"
android:text="@string/activity_account_details_title"
android:text="@string/activity_display_name_title"
android:textAlignment="center" />
<TextView
@ -29,7 +29,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/activity_account_details_subtitle"
android:text="@string/activity_display_name_subtitle"
android:textAlignment="center" />
<org.thoughtcrime.securesms.components.LabeledEditText
@ -38,7 +38,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
app:labeledEditText_background="@color/loki_darkest_gray"
app:labeledEditText_label="@string/activity_account_details_name_edit_text_label"/>
app:labeledEditText_label="@string/activity_display_name_name_edit_text_label"/>
<com.dd.CircularProgressButton
android:id="@+id/nextButton"
@ -53,7 +53,7 @@
app:cpb_colorProgress="@color/textsecure_primary"
app:cpb_cornerRadius="4dp"
app:cpb_selectorIdle="@drawable/progress_button_state"
app:cpb_textIdle="@string/activity_account_details_button_title" />
app:cpb_textIdle="@string/activity_display_name_button_title" />
</LinearLayout>

View File

@ -76,8 +76,28 @@
app:labeledEditText_background="@color/loki_darkest_gray"
app:labeledEditText_label="@string/activity_key_pair_mnemonic_edit_text_label"/>
<TextView
android:id="@+id/linkExplanationTextView"
style="@style/Signal.Text.Body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:visibility="gone"
android:text="@string/activity_key_pair_seed_explanation_3"
android:textAlignment="center" />
<org.thoughtcrime.securesms.components.LabeledEditText
android:id="@+id/publicKeyEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="20dp"
android:visibility="gone"
app:labeledEditText_background="@color/loki_darkest_gray"
app:labeledEditText_label="@string/activity_key_pair_public_key_edit_text_label"/>
<Button
android:id="@+id/toggleModeButton"
android:id="@+id/toggleRestoreModeButton"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/transparent"
@ -86,8 +106,29 @@
android:elevation="0dp"
android:stateListAnimator="@null" />
<Button
android:id="@+id/toggleRegisterModeButton"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/transparent"
android:textColor="@color/signal_primary"
android:text="@string/activity_key_pair_toggle_mode_button_title_2"
android:visibility="gone"
android:elevation="0dp"
android:stateListAnimator="@null" />
<Button
android:id="@+id/toggleLinkModeButton"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/transparent"
android:textColor="@color/signal_primary"
android:text="@string/activity_key_pair_toggle_mode_button_title_3"
android:elevation="0dp"
android:stateListAnimator="@null" />
<com.dd.CircularProgressButton
android:id="@+id/registerOrRestoreButton"
android:id="@+id/mainButton"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginTop="20dp"
@ -99,7 +140,7 @@
app:cpb_colorProgress="@color/textsecure_primary"
app:cpb_cornerRadius="4dp"
app:cpb_selectorIdle="@drawable/progress_button_state"
app:cpb_textIdle="@string/activity_key_pair_register_or_restore_button_title_1" />
app:cpb_textIdle="@string/activity_key_pair_main_button_title_1" />
</LinearLayout>

View File

@ -1549,22 +1549,26 @@
<string name="activity_landing_permission_dialog_message">Some features of Loki Messenger (such as automatic message backup) require storage access to work.</string>
<string name="activity_landing_beta_terms">"Loki Messenger is currently in beta. For development purposes the beta version collects basic usage statistics and crash logs. In addition, the beta version doesn't provide full privacy and shouldn\'t be used to transmit sensitive information."</string>
<string name="activity_landing_privacy_policy_button_title">Privacy Policy</string>
<!-- Account details activity -->
<string name="activity_account_details_title">Create Your Loki Messenger Account</string>
<string name="activity_account_details_subtitle">Enter a name to be shown to your contacts</string>
<string name="activity_account_details_name_edit_text_label">Display Name (Optional)</string>
<string name="activity_account_details_button_title">Next</string>
<!-- Display name activity -->
<string name="activity_display_name_title">Create Your Loki Messenger Account</string>
<string name="activity_display_name_subtitle">Enter a name to be shown to your contacts</string>
<string name="activity_display_name_name_edit_text_label">Display Name (Optional)</string>
<string name="activity_display_name_button_title">Next</string>
<!-- Key pair activity -->
<string name="activity_key_pair_title">Create Your Loki Messenger Account</string>
<string name="activity_key_pair_seed_explanation_1">Please save the seed below in a safe location. It can be used to restore your account if you lose access, or to migrate to a new device.</string>
<string name="activity_key_pair_seed_explanation_2">Restore your account by entering your seed below</string>
<string name="activity_key_pair_seed_explanation_3">Link to an existing device by going into its in-app settings and clicking "Link Device".</string>
<string name="activity_key_pair_copy_button_title">Copy</string>
<string name="activity_key_pair_mnemonic_edit_text_label">Your Seed</string>
<string name="activity_key_pair_toggle_mode_button_title_1">Restore Using Seed</string>
<string name="activity_key_pair_toggle_mode_button_title_2">Register a New Account</string>
<string name="activity_key_pair_toggle_mode_button_title_3">Link Device</string>
<string name="activity_key_pair_mnemonic_copied_message">Copied to clipboard</string>
<string name="activity_key_pair_register_or_restore_button_title_1">Register</string>
<string name="activity_key_pair_register_or_restore_button_title_2">Restore</string>
<string name="activity_key_pair_main_button_title_1">Register</string>
<string name="activity_key_pair_main_button_title_2">Restore</string>
<string name="activity_key_pair_main_button_title_3">Link</string>
<string name="activity_key_pair_public_key_edit_text_label">Your Public Key</string>
<!-- Conversation list activity -->
<string name="activity_conversation_list_empty_state_message">Looks like you don\'t have any conversations yet. Get started by messaging a friend.</string>
<!-- Settings activity -->

View File

@ -36,6 +36,7 @@ import org.jetbrains.annotations.NotNull;
import org.signal.aesgcmprovider.AesGcmProvider;
import org.thoughtcrime.securesms.components.TypingStatusRepository;
import org.thoughtcrime.securesms.components.TypingStatusSender;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.dependencies.AxolotlStorageModule;
@ -83,12 +84,14 @@ import org.webrtc.voiceengine.WebRtcAudioUtils;
import org.whispersystems.libsignal.logging.SignalProtocolLoggerProvider;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import org.whispersystems.signalservice.loki.api.LokiAPIDatabaseProtocol;
import org.whispersystems.signalservice.loki.api.LokiGroupChat;
import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI;
import org.whispersystems.signalservice.loki.api.LokiLongPoller;
import org.whispersystems.signalservice.loki.api.LokiP2PAPI;
import org.whispersystems.signalservice.loki.api.LokiP2PAPIDelegate;
import org.whispersystems.signalservice.loki.api.LokiRSSFeed;
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
import org.whispersystems.signalservice.loki.utilities.Analytics;
import java.security.Security;
@ -190,6 +193,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
KeyCachingService.onAppForegrounded(this);
// Loki - Start long polling if needed
startLongPollingIfNeeded();
setUpStorageAPIIfNeeded();
}
@Override
@ -424,6 +428,16 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
}
// region Loki
public void setUpStorageAPIIfNeeded() {
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(this);
if (userHexEncodedPublicKey != null && IdentityKeyUtil.hasIdentityKey(this)) {
boolean isDebugMode = BuildConfig.DEBUG;
byte[] userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(this).getPrivateKey().serialize();
LokiAPIDatabaseProtocol database = DatabaseFactory.getLokiAPIDatabase(this);
LokiStorageAPI.Companion.configure(isDebugMode, userHexEncodedPublicKey, userPrivateKey, database);
}
}
public void setUpP2PAPI() {
String hexEncodedPublicKey = TextSecurePreferences.getLocalNumber(this);
if (hexEncodedPublicKey == null) { return; }

View File

@ -156,8 +156,13 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
this.findPreference(PREFERENCE_CATEGORY_PROFILE)
.setOnPreferenceClickListener(new ProfileClickListener());
String masterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(getContext());
boolean isMasterDevice = (masterHexEncodedPublicKey != null);
Preference profilePreference = this.findPreference(PREFERENCE_CATEGORY_PROFILE);
// Hide if this is a slave device
profilePreference.setVisible(isMasterDevice);
profilePreference.setOnPreferenceClickListener(new ProfileClickListener());
/*
this.findPreference(PREFERENCE_CATEGORY_SMS_MMS)
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_SMS_MMS));
@ -182,10 +187,14 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_PUBLIC_KEY));
this.findPreference(PREFERENCE_CATEGORY_QR_CODE)
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_QR_CODE));
this.findPreference(PREFERENCE_CATEGORY_LINK_DEVICE)
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_LINK_DEVICE));
this.findPreference(PREFERENCE_CATEGORY_SEED)
.setOnPreferenceClickListener(new CategoryClickListener((PREFERENCE_CATEGORY_SEED)));
Preference linkDevicePreference = this.findPreference(PREFERENCE_CATEGORY_LINK_DEVICE);
// Hide if this is a slave device
linkDevicePreference.setVisible(isMasterDevice);
linkDevicePreference.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_LINK_DEVICE));
Preference seedPreference = this.findPreference(PREFERENCE_CATEGORY_SEED);
// Hide if this is a slave device
seedPreference.setVisible(isMasterDevice);
seedPreference.setOnPreferenceClickListener(new CategoryClickListener((PREFERENCE_CATEGORY_SEED)));
if (VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
tintIcons(getActivity());
@ -320,7 +329,10 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
*/
case PREFERENCE_CATEGORY_PUBLIC_KEY:
Analytics.Companion.getShared().track("Public Key Shared");
String hexEncodedPublicKey = TextSecurePreferences.getLocalNumber(getContext());
String hexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(getContext());
if (hexEncodedPublicKey == null) {
hexEncodedPublicKey = TextSecurePreferences.getLocalNumber(getContext());
}
Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND);
shareIntent.putExtra(Intent.EXTRA_TEXT, hexEncodedPublicKey);
@ -331,7 +343,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
QRCodeDialog.INSTANCE.show(getContext());
break;
case PREFERENCE_CATEGORY_LINK_DEVICE:
DeviceLinkingDialog.INSTANCE.show(getContext(), DeviceLinkingView.Mode.Master);
DeviceLinkingDialog.Companion.show(getContext(), DeviceLinkingView.Mode.Master, null);
break;
case PREFERENCE_CATEGORY_SEED:
Analytics.Companion.getShared().track("Seed Modal Shown");

View File

@ -5,13 +5,21 @@ import android.content.Context;
import android.support.annotation.NonNull;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.jobs.TypingSendJob;
import org.thoughtcrime.securesms.loki.MultiDeviceUtilitiesKt;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import kotlin.Unit;
@SuppressLint("UseSparseArrays")
public class TypingStatusSender {
@ -74,7 +82,23 @@ public class TypingStatusSender {
}
private void sendTyping(long threadId, boolean typingStarted) {
ApplicationContext.getInstance(context).getJobManager().add(new TypingSendJob(threadId, typingStarted));
LokiStorageAPI storageAPI = LokiStorageAPI.Companion.getShared();
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
Recipient recipient = threadDatabase.getRecipientForThreadId(threadId);
if (recipient == null) {
ApplicationContext.getInstance(context).getJobManager().add(new TypingSendJob(threadId, typingStarted));
return;
}
MultiDeviceUtilitiesKt.getAllDevicePublicKeys(context, recipient.getAddress().serialize(), storageAPI, (devicePublicKey, isFriend, friendCount) -> {
Recipient device = Recipient.from(context, Address.fromSerialized(devicePublicKey), false);
long deviceThreadID = threadDatabase.getThreadIdIfExistsFor(device);
if (deviceThreadID > -1) {
ApplicationContext.getInstance(context).getJobManager().add(new TypingSendJob(deviceThreadID, typingStarted));
}
return Unit.INSTANCE;
});
}
private class StartRunnable implements Runnable {

View File

@ -172,8 +172,7 @@ public class IdentityKeyUtil {
if (!preferencesEditor.commit()) throw new AssertionError("failed to save identity key/value to shared preferences");
}
private static void delete(Context context, String key) {
public static void delete(Context context, String key) {
context.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0).edit().remove(key).commit();
}
}

View File

@ -31,12 +31,7 @@ import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.helpers.ClassicOpenHelper;
import org.thoughtcrime.securesms.database.helpers.SQLCipherMigrationHelper;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.loki.LokiAPIDatabase;
import org.thoughtcrime.securesms.loki.LokiPreKeyRecordDatabase;
import org.thoughtcrime.securesms.loki.LokiPreKeyBundleDatabase;
import org.thoughtcrime.securesms.loki.LokiMessageDatabase;
import org.thoughtcrime.securesms.loki.LokiThreadDatabase;
import org.thoughtcrime.securesms.loki.LokiUserDatabase;
import org.thoughtcrime.securesms.loki.*;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
public class DatabaseFactory {

View File

@ -35,12 +35,7 @@ import org.thoughtcrime.securesms.database.StickerDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.loki.LokiAPIDatabase;
import org.thoughtcrime.securesms.loki.LokiPreKeyRecordDatabase;
import org.thoughtcrime.securesms.loki.LokiMessageDatabase;
import org.thoughtcrime.securesms.loki.LokiPreKeyBundleDatabase;
import org.thoughtcrime.securesms.loki.LokiThreadDatabase;
import org.thoughtcrime.securesms.loki.LokiUserDatabase;
import org.thoughtcrime.securesms.loki.*;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@ -74,6 +69,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int STICKERS = 21;
private static final int lokiV1 = 22;
private static final int lokiV2 = 23;
private static final int lokiV3 = 24;
private static final int DATABASE_VERSION = lokiV2; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
private static final String DATABASE_NAME = "signal.db";
@ -129,6 +125,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL(LokiAPIDatabase.getCreateGroupChatAuthTokenTableCommand());
db.execSQL(LokiAPIDatabase.getCreateLastMessageServerIDTableCommand());
db.execSQL(LokiAPIDatabase.getCreateLastDeletionServerIDTableCommand());
db.execSQL(LokiAPIDatabase.getCreatePairingAuthorisationTableCommand());
db.execSQL(LokiPreKeyBundleDatabase.getCreateTableCommand());
db.execSQL(LokiPreKeyRecordDatabase.getCreateTableCommand());
db.execSQL(LokiMessageDatabase.getCreateTableCommand());
@ -498,6 +495,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL(LokiUserDatabase.getCreateServerDisplayNameTableCommand());
}
if (oldVersion < lokiV3) {
db.execSQL(LokiAPIDatabase.getCreatePairingAuthorisationTableCommand());
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();

View File

@ -5,8 +5,6 @@ import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
@ -73,6 +71,7 @@ import org.thoughtcrime.securesms.loki.LokiMessageDatabase;
import org.thoughtcrime.securesms.loki.LokiPreKeyBundleDatabase;
import org.thoughtcrime.securesms.loki.LokiPreKeyRecordDatabase;
import org.thoughtcrime.securesms.loki.LokiThreadDatabase;
import org.thoughtcrime.securesms.loki.MultiDeviceUtilitiesKt;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage;
@ -123,6 +122,9 @@ import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOper
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.loki.api.DeviceLinkingSession;
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
import org.whispersystems.signalservice.loki.api.PairingAuthorisation;
import org.whispersystems.signalservice.loki.crypto.LokiServiceCipher;
import org.whispersystems.signalservice.loki.messaging.LokiMessageFriendRequestStatus;
import org.whispersystems.signalservice.loki.messaging.LokiServiceMessage;
@ -138,6 +140,7 @@ import java.util.List;
import javax.inject.Inject;
import kotlin.Unit;
import network.loki.messenger.R;
public class PushDecryptJob extends BaseJob implements InjectableType {
@ -276,17 +279,20 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
acceptFriendRequestIfNeeded(envelope, content);
// Loki - Store pre key bundle if needed
if (content.lokiMessage.isPresent()) {
LokiServiceMessage lokiMessage = content.lokiMessage.get();
if (content.lokiServiceMessage.isPresent()) {
LokiServiceMessage lokiMessage = content.lokiServiceMessage.get();
if (lokiMessage.getPreKeyBundleMessage() != null) {
Log.d("Loki", "Received a pre key bundle from: " + envelope.getSource() + ".");
int registrationID = TextSecurePreferences.getLocalRegistrationId(context);
if (registrationID > 0) {
LokiPreKeyBundleDatabase lokiPreKeyBundleDatabase = DatabaseFactory.getLokiPreKeyBundleDatabase(context);
LokiPreKeyBundleDatabase lokiPreKeyBundleDatabase = DatabaseFactory.getLokiPreKeyBundleDatabase(context);
// Only store the pre key bundle if we don't have one in our database
if (registrationID > 0 && !lokiPreKeyBundleDatabase.hasPreKeyBundle(envelope.getSource())) {
Log.d("Loki", "Received a pre key bundle from: " + envelope.getSource() + ".");
PreKeyBundle preKeyBundle = lokiMessage.getPreKeyBundleMessage().getPreKeyBundle(registrationID);
lokiPreKeyBundleDatabase.setPreKeyBundle(envelope.getSource(), preKeyBundle);
}
}
if (lokiMessage.getAddressMessage() != null) {
// TODO: Loki - Handle address message
}
@ -295,14 +301,13 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
// Loki - Store the sender display name if needed
Optional<String> rawSenderDisplayName = content.senderDisplayName;
if (rawSenderDisplayName.isPresent() && rawSenderDisplayName.get().length() > 0) {
String senderHexEncodedPublicKey = envelope.getSource();
String senderDisplayName = rawSenderDisplayName.get() + " (..." + senderHexEncodedPublicKey.substring(senderHexEncodedPublicKey.length() - 8) + ")";
DatabaseFactory.getLokiUserDatabase(context).setDisplayName(senderHexEncodedPublicKey, senderDisplayName);
setDisplayName(envelope.getSource(), rawSenderDisplayName.get());
}
// TODO: Deleting the display name
if (content.getDataMessage().isPresent()) {
if (content.getPairingAuthorisation().isPresent()) {
handlePairingMessage(content.getPairingAuthorisation().get(), envelope, content);
} else if (content.getDataMessage().isPresent()) {
SignalServiceDataMessage message = content.getDataMessage().get();
boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent() || message.getPreviews().isPresent() || message.getSticker().isPresent();
@ -1003,6 +1008,11 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
private void handleTextMessage(@NonNull SignalServiceDataMessage message, @NonNull IncomingTextMessage textMessage, @NonNull Optional<Long> smsMessageId, @NonNull Optional<Long> messageServerIDOrNull) {
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
// Ignore the message if the body is empty
if (textMessage.getMessageBody().length() == 0) { return; }
// Insert the message into the database
Optional<InsertResult> insertResult = database.insertMessageInbox(textMessage);
Long threadId;
@ -1020,6 +1030,84 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
}
}
private boolean isValidPairingMessage(@NonNull PairingAuthorisation authorisation) {
boolean isSecondaryDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context) != null;
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context);
boolean isRequest = (authorisation.getType() == PairingAuthorisation.Type.REQUEST);
if (authorisation.getRequestSignature() == null) {
Log.d("Loki", "Ignoring pairing request message without a request signature.");
return false;
} else if (isRequest && isSecondaryDevice) {
Log.d("Loki", "Ignoring unexpected pairing request message (the device is already paired as a secondary device).");
return false;
} else if (isRequest && !authorisation.getPrimaryDevicePublicKey().equals(userHexEncodedPublicKey)) {
Log.d("Loki", "Ignoring pairing request message addressed to another user.");
return false;
} else if (isRequest && authorisation.getSecondaryDevicePublicKey().equals(userHexEncodedPublicKey)) {
Log.d("Loki", "Ignoring pairing request message from self.");
return false;
}
return authorisation.verify();
}
private void handlePairingMessage(@NonNull PairingAuthorisation authorisation, @NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceContent content) {
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context);
if (authorisation.getType() == PairingAuthorisation.Type.REQUEST) {
handlePairingRequestMessage(authorisation, envelope);
} else if (authorisation.getSecondaryDevicePublicKey().equals(userHexEncodedPublicKey)) {
handlePairingAuthorisationMessage(authorisation, envelope, content);
}
}
private void handlePairingRequestMessage(@NonNull PairingAuthorisation authorisation, @NonNull SignalServiceEnvelope envelope) {
boolean isValid = isValidPairingMessage(authorisation);
DeviceLinkingSession linkingSession = DeviceLinkingSession.Companion.getShared();
if (isValid && linkingSession.isListeningForLinkingRequests()) {
linkingSession.processLinkingRequest(authorisation);
}
}
private void handlePairingAuthorisationMessage(@NonNull PairingAuthorisation authorisation, @NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceContent content) {
// Prepare
boolean isSecondaryDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context) != null;
if (isSecondaryDevice) {
Log.d("Loki", "Ignoring unexpected pairing authorisation message (the device is already paired as a secondary device).");
return;
}
boolean isValid = isValidPairingMessage(authorisation);
if (!isValid) {
Log.d("Loki", "Ignoring invalid pairing authorisation message.");
return;
}
if (!DeviceLinkingSession.Companion.getShared().isListeningForLinkingRequests()) {
Log.d("Loki", "Ignoring pairing authorisation message.");
return;
}
if (authorisation.getType() != PairingAuthorisation.Type.GRANT) { return; }
Log.d("Loki", "Received pairing authorisation message from: " + authorisation.getPrimaryDevicePublicKey() + ".");
// Process
DeviceLinkingSession.Companion.getShared().processLinkingAuthorization(authorisation);
// Store the primary device's public key
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context);
DatabaseFactory.getLokiAPIDatabase(context).removePairingAuthorisations(userHexEncodedPublicKey);
DatabaseFactory.getLokiAPIDatabase(context).insertOrUpdatePairingAuthorisation(authorisation);
TextSecurePreferences.setMasterHexEncodedPublicKey(context, authorisation.getPrimaryDevicePublicKey());
// Send a background message to the primary device
sendBackgroundMessage(authorisation.getPrimaryDevicePublicKey());
// Propagate the updates to the file server
LokiStorageAPI storageAPI = LokiStorageAPI.Companion.getShared();
storageAPI.updateUserDeviceMappings();
// Update display names
if (content.senderDisplayName.isPresent() && content.senderDisplayName.get().length() > 0) {
setDisplayName(envelope.getSource(), content.senderDisplayName.get());
}
}
private void setDisplayName(String hexEncodedPublicKey, String profileName) {
String displayName = profileName + " (..." + hexEncodedPublicKey.substring(hexEncodedPublicKey.length() - 8) + ")";
DatabaseFactory.getLokiUserDatabase(context).setDisplayName(hexEncodedPublicKey, displayName);
}
private void updateGroupChatMessageServerID(Optional<Long> messageServerIDOrNull, Optional<InsertResult> insertResult) {
if (insertResult.isPresent() && messageServerIDOrNull.isPresent()) {
long messageID = insertResult.get().getMessageId();
@ -1031,78 +1119,93 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
private void acceptFriendRequestIfNeeded(@NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceContent content) {
// If we get anything other than a friend request, we can assume that we have a session with the other user
if (envelope.isFriendRequest()) { return; }
Recipient contactID = Recipient.from(context, Address.fromSerialized(content.getSender()), false);
becomeFriendsWithContact(content.getSender());
}
private void becomeFriendsWithContact(String pubKey) {
LokiThreadDatabase lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context);
long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(contactID);
Recipient contactID = Recipient.from(context, Address.fromSerialized(pubKey), false);
long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(contactID);
LokiThreadFriendRequestStatus threadFriendRequestStatus = lokiThreadDatabase.getFriendRequestStatus(threadID);
if (threadFriendRequestStatus == LokiThreadFriendRequestStatus.FRIENDS) { return; }
SmsDatabase messageDatabase = DatabaseFactory.getSmsDatabase(context);
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
int messageCount = messageDatabase.getMessageCountForThread(threadID);
// If the thread's friend request status is not `FRIENDS`, but we're receiving a message,
// it must be a friend request accepted message. Declining a friend request doesn't send a message.
lokiThreadDatabase.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.FRIENDS);
long messageID = messageDatabase.getIDForMessageAtIndex(threadID, messageCount - 1);
lokiMessageDatabase.setFriendRequestStatus(messageID, LokiMessageFriendRequestStatus.REQUEST_ACCEPTED);
// Update the last message if needed
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
int messageCount = smsDatabase.getMessageCountForThread(threadID);
long messageID = smsDatabase.getIDForMessageAtIndex(threadID, messageCount - 1);
if (messageID > -1 && lokiMessageDatabase.getFriendRequestStatus(messageID) != LokiMessageFriendRequestStatus.REQUEST_ACCEPTED) {
lokiMessageDatabase.setFriendRequestStatus(messageID, LokiMessageFriendRequestStatus.REQUEST_ACCEPTED);
}
}
private void updateFriendRequestStatusIfNeeded(@NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message) {
if (!envelope.isFriendRequest()) { return; }
Recipient contactID = getMessageDestination(content, message);
LokiThreadDatabase lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context);
long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(contactID);
LokiThreadFriendRequestStatus threadFriendRequestStatus = lokiThreadDatabase.getFriendRequestStatus(threadID);
SmsDatabase smsMessageDatabase = DatabaseFactory.getSmsDatabase(context);
MmsDatabase mmsMessageDatabase = DatabaseFactory.getMmsDatabase(context);
LokiMessageDatabase lokiMessageDatabase= DatabaseFactory.getLokiMessageDatabase(context);
int messageCount = smsMessageDatabase.getMessageCountForThread(threadID);
if (threadFriendRequestStatus == LokiThreadFriendRequestStatus.REQUEST_SENT) {
// This can happen if Alice sent Bob a friend request, Bob declined, but then Bob changed his
// mind and sent a friend request to Alice. In this case we want Alice to auto-accept the request
// and send a friend request accepted message back to Bob. We don't check that sending the
// friend request accepted message succeeded. Even if it doesn't, the thread's current friend
// request status will be set to `FRIENDS` for Alice making it possible
// for Alice to send messages to Bob. When Bob receives a message, his thread's friend request status
// will then be set to `FRIENDS`. If we do check for a successful send
// before updating Alice's thread's friend request status to `FRIENDS`,
// we can end up in a deadlock where both users' threads' friend request statuses are
// `REQUEST_SENT`.
lokiThreadDatabase.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.FRIENDS);
long messageID = smsMessageDatabase.getIDForMessageAtIndex(threadID, messageCount - 2); // The message before the one that was just received
// TODO: MMS
lokiMessageDatabase.setFriendRequestStatus(messageID, LokiMessageFriendRequestStatus.REQUEST_ACCEPTED);
// Accept the friend request
sendBackgroundMessage(content.getSender());
} else if (threadFriendRequestStatus != LokiThreadFriendRequestStatus.FRIENDS) {
// Checking that the sender of the message isn't already a friend is necessary because otherwise
// the following situation can occur: Alice and Bob are friends. Bob loses his database and his
// friend request status is reset to `NONE`. Bob now sends Alice a friend
// request. Alice's thread's friend request status is reset to
// `REQUEST_RECEIVED`.
lokiThreadDatabase.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.REQUEST_RECEIVED);
long messageID = smsMessageDatabase.getIDForMessageAtIndex(threadID, messageCount - 1); // The message that was just received
if (messageID != -1) {
lokiMessageDatabase.setFriendRequestStatus(messageID, LokiMessageFriendRequestStatus.REQUEST_PENDING);
// This handles the case where another user sends us a regular message without authorisation
MultiDeviceUtilitiesKt.shouldAutomaticallyBecomeFriendsWithDevice(content.getSender(), context).success(becomeFriends -> {
if (becomeFriends) {
// Become friends AND update the message they sent
becomeFriendsWithContact(content.getSender());
// Send them an accept message back
sendBackgroundMessage(content.getSender());
} else {
// TODO: The code below is ugly due to Java limitations
lokiMessageDatabase.setFriendRequestStatus(mmsMessageDatabase.getIDForMessageAtIndex(threadID, 0), LokiMessageFriendRequestStatus.REQUEST_PENDING);
// Do regular friend request logic checks
Recipient contactID = getMessageDestination(content, message);
LokiThreadDatabase lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context);
long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(contactID);
LokiThreadFriendRequestStatus threadFriendRequestStatus = lokiThreadDatabase.getFriendRequestStatus(threadID);
SmsDatabase smsMessageDatabase = DatabaseFactory.getSmsDatabase(context);
MmsDatabase mmsMessageDatabase = DatabaseFactory.getMmsDatabase(context);
LokiMessageDatabase lokiMessageDatabase= DatabaseFactory.getLokiMessageDatabase(context);
int messageCount = smsMessageDatabase.getMessageCountForThread(threadID);
if (threadFriendRequestStatus == LokiThreadFriendRequestStatus.REQUEST_SENT) {
// This can happen if Alice sent Bob a friend request, Bob declined, but then Bob changed his
// mind and sent a friend request to Alice. In this case we want Alice to auto-accept the request
// and send a friend request accepted message back to Bob. We don't check that sending the
// friend request accepted message succeeded. Even if it doesn't, the thread's current friend
// request status will be set to `FRIENDS` for Alice making it possible
// for Alice to send messages to Bob. When Bob receives a message, his thread's friend request status
// will then be set to `FRIENDS`. If we do check for a successful send
// before updating Alice's thread's friend request status to `FRIENDS`,
// we can end up in a deadlock where both users' threads' friend request statuses are
// `REQUEST_SENT`.
lokiThreadDatabase.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.FRIENDS);
long messageID = smsMessageDatabase.getIDForMessageAtIndex(threadID, messageCount - 2); // The message before the one that was just received
// TODO: MMS
lokiMessageDatabase.setFriendRequestStatus(messageID, LokiMessageFriendRequestStatus.REQUEST_ACCEPTED);
// Accept the friend request
sendBackgroundMessage(content.getSender());
} else if (threadFriendRequestStatus != LokiThreadFriendRequestStatus.FRIENDS) {
// Checking that the sender of the message isn't already a friend is necessary because otherwise
// the following situation can occur: Alice and Bob are friends. Bob loses his database and his
// friend request status is reset to `NONE`. Bob now sends Alice a friend
// request. Alice's thread's friend request status is reset to
// `REQUEST_RECEIVED`.
lokiThreadDatabase.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.REQUEST_RECEIVED);
long messageID = smsMessageDatabase.getIDForMessageAtIndex(threadID, messageCount - 1); // The message that was just received
if (messageID != -1) {
lokiMessageDatabase.setFriendRequestStatus(messageID, LokiMessageFriendRequestStatus.REQUEST_PENDING);
} else {
// TODO: The code below is ugly due to Java limitations
lokiMessageDatabase.setFriendRequestStatus(mmsMessageDatabase.getIDForMessageAtIndex(threadID, 0), LokiMessageFriendRequestStatus.REQUEST_PENDING);
}
}
}
}
return Unit.INSTANCE;
});
}
private void sendBackgroundMessage(String contactHexEncodedPublicKey) {
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
SignalServiceMessageSender messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender();
SignalServiceAddress address = new SignalServiceAddress(contactHexEncodedPublicKey);
SignalServiceDataMessage message = new SignalServiceDataMessage(System.currentTimeMillis(), "");
try {
messageSender.sendMessage(0, address, Optional.absent(), message); // The message ID doesn't matter
} catch (Exception e) {
Log.d("Loki", "Failed to send background message to: " + contactHexEncodedPublicKey + ".");
}
Util.runOnMain(() -> {
SignalServiceMessageSender messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender();
SignalServiceAddress address = new SignalServiceAddress(contactHexEncodedPublicKey);
SignalServiceDataMessage message = new SignalServiceDataMessage(System.currentTimeMillis(), "");
try {
messageSender.sendMessage(0, address, Optional.absent(), message); // The message ID doesn't matter
} catch (Exception e) {
Log.d("Loki", "Failed to send background message to: " + contactHexEncodedPublicKey + ".");
}
});
}

View File

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.WorkerThread;
import com.annimon.stream.Stream;
@ -41,7 +42,6 @@ import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSy
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
import org.whispersystems.signalservice.loki.messaging.LokiMessageFriendRequestStatus;
import java.io.FileNotFoundException;
import java.io.IOException;
@ -56,23 +56,49 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
private static final String TAG = PushMediaSendJob.class.getSimpleName();
private static final String KEY_TEMPLATE_MESSAGE_ID = "template_message_id";
private static final String KEY_MESSAGE_ID = "message_id";
private static final String KEY_DESTINATION = "destination";
private static final String KEY_IS_FRIEND_REQUEST = "is_friend_request";
private static final String KEY_CUSTOM_FR_MESSAGE = "custom_friend_request_message";
@Inject SignalServiceMessageSender messageSender;
private long messageId;
private long messageId; // The message ID
private long templateMessageId; // The message ID of the message to template this send job from
public PushMediaSendJob(long messageId, Address destination) {
this(constructParameters(destination), messageId);
// Loki - Multi device
private Address destination; // Destination to check whether this is another device we're sending to
private boolean isFriendRequest; // Whether this is a friend request message
private String customFriendRequestMessage; // If this isn't set then we use the message body
public PushMediaSendJob(long messageId, Address destination) { this(messageId, messageId, destination); }
public PushMediaSendJob(long templateMessageId, long messageId, Address destination) { this(templateMessageId, messageId, destination, false, null); }
public PushMediaSendJob(long templateMessageId, long messageId, Address destination, boolean isFriendRequest, String customFriendRequestMessage) {
this(constructParameters(destination), templateMessageId, messageId, destination, isFriendRequest, customFriendRequestMessage);
}
private PushMediaSendJob(Job.Parameters parameters, long messageId) {
private PushMediaSendJob(@NonNull Job.Parameters parameters, long templateMessageId, long messageId, Address destination, boolean isFriendRequest, String customFriendRequestMessage) {
super(parameters);
this.templateMessageId = templateMessageId;
this.messageId = messageId;
this.destination = destination;
this.isFriendRequest = isFriendRequest;
this.customFriendRequestMessage = customFriendRequestMessage;
}
@WorkerThread
public static void enqueue(@NonNull Context context, @NonNull JobManager jobManager, long messageId, @NonNull Address destination) {
enqueue(context, jobManager, messageId, messageId, destination);
}
@WorkerThread
public static void enqueue(@NonNull Context context, @NonNull JobManager jobManager, long templateMessageId, long messageId, @NonNull Address destination) {
enqueue(context, jobManager, templateMessageId, messageId, destination, false, null);
}
@WorkerThread
public static void enqueue(@NonNull Context context, @NonNull JobManager jobManager, long templateMessageId, long messageId, @NonNull Address destination, Boolean isFriendRequest, @Nullable String customFriendRequestMessage) {
try {
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
OutgoingMediaMessage message = database.getOutgoingMessage(messageId);
@ -86,10 +112,10 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
List<AttachmentUploadJob> attachmentJobs = Stream.of(attachments).map(a -> new AttachmentUploadJob(((DatabaseAttachment) a).getAttachmentId())).toList();
if (attachmentJobs.isEmpty()) {
jobManager.add(new PushMediaSendJob(messageId, destination));
jobManager.add(new PushMediaSendJob(templateMessageId, messageId, destination, isFriendRequest, customFriendRequestMessage));
} else {
jobManager.startChain(attachmentJobs)
.then(new PushMediaSendJob(messageId, destination))
.then(new PushMediaSendJob(templateMessageId, messageId, destination, isFriendRequest, customFriendRequestMessage))
.enqueue();
}
@ -102,7 +128,14 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
@Override
public @NonNull Data serialize() {
return new Data.Builder().putLong(KEY_MESSAGE_ID, messageId).build();
Data.Builder builder = new Data.Builder()
.putLong(KEY_TEMPLATE_MESSAGE_ID, templateMessageId)
.putLong(KEY_MESSAGE_ID, messageId)
.putString(KEY_DESTINATION, destination.serialize())
.putBoolean(KEY_IS_FRIEND_REQUEST, isFriendRequest);
if (customFriendRequestMessage != null) { builder.putString(KEY_CUSTOM_FR_MESSAGE, customFriendRequestMessage); }
return builder.build();
}
@Override
@ -122,11 +155,9 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
{
ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager();
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
OutgoingMediaMessage message = database.getOutgoingMessage(messageId);
OutgoingMediaMessage message = database.getOutgoingMessage(templateMessageId);
message.isFriendRequest = (DatabaseFactory.getLokiMessageDatabase(context).getFriendRequestStatus(messageId) == LokiMessageFriendRequestStatus.REQUEST_SENDING);
if (database.isSent(messageId)) {
if (messageId >= 0 && database.isSent(messageId)) {
warn(TAG, "Message " + messageId + " was already sent. Ignoring.");
return;
}
@ -134,15 +165,17 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
try {
log(TAG, "Sending message: " + messageId);
Recipient recipient = message.getRecipient().resolve();
Recipient recipient = Recipient.from(context, destination, false);
byte[] profileKey = recipient.getProfileKey();
UnidentifiedAccessMode accessMode = recipient.getUnidentifiedAccessMode();
boolean unidentified = deliver(message);
database.markAsSent(messageId, true);
markAttachmentsUploaded(messageId, message.getAttachments());
database.markUnidentified(messageId, unidentified);
if (messageId >= 0) {
database.markAsSent(messageId, true);
markAttachmentsUploaded(messageId, message.getAttachments());
database.markUnidentified(messageId, unidentified);
}
if (recipient.isLocalNumber()) {
SyncMessageId id = new SyncMessageId(recipient.getAddress(), message.getSentTimeMillis());
@ -163,7 +196,7 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
}
}
if (message.getExpiresIn() > 0 && !message.isExpirationUpdate()) {
if (messageId > 0 && message.getExpiresIn() > 0 && !message.isExpirationUpdate()) {
database.markExpireStarted(messageId);
expirationManager.scheduleDeletion(messageId, true, message.getExpiresIn());
}
@ -172,9 +205,11 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
} catch (InsecureFallbackApprovalException ifae) {
warn(TAG, "Failure", ifae);
database.markAsPendingInsecureSmsFallback(messageId);
notifyMediaMessageDeliveryFailed(context, messageId);
ApplicationContext.getInstance(context).getJobManager().add(new DirectoryRefreshJob(false));
if (messageId >= 0) {
database.markAsPendingInsecureSmsFallback(messageId);
notifyMediaMessageDeliveryFailed(context, messageId);
ApplicationContext.getInstance(context).getJobManager().add(new DirectoryRefreshJob(false));
}
} catch (UntrustedIdentityException uie) {
warn(TAG, "Failure", uie);
database.addMismatchedIdentity(messageId, Address.fromSerialized(uie.getE164Number()), uie.getIdentityKey());
@ -191,22 +226,19 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
@Override
public void onCanceled() {
DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageId);
notifyMediaMessageDeliveryFailed(context, messageId);
if (messageId >= 0) {
DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageId);
notifyMediaMessageDeliveryFailed(context, messageId);
}
}
private boolean deliver(OutgoingMediaMessage message)
throws RetryLaterException, InsecureFallbackApprovalException, UntrustedIdentityException,
UndeliverableMessageException
{
if (message.getRecipient() == null) {
throw new UndeliverableMessageException("No destination address.");
}
try {
// rotateSenderCertificateIfNecessary();
SignalServiceAddress address = getPushAddress(message.getRecipient().getAddress());
Recipient recipient = Recipient.from(context, destination, false);
SignalServiceAddress address = getPushAddress(recipient.getAddress());
List<Attachment> attachments = Stream.of(message.getAttachments()).filterNot(Attachment::isSticker).toList();
List<SignalServiceAttachment> serviceAttachments = getAttachmentPointersFor(attachments);
Optional<byte[]> profileKey = getProfileKey(message.getRecipient());
@ -216,15 +248,10 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
List<Preview> previews = getPreviewsFor(message);
// Loki - Include a pre key bundle if the message is a friend request or an end session message
PreKeyBundle preKeyBundle;
if (message.isFriendRequest) {
preKeyBundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(address.getNumber());
} else {
preKeyBundle = null;
}
PreKeyBundle preKeyBundle = isFriendRequest ? DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(address.getNumber()) : null;
String body = (isFriendRequest && customFriendRequestMessage != null) ? customFriendRequestMessage : message.getBody();
SignalServiceDataMessage mediaMessage = SignalServiceDataMessage.newBuilder()
.withBody(message.getBody())
.withBody(body)
.withAttachments(serviceAttachments)
.withTimestamp(message.getSentTimeMillis())
.withExpiration((int)(message.getExpiresIn() / 1000))
@ -235,7 +262,7 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
.withPreviews(previews)
.asExpirationUpdate(message.isExpirationUpdate())
.withPreKeyBundle(preKeyBundle)
.asFriendRequest(message.isFriendRequest)
.asFriendRequest(isFriendRequest)
.build();
if (address.getNumber().equals(TextSecurePreferences.getLocalNumber(context))) {
@ -245,7 +272,7 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
messageSender.sendMessage(messageId, syncMessage, syncAccess);
return syncAccess.isPresent();
} else {
return messageSender.sendMessage(messageId, address, UnidentifiedAccessUtil.getAccessFor(context, message.getRecipient()), mediaMessage).getSuccess().isUnidentified();
return messageSender.sendMessage(messageId, address, UnidentifiedAccessUtil.getAccessFor(context, recipient), mediaMessage).getSuccess().isUnidentified();
}
} catch (UnregisteredUserException e) {
warn(TAG, e);
@ -262,7 +289,12 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
public static final class Factory implements Job.Factory<PushMediaSendJob> {
@Override
public @NonNull PushMediaSendJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new PushMediaSendJob(parameters, data.getLong(KEY_MESSAGE_ID));
long templateMessageID = data.getLong(KEY_TEMPLATE_MESSAGE_ID);
long messageID = data.getLong(KEY_MESSAGE_ID);
Address destination = Address.fromSerialized(data.getString(KEY_DESTINATION));
boolean isFriendRequest = data.getBoolean(KEY_IS_FRIEND_REQUEST);
String frMessage = data.hasString(KEY_CUSTOM_FR_MESSAGE) ? data.getString(KEY_CUSTOM_FR_MESSAGE) : null;
return new PushMediaSendJob(parameters, templateMessageID, messageID, destination, isFriendRequest, frMessage);
}
}
}

View File

@ -40,24 +40,47 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
private static final String TAG = PushTextSendJob.class.getSimpleName();
private static final String KEY_TEMPLATE_MESSAGE_ID = "template_message_id";
private static final String KEY_MESSAGE_ID = "message_id";
private static final String KEY_DESTINATION = "destination";
private static final String KEY_IS_FRIEND_REQUEST = "is_friend_request";
private static final String KEY_CUSTOM_FR_MESSAGE = "custom_friend_request_message";
@Inject SignalServiceMessageSender messageSender;
private long messageId;
private long messageId; // The message ID
private long templateMessageId; // The message ID of the message to template this send job from
public PushTextSendJob(long messageId, Address destination) {
this(constructParameters(destination), messageId);
// Loki - Multi device
private Address destination; // Destination to check whether this is another device we're sending to
private boolean isFriendRequest; // Whether this is a friend request message
private String customFriendRequestMessage; // If this isn't set then we use the message body
public PushTextSendJob(long messageId, Address destination) { this(messageId, messageId, destination); }
public PushTextSendJob(long templateMessageId, long messageId, Address destination) { this(templateMessageId, messageId, destination, false, null); }
public PushTextSendJob(long templateMessageId, long messageId, Address destination, boolean isFriendRequest, String customFriendRequestMessage) {
this(constructParameters(destination), templateMessageId, messageId, destination, isFriendRequest, customFriendRequestMessage);
}
private PushTextSendJob(@NonNull Job.Parameters parameters, long messageId) {
private PushTextSendJob(@NonNull Job.Parameters parameters, long templateMessageId, long messageId, Address destination, boolean isFriendRequest, String customFriendRequestMessage) {
super(parameters);
this.templateMessageId = templateMessageId;
this.messageId = messageId;
this.destination = destination;
this.isFriendRequest = isFriendRequest;
this.customFriendRequestMessage = customFriendRequestMessage;
}
@Override
public @NonNull Data serialize() {
return new Data.Builder().putLong(KEY_MESSAGE_ID, messageId).build();
Data.Builder builder = new Data.Builder()
.putLong(KEY_TEMPLATE_MESSAGE_ID, templateMessageId)
.putLong(KEY_MESSAGE_ID, messageId)
.putString(KEY_DESTINATION, destination.serialize())
.putBoolean(KEY_IS_FRIEND_REQUEST, isFriendRequest);
if (customFriendRequestMessage != null) { builder.putString(KEY_CUSTOM_FR_MESSAGE, customFriendRequestMessage); }
return builder.build();
}
@Override
@ -67,31 +90,38 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
@Override
public void onAdded() {
DatabaseFactory.getSmsDatabase(context).markAsSending(messageId);
if (messageId >= 0) {
DatabaseFactory.getSmsDatabase(context).markAsSending(messageId);
}
}
@Override
public void onPushSend() throws NoSuchMessageException, RetryLaterException {
ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager();
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
SmsMessageRecord record = database.getMessage(messageId);
SmsMessageRecord record = database.getMessage(templateMessageId);
if (!record.isPending() && !record.isFailed()) {
warn(TAG, "Message " + messageId + " was already sent. Ignoring.");
Recipient recordRecipient = record.getRecipient().resolve();
boolean hasSameDestination = destination.equals(recordRecipient.getAddress());
if (hasSameDestination && !record.isPending() && !record.isFailed()) {
warn(TAG, "Message " + templateMessageId + " was already sent. Ignoring.");
return;
}
try {
log(TAG, "Sending message: " + messageId);
log(TAG, "Sending message: " + templateMessageId + (hasSameDestination ? "" : "to another device."));
Recipient recipient = record.getRecipient().resolve();
Recipient recipient = Recipient.from(context, destination, false);
byte[] profileKey = recipient.getProfileKey();
UnidentifiedAccessMode accessMode = recipient.getUnidentifiedAccessMode();
boolean unidentified = deliver(record);
database.markAsSent(messageId, true);
database.markUnidentified(messageId, unidentified);
if (messageId >= 0) {
database.markAsSent(messageId, true);
database.markUnidentified(messageId, unidentified);
}
if (recipient.isLocalNumber()) {
SyncMessageId id = new SyncMessageId(recipient.getAddress(), record.getDateSent());
@ -112,12 +142,12 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
}
}
if (record.getExpiresIn() > 0) {
if (record.getExpiresIn() > 0 && messageId >= 0) {
database.markExpireStarted(messageId);
expirationManager.scheduleDeletion(record.getId(), record.isMms(), record.getExpiresIn());
}
log(TAG, "Sent message: " + messageId);
log(TAG, "Sent message: " + templateMessageId + (hasSameDestination ? "" : "to another device."));
} catch (InsecureFallbackApprovalException e) {
warn(TAG, "Failure", e);
@ -142,13 +172,15 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
@Override
public void onCanceled() {
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId);
if (messageId >= 0) {
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId);
long threadId = DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageId);
Recipient recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId);
long threadId = DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageId);
Recipient recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId);
if (threadId != -1 && recipient != null) {
MessageNotifier.notifyMessageDeliveryFailed(context, recipient, threadId);
if (threadId != -1 && recipient != null) {
MessageNotifier.notifyMessageDeliveryFailed(context, recipient, threadId);
}
}
}
@ -157,28 +189,29 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
{
try {
// rotateSenderCertificateIfNecessary();
SignalServiceAddress address = getPushAddress(message.getIndividualRecipient().getAddress());
Optional<byte[]> profileKey = getProfileKey(message.getIndividualRecipient());
Optional<UnidentifiedAccessPair> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, message.getIndividualRecipient());
Recipient recipient = Recipient.from(context, destination, false);
SignalServiceAddress address = getPushAddress(recipient.getAddress());
Optional<byte[]> profileKey = getProfileKey(recipient);
Optional<UnidentifiedAccessPair> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient);
log(TAG, "Have access key to use: " + unidentifiedAccess.isPresent());
// Loki - Include a pre key bundle if the message is a friend request or an end session message
PreKeyBundle preKeyBundle;
if (message.isFriendRequest() || message.isEndSession()) {
if (isFriendRequest || message.isEndSession()) {
preKeyBundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(address.getNumber());
} else {
preKeyBundle = null;
}
String body = (isFriendRequest && customFriendRequestMessage != null) ? customFriendRequestMessage : message.getBody();
SignalServiceDataMessage textSecureMessage = SignalServiceDataMessage.newBuilder()
.withTimestamp(message.getDateSent())
.withBody(message.getBody())
.withBody(body)
.withExpiration((int)(message.getExpiresIn() / 1000))
.withProfileKey(profileKey.orNull())
.asEndSessionMessage(message.isEndSession())
.asFriendRequest(message.isFriendRequest())
.asFriendRequest(isFriendRequest)
.withPreKeyBundle(preKeyBundle)
.build();
@ -203,7 +236,12 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
public static class Factory implements Job.Factory<PushTextSendJob> {
@Override
public @NonNull PushTextSendJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new PushTextSendJob(parameters, data.getLong(KEY_MESSAGE_ID));
long templateMessageID = data.getLong(KEY_TEMPLATE_MESSAGE_ID);
long messageID = data.getLong(KEY_MESSAGE_ID);
Address destination = Address.fromSerialized(data.getString(KEY_DESTINATION));
boolean isFriendRequest = data.getBoolean(KEY_IS_FRIEND_REQUEST);
String frMessage = data.hasString(KEY_CUSTOM_FR_MESSAGE) ? data.getString(KEY_CUSTOM_FR_MESSAGE) : null;
return new PushTextSendJob(parameters, templateMessageID, messageID, destination, isFriendRequest, frMessage);
}
}
}

View File

@ -18,6 +18,23 @@ fun <T> SQLiteDatabase.get(table: String, query: String, arguments: Array<String
return null
}
fun <T> SQLiteDatabase.getAll(table: String, query: String, arguments: Array<String>, get: (Cursor) -> T): List<T> {
val result = mutableListOf<T>()
var cursor: Cursor? = null
try {
cursor = query(table, null, query, arguments, null, null, null)
while (cursor != null && cursor.moveToNext()) {
result.add(get(cursor))
}
return result
} catch (e: Exception) {
// Do nothing
} finally {
cursor?.close()
}
return listOf()
}
fun SQLiteDatabase.insertOrUpdate(table: String, values: ContentValues, query: String, arguments: Array<String>) {
val id = insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_IGNORE).toInt()
if (id == -1) {

View File

@ -1,165 +1,67 @@
package org.thoughtcrime.securesms.loki
import android.content.Context
import android.graphics.Color
import android.graphics.PorterDuff
import android.os.Handler
import android.support.v7.app.AlertDialog
import android.util.AttributeSet
import android.util.Log
import android.view.View
import android.widget.LinearLayout
import kotlinx.android.synthetic.main.view_device_linking.view.*
import network.loki.messenger.R
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.loki.crypto.MnemonicCodec
import org.whispersystems.signalservice.loki.utilities.removing05PrefixIfNeeded
import java.io.File
import java.io.FileOutputStream
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.loki.api.DeviceLinkingSession
import org.whispersystems.signalservice.loki.api.DeviceLinkingSessionListener
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
object DeviceLinkingDialog {
class DeviceLinkingDialog private constructor(private val context: Context, private val mode: DeviceLinkingView.Mode, private val delegate: DeviceLinkingDialogDelegate?) : DeviceLinkingViewDelegate, DeviceLinkingSessionListener {
private lateinit var view: DeviceLinkingView
private lateinit var dialog: AlertDialog
fun show(context: Context, mode: DeviceLinkingView.Mode) {
val view = DeviceLinkingView(context, mode)
val dialog = AlertDialog.Builder(context).setView(view).show()
view.dismiss = { dialog.dismiss() }
}
}
companion object {
class DeviceLinkingView private constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, private val mode: Mode) : LinearLayout(context, attrs, defStyleAttr) {
private var delegate: DeviceLinkingDialogDelegate? = null
private lateinit var languageFileDirectory: File
var dismiss: (() -> Unit)? = null
// region Types
enum class Mode { Master, Slave }
// endregion
// region Lifecycle
constructor(context: Context, mode: Mode) : this(context, null, 0, mode)
private constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0, Mode.Master) // Just pass in a dummy mode
private constructor(context: Context) : this(context, null)
init {
if (mode == Mode.Slave) {
if (delegate == null) { throw IllegalStateException("Missing delegate for device linking dialog in slave mode.") }
}
setUpLanguageFileDirectory()
setUpViewHierarchy()
when (mode) {
Mode.Master -> Log.d("Loki", "TODO: DeviceLinkingSession.startListeningForLinkingRequests(this)")
Mode.Slave -> Log.d("Loki", "TODO: DeviceLinkingSession.startListeningForAuthorization(this)")
fun show(context: Context, mode: DeviceLinkingView.Mode, delegate: DeviceLinkingDialogDelegate?): DeviceLinkingDialog {
val dialog = DeviceLinkingDialog(context, mode, delegate)
dialog.show()
return dialog
}
}
private fun setUpLanguageFileDirectory() {
val languages = listOf( "english", "japanese", "portuguese", "spanish" )
val directory = File(context.applicationInfo.dataDir)
for (language in languages) {
val fileName = "$language.txt"
if (directory.list().contains(fileName)) { continue }
val inputStream = context.assets.open("mnemonic/$fileName")
val file = File(directory, fileName)
val outputStream = FileOutputStream(file)
val buffer = ByteArray(1024)
while (true) {
val count = inputStream.read(buffer)
if (count < 0) { break }
outputStream.write(buffer, 0, count)
}
inputStream.close()
outputStream.close()
private fun show() {
view = DeviceLinkingView(context, mode, this)
dialog = AlertDialog.Builder(context).setView(view).show()
view.dismiss = { dismiss() }
DeviceLinkingSession.shared.startListeningForLinkingRequests()
DeviceLinkingSession.shared.addListener(this)
}
private fun dismiss() {
DeviceLinkingSession.shared.stopListeningForLinkingRequests()
DeviceLinkingSession.shared.removeListener(this)
dialog.dismiss()
}
override fun handleDeviceLinkAuthorized(pairingAuthorisation: PairingAuthorisation) {
delegate?.handleDeviceLinkAuthorized(pairingAuthorisation)
}
override fun handleDeviceLinkingDialogDismissed() {
if (mode == DeviceLinkingView.Mode.Master && view.pairingAuthorisation != null) {
val authorisation = view.pairingAuthorisation!!
DatabaseFactory.getLokiPreKeyBundleDatabase(context).removePreKeyBundle(authorisation.secondaryDevicePublicKey)
}
languageFileDirectory = directory
delegate?.handleDeviceLinkingDialogDismissed()
}
private fun setUpViewHierarchy() {
inflate(context, R.layout.view_device_linking, this)
spinner.indeterminateDrawable.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN)
val titleID = when (mode) {
Mode.Master -> R.string.view_device_linking_title_1
Mode.Slave -> R.string.view_device_linking_title_2
override fun sendPairingAuthorizedMessage(pairingAuthorisation: PairingAuthorisation) {
delegate?.sendPairingAuthorizedMessage(pairingAuthorisation)
}
override fun requestUserAuthorization(authorisation: PairingAuthorisation) {
Util.runOnMain {
view.requestUserAuthorization(authorisation)
}
titleTextView.text = resources.getString(titleID)
val explanationID = when (mode) {
Mode.Master -> R.string.view_device_linking_explanation_1
Mode.Slave -> R.string.view_device_linking_explanation_2
DeviceLinkingSession.shared.stopListeningForLinkingRequests()
}
override fun onDeviceLinkRequestAuthorized(authorisation: PairingAuthorisation) {
Util.runOnMain {
view.onDeviceLinkAuthorized(authorisation)
}
explanationTextView.text = resources.getString(explanationID)
mnemonicTextView.visibility = if (mode == Mode.Master) View.GONE else View.VISIBLE
if (mode == Mode.Slave) {
val hexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context).removing05PrefixIfNeeded()
mnemonicTextView.text = MnemonicCodec(languageFileDirectory).encode(hexEncodedPublicKey).split(" ").slice(0 until 3).joinToString(" ")
}
authorizeButton.visibility = View.GONE
cancelButton.setOnClickListener { cancel() }
DeviceLinkingSession.shared.stopListeningForLinkingRequests()
}
// endregion
// region Device Linking
private fun requestUserAuthorization() { // TODO: deviceLink parameter
// To be called by DeviceLinkingSession when a linking request has been received
// TODO: this.deviceLink = deviceLink
spinner.visibility = View.GONE
val titleTextViewLayoutParams = titleTextView.layoutParams as LayoutParams
titleTextViewLayoutParams.topMargin = toPx(16, resources)
titleTextView.layoutParams = titleTextViewLayoutParams
titleTextView.text = resources.getString(R.string.view_device_linking_title_3)
explanationTextView.text = resources.getString(R.string.view_device_linking_explanation_2)
mnemonicTextView.visibility = View.VISIBLE
val hexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context).removing05PrefixIfNeeded() // TODO: deviceLink.slave.hexEncodedPublicKey.removing05PrefixIfNeeded()
mnemonicTextView.text = MnemonicCodec(languageFileDirectory).encode(hexEncodedPublicKey).split(" ").slice(0 until 3).joinToString(" ")
authorizeButton.visibility = View.VISIBLE
}
private fun authorizeDeviceLink() {
// TODO: val deviceLink = this.deviceLink!!
// TODO: val linkingAuthorizationMessage = DeviceLinkingUtilities.getLinkingAuthorizationMessage(deviceLink)
// TODO: Send the linking authorization message
// TODO: val session = DeviceLinkingSession.current!!
// TODO: session.stopListeningForLinkingRequests()
// TODO: session.markLinkingRequestAsProcessed()
dismiss?.invoke()
// TODO: val master = DeviceLink.Device(deviceLink.master.hexEncodedPublicKey, linkingAuthorizationMessage.masterSignature)
// TODO: val signedDeviceLink = DeviceLink(master, deviceLink.slave)
// TODO: LokiStorageAPI.addDeviceLink(signedDeviceLink).fail { error ->
// TODO: Log.d("Loki", "Failed to add device link due to error: $error.")
// TODO: }
}
private fun handleDeviceLinkAuthorized() { // TODO: deviceLink parameter
// To be called by DeviceLinkingSession when a device link has been authorized
// TODO: val session = DeviceLinkingSession.current!!
// TODO: session.stopListeningForLinkingAuthorization()
spinner.visibility = View.GONE
val titleTextViewLayoutParams = titleTextView.layoutParams as LayoutParams
titleTextViewLayoutParams.topMargin = toPx(8, resources)
titleTextView.layoutParams = titleTextViewLayoutParams
titleTextView.text = resources.getString(R.string.view_device_linking_title_4)
val explanationTextViewLayoutParams = explanationTextView.layoutParams as LayoutParams
explanationTextViewLayoutParams.bottomMargin = toPx(12, resources)
explanationTextView.layoutParams = explanationTextViewLayoutParams
explanationTextView.text = resources.getString(R.string.view_device_linking_explanation_3)
titleTextView.text = resources.getString(R.string.view_device_linking_title_4)
mnemonicTextView.visibility = View.GONE
buttonContainer.visibility = View.GONE
// TODO: LokiStorageAPI.addDeviceLink(signedDeviceLink).fail { error ->
// TODO: Log.d("Loki", "Failed to add device link due to error: $error.")
// TODO: }
Handler().postDelayed({
delegate?.handleDeviceLinkAuthorized()
dismiss?.invoke()
}, 4000)
}
// endregion
// region Interaction
private fun cancel() {
// TODO: val session = DeviceLinkingSession.current!!
// TODO: session.stopListeningForLinkingRequests()
// TODO: session.markLinkingRequestAsProcessed() // Only relevant in master mode
delegate?.handleDeviceLinkingDialogDismissed() // Only relevant in slave mode
dismiss?.invoke()
}
// endregion
}

View File

@ -1,7 +1,10 @@
package org.thoughtcrime.securesms.loki
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
interface DeviceLinkingDialogDelegate {
fun handleDeviceLinkAuthorized() // TODO: Device link
fun handleDeviceLinkingDialogDismissed()
fun handleDeviceLinkAuthorized(pairingAuthorisation: PairingAuthorisation) { }
fun handleDeviceLinkingDialogDismissed() { }
fun sendPairingAuthorizedMessage(pairingAuthorisation: PairingAuthorisation) { }
}

View File

@ -0,0 +1,137 @@
package org.thoughtcrime.securesms.loki
import android.content.Context
import android.graphics.Color
import android.graphics.PorterDuff
import android.os.Handler
import android.util.AttributeSet
import android.view.View
import android.widget.LinearLayout
import kotlinx.android.synthetic.main.view_device_linking.view.*
import network.loki.messenger.R
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
import org.whispersystems.signalservice.loki.crypto.MnemonicCodec
import org.whispersystems.signalservice.loki.utilities.removing05PrefixIfNeeded
import java.io.File
import java.io.FileOutputStream
class DeviceLinkingView private constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, private val mode: Mode, private var delegate: DeviceLinkingViewDelegate) : LinearLayout(context, attrs, defStyleAttr) {
private lateinit var languageFileDirectory: File
var dismiss: (() -> Unit)? = null
var pairingAuthorisation: PairingAuthorisation? = null
private set
// region Types
enum class Mode { Master, Slave }
// endregion
// region Lifecycle
constructor(context: Context, mode: Mode, delegate: DeviceLinkingViewDelegate) : this(context, null, 0, mode, delegate)
private constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0, Mode.Master, object : DeviceLinkingViewDelegate { }) // Just pass in a dummy mode
private constructor(context: Context) : this(context, null)
init {
setUpLanguageFileDirectory()
setUpViewHierarchy()
}
private fun setUpLanguageFileDirectory() {
val languages = listOf( "english", "japanese", "portuguese", "spanish" )
val directory = File(context.applicationInfo.dataDir)
for (language in languages) {
val fileName = "$language.txt"
if (directory.list().contains(fileName)) { continue }
val inputStream = context.assets.open("mnemonic/$fileName")
val file = File(directory, fileName)
val outputStream = FileOutputStream(file)
val buffer = ByteArray(1024)
while (true) {
val count = inputStream.read(buffer)
if (count < 0) { break }
outputStream.write(buffer, 0, count)
}
inputStream.close()
outputStream.close()
}
languageFileDirectory = directory
}
private fun setUpViewHierarchy() {
inflate(context, R.layout.view_device_linking, this)
spinner.indeterminateDrawable.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN)
val titleID = when (mode) {
Mode.Master -> R.string.view_device_linking_title_1
Mode.Slave -> R.string.view_device_linking_title_2
}
titleTextView.text = resources.getString(titleID)
val explanationID = when (mode) {
Mode.Master -> R.string.view_device_linking_explanation_1
Mode.Slave -> R.string.view_device_linking_explanation_2
}
explanationTextView.text = resources.getString(explanationID)
mnemonicTextView.visibility = if (mode == Mode.Master) View.GONE else View.VISIBLE
if (mode == Mode.Slave) {
val hexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context).removing05PrefixIfNeeded()
mnemonicTextView.text = MnemonicCodec(languageFileDirectory).encode(hexEncodedPublicKey).split(" ").slice(0 until 3).joinToString(" ")
}
authorizeButton.visibility = View.GONE
authorizeButton.setOnClickListener { authorizePairing() }
cancelButton.setOnClickListener { cancel() }
}
// endregion
// region Device Linking
fun requestUserAuthorization(pairingAuthorisation: PairingAuthorisation) {
if (mode != Mode.Master || pairingAuthorisation.type != PairingAuthorisation.Type.REQUEST || this.pairingAuthorisation != null) { return }
this.pairingAuthorisation = pairingAuthorisation
spinner.visibility = View.GONE
val titleTextViewLayoutParams = titleTextView.layoutParams as LayoutParams
titleTextViewLayoutParams.topMargin = toPx(16, resources)
titleTextView.layoutParams = titleTextViewLayoutParams
titleTextView.text = resources.getString(R.string.view_device_linking_title_3)
explanationTextView.text = resources.getString(R.string.view_device_linking_explanation_2)
mnemonicTextView.visibility = View.VISIBLE
val hexEncodedPublicKey = pairingAuthorisation.secondaryDevicePublicKey.removing05PrefixIfNeeded()
mnemonicTextView.text = MnemonicCodec(languageFileDirectory).encode(hexEncodedPublicKey).split(" ").slice(0 until 3).joinToString(" ")
authorizeButton.visibility = View.VISIBLE
}
fun onDeviceLinkAuthorized(pairingAuthorisation: PairingAuthorisation) {
if (mode != Mode.Slave || pairingAuthorisation.type != PairingAuthorisation.Type.GRANT || this.pairingAuthorisation != null) { return }
this.pairingAuthorisation = pairingAuthorisation
spinner.visibility = View.GONE
val titleTextViewLayoutParams = titleTextView.layoutParams as LayoutParams
titleTextViewLayoutParams.topMargin = toPx(8, resources)
titleTextView.layoutParams = titleTextViewLayoutParams
titleTextView.text = resources.getString(R.string.view_device_linking_title_4)
val explanationTextViewLayoutParams = explanationTextView.layoutParams as LayoutParams
explanationTextViewLayoutParams.bottomMargin = toPx(12, resources)
explanationTextView.layoutParams = explanationTextViewLayoutParams
explanationTextView.text = resources.getString(R.string.view_device_linking_explanation_3)
titleTextView.text = resources.getString(R.string.view_device_linking_title_4)
mnemonicTextView.visibility = View.GONE
buttonContainer.visibility = View.GONE
cancelButton.visibility = View.GONE
Handler().postDelayed({
delegate.handleDeviceLinkAuthorized(pairingAuthorisation)
dismiss?.invoke()
}, 4000)
}
// endregion
// region Interaction
private fun authorizePairing() {
val pairingAuthorisation = this.pairingAuthorisation
if (mode != Mode.Master || pairingAuthorisation == null) { return; }
delegate.sendPairingAuthorizedMessage(pairingAuthorisation)
delegate.handleDeviceLinkAuthorized(pairingAuthorisation)
dismiss?.invoke()
}
private fun cancel() {
delegate.handleDeviceLinkingDialogDismissed()
dismiss?.invoke()
}
// endregion
}

View File

@ -0,0 +1,10 @@
package org.thoughtcrime.securesms.loki
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
interface DeviceLinkingViewDelegate {
fun handleDeviceLinkAuthorized(pairingAuthorisation: PairingAuthorisation) { }
fun handleDeviceLinkingDialogDismissed() { }
fun sendPairingAuthorizedMessage(pairingAuthorisation: PairingAuthorisation) { }
}

View File

@ -42,6 +42,7 @@ class DisplayNameActivity : BaseActionBarActivity() {
val application = ApplicationContext.getInstance(this)
application.setUpP2PAPI()
application.startLongPollingIfNeeded()
application.setUpStorageAPIIfNeeded()
startActivity(Intent(this, ConversationListActivity::class.java))
finish()
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(this)

View File

@ -1,8 +1,14 @@
package org.thoughtcrime.securesms.loki
import android.content.Context
import android.content.res.Resources
import android.os.Build
import android.support.annotation.ColorRes
import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.recipients.Recipient
import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus
import kotlin.math.roundToInt
fun Resources.getColorWithID(@ColorRes id: Int, theme: Resources.Theme?): Int {
@ -16,4 +22,20 @@ fun Resources.getColorWithID(@ColorRes id: Int, theme: Resources.Theme?): Int {
fun toPx(dp: Int, resources: Resources): Int {
val scale = resources.displayMetrics.density
return (dp * scale).roundToInt()
}
fun isGroupRecipient(recipient: String): Boolean {
return (LokiGroupChatAPI.publicChatServer == recipient)
}
fun getFriendPublicKeys(context: Context, devicePublicKeys: Set<String>): Set<String> {
val lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context)
return devicePublicKeys.mapNotNull { device ->
val address = Address.fromSerialized(device)
val recipient = Recipient.from(context, address, false)
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(recipient)
if (threadID < 0) { return@mapNotNull null }
val friendRequestStatus = lokiThreadDatabase.getFriendRequestStatus(threadID)
if (friendRequestStatus == LokiThreadFriendRequestStatus.FRIENDS) device else null
}.toSet()
}

View File

@ -4,9 +4,11 @@ import android.content.ContentValues
import android.content.Context
import org.thoughtcrime.securesms.database.Database
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
import org.thoughtcrime.securesms.util.Base64
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.loki.api.LokiAPIDatabaseProtocol
import org.whispersystems.signalservice.loki.api.LokiAPITarget
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
// TODO: Clean this up a bit
@ -45,6 +47,14 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
private val lastDeletionServerIDCacheIndex = "loki_api_last_deletion_server_id_cache_index"
private val lastDeletionServerID = "last_deletion_server_id"
@JvmStatic val createLastDeletionServerIDTableCommand = "CREATE TABLE $lastDeletionServerIDCache ($lastDeletionServerIDCacheIndex STRING PRIMARY KEY, $lastDeletionServerID INTEGER DEFAULT 0);"
// Pairing authorisation cache
private val pairingAuthorisationCache = "loki_pairing_authorisation_cache"
private val primaryDevicePublicKey = "primary_device"
private val secondaryDevicePublicKey = "secondary_device"
private val requestSignature = "request_signature"
private val grantSignature = "grant_signature"
@JvmStatic val createPairingAuthorisationTableCommand = "CREATE TABLE $pairingAuthorisationCache ($primaryDevicePublicKey TEXT, $secondaryDevicePublicKey TEXT, " +
"$requestSignature TEXT NULLABLE DEFAULT NULL, $grantSignature TEXT NULLABLE DEFAULT NULL, PRIMARY KEY ($primaryDevicePublicKey, $secondaryDevicePublicKey));"
}
override fun getSwarmCache(hexEncodedPublicKey: String): Set<LokiAPITarget>? {
@ -95,14 +105,14 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
database.insertOrUpdate(receivedMessageHashValuesCache, row, "$userID = ?", wrap(userPublicKey))
}
override fun getGroupChatAuthToken(server: String): String? {
override fun getAuthToken(server: String): String? {
val database = databaseHelper.readableDatabase
return database.get(groupChatAuthTokenTable, "${Companion.server} = ?", wrap(server)) { cursor ->
cursor.getString(cursor.getColumnIndexOrThrow(token))
}
}
override fun setGroupChatAuthToken(server: String, newValue: String?) {
override fun setAuthToken(server: String, newValue: String?) {
val database = databaseHelper.writableDatabase
if (newValue != null) {
val row = wrap(mapOf(Companion.server to server, token to newValue))
@ -141,6 +151,32 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
val row = wrap(mapOf( lastDeletionServerIDCacheIndex to index, lastDeletionServerID to newValue.toString() ))
database.insertOrUpdate(lastDeletionServerIDCache, row, "$lastDeletionServerIDCacheIndex = ?", wrap(index))
}
override fun getPairingAuthorisations(hexEncodedPublicKey: String): List<PairingAuthorisation> {
val database = databaseHelper.readableDatabase
return database.getAll(pairingAuthorisationCache, "$primaryDevicePublicKey = ? OR $secondaryDevicePublicKey = ?", arrayOf( hexEncodedPublicKey, hexEncodedPublicKey )) { cursor ->
val primaryDevicePubKey = cursor.getString(primaryDevicePublicKey)
val secondaryDevicePubKey = cursor.getString(secondaryDevicePublicKey)
val requestSignature: ByteArray? = if (cursor.isNull(cursor.getColumnIndexOrThrow(requestSignature))) null else cursor.getBase64EncodedData(requestSignature)
val grantSignature: ByteArray? = if (cursor.isNull(cursor.getColumnIndexOrThrow(grantSignature))) null else cursor.getBase64EncodedData(grantSignature)
PairingAuthorisation(primaryDevicePubKey, secondaryDevicePubKey, requestSignature, grantSignature)
}
}
override fun insertOrUpdatePairingAuthorisation(authorisation: PairingAuthorisation) {
val database = databaseHelper.writableDatabase
val values = ContentValues()
values.put(primaryDevicePublicKey, authorisation.primaryDevicePublicKey)
values.put(secondaryDevicePublicKey, authorisation.secondaryDevicePublicKey)
if (authorisation.requestSignature != null) { values.put(requestSignature, Base64.encodeBytes(authorisation.requestSignature)) }
if (authorisation.grantSignature != null) { values.put(grantSignature, Base64.encodeBytes(authorisation.grantSignature)) }
database.insertOrUpdate(pairingAuthorisationCache, values, "$primaryDevicePublicKey = ? AND $secondaryDevicePublicKey = ?", arrayOf( authorisation.primaryDevicePublicKey, authorisation.secondaryDevicePublicKey ))
}
override fun removePairingAuthorisations(hexEncodedPublicKey: String) {
val database = databaseHelper.readableDatabase
database.delete(pairingAuthorisationCache, "$primaryDevicePublicKey = ? OR $secondaryDevicePublicKey = ?", arrayOf( hexEncodedPublicKey, hexEncodedPublicKey ))
}
}
// region Convenience

View File

@ -89,4 +89,10 @@ class LokiPreKeyBundleDatabase(context: Context, helper: SQLCipherOpenHelper) :
val database = databaseHelper.writableDatabase
database.delete(tableName, "${Companion.hexEncodedPublicKey} = ?", arrayOf( hexEncodedPublicKey ))
}
fun hasPreKeyBundle(hexEncodedPublicKey: String): Boolean {
val database = databaseHelper.readableDatabase
val cursor = database.query(tableName, null, "${Companion.hexEncodedPublicKey} = ?", arrayOf( hexEncodedPublicKey ), null, null, null)
return cursor != null && cursor.count > 0
}
}

View File

@ -0,0 +1,87 @@
package org.thoughtcrime.securesms.loki
import android.content.Context
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.deferred
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.logging.Log
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.libsignal.util.guava.Optional
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.loki.api.LokiStorageAPI
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus
fun getAllDevicePublicKeys(context: Context, hexEncodedPublicKey: String, storageAPI: LokiStorageAPI, block: (devicePublicKey: String, isFriend: Boolean, friendCount: Int) -> Unit) {
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
storageAPI.getAllDevicePublicKeys(hexEncodedPublicKey).success { items ->
val devices = items.toMutableSet()
if (hexEncodedPublicKey != userHexEncodedPublicKey) {
devices.remove(userHexEncodedPublicKey)
}
val friends = getFriendPublicKeys(context, devices)
for (device in devices) {
block(device, friends.contains(device), friends.count())
}
}
}
fun shouldAutomaticallyBecomeFriendsWithDevice(publicKey: String, context: Context): Promise<Boolean, Unit> {
val lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context)
val storageAPI = LokiStorageAPI.shared
val deferred = deferred<Boolean, Unit>()
storageAPI.getPrimaryDevicePublicKey(publicKey).success { primaryDevicePublicKey ->
if (primaryDevicePublicKey == null) {
deferred.resolve(false)
return@success
}
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
if (primaryDevicePublicKey == userHexEncodedPublicKey) {
storageAPI.getSecondaryDevicePublicKeys(userHexEncodedPublicKey).success { secondaryDevices ->
deferred.resolve(secondaryDevices.contains(publicKey))
}.fail {
deferred.resolve(false)
}
return@success
}
val primaryDevice = Recipient.from(context, Address.fromSerialized(primaryDevicePublicKey), false)
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(primaryDevice)
if (threadID < 0) {
deferred.resolve(false)
return@success
}
deferred.resolve(lokiThreadDatabase.getFriendRequestStatus(threadID) == LokiThreadFriendRequestStatus.FRIENDS)
}
return deferred.promise
}
fun sendPairingAuthorisationMessage(context: Context, contactHexEncodedPublicKey: String, authorisation: PairingAuthorisation): Promise<Unit, Exception> {
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
val address = SignalServiceAddress(contactHexEncodedPublicKey)
val message = SignalServiceDataMessage.newBuilder().withBody("").withPairingAuthorisation(authorisation)
// A REQUEST should always act as a friend request. A GRANT should always be replying back as a normal message.
if (authorisation.type == PairingAuthorisation.Type.REQUEST) {
val preKeyBundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(address.number)
message.asFriendRequest(true).withPreKeyBundle(preKeyBundle)
}
return try {
Log.d("Loki", "Sending authorisation message to: $contactHexEncodedPublicKey.")
val result = messageSender.sendMessage(0, address, Optional.absent<UnidentifiedAccessPair>(), message.build())
if (result.success == null) {
val exception = when {
result.isNetworkFailure -> "Failed to send authorisation message due to a network error."
else -> "Failed to send authorisation message."
}
throw Exception(exception)
}
Promise.ofSuccess(Unit)
} catch (e: Exception) {
Log.d("Loki", "Failed to send authorisation message to: $contactHexEncodedPublicKey.")
Promise.ofFail(e)
}
}

View File

@ -28,7 +28,7 @@ class QRCodeView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : Li
init {
inflate(context, R.layout.view_qr_code, this)
val hexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
val hexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(getContext()) ?: TextSecurePreferences.getLocalNumber(context)
val displayMetrics = DisplayMetrics()
ServiceUtil.getWindowManager(context).defaultDisplay.getMetrics(displayMetrics)
val size = displayMetrics.widthPixels - 2 * toPx(96, resources)

View File

@ -9,23 +9,33 @@ import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import kotlinx.android.synthetic.main.activity_seed.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import network.loki.messenger.R
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.ConversationListActivity
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.IdentityDatabase
import org.thoughtcrime.securesms.logging.Log
import org.thoughtcrime.securesms.util.Hex
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.curve25519.Curve25519
import org.whispersystems.libsignal.util.KeyHelper
import org.whispersystems.signalservice.loki.api.LokiStorageAPI
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
import org.whispersystems.signalservice.loki.crypto.MnemonicCodec
import org.whispersystems.signalservice.loki.utilities.Analytics
import org.whispersystems.signalservice.loki.utilities.PublicKeyValidation
import org.whispersystems.signalservice.loki.utilities.hexEncodedPublicKey
import org.whispersystems.signalservice.loki.utilities.retryIfNeeded
import java.io.File
import java.io.FileOutputStream
class SeedActivity : BaseActionBarActivity() {
class SeedActivity : BaseActionBarActivity(), DeviceLinkingDialogDelegate {
private lateinit var languageFileDirectory: File
private var mode = Mode.Register
set(newValue) { field = newValue; updateUI() }
@ -35,7 +45,7 @@ class SeedActivity : BaseActionBarActivity() {
set(newValue) { field = newValue; updateMnemonicTextView() }
// region Types
enum class Mode { Register, Restore }
enum class Mode { Register, Restore, Link }
// endregion
// region Lifecycle
@ -45,8 +55,10 @@ class SeedActivity : BaseActionBarActivity() {
setUpLanguageFileDirectory()
updateSeed()
copyButton.setOnClickListener { copy() }
toggleModeButton.setOnClickListener { toggleMode() }
registerOrRestoreButton.setOnClickListener { registerOrRestore() }
toggleRegisterModeButton.setOnClickListener { mode = Mode.Register }
toggleRestoreModeButton.setOnClickListener { mode = Mode.Restore }
toggleLinkModeButton.setOnClickListener { mode = Mode.Link }
mainButton.setOnClickListener { handleMainButtonTapped() }
Analytics.shared.track("Seed Screen Viewed")
}
// endregion
@ -86,15 +98,25 @@ class SeedActivity : BaseActionBarActivity() {
}
private fun updateUI() {
seedExplanationTextView1.visibility = if (mode == Mode.Register) View.VISIBLE else View.GONE
mnemonicTextView.visibility = if (mode == Mode.Register) View.VISIBLE else View.GONE
copyButton.visibility = if (mode == Mode.Register) View.VISIBLE else View.GONE
seedExplanationTextView2.visibility = if (mode == Mode.Restore) View.VISIBLE else View.GONE
mnemonicEditText.visibility = if (mode == Mode.Restore) View.VISIBLE else View.GONE
val toggleModeButtonTitleID = if (mode == Mode.Register) R.string.activity_key_pair_toggle_mode_button_title_1 else R.string.activity_key_pair_toggle_mode_button_title_2
toggleModeButton.setText(toggleModeButtonTitleID)
val registerOrRestoreButtonTitleID = if (mode == Mode.Register) R.string.activity_key_pair_register_or_restore_button_title_1 else R.string.activity_key_pair_register_or_restore_button_title_2
registerOrRestoreButton.setText(registerOrRestoreButtonTitleID)
val registerModeVisibility = if (mode == Mode.Register) View.VISIBLE else View.GONE
val restoreModeVisibility = if (mode == Mode.Restore) View.VISIBLE else View.GONE
val linkModeVisibility = if (mode == Mode.Link) View.VISIBLE else View.GONE
seedExplanationTextView1.visibility = registerModeVisibility
mnemonicTextView.visibility = registerModeVisibility
copyButton.visibility = registerModeVisibility
seedExplanationTextView2.visibility = restoreModeVisibility
mnemonicEditText.visibility = restoreModeVisibility
linkExplanationTextView.visibility = linkModeVisibility
publicKeyEditText.visibility = linkModeVisibility
toggleRegisterModeButton.visibility = if (mode != Mode.Register) View.VISIBLE else View.GONE
toggleRestoreModeButton.visibility = if (mode != Mode.Restore) View.VISIBLE else View.GONE
toggleLinkModeButton.visibility = if (mode != Mode.Link) View.VISIBLE else View.GONE
val mainButtonTitleID = when (mode) {
Mode.Register -> R.string.activity_key_pair_main_button_title_1
Mode.Restore -> R.string.activity_key_pair_main_button_title_2
Mode.Link -> R.string.activity_key_pair_main_button_title_3
}
mainButton.setText(mainButtonTitleID)
if (mode == Mode.Restore) {
mnemonicEditText.requestFocus()
} else {
@ -102,6 +124,13 @@ class SeedActivity : BaseActionBarActivity() {
val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow(mnemonicEditText.windowToken, 0)
}
if (mode == Mode.Link) {
publicKeyEditText.requestFocus()
} else {
publicKeyEditText.clearFocus()
val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow(publicKeyEditText.windowToken, 0)
}
}
private fun updateMnemonic() {
@ -122,14 +151,7 @@ class SeedActivity : BaseActionBarActivity() {
Toast.makeText(this, R.string.activity_key_pair_mnemonic_copied_message, Toast.LENGTH_SHORT).show()
}
private fun toggleMode() {
mode = when (mode) {
Mode.Register -> Mode.Restore
Mode.Restore -> Mode.Register
}
}
private fun registerOrRestore() {
private fun handleMainButtonTapped() {
var seed: ByteArray
when (mode) {
Mode.Register -> seed = this.seed!!
@ -143,10 +165,17 @@ class SeedActivity : BaseActionBarActivity() {
return Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}
}
Mode.Link -> {
val hexEncodedPublicKey = publicKeyEditText.text.trim().toString()
if (!PublicKeyValidation.isValid(hexEncodedPublicKey)) {
return Toast.makeText(this, "Invalid public key", Toast.LENGTH_SHORT).show()
}
seed = this.seed!!
}
}
val hexEncodedSeed = Hex.toStringCondensed(seed)
IdentityKeyUtil.save(this, IdentityKeyUtil.lokiSeedKey, hexEncodedSeed)
if (seed.count() == 16) seed = seed + seed
if (seed.count() == 16) seed += seed
if (mode == Mode.Restore) {
IdentityKeyUtil.generateIdentityKeyPair(this, seed)
}
@ -161,13 +190,69 @@ class SeedActivity : BaseActionBarActivity() {
when (mode) {
Mode.Register -> Analytics.shared.track("Seed Created")
Mode.Restore -> Analytics.shared.track("Seed Restored")
// TODO: Mode.Link -> Analytics.shared.track("Device Linking Attempted")
Mode.Link -> Analytics.shared.track("Device Linking Attempted")
}
startActivity(Intent(this, DisplayNameActivity::class.java))
if (mode == Mode.Link) {
TextSecurePreferences.setHasSeenWelcomeScreen(this, true)
TextSecurePreferences.setPromptedPushRegistration(this, true)
val primaryDevicePublicKey = publicKeyEditText.text.trim().toString()
val authorisation = PairingAuthorisation(primaryDevicePublicKey, hexEncodedPublicKey).sign(PairingAuthorisation.Type.REQUEST, keyPair.privateKey.serialize())
if (authorisation == null) {
Log.d("Loki", "Failed to sign outgoing pairing request.")
resetForRegistration()
return Toast.makeText(application, "Failed to link device.", Toast.LENGTH_SHORT).show()
}
val application = ApplicationContext.getInstance(this)
application.startLongPollingIfNeeded()
application.setUpP2PAPI()
application.setUpStorageAPIIfNeeded()
DeviceLinkingDialog.show(this, DeviceLinkingView.Mode.Slave, this)
CoroutineScope(Dispatchers.Main).launch {
retryIfNeeded(8) {
sendPairingAuthorisationMessage(this@SeedActivity, authorisation.primaryDevicePublicKey, authorisation).get()
}
}
} else {
startActivity(Intent(this, DisplayNameActivity::class.java))
finish()
}
}
override fun handleDeviceLinkAuthorized(pairingAuthorisation: PairingAuthorisation) {
Analytics.shared.track("Device Linked Successfully")
if (pairingAuthorisation.secondaryDevicePublicKey == TextSecurePreferences.getLocalNumber(this)) {
TextSecurePreferences.setMasterHexEncodedPublicKey(this, pairingAuthorisation.primaryDevicePublicKey)
}
startActivity(Intent(this, ConversationListActivity::class.java))
finish()
}
// TODO: Analytics.shared.track("Device Linked Successfully")
override fun handleDeviceLinkingDialogDismissed() {
resetForRegistration()
}
override fun sendPairingAuthorizedMessage(pairingAuthorisation: PairingAuthorisation) {
val userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(this).privateKey.serialize()
val signedPairingAuthorisation = pairingAuthorisation.sign(PairingAuthorisation.Type.GRANT, userPrivateKey)
if (signedPairingAuthorisation == null || signedPairingAuthorisation.type != PairingAuthorisation.Type.GRANT) {
Log.d("Loki", "Failed to sign pairing authorization.")
return
}
retryIfNeeded(8) {
sendPairingAuthorisationMessage(this, pairingAuthorisation.secondaryDevicePublicKey, signedPairingAuthorisation).get()
}.fail {
Log.d("Loki", "Failed to send pairing authorization message to ${pairingAuthorisation.secondaryDevicePublicKey}.")
}
DatabaseFactory.getLokiAPIDatabase(this).insertOrUpdatePairingAuthorisation(signedPairingAuthorisation)
LokiStorageAPI.shared.updateUserDeviceMappings()
}
private fun resetForRegistration() {
IdentityKeyUtil.delete(this, IdentityKeyUtil.lokiSeedKey)
TextSecurePreferences.removeLocalRegistrationId(this)
TextSecurePreferences.removeLocalNumber(this)
TextSecurePreferences.setHasSeenWelcomeScreen(this, false)
TextSecurePreferences.setPromptedPushRegistration(this, false)
}
// endregion
}

View File

@ -20,14 +20,16 @@ import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
import org.thoughtcrime.securesms.jobs.MultiDeviceReadUpdateJob;
import org.thoughtcrime.securesms.jobs.SendReadReceiptJob;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.loki.LokiThreadDatabase;
import org.thoughtcrime.securesms.loki.MultiDeviceUtilitiesKt;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus;
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import kotlin.Unit;
public class MarkReadReceiver extends BroadcastReceiver {
private static final String TAG = MarkReadReceiver.class.getSimpleName();
@ -86,17 +88,17 @@ public class MarkReadReceiver extends BroadcastReceiver {
.collect(Collectors.groupingBy(SyncMessageId::getAddress));
for (Address address : addressMap.keySet()) {
// Loki - This also prevents read receipts from being sent in group chats as they don't maintain a friend request status
LokiThreadDatabase lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context);
long threadID = lokiThreadDatabase.getThreadID(address.serialize());
LokiThreadFriendRequestStatus friendRequestStatus = lokiThreadDatabase.getFriendRequestStatus(threadID);
if (friendRequestStatus != LokiThreadFriendRequestStatus.FRIENDS) { return; }
LokiStorageAPI storageAPI = LokiStorageAPI.Companion.getShared();
List<Long> timestamps = Stream.of(addressMap.get(address)).map(SyncMessageId::getTimetamp).toList();
ApplicationContext.getInstance(context)
.getJobManager()
.add(new SendReadReceiptJob(address, timestamps));
MultiDeviceUtilitiesKt.getAllDevicePublicKeys(context, address.serialize(), storageAPI, (devicePublicKey, isFriend, friendCount) -> {
// Loki - This also prevents read receipts from being sent in group chats as they don't maintain a friend request status
if (isFriend) {
ApplicationContext.getInstance(context).getJobManager().add(new SendReadReceiptJob(Address.fromSerialized(devicePublicKey), timestamps));
}
return Unit.INSTANCE;
});
}
}

View File

@ -42,6 +42,8 @@ import org.thoughtcrime.securesms.jobs.SmsSendJob;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.loki.GeneralUtilitiesKt;
import org.thoughtcrime.securesms.loki.MultiDeviceUtilitiesKt;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.push.AccountManagerFactory;
@ -52,10 +54,13 @@ import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
import org.whispersystems.signalservice.loki.messaging.LokiMessageFriendRequestStatus;
import java.io.IOException;
import kotlin.Unit;
public class MessageSender {
private static final String TAG = MessageSender.class.getSimpleName();
@ -203,13 +208,64 @@ public class MessageSender {
}
private static void sendTextPush(Context context, Recipient recipient, long messageId) {
LokiStorageAPI storageAPI = LokiStorageAPI.Companion.getShared();
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
jobManager.add(new PushTextSendJob(messageId, recipient.getAddress()));
// Just send the message normally if it's a group message
String recipientPublicKey = recipient.getAddress().serialize();
if (GeneralUtilitiesKt.isGroupRecipient(recipientPublicKey)) {
jobManager.add(new PushTextSendJob(messageId, recipient.getAddress()));
return;
}
MultiDeviceUtilitiesKt.getAllDevicePublicKeys(context, recipientPublicKey, storageAPI, (devicePublicKey, isFriend, friendCount) -> {
Address address = Address.fromSerialized(devicePublicKey);
long messageIDToUse = recipientPublicKey.equals(devicePublicKey) ? messageId : -1L;
if (isFriend) {
// Send a normal message if the user is friends with the recipient
jobManager.add(new PushTextSendJob(messageId, messageIDToUse, address));
} else {
// Send friend requests to non friends. If the user is friends with any
// of the devices then send out a default friend request message.
boolean isFriendsWithAny = (friendCount > 0);
String defaultFriendRequestMessage = isFriendsWithAny ? "Accept this friend request to enable messages to be synced across devices" : null;
jobManager.add(new PushTextSendJob(messageId, messageIDToUse, address, true, defaultFriendRequestMessage));
}
return Unit.INSTANCE;
});
}
private static void sendMediaPush(Context context, Recipient recipient, long messageId) {
LokiStorageAPI storageAPI = LokiStorageAPI.Companion.getShared();
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
PushMediaSendJob.enqueue(context, jobManager, messageId, recipient.getAddress());
// Just send the message normally if it's a group message
String recipientPublicKey = recipient.getAddress().serialize();
if (GeneralUtilitiesKt.isGroupRecipient(recipientPublicKey)) {
PushMediaSendJob.enqueue(context, jobManager, messageId, recipient.getAddress());
return;
}
MultiDeviceUtilitiesKt.getAllDevicePublicKeys(context, recipientPublicKey, storageAPI, (devicePublicKey, isFriend, friendCount) -> {
Address address = Address.fromSerialized(devicePublicKey);
long messageIDToUse = recipientPublicKey.equals(devicePublicKey) ? messageId : -1L;
if (isFriend) {
// Send a normal message if the user is friends with the recipient
PushMediaSendJob.enqueue(context, jobManager, messageId, messageIDToUse, address);
} else {
// Send friend requests to non friends. If the user is friends with any
// of the devices then send out a default friend request message.
boolean isFriendsWithAny = friendCount > 0;
String defaultFriendRequestMessage = isFriendsWithAny ? "Accept this friend request to enable messages to be synced across devices" : null;
PushMediaSendJob.enqueue(context, jobManager, messageId, messageIDToUse, address, true, defaultFriendRequestMessage);
}
return Unit.INSTANCE;
});
}
private static void sendGroupPush(Context context, Recipient recipient, long messageId, Address filterAddress) {

View File

@ -546,6 +546,10 @@ public class TextSecurePreferences {
setIntegerPrefrence(context, LOCAL_REGISTRATION_ID_PREF, registrationId);
}
public static void removeLocalRegistrationId(Context context) {
removePreference(context, LOCAL_REGISTRATION_ID_PREF);
}
public static boolean isInThreadNotifications(Context context) {
return getBooleanPreference(context, IN_THREAD_NOTIFICATION_PREF, true);
}
@ -639,6 +643,10 @@ public class TextSecurePreferences {
setStringPreference(context, LOCAL_NUMBER_PREF, localNumber);
}
public static void removeLocalNumber(Context context) {
removePreference(context, LOCAL_NUMBER_PREF);
}
public static String getPushServerPassword(Context context) {
return getStringPreference(context, GCM_PASSWORD_PREF, null);
}
@ -1169,5 +1177,13 @@ public class TextSecurePreferences {
public static void markChatSetUp(Context context, String id) {
setBooleanPreference(context, "is_chat_set_up" + "?chat=" + id, true);
}
public static String getMasterHexEncodedPublicKey(Context context) {
return getStringPreference(context, "master_hex_encoded_public_key", null);
}
public static void setMasterHexEncodedPublicKey(Context context, String masterHexEncodedPublicKey) {
setStringPreference(context, "master_hex_encoded_publicKey", masterHexEncodedPublicKey);
}
// endregion
}