Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Password Protection and biometric authentication by Encrypted Shared Preferences and biometrics in android #125

Merged
merged 3 commits into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ android {
}

dependencies {

implementation("androidx.appcompat:appcompat:1.7.0")
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
Expand Down Expand Up @@ -108,7 +108,8 @@ dependencies {
implementation ("androidx.glance:glance-appwidget:1.0.0")
implementation ("androidx.glance:glance-material3:1.0.0")
implementation ("androidx.lifecycle:lifecycle-livedata-ktx:2.8.6")

implementation("androidx.security:security-crypto:1.1.0-alpha04")
implementation("androidx.biometric:biometric:1.4.0-alpha02")
}

kapt{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,31 @@
package rocks.poopjournal.fucksgiven

import android.app.LocaleConfig
import android.app.LocaleManager
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import android.os.Bundle
import android.os.LocaleList
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.collectAsState
import androidx.core.os.LocaleListCompat
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.navigation.compose.rememberNavController
import dagger.hilt.android.AndroidEntryPoint
import rocks.poopjournal.fucksgiven.data.getPasswordProtectionEnabled
import rocks.poopjournal.fucksgiven.presentation.component.BiometricPromptManager
import rocks.poopjournal.fucksgiven.presentation.navigation.NavGraph
import rocks.poopjournal.fucksgiven.presentation.screens.PasswordPromptScreen
import rocks.poopjournal.fucksgiven.presentation.ui.theme.FucksGivenTheme
import rocks.poopjournal.fucksgiven.presentation.ui.utils.AppTheme
import rocks.poopjournal.fucksgiven.presentation.ui.utils.ThemeSetting
import javax.inject.Inject

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
class MainActivity : AppCompatActivity() {
@Inject
lateinit var themeSetting: ThemeSetting

Expand All @@ -39,7 +41,15 @@ class MainActivity : ComponentActivity() {
AppTheme.DARK -> true
}
FucksGivenTheme(darkTheme = useDarkColors) {
NavGraph(navController = rememberNavController(), themeSetting = themeSetting, context = this)
var isAuthenticated by remember { mutableStateOf(false) }
val isPasswordProtectionEnabled = getPasswordProtectionEnabled(context = this)
if (isAuthenticated || !isPasswordProtectionEnabled) {
NavGraph(navController = rememberNavController(), themeSetting = themeSetting, context = this)
} else {
PasswordPromptScreen(context = this) {
isAuthenticated = true
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package rocks.poopjournal.fucksgiven.data

import android.content.Context
import android.content.SharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys

fun getEncryptedSharedPreferences(context: Context): SharedPreferences {
val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)

return EncryptedSharedPreferences.create(
"secure_prefs",
masterKeyAlias,
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}

fun savePassword(context: Context, password: String) {
val sharedPreferences = getEncryptedSharedPreferences(context)
val editor = sharedPreferences.edit()
editor.putString("user_password", password)
editor.apply()
}

fun getPassword(context: Context): String? {
val sharedPreferences = getEncryptedSharedPreferences(context)
return sharedPreferences.getString("user_password", null)
}

fun clearStoredPassword(context: Context) {
val sharedPreferences = getEncryptedSharedPreferences(context)
val editor = sharedPreferences.edit()
editor.remove("user_password")
editor.apply()
}

fun setPasswordProtectionEnabled(context: Context, enabled: Boolean) {
val sharedPreferences = getEncryptedSharedPreferences(context)
val editor = sharedPreferences.edit()
editor.putBoolean("password_protection_enabled", enabled)
editor.apply()
}

fun getPasswordProtectionEnabled(context: Context): Boolean {
val sharedPreferences = getEncryptedSharedPreferences(context)
return sharedPreferences.getBoolean("password_protection_enabled", false)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package rocks.poopjournal.fucksgiven.presentation.component

import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
import androidx.biometric.BiometricPrompt
import androidx.biometric.BiometricPrompt.PromptInfo
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.receiveAsFlow

class BiometricPromptManager(
private val activity: AppCompatActivity
) {
private val resultChannel = Channel<BiometricResult>()
val promptResults = resultChannel.receiveAsFlow()

fun showBiometricPrompt(
title: String,
description: String
) {
val manager = BiometricManager.from(activity)
val authenticators = if(Build.VERSION.SDK_INT >= 30) {
BIOMETRIC_STRONG or DEVICE_CREDENTIAL
} else BIOMETRIC_STRONG

val promptInfo = PromptInfo.Builder()
.setTitle(title)
.setDescription(description)
.setAllowedAuthenticators(authenticators)

if(Build.VERSION.SDK_INT < 30) {
promptInfo.setNegativeButtonText("Cancel")
}

when(manager.canAuthenticate(authenticators)) {
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> {
resultChannel.trySend(BiometricResult.HardwareUnavailable)
return
}
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
resultChannel.trySend(BiometricResult.FeatureUnavailable)
return
}
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
resultChannel.trySend(BiometricResult.AuthenticationNotSet)
return
}
else -> Unit
}

val prompt = BiometricPrompt(
activity,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
resultChannel.trySend(BiometricResult.AuthenticationError(errString.toString()))
}

override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
resultChannel.trySend(BiometricResult.AuthenticationSuccess)
}

override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
resultChannel.trySend(BiometricResult.AuthenticationFailed)
}
}
)
prompt.authenticate(promptInfo.build())
}

sealed interface BiometricResult {
data object HardwareUnavailable: BiometricResult
data object FeatureUnavailable: BiometricResult
data class AuthenticationError(val error: String): BiometricResult
data object AuthenticationFailed: BiometricResult
data object AuthenticationSuccess: BiometricResult
data object AuthenticationNotSet: BiometricResult
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ package rocks.poopjournal.fucksgiven.presentation.navigation
const val HOME_SCREEN = "HomeScreen"
const val SETTINGS_SCREEN = "SettingsScreen"
const val STATS_SCREEN = "StatsScreen"
const val ABOUT_SCREEN = "AboutScreen"
const val ABOUT_SCREEN = "AboutScreen"
const val PASSWORD_PROMPT_SCREEN = "PasswordPromptScreen"
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import androidx.navigation.compose.composable
import rocks.poopjournal.fucksgiven.presentation.screens.AboutScreen
import rocks.poopjournal.fucksgiven.presentation.viewmodel.HomeViewModel
import rocks.poopjournal.fucksgiven.presentation.screens.HomeScreen
import rocks.poopjournal.fucksgiven.presentation.screens.PasswordPromptScreen
import rocks.poopjournal.fucksgiven.presentation.screens.SettingScreen
import rocks.poopjournal.fucksgiven.presentation.screens.StatsScreen
import rocks.poopjournal.fucksgiven.presentation.ui.utils.ThemeSetting
Expand All @@ -19,7 +20,7 @@ import rocks.poopjournal.fucksgiven.presentation.viewmodel.StatsViewModel

@RequiresApi(Build.VERSION_CODES.P)
@Composable
fun NavGraph(navController: NavHostController,themeSetting: ThemeSetting,context: Context){
fun NavGraph(navController: NavHostController,themeSetting: ThemeSetting, context: Context){
val viewModel : HomeViewModel = hiltViewModel()
val statsViewModel : StatsViewModel = hiltViewModel()
val settingsViewModel : SettingsViewModel = hiltViewModel()
Expand All @@ -37,8 +38,12 @@ fun NavGraph(navController: NavHostController,themeSetting: ThemeSetting,context
}

composable(route = SETTINGS_SCREEN){
SettingScreen(navController = navController, viewModel = settingsViewModel)
SettingScreen(navController = navController, viewModel = settingsViewModel, context = context)
}
composable(route = PASSWORD_PROMPT_SCREEN){
PasswordPromptScreen(context, onAuthenticated = {
navController.navigate(HOME_SCREEN)
})
}

}
}
Loading
Loading