Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import com.facebook.react.common.ReleaseLevel
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint
import com.facebook.react.defaults.DefaultReactNativeHost

import com.internxt.cloud.auth.InternxtAuthCredentialsPackage

import expo.modules.ApplicationLifecycleDispatcher
import expo.modules.ReactNativeHostWrapper

Expand All @@ -24,6 +26,7 @@ class MainApplication : Application(), ReactApplication {
override fun getPackages(): List<ReactPackage> =
PackageList(this).packages.apply {
add(ShareIntentPackage())
add(InternxtAuthCredentialsPackage())
}

override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.internxt.cloud.auth

import android.provider.DocumentsContract
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.ReadableMap
import com.internxt.cloud.documents.InternxtDocumentsProvider
import com.internxt.cloud.documents.auth.InternxtAuthManager

class InternxtAuthCredentialsModule(private val ctx: ReactApplicationContext) :
ReactContextBaseJavaModule(ctx) {

private val authManager by lazy { InternxtAuthManager.create(ctx.applicationContext) }

override fun getName() = MODULE_NAME

@ReactMethod
fun setCredentials(map: ReadableMap, promise: Promise) {
try {
val creds = InternxtAuthManager.Credentials(
bearerToken = map.requireString("bearerToken"),
userId = map.requireString("userId"),
bridgeUser = map.requireString("bridgeUser"),
rootFolderUuid = map.requireString("rootFolderUuid"),
email = map.optString("email"),
driveBaseUrl = map.requireString("driveBaseUrl"),
bridgeBaseUrl = map.requireString("bridgeBaseUrl"),
desktopToken = map.optString("desktopToken"),
)
authManager.saveCredentials(creds)
notifyRootsChanged()
promise.resolve(null)
} catch (e: IllegalArgumentException) {
promise.reject("E_MISSING_FIELD", e.message, e)
} catch (e: Exception) {
promise.reject("E_SAVE_CREDENTIALS", e.message, e)
}
}

@ReactMethod
fun clearCredentials(promise: Promise) {
try {
authManager.clear()
notifyRootsChanged()
promise.resolve(null)
} catch (e: Exception) {
promise.reject("E_CLEAR_CREDENTIALS", e.message, e)
}
}

private fun notifyRootsChanged() {
ctx.contentResolver.notifyChange(
DocumentsContract.buildRootsUri(InternxtDocumentsProvider.AUTHORITY),
null,
)
}

private fun ReadableMap.nonBlankString(key: String): String? =
if (hasKey(key) && !isNull(key)) getString(key)?.takeIf { it.isNotBlank() } else null

private fun ReadableMap.requireString(key: String): String =
nonBlankString(key) ?: throw IllegalArgumentException("Missing or blank credential field: $key")

private fun ReadableMap.optString(key: String): String? = nonBlankString(key)

companion object {
const val MODULE_NAME = "InternxtAuthCredentialsModule"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.internxt.cloud.auth

import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager

class InternxtAuthCredentialsPackage : ReactPackage {
override fun createNativeModules(context: ReactApplicationContext): List<NativeModule> =
listOf(InternxtAuthCredentialsModule(context))

override fun createViewManagers(context: ReactApplicationContext): List<ViewManager<*, *>> =
emptyList()
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,35 @@ import android.database.Cursor
import android.database.MatrixCursor
import android.os.CancellationSignal
import android.os.ParcelFileDescriptor
import android.provider.DocumentsContract
import android.provider.DocumentsContract.Document
import android.provider.DocumentsContract.Root
import android.provider.DocumentsProvider
import com.internxt.cloud.R
import com.internxt.cloud.documents.auth.InternxtAuthManager

class InternxtDocumentsProvider : DocumentsProvider() {

override fun onCreate(): Boolean = true
private lateinit var authManager: InternxtAuthManager

override fun onCreate(): Boolean {
authManager = InternxtAuthManager.create(context!!.applicationContext)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Be carefull with not null assertion operator, I think this logic could lead in a unexpected bugs, better return false if there are no context

val ctx = context ?: return false
authManager = InternxtAuthManager.create(ctx.applicationContext)
return true

return true
}

override fun queryRoots(projection: Array<String>?): Cursor {
val cursor = MatrixCursor(resolveRootProjection(projection))
val ctx = context ?: return cursor
cursor.setNotificationUri(ctx.contentResolver, DocumentsContract.buildRootsUri(AUTHORITY))

val rootUuid = authManager.authenticatedRootUuid() ?: return cursor

cursor.newRow().apply {
add(Root.COLUMN_ROOT_ID, ROOT_ID)
add(Root.COLUMN_DOCUMENT_ID, ROOT_DOCUMENT_ID)
add(Root.COLUMN_TITLE, context?.getString(R.string.documents_provider_label))
add(Root.COLUMN_FLAGS, 0)
add(Root.COLUMN_DOCUMENT_ID, rootUuid)
add(Root.COLUMN_TITLE, ctx.getString(R.string.documents_provider_label))
authManager.userEmail()?.let { add(Root.COLUMN_SUMMARY, it) }
add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE or Root.FLAG_SUPPORTS_IS_CHILD)
add(Root.COLUMN_ICON, R.mipmap.ic_launcher)
}
return cursor
Expand Down Expand Up @@ -50,8 +63,7 @@ class InternxtDocumentsProvider : DocumentsProvider() {

companion object {
const val AUTHORITY = "com.internxt.cloud.documents"
private const val ROOT_ID = "root"
private const val ROOT_DOCUMENT_ID = "root"
private const val ROOT_ID = "internxt-root"

private val DEFAULT_ROOT_PROJECTION = arrayOf(
Root.COLUMN_ROOT_ID,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.internxt.cloud.documents.auth

import android.content.Context
import android.content.SharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import com.internxt.cloud.BuildConfig
import com.internxt.cloud.documents.api.AuthConfig

class InternxtAuthManager(private val prefs: SharedPreferences) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this AuthManager in docuemtns instead of the other auth directory?


data class Credentials(
val bearerToken: String,
val userId: String,
val bridgeUser: String,
val rootFolderUuid: String,
val email: String?,
val driveBaseUrl: String,
val bridgeBaseUrl: String,
val desktopToken: String?,
)

fun isLoggedIn(): Boolean =
REQUIRED_KEYS.all { !prefs.getString(it, null).isNullOrBlank() }

fun rootFolderUuid(): String? = prefs.getString(KEY_ROOT_FOLDER_UUID, null)?.takeIf { it.isNotBlank() }

fun authenticatedRootUuid(): String? = if (isLoggedIn()) rootFolderUuid() else null

fun userEmail(): String? = prefs.getString(KEY_EMAIL, null)?.takeIf { it.isNotBlank() }

fun loadAuthConfig(): AuthConfig? {
if (!isLoggedIn()) return null
return AuthConfig(
driveBaseUrl = required(KEY_DRIVE_BASE_URL),
bridgeBaseUrl = required(KEY_BRIDGE_BASE_URL),
bearerToken = required(KEY_BEARER_TOKEN),
bridgeUser = required(KEY_BRIDGE_USER),
userId = required(KEY_USER_ID),
clientName = BuildConfig.INTERNXT_CLIENT_NAME,
clientVersion = BuildConfig.INTERNXT_CLIENT_VERSION,
desktopToken = prefs.getString(KEY_DESKTOP_TOKEN, null)?.takeIf { it.isNotBlank() },
)
}

private fun required(key: String): String =
prefs.getString(key, null) ?: error("$key missing after isLoggedIn() returned true")

fun saveCredentials(creds: Credentials) {
prefs.edit()
.putString(KEY_BEARER_TOKEN, creds.bearerToken)
.putString(KEY_USER_ID, creds.userId)
.putString(KEY_BRIDGE_USER, creds.bridgeUser)
.putString(KEY_ROOT_FOLDER_UUID, creds.rootFolderUuid)
.putString(KEY_EMAIL, creds.email)
.putString(KEY_DRIVE_BASE_URL, creds.driveBaseUrl)
.putString(KEY_BRIDGE_BASE_URL, creds.bridgeBaseUrl)
.putString(KEY_DESKTOP_TOKEN, creds.desktopToken)
.apply()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apply is async, maybe for this it is preferable use commit

}

fun clear() {
prefs.edit().clear().apply()
}

companion object {
private const val PREFS_FILE = "internxt_documents_auth"

private const val KEY_BEARER_TOKEN = "bearerToken"
private const val KEY_USER_ID = "userId"
private const val KEY_BRIDGE_USER = "bridgeUser"
private const val KEY_ROOT_FOLDER_UUID = "rootFolderUuid"
private const val KEY_EMAIL = "email"
private const val KEY_DRIVE_BASE_URL = "driveBaseUrl"
private const val KEY_BRIDGE_BASE_URL = "bridgeBaseUrl"
private const val KEY_DESKTOP_TOKEN = "desktopToken"

private val REQUIRED_KEYS = listOf(
KEY_BEARER_TOKEN,
KEY_USER_ID,
KEY_BRIDGE_USER,
KEY_ROOT_FOLDER_UUID,
KEY_DRIVE_BASE_URL,
KEY_BRIDGE_BASE_URL,
)

fun create(context: Context): InternxtAuthManager {
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
val prefs = EncryptedSharedPreferences.create(
context,
PREFS_FILE,
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
)
return InternxtAuthManager(prefs)
}
Comment on lines +87 to +99
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that is recommendable to wrap this logic in a try/catch and return null on failure so the provider degrades gracefully instead of crashing

fun create(context: Context): InternxtAuthManager? {
      return try {     
          ...
      } catch (e: GeneralSecurityException) {
          Log.e(TAG, "Keystore unavailable, SAF auth disabled", e)                                                                   
          null
      } catch (e: IOException) {                                                                                                     
          Log.e(TAG, "Could not open encrypted prefs, SAF auth disabled", e)                                                       
          null                                                                                                                       
      }                
  }                      

If implement something like the example, then we need to handle the nullable return in onCreate().
We could manage as if create() returns null, onCreate return false

}
}
Loading
Loading