mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-19 08:09:12 +01:00
Add key transparency backend support.
This commit is contained in:
committed by
Greyson Parrelli
parent
26739491a5
commit
423b8c942c
@@ -199,6 +199,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
const val SORT_NAME = "sort_name"
|
||||
const val IDENTITY_STATUS = "identity_status"
|
||||
const val IDENTITY_KEY = "identity_key"
|
||||
const val KEY_TRANSPARENCY_DATA = "key_transparency_data"
|
||||
|
||||
@JvmField
|
||||
val CREATE_TABLE =
|
||||
@@ -267,7 +268,8 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
$NICKNAME_FAMILY_NAME TEXT DEFAULT NULL,
|
||||
$NICKNAME_JOINED_NAME TEXT DEFAULT NULL,
|
||||
$NOTE TEXT DEFAULT NULL,
|
||||
$MESSAGE_EXPIRATION_TIME_VERSION INTEGER DEFAULT 1 NOT NULL
|
||||
$MESSAGE_EXPIRATION_TIME_VERSION INTEGER DEFAULT 1 NOT NULL,
|
||||
$KEY_TRANSPARENCY_DATA BLOB DEFAULT NULL
|
||||
)
|
||||
"""
|
||||
|
||||
@@ -331,7 +333,8 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
PHONE_NUMBER_SHARING,
|
||||
NICKNAME_GIVEN_NAME,
|
||||
NICKNAME_FAMILY_NAME,
|
||||
NOTE
|
||||
NOTE,
|
||||
KEY_TRANSPARENCY_DATA
|
||||
)
|
||||
|
||||
private val ID_PROJECTION = arrayOf(ID)
|
||||
@@ -4026,6 +4029,25 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
.readToSingleLong(0L)
|
||||
}
|
||||
|
||||
fun getKeyTransparencyData(aci: ACI): ByteArray? {
|
||||
return readableDatabase
|
||||
.select(KEY_TRANSPARENCY_DATA)
|
||||
.from(TABLE_NAME)
|
||||
.where("$ACI_COLUMN = ?", aci.toString())
|
||||
.run()
|
||||
.readToSingleObject { cursor ->
|
||||
cursor.requireBlob(KEY_TRANSPARENCY_DATA)
|
||||
}
|
||||
}
|
||||
|
||||
fun setKeyTransparencyData(aci: ACI, data: ByteArray?) {
|
||||
writableDatabase
|
||||
.update(TABLE_NAME)
|
||||
.values(KEY_TRANSPARENCY_DATA to data)
|
||||
.where("$ACI_COLUMN = ?", aci.toString())
|
||||
.run()
|
||||
}
|
||||
|
||||
/**
|
||||
* Will update the database with the content values you specified. It will make an intelligent
|
||||
* query such that this will only return true if a row was *actually* updated.
|
||||
|
||||
@@ -165,7 +165,8 @@ object RecipientTableCursorUtil {
|
||||
callLinkRoomId = cursor.requireString(RecipientTable.CALL_LINK_ROOM_ID)?.let { CallLinkRoomId.DatabaseSerializer.deserialize(it) },
|
||||
phoneNumberSharing = cursor.requireInt(RecipientTable.PHONE_NUMBER_SHARING).let { RecipientTable.PhoneNumberSharingState.fromId(it) },
|
||||
nickname = ProfileName.fromParts(cursor.requireString(RecipientTable.NICKNAME_GIVEN_NAME), cursor.requireString(RecipientTable.NICKNAME_FAMILY_NAME)),
|
||||
note = cursor.requireString(RecipientTable.NOTE)
|
||||
note = cursor.requireString(RecipientTable.NOTE),
|
||||
keyTransparencyData = cursor.requireBlob(RecipientTable.KEY_TRANSPARENCY_DATA)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -153,6 +153,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V296_RemovePollVote
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V297_AddPinnedMessageColumns
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V298_DoNotBackupReleaseNotes
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V299_AddAttachmentMetadataTable
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V300_AddKeyTransparencyColumn
|
||||
import org.thoughtcrime.securesms.database.SQLiteDatabase as SignalSqliteDatabase
|
||||
|
||||
/**
|
||||
@@ -312,10 +313,11 @@ object SignalDatabaseMigrations {
|
||||
296 to V296_RemovePollVoteConstraint,
|
||||
297 to V297_AddPinnedMessageColumns,
|
||||
298 to V298_DoNotBackupReleaseNotes,
|
||||
299 to V299_AddAttachmentMetadataTable
|
||||
299 to V299_AddAttachmentMetadataTable,
|
||||
300 to V300_AddKeyTransparencyColumn
|
||||
)
|
||||
|
||||
const val DATABASE_VERSION = 299
|
||||
const val DATABASE_VERSION = 300
|
||||
|
||||
@JvmStatic
|
||||
fun migrate(context: Application, db: SignalSqliteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.thoughtcrime.securesms.database.helpers.migration
|
||||
|
||||
import android.app.Application
|
||||
import org.thoughtcrime.securesms.database.SQLiteDatabase
|
||||
|
||||
/**
|
||||
* Adds column to recipient to track key transparency data
|
||||
*/
|
||||
@Suppress("ClassName")
|
||||
object V300_AddKeyTransparencyColumn : SignalDatabaseMigration {
|
||||
|
||||
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
db.execSQL("ALTER TABLE recipient ADD COLUMN key_transparency_data BLOB DEFAULT NULL")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.thoughtcrime.securesms.database.model
|
||||
|
||||
import org.signal.core.util.logging.Log.tag
|
||||
import org.signal.libsignal.keytrans.Store
|
||||
import org.signal.libsignal.protocol.ServiceId
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import java.util.Optional
|
||||
|
||||
/**
|
||||
* Store used by [org.signal.libsignal.net.KeyTransparencyClient] during key transparency
|
||||
*/
|
||||
data object KeyTransparencyStore : Store {
|
||||
|
||||
private val TAG: String = tag(KeyTransparencyStore::class.java)
|
||||
|
||||
override fun getLastDistinguishedTreeHead(): Optional<ByteArray> {
|
||||
return Optional.ofNullable(SignalStore.account.distinguishedHead)
|
||||
}
|
||||
|
||||
override fun setLastDistinguishedTreeHead(lastDistinguishedTreeHead: ByteArray) {
|
||||
SignalStore.account.distinguishedHead = lastDistinguishedTreeHead
|
||||
}
|
||||
|
||||
override fun getAccountData(libsignalAci: ServiceId.Aci): Optional<ByteArray> {
|
||||
val aci = org.signal.core.models.ServiceId.ACI.fromLibSignal(libsignalAci)
|
||||
return Optional.ofNullable(SignalDatabase.recipients.getKeyTransparencyData(aci))
|
||||
}
|
||||
|
||||
override fun setAccountData(libsignalAci: ServiceId.Aci, data: ByteArray) {
|
||||
val aci = org.signal.core.models.ServiceId.ACI.fromLibSignal(libsignalAci)
|
||||
SignalDatabase.recipients.setKeyTransparencyData(aci, data)
|
||||
}
|
||||
}
|
||||
@@ -81,7 +81,8 @@ data class RecipientRecord(
|
||||
val callLinkRoomId: CallLinkRoomId?,
|
||||
val phoneNumberSharing: PhoneNumberSharingState,
|
||||
val nickname: ProfileName,
|
||||
val note: String?
|
||||
val note: String?,
|
||||
val keyTransparencyData: ByteArray? = null
|
||||
) {
|
||||
|
||||
fun e164Only(): Boolean {
|
||||
|
||||
@@ -369,6 +369,9 @@ object AppDependencies {
|
||||
val donationsApi: DonationsApi
|
||||
get() = networkModule.donationsApi
|
||||
|
||||
val keyTransparencyApi: KeyTransparencyApi
|
||||
get() = networkModule.keyTransparencyApi
|
||||
|
||||
@JvmStatic
|
||||
val okHttpClient: OkHttpClient
|
||||
get() = networkModule.okHttpClient
|
||||
@@ -463,5 +466,6 @@ object AppDependencies {
|
||||
fun provideRemoteConfigApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket, pushServiceSocket: PushServiceSocket): RemoteConfigApi
|
||||
fun provideDonationsApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket, unauthWebSocket: SignalWebSocket.UnauthenticatedWebSocket): DonationsApi
|
||||
fun provideSvrBApi(libSignalNetwork: Network): SvrBApi
|
||||
fun provideKeyTransparencyApi(unauthWebSocket: SignalWebSocket.UnauthenticatedWebSocket): KeyTransparencyApi
|
||||
}
|
||||
}
|
||||
|
||||
@@ -579,6 +579,11 @@ public class ApplicationDependencyProvider implements AppDependencies.Provider {
|
||||
return new SvrBApi(libSignalNetwork);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull KeyTransparencyApi provideKeyTransparencyApi(@NonNull SignalWebSocket.UnauthenticatedWebSocket unauthWebSocket) {
|
||||
return new KeyTransparencyApi(unauthWebSocket);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static class DynamicCredentialsProvider implements CredentialsProvider {
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package org.thoughtcrime.securesms.dependencies
|
||||
|
||||
import org.signal.libsignal.internal.mapWithCancellation
|
||||
import org.signal.libsignal.keytrans.KeyTransparencyException
|
||||
import org.signal.libsignal.keytrans.VerificationFailedException
|
||||
import org.signal.libsignal.net.AppExpiredException
|
||||
import org.signal.libsignal.net.BadRequestError
|
||||
import org.signal.libsignal.net.KeyTransparency
|
||||
import org.signal.libsignal.net.NetworkException
|
||||
import org.signal.libsignal.net.NetworkProtocolException
|
||||
import org.signal.libsignal.net.RequestResult
|
||||
import org.signal.libsignal.net.RetryLaterException
|
||||
import org.signal.libsignal.net.ServerSideErrorException
|
||||
import org.signal.libsignal.net.TimeoutException
|
||||
import org.signal.libsignal.net.getOrError
|
||||
import org.signal.libsignal.protocol.IdentityKey
|
||||
import org.signal.libsignal.protocol.ServiceId
|
||||
import org.thoughtcrime.securesms.database.model.KeyTransparencyStore
|
||||
import org.whispersystems.signalservice.api.websocket.SignalWebSocket
|
||||
|
||||
/**
|
||||
* Operations used when interacting with [org.signal.libsignal.net.KeyTransparencyClient]
|
||||
*/
|
||||
class KeyTransparencyApi(private val unauthWebSocket: SignalWebSocket.UnauthenticatedWebSocket) {
|
||||
|
||||
/**
|
||||
* Uses KT to verify recipient. This is an unauthenticated and should only be called the first time KT is being requested for this recipient.
|
||||
*/
|
||||
suspend fun search(aci: ServiceId.Aci, aciIdentityKey: IdentityKey, e164: String, unidentifiedAccessKey: ByteArray, keyTransparencyStore: KeyTransparencyStore): RequestResult<Unit, KeyTransparencyError> {
|
||||
return unauthWebSocket.runCatchingWithUnauthChatConnection { chatConnection ->
|
||||
chatConnection.keyTransparencyClient().search(aci, aciIdentityKey, e164, unidentifiedAccessKey, null, keyTransparencyStore)
|
||||
.mapWithCancellation(
|
||||
onSuccess = { RequestResult.Success(Unit) },
|
||||
onError = { throwable ->
|
||||
when (throwable) {
|
||||
is TimeoutException,
|
||||
is ServerSideErrorException,
|
||||
is NetworkException,
|
||||
is NetworkProtocolException -> {
|
||||
RequestResult.RetryableNetworkError(throwable, null)
|
||||
}
|
||||
is RetryLaterException -> {
|
||||
RequestResult.RetryableNetworkError(throwable, throwable.duration)
|
||||
}
|
||||
is VerificationFailedException,
|
||||
is KeyTransparencyException,
|
||||
is AppExpiredException,
|
||||
is IllegalArgumentException -> {
|
||||
RequestResult.NonSuccess(KeyTransparencyError(throwable))
|
||||
}
|
||||
else -> {
|
||||
RequestResult.ApplicationError(throwable)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}.getOrError()
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitors KT to verify recipient. This is an unauthenticated and should only be called following a successful [search].
|
||||
*/
|
||||
suspend fun monitor(monitorMode: KeyTransparency.MonitorMode, aci: ServiceId.Aci, aciIdentityKey: IdentityKey, e164: String, unidentifiedAccessKey: ByteArray, keyTransparencyStore: KeyTransparencyStore): RequestResult<Unit, KeyTransparencyError> {
|
||||
return unauthWebSocket.runCatchingWithUnauthChatConnection { chatConnection ->
|
||||
chatConnection.keyTransparencyClient().monitor(monitorMode, aci, aciIdentityKey, e164, unidentifiedAccessKey, null, keyTransparencyStore)
|
||||
.mapWithCancellation(
|
||||
onSuccess = { RequestResult.Success(Unit) },
|
||||
onError = { throwable ->
|
||||
when (throwable) {
|
||||
is TimeoutException,
|
||||
is ServerSideErrorException,
|
||||
is NetworkException,
|
||||
is NetworkProtocolException -> {
|
||||
RequestResult.RetryableNetworkError(throwable, null)
|
||||
}
|
||||
is RetryLaterException -> {
|
||||
RequestResult.RetryableNetworkError(throwable, throwable.duration)
|
||||
}
|
||||
is VerificationFailedException,
|
||||
is KeyTransparencyException,
|
||||
is AppExpiredException,
|
||||
is IllegalArgumentException -> {
|
||||
RequestResult.NonSuccess(KeyTransparencyError(throwable))
|
||||
}
|
||||
else -> {
|
||||
RequestResult.ApplicationError(throwable)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}.getOrError()
|
||||
}
|
||||
}
|
||||
|
||||
data class KeyTransparencyError(val exception: Throwable) : BadRequestError
|
||||
@@ -225,6 +225,10 @@ class NetworkDependenciesModule(
|
||||
provider.provideSvrBApi(libsignalNetwork)
|
||||
}
|
||||
|
||||
val keyTransparencyApi: KeyTransparencyApi by lazy {
|
||||
provider.provideKeyTransparencyApi(unauthWebSocket)
|
||||
}
|
||||
|
||||
val okHttpClient: OkHttpClient by lazy {
|
||||
OkHttpClient.Builder()
|
||||
.addInterceptor(StandardUserAgentInterceptor())
|
||||
|
||||
@@ -88,6 +88,7 @@ class AccountValues internal constructor(store: KeyValueStore, context: Context)
|
||||
private const val KEY_ACCOUNT_ENTROPY_POOL = "account.account_entropy_pool"
|
||||
private const val KEY_RESTORED_ACCOUNT_ENTROPY_KEY = "account.restored_account_entropy_pool"
|
||||
private const val KEY_RESTORED_ACCOUNT_ENTROPY_KEY_FROM_PRIMARY = "account.restore_account_entropy_pool_primary"
|
||||
private const val KEY_KT_DISTINGUISHED_HEAD = "account.key_transparency_distinguished_head"
|
||||
|
||||
private val AEP_LOCK = ReentrantLock()
|
||||
}
|
||||
@@ -517,6 +518,17 @@ class AccountValues internal constructor(store: KeyValueStore, context: Context)
|
||||
.apply()
|
||||
}
|
||||
|
||||
var distinguishedHead: ByteArray?
|
||||
get() {
|
||||
return getBlob(KEY_KT_DISTINGUISHED_HEAD, null)
|
||||
}
|
||||
set(value) {
|
||||
store
|
||||
.beginWrite()
|
||||
.putBlob(KEY_KT_DISTINGUISHED_HEAD, value)
|
||||
.apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* There are some cases where our username may fall out of sync with the service. In particular, we may get a new value for our username from
|
||||
* storage service but then find that it doesn't match what's on the service.
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
package org.thoughtcrime.securesms.net
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.dependencies.KeyTransparencyApi
|
||||
import org.whispersystems.signalservice.api.account.AccountApi
|
||||
import org.whispersystems.signalservice.api.archive.ArchiveApi
|
||||
import org.whispersystems.signalservice.api.attachment.AttachmentApi
|
||||
@@ -98,4 +99,7 @@ object SignalNetwork {
|
||||
|
||||
val svrB: SvrBApi
|
||||
get() = AppDependencies.svrBApi
|
||||
|
||||
val keyTransparency: KeyTransparencyApi
|
||||
get() = AppDependencies.keyTransparencyApi
|
||||
}
|
||||
|
||||
@@ -120,7 +120,8 @@ class Recipient(
|
||||
private val groupRecord: Optional<GroupRecord> = Optional.empty(),
|
||||
val phoneNumberSharing: PhoneNumberSharingState = PhoneNumberSharingState.UNKNOWN,
|
||||
val nickname: ProfileName = ProfileName.EMPTY,
|
||||
val note: String? = null
|
||||
val note: String? = null,
|
||||
val keyTransparencyData: ByteArray? = null
|
||||
) {
|
||||
|
||||
/** The recipient's [ServiceId], which could be either an [ACI] or [PNI]. */
|
||||
@@ -822,7 +823,8 @@ class Recipient(
|
||||
callLinkRoomId == other.callLinkRoomId &&
|
||||
phoneNumberSharing == other.phoneNumberSharing &&
|
||||
nickname == other.nickname &&
|
||||
note == other.note
|
||||
note == other.note &&
|
||||
keyTransparencyData.contentEquals(other.keyTransparencyData)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
|
||||
@@ -202,7 +202,8 @@ object RecipientCreator {
|
||||
groupRecord = groupRecord,
|
||||
phoneNumberSharing = record.phoneNumberSharing,
|
||||
nickname = record.nickname,
|
||||
note = record.note
|
||||
note = record.note,
|
||||
keyTransparencyData = record.keyTransparencyData
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user