Browse Source

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

pull/1642/head
ganfra 11 months ago
parent
commit
49e2060961
  1. 2
      appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt
  2. 10
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt
  3. 9
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt
  4. 5
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt
  5. 56
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt
  6. 2
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt
  7. 2
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt
  8. 10
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt
  9. 5
      features/lockscreen/impl/src/main/res/values/localazy.xml

2
appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt

@ -21,7 +21,7 @@ object LockScreenConfig { @@ -21,7 +21,7 @@ object LockScreenConfig {
/**
* 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.

10
features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt

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

9
features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt

@ -23,8 +23,6 @@ import io.element.android.libraries.cryptography.api.EncryptionResult @@ -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.di.AppScope
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 javax.inject.Inject
@ -52,6 +50,13 @@ class DefaultPinCodeManager @Inject constructor( @@ -52,6 +50,13 @@ class DefaultPinCodeManager @Inject constructor(
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) {
val secretKey = secretKeyProvider.getOrCreateKey(SECRET_KEY_ALIAS)
val encryptedPinCode = encryptionDecryptionService.encrypt(secretKey, pinCode.toByteArray()).toBase64()

5
features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt

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

56
features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt

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

2
features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt

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

2
features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt

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

10
features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt

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

5
features/lockscreen/impl/src/main/res/values/localazy.xml

@ -1,5 +1,9 @@ @@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<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">
<item quantity="one">"Wrong PIN. You have %1$d more chance"</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 @@ -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_signout_alert_message">"You’ll 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_subtitle">"You have 3 attempts to unlock"</string>
<string name="screen_signout_in_progress_dialog_content">"Signing out…"</string>
</resources>

Loading…
Cancel
Save