From 1d314e198a0eb9c25c898aa9102dca4605d135d8 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 25 Oct 2023 16:45:42 +0200 Subject: [PATCH] PIN: clean pin code storage --- features/lockscreen/impl/build.gradle.kts | 1 + .../impl/pin/storage/PinCodeStore.kt | 17 +-- .../pin/storage/PreferencesPinCodeStore.kt | 89 +++++++++++++++ .../storage/SharedPreferencesPinCodeStore.kt | 101 ------------------ .../impl/pin/storage/InMemoryPinCodeStore.kt | 12 +-- 5 files changed, 93 insertions(+), 127 deletions(-) create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PreferencesPinCodeStore.kt delete mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/SharedPreferencesPinCodeStore.kt diff --git a/features/lockscreen/impl/build.gradle.kts b/features/lockscreen/impl/build.gradle.kts index 05e9ce20f3..fdb9dcd178 100644 --- a/features/lockscreen/impl/build.gradle.kts +++ b/features/lockscreen/impl/build.gradle.kts @@ -45,6 +45,7 @@ dependencies { implementation(projects.libraries.uiStrings) implementation(projects.libraries.sessionStorage.api) implementation(projects.services.appnavstate.api) + implementation(libs.androidx.datastore.preferences) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PinCodeStore.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PinCodeStore.kt index e72cbca2db..818bc6a47d 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PinCodeStore.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PinCodeStore.kt @@ -18,10 +18,6 @@ package io.element.android.features.lockscreen.impl.pin.storage interface PinCodeStore : EncryptedPinCodeStorage { - interface Listener { - fun onPinSetUpChange(isConfigured: Boolean) - } - /** * Returns the remaining PIN code attempts. When this reaches 0 the PIN code access won't be available for some time. */ @@ -29,24 +25,13 @@ interface PinCodeStore : EncryptedPinCodeStorage { /** * Should decrement the number of remaining PIN code attempts. - * @return The remaining attempts. */ - suspend fun onWrongPin(): Int + suspend fun onWrongPin() /** * Resets the counter of attempts for PIN code and biometric access. */ suspend fun resetCounter() - - /** - * Adds a listener to be notified when the PIN code us created or removed. - */ - fun addListener(listener: Listener) - - /** - * Removes a listener to be notified when the PIN code us created or removed. - */ - fun removeListener(listener: Listener) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PreferencesPinCodeStore.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PreferencesPinCodeStore.kt new file mode 100644 index 0000000000..2bf9225a2c --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PreferencesPinCodeStore.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.lockscreen.impl.pin.storage + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.appconfig.LockScreenConfig +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.SingleIn +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +private val Context.dataStore: DataStore by preferencesDataStore(name = "pin_code_store") + +@SingleIn(AppScope::class) +@ContributesBinding(AppScope::class) +class PreferencesPinCodeStore @Inject constructor( + @ApplicationContext private val context: Context, +) : PinCodeStore { + + private val pinCodeKey = stringPreferencesKey("encoded_pin_code") + private val remainingAttemptsKey = intPreferencesKey("remaining_pin_code_attempts") + + override suspend fun getRemainingPinCodeAttemptsNumber(): Int { + return context.dataStore.data.map { preferences -> + preferences[remainingAttemptsKey] ?: 0 + }.first() + } + + override suspend fun onWrongPin() { + context.dataStore.edit { preferences -> + val current = preferences[remainingAttemptsKey] ?: 0 + val remaining = (current - 1).coerceAtLeast(0) + preferences[remainingAttemptsKey] = remaining + } + } + + override suspend fun resetCounter() { + context.dataStore.edit { preferences -> + preferences[remainingAttemptsKey] = LockScreenConfig.MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT + } + } + + override suspend fun getEncryptedCode(): String? { + return context.dataStore.data.map { preferences -> + preferences[pinCodeKey] + }.first() + } + + override suspend fun saveEncryptedPinCode(pinCode: String) { + context.dataStore.edit { preferences -> + preferences[pinCodeKey] = pinCode + } + } + + override suspend fun deleteEncryptedPinCode() { + context.dataStore.edit { preferences -> + preferences.remove(pinCodeKey) + } + } + + override suspend fun hasPinCode(): Boolean { + return context.dataStore.data.map { preferences -> + preferences[pinCodeKey] != null + }.first() + } +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/SharedPreferencesPinCodeStore.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/SharedPreferencesPinCodeStore.kt deleted file mode 100644 index db84282ebc..0000000000 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/SharedPreferencesPinCodeStore.kt +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (c) 2023 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.features.lockscreen.impl.pin.storage - -import android.content.SharedPreferences -import androidx.core.content.edit -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.appconfig.LockScreenConfig -import io.element.android.libraries.core.coroutine.CoroutineDispatchers -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.DefaultPreferences -import io.element.android.libraries.di.SingleIn -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext -import java.util.concurrent.CopyOnWriteArrayList -import javax.inject.Inject - -private const val ENCODED_PIN_CODE_KEY = "ENCODED_PIN_CODE_KEY" -private const val REMAINING_PIN_CODE_ATTEMPTS_KEY = "REMAINING_PIN_CODE_ATTEMPTS_KEY" - -@SingleIn(AppScope::class) -@ContributesBinding(AppScope::class) -class SharedPreferencesPinCodeStore @Inject constructor( - private val dispatchers: CoroutineDispatchers, - @DefaultPreferences private val sharedPreferences: SharedPreferences, -) : PinCodeStore { - - private val listeners = CopyOnWriteArrayList() - private val mutex = Mutex() - - override suspend fun getEncryptedCode(): String? = withContext(dispatchers.io) { - sharedPreferences.getString(ENCODED_PIN_CODE_KEY, null) - } - - override suspend fun saveEncryptedPinCode(pinCode: String) = withContext(dispatchers.io) { - sharedPreferences.edit { - putString(ENCODED_PIN_CODE_KEY, pinCode) - } - withContext(dispatchers.main) { - listeners.forEach { it.onPinSetUpChange(isConfigured = true) } - } - } - - override suspend fun deleteEncryptedPinCode() = withContext(dispatchers.io) { - sharedPreferences.edit { - remove(ENCODED_PIN_CODE_KEY) - } - withContext(dispatchers.main) { - listeners.forEach { it.onPinSetUpChange(isConfigured = false) } - } - } - - override suspend fun hasPinCode(): Boolean = withContext(dispatchers.io) { - sharedPreferences.contains(ENCODED_PIN_CODE_KEY) - } - - override suspend fun getRemainingPinCodeAttemptsNumber(): Int = withContext(dispatchers.io) { - sharedPreferences.getInt(REMAINING_PIN_CODE_ATTEMPTS_KEY, LockScreenConfig.MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT) - } - - override suspend fun onWrongPin(): Int = withContext(dispatchers.io) { - mutex.withLock { - val remaining = (getRemainingPinCodeAttemptsNumber() - 1).coerceAtLeast(0) - sharedPreferences.edit { - putInt(REMAINING_PIN_CODE_ATTEMPTS_KEY, remaining) - } - remaining - } - } - - override suspend fun resetCounter() = withContext(dispatchers.io) { - mutex.withLock { - sharedPreferences.edit { - remove(REMAINING_PIN_CODE_ATTEMPTS_KEY) - } - } - } - - override fun addListener(listener: PinCodeStore.Listener) { - listeners.add(listener) - } - - override fun removeListener(listener: PinCodeStore.Listener) { - listeners.remove(listener) - } -} diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/storage/InMemoryPinCodeStore.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/storage/InMemoryPinCodeStore.kt index 0b7c2f256b..6b6597728c 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/storage/InMemoryPinCodeStore.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/storage/InMemoryPinCodeStore.kt @@ -27,22 +27,14 @@ class InMemoryPinCodeStore : PinCodeStore { return remainingAttempts } - override suspend fun onWrongPin(): Int { - return remainingAttempts-- + override suspend fun onWrongPin() { + remainingAttempts-- } override suspend fun resetCounter() { remainingAttempts = DEFAULT_REMAINING_ATTEMPTS } - override fun addListener(listener: PinCodeStore.Listener) { - // no-op - } - - override fun removeListener(listener: PinCodeStore.Listener) { - // no-op - } - override suspend fun getEncryptedCode(): String? { return pinCode }