PIN unlock : makes sure to load the pin size from storage

This commit is contained in:
ganfra 2023-10-25 15:07:05 +02:00
parent d59eaab7a8
commit 432e209618
9 changed files with 73 additions and 28 deletions

View file

@ -21,7 +21,7 @@ object LockScreenConfig {
/** /**
* Whether the PIN is mandatory or not. * Whether the PIN is mandatory or not.
*/ */
const val IS_PIN_MANDATORY: Boolean = true const val IS_PIN_MANDATORY: Boolean = false
/** /**
* Some PINs are blacklisted. * Some PINs are blacklisted.

View file

@ -20,7 +20,8 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
@ -58,16 +59,17 @@ fun PinEntryTextField(
) )
} }
@OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
private fun PinEntryRow( private fun PinEntryRow(
pinEntry: PinEntry, pinEntry: PinEntry,
isSecured: Boolean, isSecured: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Row( FlowRow(
modifier = modifier, modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.CenterHorizontally), horizontalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.CenterHorizontally),
verticalAlignment = Alignment.CenterVertically, verticalArrangement = Arrangement.spacedBy(8.dp),
) { ) {
for (digit in pinEntry.digits) { for (digit in pinEntry.digits) {
PinDigitView(digit = digit, isSecured = isSecured) PinDigitView(digit = digit, isSecured = isSecured)
@ -98,7 +100,7 @@ private fun PinDigitView(
) { ) {
if (digit is PinDigit.Filled) { if (digit is PinDigit.Filled) {
val text = if(isSecured) { val text = if (isSecured) {
"" ""
} else { } else {
digit.value.toString() digit.value.toString()

View file

@ -23,8 +23,6 @@ import io.element.android.libraries.cryptography.api.EncryptionResult
import io.element.android.libraries.cryptography.api.SecretKeyProvider import io.element.android.libraries.cryptography.api.SecretKeyProvider
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SingleIn import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
import kotlinx.coroutines.launch
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
import javax.inject.Inject import javax.inject.Inject
@ -52,6 +50,13 @@ class DefaultPinCodeManager @Inject constructor(
return pinCodeStore.hasPinCode() return pinCodeStore.hasPinCode()
} }
override suspend fun getPinCodeSize(): Int {
val encryptedPinCode = pinCodeStore.getEncryptedCode() ?: return 0
val secretKey = secretKeyProvider.getOrCreateKey(SECRET_KEY_ALIAS)
val decryptedPinCode = encryptionDecryptionService.decrypt(secretKey, EncryptionResult.fromBase64(encryptedPinCode))
return decryptedPinCode.size
}
override suspend fun createPinCode(pinCode: String) { override suspend fun createPinCode(pinCode: String) {
val secretKey = secretKeyProvider.getOrCreateKey(SECRET_KEY_ALIAS) val secretKey = secretKeyProvider.getOrCreateKey(SECRET_KEY_ALIAS)
val encryptedPinCode = encryptionDecryptionService.encrypt(secretKey, pinCode.toByteArray()).toBase64() val encryptedPinCode = encryptionDecryptionService.encrypt(secretKey, pinCode.toByteArray()).toBase64()

View file

@ -57,6 +57,11 @@ interface PinCodeManager {
*/ */
suspend fun isPinCodeAvailable(): Boolean suspend fun isPinCodeAvailable(): Boolean
/**
* @return the size of the saved pin code.
*/
suspend fun getPinCodeSize(): Int
/** /**
* Creates a new encrypted pin code. * Creates a new encrypted pin code.
* @param pinCode the clear pin code to create * @param pinCode the clear pin code to create

View file

@ -24,13 +24,13 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import io.element.android.appconfig.LockScreenConfig
import io.element.android.features.lockscreen.impl.pin.PinCodeManager import io.element.android.features.lockscreen.impl.pin.PinCodeManager
import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.pin.model.PinEntry
import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -44,10 +44,10 @@ class PinUnlockPresenter @Inject constructor(
@Composable @Composable
override fun present(): PinUnlockState { override fun present(): PinUnlockState {
var pinEntry by remember { val pinEntryState = remember {
//TODO fetch size from db mutableStateOf<Async<PinEntry>>(Async.Uninitialized)
mutableStateOf(PinEntry.createEmpty(LockScreenConfig.PIN_SIZE))
} }
val pinEntry by pinEntryState
var remainingAttempts by remember { var remainingAttempts by remember {
mutableStateOf<Async<Int>>(Async.Uninitialized) mutableStateOf<Async<Int>>(Async.Uninitialized)
} }
@ -62,11 +62,18 @@ class PinUnlockPresenter @Inject constructor(
mutableStateOf<Async<String?>>(Async.Uninitialized) mutableStateOf<Async<String?>>(Async.Uninitialized)
} }
LaunchedEffect(Unit) {
suspend {
val pinCodeSize = pinCodeManager.getPinCodeSize()
PinEntry.createEmpty(pinCodeSize)
}.runCatchingUpdatingState(pinEntryState)
}
LaunchedEffect(pinEntry) { LaunchedEffect(pinEntry) {
if (pinEntry.isComplete()) { if (pinEntry.isComplete()) {
val isVerified = pinCodeManager.verifyPinCode(pinEntry.toText()) val isVerified = pinCodeManager.verifyPinCode(pinEntry.toText())
if (!isVerified) { if (!isVerified) {
pinEntry = pinEntry.clear() pinEntryState.value = pinEntry.clear()
showWrongPinTitle = true showWrongPinTitle = true
} }
} }
@ -80,7 +87,7 @@ class PinUnlockPresenter @Inject constructor(
fun handleEvents(event: PinUnlockEvents) { fun handleEvents(event: PinUnlockEvents) {
when (event) { when (event) {
is PinUnlockEvents.OnPinKeypadPressed -> { is PinUnlockEvents.OnPinKeypadPressed -> {
pinEntry = pinEntry.process(event.pinKeypadModel) pinEntryState.value = pinEntry.process(event.pinKeypadModel)
} }
PinUnlockEvents.OnForgetPin -> showSignOutPrompt = true PinUnlockEvents.OnForgetPin -> showSignOutPrompt = true
PinUnlockEvents.ClearSignOutPrompt -> showSignOutPrompt = false PinUnlockEvents.ClearSignOutPrompt -> showSignOutPrompt = false
@ -103,17 +110,38 @@ class PinUnlockPresenter @Inject constructor(
) )
} }
private fun Async<PinEntry>.isComplete(): Boolean {
return dataOrNull()?.isComplete().orFalse()
}
private fun Async<PinEntry>.toText(): String {
return dataOrNull()?.toText() ?: ""
}
private fun Async<PinEntry>.clear(): Async<PinEntry> {
return when (this) {
is Async.Success -> Async.Success(data.clear())
else -> this
}
}
private fun Async<PinEntry>.process(pinKeypadModel: PinKeypadModel): Async<PinEntry> {
return when (this) {
is Async.Success -> {
val pinEntry = when (pinKeypadModel) {
PinKeypadModel.Back -> data.deleteLast()
is PinKeypadModel.Number -> data.addDigit(pinKeypadModel.number)
PinKeypadModel.Empty -> data
}
Async.Success(pinEntry)
}
else -> this
}
}
private fun CoroutineScope.signOut(signOutAction: MutableState<Async<String?>>) = launch { private fun CoroutineScope.signOut(signOutAction: MutableState<Async<String?>>) = launch {
suspend { suspend {
matrixClient.logout() matrixClient.logout()
}.runCatchingUpdatingState(signOutAction) }.runCatchingUpdatingState(signOutAction)
} }
private fun PinEntry.process(pinKeypadModel: PinKeypadModel): PinEntry {
return when (pinKeypadModel) {
PinKeypadModel.Back -> deleteLast()
is PinKeypadModel.Number -> addDigit(pinKeypadModel.number)
PinKeypadModel.Empty -> this
}
}
} }

View file

@ -20,7 +20,7 @@ import io.element.android.features.lockscreen.impl.pin.model.PinEntry
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
data class PinUnlockState( data class PinUnlockState(
val pinEntry: PinEntry, val pinEntry: Async<PinEntry>,
val showWrongPinTitle: Boolean, val showWrongPinTitle: Boolean,
val remainingAttempts: Async<Int>, val remainingAttempts: Async<Int>,
val showSignOutPrompt: Boolean, val showSignOutPrompt: Boolean,

View file

@ -39,7 +39,7 @@ fun aPinUnlockState(
showSignOutPrompt: Boolean = false, showSignOutPrompt: Boolean = false,
signOutAction: Async<String?> = Async.Uninitialized, signOutAction: Async<String?> = Async.Uninitialized,
) = PinUnlockState( ) = PinUnlockState(
pinEntry = pinEntry, pinEntry = Async.Success(pinEntry),
showWrongPinTitle = showWrongPinTitle, showWrongPinTitle = showWrongPinTitle,
remainingAttempts = Async.Success(remainingAttempts), remainingAttempts = Async.Success(remainingAttempts),
showSignOutPrompt = showSignOutPrompt, showSignOutPrompt = showSignOutPrompt,

View file

@ -258,7 +258,7 @@ private fun PinUnlockHeader(
if (state.showWrongPinTitle) { if (state.showWrongPinTitle) {
pluralStringResource(id = R.plurals.screen_app_lock_subtitle_wrong_pin, count = remainingAttempts, remainingAttempts) pluralStringResource(id = R.plurals.screen_app_lock_subtitle_wrong_pin, count = remainingAttempts, remainingAttempts)
} else { } else {
stringResource(id = R.string.screen_app_lock_subtitle) pluralStringResource(id = R.plurals.screen_app_lock_subtitle, count = remainingAttempts, remainingAttempts)
} }
} else { } else {
"" ""
@ -276,14 +276,16 @@ private fun PinUnlockHeader(
color = subtitleColor, color = subtitleColor,
) )
Spacer(Modifier.height(24.dp)) Spacer(Modifier.height(24.dp))
PinDotsRow(state.pinEntry) if (state.pinEntry is Async.Success) {
PinDotsRow(state.pinEntry.data)
}
} }
} }
@Composable @Composable
private fun PinUnlockFooter( private fun PinUnlockFooter(
onUseBiometric: ()->Unit, onUseBiometric: () -> Unit,
onForgotPin: ()->Unit, onForgotPin: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Row(modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceAround) { Row(modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceAround) {

View file

@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"You have %1$d attempt to unlock"</item>
<item quantity="other">"You have %1$d attempts to unlock"</item>
</plurals>
<plurals name="screen_app_lock_subtitle_wrong_pin"> <plurals name="screen_app_lock_subtitle_wrong_pin">
<item quantity="one">"Wrong PIN. You have %1$d more chance"</item> <item quantity="one">"Wrong PIN. You have %1$d more chance"</item>
<item quantity="other">"Wrong PIN. You have %1$d more chances"</item> <item quantity="other">"Wrong PIN. You have %1$d more chances"</item>
@ -26,6 +30,5 @@ Choose something memorable. If you forget this PIN, you will be logged out of th
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PINs don\'t match"</string> <string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PINs don\'t match"</string>
<string name="screen_app_lock_signout_alert_message">"Youll need to re-login and create a new PIN to proceed"</string> <string name="screen_app_lock_signout_alert_message">"Youll need to re-login and create a new PIN to proceed"</string>
<string name="screen_app_lock_signout_alert_title">"You are being signed out"</string> <string name="screen_app_lock_signout_alert_title">"You are being signed out"</string>
<string name="screen_app_lock_subtitle">"You have 3 attempts to unlock"</string>
<string name="screen_signout_in_progress_dialog_content">"Signing out…"</string> <string name="screen_signout_in_progress_dialog_content">"Signing out…"</string>
</resources> </resources>