diff options
Diffstat (limited to 'app/src/main/kotlin/com')
9 files changed, 177 insertions, 3 deletions
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt index 18ae4b0b..cf8acdd3 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt @@ -42,6 +42,7 @@ import com.pitchedapps.frost.db.save import com.pitchedapps.frost.db.saveTabs import com.pitchedapps.frost.db.selectAll import com.pitchedapps.frost.facebook.FbCookie +import com.pitchedapps.frost.utils.BiometricUtils import com.pitchedapps.frost.utils.EXTRA_COOKIES import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.Prefs @@ -79,6 +80,7 @@ class StartActivity : KauBaseActivity() { } launch { + val authDefer = BiometricUtils.authenticate(this@StartActivity) try { migrate() FbCookie.switchBackUser() @@ -86,6 +88,7 @@ class StartActivity : KauBaseActivity() { L.i { "Cookies loaded at time ${System.currentTimeMillis()}" } L._d { "Cookies: ${cookies.joinToString("\t", transform = CookieEntity::toSensitiveString)}" } loadAssets() + authDefer.await() when { cookies.isEmpty() -> launchNewTask<LoginActivity>() // Has cookies but no selected account diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt index c3089c7a..bc20aa2d 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt @@ -45,6 +45,7 @@ import com.pitchedapps.frost.settings.getDebugPrefs import com.pitchedapps.frost.settings.getExperimentalPrefs import com.pitchedapps.frost.settings.getFeedPrefs import com.pitchedapps.frost.settings.getNotificationPrefs +import com.pitchedapps.frost.settings.getSecurityPrefs import com.pitchedapps.frost.settings.sendDebug import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.Prefs @@ -146,6 +147,11 @@ class SettingsActivity : KPrefActivity() { iicon = GoogleMaterial.Icon.gmd_notifications } + subItems(R.string.security, getSecurityPrefs()) { + descRes = R.string.security_desc + iicon = GoogleMaterial.Icon.gmd_lock + } + // subItems(R.string.network, getNetworkPrefs()) { // descRes = R.string.network_desc // iicon = GoogleMaterial.Icon.gmd_network_cell diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt index f3226e2f..ec4ff9dd 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt @@ -64,6 +64,7 @@ import com.pitchedapps.frost.kotlin.subscribeDuringJob import com.pitchedapps.frost.services.FrostRunnable import com.pitchedapps.frost.utils.ARG_URL import com.pitchedapps.frost.utils.ARG_USER_ID +import com.pitchedapps.frost.utils.BiometricUtils import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.Prefs import com.pitchedapps.frost.utils.Showcase @@ -214,8 +215,10 @@ open class WebOverlayActivityBase(private val forceDesktopAgent: Boolean) : Base userAgentString = USER_AGENT_DESKTOP Prefs.prevId = Prefs.userId launch { + val authDefer = BiometricUtils.authenticate(this@WebOverlayActivityBase) if (userId != Prefs.userId) FbCookie.switchUser(userId) + authDefer.await() reloadBase(true) if (Showcase.firstWebOverlay) { coordinator.frostSnackbar(R.string.web_overlay_swipe_hint) { diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/FrostParser.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/FrostParser.kt index 24c39e28..6af21259 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/FrostParser.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/FrostParser.kt @@ -76,7 +76,7 @@ const val FALLBACK_TIME_MOD = 1000000 data class FrostLink(val text: String, val href: String) -data class ParseResponse<out T: ParseData>(val cookie: String, val data: T) { +data class ParseResponse<out T : ParseData>(val cookie: String, val data: T) { override fun toString() = "ParseResponse\ncookie: $cookie\ndata:\n$data" } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/settings/Debug.kt b/app/src/main/kotlin/com/pitchedapps/frost/settings/Debug.kt index e0ae6de5..1ee06464 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/settings/Debug.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/settings/Debug.kt @@ -52,7 +52,7 @@ import java.io.File */ fun SettingsActivity.getDebugPrefs(): KPrefAdapterBuilder.() -> Unit = { - plainText(R.string.experimental_disclaimer) { + plainText(R.string.disclaimer) { descRes = R.string.debug_disclaimer_info } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/settings/Experimental.kt b/app/src/main/kotlin/com/pitchedapps/frost/settings/Experimental.kt index e0d314a8..41a60594 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/settings/Experimental.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/settings/Experimental.kt @@ -30,7 +30,7 @@ import com.pitchedapps.frost.utils.Showcase */ fun SettingsActivity.getExperimentalPrefs(): KPrefAdapterBuilder.() -> Unit = { - plainText(R.string.experimental_disclaimer) { + plainText(R.string.disclaimer) { descRes = R.string.experimental_disclaimer_info } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/settings/Security.kt b/app/src/main/kotlin/com/pitchedapps/frost/settings/Security.kt new file mode 100644 index 00000000..754e19de --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/settings/Security.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2018 Allan Wang + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package com.pitchedapps.frost.settings + +import ca.allanwang.kau.kpref.activity.KPrefAdapterBuilder +import com.pitchedapps.frost.R +import com.pitchedapps.frost.activities.SettingsActivity +import com.pitchedapps.frost.utils.BiometricUtils +import com.pitchedapps.frost.utils.Prefs +import kotlinx.coroutines.launch + +/** + * Created by Allan Wang on 20179-05-01. + */ +fun SettingsActivity.getSecurityPrefs(): KPrefAdapterBuilder.() -> Unit = { + + plainText(R.string.disclaimer) { + descRes = R.string.security_disclaimer_info + } + + checkbox(R.string.enable_biometrics, Prefs::biometricsEnabled, { + launch { + /* + * For security, we should request authentication when: + * - enabling to ensure that it is supported + * - disabling to ensure that it is permitted + */ + BiometricUtils.authenticate(this@getSecurityPrefs, force = true).await() + Prefs.biometricsEnabled = it + reloadByTitle(R.string.enable_biometrics) + } + }) { + descRes = R.string.enable_biometrics_desc + enabler = { BiometricUtils.isSupported(this@getSecurityPrefs) } + } +} diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/BiometricUtils.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/BiometricUtils.kt new file mode 100644 index 00000000..db901073 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/BiometricUtils.kt @@ -0,0 +1,110 @@ +/* + * Copyright 2019 Allan Wang + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package com.pitchedapps.frost.utils + +import android.content.Context +import android.hardware.fingerprint.FingerprintManager +import android.os.Build +import androidx.biometric.BiometricPrompt +import androidx.fragment.app.FragmentActivity +import ca.allanwang.kau.utils.string +import com.pitchedapps.frost.R +import kotlinx.coroutines.CompletableDeferred +import java.util.concurrent.Executor +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +typealias BiometricDeferred = CompletableDeferred<BiometricPrompt.CryptoObject?> + +/** + * Container for [BiometricPrompt] + * Inspired by coroutine's CommonPool + */ +object BiometricUtils { + + private val executor: Executor + get() = pool ?: getOrCreatePoolSync() + + @Volatile + private var pool: ExecutorService? = null + + private var lastUnlockTime = -1L + + private const val UNLOCK_TIME_INTERVAL = 15 * 60 * 1000 + + /** + * Checks if biometric authentication is possible + * Currently, this means checking for enrolled fingerprints + */ + @Suppress("DEPRECATION") + fun isSupported(context: Context): Boolean { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return false + val fingerprintManager = context.getSystemService(FingerprintManager::class.java) ?: return false + return fingerprintManager.isHardwareDetected && fingerprintManager.hasEnrolledFingerprints() + } + + private fun getOrCreatePoolSync(): Executor = + pool ?: Executors.newSingleThreadExecutor().also { pool = it } + + private fun shouldPrompt(context: Context): Boolean { + return Prefs.biometricsEnabled && System.currentTimeMillis() - lastUnlockTime > UNLOCK_TIME_INTERVAL + } + + fun authenticate(activity: FragmentActivity, force: Boolean = false): BiometricDeferred { + val deferred: BiometricDeferred = CompletableDeferred() + if (!force && !shouldPrompt(activity)) { + deferred.complete(null) + return deferred + } + val info = BiometricPrompt.PromptInfo.Builder() + .setTitle(activity.string(R.string.biometrics_prompt_title)) + .setNegativeButtonText(activity.string(R.string.kau_cancel)) + .build() + BiometricPrompt(activity, executor, Callback(activity, deferred)).authenticate(info) + return deferred + } + + private class Callback(val activity: FragmentActivity, val deferred: BiometricDeferred) : + BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + deferred.cancel() + activity.finish() + } + + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + lastUnlockTime = System.currentTimeMillis() + deferred.complete(result.cryptoObject) + } + + override fun onAuthenticationFailed() { + deferred.cancel() + activity.finish() + } + } + + /** + * For completeness we provide a shutdown function. + * In practice, we initialize the executor only when it is first used, + * and keep it alive throughout the app lifecycle, as it will be used an arbitrary number of times, + * with unknown frequency + */ + @Synchronized + fun shutdown() { + pool?.shutdown() + pool = null + } +} diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt index 391d422a..7656a081 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt @@ -156,6 +156,8 @@ object Prefs : KPref() { var analytics: Boolean by kpref("analytics", true) + var biometricsEnabled: Boolean by kpref("biometrics_enabled", false) + var overlayEnabled: Boolean by kpref("overlay_enabled", true) var overlayFullScreenSwipe: Boolean by kpref("overlay_full_screen_swipe", true) |