Refactor PushDecryptJob

This commit is contained in:
nielsandriesse
2020-05-13 12:29:31 +10:00
parent afe049c9c3
commit 6205b6c9f6
20 changed files with 462 additions and 693 deletions

View File

@@ -1,93 +0,0 @@
package org.thoughtcrime.securesms.loki
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil
import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.dependencies.InjectableType
import org.thoughtcrime.securesms.jobmanager.Data
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
import org.thoughtcrime.securesms.jobs.BaseJob
import org.thoughtcrime.securesms.recipients.Recipient
import org.whispersystems.signalservice.api.SignalServiceMessageSender
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import java.io.IOException
import java.util.concurrent.TimeUnit
import javax.inject.Inject
class PushMessageSyncSendJob private constructor(
parameters: Parameters,
private val messageID: Long,
private val recipient: Address,
private val timestamp: Long,
private val message: ByteArray,
private val ttl: Int
) : BaseJob(parameters), InjectableType {
companion object {
const val KEY = "PushMessageSyncSendJob"
private val TAG = PushMessageSyncSendJob::class.java.simpleName
private val KEY_MESSAGE_ID = "message_id"
private val KEY_RECIPIENT = "recipient"
private val KEY_TIMESTAMP = "timestamp"
private val KEY_MESSAGE = "message"
private val KEY_TTL = "ttl"
}
@Inject
lateinit var messageSender: SignalServiceMessageSender
constructor(messageID: Long, recipient: Address, timestamp: Long, message: ByteArray, ttl: Int) : this(Parameters.Builder()
.addConstraint(NetworkConstraint.KEY)
.setQueue(KEY)
.setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(1)
.build(),
messageID, recipient, timestamp, message, ttl)
override fun serialize(): Data {
return Data.Builder()
.putLong(KEY_MESSAGE_ID, messageID)
.putString(KEY_RECIPIENT, recipient.serialize())
.putLong(KEY_TIMESTAMP, timestamp)
.putByteArray(KEY_MESSAGE, message)
.putInt(KEY_TTL, ttl)
.build()
}
override fun getFactoryKey(): String {
return KEY
}
@Throws(IOException::class, UntrustedIdentityException::class)
public override fun onRun() {
// Don't send sync messages to a group
if (recipient.isGroup || recipient.isEmail) { return }
val unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, Recipient.from(context, recipient, false))
messageSender.lokiSendSyncMessage(messageID, SignalServiceAddress(recipient.toPhoneString()), unidentifiedAccess, timestamp, message, ttl)
}
public override fun onShouldRetry(e: Exception): Boolean {
// Loki - Disable since we have our own retrying when sending messages
return false
}
override fun onCanceled() {}
class Factory : Job.Factory<PushMessageSyncSendJob> {
override fun create(parameters: Parameters, data: Data): PushMessageSyncSendJob {
try {
return PushMessageSyncSendJob(parameters,
data.getLong(KEY_MESSAGE_ID),
Address.fromSerialized(data.getString(KEY_RECIPIENT)),
data.getLong(KEY_TIMESTAMP),
data.getByteArray(KEY_MESSAGE),
data.getInt(KEY_TTL))
} catch (e: IOException) {
throw AssertionError(e)
}
}
}
}

View File

@@ -40,7 +40,7 @@ class LandingActivity : BaseActionBarActivity(), LinkDeviceSlaveModeDialogDelega
registerButton.setOnClickListener { register() }
restoreButton.setOnClickListener { restore() }
linkButton.setOnClickListener { linkDevice() }
if (TextSecurePreferences.setNeedsDatabaseResetFromUnlink(this)) {
if (TextSecurePreferences.getWasUnlinked(this)) {
Toast.makeText(this, "Your device was unlinked successfully", Toast.LENGTH_LONG).show()
}
}

View File

