Skip to content

Commit

Permalink
Implement PIN and Biometric authentication (#48)
Browse files Browse the repository at this point in the history
Fixes #20
  • Loading branch information
X1nto authored Oct 27, 2023
1 parent 384dd61 commit d6ecb87
Show file tree
Hide file tree
Showing 30 changed files with 1,660 additions and 33 deletions.
4 changes: 4 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ dependencies {
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.material:material-icons-extended")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.ui:ui-tooling-preview")
androidTestImplementation(composeBom)
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-tooling")
Expand Down Expand Up @@ -120,6 +121,9 @@ dependencies {
implementation("androidx.room:room-ktx:$roomVersion")
ksp("androidx.room:room-compiler:$roomVersion")

implementation("androidx.biometric:biometric:1.1.0")
implementation("androidx.security:security-crypto-ktx:1.1.0-alpha06")

implementation("androidx.datastore:datastore-preferences:1.0.0")

implementation("dev.olshevski.navigation:reimagined:1.5.0-beta01")
Expand Down
13 changes: 13 additions & 0 deletions app/src/main/java/com/xinto/mauth/core/auth/AuthManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.xinto.mauth.core.auth

import kotlinx.coroutines.flow.Flow

interface AuthManager {

fun getCode(): Flow<String?>

fun setCode(code: String)

fun removeCode()

}
54 changes: 54 additions & 0 deletions app/src/main/java/com/xinto/mauth/core/auth/DefaultAuthManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.xinto.mauth.core.auth

import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow

class DefaultAuthManager(
context: Context
) : AuthManager {

private val prefs = EncryptedSharedPreferences.create(
context,
"auth",
MasterKey(context = context),
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
)

override fun getCode(): Flow<String?> {
return callbackFlow {
val listener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
if (key == KEY_CODE) {
trySend(sharedPreferences.getString(KEY_CODE, null))
}
}
send(prefs.getString(KEY_CODE, null))
prefs.registerOnSharedPreferenceChangeListener(listener)
awaitClose {
prefs.unregisterOnSharedPreferenceChangeListener(listener)
}
}
}

override fun setCode(code: String) {
prefs.edit {
putString(KEY_CODE, code)
}
}

override fun removeCode() {
prefs.edit {
remove(KEY_CODE)
}
}

private companion object {
const val KEY_CODE = "code"
}
}
13 changes: 13 additions & 0 deletions app/src/main/java/com/xinto/mauth/core/settings/DefaultSettings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ class DefaultSettings(context: Context) : Settings {
}
}

override fun getUseBiometrics(): Flow<Boolean> {
return preferences.data.map {
it[KEY_USE_BIOMETRICS] ?: false
}
}

override suspend fun setSecureMode(value: Boolean) {
preferences.edit {
it[KEY_SECURE_MODE] = value
Expand All @@ -40,9 +46,16 @@ class DefaultSettings(context: Context) : Settings {
}
}

override suspend fun setUseBiometrics(value: Boolean) {
preferences.edit {
it[KEY_USE_BIOMETRICS] = value
}
}

private companion object {
val KEY_SECURE_MODE = booleanPreferencesKey("private_mode")
val KEY_SORT_MODE = stringPreferencesKey("sort_mode")
val KEY_USE_BIOMETRICS = booleanPreferencesKey("use_biometrics")
}

}
2 changes: 2 additions & 0 deletions app/src/main/java/com/xinto/mauth/core/settings/Settings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import kotlinx.coroutines.flow.Flow
interface Settings {
fun getSecureMode(): Flow<Boolean>
fun getSortMode(): Flow<SortSetting>
fun getUseBiometrics(): Flow<Boolean>

suspend fun setSecureMode(value: Boolean)
suspend fun setSortMode(value: SortSetting)
suspend fun setUseBiometrics(value: Boolean)
}
11 changes: 11 additions & 0 deletions app/src/main/java/com/xinto/mauth/di/MauthDI.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.xinto.mauth.di

import androidx.room.Room
import com.xinto.mauth.core.auth.AuthManager
import com.xinto.mauth.core.auth.DefaultAuthManager
import com.xinto.mauth.core.otp.generator.DefaultOtpGenerator
import com.xinto.mauth.core.otp.generator.OtpGenerator
import com.xinto.mauth.core.otp.parser.DefaultOtpUriParser
Expand All @@ -14,8 +16,12 @@ import com.xinto.mauth.domain.QrRepository
import com.xinto.mauth.domain.SettingsRepository
import com.xinto.mauth.core.settings.DefaultSettings
import com.xinto.mauth.core.settings.Settings
import com.xinto.mauth.domain.AuthRepository
import com.xinto.mauth.ui.screen.account.AccountViewModel
import com.xinto.mauth.ui.screen.auth.AuthViewModel
import com.xinto.mauth.ui.screen.home.HomeViewModel
import com.xinto.mauth.ui.screen.pinremove.PinRemoveViewModel
import com.xinto.mauth.ui.screen.pinsetup.PinSetupViewModel
import com.xinto.mauth.ui.screen.qrscan.QrScanViewModel
import com.xinto.mauth.ui.screen.settings.SettingsViewModel
import org.apache.commons.codec.binary.Base32
Expand All @@ -34,6 +40,7 @@ object MauthDI {
DefaultKeyTransformer(Base32())
} bind KeyTransformer::class
singleOf(::DefaultSettings) bind Settings::class
singleOf(::DefaultAuthManager) bind AuthManager::class
}

