From 274ed49f16c59a29680bb3da37b172510cc8a0e1 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 7 Jun 2024 13:56:53 +0200 Subject: [PATCH] Update ZRTP related code to use newly added APIs --- .../ui/call/fragment/ActiveCallFragment.kt | 34 ++++--- .../model/ZrtpSasConfirmationDialogModel.kt | 59 ++++-------- .../ui/call/viewmodel/CurrentCallViewModel.kt | 92 ++++++++++++------- .../layout-land/dialog_confirm_zrtp_sas.xml | 5 +- .../res/layout/dialog_confirm_zrtp_sas.xml | 5 +- app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 7 files changed, 103 insertions(+), 94 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/call/fragment/ActiveCallFragment.kt b/app/src/main/java/org/linphone/ui/call/fragment/ActiveCallFragment.kt index 16e017ef7..7c4af1a05 100644 --- a/app/src/main/java/org/linphone/ui/call/fragment/ActiveCallFragment.kt +++ b/app/src/main/java/org/linphone/ui/call/fragment/ActiveCallFragment.kt @@ -231,24 +231,17 @@ class ActiveCallFragment : GenericCallFragment() { val model = ZrtpSasConfirmationDialogModel(pair.first, pair.second) val dialog = DialogUtils.getZrtpSasConfirmationDialog(requireActivity(), model) - model.dismissEvent.observe(viewLifecycleOwner) { event -> + model.skipEvent.observe(viewLifecycleOwner) { event -> event.consume { + callViewModel.skipZrtpSas() dialog.dismiss() } } - model.trustVerified.observe(viewLifecycleOwner) { event -> - event.consume { verified -> - callViewModel.updateZrtpSas(verified) + model.authTokenClickedEvent.observe(viewLifecycleOwner) { event -> + event.consume { authToken -> + callViewModel.updateZrtpSas(authToken) dialog.dismiss() - - if (verified) { - (requireActivity() as GenericActivity).showBlueToast( - getString(R.string.call_can_be_trusted_toast), - R.drawable.trusted, - doNotTint = true - ) - } } } @@ -257,6 +250,23 @@ class ActiveCallFragment : GenericCallFragment() { } } + callViewModel.zrtpAuthTokenVerifiedEvent.observe(viewLifecycleOwner) { + it.consume { verified -> + if (verified) { + (requireActivity() as GenericActivity).showBlueToast( + getString(R.string.call_can_be_trusted_toast), + R.drawable.trusted, + doNotTint = true + ) + } else { + (requireActivity() as GenericActivity).showRedToast( + getString(R.string.call_can_not_be_trusted_alert_toast), + R.drawable.warning_circle + ) + } + } + } + callViewModel.callDuration.observe(viewLifecycleOwner) { duration -> binding.chronometer.base = SystemClock.elapsedRealtime() - (1000 * duration) binding.chronometer.start() diff --git a/app/src/main/java/org/linphone/ui/call/model/ZrtpSasConfirmationDialogModel.kt b/app/src/main/java/org/linphone/ui/call/model/ZrtpSasConfirmationDialogModel.kt index dfcf69d66..b822a896f 100644 --- a/app/src/main/java/org/linphone/ui/call/model/ZrtpSasConfirmationDialogModel.kt +++ b/app/src/main/java/org/linphone/ui/call/model/ZrtpSasConfirmationDialogModel.kt @@ -21,7 +21,6 @@ package org.linphone.ui.call.model import androidx.annotation.UiThread import androidx.lifecycle.MutableLiveData -import java.util.Random import org.linphone.R import org.linphone.core.tools.Log import org.linphone.ui.GenericViewModel @@ -30,11 +29,10 @@ import org.linphone.utils.Event class ZrtpSasConfirmationDialogModel @UiThread constructor( authTokenToRead: String, - private val authTokenToListen: String + authTokensToListen: List ) : GenericViewModel() { companion object { private const val TAG = "[ZRTP SAS Confirmation Dialog]" - private const val ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" } val message = MutableLiveData() @@ -43,57 +41,36 @@ class ZrtpSasConfirmationDialogModel @UiThread constructor( val letters3 = MutableLiveData() val letters4 = MutableLiveData() - val trustVerified = MutableLiveData>() + val authTokenClickedEvent = MutableLiveData>() - val dismissEvent = MutableLiveData>() + val skipEvent = MutableLiveData>() init { message.value = AppUtils.getFormattedString( R.string.call_dialog_zrtp_validate_trust_subtitle, authTokenToRead ) - - // TODO FIXME: use SDK API when it will be available - val rnd = Random() - val randomLetters1 = "${ALPHABET[rnd.nextInt(ALPHABET.length)]}${ALPHABET[ - rnd.nextInt( - ALPHABET.length - ) - ]}" - val randomLetters2 = "${ALPHABET[rnd.nextInt(ALPHABET.length)]}${ALPHABET[ - rnd.nextInt( - ALPHABET.length - ) - ]}" - val randomLetters3 = "${ALPHABET[rnd.nextInt(ALPHABET.length)]}${ALPHABET[ - rnd.nextInt( - ALPHABET.length - ) - ]}" - val randomLetters4 = "${ALPHABET[rnd.nextInt(ALPHABET.length)]}${ALPHABET[ - rnd.nextInt( - ALPHABET.length - ) - ]}" - - val correctLetters = rnd.nextInt(4) - letters1.value = if (correctLetters == 0) authTokenToListen else randomLetters1 - letters2.value = if (correctLetters == 1) authTokenToListen else randomLetters2 - letters3.value = if (correctLetters == 2) authTokenToListen else randomLetters3 - letters4.value = if (correctLetters == 3) authTokenToListen else randomLetters4 + letters1.value = authTokensToListen[0] + letters2.value = authTokensToListen[1] + letters3.value = authTokensToListen[2] + letters4.value = authTokensToListen[3] } @UiThread - fun dismiss() { - dismissEvent.value = Event(true) + fun skip() { + skipEvent.value = Event(true) + } + + @UiThread + fun notFound() { + Log.e("$TAG User clicked on 'Not Found' button!") + authTokenClickedEvent.value = Event("") } @UiThread fun lettersClicked(letters: MutableLiveData) { - val verified = letters.value == authTokenToListen - Log.i( - "$TAG User clicked on ${if (verified) "right" else "wrong"} letters" - ) - trustVerified.value = Event(verified) + val token = letters.value.orEmpty() + Log.i("$TAG User clicked on [$token] letters") + authTokenClickedEvent.value = Event(token) } } diff --git a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt index 7ec09311f..a66447300 100644 --- a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt @@ -27,7 +27,6 @@ import androidx.annotation.WorkerThread import androidx.core.app.ActivityCompat import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope -import java.util.Locale import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -165,8 +164,12 @@ class CurrentCallViewModel @UiThread constructor() : GenericViewModel() { // ZRTP related - val showZrtpSasDialogEvent: MutableLiveData>> by lazy { - MutableLiveData>>() + val showZrtpSasDialogEvent: MutableLiveData>>> by lazy { + MutableLiveData>>>() + } + + val zrtpAuthTokenVerifiedEvent: MutableLiveData> by lazy { + MutableLiveData>() } // Chat @@ -226,10 +229,18 @@ class CurrentCallViewModel @UiThread constructor() : GenericViewModel() { private val callListener = object : CallListenerStub() { @WorkerThread override fun onEncryptionChanged(call: Call, on: Boolean, authenticationToken: String?) { + Log.i("$TAG Call encryption changed, updating...") updateEncryption() callMediaEncryptionModel.update(call) } + override fun onAuthenticationTokenVerified(call: Call, verified: Boolean) { + Log.w( + "$TAG Notified that authentication token is [${if (verified) "verified" else "not verified!"}]" + ) + zrtpAuthTokenVerifiedEvent.postValue(Event(verified)) + } + override fun onRemoteRecording(call: Call, recording: Boolean) { Log.i("$TAG Remote recording changed: $recording") isRemoteRecordingEvent.postValue(Event(Pair(recording, displayedName.value.orEmpty()))) @@ -523,14 +534,29 @@ class CurrentCallViewModel @UiThread constructor() : GenericViewModel() { } @UiThread - fun updateZrtpSas(verified: Boolean) { + fun skipZrtpSas() { coreContext.postOnCoreThread { if (::currentCall.isInitialized) { - if (!verified && !currentCall.authenticationTokenVerified) { - Log.w("$TAG ZRTP SAS validation failed") + Log.w("$TAG User skipped SAS validation in ZRTP call") + currentCall.skipZrtpAuthentication() + } + } + } + + @UiThread + fun updateZrtpSas(authTokenClicked: String) { + coreContext.postOnCoreThread { + if (::currentCall.isInitialized) { + if (authTokenClicked.isEmpty()) { + Log.e( + "$TAG Doing a fake ZRTP SAS check with empty token because user clicked on 'Not Found' button!" + ) } else { - currentCall.authenticationTokenVerified = verified + Log.i( + "$TAG Checking if ZRTP SAS auth token [$authTokenClicked] is the right one" + ) } + currentCall.checkAuthenticationTokenSelected(authTokenClicked) } } } @@ -921,45 +947,33 @@ class CurrentCallViewModel @UiThread constructor() : GenericViewModel() { fun showZrtpSasDialogIfPossible() { coreContext.postOnCoreThread { if (currentCall.currentParams.mediaEncryption == MediaEncryption.ZRTP) { - val authToken = currentCall.authenticationToken - val isDeviceTrusted = currentCall.authenticationTokenVerified && authToken != null + val isDeviceTrusted = currentCall.authenticationTokenVerified Log.i( - "$TAG Current call media encryption is ZRTP, auth token is ${if (isDeviceTrusted) "trusted" else "not trusted yet"}" + "$TAG Current call media encryption is ZRTP, auth token is [${if (isDeviceTrusted) "trusted" else "not trusted yet"}]" ) - if (!authToken.isNullOrEmpty()) { - showZrtpSasDialog(authToken) + val tokenToRead = currentCall.localAuthenticationToken + val tokensToDisplay = currentCall.remoteAuthenticationTokens.toList() + if (!tokenToRead.isNullOrEmpty() && tokensToDisplay.size == 4) { + showZrtpSasDialogEvent.postValue(Event(Pair(tokenToRead, tokensToDisplay))) + } else { + Log.w( + "$TAG Either local auth token is null/empty or remote tokens list doesn't contains 4 elements!" + ) } } } } - @WorkerThread - private fun showZrtpSasDialog(authToken: String) { - val upperCaseAuthToken = authToken.uppercase(Locale.getDefault()) - val toRead: String - val toListen: String - when (currentCall.dir) { - Call.Dir.Incoming -> { - toRead = upperCaseAuthToken.substring(0, 2) - toListen = upperCaseAuthToken.substring(2) - } - else -> { - toRead = upperCaseAuthToken.substring(2) - toListen = upperCaseAuthToken.substring(0, 2) - } - } - showZrtpSasDialogEvent.postValue(Event(Pair(toRead, toListen))) - } - @WorkerThread private fun updateEncryption(): Boolean { when (val mediaEncryption = currentCall.currentParams.mediaEncryption) { MediaEncryption.ZRTP -> { - val authToken = currentCall.authenticationToken - val isDeviceTrusted = currentCall.authenticationTokenVerified && authToken != null + val isDeviceTrusted = currentCall.authenticationTokenVerified + val cacheMismatch = currentCall.zrtpCacheMismatchFlag Log.i( - "$TAG Current call media encryption is ZRTP, auth token is ${if (isDeviceTrusted) "trusted" else "not trusted yet"}" + "$TAG Current call media encryption is ZRTP, auth token is [${if (isDeviceTrusted) "trusted" else "not trusted yet"}], cache mismatch is [$cacheMismatch]" ) + val securityLevel = if (isDeviceTrusted) SecurityLevel.EndToEndEncryptedAndVerified else SecurityLevel.EndToEndEncrypted val avatarModel = contact.value if (avatarModel != null && currentCall.conference == null) { // Don't do it for conferences @@ -984,9 +998,17 @@ class CurrentCallViewModel @UiThread constructor() : GenericViewModel() { coreContext.core.postQuantumAvailable && stats?.isZrtpKeyAgreementAlgoPostQuantum == true ) - if (!isDeviceTrusted && !authToken.isNullOrEmpty()) { + if (cacheMismatch || !isDeviceTrusted) { Log.i("$TAG Showing ZRTP SAS confirmation dialog") - showZrtpSasDialog(authToken) + val tokenToRead = currentCall.localAuthenticationToken + val tokensToDisplay = currentCall.remoteAuthenticationTokens.toList() + if (!tokenToRead.isNullOrEmpty() && tokensToDisplay.size == 4) { + showZrtpSasDialogEvent.postValue(Event(Pair(tokenToRead, tokensToDisplay))) + } else { + Log.w( + "$TAG Either local auth token is null/empty or remote tokens list doesn't contains 4 elements!" + ) + } } return isDeviceTrusted diff --git a/app/src/main/res/layout-land/dialog_confirm_zrtp_sas.xml b/app/src/main/res/layout-land/dialog_confirm_zrtp_sas.xml index 63440f1af..3aa70350c 100644 --- a/app/src/main/res/layout-land/dialog_confirm_zrtp_sas.xml +++ b/app/src/main/res/layout-land/dialog_confirm_zrtp_sas.xml @@ -12,7 +12,6 @@ @@ -135,7 +134,7 @@ app:layout_constraintBottom_toBottomOf="@id/letters_1"/> @@ -136,7 +135,7 @@ app:layout_constraintBottom_toBottomOf="@id/letters_3"/> L\'historique des appels a été supprimé Cet appel est complètement sécurisé + Problème de sécurité ! Appel en cours de transfert L\'appel a été transferré Le transfert a échoué ! diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 80e3beec9..d9cee319a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -648,6 +648,7 @@ History has been deleted This call can be trusted + Security alert ! Call is being transferred Call has been successfully transferred Call transfer failed!