Skip to content

Commit

Permalink
feat: Experimental biometric support on MacOS #663
Browse files Browse the repository at this point in the history
  • Loading branch information
AChep committed Feb 4, 2025
1 parent 2268e8d commit 7985d3f
Show file tree
Hide file tree
Showing 55 changed files with 1,171 additions and 223 deletions.
1 change: 1 addition & 0 deletions androidApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ android {
kotlinOptions {
freeCompilerArgs += listOf(
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
"-Xexpect-actual-classes",
)
}
}
Expand Down
1 change: 1 addition & 0 deletions common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ kotlin {
api(libs.mayakapps.window.styler)
api(libs.wunderbox.nativefiledialog)
api(libs.willena.sqlite.jdbc)
api(project(":desktopLibJvm"))
}
}
val androidMain by getting {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import android.content.pm.PackageManager
import com.artemchep.keyguard.android.downloader.DownloadClientAndroid
import com.artemchep.keyguard.android.downloader.DownloadManagerImpl
import com.artemchep.keyguard.android.downloader.DownloadTaskAndroid
import com.artemchep.keyguard.android.downloader.ExportManagerImpl
import com.artemchep.keyguard.android.downloader.journal.DownloadRepository
import com.artemchep.keyguard.android.downloader.journal.DownloadRepositoryImpl
import com.artemchep.keyguard.android.downloader.journal.room.DownloadDatabaseManager
Expand All @@ -17,9 +16,9 @@ import com.artemchep.keyguard.common.service.connectivity.ConnectivityService
import com.artemchep.keyguard.common.service.dirs.DirsService
import com.artemchep.keyguard.common.service.download.DownloadManager
import com.artemchep.keyguard.common.service.download.DownloadTask
import com.artemchep.keyguard.common.service.export.ExportManager
import com.artemchep.keyguard.common.service.keychain.KeychainRepository
import com.artemchep.keyguard.common.service.keychain.impl.KeychainRepositoryNoOp
import com.artemchep.keyguard.common.service.keyvalue.KeyValueStore
import com.artemchep.keyguard.common.service.logging.LogRepository
import com.artemchep.keyguard.common.service.permission.PermissionService
import com.artemchep.keyguard.common.service.power.PowerService
import com.artemchep.keyguard.common.service.review.ReviewService
Expand All @@ -28,18 +27,12 @@ import com.artemchep.keyguard.common.service.text.TextService
import com.artemchep.keyguard.common.usecase.BiometricStatusUseCase
import com.artemchep.keyguard.common.usecase.CleanUpAttachment
import com.artemchep.keyguard.common.usecase.ClearData
import com.artemchep.keyguard.common.usecase.DisableBiometric
import com.artemchep.keyguard.common.usecase.EnableBiometric
import com.artemchep.keyguard.common.usecase.GetBarcodeImage
import com.artemchep.keyguard.common.usecase.GetBiometricRemainingDuration
import com.artemchep.keyguard.common.usecase.GetLocale
import com.artemchep.keyguard.common.usecase.GetPurchased
import com.artemchep.keyguard.common.usecase.GetSuggestions
import com.artemchep.keyguard.common.usecase.PutLocale
import com.artemchep.keyguard.common.usecase.impl.CleanUpAttachmentImpl
import com.artemchep.keyguard.common.usecase.impl.DisableBiometricImpl
import com.artemchep.keyguard.common.usecase.impl.EnableBiometricImpl
import com.artemchep.keyguard.common.usecase.impl.GetBiometricRemainingDurationImpl
import com.artemchep.keyguard.common.usecase.impl.GetPurchasedImpl
import com.artemchep.keyguard.common.usecase.impl.GetSuggestionsImpl
import com.artemchep.keyguard.copy.AutofillServiceAndroid
Expand All @@ -48,7 +41,6 @@ import com.artemchep.keyguard.copy.ClipboardServiceAndroid
import com.artemchep.keyguard.copy.ConnectivityServiceAndroid
import com.artemchep.keyguard.copy.GetBarcodeImageJvm
import com.artemchep.keyguard.copy.LinkInfoExtractorAndroid
import com.artemchep.keyguard.common.service.extract.impl.LinkInfoExtractorExecute
import com.artemchep.keyguard.copy.DirsServiceAndroid
import com.artemchep.keyguard.copy.LinkInfoExtractorLaunch
import com.artemchep.keyguard.copy.LogRepositoryAndroid
Expand All @@ -59,7 +51,6 @@ import com.artemchep.keyguard.copy.SharedPreferencesStoreFactory
import com.artemchep.keyguard.copy.SharedPreferencesStoreFactoryDefault
import com.artemchep.keyguard.copy.SubscriptionServiceAndroid
import com.artemchep.keyguard.copy.TextServiceAndroid
import com.artemchep.keyguard.copy.download.DownloadTaskJvm
import com.artemchep.keyguard.core.session.usecase.BiometricStatusUseCaseImpl
import com.artemchep.keyguard.core.session.usecase.GetLocaleAndroid
import com.artemchep.keyguard.core.session.usecase.PutLocaleAndroid
Expand Down Expand Up @@ -89,24 +80,13 @@ fun diFingerprintRepositoryModule() = DI.Module(
directDI = this,
)
}
bindSingleton<GetBarcodeImage> {
GetBarcodeImageJvm(
directDI = this,
)
}