@@ -5,11 +5,13 @@ import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore
import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.loki.utilities.recipient
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.sms.MessageSender
import org.thoughtcrime.securesms.util.GroupUtil
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.libsignal.SignalProtocolAddress
import org.whispersystems.signalservice.api.messages.SignalServiceContent
import org.whispersystems.signalservice.api.messages.SignalServiceGroup
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.loki.protocol.multidevice.MultiDeviceProtocol
@@ -17,10 +19,20 @@ import org.whispersystems.signalservice.loki.protocol.multidevice.MultiDevicePro
object ClosedGroupsProtocol {
@JvmStatic
fun shouldIgnoreMessage(context: Context, group: SignalServiceGroup): Boolean {
fun shouldIgnoreContentMessage(context: Context, conversation: Recipient, groupID: String?, content: SignalServiceContent): Boolean {
if (!conversation.address.isClosedGroup || groupID == null) { return false }
val senderPublicKey = content.sender
val senderMasterPublicKey = MultiDeviceProtocol.shared.getMasterDevice(senderPublicKey)
val publicKeyToCheckFor = senderMasterPublicKey ?: senderPublicKey
val members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupID, true)
return !members.contains(recipient(context, publicKeyToCheckFor))
}
@JvmStatic
fun shouldIgnoreGroupCreatedMessage(context: Context, group: SignalServiceGroup): Boolean {
val members = group.members
val masterDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context)
return !members.isPresent || !members.get().contains(masterDevice)
val userMasterDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context)
return !members.isPresent || !members.get().contains(userMasterDevice)
}
@JvmStatic
@@ -31,19 +43,20 @@ object ClosedGroupsProtocol {
result.add(Address.fromSerialized(groupID))
return result
} else {
// A closed group's members should never include slave devices
val members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupID, false)
val recipients = members.flatMap { member ->
val destinations = members.flatMap { member ->
MultiDeviceProtocol.shared.getAllLinkedDevices(member.address.serialize()).map { Address.fromSerialized(it) }
}.toMutableSet()
val masterPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context)
if (masterPublicKey != null && recipients.contains(Address.fromSerialized(masterPublicKey))) {
recipients.remove(Address.fromSerialized(masterPublicKey))
val userMasterPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context)
if (userMasterPublicKey != null && destinations.contains(Address.fromSerialized(userMasterPublicKey))) {
destinations.remove(Address.fromSerialized(userMasterPublicKey))
}
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
if (userPublicKey != null && recipients.contains(Address.fromSerialized(userPublicKey))) {
recipients.remove(Address.fromSerialized(userPublicKey))
if (userPublicKey != null && destinations.contains(Address.fromSerialized(userPublicKey))) {
destinations.remove(Address.fromSerialized(userPublicKey))
}
return recipients.toList()
return destinations.toList()
}
}
@@ -54,39 +67,36 @@ object ClosedGroupsProtocol {
val message = GroupUtil.createGroupLeaveMessage(context, recipient)
if (threadID < 0 || !message.isPresent) { return false }
MessageSender.send(context, message.get(), threadID, false, null)
// Remove the *master* device from the group
// Remove the master device from the group (a closed group's members should never include slave devices)
val masterPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context)
val publicKeyToUse = masterPublicKey ?: TextSecurePreferences.getLocalNumber(context)
val publicKeyToRemove = masterPublicKey ?: TextSecurePreferences.getLocalNumber(context)
val groupDatabase = DatabaseFactory.getGroupDatabase(context)
val groupID = recipient.address.toGroupString()
groupDatabase.setActive(groupID, false)
groupDatabase.remove(groupID, Address.fromSerialized(publicKeyToUse))
groupDatabase.remove(groupID, Address.fromSerialized(publicKeyToRemove))
return true
}
@JvmStatic
fun establishSessionsWithMembersIfNeeded(context: Context, members: List<String>) {
// A closed group's members should never include slave devices
val allDevices = members.flatMap { member ->
MultiDeviceProtocol.shared.getAllLinkedDevices(member)
}.toMutableSet()
val masterPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context)
if (masterPublicKey != null && allDevices.contains(masterPublicKey)) {
allDevices.remove(masterPublicKey)
val userMasterPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context)
if (userMasterPublicKey != null && allDevices.contains(userMasterPublicKey)) {
allDevices.remove(userMasterPublicKey)
}
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
if (userPublicKey != null && allDevices.contains(userPublicKey)) {
allDevices.remove(userPublicKey)
}
for (device in allDevices) {
val address = SignalProtocolAddress(device, SignalServiceAddress.DEFAULT_DEVICE_ID)
val hasSession = TextSecureSessionStore(context).containsSession(address)
if (!hasSession) { sendSessionRequest(context, device) }
val deviceAsAddress = SignalProtocolAddress(device, SignalServiceAddress.DEFAULT_DEVICE_ID)
val hasSession = TextSecureSessionStore(context).containsSession(deviceAsAddress)
if (hasSession) { continue }
val sessionRequest = EphemeralMessage.createSessionRequest(device)
ApplicationContext.getInstance(context).jobManager.add(PushEphemeralMessageSendJob(sessionRequest))
}
}
@JvmStatic
fun sendSessionRequest(context: Context, publicKey: String) {
val sessionRequest = EphemeralMessage.createSessionRequest(publicKey)
ApplicationContext.getInstance(context).jobManager.add(PushEphemeralMessageSendJob(sessionRequest))
}
}

