Initial commit — SyncFlow Android file sync app
Supports WebDAV, SFTP, SFTPGo, Nextcloud, ownCloud, Google Drive, Dropbox, and OneDrive. Credentials encrypted with Android Keystore. Biometric app-lock, conflict resolution, and auto-sync via WorkManager. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
package com.syncflow.ui.auth
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.util.Base64
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import com.syncflow.data.security.CredentialStore
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.security.MessageDigest
|
||||
import java.security.SecureRandom
|
||||
|
||||
const val OAUTH_REDIRECT_ACTION = "com.syncflow.OAUTH_RESULT"
|
||||
const val OAUTH_EXTRA_TOKEN = "token"
|
||||
const val OAUTH_EXTRA_EMAIL = "email"
|
||||
const val OAUTH_EXTRA_PROVIDER = "provider"
|
||||
|
||||
private val client = OkHttpClient()
|
||||
private val random = SecureRandom()
|
||||
|
||||
private fun generateVerifier(): String {
|
||||
val bytes = ByteArray(32)
|
||||
random.nextBytes(bytes)
|
||||
return Base64.encodeToString(bytes, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
|
||||
}
|
||||
|
||||
private fun generateChallenge(verifier: String): String {
|
||||
val digest = MessageDigest.getInstance("SHA-256").digest(verifier.toByteArray(Charsets.US_ASCII))
|
||||
return Base64.encodeToString(digest, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
|
||||
}
|
||||
|
||||
fun launchDropboxOAuth(context: Context, credentialStore: CredentialStore, appKey: String) {
|
||||
val verifier = generateVerifier()
|
||||
credentialStore.savePkceVerifier("dropbox", verifier)
|
||||
val challenge = generateChallenge(verifier)
|
||||
val url = "https://www.dropbox.com/oauth2/authorize" +
|
||||
"?client_id=$appKey" +
|
||||
"&response_type=code" +
|
||||
"&redirect_uri=syncflow%3A%2F%2Foauth%2Fdropbox" +
|
||||
"&code_challenge=$challenge" +
|
||||
"&code_challenge_method=S256" +
|
||||
"&token_access_type=offline"
|
||||
openCustomTab(context, url)
|
||||
}
|
||||
|
||||
fun launchOneDriveOAuth(context: Context, credentialStore: CredentialStore, clientId: String) {
|
||||
val verifier = generateVerifier()
|
||||
credentialStore.savePkceVerifier("onedrive", verifier)
|
||||
val challenge = generateChallenge(verifier)
|
||||
val scopes = "Files.ReadWrite+User.Read+offline_access"
|
||||
val url = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize" +
|
||||
"?client_id=$clientId" +
|
||||
"&response_type=code" +
|
||||
"&redirect_uri=syncflow%3A%2F%2Foauth%2Fonedrive" +
|
||||
"&scope=$scopes" +
|
||||
"&code_challenge=$challenge" +
|
||||
"&code_challenge_method=S256"
|
||||
openCustomTab(context, url)
|
||||
}
|
||||
|
||||
private fun openCustomTab(context: Context, url: String) {
|
||||
try {
|
||||
CustomTabsIntent.Builder().build().launchUrl(context, Uri.parse(url))
|
||||
} catch (_: Exception) {
|
||||
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK })
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun exchangeDropboxCode(
|
||||
credentialStore: CredentialStore,
|
||||
code: String,
|
||||
appKey: String,
|
||||
): Pair<String, String>? = withContext(Dispatchers.IO) {
|
||||
val verifier = credentialStore.getPkceVerifier("dropbox") ?: return@withContext null
|
||||
credentialStore.removePkceVerifier("dropbox")
|
||||
val body = FormBody.Builder()
|
||||
.add("code", code)
|
||||
.add("grant_type", "authorization_code")
|
||||
.add("client_id", appKey)
|
||||
.add("redirect_uri", "syncflow://oauth/dropbox")
|
||||
.add("code_verifier", verifier)
|
||||
.build()
|
||||
val req = Request.Builder().url("https://api.dropboxapi.com/oauth2/token").post(body).build()
|
||||
val resp = runCatching { client.newCall(req).execute() }.getOrNull() ?: return@withContext null
|
||||
val text = resp.body?.string() ?: return@withContext null
|
||||
val json = runCatching { Json.parseToJsonElement(text).jsonObject }.getOrNull() ?: return@withContext null
|
||||
val token = json["access_token"]?.jsonPrimitive?.content ?: return@withContext null
|
||||
val email = json["account_id"]?.jsonPrimitive?.content ?: ""
|
||||
Pair(token, email)
|
||||
}
|
||||
|
||||
suspend fun exchangeOneDriveCode(
|
||||
credentialStore: CredentialStore,
|
||||
code: String,
|
||||
clientId: String,
|
||||
): Pair<String, String>? = withContext(Dispatchers.IO) {
|
||||
val verifier = credentialStore.getPkceVerifier("onedrive") ?: return@withContext null
|
||||
credentialStore.removePkceVerifier("onedrive")
|
||||
val body = FormBody.Builder()
|
||||
.add("code", code)
|
||||
.add("grant_type", "authorization_code")
|
||||
.add("client_id", clientId)
|
||||
.add("redirect_uri", "syncflow://oauth/onedrive")
|
||||
.add("code_verifier", verifier)
|
||||
.add("scope", "Files.ReadWrite User.Read offline_access")
|
||||
.build()
|
||||
val req = Request.Builder().url("https://login.microsoftonline.com/common/oauth2/v2.0/token").post(body).build()
|
||||
val resp = runCatching { client.newCall(req).execute() }.getOrNull() ?: return@withContext null
|
||||
val text = resp.body?.string() ?: return@withContext null
|
||||
val json = runCatching { Json.parseToJsonElement(text).jsonObject }.getOrNull() ?: return@withContext null
|
||||
val token = json["access_token"]?.jsonPrimitive?.content ?: return@withContext null
|
||||
Pair(token, "")
|
||||
}
|
||||
Reference in New Issue
Block a user