Refactor multi device

This commit is contained in:
Niels Andriesse 2020-02-13 14:39:29 +11:00
parent 9c71a4c3cd
commit 07b1ffa77e
7 changed files with 86 additions and 95 deletions

View File

@ -184,9 +184,12 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
setUpP2PAPI();
// Loki - Update device mappings
if (setUpStorageAPIIfNeeded()) {
LokiFileServerAPI.Companion.getShared().updateUserDeviceLinks();
if (TextSecurePreferences.needsRevocationCheck(this)) {
checkNeedsRevocation();
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(this);
if (userHexEncodedPublicKey != null) {
LokiFileServerAPI.Companion.getShared().getDeviceLinks(userHexEncodedPublicKey, true);
if (TextSecurePreferences.getNeedsIsRevokedSlaveDeviceCheck(this)) {
MultiDeviceUtilities.checkIsRevokedSlaveDevice(this);
}
}
}
// Loki - Set up public chat manager
@ -613,11 +616,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
}
});
}
// endregion
public void checkNeedsRevocation() {
MultiDeviceUtilities.checkForRevocation(this);
}
public void checkNeedsDatabaseReset() {
if (TextSecurePreferences.resetDatabase(this)) {
@ -643,4 +641,5 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
this.startActivity(mainIntent);
Runtime.getRuntime().exit(0);
}
// endregion
}

View File