View File

@@ -16,7 +16,7 @@ class EphemeralMessage private constructor(val data: Map<*, *>) {
fun createSessionRestorationRequest(publicKey: String) = EphemeralMessage(mapOf( "recipient" to publicKey, "friendRequest" to true, "sessionRestore" to true ))
@JvmStatic
fun createSessionRequest(publicKey: String) = EphemeralMessage(mapOf("recipient" to publicKey, "friendRequest" to true, "sessionRequest" to true))
fun createSessionRequest(publicKey: String) = EphemeralMessage(mapOf( "recipient" to publicKey, "friendRequest" to true, "sessionRequest" to true ))
internal fun parse(serialized: String): EphemeralMessage {
val data = JsonUtil.fromJson(serialized, Map::class.java) ?: throw IllegalArgumentException("Couldn't parse string to JSON")

View File

@@ -1,15 +1,122 @@
package org.thoughtcrime.securesms.loki.protocol
import android.content.Context
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.loki.utilities.recipient
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage
import org.thoughtcrime.securesms.sms.OutgoingTextMessage
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.api.messages.SignalServiceContent
import org.whispersystems.signalservice.loki.protocol.multidevice.MultiDeviceProtocol
import org.whispersystems.signalservice.loki.protocol.todo.LokiMessageFriendRequestStatus
import org.whispersystems.signalservice.loki.protocol.todo.LokiThreadFriendRequestStatus
object FriendRequestProtocol {
private fun getLastMessageID(context: Context, threadID: Long): Long? {
val db = DatabaseFactory.getSmsDatabase(context)
val messageCount = db.getMessageCountForThread(threadID)
if (messageCount == 0) { return null }
return db.getIDForMessageAtIndex(threadID, messageCount - 1)
}
@JvmStatic
fun handleFriendRequestAcceptanceIfNeeded(context: Context, publicKey: String, content: SignalServiceContent) {
// If we get an envelope that isn't a friend request, then we can infer that we had to use
// Signal cipher decryption and thus that we have a session with the other person.
if (content.isFriendRequest) { return }
val recipient = recipient(context, publicKey)
// Friend requests don't apply to groups
if (recipient.isGroupRecipient) { return }
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient)
val lokiThreadDB = DatabaseFactory.getLokiThreadDatabase(context)
val threadFRStatus = lokiThreadDB.getFriendRequestStatus(threadID)
// Guard against invalid state transitions
if (threadFRStatus != LokiThreadFriendRequestStatus.REQUEST_SENDING && threadFRStatus != LokiThreadFriendRequestStatus.REQUEST_SENT
&& threadFRStatus != LokiThreadFriendRequestStatus.REQUEST_RECEIVED) { return }
lokiThreadDB.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.FRIENDS)
val lastMessageID = getLastMessageID(context, threadID)
if (lastMessageID != null) {
DatabaseFactory.getLokiMessageDatabase(context).setFriendRequestStatus(lastMessageID, LokiMessageFriendRequestStatus.REQUEST_ACCEPTED)
}
// Send a contact sync message if needed
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
val allUserDevices = MultiDeviceProtocol.shared.getAllLinkedDevices(userPublicKey)
if (allUserDevices.contains(publicKey)) { return }
val deviceToSync = MultiDeviceProtocol.shared.getMasterDevice(publicKey) ?: publicKey
SyncMessagesProtocol.syncContact(context, Address.fromSerialized(deviceToSync))
}
private fun canFriendRequestBeAutoAccepted(context: Context, publicKey: String): Boolean {
val recipient = recipient(context, publicKey)
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient)
val lokiThreadDB = DatabaseFactory.getLokiThreadDatabase(context)
val threadFRStatus = lokiThreadDB.getFriendRequestStatus(threadID)
if (threadFRStatus == 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 succeeds. 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 SENT.
return true
}
// Auto-accept any friend requests from the user's own linked devices
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
val allUserDevices = MultiDeviceProtocol.shared.getAllLinkedDevices(userPublicKey)
if (allUserDevices.contains(publicKey)) { return true }
// Auto-accept if the user is friends with any of the sender's linked devices.
val allSenderDevices = MultiDeviceProtocol.shared.getAllLinkedDevices(publicKey)
if (allSenderDevices.any { device ->
val deviceAsRecipient = recipient(context, publicKey)
val deviceThreadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(deviceAsRecipient)
lokiThreadDB.getFriendRequestStatus(deviceThreadID) == LokiThreadFriendRequestStatus.FRIENDS
}) {
return true
}
return false
}
@JvmStatic
fun handleFriendRequestMessageIfNeeded(context: Context, publicKey: String, content: SignalServiceContent) {
if (!content.isFriendRequest) { return }
val recipient = recipient(context, publicKey)
// Friend requests don't apply to groups
if (recipient.isGroupRecipient) { return }
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient)
val lokiThreadDB = DatabaseFactory.getLokiThreadDatabase(context)
val threadFRStatus = lokiThreadDB.getFriendRequestStatus(threadID)
if (canFriendRequestBeAutoAccepted(context, publicKey)) {
lokiThreadDB.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.FRIENDS)
val lastMessageID = getLastMessageID(context, threadID)
if (lastMessageID != null) {
DatabaseFactory.getLokiMessageDatabase(context).setFriendRequestStatus(lastMessageID, LokiMessageFriendRequestStatus.REQUEST_ACCEPTED)
}
val ephemeralMessage = EphemeralMessage.create(publicKey)
ApplicationContext.getInstance(context).jobManager.add(PushEphemeralMessageSendJob(ephemeralMessage))
} else if (threadFRStatus != 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 RECEIVED
lokiThreadDB.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.REQUEST_RECEIVED)
val lastMessageID = getLastMessageID(context, threadID)
if (lastMessageID != null) {
DatabaseFactory.getLokiMessageDatabase(context).setFriendRequestStatus(lastMessageID, LokiMessageFriendRequestStatus.REQUEST_PENDING)
}
}
}
@JvmStatic
fun isFriendRequestFromBeforeRestoration(context: Context, content: SignalServiceContent): Boolean {
return content.isFriendRequest && content.timestamp < TextSecurePreferences.getRestorationTime(context)
}
@JvmStatic
fun shouldUpdateFriendRequestStatusFromOutgoingTextMessage(context: Context, message: OutgoingTextMessage): Boolean {
// The order of these checks matters
@@ -43,4 +150,34 @@ object FriendRequestProtocol {
threadDB.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.REQUEST_SENDING)
}
}
@JvmStatic
fun setFriendRequestStatusToSentIfNeeded(context: Context, messageID: Long, threadID: Long) {
val messageDB = DatabaseFactory.getLokiMessageDatabase(context)
val messageFRStatus = messageDB.getFriendRequestStatus(messageID)
if (messageFRStatus == LokiMessageFriendRequestStatus.NONE || messageFRStatus == LokiMessageFriendRequestStatus.REQUEST_EXPIRED
|| messageFRStatus == LokiMessageFriendRequestStatus.REQUEST_SENDING) {
messageDB.setFriendRequestStatus(messageID, LokiMessageFriendRequestStatus.REQUEST_PENDING)
}
val threadDB = DatabaseFactory.getLokiThreadDatabase(context)
val threadFRStatus = threadDB.getFriendRequestStatus(threadID)
if (threadFRStatus == LokiThreadFriendRequestStatus.NONE || threadFRStatus == LokiThreadFriendRequestStatus.REQUEST_EXPIRED
|| threadFRStatus == LokiThreadFriendRequestStatus.REQUEST_SENDING) {
threadDB.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.REQUEST_SENT)
}
}
@JvmStatic
fun setFriendRequestStatusToFailedIfNeeded(context: Context, messageID: Long, threadID: Long) {
val messageDB = DatabaseFactory.getLokiMessageDatabase(context)
val messageFRStatus = messageDB.getFriendRequestStatus(messageID)
if (messageFRStatus == LokiMessageFriendRequestStatus.REQUEST_SENDING) {
messageDB.setFriendRequestStatus(messageID, LokiMessageFriendRequestStatus.REQUEST_FAILED)
}
val threadDB = DatabaseFactory.getLokiThreadDatabase(context)
val threadFRStatus = threadDB.getFriendRequestStatus(threadID)
if (threadFRStatus == LokiThreadFriendRequestStatus.REQUEST_SENDING) {
threadDB.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.NONE)
}
}
}

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.loki.protocol
import android.content.Context
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.whispersystems.libsignal.loki.LokiSessionResetProtocol
import org.whispersystems.libsignal.loki.LokiSessionResetStatus
@@ -18,7 +19,8 @@ class LokiSessionResetImplementation(private val context: Context) : LokiSession
override fun onNewSessionAdopted(hexEncodedPublicKey: String, oldSessionResetStatus: LokiSessionResetStatus) {
if (oldSessionResetStatus == LokiSessionResetStatus.IN_PROGRESS) {
SessionMetaProtocol.sendEphemeralMessage(context, hexEncodedPublicKey)
val ephemeralMessage = EphemeralMessage.create(hexEncodedPublicKey)
ApplicationContext.getInstance(context).jobManager.add(PushEphemeralMessageSendJob(ephemeralMessage))
}
// TODO: Show session reset succeed message
}