bindSingleton<GetBiometricRemainingDuration> {
GetBiometricRemainingDurationImpl(
bindSingleton<KeychainRepository> {
KeychainRepositoryNoOp(
directDI = this,
)
}
bindSingleton<DisableBiometric> {
DisableBiometricImpl(
directDI = this,
)
}
bindSingleton<EnableBiometric> {
EnableBiometricImpl(
bindSingleton<GetBarcodeImage> {
GetBarcodeImageJvm(
directDI = this,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
import com.artemchep.keyguard.common.model.BiometricPurpose
import com.artemchep.keyguard.common.model.BiometricStatus
import com.artemchep.keyguard.common.usecase.BiometricStatusUseCase
import com.artemchep.keyguard.platform.LeBiometricCipher
import com.artemchep.keyguard.platform.LeBiometricCipherJvm
import kotlinx.coroutines.flow.MutableStateFlow
import org.kodein.di.DirectDI
import org.kodein.di.instance
Expand Down Expand Up @@ -84,7 +86,7 @@ private fun deleteCipher() {

private fun createCipher(
biometricPurpose: BiometricPurpose,
): Cipher = createEmptyCipher().apply {
): LeBiometricCipher = createEmptyCipher().apply {
val key = getSecretKey()
when (biometricPurpose) {
// Init cipher in encrypt mode with random iv
Expand All @@ -95,6 +97,8 @@ private fun createCipher(
init(Cipher.DECRYPT_MODE, key, spec)
}
}
}.let { platformCipher ->
LeBiometricCipherJvm(platformCipher)
}

private fun createEmptyCipher(): Cipher = Cipher
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import com.artemchep.keyguard.common.model.BiometricAuthPrompt
import com.artemchep.keyguard.common.model.BiometricAuthPromptSimple
import com.artemchep.keyguard.common.model.PureBiometricAuthPrompt
import com.artemchep.keyguard.feature.localization.textResource
import com.artemchep.keyguard.platform.LeBiometricCipherJvm
import com.artemchep.keyguard.ui.CollectedEffect
import kotlinx.coroutines.flow.Flow

Expand Down Expand Up @@ -67,13 +68,18 @@ private suspend fun FragmentActivity.launchPrompt(

override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
val cipher = result.cryptoObject?.cipher
val platformCipher = result.cryptoObject?.cipher
?: return
val cipher = LeBiometricCipherJvm(platformCipher)
event.onComplete(cipher.right())
}
},
)
val crypto = BiometricPrompt.CryptoObject(event.cipher)
val platformCipher = kotlin.run {
val platform = event.cipher as LeBiometricCipherJvm
platform.cipher
}
val crypto = BiometricPrompt.CryptoObject(platformCipher)
prompt.authenticate(promptInfo, crypto)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@ package com.artemchep.keyguard.common.model

import arrow.core.Either
import com.artemchep.keyguard.feature.localization.TextHolder
import com.artemchep.keyguard.platform.LeCipher
import com.artemchep.keyguard.platform.LeBiometricCipher

sealed interface PureBiometricAuthPrompt

class BiometricAuthPrompt(
val title: TextHolder,
val text: TextHolder? = null,
val cipher: LeCipher,
val cipher: LeBiometricCipher,
val requireConfirmation: Boolean,
/**
* Called when the user either failed the authentication or
* successfully passed it.
*/
val onComplete: (
result: Either<BiometricAuthException, LeCipher>,
result: Either<BiometricAuthException, LeBiometricCipher>,
) -> Unit,
) : PureBiometricAuthPrompt

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
package com.artemchep.keyguard.common.model

import com.artemchep.keyguard.platform.LeCipher
import com.artemchep.keyguard.platform.LeBiometricCipher

sealed interface BiometricStatus {
class Available(
/**
* Creates a cipher to use with a biometric
* prompt.
*/
val createCipher: (BiometricPurpose) -> LeCipher,
val deleteCipher: () -> Unit,
val createCipher: suspend (BiometricPurpose) -> LeBiometricCipher,
val deleteCipher: suspend () -> Unit,
) : BiometricStatus

data object Unavailable : BiometricStatus
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.artemchep.keyguard.common.model

import arrow.core.Either
import com.artemchep.keyguard.common.io.IO
import com.artemchep.keyguard.platform.LeCipher
import com.artemchep.keyguard.platform.LeBiometricCipher
import org.kodein.di.DI

sealed interface VaultState {
Expand All @@ -15,7 +15,7 @@ sealed interface VaultState {
)

class WithBiometric(
val getCipher: () -> Either<Throwable, LeCipher>,
val getCipher: suspend () -> Either<Throwable, LeBiometricCipher>,
val getCreateIo: (String) -> IO<Unit>,
val requireConfirmation: Boolean,
)
Expand All @@ -31,7 +31,7 @@ sealed interface VaultState {
)

class WithBiometric(
val getCipher: () -> Either<Throwable, LeCipher>,
val getCipher: suspend () -> Either<Throwable, LeBiometricCipher>,
val getCreateIo: () -> IO<Unit>,
val requireConfirmation: Boolean,
)
Expand All @@ -52,7 +52,7 @@ sealed interface VaultState {
)

class WithBiometric(
val getCipher: () -> Either<Throwable, LeCipher>,
val getCipher: suspend () -> Either<Throwable, LeBiometricCipher>,
val getCreateIo: (String, String) -> IO<Unit>,
)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.artemchep.keyguard.common.model

import com.artemchep.keyguard.common.io.IO
import com.artemchep.keyguard.platform.LeCipher
import com.artemchep.keyguard.platform.LeBiometricCipher

class WithBiometric(
val getCipher: () -> LeCipher,
val getCipher: suspend () -> LeBiometricCipher,
val getCreateIo: () -> IO<Unit>,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.artemchep.keyguard.common.service.biometrics

interface BiometricsService {
fun isSupported(): Boolean

suspend fun confirm(): Boolean
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.artemchep.keyguard.common.service.keychain

enum class KeychainIds(
val value: String,
) {
BIOMETRIC_UNLOCK("com.artemchep.keyguard.biometric-unlock"),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.artemchep.keyguard.common.service.keychain

import com.artemchep.keyguard.common.io.IO

/**
* Provides a generic interface for keychain
* implementation.
*/
interface KeychainRepository {
fun put(
id: String,
password: String,
requireUserPresence: Boolean = false,
): IO<Unit>

fun get(id: String): IO<String>

fun delete(id: String): IO<Boolean>

fun contains(id: String): IO<Boolean>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.artemchep.keyguard.common.service.keychain.impl

import com.artemchep.keyguard.common.io.ioRaise
import com.artemchep.keyguard.common.service.keychain.KeychainRepository
import org.kodein.di.DirectDI

class KeychainRepositoryNoOp(
) : KeychainRepository {
constructor(directDI: DirectDI) : this()

override fun put(
id: String,
password: String,
requireUserPresence: Boolean,
) = ioRaiseDefault()

override fun get(id: String) = ioRaiseDefault()

override fun delete(id: String) = ioRaiseDefault()

override fun contains(id: String) = ioRaiseDefault()

private fun ioRaiseDefault() = kotlin.run {
val e = IllegalStateException("This platform does not have the keychain support!")
ioRaise<Nothing>(e)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ package com.artemchep.keyguard.common.usecase

import com.artemchep.keyguard.common.io.IO
import com.artemchep.keyguard.common.model.MasterKey
import com.artemchep.keyguard.platform.LeCipher
import com.artemchep.keyguard.platform.LeBiometricCipher

interface BiometricKeyDecryptUseCase : (
IO<LeCipher>,
IO<LeBiometricCipher>,
ByteArray,
) -> IO<MasterKey>
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ package com.artemchep.keyguard.common.usecase

import com.artemchep.keyguard.common.io.IO
import com.artemchep.keyguard.common.model.MasterKey
import com.artemchep.keyguard.platform.LeCipher
import com.artemchep.keyguard.platform.LeBiometricCipher

interface BiometricKeyEncryptUseCase : (
IO<LeCipher>,
IO<LeBiometricCipher>,
MasterKey,
) -> IO<ByteArray>
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import com.artemchep.keyguard.common.io.IO
import com.artemchep.keyguard.common.io.map
import com.artemchep.keyguard.common.model.MasterKey
import com.artemchep.keyguard.common.usecase.BiometricKeyDecryptUseCase
import com.artemchep.keyguard.core.session.util.encode
import com.artemchep.keyguard.platform.LeCipher
import com.artemchep.keyguard.platform.LeBiometricCipher
import com.artemchep.keyguard.platform.encode
import org.kodein.di.DirectDI

class BiometricKeyDecryptUseCaseImpl() : BiometricKeyDecryptUseCase {
constructor(directDI: DirectDI) : this()

override fun invoke(
cipher: IO<LeCipher>,
cipher: IO<LeBiometricCipher>,
encryptedMasterKey: ByteArray,
): IO<MasterKey> = encryptedMasterKey
.encode(cipher)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ package com.artemchep.keyguard.common.usecase.impl
import com.artemchep.keyguard.common.io.IO
import com.artemchep.keyguard.common.model.MasterKey
import com.artemchep.keyguard.common.usecase.BiometricKeyEncryptUseCase
import com.artemchep.keyguard.core.session.util.encode
import com.artemchep.keyguard.platform.LeCipher
import com.artemchep.keyguard.platform.LeBiometricCipher
import com.artemchep.keyguard.platform.encode
import org.kodein.di.DirectDI

class BiometricKeyEncryptUseCaseImpl() : BiometricKeyEncryptUseCase {
constructor(directDI: DirectDI) : this()

override fun invoke(
cipher: IO<LeCipher>,
cipher: IO<LeBiometricCipher>,
masterKey: MasterKey,
): IO<ByteArray> = masterKey.byteArray
.encode(cipher)
Expand Down
Loading

0 comments on commit 7985d3f

Please sign in to comment.