Fix issue with using registration recovery password.

This commit is contained in:
Greyson Parrelli
2024-12-06 15:59:46 -05:00
committed by GitHub
parent 6824f09631
commit 014218782f
10 changed files with 150 additions and 80 deletions

View File

@@ -54,11 +54,11 @@ import org.thoughtcrime.securesms.jobs.StorageForcePushJob
import org.thoughtcrime.securesms.jobs.StorageSyncJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.kbs.MasterKey
import org.whispersystems.signalservice.api.storage.RecordIkm
import org.whispersystems.signalservice.api.storage.SignalStorageManifest
import org.whispersystems.signalservice.api.storage.SignalStorageRecord
import org.whispersystems.signalservice.api.storage.StorageId
import org.whispersystems.signalservice.api.storage.StorageKey
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord
import org.whispersystems.signalservice.internal.storage.protos.StorageRecord
@@ -171,12 +171,12 @@ fun ToolScreen(
SignalStore.storageService.manifest = SignalStorageManifest.EMPTY
}
ActionRow("Set initial storage key", "Initializes it to something random. Will cause a decryption failure.") {
SignalStore.storageService.storageKeyForInitialDataRestore = StorageKey(Util.getSecretBytes(32))
ActionRow("Set initial master key", "Initializes it to something random. Will cause a decryption failure.") {
SignalStore.svr.masterKeyForInitialDataRestore = MasterKey(Util.getSecretBytes(32))
}
ActionRow("Clear initial storage key", "Sets it to null.") {
SignalStore.storageService.storageKeyForInitialDataRestore = null
ActionRow("Clear initial master key", "Sets it to null.") {
SignalStore.svr.masterKeyForInitialDataRestore = null
}
Rows.ToggleRow(

View File

@@ -124,7 +124,7 @@ class StorageForcePushJob private constructor(parameters: Parameters) : BaseJob(
Log.i(TAG, "Force push succeeded. Updating local manifest version to: $newVersion")
SignalStore.storageService.manifest = manifest
SignalStore.storageService.storageKeyForInitialDataRestore = null
SignalStore.svr.masterKeyForInitialDataRestore = null
SignalDatabase.recipients.applyStorageIdUpdates(newContactStorageIds)
SignalDatabase.recipients.applyStorageIdUpdates(Collections.singletonMap(Recipient.self().id, accountRecord.id))
SignalDatabase.unknownStorageIds.deleteAll()

View File

@@ -89,7 +89,7 @@ class StorageRotateManifestJob private constructor(parameters: Parameters) : Job
return when (val result = repository.writeUnchangedManifest(storageServiceKey, manifestWithNewVersion)) {
StorageServiceRepository.WriteStorageRecordsResult.Success -> {
Log.i(TAG, "Successfully rotated the manifest as version ${manifestWithNewVersion.version}.${manifestWithNewVersion.sourceDeviceId}. Clearing restore key.")
SignalStore.storageService.storageKeyForInitialDataRestore = null
SignalStore.svr.masterKeyForInitialDataRestore = null
Result.success()
}
StorageServiceRepository.WriteStorageRecordsResult.ConflictError -> {

View File

@@ -369,7 +369,7 @@ class StorageSyncJob private constructor(parameters: Parameters) : BaseJob(param
Log.i(TAG, "Saved new manifest. Now at version: ${remoteWriteOperation.manifest.versionString}")
SignalStore.storageService.manifest = remoteWriteOperation.manifest
SignalStore.storageService.storageKeyForInitialDataRestore = null
SignalStore.svr.masterKeyForInitialDataRestore = null
stopwatch.split("remote-write")

View File

@@ -15,7 +15,6 @@ class StorageServiceValues internal constructor(store: KeyValueStore) : SignalSt
// TODO [linked-device] No need to track this separately -- we'd get the AEP from the primary
private const val SYNC_STORAGE_KEY = "storage.syncStorageKey"
private const val INITIAL_RESTORE_STORAGE_KEY = "storage.initialRestoreStorageKey"
}
public override fun onFirstEverAppLaunch() = Unit
@@ -63,26 +62,8 @@ class StorageServiceValues internal constructor(store: KeyValueStore) : SignalSt
/**
* The [StorageKey] that should be used for our initial storage service data restore.
* The presence of this value indicates that it hasn't been used yet.
* Once there has been *any* write to storage service, this value needs to be cleared.
* Once there has been *any* write to storage service, [SvrValues.masterKeyForInitialDataRestore] needs to be cleared.
*/
@get:Synchronized
@set:Synchronized
var storageKeyForInitialDataRestore: StorageKey?
get() {
return getBlob(INITIAL_RESTORE_STORAGE_KEY, null)?.let { StorageKey(it) }
}
set(value) {
if (value != storageKeyForInitialDataRestore) {
if (value == storageKey) {
Log.w(TAG, "The key already matches the one derived from the AEP! All good, no need to store it.")
store.beginWrite().putBlob(INITIAL_RESTORE_STORAGE_KEY, null).commit()
} else if (value != null) {
Log.w(TAG, "Setting initial restore key!", Throwable())
store.beginWrite().putBlob(INITIAL_RESTORE_STORAGE_KEY, value.serialize()).commit()
} else {
Log.w(TAG, "Clearing initial restore key!", Throwable())
store.beginWrite().putBlob(INITIAL_RESTORE_STORAGE_KEY, null).commit()
}
}
}
val storageKeyForInitialDataRestore: StorageKey?
get() = SignalStore.svr.masterKeyForInitialDataRestore?.deriveStorageServiceKey()
}

View File

@@ -20,6 +20,7 @@ class SvrValues internal constructor(store: KeyValueStore) : SignalStoreValues(s
private const val SVR_LAST_AUTH_REFRESH_TIMESTAMP = "kbs.kbs_auth_tokens.last_refresh_timestamp"
private const val SVR3_AUTH_TOKENS = "kbs.svr3_auth_tokens"
private const val RESTORED_VIA_ACCOUNT_ENTROPY_KEY = "kbs.restore_via_account_entropy_pool"
private const val INITIAL_RESTORE_MASTER_KEY = "kbs.initialRestoreMasterKey"
}
public override fun onFirstEverAppLaunch() = Unit
@@ -83,6 +84,32 @@ class SvrValues internal constructor(store: KeyValueStore) : SignalStoreValues(s
val masterKey: MasterKey
get() = SignalStore.account.accountEntropyPool.deriveMasterKey()
/**
* The [MasterKey] that should be used for our initial syncs with storage service + recovery password.
* The presence of this value indicates that it hasn't been used yet for storage service.
* Once there has been *any* write to storage service, this value needs to be cleared.
*/
@get:Synchronized
@set:Synchronized
var masterKeyForInitialDataRestore: MasterKey?
get() {
return getBlob(INITIAL_RESTORE_MASTER_KEY, null)?.let { MasterKey(it) }
}
set(value) {
if (value != masterKeyForInitialDataRestore) {
if (value == masterKey) {
Log.w(TAG, "The master key already matches the one derived from the AEP! All good, no need to store it.")
store.beginWrite().putBlob(INITIAL_RESTORE_MASTER_KEY, null).commit()
} else if (value != null) {
Log.w(TAG, "Setting initial restore master key!", Throwable())
store.beginWrite().putBlob(INITIAL_RESTORE_MASTER_KEY, value.serialize()).commit()
} else {
Log.w(TAG, "Clearing initial restore master key!", Throwable())
store.beginWrite().putBlob(INITIAL_RESTORE_MASTER_KEY, null).commit()
}
}
}
@get:Synchronized
val pinBackedMasterKey: MasterKey?
/** Returns null if master key is not backed up by a pin. */
@@ -102,7 +129,7 @@ class SvrValues internal constructor(store: KeyValueStore) : SignalStoreValues(s
val recoveryPassword: String?
get() {
return if (hasOptedInWithAccess()) {
masterKey.deriveRegistrationRecoveryPassword()
masterKeyForInitialDataRestore?.deriveRegistrationRecoveryPassword() ?: masterKey.deriveRegistrationRecoveryPassword()
} else {
null
}

View File

@@ -168,7 +168,7 @@ object SvrRepository {
SignalStore.registration.localRegistrationMetadata = metadata.copy(masterKey = response.masterKey.serialize().toByteString(), pin = userPin)
}
SignalStore.storageService.storageKeyForInitialDataRestore = response.masterKey.deriveStorageServiceKey()
SignalStore.svr.masterKeyForInitialDataRestore = response.masterKey
SignalStore.svr.setPin(userPin)
SignalStore.svr.isRegistrationLockEnabled = false
SignalStore.pin.resetPinReminders()
@@ -321,14 +321,14 @@ object SvrRepository {
Log.i(TAG, "[onRegistrationComplete] ReRegistration Skip SMS", true)
}
SignalStore.storageService.storageKeyForInitialDataRestore = masterKey.deriveStorageServiceKey()
SignalStore.svr.masterKeyForInitialDataRestore = masterKey
SignalStore.svr.setPin(userPin)
SignalStore.pin.resetPinReminders()
AppDependencies.jobManager.add(ResetSvrGuessCountJob())
} else if (masterKey != null) {
Log.i(TAG, "[onRegistrationComplete] ReRegistered with key without pin")
SignalStore.storageService.storageKeyForInitialDataRestore = masterKey.deriveStorageServiceKey()
SignalStore.svr.masterKeyForInitialDataRestore = masterKey
} else if (hasPinToRestore) {
Log.i(TAG, "[onRegistrationComplete] Has a PIN to restore.", true)
SignalStore.svr.clearRegistrationLockAndPin()

View File

@@ -275,8 +275,6 @@ object RegistrationRepository {
withContext(Dispatchers.IO) {
val credentialSet = SvrAuthCredentialSet(svr2Credentials = svr2Credentials, svr3Credentials = svr3Credentials)
val masterKey = SvrRepository.restoreMasterKeyPreRegistration(credentialSet, pin)
SignalStore.storageService.storageKeyForInitialDataRestore = masterKey.deriveStorageServiceKey()
SignalStore.svr.setPin(pin)
return@withContext masterKey
}

View File

@@ -19,6 +19,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.flow.updateAndGet
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.signal.core.util.Base64
import org.signal.core.util.Stopwatch
import org.signal.core.util.isNotNullOrBlank
import org.signal.core.util.logging.Log
@@ -66,9 +67,12 @@ import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.dualsim.MccMncProducer
import org.whispersystems.signalservice.api.SvrNoDataException
import org.whispersystems.signalservice.api.kbs.MasterKey
import org.whispersystems.signalservice.api.svr.Svr3Credentials
import org.whispersystems.signalservice.internal.push.AuthCredentials
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataJson
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse
import java.io.IOException
import java.nio.charset.StandardCharsets
import java.util.concurrent.TimeUnit
import kotlin.jvm.optionals.getOrNull
import kotlin.time.Duration.Companion.minutes
@@ -615,31 +619,18 @@ class RegistrationViewModel : ViewModel() {
fun verifyReRegisterWithPin(context: Context, pin: String, wrongPinHandler: () -> Unit) {
setInProgress(true)
// Local recovery password
if (RegistrationRepository.canUseLocalRecoveryPassword()) {
if (RegistrationRepository.doesPinMatchLocalHash(pin)) {
Log.d(TAG, "Found recovery password, attempting to re-register.")
viewModelScope.launch(context = coroutineExceptionHandler) {
verifyReRegisterInternal(context, pin, SignalStore.svr.masterKey)
setInProgress(false)
}
} else {
Log.d(TAG, "Entered PIN did not match local PIN hash.")
wrongPinHandler()
setInProgress(false)
}
return
}
// remote recovery password
val svr2Credentials = store.value.svr2AuthCredentials
val svr3Credentials = store.value.svr3AuthCredentials
val svr2Credentials = store.value.svr2AuthCredentials ?: SignalStore.svr.svr2AuthTokens.toSvrCredentials()
val svr3Credentials = store.value.svr3AuthCredentials ?: SignalStore.svr.svr3AuthTokens.toSvrCredentials()?.let { Svr3Credentials(it.username(), it.password(), null) }
if (svr2Credentials != null || svr3Credentials != null) {
Log.d(TAG, "Found SVR auth credentials, fetching recovery password from SVR (svr2: ${svr2Credentials != null}, svr3: ${svr3Credentials != null}).")
viewModelScope.launch(context = coroutineExceptionHandler) {
try {
val masterKey = RegistrationRepository.fetchMasterKeyFromSvrRemote(pin, svr2Credentials, svr3Credentials)
SignalStore.svr.masterKeyForInitialDataRestore = masterKey
SignalStore.svr.setPin(pin)
setRecoveryPassword(masterKey.deriveRegistrationRecoveryPassword())
updateSvrTriesRemaining(10)
verifyReRegisterInternal(context, pin, masterKey)
@@ -657,6 +648,22 @@ class RegistrationViewModel : ViewModel() {
return
}
// Local recovery password
if (RegistrationRepository.canUseLocalRecoveryPassword()) {
if (RegistrationRepository.doesPinMatchLocalHash(pin)) {
Log.d(TAG, "Found recovery password, attempting to re-register.")
viewModelScope.launch(context = coroutineExceptionHandler) {
verifyReRegisterInternal(context, pin, SignalStore.svr.masterKey)
setInProgress(false)
}
} else {
Log.d(TAG, "Entered PIN did not match local PIN hash.")
wrongPinHandler()
setInProgress(false)
}
return
}
Log.w(TAG, "Could not get credentials to skip SMS registration, aborting!")
store.update {
it.copy(canSkipSms = false, inProgress = false)
@@ -917,6 +924,31 @@ class RegistrationViewModel : ViewModel() {
setInProgress(false)
}
/** Converts the basic-auth creds we have locally into username:password pairs that are suitable for handing off to the service. */
private fun List<String?>.toSvrCredentials(): AuthCredentials? {
return this
.asSequence()
.filterNotNull()
.map { it.replace("Basic ", "").trim() }
.mapNotNull {
try {
Base64.decode(it)
} catch (e: IOException) {
Log.w(TAG, "Encountered error trying to decode a token!", e)
null
}
}
.map { String(it, StandardCharsets.ISO_8859_1) }
.mapNotNull {
val colonIndex = it.indexOf(":")
if (colonIndex < 0) {
return@mapNotNull null
}
AuthCredentials.create(it.substring(0, colonIndex), it.substring(colonIndex + 1))
}
.firstOrNull()
}
companion object {
private val TAG = Log.tag(RegistrationViewModel::class.java)

View File

@@ -21,6 +21,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.flow.updateAndGet
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.signal.core.util.Base64
import org.signal.core.util.Stopwatch
import org.signal.core.util.isNotNullOrBlank
import org.signal.core.util.logging.Log
@@ -71,8 +72,11 @@ import org.thoughtcrime.securesms.util.dualsim.MccMncProducer
import org.whispersystems.signalservice.api.AccountEntropyPool
import org.whispersystems.signalservice.api.SvrNoDataException
import org.whispersystems.signalservice.api.kbs.MasterKey
import org.whispersystems.signalservice.api.svr.Svr3Credentials
import org.whispersystems.signalservice.internal.push.AuthCredentials
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse
import java.io.IOException
import java.nio.charset.StandardCharsets
import java.util.concurrent.TimeUnit
import kotlin.jvm.optionals.getOrNull
import kotlin.time.Duration.Companion.minutes
@@ -621,6 +625,35 @@ class RegistrationViewModel : ViewModel() {
fun verifyReRegisterWithPin(context: Context, pin: String, wrongPinHandler: () -> Unit) {
setInProgress(true)
// remote recovery password
val svr2Credentials = store.value.svr2AuthCredentials ?: SignalStore.svr.svr2AuthTokens.toSvrCredentials()
val svr3Credentials = store.value.svr3AuthCredentials ?: SignalStore.svr.svr3AuthTokens.toSvrCredentials()?.let { Svr3Credentials(it.username(), it.password(), null) }
if (svr2Credentials != null || svr3Credentials != null) {
Log.d(TAG, "Found SVR auth credentials, fetching recovery password from SVR (svr2: ${svr2Credentials != null}, svr3: ${svr3Credentials != null}).")
viewModelScope.launch(context = coroutineExceptionHandler) {
try {
val masterKey = RegistrationRepository.fetchMasterKeyFromSvrRemote(pin, svr2Credentials, svr3Credentials)
SignalStore.svr.masterKeyForInitialDataRestore = masterKey
SignalStore.svr.setPin(pin)
setRecoveryPassword(masterKey.deriveRegistrationRecoveryPassword())
updateSvrTriesRemaining(10)
verifyReRegisterInternal(context, pin, masterKey)
} catch (rejectedPin: SvrWrongPinException) {
Log.w(TAG, "Submitted PIN was rejected by SVR.", rejectedPin)
updateSvrTriesRemaining(rejectedPin.triesRemaining)
wrongPinHandler()
} catch (noData: SvrNoDataException) {
Log.w(TAG, "SVR has no data for these credentials. Aborting skip SMS flow.", noData)
updateSvrTriesRemaining(0)
setUserSkippedReRegisterFlow(true)
}
setInProgress(false)
}
return
}
// Local recovery password
if (RegistrationRepository.canUseLocalRecoveryPassword()) {
if (RegistrationRepository.doesPinMatchLocalHash(pin)) {
@@ -639,32 +672,6 @@ class RegistrationViewModel : ViewModel() {
return
}
// remote recovery password
val svr2Credentials = store.value.svr2AuthCredentials
val svr3Credentials = store.value.svr3AuthCredentials
if (svr2Credentials != null || svr3Credentials != null) {
Log.d(TAG, "Found SVR auth credentials, fetching recovery password from SVR (svr2: ${svr2Credentials != null}, svr3: ${svr3Credentials != null}).")
viewModelScope.launch(context = coroutineExceptionHandler) {
try {
val masterKey = RegistrationRepository.fetchMasterKeyFromSvrRemote(pin, svr2Credentials, svr3Credentials)
setRecoveryPassword(masterKey.deriveRegistrationRecoveryPassword())
updateSvrTriesRemaining(10)
verifyReRegisterInternal(context, pin, masterKey)
} catch (rejectedPin: SvrWrongPinException) {
Log.w(TAG, "Submitted PIN was rejected by SVR.", rejectedPin)
updateSvrTriesRemaining(rejectedPin.triesRemaining)
wrongPinHandler()
} catch (noData: SvrNoDataException) {
Log.w(TAG, "SVR has no data for these credentials. Aborting skip SMS flow.", noData)
updateSvrTriesRemaining(0)
setUserSkippedReRegisterFlow(true)
}
setInProgress(false)
}
return
}
Log.w(TAG, "Could not get credentials to skip SMS registration, aborting!")
store.update {
it.copy(canSkipSms = false, inProgress = false)
@@ -946,6 +953,31 @@ class RegistrationViewModel : ViewModel() {
}
}
/** Converts the basic-auth creds we have locally into username:password pairs that are suitable for handing off to the service. */
private fun List<String?>.toSvrCredentials(): AuthCredentials? {
return this
.asSequence()
.filterNotNull()
.map { it.replace("Basic ", "").trim() }
.mapNotNull {
try {
Base64.decode(it)
} catch (e: IOException) {
Log.w(TAG, "Encountered error trying to decode a token!", e)
null
}
}
.map { String(it, StandardCharsets.ISO_8859_1) }
.mapNotNull {
val colonIndex = it.indexOf(":")
if (colonIndex < 0) {
return@mapNotNull null
}
AuthCredentials.create(it.substring(0, colonIndex), it.substring(colonIndex + 1))
}
.firstOrNull()
}
companion object {
private val TAG = Log.tag(RegistrationViewModel::class.java)