@ -133,7 +133,6 @@ import org.whispersystems.signalservice.loki.api.DeviceLink;
import org.whispersystems.signalservice.loki.api.DeviceLinkingSession;
import org.whispersystems.signalservice.loki.api.LokiAPI;
import org.whispersystems.signalservice.loki.api.LokiDeviceLinkUtilities;
import org.whispersystems.signalservice.loki.api.LokiFileServerAPI;
import org.whispersystems.signalservice.loki.crypto.LokiServiceCipher;
import org.whispersystems.signalservice.loki.messaging.LokiMessageFriendRequestStatus;
import org.whispersystems.signalservice.loki.messaging.LokiServiceMessage;
@ -336,7 +335,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
String ourMasterDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context);
if (ourMasterDevice != null && ourMasterDevice.equals(content.getSender())) {
TextSecurePreferences.setDatabaseResetFromUnpair(context, true);
MultiDeviceUtilities.checkForRevocation(context);
MultiDeviceUtilities.checkIsRevokedSlaveDevice(context);
}
} else {
// Loki - Don't process session restore message any further
@ -1170,8 +1169,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
TextSecurePreferences.setMultiDevice(context, true);
// Send a background message to the master device
MessageSender.sendBackgroundMessage(context, deviceLink.getMasterHexEncodedPublicKey());
// Propagate the updates to the file server
LokiFileServerAPI.Companion.getShared().updateUserDeviceLinks();
// Update display name if needed
if (content.senderDisplayName.isPresent() && content.senderDisplayName.get().length() > 0) {
TextSecurePreferences.setProfileName(context, content.senderDisplayName.get());
@ -1184,6 +1181,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
if (content.getSyncMessage().isPresent() && content.getSyncMessage().get().getContacts().isPresent()) {
handleContactSyncMessage(content.getSyncMessage().get().getContacts().get());
}
// The device link is propagated to the file server in LandingActivity.onDeviceLinkAuthorized because we can handle the error there
}
private void setDisplayName(String hexEncodedPublicKey, String profileName) {
@ -1775,7 +1773,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
*/
private Recipient getMasterRecipient(String hexEncodedPublicKey) {
try {
String masterHexEncodedPublicKey = PromiseUtil.timeout(LokiDeviceLinkUtilities.INSTANCE.getMasterHexEncodedPublicKey(hexEncodedPublicKey), 5000).get();
String masterHexEncodedPublicKey = LokiDeviceLinkUtilities.INSTANCE.getMasterHexEncodedPublicKey(hexEncodedPublicKey).get();
String targetHexEncodedPublicKey = (masterHexEncodedPublicKey != null) ? masterHexEncodedPublicKey : hexEncodedPublicKey;
// If the public key matches our master device then we need to forward the message to ourselves (note to self)
String ourMasterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context);

View File

@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.loki
import android.content.Context
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.all
import nl.komponents.kovenant.functional.bind
import nl.komponents.kovenant.functional.map
import nl.komponents.kovenant.toFailVoid
@ -16,7 +15,6 @@ 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.sms.MessageSender
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
import org.whispersystems.signalservice.api.push.SignalServiceAddress
@ -26,25 +24,21 @@ import org.whispersystems.signalservice.loki.api.LokiFileServerAPI
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus
import org.whispersystems.signalservice.loki.utilities.recover
import org.whispersystems.signalservice.loki.utilities.retryIfNeeded
import java.util.*
import kotlin.concurrent.schedule
fun checkForRevocation(context: Context) {
val primaryDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context) ?: return
val ourDevice = TextSecurePreferences.getLocalNumber(context)
LokiFileServerAPI.shared.getDeviceLinks(primaryDevice, true).bind { mappings ->
val ourMapping = mappings.find { it.slaveHexEncodedPublicKey == ourDevice }
if (ourMapping != null) throw Error("Device has not been revoked")
// remove pairing authorisations for our device
DatabaseFactory.getLokiAPIDatabase(context).clearDeviceLinks(ourDevice)
LokiFileServerAPI.shared.updateUserDeviceLinks()
fun checkIsRevokedSlaveDevice(context: Context) {
val masterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context) ?: return
val hexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
LokiFileServerAPI.shared.getDeviceLinks(masterHexEncodedPublicKey, true).bind { deviceLinks ->
val deviceLink = deviceLinks.find { it.masterHexEncodedPublicKey == masterHexEncodedPublicKey && it.slaveHexEncodedPublicKey == hexEncodedPublicKey }
if (deviceLink != null) throw Error("Device hasn't been revoked.")
DatabaseFactory.getLokiAPIDatabase(context).clearDeviceLinks(hexEncodedPublicKey)
LokiFileServerAPI.shared.setDeviceLinks(setOf())
}.successUi {
TextSecurePreferences.setNeedsRevocationCheck(context, false)
TextSecurePreferences.setNeedsIsRevokedSlaveDeviceCheck(context, false)
ApplicationContext.getInstance(context).clearData()
}.fail { error ->
TextSecurePreferences.setNeedsRevocationCheck(context, true)
Log.d("Loki", "Revocation check failed: ${error.message ?: error}")
TextSecurePreferences.setNeedsIsRevokedSlaveDeviceCheck(context, true)
Log.d("Loki", "Revocation check failed due to error: ${error.message ?: error}.")
}
}
@ -109,66 +103,44 @@ fun shouldAutomaticallyBecomeFriendsWithDevice(publicKey: String, context: Conte
}
}
fun sendPairingAuthorisationMessage(context: Context, contactHexEncodedPublicKey: String, authorisation: DeviceLink): Promise<Unit, Exception> {
fun sendDeviceLinkMessage(context: Context, hexEncodedPublicKey: String, deviceLink: DeviceLink): Promise<Unit, Exception> {
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
val address = SignalServiceAddress(contactHexEncodedPublicKey)
val message = SignalServiceDataMessage.newBuilder().withDeviceLink(authorisation)
// A REQUEST should always act as a friend request. A GRANT should always be replying back as a normal message.
if (authorisation.type == DeviceLink.Type.REQUEST) {
val address = SignalServiceAddress(hexEncodedPublicKey)
val message = SignalServiceDataMessage.newBuilder().withDeviceLink(deviceLink)
// A REQUEST should always act as a friend request. An AUTHORIZATION should always be a normal message.
if (deviceLink.type == DeviceLink.Type.REQUEST) {
val preKeyBundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(address.number)
message.asFriendRequest(true).withPreKeyBundle(preKeyBundle)
} else {
// Send over our profile key so that our linked device can get our profile picture
message.withProfileKey(ProfileKeyUtil.getProfileKey(context))
}
return try {
Log.d("Loki", "Sending authorisation message to: $contactHexEncodedPublicKey.")
val udAccess = UnidentifiedAccessUtil.getAccessFor(context, Recipient.from(context, Address.fromSerialized(contactHexEncodedPublicKey), false))
Log.d("Loki", "Sending device link message to: $hexEncodedPublicKey.")
val udAccess = UnidentifiedAccessUtil.getAccessFor(context, Recipient.from(context, Address.fromSerialized(hexEncodedPublicKey), false))
val result = messageSender.sendMessage(0, address, udAccess, 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."
result.isNetworkFailure -> "Failed to send device link message due to a network error."
else -> "Failed to send device link message."
}
throw Exception(exception)
}
Promise.ofSuccess(Unit)
} catch (e: Exception) {
Log.d("Loki", "Failed to send authorisation message to: $contactHexEncodedPublicKey.")
Log.d("Loki", "Failed to send device link message to: $hexEncodedPublicKey.")
Promise.ofFail(e)
}
}
fun signAndSendPairingAuthorisationMessage(context: Context, pairingAuthorisation: DeviceLink) {
fun signAndSendDeviceLinkMessage(context: Context, deviceLink: DeviceLink): Promise<Unit, Exception> {
val userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(context).privateKey.serialize()
val signedPairingAuthorisation = pairingAuthorisation.sign(DeviceLink.Type.AUTHORIZATION, userPrivateKey)
if (signedPairingAuthorisation == null || signedPairingAuthorisation.type != DeviceLink.Type.AUTHORIZATION) {
Log.d("Loki", "Failed to sign pairing authorization.")
return
val signedDeviceLink = deviceLink.sign(DeviceLink.Type.AUTHORIZATION, userPrivateKey)
if (signedDeviceLink == null || signedDeviceLink.type != DeviceLink.Type.AUTHORIZATION) {
return Promise.ofFail(Exception("Failed to sign device link."))
}
DatabaseFactory.getLokiAPIDatabase(context).addDeviceLink(signedPairingAuthorisation)
TextSecurePreferences.setMultiDevice(context, true)
val address = Address.fromSerialized(pairingAuthorisation.slaveHexEncodedPublicKey)
val sendPromise = retryIfNeeded(8) {
sendPairingAuthorisationMessage(context, address.serialize(), signedPairingAuthorisation)
}.fail {
Log.d("Loki", "Failed to send pairing authorization message to ${address.serialize()}.")
}
val updatePromise = LokiFileServerAPI.shared.updateUserDeviceLinks().fail {
Log.d("Loki", "Failed to update device mapping")
}
// If both promises complete successfully then we should sync our contacts
all(listOf(sendPromise, updatePromise), cancelOthersOnError = false).success {
Log.d("Loki", "Successfully pairing with a secondary device! Syncing contacts.")
// Send out sync contact after a delay
Timer().schedule(3000) {
MessageSender.syncAllContacts(context, address)
}
return retryIfNeeded(8) {
sendDeviceLinkMessage(context, deviceLink.slaveHexEncodedPublicKey, signedDeviceLink)
}
}

View File

@ -18,7 +18,7 @@ import org.thoughtcrime.securesms.loki.redesign.dialogs.LinkDeviceSlaveModeDialo
import org.thoughtcrime.securesms.loki.redesign.utilities.push
import org.thoughtcrime.securesms.loki.redesign.utilities.setUpActionBarSessionLogo
import org.thoughtcrime.securesms.loki.redesign.utilities.show
import org.thoughtcrime.securesms.loki.sendPairingAuthorisationMessage
import org.thoughtcrime.securesms.loki.sendDeviceLinkMessage
import org.thoughtcrime.securesms.util.Base64
import org.thoughtcrime.securesms.util.Hex
import org.thoughtcrime.securesms.util.TextSecurePreferences
@ -27,6 +27,7 @@ import org.whispersystems.libsignal.ecc.Curve
import org.whispersystems.libsignal.ecc.ECKeyPair
import org.whispersystems.libsignal.util.KeyHelper
import org.whispersystems.signalservice.loki.api.DeviceLink
import org.whispersystems.signalservice.loki.api.LokiFileServerAPI
import org.whispersystems.signalservice.loki.utilities.hexEncodedPublicKey
import org.whispersystems.signalservice.loki.utilities.retryIfNeeded
@ -92,11 +93,11 @@ class LandingActivity : BaseActionBarActivity(), LinkDeviceSlaveModeDialogDelega
TextSecurePreferences.setLocalNumber(this, userHexEncodedPublicKey)
TextSecurePreferences.setHasSeenWelcomeScreen(this, true)
TextSecurePreferences.setPromptedPushRegistration(this, true)
val authorisation = DeviceLink(hexEncodedPublicKey, userHexEncodedPublicKey).sign(DeviceLink.Type.REQUEST, keyPair!!.privateKey.serialize())
if (authorisation == null) {
val deviceLink = DeviceLink(hexEncodedPublicKey, userHexEncodedPublicKey).sign(DeviceLink.Type.REQUEST, keyPair!!.privateKey.serialize())
if (deviceLink == null) {
Log.d("Loki", "Failed to sign device link request.")
reset()
return Toast.makeText(application, "Couldn't link device.", Toast.LENGTH_SHORT).show()
return Toast.makeText(application, "Couldn't link device.", Toast.LENGTH_LONG).show()
}
val application = ApplicationContext.getInstance(this)
application.startLongPollingIfNeeded()
@ -107,12 +108,13 @@ class LandingActivity : BaseActionBarActivity(), LinkDeviceSlaveModeDialogDelega
linkDeviceDialog.show(supportFragmentManager, "Link Device Dialog")
AsyncTask.execute {
retryIfNeeded(8) {
sendPairingAuthorisationMessage(this@LandingActivity, authorisation.masterHexEncodedPublicKey, authorisation)
sendDeviceLinkMessage(this@LandingActivity, deviceLink.masterHexEncodedPublicKey, deviceLink)
}
}
}
override fun onDeviceLinkRequestAuthorized(deviceLink: DeviceLink) {
LokiFileServerAPI.shared.addDeviceLink(deviceLink)
TextSecurePreferences.setMasterHexEncodedPublicKey(this, deviceLink.masterHexEncodedPublicKey)
val intent = Intent(this, HomeActivity::class.java)
show(intent)

View File

@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.loki.redesign.activities
import android.os.AsyncTask
import android.os.Bundle
import android.support.v4.app.LoaderManager
import android.support.v4.content.Loader
@ -12,16 +11,20 @@ import android.view.View
import android.widget.Toast
import kotlinx.android.synthetic.main.activity_linked_devices.*
import network.loki.messenger.R
import nl.komponents.kovenant.ui.failUi
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.devicelist.Device
import org.thoughtcrime.securesms.loki.redesign.dialogs.*
import org.thoughtcrime.securesms.loki.signAndSendPairingAuthorisationMessage
import org.thoughtcrime.securesms.loki.signAndSendDeviceLinkMessage
import org.thoughtcrime.securesms.sms.MessageSender
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.loki.api.DeviceLink
import org.whispersystems.signalservice.loki.api.LokiFileServerAPI
import java.util.*
import kotlin.concurrent.schedule
class LinkedDevicesActivity : PassphraseRequiredActionBarActivity, LoaderManager.LoaderCallbacks<List<Device>>, DeviceClickListener, EditDeviceNameDialogDelegate, LinkDeviceMasterModeDialogDelegate {
private var devices = listOf<Device>()
@ -118,20 +121,37 @@ class LinkedDevicesActivity : PassphraseRequiredActionBarActivity, LoaderManager
private fun unlinkDevice(slaveDeviceHexEncodedPublicKey: String) {
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(this)
val database = DatabaseFactory.getLokiAPIDatabase(this)
database.removeDeviceLink(userHexEncodedPublicKey, slaveDeviceHexEncodedPublicKey)
LokiFileServerAPI.shared.updateUserDeviceLinks().success {
MessageSender.sendUnpairRequest(this, slaveDeviceHexEncodedPublicKey)
val deviceLink = database.getDeviceLinks(userHexEncodedPublicKey).find { it.masterHexEncodedPublicKey == userHexEncodedPublicKey && it.slaveHexEncodedPublicKey == slaveDeviceHexEncodedPublicKey }
if (deviceLink == null) {
return Toast.makeText(this, "Couldn't unlink device.", Toast.LENGTH_LONG).show()
}
LokiFileServerAPI.shared.removeDeviceLink(deviceLink).success {
MessageSender.sendUnpairRequest(this, slaveDeviceHexEncodedPublicKey)
LoaderManager.getInstance(this).restartLoader(0, null, this)
Toast.makeText(this, "Your device was unlinked successfully", Toast.LENGTH_LONG).show()
}.fail {
Toast.makeText(this, "Couldn't unlink device.", Toast.LENGTH_LONG).show()
}
LoaderManager.getInstance(this).restartLoader(0, null, this)
Toast.makeText(this, "Your device was unlinked successfully", Toast.LENGTH_LONG).show()
}
override fun onDeviceLinkRequestAuthorized(authorization: DeviceLink) {
AsyncTask.execute {
signAndSendPairingAuthorisationMessage(this, authorization)
Util.runOnMain {
LoaderManager.getInstance(this).restartLoader(0, null, this)
override fun onDeviceLinkRequestAuthorized(deviceLink: DeviceLink) {
LokiFileServerAPI.shared.addDeviceLink(deviceLink).success {
signAndSendDeviceLinkMessage(this, deviceLink).success {
TextSecurePreferences.setMultiDevice(this, true)
Util.runOnMain {
LoaderManager.getInstance(this).restartLoader(0, null, this)
}
Timer().schedule(4000) {
MessageSender.syncAllContacts(this@LinkedDevicesActivity, Address.fromSerialized(deviceLink.slaveHexEncodedPublicKey))
}
}.fail {
LokiFileServerAPI.shared.removeDeviceLink(deviceLink) // If this fails we have a problem
Util.runOnMain {
Toast.makeText(this, "Couldn't link device", Toast.LENGTH_LONG).show()
}
}
}.failUi {
Toast.makeText(this, "Couldn't link device", Toast.LENGTH_LONG).show()
}
}

View File

@ -190,6 +190,11 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
}.toSet()
}
override fun clearDeviceLinks(hexEncodedPublicKey: String) {
val database = databaseHelper.writableDatabase
database.delete(deviceLinkCache, "$masterHexEncodedPublicKey = ? OR $slaveHexEncodedPublicKey = ?", arrayOf( hexEncodedPublicKey, hexEncodedPublicKey ))
}
override fun addDeviceLink(deviceLink: DeviceLink) {
val database = databaseHelper.writableDatabase
val values = ContentValues()
@ -200,14 +205,9 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
database.insertOrUpdate(deviceLinkCache, values, "$masterHexEncodedPublicKey = ? AND $slaveHexEncodedPublicKey = ?", arrayOf( deviceLink.masterHexEncodedPublicKey, deviceLink.slaveHexEncodedPublicKey ))
}
override fun clearDeviceLinks(hexEncodedPublicKey: String) {
override fun removeDeviceLink(deviceLink: DeviceLink) {
val database = databaseHelper.writableDatabase
database.delete(deviceLinkCache, "$masterHexEncodedPublicKey = ? OR $slaveHexEncodedPublicKey = ?", arrayOf( hexEncodedPublicKey, hexEncodedPublicKey ))
}
fun removeDeviceLink(masterHexEncodedPublicKey: String, slaveHexEncodedPublicKey: String) {
val database = databaseHelper.writableDatabase
database.delete(deviceLinkCache, "${Companion.masterHexEncodedPublicKey} = ? OR ${Companion.slaveHexEncodedPublicKey} = ?", arrayOf( masterHexEncodedPublicKey, slaveHexEncodedPublicKey ))
database.delete(deviceLinkCache, "$masterHexEncodedPublicKey = ? OR $slaveHexEncodedPublicKey = ?", arrayOf( deviceLink.masterHexEncodedPublicKey, deviceLink.slaveHexEncodedPublicKey ))
}
fun getUserCount(group: Long, server: String): Int? {

View File

@ -1235,11 +1235,11 @@ public class TextSecurePreferences {
return getBooleanPreference(context, "database_reset_unpair", false);
}
public static void setNeedsRevocationCheck(Context context, boolean needsCheck) {
setBooleanPreference(context, "needs_revocation", needsCheck);
public static void setNeedsIsRevokedSlaveDeviceCheck(Context context, boolean value) {
setBooleanPreference(context, "needs_revocation", value);
}
public static boolean needsRevocationCheck(Context context) {
public static boolean getNeedsIsRevokedSlaveDeviceCheck(Context context) {
return getBooleanPreference(context, "needs_revocation", false);
}