Update ZRTP related code to use newly added APIs

This commit is contained in:
Sylvain Berfini 2024-06-07 13:56:53 +02:00
parent fb75ea344c
commit 274ed49f16
7 changed files with 103 additions and 94 deletions

View file

@ -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()

View file

@ -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<String>
) : GenericViewModel() {
companion object {
private const val TAG = "[ZRTP SAS Confirmation Dialog]"
private const val ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
}
val message = MutableLiveData<String>()
@ -43,57 +41,36 @@ class ZrtpSasConfirmationDialogModel @UiThread constructor(
val letters3 = MutableLiveData<String>()
val letters4 = MutableLiveData<String>()
val trustVerified = MutableLiveData<Event<Boolean>>()
val authTokenClickedEvent = MutableLiveData<Event<String>>()
val dismissEvent = MutableLiveData<Event<Boolean>>()
val skipEvent = MutableLiveData<Event<Boolean>>()
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<String>) {
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)
}
}

View file

@ -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<Event<Pair<String, String>>> by lazy {
MutableLiveData<Event<Pair<String, String>>>()
val showZrtpSasDialogEvent: MutableLiveData<Event<Pair<String, List<String>>>> by lazy {
MutableLiveData<Event<Pair<String, List<String>>>>()
}
val zrtpAuthTokenVerifiedEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
// 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

View file

@ -12,7 +12,6 @@
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:onClick="@{() -> viewModel.dismiss()}"
android:layout_width="match_parent"
android:layout_height="match_parent">
@ -135,7 +134,7 @@
app:layout_constraintBottom_toBottomOf="@id/letters_1"/>
<androidx.appcompat.widget.AppCompatTextView
android:onClick="@{() -> viewModel.dismiss()}"
android:onClick="@{() -> viewModel.skip()}"
style="@style/default_text_style_600"
android:id="@+id/skip"
android:layout_width="wrap_content"
@ -153,7 +152,7 @@
app:layout_constraintBottom_toTopOf="@id/nothing_matches"/>
<androidx.appcompat.widget.AppCompatTextView
android:onClick="@{() -> viewModel.dismiss()}"
android:onClick="@{() -> viewModel.notFound()}"
style="@style/default_text_style_600"
android:id="@+id/nothing_matches"
android:layout_width="0dp"

View file

@ -12,7 +12,6 @@
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:onClick="@{() -> viewModel.dismiss()}"
android:layout_width="match_parent"
android:layout_height="match_parent">
@ -136,7 +135,7 @@
app:layout_constraintBottom_toBottomOf="@id/letters_3"/>
<androidx.appcompat.widget.AppCompatTextView
android:onClick="@{() -> viewModel.dismiss()}"
android:onClick="@{() -> viewModel.skip()}"
style="@style/default_text_style_600"
android:id="@+id/skip"
android:layout_width="wrap_content"
@ -154,7 +153,7 @@
app:layout_constraintBottom_toTopOf="@id/nothing_matches"/>
<androidx.appcompat.widget.AppCompatTextView
android:onClick="@{() -> viewModel.dismiss()}"
android:onClick="@{() -> viewModel.notFound()}"
style="@style/default_text_style_600"
android:id="@+id/nothing_matches"
android:layout_width="0dp"

View file

@ -611,6 +611,7 @@
<string name="call_history_deleted_toast">L\'historique des appels a été supprimé</string>
<string name="call_can_be_trusted_toast">Cet appel est complètement sécurisé</string>
<string name="call_can_not_be_trusted_alert_toast">Problème de sécurité !</string>
<string name="call_transfer_in_progress_toast">Appel en cours de transfert</string>
<string name="call_transfer_successful_toast">L\'appel a été transferré</string>
<string name="call_transfer_failed_toast">Le transfert a échoué !</string>

View file

@ -648,6 +648,7 @@
<string name="call_history_deleted_toast">History has been deleted</string>
<string name="call_can_be_trusted_toast">This call can be trusted</string>
<string name="call_can_not_be_trusted_alert_toast">Security alert !</string>
<string name="call_transfer_in_progress_toast">Call is being transferred</string>
<string name="call_transfer_successful_toast">Call has been successfully transferred</string>
<string name="call_transfer_failed_toast">Call transfer failed!</string>