mirror of
https://github.com/oxen-io/session-android.git
synced 2025-12-03 14:42:29 +00:00
Refactor PushDecryptJob
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user