mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 13:08:46 +00:00
Fix issue with using registration recovery password.
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 -> {
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user