View File

@@ -19,7 +19,6 @@ import javax.inject.Inject
class MultiDeviceOpenGroupUpdateJob private constructor(parameters: Parameters) : BaseJob(parameters), InjectableType {
companion object {
const val KEY = "MultiDeviceOpenGroupUpdateJob"
}
@@ -35,7 +34,7 @@ class MultiDeviceOpenGroupUpdateJob private constructor(parameters: Parameters)
override fun getFactoryKey(): String { return KEY }
override fun serialize(): Data { return Data.EMPTY }
override fun serialize(): Data { return Data.EMPTY } // TODO: Should we implement this?
@Throws(Exception::class)
public override fun onRun() {

View File

@@ -1,26 +1,28 @@
package org.thoughtcrime.securesms.loki.protocol
import android.content.Context
import android.util.Log
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.jobs.PushMediaSendJob
import org.thoughtcrime.securesms.jobs.PushSendJob
import org.thoughtcrime.securesms.jobs.PushTextSendJob
import org.thoughtcrime.securesms.loki.utilities.Broadcaster
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.api.messages.SignalServiceContent
import org.whispersystems.signalservice.loki.api.fileserver.LokiFileServerAPI
import org.whispersystems.signalservice.loki.protocol.meta.SessionMetaProtocol
import org.whispersystems.signalservice.loki.protocol.multidevice.DeviceLink
import org.whispersystems.signalservice.loki.protocol.multidevice.DeviceLinkingSession
import org.whispersystems.signalservice.loki.protocol.multidevice.MultiDeviceProtocol
import org.whispersystems.signalservice.loki.protocol.todo.LokiMessageFriendRequestStatus
import org.whispersystems.signalservice.loki.protocol.todo.LokiThreadFriendRequestStatus
object MultiDeviceProtocol {
@JvmStatic
fun sendUnlinkingRequest(context: Context, publicKey: String) {
val unlinkingRequest = EphemeralMessage.createUnlinkingRequest(publicKey)
ApplicationContext.getInstance(context).jobManager.add(PushEphemeralMessageSendJob(unlinkingRequest))
}
// TODO: Closed groups
enum class MessageType { Text, Media }
@@ -34,7 +36,6 @@ object MultiDeviceProtocol {
sendMessagePush(context, recipient, messageID, MessageType.Media)
}
// TODO: Closed groups
private fun sendMessagePushToDevice(context: Context, recipient: Recipient, messageID: Long, messageType: MessageType): PushSendJob {
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient)
val threadFRStatus = DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadID)
@@ -92,4 +93,93 @@ object MultiDeviceProtocol {
}
}
}
@JvmStatic
fun handleDeviceLinkMessageIfNeeded(context: Context, deviceLink: DeviceLink, content: SignalServiceContent) {
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
if (deviceLink.type == DeviceLink.Type.REQUEST) {
handleDeviceLinkRequestMessage(context, deviceLink, content)
} else if (deviceLink.slaveHexEncodedPublicKey == userPublicKey) {
handleDeviceLinkAuthorizedMessage(context, deviceLink, content)
}
}
private fun isValidDeviceLinkMessage(context: Context, deviceLink: DeviceLink): Boolean {
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
val isRequest = (deviceLink.type == DeviceLink.Type.REQUEST)
if (deviceLink.requestSignature == null) {
Log.d("Loki", "Ignoring device link without a request signature.")
return false
} else if (isRequest && TextSecurePreferences.getMasterHexEncodedPublicKey(context) != null) {
Log.d("Loki", "Ignoring unexpected device link message (the device is a slave device).")
return false
} else if (isRequest && deviceLink.masterHexEncodedPublicKey != userPublicKey) {
Log.d("Loki", "Ignoring device linking message addressed to another user.")
return false
} else if (isRequest && deviceLink.slaveHexEncodedPublicKey == userPublicKey) {
Log.d("Loki", "Ignoring device linking request message from self.")
return false
}
return deviceLink.verify()
}
private fun handleDeviceLinkRequestMessage(context: Context, deviceLink: DeviceLink, content: SignalServiceContent) {
val linkingSession = DeviceLinkingSession.shared
if (!linkingSession.isListeningForLinkingRequests) {
return Broadcaster(context).broadcast("unexpectedDeviceLinkRequestReceived")
}
val isValid = isValidDeviceLinkMessage(context, deviceLink)
if (!isValid) { return }
SessionManagementProtocol.handlePreKeyBundleMessageIfNeeded(context, content)
linkingSession.processLinkingRequest(deviceLink)
}
private fun handleDeviceLinkAuthorizedMessage(context: Context, deviceLink: DeviceLink, content: SignalServiceContent) {
val linkingSession = DeviceLinkingSession.shared
if (!linkingSession.isListeningForLinkingRequests) {
return
}
val isValid = isValidDeviceLinkMessage(context, deviceLink)
if (!isValid) { return }
SessionManagementProtocol.handlePreKeyBundleMessageIfNeeded(context, content)
linkingSession.processLinkingAuthorization(deviceLink)
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
DatabaseFactory.getLokiAPIDatabase(context).clearDeviceLinks(userPublicKey)
DatabaseFactory.getLokiAPIDatabase(context).addDeviceLink(deviceLink)
TextSecurePreferences.setMasterHexEncodedPublicKey(context, deviceLink.masterHexEncodedPublicKey)
TextSecurePreferences.setMultiDevice(context, true)
LokiFileServerAPI.shared.addDeviceLink(deviceLink)
org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol.handleProfileUpdateIfNeeded(context, content)
org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol.handleProfileKeyUpdateIfNeeded(context, content)
}
@JvmStatic
fun handleUnlinkingRequestIfNeeded(context: Context, content: SignalServiceContent) {
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
// Check that the request was sent by the user's master device
val masterDevicePublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context) ?: return
val wasSentByMasterDevice = (content.sender == masterDevicePublicKey)
if (!wasSentByMasterDevice) { return }
// Ignore the request if we don't know about the device link in question
val masterDeviceLinks = DatabaseFactory.getLokiAPIDatabase(context).getDeviceLinks(masterDevicePublicKey)
if (masterDeviceLinks.none {
it.masterHexEncodedPublicKey == masterDevicePublicKey && it.slaveHexEncodedPublicKey == userPublicKey
}) {
return
}
LokiFileServerAPI.shared.getDeviceLinks(userPublicKey, true).success { slaveDeviceLinks ->
// Check that the device link IS present on the file server.
// Note that the device link as seen from the master device's perspective has been deleted at this point, but the
// device link as seen from the slave perspective hasn't.
if (slaveDeviceLinks.any {
it.masterHexEncodedPublicKey == masterDevicePublicKey && it.slaveHexEncodedPublicKey == userPublicKey
}) {
for (slaveDeviceLink in slaveDeviceLinks) { // In theory there should only be one
LokiFileServerAPI.shared.removeDeviceLink(slaveDeviceLink) // Attempt to clean up on the file server
}
TextSecurePreferences.setWasUnlinked(context, true)
ApplicationContext.getInstance(context).clearData()
}
}
}
}

