Browse Source

Merge pull request #1592 from vector-im/feature/fga/setup_crypto_for_pin

Feature/fga/setup crypto for pin
pull/1598/head
ganfra 11 months ago committed by GitHub
parent
commit
00e885fa9f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 24
      appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
  2. 6
      build.gradle.kts
  3. 2
      features/lockscreen/api/build.gradle.kts
  4. 4
      features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenEntryPoint.kt
  5. 8
      features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenState.kt
  6. 6
      features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenStateService.kt
  7. 7
      features/lockscreen/impl/build.gradle.kts
  8. 8
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPoint.kt
  9. 10
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt
  10. 2
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationEvents.kt
  11. 2
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationNode.kt
  12. 6
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationPresenter.kt
  13. 2
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationState.kt
  14. 2
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationStateProvider.kt
  15. 2
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationView.kt
  16. 2
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt
  17. 2
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinNode.kt
  18. 2
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt
  19. 2
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt
  20. 2
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt
  21. 2
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt
  22. 72
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt
  23. 61
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt
  24. 43
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/EncryptedPinCodeStorage.kt
  25. 52
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PinCodeStore.kt
  26. 104
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/SharedPreferencesPinCodeStore.kt
  27. 18
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/state/DefaultLockScreenStateService.kt
  28. 53
      features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManagerTest.kt
  29. 61
      features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/storage/InMemoryPinCodeStore.kt
  30. 10
      gradle/libs.versions.toml
  31. 23
      libraries/cryptography/api/build.gradle.kts
  32. 27
      libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/AESEncryptionSpecs.kt
  33. 30
      libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/EncryptionDecryptionService.kt
  34. 59
      libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/EncryptionResult.kt
  35. 27
      libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/SecretKeyProvider.kt
  36. 39
      libraries/cryptography/impl/build.gradle.kts
  37. 58
      libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/AESEncryptionDecryptionService.kt
  38. 62
      libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyProvider.kt
  39. 54
      libraries/cryptography/impl/src/test/kotlin/io/element/android/libraries/cryptography/impl/AESEncryptionDecryptionServiceTest.kt
  40. 27
      libraries/cryptography/test/build.gradle.kts
  41. 39
      libraries/cryptography/test/src/main/kotlin/io/element/android/libraries/cryptography/test/SimpleSecretKeyProvider.kt
  42. 0
      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
  43. 0
      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
  44. 0
      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
  45. 0
      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

24
appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt

