2020-05-13 10:24:20 +10:00
package org.thoughtcrime.securesms.loki.protocol
import android.content.Context
2020-05-13 12:29:31 +10:00
import android.util.Log
2020-05-14 12:11:42 +10:00
import nl.komponents.kovenant.Promise
2020-05-13 10:24:20 +10:00
import org.thoughtcrime.securesms.ApplicationContext
2020-05-14 12:11:42 +10:00
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil
2020-05-13 11:15:17 +10:00
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.jobs.PushMediaSendJob
import org.thoughtcrime.securesms.jobs.PushSendJob
import org.thoughtcrime.securesms.jobs.PushTextSendJob
2020-05-13 12:29:31 +10:00
import org.thoughtcrime.securesms.loki.utilities.Broadcaster
2020-05-14 12:11:42 +10:00
import org.thoughtcrime.securesms.loki.utilities.recipient
2020-05-13 10:24:20 +10:00
import org.thoughtcrime.securesms.recipients.Recipient
2020-05-13 12:29:31 +10:00
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.api.messages.SignalServiceContent
2020-05-14 12:11:42 +10:00
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
import org.whispersystems.signalservice.api.push.SignalServiceAddress
2020-05-13 11:15:17 +10:00
import org.whispersystems.signalservice.loki.api.fileserver.LokiFileServerAPI
import org.whispersystems.signalservice.loki.protocol.meta.SessionMetaProtocol
2020-05-13 12:29:31 +10:00
import org.whispersystems.signalservice.loki.protocol.multidevice.DeviceLink
import org.whispersystems.signalservice.loki.protocol.multidevice.DeviceLinkingSession
2020-05-13 11:15:17 +10:00
import org.whispersystems.signalservice.loki.protocol.multidevice.MultiDeviceProtocol
import org.whispersystems.signalservice.loki.protocol.todo.LokiThreadFriendRequestStatus
2020-05-14 12:11:42 +10:00
import org.whispersystems.signalservice.loki.utilities.retryIfNeeded
2020-05-13 10:24:20 +10:00
object MultiDeviceProtocol {
2020-05-13 11:15:17 +10:00
enum class MessageType { Text , Media }
2020-05-13 10:24:20 +10:00
@JvmStatic
2020-05-22 13:41:36 +10:00
fun sendTextPush ( context : Context , recipient : Recipient , messageID : Long , isEndSession : Boolean ) {
sendMessagePush ( context , recipient , messageID , MessageType . Text , isEndSession )
2020-05-13 10:24:20 +10:00
}
@JvmStatic
fun sendMediaPush ( context : Context , recipient : Recipient , messageID : Long ) {
2020-05-22 13:41:36 +10:00
sendMessagePush ( context , recipient , messageID , MessageType . Media , false )
2020-05-13 10:24:20 +10:00
}
2020-05-22 13:41:36 +10:00
private fun sendMessagePushToDevice ( context : Context , recipient : Recipient , messageID : Long , messageType : MessageType , isEndSession : Boolean ) : PushSendJob {
2020-05-13 11:15:17 +10:00
val threadID = DatabaseFactory . getThreadDatabase ( context ) . getThreadIdFor ( recipient )
val threadFRStatus = DatabaseFactory . getLokiThreadDatabase ( context ) . getFriendRequestStatus ( threadID )
2020-05-21 11:00:54 +10:00
val isNoteToSelf = SessionMetaProtocol . shared . isNoteToSelf ( recipient . address . serialize ( ) )
val isContactFriend = ( threadFRStatus == LokiThreadFriendRequestStatus . FRIENDS || isNoteToSelf ) // In the note to self case the device linking request was the FR
2020-05-22 13:41:36 +10:00
val isFRMessage = !is ContactFriend
2020-05-13 11:15:17 +10:00
val hasVisibleContent = when ( messageType ) {
MessageType . Text -> DatabaseFactory . getSmsDatabase ( context ) . getMessage ( messageID ) . body . isNotBlank ( )
MessageType . Media -> {
val outgoingMediaMessage = DatabaseFactory . getMmsDatabase ( context ) . getOutgoingMessage ( messageID )
outgoingMediaMessage . body . isNotBlank ( ) || outgoingMediaMessage . attachments . isNotEmpty ( )
}
}
val shouldSendAutoGeneratedFR = !is ContactFriend && !is FRMessage
2020-05-21 11:00:54 +10:00
&& !is NoteToSelf && ! recipient . address . isGroup // Group threads work through session requests
2020-05-22 13:41:36 +10:00
&& hasVisibleContent && !is EndSession
2020-05-13 11:15:17 +10:00
if ( ! shouldSendAutoGeneratedFR ) {
when ( messageType ) {
2020-05-14 14:51:34 +10:00
MessageType . Text -> return PushTextSendJob ( messageID , messageID , recipient . address , isFRMessage , null )
MessageType . Media -> return PushMediaSendJob ( messageID , messageID , recipient . address , isFRMessage , null )
2020-05-13 11:15:17 +10:00
}
} else {
val autoGeneratedFRMessage = " Please accept to enable messages to be synced across devices "
when ( messageType ) {
MessageType . Text -> return PushTextSendJob ( messageID , messageID , recipient . address , true , autoGeneratedFRMessage )
MessageType . Media -> return PushMediaSendJob ( messageID , messageID , recipient . address , true , autoGeneratedFRMessage )
}
}
}
2020-05-22 13:41:36 +10:00
private fun sendMessagePush ( context : Context , recipient : Recipient , messageID : Long , messageType : MessageType , isEndSession : Boolean ) {
2020-05-13 11:15:17 +10:00
val jobManager = ApplicationContext . getInstance ( context ) . jobManager
val isMultiDeviceRequired = ! recipient . address . isOpenGroup
if ( !is MultiDeviceRequired ) {
when ( messageType ) {
MessageType . Text -> jobManager . add ( PushTextSendJob ( messageID , recipient . address ) )
MessageType . Media -> PushMediaSendJob . enqueue ( context , jobManager , messageID , recipient . address )
}
}
val publicKey = recipient . address . serialize ( )
LokiFileServerAPI . shared . getDeviceLinks ( publicKey ) . success {
val devices = MultiDeviceProtocol . shared . getAllLinkedDevices ( publicKey )
2020-05-22 13:41:36 +10:00
val jobs = devices . map { sendMessagePushToDevice ( context , recipient ( context , it ) , messageID , messageType , isEndSession ) }
2020-05-13 11:15:17 +10:00
@Suppress ( " UNCHECKED_CAST " )
when ( messageType ) {
MessageType . Text -> jobManager . startChain ( jobs ) . enqueue ( )
MessageType . Media -> PushMediaSendJob . enqueue ( context , jobManager , jobs as List < PushMediaSendJob > )
}
2020-05-15 15:44:45 +10:00
} . fail {
2020-05-13 11:15:17 +10:00
// Proceed even if updating the recipient's device links failed, so that message sending
// is independent of whether the file server is online
when ( messageType ) {
MessageType . Text -> jobManager . add ( PushTextSendJob ( messageID , recipient . address ) )
MessageType . Media -> PushMediaSendJob . enqueue ( context , jobManager , messageID , recipient . address )
}
}
}
2020-05-13 12:29:31 +10:00
2020-05-14 12:11:42 +10:00
fun sendDeviceLinkMessage ( context : Context , publicKey : String , deviceLink : DeviceLink ) : Promise < Unit , Exception > {
val messageSender = ApplicationContext . getInstance ( context ) . communicationModule . provideSignalMessageSender ( )
val address = SignalServiceAddress ( publicKey )
val message = SignalServiceDataMessage . newBuilder ( ) . withDeviceLink ( deviceLink )
// A request should include a pre key bundle. An authorization should be a normal message.
if ( deviceLink . type == DeviceLink . Type . REQUEST ) {
val preKeyBundle = DatabaseFactory . getLokiPreKeyBundleDatabase ( context ) . generatePreKeyBundle ( address . number )
message . asFriendRequest ( true ) . withPreKeyBundle ( preKeyBundle )
} else {
// Include the user's profile key so that the slave device can get the user's profile picture
message . withProfileKey ( ProfileKeyUtil . getProfileKey ( context ) )
}
return try {
Log . d ( " Loki " , " Sending device link message to: $publicKey . " )
val udAccess = UnidentifiedAccessUtil . getAccessFor ( context , recipient ( context , publicKey ) )
val result = messageSender . sendMessage ( 0 , address , udAccess , message . build ( ) )
if ( result . success == null ) {
val exception = when {
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 device link message to: $publicKey due to error: $e . " )
Promise . ofFail ( e )
}
}
fun signAndSendDeviceLinkMessage ( context : Context , deviceLink : DeviceLink ) : Promise < Unit , Exception > {
val userPrivateKey = IdentityKeyUtil . getIdentityKeyPair ( context ) . privateKey . serialize ( )
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. " ) )
}
return retryIfNeeded ( 8 ) {
sendDeviceLinkMessage ( context , deviceLink . slaveHexEncodedPublicKey , signedDeviceLink )
}
}
2020-05-13 12:29:31 +10:00
@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 ( !is Valid ) { return }
2020-05-21 11:00:54 +10:00
// The line below isn't actually necessary because this is called after PushDecryptJob
// calls handlePreKeyBundleMessageIfNeeded, but it also doesn't hurt.
2020-05-13 12:29:31 +10:00
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 ( !is Valid ) { 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 )
2020-06-03 17:03:35 +10:00
org . thoughtcrime . securesms . loki . protocol . SessionMetaProtocol . handleProfileKey ( context , content )
2020-05-13 12:29:31 +10:00
}
@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 ( )
}
}
}
2020-05-13 10:24:20 +10:00
}