View File

@@ -18,8 +18,7 @@ import java.util.concurrent.TimeUnit
class PushEphemeralMessageSendJob private constructor(parameters: Parameters, private val message: EphemeralMessage) : BaseJob(parameters) {
companion object {
private val KEY_MESSAGE = "message"
private const val KEY_MESSAGE = "message"
const val KEY = "PushBackgroundMessageSendJob"
}
@@ -32,14 +31,13 @@ class PushEphemeralMessageSendJob private constructor(parameters: Parameters, pr
message)
override fun serialize(): Data {
// TODO: Is this correct?
return Data.Builder()
.putString(KEY_MESSAGE, message.serialize())
.build()
}
override fun getFactoryKey(): String {
return KEY
}
override fun getFactoryKey(): String { return KEY }
public override fun onRun() {
val recipient = message.get<String?>("recipient", null) ?: throw IllegalStateException()

View File

@@ -5,8 +5,16 @@ import android.util.Log
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.crypto.PreKeyUtil
import org.thoughtcrime.securesms.crypto.SecurityEvent
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.jobs.CleanPreKeysJob
import org.thoughtcrime.securesms.loki.utilities.recipient
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.libsignal.loki.LokiSessionResetStatus
import org.whispersystems.signalservice.api.messages.SignalServiceContent
import org.whispersystems.signalservice.loki.protocol.todo.LokiThreadFriendRequestStatus
object SessionManagementProtocol {
@@ -24,8 +32,50 @@ object SessionManagementProtocol {
}
@JvmStatic
fun sendSessionRestorationRequest(context: Context, publicKey: String) {
val sessionRestorationRequest = EphemeralMessage.createSessionRestorationRequest(publicKey)
ApplicationContext.getInstance(context).jobManager.add(PushEphemeralMessageSendJob(sessionRestorationRequest))
fun handlePreKeyBundleMessageIfNeeded(context: Context, content: SignalServiceContent) {
val recipient = recipient(context, content.sender)
if (recipient.isGroupRecipient) { return }
val preKeyBundleMessage = content.lokiServiceMessage.orNull()?.preKeyBundleMessage ?: return
val registrationID = TextSecurePreferences.getLocalRegistrationId(context) // TODO: It seems wrong to use the local registration ID for this?
val lokiPreKeyBundleDatabase = DatabaseFactory.getLokiPreKeyBundleDatabase(context)
Log.d("Loki", "Received a pre key bundle from: " + content.sender.toString() + ".")
val preKeyBundle = preKeyBundleMessage.getPreKeyBundle(registrationID)
lokiPreKeyBundleDatabase.setPreKeyBundle(content.sender, preKeyBundle)
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient)
val lokiThreadDB = DatabaseFactory.getLokiThreadDatabase(context)
val threadFRStatus = lokiThreadDB.getFriendRequestStatus(threadID)
// If we received a friend request (i.e. also a new pre key bundle), but we were already friends with the other user, reset the session.
if (content.isFriendRequest && threadFRStatus == LokiThreadFriendRequestStatus.FRIENDS) {
val sessionStore = TextSecureSessionStore(context)
sessionStore.archiveAllSessions(content.sender)
val ephemeralMessage = EphemeralMessage.create(content.sender)
ApplicationContext.getInstance(context).jobManager.add(PushEphemeralMessageSendJob(ephemeralMessage))
}
}
@JvmStatic
fun handleSessionRequestIfNeeded(context: Context, content: SignalServiceContent) {
// Auto-accept all session requests
val ephemeralMessage = EphemeralMessage.create(content.sender)
ApplicationContext.getInstance(context).jobManager.add(PushEphemeralMessageSendJob(ephemeralMessage))
}
@JvmStatic
fun handleEndSessionMessage(context: Context, content: SignalServiceContent) {
// TODO: Notify the user
val sessionStore = TextSecureSessionStore(context)
val lokiThreadDB = DatabaseFactory.getLokiThreadDatabase(context)
Log.d("Loki", "Received a session reset request from: ${content.sender}; archiving the session.")
sessionStore.archiveAllSessions(content.sender)
lokiThreadDB.setSessionResetStatus(content.sender, LokiSessionResetStatus.REQUEST_RECEIVED)
Log.d("Loki", "Sending an ephemeral message back to: ${content.sender}.")
val ephemeralMessage = EphemeralMessage.create(content.sender)
ApplicationContext.getInstance(context).jobManager.add(PushEphemeralMessageSendJob(ephemeralMessage))
SecurityEvent.broadcastSecurityUpdateEvent(context)
}
@JvmStatic
private fun isSessionRequest(content: SignalServiceContent): Boolean {
return content.dataMessage.isPresent && content.dataMessage.get().isSessionRequest
}
}

View File

@@ -5,14 +5,38 @@ import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.api.messages.SignalServiceContent
import org.whispersystems.signalservice.loki.protocol.multidevice.MultiDeviceProtocol
import org.whispersystems.signalservice.loki.protocol.todo.LokiThreadFriendRequestStatus
object SessionMetaProtocol {
@JvmStatic
fun sendEphemeralMessage(context: Context, publicKey: String) {
val ephemeralMessage = EphemeralMessage.create(publicKey)
ApplicationContext.getInstance(context).jobManager.add(PushEphemeralMessageSendJob(ephemeralMessage))
fun handleProfileUpdateIfNeeded(context: Context, content: SignalServiceContent) {
val rawDisplayName = content.senderDisplayName.orNull() ?: return
if (rawDisplayName.isBlank()) { return }
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
val userMasterPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context)
val sender = content.sender.toLowerCase()
if (userMasterPublicKey == sender) {
// Update the user's local name if the message came from their master device
TextSecurePreferences.setProfileName(context, rawDisplayName)
}
// Don't overwrite if the message came from a linked device; the device name is
// stored as a user name
val allUserDevices = MultiDeviceProtocol.shared.getAllLinkedDevices(userPublicKey)
if (!allUserDevices.contains(sender)) {
val displayName = rawDisplayName + " (..." + sender.substring(sender.length - 8) + ")"
DatabaseFactory.getLokiUserDatabase(context).setDisplayName(sender, displayName)
}
}
@JvmStatic
fun handleProfileKeyUpdateIfNeeded(context: Context, content: SignalServiceContent) {
val userMasterPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context)
if (userMasterPublicKey != content.sender) { return }
ApplicationContext.getInstance(context).updatePublicChatProfilePictureIfNeeded()
}
/**

View File

@@ -10,12 +10,24 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob
import org.thoughtcrime.securesms.jobs.MultiDeviceGroupUpdateJob
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.loki.protocol.multidevice.MultiDeviceProtocol
import org.whispersystems.signalservice.loki.protocol.todo.LokiThreadFriendRequestStatus
import org.whispersystems.signalservice.loki.utilities.PublicKeyValidation
import java.util.*
object SyncMessagesProtocol {
@JvmStatic
fun shouldIgnoreSyncMessage(context: Context, sender: Recipient): Boolean {
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
return MultiDeviceProtocol.shared.getAllLinkedDevices(userPublicKey).contains(sender.address.serialize())
}
@JvmStatic
fun syncContact(context: Context, address: Address) {
ApplicationContext.getInstance(context).jobManager.add(MultiDeviceContactUpdateJob(context, address, true))
}
@JvmStatic
fun syncAllContacts(context: Context) {
ApplicationContext.getInstance(context).jobManager.add(MultiDeviceContactUpdateJob(context, true))

View File

@@ -0,0 +1,9 @@
package org.thoughtcrime.securesms.loki.utilities
import android.content.Context
import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.recipients.Recipient
fun recipient(context: Context, publicKey: String): Recipient {
return Recipient.from(context, Address.fromSerialized(publicKey), false)
}