security: fix all review findings, bump to 1.0.19 (build 20)
CRITICAL - SftpProvider: replace PromiscuousVerifier with TofuHostKeyVerifier (trust-on-first-use; stores SHA-256 fingerprints in EncryptedSharedPreferences; rejects key changes on subsequent connections) HIGH - GoogleDriveProvider: replace raw string interpolation with buildJsonObject in uploadFile, createDirectory, and moveFile to prevent JSON injection - DropboxProvider: replace all raw JSON strings and Dropbox-API-Arg headers with buildJsonObject for the same reason - OAuthHelper: add cryptographically random state parameter to Dropbox and OneDrive authorization URLs (stored alongside the PKCE verifier) - OAuthRedirectActivity: validate returned state against stored value before exchanging the authorization code (CSRF protection) MEDIUM - WebDavProvider: block cross-host redirects in the manual redirect interceptor so Authorization headers are never forwarded to a different server - AccountSetupScreen: set FLAG_SECURE on the window while credential fields are visible to prevent screenshots and screen-recording capture - libs.versions.toml: security-crypto alpha06 → stable 1.0.0; biometric-ktx alpha05 → biometric 1.1.0 (stable, non-ktx artifact matches the BiometricManager/BiometricPrompt API actually used in MainActivity) - CredentialStore: migrate to security-crypto 1.0.0 API (MasterKeys.getOrCreate + positional create() args); add saveHostKey/getHostFingerprint for SFTP TOFU Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ package com.syncflow.ui.auth
|
||||
|
||||
import android.accounts.AccountManager
|
||||
import android.app.Activity
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.Image
|
||||
@@ -201,6 +202,15 @@ private fun CredentialContent(
|
||||
) {
|
||||
val provider = state.providerType ?: return
|
||||
|
||||
// Prevent screenshots and screen recording while credentials are visible
|
||||
val activity = LocalContext.current as? Activity
|
||||
DisposableEffect(Unit) {
|
||||
activity?.window?.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
onDispose {
|
||||
activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.padding(horizontal = 20.dp)
|
||||
|
||||
@@ -38,7 +38,9 @@ private fun generateChallenge(verifier: String): String {
|
||||
|
||||
fun launchDropboxOAuth(context: Context, credentialStore: CredentialStore, appKey: String) {
|
||||
val verifier = generateVerifier()
|
||||
val state = generateVerifier()
|
||||
credentialStore.savePkceVerifier("dropbox", verifier)
|
||||
credentialStore.savePkceVerifier("dropbox_state", state)
|
||||
val challenge = generateChallenge(verifier)
|
||||
val url = "https://www.dropbox.com/oauth2/authorize" +
|
||||
"?client_id=$appKey" +
|
||||
@@ -46,13 +48,16 @@ fun launchDropboxOAuth(context: Context, credentialStore: CredentialStore, appKe
|
||||
"&redirect_uri=syncflow%3A%2F%2Foauth%2Fdropbox" +
|
||||
"&code_challenge=$challenge" +
|
||||
"&code_challenge_method=S256" +
|
||||
"&token_access_type=offline"
|
||||
"&token_access_type=offline" +
|
||||
"&state=$state"
|
||||
openCustomTab(context, url)
|
||||
}
|
||||
|
||||
fun launchOneDriveOAuth(context: Context, credentialStore: CredentialStore, clientId: String) {
|
||||
val verifier = generateVerifier()
|
||||
val state = generateVerifier()
|
||||
credentialStore.savePkceVerifier("onedrive", verifier)
|
||||
credentialStore.savePkceVerifier("onedrive_state", state)
|
||||
val challenge = generateChallenge(verifier)
|
||||
val scopes = "Files.ReadWrite+User.Read+offline_access"
|
||||
val url = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize" +
|
||||
@@ -61,7 +66,8 @@ fun launchOneDriveOAuth(context: Context, credentialStore: CredentialStore, clie
|
||||
"&redirect_uri=syncflow%3A%2F%2Foauth%2Fonedrive" +
|
||||
"&scope=$scopes" +
|
||||
"&code_challenge=$challenge" +
|
||||
"&code_challenge_method=S256"
|
||||
"&code_challenge_method=S256" +
|
||||
"&state=$state"
|
||||
openCustomTab(context, url)
|
||||
}
|
||||
|
||||
|
||||
@@ -28,11 +28,22 @@ class OAuthRedirectActivity : ComponentActivity() {
|
||||
private fun handleIntent(intent: Intent) {
|
||||
val uri = intent.data ?: run { finish(); return }
|
||||
val code = uri.getQueryParameter("code") ?: run { finish(); return }
|
||||
val returnedState = uri.getQueryParameter("state") ?: run { finish(); return }
|
||||
|
||||
val provider = when {
|
||||
uri.host == "oauth" && uri.path?.contains("dropbox") == true -> "dropbox"
|
||||
uri.host == "oauth" && uri.path?.contains("onedrive") == true -> "onedrive"
|
||||
else -> run { finish(); return }
|
||||
}
|
||||
|
||||
// Validate state before doing anything with the code (CSRF protection)
|
||||
val storedState = credentialStore.getPkceVerifier("${provider}_state")
|
||||
if (storedState == null || returnedState != storedState) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
credentialStore.removePkceVerifier("${provider}_state")
|
||||
|
||||
val appKey = getString(com.syncflow.R.string.dropbox_app_key)
|
||||
val odClientId = getString(com.syncflow.R.string.onedrive_client_id)
|
||||
lifecycleScope.launch {
|
||||
|
||||
Reference in New Issue
Block a user