diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index 82786b1a0c..2f7eabb582 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -50,9 +50,9 @@ import io.element.android.features.ftue.api.state.FtueState import io.element.android.features.invitelist.api.InviteListEntryPoint import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkStatus -import io.element.android.features.pin.api.PinEntryPoint -import io.element.android.features.pin.api.PinState -import io.element.android.features.pin.api.PinStateService +import io.element.android.features.lockscreen.api.LockScreenEntryPoint +import io.element.android.features.lockscreen.api.LockScreenState +import io.element.android.features.lockscreen.api.LockScreenStateService import io.element.android.features.preferences.api.PreferencesEntryPoint import io.element.android.features.roomlist.api.RoomListEntryPoint import io.element.android.features.verifysession.api.VerifySessionEntryPoint @@ -93,8 +93,8 @@ class LoggedInFlowNode @AssistedInject constructor( private val networkMonitor: NetworkMonitor, private val notificationDrawerManager: NotificationDrawerManager, private val ftueState: FtueState, - private val pinEntryPoint: PinEntryPoint, - private val pinStateService: PinStateService, + private val lockScreenEntryPoint: LockScreenEntryPoint, + private val lockScreenStateService: LockScreenStateService, private val matrixClient: MatrixClient, snackbarDispatcher: SnackbarDispatcher, ) : BackstackNode( @@ -136,12 +136,12 @@ class LoggedInFlowNode @AssistedInject constructor( }, onResume = { coroutineScope.launch { - pinStateService.entersForeground() + lockScreenStateService.entersForeground() } }, onPause = { coroutineScope.launch { - pinStateService.entersBackground() + lockScreenStateService.entersBackground() } }, onStop = { @@ -218,7 +218,7 @@ class LoggedInFlowNode @AssistedInject constructor( createNode(buildContext) } NavTarget.LockPermanent -> { - pinEntryPoint.createNode(this, buildContext) + lockScreenEntryPoint.createNode(this, buildContext) } NavTarget.RoomList -> { val callback = object : RoomListEntryPoint.Callback { @@ -345,9 +345,9 @@ class LoggedInFlowNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { Box(modifier = modifier) { - val pinState by pinStateService.pinState.collectAsState() - when (pinState) { - PinState.Unlocked -> { + val lockScreenState by lockScreenStateService.state.collectAsState() + when (lockScreenState) { + LockScreenState.Unlocked -> { Children( navModel = backstack, modifier = Modifier, @@ -359,7 +359,7 @@ class LoggedInFlowNode @AssistedInject constructor( PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent) } } - PinState.Locked -> { + LockScreenState.Locked -> { MoveActivityToBackgroundBackHandler() PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LockPermanent) } diff --git a/build.gradle.kts b/build.gradle.kts index 704bbe6bbc..487776d948 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -251,9 +251,9 @@ koverMerged { // Some options can't be tested at the moment excludes += "io.element.android.features.preferences.impl.developer.DeveloperSettingsPresenter$*" // Temporary until we have actually something to test. - excludes += "io.element.android.features.pin.impl.auth.PinAuthenticationPresenter" - excludes += "io.element.android.features.pin.impl.auth.PinAuthenticationPresenter$*" - excludes += "io.element.android.features.pin.impl.create.CreatePinPresenter" + excludes += "io.element.android.features.lockscreen.impl.auth.PinAuthenticationPresenter" + excludes += "io.element.android.features.lockscreen.impl.auth.PinAuthenticationPresenter$*" + excludes += "io.element.android.features.lockscreen.impl.create.CreatePinPresenter" } bound { minValue = 85 diff --git a/features/pin/api/build.gradle.kts b/features/lockscreen/api/build.gradle.kts similarity index 92% rename from features/pin/api/build.gradle.kts rename to features/lockscreen/api/build.gradle.kts index 95b062b0c8..97f472517c 100644 --- a/features/pin/api/build.gradle.kts +++ b/features/lockscreen/api/build.gradle.kts @@ -19,7 +19,7 @@ plugins { } android { - namespace = "io.element.android.features.pin.api" + namespace = "io.element.android.features.lockscreen.api" } dependencies { diff --git a/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinEntryPoint.kt b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenEntryPoint.kt similarity index 86% rename from features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinEntryPoint.kt rename to features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenEntryPoint.kt index 1fe3caf574..3c9aceb2c8 100644 --- a/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinEntryPoint.kt +++ b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenEntryPoint.kt @@ -14,8 +14,8 @@ * limitations under the License. */ -package io.element.android.features.pin.api +package io.element.android.features.lockscreen.api import io.element.android.libraries.architecture.SimpleFeatureEntryPoint -interface PinEntryPoint : SimpleFeatureEntryPoint +interface LockScreenEntryPoint : SimpleFeatureEntryPoint diff --git a/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenState.kt b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenState.kt new file mode 100644 index 0000000000..d1e53cfdcc --- /dev/null +++ b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenState.kt @@ -0,0 +1,22 @@ +/* + * 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.api + +sealed interface LockScreenState { + data object Unlocked : LockScreenState + data object Locked : LockScreenState +} diff --git a/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinStateService.kt b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenStateService.kt similarity index 85% rename from features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinStateService.kt rename to features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenStateService.kt index 4ecb473c18..2f2e6b2376 100644 --- a/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinStateService.kt +++ b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenStateService.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package io.element.android.features.pin.api +package io.element.android.features.lockscreen.api import kotlinx.coroutines.flow.StateFlow -interface PinStateService { - val pinState: StateFlow +interface LockScreenStateService { + val state: StateFlow suspend fun entersForeground() suspend fun entersBackground() diff --git a/features/pin/impl/build.gradle.kts b/features/lockscreen/impl/build.gradle.kts similarity index 84% rename from features/pin/impl/build.gradle.kts rename to features/lockscreen/impl/build.gradle.kts index 6cfc9fce11..af63538db5 100644 --- a/features/pin/impl/build.gradle.kts +++ b/features/lockscreen/impl/build.gradle.kts @@ -22,7 +22,7 @@ plugins { } android { - namespace = "io.element.android.features.pin.impl" + namespace = "io.element.android.features.lockscreen.impl" } anvil { @@ -32,13 +32,14 @@ anvil { dependencies { implementation(projects.anvilannotations) anvil(projects.anvilcodegen) - api(projects.features.pin.api) + api(projects.features.lockscreen.api) implementation(projects.libraries.core) implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixui) implementation(projects.libraries.designsystem) implementation(projects.libraries.featureflag.api) + implementation(projects.libraries.cryptography.api) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) @@ -46,6 +47,8 @@ dependencies { testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.libraries.cryptography.test) + testImplementation(projects.libraries.cryptography.impl) ksp(libs.showkase.processor) } diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/DefaultPinEntryPoint.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPoint.kt similarity index 78% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/DefaultPinEntryPoint.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPoint.kt index 920691cad2..736be374cd 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/DefaultPinEntryPoint.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPoint.kt @@ -14,20 +14,20 @@ * limitations under the License. */ -package io.element.android.features.pin.impl +package io.element.android.features.lockscreen.impl import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.features.pin.api.PinEntryPoint +import io.element.android.features.lockscreen.api.LockScreenEntryPoint import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.AppScope import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultPinEntryPoint @Inject constructor() : PinEntryPoint { +class DefaultLockScreenEntryPoint @Inject constructor() : LockScreenEntryPoint { override fun createNode(parentNode: Node, buildContext: BuildContext): Node { - return parentNode.createNode(buildContext) + return parentNode.createNode(buildContext) } } diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/PinFlowNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt similarity index 88% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/PinFlowNode.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt index a76504ce8a..d2989d53cc 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/PinFlowNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl +package io.element.android.features.lockscreen.impl import android.os.Parcelable import androidx.compose.runtime.Composable @@ -27,8 +27,8 @@ import com.bumble.appyx.navmodel.backstack.BackStack import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode -import io.element.android.features.pin.impl.auth.PinAuthenticationNode -import io.element.android.features.pin.impl.create.CreatePinNode +import io.element.android.features.lockscreen.impl.auth.PinAuthenticationNode +import io.element.android.features.lockscreen.impl.create.CreatePinNode import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.createNode @@ -36,10 +36,10 @@ import io.element.android.libraries.di.AppScope import kotlinx.parcelize.Parcelize @ContributesNode(AppScope::class) -class PinFlowNode @AssistedInject constructor( +class LockScreenFlowNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, -) : BackstackNode( +) : BackstackNode( backstack = BackStack( initialElement = NavTarget.Auth, savedStateMap = buildContext.savedStateMap, diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationEvents.kt similarity index 92% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationEvents.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationEvents.kt index 110c62660a..f9f46c430a 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationEvents.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationEvents.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.auth +package io.element.android.features.lockscreen.impl.auth sealed interface PinAuthenticationEvents { data object Unlock : PinAuthenticationEvents diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationNode.kt similarity index 96% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationNode.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationNode.kt index b5dab44c96..d236d40cf1 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationNode.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.auth +package io.element.android.features.lockscreen.impl.auth import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationPresenter.kt similarity index 87% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationPresenter.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationPresenter.kt index 5e7e274ba7..ecc82f421c 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationPresenter.kt @@ -14,17 +14,17 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.auth +package io.element.android.features.lockscreen.impl.auth import androidx.compose.runtime.Composable -import io.element.android.features.pin.api.PinStateService +import io.element.android.features.lockscreen.api.LockScreenStateService import io.element.android.libraries.architecture.Presenter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import javax.inject.Inject class PinAuthenticationPresenter @Inject constructor( - private val pinStateService: PinStateService, + private val pinStateService: LockScreenStateService, private val coroutineScope: CoroutineScope, ) : Presenter { diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationState.kt similarity index 92% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationState.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationState.kt index 2df1e50f83..387467534f 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationState.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.auth +package io.element.android.features.lockscreen.impl.auth data class PinAuthenticationState( val eventSink: (PinAuthenticationEvents) -> Unit diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationStateProvider.kt similarity index 94% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationStateProvider.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationStateProvider.kt index 8e3f45ac07..a2612ed858 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationStateProvider.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.auth +package io.element.android.features.lockscreen.impl.auth import androidx.compose.ui.tooling.preview.PreviewParameterProvider diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationView.kt similarity index 98% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationView.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationView.kt index 9fe689bb39..2b62e46800 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationView.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.auth +package io.element.android.features.lockscreen.impl.auth import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt similarity index 91% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinEvents.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt index 280856b5c8..deb3095e69 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinEvents.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.create +package io.element.android.features.lockscreen.impl.create sealed interface CreatePinEvents { object MyEvent : CreatePinEvents diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinNode.kt similarity index 96% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinNode.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinNode.kt index 0ed0343a5b..3689c0cc76 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinNode.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.create +package io.element.android.features.lockscreen.impl.create import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt similarity index 94% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinPresenter.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt index d45257b4bd..08ba24e074 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.create +package io.element.android.features.lockscreen.impl.create import androidx.compose.runtime.Composable import io.element.android.libraries.architecture.Presenter diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt similarity index 91% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinState.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt index c405db82ec..67311639ad 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.create +package io.element.android.features.lockscreen.impl.create data class CreatePinState( val eventSink: (CreatePinEvents) -> Unit diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt similarity index 94% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinStateProvider.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt index 4bff72023e..a918b5193e 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.create +package io.element.android.features.lockscreen.impl.create import androidx.compose.ui.tooling.preview.PreviewParameterProvider diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt similarity index 96% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinView.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt index 64e5be4091..120c0b6079 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.create +package io.element.android.features.lockscreen.impl.create import androidx.compose.foundation.layout.Box import androidx.compose.material3.MaterialTheme diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt new file mode 100644 index 0000000000..e7529e9280 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt @@ -0,0 +1,72 @@ +/* + * 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 + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.lockscreen.impl.pin.storage.PinCodeStore +import io.element.android.libraries.cryptography.api.EncryptionDecryptionService +import io.element.android.libraries.cryptography.api.EncryptionResult +import io.element.android.libraries.cryptography.api.SecretKeyProvider +import io.element.android.libraries.di.AppScope +import javax.inject.Inject + +private const val SECRET_KEY_ALIAS = "SECRET_KEY_ALIAS_PIN_CODE" + +@ContributesBinding(AppScope::class) +class DefaultPinCodeManager @Inject constructor( + private val secretKeyProvider: SecretKeyProvider, + private val encryptionDecryptionService: EncryptionDecryptionService, + private val pinCodeStore: PinCodeStore, +) : PinCodeManager { + + override suspend fun isPinCodeAvailable(): Boolean { + return pinCodeStore.hasPinCode() + } + + override suspend fun createPinCode(pinCode: String) { + val secretKey = secretKeyProvider.getOrCreateKey(SECRET_KEY_ALIAS) + val encryptedPinCode = encryptionDecryptionService.encrypt(secretKey, pinCode.toByteArray()).toBase64() + pinCodeStore.saveEncryptedPinCode(encryptedPinCode) + } + + override suspend fun verifyPinCode(pinCode: String): Boolean { + val encryptedPinCode = pinCodeStore.getEncryptedCode() ?: return false + return try { + val secretKey = secretKeyProvider.getOrCreateKey(SECRET_KEY_ALIAS) + val decryptedPinCode = encryptionDecryptionService.decrypt(secretKey, EncryptionResult.fromBase64(encryptedPinCode)) + decryptedPinCode.contentEquals(pinCode.toByteArray()) + } catch (failure: Throwable) { + false + } + } + + override suspend fun deletePinCode() { + pinCodeStore.deleteEncryptedPinCode() + } + + override suspend fun getRemainingPinCodeAttemptsNumber(): Int { + return pinCodeStore.getRemainingPinCodeAttemptsNumber() + } + + override suspend fun onWrongPin(): Int { + return pinCodeStore.onWrongPin() + } + + override suspend fun resetCounter() { + pinCodeStore.resetCounter() + } +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt new file mode 100644 index 0000000000..5f84f5296d --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt @@ -0,0 +1,61 @@ +/* + * 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 + +/** + * This interface is the main interface to manage the pin code. + * Implementation should take care of encrypting the pin code and storing it. + */ +interface PinCodeManager { + /** + * @return true if a pin code is available. + */ + suspend fun isPinCodeAvailable(): Boolean + + /** + * Creates a new encrypted pin code. + * @param pinCode the clear pin code to create + */ + suspend fun createPinCode(pinCode: String) + + /** + * @return true if the pin code is correct. + */ + suspend fun verifyPinCode(pinCode: String): Boolean + + /** + * Deletes the previously created pin code. + */ + suspend fun deletePinCode() + + /** + * @return the number of remaining attempts before the pin code is blocked. + */ + suspend fun getRemainingPinCodeAttemptsNumber(): Int + + /** + * Should be called when the pin code is incorrect. + * Will decrement the remaining attempts number. + * @return the number of remaining attempts before the pin code is blocked. + */ + suspend fun onWrongPin(): Int + + /** + * Resets the counter of attempts for PIN code. + */ + suspend fun resetCounter() +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/EncryptedPinCodeStorage.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/EncryptedPinCodeStorage.kt new file mode 100644 index 0000000000..2345eaf481 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/EncryptedPinCodeStorage.kt @@ -0,0 +1,43 @@ +/* + * 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 + +/** + * Should be implemented by any class that provides access to the encrypted PIN code. + * All methods are suspending in case there are async IO operations involved. + */ +interface EncryptedPinCodeStorage { + /** + * Returns the encrypted PIN code. + */ + suspend fun getEncryptedCode(): String? + + /** + * Saves the encrypted PIN code to some persistable storage. + */ + suspend fun saveEncryptedPinCode(pinCode: String) + + /** + * Deletes the PIN code from some persistable storage. + */ + suspend fun deleteEncryptedPinCode() + + /** + * Returns whether the PIN code is stored or not. + */ + suspend fun hasPinCode(): Boolean +} 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 new file mode 100644 index 0000000000..e72cbca2db --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PinCodeStore.kt @@ -0,0 +1,52 @@ +/* + * 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 + +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. + */ + suspend fun getRemainingPinCodeAttemptsNumber(): Int + + /** + * Should decrement the number of remaining PIN code attempts. + * @return The remaining attempts. + */ + suspend fun onWrongPin(): Int + + /** + * 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/SharedPreferencesPinCodeStore.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/SharedPreferencesPinCodeStore.kt new file mode 100644 index 0000000000..27f4636400 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/SharedPreferencesPinCodeStore.kt @@ -0,0 +1,104 @@ +/* + * 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.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.di.AppScope +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" +private const val MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT = 3 + +@SingleIn(AppScope::class) +@ContributesBinding(AppScope::class) +class SharedPreferencesPinCodeStore @Inject constructor( + private val dispatchers: CoroutineDispatchers, + 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) { + // Also reset the counters + resetCounter() + 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) { + mutex.withLock { + sharedPreferences.getInt(REMAINING_PIN_CODE_ATTEMPTS_KEY, MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT) + } + } + + override suspend fun onWrongPin(): Int = withContext(dispatchers.io) { + mutex.withLock { + val remaining = getRemainingPinCodeAttemptsNumber() - 1 + 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/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/state/DefaultPinStateService.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/state/DefaultLockScreenStateService.kt similarity index 75% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/state/DefaultPinStateService.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/state/DefaultLockScreenStateService.kt index 9accef2b80..dbfeca2c6a 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/state/DefaultPinStateService.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/state/DefaultLockScreenStateService.kt @@ -14,11 +14,11 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.state +package io.element.android.features.lockscreen.impl.state import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.features.pin.api.PinState -import io.element.android.features.pin.api.PinStateService +import io.element.android.features.lockscreen.api.LockScreenState +import io.element.android.features.lockscreen.api.LockScreenStateService import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn import io.element.android.libraries.featureflag.api.FeatureFlagService @@ -35,18 +35,18 @@ private const val GRACE_PERIOD_IN_MILLIS = 90 * 1000L @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) -class DefaultPinStateService @Inject constructor( +class DefaultLockScreenStateService @Inject constructor( private val featureFlagService: FeatureFlagService, -) : PinStateService { +) : LockScreenStateService { - private val _pinState = MutableStateFlow(PinState.Unlocked) - override val pinState: StateFlow = _pinState + private val _lockScreenState = MutableStateFlow(LockScreenState.Unlocked) + override val state: StateFlow = _lockScreenState private var lockJob: Job? = null override suspend fun unlock() { if (featureFlagService.isFeatureEnabled(FeatureFlags.PinUnlock)) { - _pinState.value = PinState.Unlocked + _lockScreenState.value = LockScreenState.Unlocked } } @@ -58,7 +58,7 @@ class DefaultPinStateService @Inject constructor( lockJob = launch { if (featureFlagService.isFeatureEnabled(FeatureFlags.PinUnlock)) { delay(GRACE_PERIOD_IN_MILLIS) - _pinState.value = PinState.Locked + _lockScreenState.value = LockScreenState.Locked } } } diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManagerTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManagerTest.kt new file mode 100644 index 0000000000..8b14d15e5e --- /dev/null +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManagerTest.kt @@ -0,0 +1,53 @@ +/* + * 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 + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.lockscreen.impl.pin.storage.InMemoryPinCodeStore +import io.element.android.libraries.cryptography.impl.AESEncryptionDecryptionService +import io.element.android.libraries.cryptography.test.SimpleSecretKeyProvider +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class DefaultPinCodeManagerTest { + + private val pinCodeStore = InMemoryPinCodeStore() + private val secretKeyProvider = SimpleSecretKeyProvider() + private val encryptionDecryptionService = AESEncryptionDecryptionService() + private val pinCodeManager = DefaultPinCodeManager(secretKeyProvider, encryptionDecryptionService, pinCodeStore) + + @Test + fun `given a pin code when create and delete assert no pin code left`() = runTest { + pinCodeManager.createPinCode("1234") + assertThat(pinCodeManager.isPinCodeAvailable()).isTrue() + pinCodeManager.deletePinCode() + assertThat(pinCodeManager.isPinCodeAvailable()).isFalse() + } + + @Test + fun `given a pin code when create and verify with the same pin succeed`() = runTest { + val pinCode = "1234" + pinCodeManager.createPinCode(pinCode) + assertThat(pinCodeManager.verifyPinCode(pinCode)).isTrue() + } + + @Test + fun `given a pin code when create and verify with a different pin fails`() = runTest { + pinCodeManager.createPinCode("1234") + assertThat(pinCodeManager.verifyPinCode("1235")).isFalse() + } +} 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 new file mode 100644 index 0000000000..0b7c2f256b --- /dev/null +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/storage/InMemoryPinCodeStore.kt @@ -0,0 +1,61 @@ +/* + * 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 + +private const val DEFAULT_REMAINING_ATTEMPTS = 3 + +class InMemoryPinCodeStore : PinCodeStore { + + private var pinCode: String? = null + private var remainingAttempts: Int = DEFAULT_REMAINING_ATTEMPTS + + override suspend fun getRemainingPinCodeAttemptsNumber(): Int { + return remainingAttempts + } + + override suspend fun onWrongPin(): Int { + return 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 + } + + override suspend fun saveEncryptedPinCode(pinCode: String) { + this.pinCode = pinCode + } + + override suspend fun deleteEncryptedPinCode() { + pinCode = null + } + + override suspend fun hasPinCode(): Boolean { + return pinCode != null + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 947b3b2752..9a0bfc3a04 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -57,6 +57,11 @@ autoservice = "1.1.1" # quality detekt = "1.23.1" dependencygraph = "0.12" +junit = "4.13.2" +androidx-test-ext-junit = "1.1.5" +espresso-core = "3.5.1" +appcompat = "1.6.1" +material = "1.9.0" [libraries] # Project @@ -184,6 +189,11 @@ google_autoservice_annotations = { module = "com.google.auto.service:auto-servic # value of `composecompiler` (which is used to set composeOptions.kotlinCompilerExtensionVersion. # See https://github.com/renovatebot/renovate/issues/18354 android_composeCompiler = { module = "androidx.compose.compiler:compiler", version.ref = "composecompiler" } +junit = { group = "junit", name = "junit", version.ref = "junit" } +androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" } +espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" } +appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +material = { group = "com.google.android.material", name = "material", version.ref = "material" } [bundles] diff --git a/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinState.kt b/libraries/cryptography/api/build.gradle.kts similarity index 81% rename from features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinState.kt rename to libraries/cryptography/api/build.gradle.kts index 0ff1b0b3d5..e8cee5dbd6 100644 --- a/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinState.kt +++ b/libraries/cryptography/api/build.gradle.kts @@ -14,9 +14,10 @@ * limitations under the License. */ -package io.element.android.features.pin.api +plugins { + id("io.element.android-library") +} -sealed interface PinState { - data object Unlocked : PinState - data object Locked : PinState +android { + namespace = "io.element.android.libraries.cryptography.api" } diff --git a/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/AESEncryptionSpecs.kt b/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/AESEncryptionSpecs.kt new file mode 100644 index 0000000000..d4be4a1f4f --- /dev/null +++ b/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/AESEncryptionSpecs.kt @@ -0,0 +1,27 @@ +/* + * 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.libraries.cryptography.api + +import android.security.keystore.KeyProperties + +object AESEncryptionSpecs { + const val BLOCK_MODE = KeyProperties.BLOCK_MODE_GCM + const val PADDINGS = KeyProperties.ENCRYPTION_PADDING_NONE + const val ALGORITHM = KeyProperties.KEY_ALGORITHM_AES + const val KEY_SIZE = 128 + const val CIPHER_TRANSFORMATION = "$ALGORITHM/$BLOCK_MODE/$PADDINGS" +} diff --git a/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/EncryptionDecryptionService.kt b/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/EncryptionDecryptionService.kt new file mode 100644 index 0000000000..d670f7b1d2 --- /dev/null +++ b/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/EncryptionDecryptionService.kt @@ -0,0 +1,30 @@ +/* + * 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.libraries.cryptography.api + +import javax.crypto.Cipher +import javax.crypto.SecretKey + +/** + * Simple service to provide encryption and decryption operations. + */ +interface EncryptionDecryptionService { + fun createEncryptionCipher(key: SecretKey): Cipher + fun createDecryptionCipher(key: SecretKey, initializationVector: ByteArray): Cipher + fun encrypt(key: SecretKey, input: ByteArray): EncryptionResult + fun decrypt(key: SecretKey, encryptionResult: EncryptionResult): ByteArray +} diff --git a/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/EncryptionResult.kt b/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/EncryptionResult.kt new file mode 100644 index 0000000000..5aa3a0cbea --- /dev/null +++ b/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/EncryptionResult.kt @@ -0,0 +1,59 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalEncodingApi::class) + +package io.element.android.libraries.cryptography.api + +import java.nio.ByteBuffer +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi + +/** + * Holds the result of an encryption operation. + */ +class EncryptionResult( + val encryptedByteArray: ByteArray, + val initializationVector: ByteArray +) { + fun toBase64(): String { + val initializationVectorSize = ByteBuffer.allocate(Int.SIZE_BYTES).putInt(initializationVector.size).array() + val cipherTextWithIv: ByteArray = + ByteBuffer.allocate(Int.SIZE_BYTES + initializationVector.size + encryptedByteArray.size) + .put(initializationVectorSize) + .put(initializationVector) + .put(encryptedByteArray) + .array() + return Base64.encode(cipherTextWithIv) + } + + companion object { + /** + * @param base64 the base64 representation of the encrypted data. + * @return the [EncryptionResult] from the base64 representation. + */ + fun fromBase64(base64: String): EncryptionResult { + val cipherTextWithIv = Base64.decode(base64) + val buffer = ByteBuffer.wrap(cipherTextWithIv) + val initializationVectorSize = buffer.int + val initializationVector = ByteArray(initializationVectorSize) + buffer.get(initializationVector) + val encryptedByteArray = ByteArray(buffer.remaining()) + buffer.get(encryptedByteArray) + return EncryptionResult(encryptedByteArray, initializationVector) + } + } +} diff --git a/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/SecretKeyProvider.kt b/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/SecretKeyProvider.kt new file mode 100644 index 0000000000..85f57ac07f --- /dev/null +++ b/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/SecretKeyProvider.kt @@ -0,0 +1,27 @@ +/* + * 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.libraries.cryptography.api + +import javax.crypto.SecretKey + +/** + * Simple interface to get or create a secret key for a given alias. + * Implementation should be able to store the generated key securely. + */ +interface SecretKeyProvider { + fun getOrCreateKey(alias: String): SecretKey +} diff --git a/libraries/cryptography/impl/build.gradle.kts b/libraries/cryptography/impl/build.gradle.kts new file mode 100644 index 0000000000..263fecec27 --- /dev/null +++ b/libraries/cryptography/impl/build.gradle.kts @@ -0,0 +1,39 @@ +/* + * 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. + */ + +plugins { + id("io.element.android-library") + alias(libs.plugins.anvil) +} + +android { + namespace = "io.element.android.libraries.cryptography.impl" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + anvil(projects.anvilcodegen) + implementation(libs.dagger) + implementation(projects.anvilannotations) + implementation(projects.libraries.di) + implementation(projects.libraries.cryptography.api) + + testImplementation(libs.test.junit) + testImplementation(libs.test.truth) +} diff --git a/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/AESEncryptionDecryptionService.kt b/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/AESEncryptionDecryptionService.kt new file mode 100644 index 0000000000..cf1ea93e3a --- /dev/null +++ b/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/AESEncryptionDecryptionService.kt @@ -0,0 +1,58 @@ +/* + * 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.libraries.cryptography.impl + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.cryptography.api.AESEncryptionSpecs +import io.element.android.libraries.cryptography.api.EncryptionDecryptionService +import io.element.android.libraries.cryptography.api.EncryptionResult +import io.element.android.libraries.di.AppScope +import javax.crypto.Cipher +import javax.crypto.SecretKey +import javax.crypto.spec.GCMParameterSpec +import javax.inject.Inject + +/** + * Default implementation of [EncryptionDecryptionService] using AES encryption. + */ +@ContributesBinding(AppScope::class) +class AESEncryptionDecryptionService @Inject constructor() : EncryptionDecryptionService { + + override fun createEncryptionCipher(key: SecretKey): Cipher { + return Cipher.getInstance(AESEncryptionSpecs.CIPHER_TRANSFORMATION).apply { + init(Cipher.ENCRYPT_MODE, key) + } + } + + override fun createDecryptionCipher(key: SecretKey, initializationVector: ByteArray): Cipher { + val spec = GCMParameterSpec(128, initializationVector) + return Cipher.getInstance(AESEncryptionSpecs.CIPHER_TRANSFORMATION).apply { + init(Cipher.DECRYPT_MODE, key, spec) + } + } + + override fun encrypt(key: SecretKey, input: ByteArray): EncryptionResult { + val cipher = createEncryptionCipher(key) + val encryptedData = cipher.doFinal(input) + return EncryptionResult(encryptedData, cipher.iv) + } + + override fun decrypt(key: SecretKey, encryptionResult: EncryptionResult): ByteArray { + val cipher = createDecryptionCipher(key, encryptionResult.initializationVector) + return cipher.doFinal(encryptionResult.encryptedByteArray) + } +} diff --git a/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyProvider.kt b/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyProvider.kt new file mode 100644 index 0000000000..2cd09ea8f6 --- /dev/null +++ b/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyProvider.kt @@ -0,0 +1,62 @@ +/* + * 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.libraries.cryptography.impl + +import android.annotation.SuppressLint +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.cryptography.api.AESEncryptionSpecs +import io.element.android.libraries.cryptography.api.SecretKeyProvider +import io.element.android.libraries.di.AppScope +import java.security.KeyStore +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey +import javax.inject.Inject + +private const val ANDROID_KEYSTORE = "AndroidKeyStore" + +/** + * Default implementation of [SecretKeyProvider] that uses the Android Keystore to store the keys. + * The generated key uses AES algorithm, with a key size of 128 bits, and the GCM block mode. + */ +@ContributesBinding(AppScope::class) +class KeyStoreSecretKeyProvider @Inject constructor() : SecretKeyProvider { + + // False positive lint issue + @SuppressLint("WrongConstant") + override fun getOrCreateKey(alias: String): SecretKey { + val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE) + val secretKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry) + ?.secretKey + return if (secretKeyEntry == null) { + val generator = KeyGenerator.getInstance(AESEncryptionSpecs.ALGORITHM, ANDROID_KEYSTORE) + val keyGenSpec = KeyGenParameterSpec.Builder( + alias, + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT + ) + .setBlockModes(AESEncryptionSpecs.BLOCK_MODE) + .setEncryptionPaddings(AESEncryptionSpecs.PADDINGS) + .setKeySize(AESEncryptionSpecs.KEY_SIZE) + .build() + generator.init(keyGenSpec) + generator.generateKey() + } else { + secretKeyEntry + } + } +} diff --git a/libraries/cryptography/impl/src/test/kotlin/io/element/android/libraries/cryptography/impl/AESEncryptionDecryptionServiceTest.kt b/libraries/cryptography/impl/src/test/kotlin/io/element/android/libraries/cryptography/impl/AESEncryptionDecryptionServiceTest.kt new file mode 100644 index 0000000000..38e1c924ca --- /dev/null +++ b/libraries/cryptography/impl/src/test/kotlin/io/element/android/libraries/cryptography/impl/AESEncryptionDecryptionServiceTest.kt @@ -0,0 +1,54 @@ +/* + * 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.libraries.cryptography.impl + +import android.security.keystore.KeyProperties +import com.google.common.truth.Truth.assertThat +import org.junit.Assert.assertThrows +import org.junit.Test +import java.security.GeneralSecurityException +import javax.crypto.KeyGenerator + +class AESEncryptionDecryptionServiceTest { + + private val encryptionDecryptionService = AESEncryptionDecryptionService() + + @Test + fun `given a valid key then encrypt decrypt work`() { + val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES) + keyGenerator.init(128) + val key = keyGenerator.generateKey() + val input = "Hello World".toByteArray() + val encryptionResult = encryptionDecryptionService.encrypt(key, input) + val decrypted = encryptionDecryptionService.decrypt(key, encryptionResult) + assertThat(decrypted).isEqualTo(input) + } + + @Test + fun `given a wrong key then decrypt fail`() { + val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES) + keyGenerator.init(128) + val encryptionKey = keyGenerator.generateKey() + val input = "Hello World".toByteArray() + val encryptionResult = encryptionDecryptionService.encrypt(encryptionKey, input) + val decryptionKey = keyGenerator.generateKey() + assertThrows(GeneralSecurityException::class.java) { + encryptionDecryptionService.decrypt(decryptionKey, encryptionResult) + } + } + +} diff --git a/libraries/cryptography/test/build.gradle.kts b/libraries/cryptography/test/build.gradle.kts new file mode 100644 index 0000000000..3b9074c897 --- /dev/null +++ b/libraries/cryptography/test/build.gradle.kts @@ -0,0 +1,27 @@ +/* + * 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. + */ + +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.libraries.cryptography.test" + + dependencies { + api(projects.libraries.cryptography.api) + } +} diff --git a/libraries/cryptography/test/src/main/kotlin/io/element/android/libraries/cryptography/test/SimpleSecretKeyProvider.kt b/libraries/cryptography/test/src/main/kotlin/io/element/android/libraries/cryptography/test/SimpleSecretKeyProvider.kt new file mode 100644 index 0000000000..d06a545d78 --- /dev/null +++ b/libraries/cryptography/test/src/main/kotlin/io/element/android/libraries/cryptography/test/SimpleSecretKeyProvider.kt @@ -0,0 +1,39 @@ +/* + * 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.libraries.cryptography.test + +import io.element.android.libraries.cryptography.api.AESEncryptionSpecs +import io.element.android.libraries.cryptography.api.SecretKeyProvider +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey + +class SimpleSecretKeyProvider : SecretKeyProvider { + + private var secretKeyForAlias = HashMap() + + override fun getOrCreateKey(alias: String): SecretKey { + return secretKeyForAlias.getOrPut(alias) { + generateKey() + } + } + + private fun generateKey(): SecretKey { + val keyGenerator = KeyGenerator.getInstance(AESEncryptionSpecs.ALGORITHM) + keyGenerator.init(AESEncryptionSpecs.KEY_SIZE) + return keyGenerator.generateKey() + } +} diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.auth_null_PinAuthenticationView-D-0_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.auth_null_PinAuthenticationView-D-0_0_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.auth_null_PinAuthenticationView-D-0_0_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.auth_null_PinAuthenticationView-D-0_0_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.auth_null_PinAuthenticationView-N-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.auth_null_PinAuthenticationView-N-0_1_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.auth_null_PinAuthenticationView-N-0_1_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.auth_null_PinAuthenticationView-N-0_1_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png