val DbModule = module {
Expand All @@ -60,13 +67,17 @@ object MauthDI {
singleOf(::OtpRepository)
singleOf(::QrRepository)
singleOf(::SettingsRepository)
singleOf(::AuthRepository)
}

val UiModule = module {
viewModelOf(::AccountViewModel)
viewModelOf(::SettingsViewModel)
viewModelOf(::QrScanViewModel)
viewModelOf(::PinSetupViewModel)
viewModelOf(::PinRemoveViewModel)
viewModelOf(::HomeViewModel)
viewModelOf(::AuthViewModel)
}

val all = listOf(CoreModule, DbModule, DomainModule, UiModule)
Expand Down
34 changes: 34 additions & 0 deletions app/src/main/java/com/xinto/mauth/domain/AuthRepository.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.xinto.mauth.domain

import com.xinto.mauth.core.auth.AuthManager
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.last
import kotlinx.coroutines.flow.map

class AuthRepository(
private val authManager: AuthManager
) {

private val liveCode = authManager.getCode()

fun observeIsProtected(): Flow<Boolean> {
return liveCode.map { it != null }
}

suspend fun isProtected(): Boolean {
return liveCode.first() != null
}

suspend fun validate(code: String): Boolean {
return liveCode.first() == code
}

fun updateCode(code: String) {
authManager.setCode(code)
}

fun removeCode() {
authManager.removeCode()
}
}
46 changes: 44 additions & 2 deletions app/src/main/java/com/xinto/mauth/ui/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,21 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import com.xinto.mauth.domain.AuthRepository
import com.xinto.mauth.domain.OtpRepository
import com.xinto.mauth.domain.SettingsRepository
import com.xinto.mauth.domain.model.DomainAccountInfo
import com.xinto.mauth.ui.navigation.MauthDestination
import com.xinto.mauth.ui.screen.account.AddAccountScreen
import com.xinto.mauth.ui.screen.account.EditAccountScreen
import com.xinto.mauth.ui.screen.auth.AuthScreen
import com.xinto.mauth.ui.screen.home.HomeScreen
import com.xinto.mauth.ui.screen.pinremove.PinRemoveScreen
import com.xinto.mauth.ui.screen.pinsetup.PinSetupScreen
import com.xinto.mauth.ui.screen.qrscan.QrScanScreen
import com.xinto.mauth.ui.screen.settings.SettingsScreen
import com.xinto.mauth.ui.theme.MauthTheme
Expand All @@ -37,12 +42,14 @@ import dev.olshevski.navigation.reimagined.rememberNavController
import dev.olshevski.navigation.reimagined.replaceAll
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
import org.koin.android.ext.android.inject

class MainActivity : ComponentActivity() {
class MainActivity : FragmentActivity() {

private val settings: SettingsRepository by inject()
private val otp: OtpRepository by inject()
private val auth: AuthRepository by inject()

override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
Expand All @@ -63,13 +70,21 @@ class MainActivity : ComponentActivity() {
}
.launchIn(lifecycleScope)

val initialScreen = runBlocking {
if (auth.isProtected()) {
MauthDestination.Auth
} else {
MauthDestination.Home
}
}

setContent {
MauthTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
val navigator = rememberNavController<MauthDestination>(MauthDestination.Home)
val navigator = rememberNavController(initialScreen)

LaunchedEffect(intent.data) {
val accountInfo = otp.parseUriToAccountInfo(intent.data.toString())
Expand Down Expand Up @@ -104,6 +119,13 @@ class MainActivity : ComponentActivity() {
}
) { screen ->
when (screen) {
is MauthDestination.Auth -> {
AuthScreen(
onAuthSuccess = {
navigator.replaceAll(MauthDestination.Home)
}
)
}
is MauthDestination.Home -> {
HomeScreen(
onAddAccountManually = {
Expand Down Expand Up @@ -138,6 +160,12 @@ class MainActivity : ComponentActivity() {
SettingsScreen(
onBack = {
navigator.pop()
},
onSetupPinCode = {
navigator.navigate(MauthDestination.PinSetup)
},
onDisablePinCode = {
navigator.navigate(MauthDestination.PinRemove)
}
)
}
Expand All @@ -157,6 +185,20 @@ class MainActivity : ComponentActivity() {
}
)
}
is MauthDestination.PinSetup -> {
PinSetupScreen(
onExit = {
navigator.pop()
}
)
}
is MauthDestination.PinRemove -> {
PinRemoveScreen(
onExit = {
navigator.pop()
}
)
}
}
}
}
Expand Down
Loading

0 comments on commit d6ecb87

Please sign in to comment.