@ -50,9 +50,9 @@ import io.element.android.features.ftue.api.state.FtueState @@ -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( @@ -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<LoggedInFlowNode.NavTarget>(
@ -136,12 +136,12 @@ class LoggedInFlowNode @AssistedInject constructor( @@ -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( @@ -218,7 +218,7 @@ class LoggedInFlowNode @AssistedInject constructor(
createNode<LoggedInNode>(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( @@ -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( @@ -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)
}

6
build.gradle.kts

@ -251,9 +251,9 @@ koverMerged { @@ -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

2
features/pin/api/build.gradle.kts → features/lockscreen/api/build.gradle.kts

@ -19,7 +19,7 @@ plugins { @@ -19,7 +19,7 @@ plugins {
}
android {
namespace = "io.element.android.features.pin.api"
namespace = "io.element.android.features.lockscreen.api"
}
dependencies {

4
features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinEntryPoint.kt → features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenEntryPoint.kt

@ -14,8 +14,8 @@ @@ -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

8
features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinState.kt → features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenState.kt

@ -14,9 +14,9 @@ @@ -14,9 +14,9 @@
* limitations under the License.
*/
package io.element.android.features.pin.api
package io.element.android.features.lockscreen.api
sealed interface PinState {
data object Unlocked : PinState
data object Locked : PinState
sealed interface LockScreenState {
data object Unlocked : LockScreenState
data object Locked : LockScreenState
}

6
features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinStateService.kt → features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenStateService.kt

@ -14,12 +14,12 @@ @@ -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<PinState>
interface LockScreenStateService {
val state: StateFlow<LockScreenState>
suspend fun entersForeground()
suspend fun entersBackground()

7
features/pin/impl/build.gradle.kts → features/lockscreen/impl/build.gradle.kts

@ -22,7 +22,7 @@ plugins { @@ -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 { @@ -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 { @@ -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)
}

8
features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/DefaultPinEntryPoint.kt → features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPoint.kt

@ -14,20 +14,20 @@ @@ -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<PinFlowNode>(buildContext)
return parentNode.createNode<LockScreenFlowNode>(buildContext)
}
}

10
features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/PinFlowNode.kt → features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt

@ -14,7 +14,7 @@ @@ -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 @@ -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 @@ -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<Plugin>,
) : BackstackNode<PinFlowNode.NavTarget>(
) : BackstackNode<LockScreenFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.Auth,
savedStateMap = buildContext.savedStateMap,

2
features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationEvents.kt → features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationEvents.kt

@ -14,7 +14,7 @@ @@ -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

2
features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationNode.kt → features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationNode.kt

@ -14,7 +14,7 @@ @@ -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

6
features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationPresenter.kt → features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationPresenter.kt

@ -14,17 +14,17 @@ @@ -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<PinAuthenticationState> {

2
features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationState.kt → features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationState.kt

@ -14,7 +14,7 @@ @@ -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

2
features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationStateProvider.kt → features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationStateProvider.kt

@ -14,7 +14,7 @@ @@ -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

2
features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationView.kt → features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationView.kt

@ -14,7 +14,7 @@ @@ -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

2
features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinEvents.kt → features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt

@ -14,7 +14,7 @@ @@ -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

2
features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinNode.kt → features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinNode.kt

@ -14,7 +14,7 @@ @@ -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

2
features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinPresenter.kt → features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt

@ -14,7 +14,7 @@ @@ -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

2
features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinState.kt → features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt

@ -14,7 +14,7 @@ @@ -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

2
features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinStateProvider.kt → features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt

@ -14,7 +14,7 @@ @@ -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

2
features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinView.kt → features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt

@ -14,7 +14,7 @@ @@ -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

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

@ -0,0 +1,72 @@ @@ -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()
}
}

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

@ -0,0 +1,61 @@ @@ -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()
}

43
features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/EncryptedPinCodeStorage.kt

@ -0,0 +1,43 @@ @@ -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
}

52
features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PinCodeStore.kt

@ -0,0 +1,52 @@ @@ -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)
}

104
features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/SharedPreferencesPinCodeStore.kt

@ -0,0 +1,104 @@ @@ -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<PinCodeStore.Listener>()
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)
}
}

18
features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/state/DefaultPinStateService.kt → features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/state/DefaultLockScreenStateService.kt

@ -14,11 +14,11 @@ @@ -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 @@ -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>(PinState.Unlocked)
override val pinState: StateFlow<PinState> = _pinState
private val _lockScreenState = MutableStateFlow<LockScreenState>(LockScreenState.Unlocked)
override val state: StateFlow<LockScreenState> = _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( @@ -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
}
}
}

53
features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManagerTest.kt

@ -0,0 +1,53 @@ @@ -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()
}
}

61
features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/storage/InMemoryPinCodeStore.kt

@ -0,0 +1,61 @@ @@ -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
}
}

10
gradle/libs.versions.toml

@ -57,6 +57,11 @@ autoservice = "1.1.1" @@ -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 @@ -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]

23
libraries/cryptography/api/build.gradle.kts

@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
/*
* 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.api"
}

27
libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/AESEncryptionSpecs.kt

@ -0,0 +1,27 @@ @@ -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"
}

30
libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/EncryptionDecryptionService.kt

@ -0,0 +1,30 @@ @@ -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
}

59
libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/EncryptionResult.kt

@ -0,0 +1,59 @@ @@ -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)
}
}
}

27
libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/SecretKeyProvider.kt

@ -0,0 +1,27 @@ @@ -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
}

39
libraries/cryptography/impl/build.gradle.kts

@ -0,0 +1,39 @@ @@ -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)
}

58
libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/AESEncryptionDecryptionService.kt

@ -0,0 +1,58 @@ @@ -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)
}
}

62
libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyProvider.kt

@ -0,0 +1,62 @@ @@ -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
}
}
}

54
libraries/cryptography/impl/src/test/kotlin/io/element/android/libraries/cryptography/impl/AESEncryptionDecryptionServiceTest.kt

@ -0,0 +1,54 @@ @@ -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)
}
}
}

27
libraries/cryptography/test/build.gradle.kts

@ -0,0 +1,27 @@ @@ -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)
}
}

39
libraries/cryptography/test/src/main/kotlin/io/element/android/libraries/cryptography/test/SimpleSecretKeyProvider.kt

@ -0,0 +1,39 @@ @@ -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<String, SecretKey>()
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()
}
}

0
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 → 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

0
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 → 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

0
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 → 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

0
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 → 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

Loading…
Cancel
Save