Add key transparency backend support.

This commit is contained in:
Michelle Tang
2026-01-30 13:17:26 -05:00
committed by Greyson Parrelli
parent 26739491a5
commit 423b8c942c
18 changed files with 355 additions and 9 deletions

View File

@@ -84,6 +84,13 @@ class VerifyDisplayFragment : Fragment() {
binding.caption.setLinkColor(ContextCompat.getColor(requireContext(), CoreUiR.color.signal_colorPrimary))
viewModel.getAutomaticVerification().observe(viewLifecycleOwner) { status ->
if (status == AutomaticVerificationStatus.NONE) {
binding.autoVerifyContainer.setOnClickListener {
viewModel.verifyAutomatically()
}
} else {
binding.autoVerifyContainer.setOnClickListener(null)
}
updateStatus(status)
}

View File

@@ -0,0 +1,81 @@
package org.thoughtcrime.securesms.verify
import org.signal.core.util.logging.Log
import org.signal.libsignal.net.KeyTransparency
import org.signal.libsignal.net.RequestResult
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
import org.thoughtcrime.securesms.database.model.KeyTransparencyStore
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.net.SignalNetwork
import org.thoughtcrime.securesms.recipients.Recipient
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess
import java.time.Duration
/**
* Repository for safety numbers, namely to support key transparency / automatic verification
*/
object VerifySafetyNumberRepository {
private val TAG = Log.tag(VerifySafetyNumberRepository::class.java)
/**
* Given a recipient will try to verify via search (first time) or monitor (subsequent).
*/
suspend fun verifyAutomatically(recipient: Recipient): VerifyResult {
if (recipient.aci.isEmpty || recipient.e164.isEmpty) {
return VerifyResult.UnretryableFailure
}
val identityRecord = AppDependencies.protocolStore.aci().identities().getIdentityRecord(recipient.id)
val aciIdentityKey = identityRecord.get().identityKey
val aci = recipient.requireAci().libSignalAci
val e164 = recipient.requireE164()
val unidentifiedAccessKey = ProfileKeyUtil.profileKeyOrNull(recipient.profileKey).let { UnidentifiedAccess.deriveAccessKeyFrom(it) }
val monitorMode = if (recipient.isSelf) KeyTransparency.MonitorMode.SELF else KeyTransparency.MonitorMode.OTHER
val firstSearch = recipient.keyTransparencyData == null
val result = if (firstSearch) {
Log.i(TAG, "First search in key transparency")
SignalNetwork.keyTransparency.search(aci, aciIdentityKey, e164, unidentifiedAccessKey, KeyTransparencyStore)
} else {
Log.i(TAG, "Monitoring search in key transparency")
SignalNetwork.keyTransparency.monitor(monitorMode, aci, aciIdentityKey, e164, unidentifiedAccessKey, KeyTransparencyStore)
}
Log.i(TAG, "Key transparency complete, result: $result")
return when (result) {
is RequestResult.Success -> {
VerifyResult.Success
}
is RequestResult.NonSuccess -> {
if (result.error.exception is IllegalArgumentException) {
VerifyResult.CorruptedFailure
} else {
VerifyResult.UnretryableFailure
}
}
is RequestResult.RetryableNetworkError -> {
if (result.retryAfter != null) {
VerifyResult.RetryableFailure(result.retryAfter!!)
} else {
VerifyResult.UnretryableFailure
}
}
is RequestResult.ApplicationError -> VerifyResult.UnretryableFailure
}
}
sealed interface VerifyResult {
/** Successful verification */
data object Success : VerifyResult
/** Retryable failure **/
data class RetryableFailure(val duration: Duration) : VerifyResult
/** Failure when either the head or the data is corrupted. Retryable if both are reset. */
data object CorruptedFailure : VerifyResult
/** Failures that should not be retried. */
data object UnretryableFailure : VerifyResult
}
}

View File

@@ -10,6 +10,11 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import org.signal.core.util.concurrent.SignalDispatchers
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.logging.Log
import org.signal.libsignal.protocol.IdentityKey
@@ -17,6 +22,7 @@ import org.signal.libsignal.protocol.fingerprint.Fingerprint
import org.signal.libsignal.protocol.fingerprint.NumericFingerprintGenerator
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock
import org.thoughtcrime.securesms.database.IdentityTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobs.MultiDeviceVerifiedUpdateJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
@@ -43,6 +49,52 @@ class VerifySafetyNumberViewModel(
init {
initializeFingerprints()
checkAutomaticVerificationEligibility()
}
private fun checkAutomaticVerificationEligibility() {
if (recipient.get().e164.isEmpty || recipient.get().aci.isEmpty) {
automaticVerificationLiveData.postValue(AutomaticVerificationStatus.UNAVAILABLE_PERMANENT)
}
}
fun verifyAutomatically(canRetry: Boolean = true) {
viewModelScope.launch(SignalDispatchers.IO) {
if (automaticVerificationLiveData.value == AutomaticVerificationStatus.UNAVAILABLE_PERMANENT || !isActive) {
return@launch
}
automaticVerificationLiveData.postValue(AutomaticVerificationStatus.VERIFYING)
when (val result = VerifySafetyNumberRepository.verifyAutomatically(recipient.get())) {
VerifySafetyNumberRepository.VerifyResult.Success -> {
automaticVerificationLiveData.postValue(AutomaticVerificationStatus.VERIFIED)
}
is VerifySafetyNumberRepository.VerifyResult.RetryableFailure -> {
if (canRetry) {
delay(result.duration.toMillis())
verifyAutomatically(canRetry = false)
} else {
Log.i(TAG, "Got a retryable exception, but we already retried once. Ignoring.")
automaticVerificationLiveData.postValue(AutomaticVerificationStatus.UNAVAILABLE_TEMPORARY)
}
}
VerifySafetyNumberRepository.VerifyResult.CorruptedFailure -> {
Log.w(TAG, "KT store was corrupted. Clearing everything and starting again.")
SignalStore.account.distinguishedHead = null
SignalDatabase.recipients.setKeyTransparencyData(recipient.get().requireAci(), null)
if (canRetry) {
verifyAutomatically(canRetry = false)
} else {
Log.i(TAG, "Store was corrupted and we can retry, but we already retried once. Ignoring.")
automaticVerificationLiveData.postValue(AutomaticVerificationStatus.UNAVAILABLE_TEMPORARY)
}
}
VerifySafetyNumberRepository.VerifyResult.UnretryableFailure -> {
automaticVerificationLiveData.postValue(AutomaticVerificationStatus.UNAVAILABLE_TEMPORARY)
}
}
}
}
private fun initializeFingerprints() {