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

Protect the nsec with authentication #258

Merged
merged 3 commits into from
Dec 19, 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
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ dependencies {
testImplementation(libs.secp256k1.kmp.jni.jvm)
implementation(libs.spongycastle.core)
implementation(libs.androidx.security.crypto)
implementation(libs.androidx.biometric)

implementation(libs.url.detector)

Expand Down
1 change: 0 additions & 1 deletion app/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@
<ID>LongMethod:FeedNoteCard.kt$@Composable private fun FeedNote( data: FeedPostUi, fullWidthContent: Boolean, avatarSizeDp: Dp, avatarPaddingValues: PaddingValues, notePaddingValues: PaddingValues, enableTweetsMode: Boolean, headerSingleLine: Boolean, showReplyTo: Boolean, forceContentIndent: Boolean, expanded: Boolean, textSelectable: Boolean, showNoteStatCounts: Boolean, noteCallbacks: NoteCallbacks, onPostAction: ((FeedPostAction) -&gt; Unit)? = null, onPostLongClickAction: ((FeedPostAction) -&gt; Unit)? = null, contentFooter: @Composable () -&gt; Unit = {}, )</ID>
<ID>LongMethod:FeedNoteCard.kt$@ExperimentalMaterial3Api @Composable private fun FeedNoteCard( data: FeedPostUi, state: NoteContract.UiState, eventPublisher: (UiEvent) -&gt; Unit, modifier: Modifier = Modifier, shape: Shape = CardDefaults.shape, colors: CardColors = noteCardColors(), cardPadding: PaddingValues = PaddingValues(all = 0.dp), enableTweetsMode: Boolean = false, headerSingleLine: Boolean = true, fullWidthContent: Boolean = false, forceContentIndent: Boolean = false, drawLineAboveAvatar: Boolean = false, drawLineBelowAvatar: Boolean = false, expanded: Boolean = false, textSelectable: Boolean = false, showReplyTo: Boolean = true, noteOptionsMenuEnabled: Boolean = true, showNoteStatCounts: Boolean = true, noteCallbacks: NoteCallbacks = NoteCallbacks(), onGoToWallet: (() -&gt; Unit)? = null, contentFooter: @Composable () -&gt; Unit = {}, )</ID>
<ID>LongMethod:HomeFeedScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun HomeFeedScreen( state: HomeFeedContract.UiState, onTopLevelDestinationChanged: (PrimalTopLevelDestination) -&gt; Unit, onDrawerScreenClick: (DrawerScreenDestination) -&gt; Unit, onDrawerQrCodeClick: () -&gt; Unit, onSearchClick: () -&gt; Unit, noteCallbacks: NoteCallbacks, onGoToWallet: () -&gt; Unit, onNewPostClick: (content: TextFieldValue?) -&gt; Unit, eventPublisher: (UiEvent) -&gt; Unit, )</ID>
<ID>LongMethod:KeysSettingsScreen.kt$@Composable fun PrivateKeySection(nsec: String)</ID>
<ID>LongMethod:LegendaryProfileCustomizationScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun LegendaryProfileCustomizationScreen( state: LegendaryProfileCustomizationContract.UiState, eventPublisher: (LegendaryProfileCustomizationContract.UiEvent) -&gt; Unit, onClose: () -&gt; Unit, )</ID>
<ID>LongMethod:MessageConversationListScreen.kt$@Composable private fun ConversationListItem( conversation: MessageConversationUi, onConversationClick: (String) -&gt; Unit, onProfileClick: (profileId: String) -&gt; Unit, )</ID>
<ID>LongMethod:MessageConversationListScreen.kt$@Composable private fun MessagesTabs( relation: ConversationRelation, onFollowsTabClick: () -&gt; Unit, onOtherTabClick: () -&gt; Unit, onMarkAllRead: () -&gt; Unit, )</ID>
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/kotlin/net/primal/android/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package net.primal.android

import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionLayout
Expand All @@ -16,6 +15,7 @@ import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.produceState
import androidx.compose.ui.graphics.toArgb
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
Expand All @@ -33,7 +33,7 @@ import net.primal.android.user.accounts.active.ActiveAccountStore
import net.primal.android.user.domain.ContentDisplaySettings

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

@Inject
lateinit var themeStore: ActiveThemeStore
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package net.primal.android.core.compose

import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import net.primal.android.R
import net.primal.android.security.BiometricPromptParams
import net.primal.android.security.verifyBiometricIdentity

@Composable
fun BiometricPrompt(
onAuthSuccess: () -> Unit,
onAuthDismiss: () -> Unit,
title: String = stringResource(id = R.string.biometric_prompt_title),
subtitle: String? = null,
description: String? = stringResource(id = R.string.biometric_prompt_description),
onAuthError: ((errorCode: Int, errString: CharSequence) -> Unit)? = null,
) {
val context = LocalContext.current
LaunchedEffect(Unit) {
verifyBiometricIdentity(
activity = context,
onAuthSucceed = {
onAuthDismiss()
onAuthSuccess()
},
onAuthError = { errorCode, errorMessage ->
onAuthDismiss()
onAuthError?.invoke(errorCode, errorMessage)
},
biometricPromptParams = BiometricPromptParams(
title = title,
subtitle = subtitle,
description = description,
),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import com.linkedin.urls.detection.UrlDetector
import com.linkedin.urls.detection.UrlDetectorOptions
import java.net.MalformedURLException
import java.net.URL
import net.primal.android.nostr.ext.parseNostrUris
import net.primal.android.nostr.ext.detectUrls
import net.primal.android.nostr.ext.parseNostrUris
import timber.log.Timber

fun String.parseUris(): List<String> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ private val nostrUriRegexPattern: Pattern = Pattern.compile(

private val urlRegexPattern: Pattern = Pattern.compile(
"https?://(www\\.)?[-a-zA-Z0-9@:%.+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()_@:%+.~#?&//=]*)",
Pattern.CASE_INSENSITIVE
Pattern.CASE_INSENSITIVE,
)

fun String.isNostrUri(): Boolean {
Expand Down
86 changes: 86 additions & 0 deletions app/src/main/kotlin/net/primal/android/security/BiometricHelper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package net.primal.android.security

import android.content.Context
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity

fun verifyBiometricIdentity(
activity: Context,
biometricPromptParams: BiometricPromptParams,
onAuthSucceed: () -> Unit,
onAuthFailed: (() -> Unit)? = null,
onAuthError: ((errorCode: Int, errString: CharSequence) -> Unit)? = null,
) {
if (isBiometricAvailable(activity)) {
showBiometricPrompt(
activity = activity,
params = biometricPromptParams,
onAuthSucceed = onAuthSucceed,
onAuthFailed = onAuthFailed,
onAuthError = onAuthError,
)
} else {
onAuthSucceed()
}
}

private fun isBiometricAvailable(context: Context): Boolean {
val biometricManager = BiometricManager.from(context)
return when (
biometricManager.canAuthenticate(
BiometricManager.Authenticators.BIOMETRIC_STRONG
or BiometricManager.Authenticators.DEVICE_CREDENTIAL,
)
) {
BiometricManager.BIOMETRIC_SUCCESS -> true
else -> false
}
}

private fun showBiometricPrompt(
activity: Context,
params: BiometricPromptParams,
onAuthSucceed: () -> Unit,
onAuthFailed: (() -> Unit)?,
onAuthError: ((errorCode: Int, errString: CharSequence) -> Unit)?,
) {
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle(params.title)
.setSubtitle(params.subtitle)
.setDescription(params.description)
.setConfirmationRequired(false)
.setAllowedAuthenticators(
BiometricManager.Authenticators.BIOMETRIC_STRONG
or BiometricManager.Authenticators.DEVICE_CREDENTIAL,
)
.build()

val biometricPrompt =
BiometricPrompt(
activity as FragmentActivity,
ContextCompat.getMainExecutor(activity),
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
onAuthSucceed()
}

override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
onAuthError?.invoke(errorCode, errString)
}

override fun onAuthenticationFailed() {
onAuthFailed?.invoke()
}
},
)

biometricPrompt.authenticate(promptInfo)
}

data class BiometricPromptParams(
val title: String,
val subtitle: String?,
val description: String?,
)
Loading