ultisuite-client/mobile/native/android/sso/SharedSessionProvider.kt
R3D347HR4Y d6d18f911b
Some checks failed
E2E / Playwright e2e (push) Has been cancelled
Lots of stuff and mobile app
2026-06-17 00:13:28 +02:00

76 lines
3.0 KiB
Kotlin

// Android cross-app SSO scaffold.
//
// All suite apps are signed with the SAME key, so a ContentProvider guarded by
// a signature-level permission lets them share the OIDC session that ulti-core
// persists. One app (e.g. UltiMail) hosts the provider; siblings read/write
// through it. Backed by EncryptedSharedPreferences (androidx.security:security-crypto).
//
// Alternative: a custom AccountManager account type ("space.ulti.suite") with a
// shared authenticator — heavier, but integrates with system account settings.
//
// Manifest (host app), plus a matching <uses-permission> in every sibling:
// <permission android:name="space.ulti.suite.permission.SESSION"
// android:protectionLevel="signature" />
// <provider
// android:name=".sso.SharedSessionProvider"
// android:authorities="space.ulti.suite.session"
// android:exported="true"
// android:readPermission="space.ulti.suite.permission.SESSION"
// android:writePermission="space.ulti.suite.permission.SESSION" />
package space.ulti.suite.sso
import android.content.ContentProvider
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.database.MatrixCursor
import android.net.Uri
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
class SharedSessionProvider : ContentProvider() {
override fun onCreate(): Boolean = true
private fun prefs(ctx: Context) = EncryptedSharedPreferences.create(
ctx,
"ulti_shared_session",
MasterKey.Builder(ctx).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build(),
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
)
// query(content://space.ulti.suite.session/<key>) -> single-row "value".
override fun query(
uri: Uri, projection: Array<out String>?, selection: String?,
selectionArgs: Array<out String>?, sortOrder: String?,
): Cursor {
val key = uri.lastPathSegment ?: ""
val value = prefs(context!!).getString(key, null)
return MatrixCursor(arrayOf("value")).apply { addRow(arrayOf(value)) }
}
// insert with values{ key, value } (value=null deletes).
override fun insert(uri: Uri, values: ContentValues?): Uri? {
val key = values?.getAsString("key") ?: return null
val value = values.getAsString("value")
prefs(context!!).edit().apply {
if (value == null) remove(key) else putString(key, value)
}.apply()
return uri
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
val key = uri.lastPathSegment ?: return 0
prefs(context!!).edit().remove(key).apply()
return 1
}
override fun update(
uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?,
): Int = insert(uri, values)?.let { 1 } ?: 0
override fun getType(uri: Uri): String = "vnd.android.cursor.item/ulti.session"
}