Implement Android Credential Manager and Autofill providers (#109)
Android ARM verification / rust (push) Failing after 49m12s
Android ARM verification / android (push) Failing after 8s
Dependency security audit / rustsec (push) Failing after 21s

This commit is contained in:
hugo committed 2026-10-12 19:18:10 +02:00
1 parent 316a875d63
commit 0c8366d34f
48 files changed
+4138 -78

No files matched your search

+2
View File
@@ -70,6 +70,8 @@ kotlin {
}
dependencies {
implementation(libs.credentials)
implementation(libs.autofill)
implementation(project(":features"))
implementation(project(":core"))
implementation(project(":platform"))
+3 -2
View File
@@ -14,8 +14,8 @@ androidx.appcompat:appcompat-resources:1.2.0=benchmarkLintChecksClasspath,benchm
androidx.appcompat:appcompat:1.2.0=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.arch.core:core-common:2.2.0=benchmarkCompileClasspath,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.arch.core:core-runtime:2.2.0=benchmarkCompileClasspath,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.autofill:autofill:1.0.0=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.biometric:biometric:1.1.0=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.autofill:autofill:1.3.0=benchmarkCompileClasspath,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.biometric:biometric:1.1.0=benchmarkCompileClasspath,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.camera.featurecombinationquery:featurecombinationquery:1.6.2=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.camera.viewfinder:viewfinder-core:1.5.1=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.camera:camera-camera2-pipe:1.6.2=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
@@ -84,6 +84,7 @@ androidx.core:core-backported-fixes:1.0.0=benchmarkLintChecksClasspath,benchmark
androidx.core:core-ktx:1.18.0=benchmarkCompileClasspath,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.core:core-viewtree:1.0.0=benchmarkCompileClasspath,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.core:core:1.18.0=benchmarkCompileClasspath,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.credentials:credentials:1.6.0=benchmarkCompileClasspath,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.cursoradapter:cursoradapter:1.0.0=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.customview:customview-poolingcontainer:1.0.0=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.customview:customview:1.0.0=benchmarkCompileClasspath,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileLintChecksClasspath,profileRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
@@ -0,0 +1,108 @@
package de.rfc1437.ironstorage
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput
import de.rfc1437.ironstorage.core.CredentialCandidate
import de.rfc1437.ironstorage.core.ProviderRequest
import de.rfc1437.ironstorage.features.CredentialSelectionScreen
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
class CredentialSelectionScreenTest {
@get:Rule val compose = createAndroidComposeRule<MainActivity>()
private var selected by mutableStateOf<String?>(null)
private var destination by mutableStateOf("")
private var consent by mutableStateOf(false)
private var overwrite by mutableStateOf(false)
private var query by mutableStateOf("")
private var passphrase by mutableStateOf("")
private var confirmed = 0
private fun display(saving: Boolean) {
compose.activity.runOnUiThread {
compose.activity.setProtectedTestContent {
MaterialTheme {
CredentialSelectionScreen(
ProviderRequest(
"public-test-token",
"example.client",
"revision",
listOf(
CredentialCandidate("example.client/alice", true)
)
),
saving,
selected,
query,
destination,
passphrase,
consent,
overwrite,
false,
null,
onSelect = {
selected = it
consent = false
},
onQuery = { query = it },
onDestination = {
destination = it
consent = false
overwrite = false
},
onPassphrase = { passphrase = it },
onConsent = { consent = it },
onOverwrite = { overwrite = it },
onConfirm = { confirmed++ },
onCancel = {
consent = false
passphrase = ""
}
)
}
}
}
}
@Test fun fillRequiresSelectionAndNamedCallerConsent() {
display(false)
compose.onNodeWithText("Unlock and fill").assertIsNotEnabled()
compose.onNodeWithText("example.client/alice").performClick()
compose.onNodeWithText("Unlock and fill").assertIsNotEnabled()
compose.onNodeWithText(
"Allow example.client to receive this entry's username and password"
).performClick()
compose.onNodeWithText("Unlock and fill").assertIsEnabled().performClick()
assertEquals(1, confirmed)
compose.onNodeWithText("Choose another entry").performClick()
assertEquals(false, consent)
compose.onNodeWithText("Unlock and fill").assertIsNotEnabled()
}
@Test fun saveRequiresDestinationAndChangingItClearsConsents() {
display(true)
compose.onNodeWithText("Confirm save").assertIsNotEnabled()
compose.onNodeWithText("Destination entry path").performTextInput("chosen-entry")
compose.onNodeWithText("Allow updating this destination if it exists").performClick()
compose.onNodeWithText(
"Save the supplied credential at the chosen destination"
).performClick()
compose.onNodeWithText("Confirm save").assertIsEnabled()
compose.onNodeWithText("Destination entry path").performTextInput("-other")
compose.runOnIdle {
assertEquals(false, consent)
assertEquals(false, overwrite)
}
compose.onNodeWithText("Confirm save").assertIsNotEnabled()
compose.onNodeWithText("Cancel").performClick()
assertEquals(0, confirmed)
}
}
@@ -0,0 +1,51 @@
package de.rfc1437.ironstorage
import androidx.test.platform.app.InstrumentationRegistry
import dagger.hilt.EntryPoints
import de.rfc1437.ironstorage.core.CredentialCaller
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import org.junit.Assume.assumeTrue
import org.junit.Test
class ProviderFixtureSetupTest {
/** Opt-in only. Never replaces a configured store or key on the retained emulator. */
@Test fun seedEmptyEmulatorForComputerUse(): Unit = runBlocking {
assumeTrue(InstrumentationRegistry.getArguments().getString("providerFixture") == "true")
val context = InstrumentationRegistry.getInstrumentation().targetContext
val access = EntryPoints.get(context.applicationContext, ProviderFixtureAccess::class.java)
val passphrase = "public-provider-fixture-passphrase"
val setup = access.onboarding()
withContext(Dispatchers.Main.immediate) { setup.refresh() }
val before = withTimeout(30000) { setup.setup.first { !it.busy } }
check(!before.configured && !before.existingClone && before.keys.isEmpty())
withContext(Dispatchers.Main.immediate) {
setup.generate("Provider emulator fixture", passphrase)
}
val generated = withTimeout(30000) {
setup.setup.first {
!it.busy &&
it.selectedKey.isNotEmpty()
}
}
withContext(Dispatchers.Main.immediate) {
setup.createLocal(generated.selectedKey, passphrase)
}
withTimeout(30000) { setup.setup.first { !it.busy && it.configured } }
// Exercise exactly the explicit storage save contract used by both OS services.
val credentials = access.credentials()
val caller = CredentialCaller("de.rfc1437.ironstorage.test", listOf("AA".repeat(32)), null)
val request = credentials.begin(caller)
credentials.stage(
request.token,
caller,
"fixture-alice",
"public-provider-fixture-password"
)
credentials.save(request.token, caller, "provider.example", true, false, passphrase)
withContext(Dispatchers.Main.immediate) { access.authentication().lock() }
}
}
@@ -0,0 +1,108 @@
package de.rfc1437.ironstorage
import android.content.ComponentCallbacks2
import android.content.Intent
import android.os.Build
import androidx.test.platform.app.InstrumentationRegistry
import dagger.hilt.EntryPoints
import de.rfc1437.ironstorage.core.CredentialCaller
import de.rfc1437.ironstorage.providers.ProviderContext
import de.rfc1437.ironstorage.providers.ProviderRequests
import de.rfc1437.ironstorage.providers.installedCaller
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Assume.assumeTrue
import org.junit.Test
class ProviderIntentSecurityTest {
@Test
fun retainedSystemSaveContainsSubmittedFields(): Unit = runBlocking {
assumeTrue(
InstrumentationRegistry.getArguments().getString("providerSavedFixtureValidation") ==
"true"
)
val context = InstrumentationRegistry.getInstrumentation().targetContext
val caller = context.installedCaller("de.rfc1437.ironstorage.providerclient")
val access = EntryPoints.get(context.applicationContext, ProviderFixtureAccess::class.java)
withContext(Dispatchers.Main.immediate) { access.authentication().lock() }
for (path in listOf("sacrd", "afsave")) {
val request = access.credentials().begin(caller)
try {
val value = access.credentials().fill(
request.token,
caller,
path,
true,
"public-provider-fixture-passphrase",
emptyList()
)
assertEquals("fixture-bob", value.username)
assertEquals("public-provider-new-password", value.password)
} finally {
access.credentials().cancel(request.token)
withContext(Dispatchers.Main.immediate) { access.authentication().lock() }
}
}
}
@Suppress("DEPRECATION")
@Test
fun retainedFixtureCallerCanPrepareMetadata(): Unit = runBlocking {
assumeTrue(
InstrumentationRegistry.getArguments().getString("providerFixtureValidation") == "true"
)
val context = InstrumentationRegistry.getInstrumentation().targetContext
val caller = context.installedCaller("de.rfc1437.ironstorage.providerclient")
val access = EntryPoints.get(context.applicationContext, ProviderFixtureAccess::class.java)
withContext(Dispatchers.Main.immediate) { access.authentication().lock() }
val request = access.credentials().begin(caller)
try {
assertTrue(request.candidates.any { it.path == "provider.example" })
withContext(Dispatchers.Main.immediate) {
val app = context.applicationContext as IronStorageApplication
app.onTrimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN)
app.onTrimMemory(ComponentCallbacks2.TRIM_MEMORY_BACKGROUND)
}
assertTrue(access.credentials().request(request.token, caller).candidates.isNotEmpty())
withContext(Dispatchers.Main.immediate) {
(context.applicationContext as IronStorageApplication).onTrimMemory(
ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW
)
}
assertTrue(
runCatching {
access.credentials().request(request.token, caller)
}.isFailure
)
} finally {
access.credentials().cancel(request.token)
}
}
@Test fun inlineAttributionCannotInvokeThePrivateAuthenticationActivity() {
assumeTrue(Build.VERSION.SDK_INT >= 31)
val context = InstrumentationRegistry.getInstrumentation().targetContext
assertNull(ProviderRequests.get(Intent()))
val token = "public-pending-intent-fixture"
val caller = CredentialCaller("example.client", listOf("AA".repeat(32)), null)
try {
val authentication = ProviderRequests.put(
context,
ProviderContext(token, caller, "autofill"),
"fixture"
)
val attribution = ProviderRequests.attribution(context)
assertTrue(attribution.isImmutable)
assertFalse(authentication.isImmutable)
assertNotEquals(authentication, attribution)
} finally {
ProviderRequests.remove(token)
}
}
}
@@ -0,0 +1,16 @@
package de.rfc1437.ironstorage
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import de.rfc1437.ironstorage.core.NativeAuthenticationRepository
import de.rfc1437.ironstorage.core.NativeCredentialRepository
import de.rfc1437.ironstorage.core.OnboardingRepository
@EntryPoint
@InstallIn(SingletonComponent::class)
interface ProviderFixtureAccess {
fun onboarding(): OnboardingRepository
fun authentication(): NativeAuthenticationRepository
fun credentials(): NativeCredentialRepository
}
+17
View File
@@ -3,9 +3,14 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.CAMERA" />
<!-- OS-mediated clients need signer verification even without discoverable intent filters. -->
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" tools:node="remove" />
<uses-feature android:name="android.hardware.camera.any" android:required="false" />
<!-- Credential Manager is a provider on API 34+; API 26–33 uses framework Autofill. -->
<application
tools:ignore="CredentialDependency"
android:name=".IronStorageApplication"
android:allowBackup="false"
android:fullBackupContent="@xml/backup_rules"
@@ -15,6 +20,18 @@
android:supportsRtl="true"
android:theme="@style/Theme.IronStorage"
android:usesCleartextTraffic="false">
<service android:name=".providers.IronAutofillService" android:exported="true"
android:permission="android.permission.BIND_AUTOFILL_SERVICE">
<intent-filter><action android:name="android.service.autofill.AutofillService" /></intent-filter>
<meta-data android:name="android.autofill" android:resource="@xml/autofill_service" />
</service>
<service tools:targetApi="34" android:name=".providers.IronCredentialProviderService" android:exported="true" android:enabled="@bool/credential_provider_enabled"
android:permission="android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE">
<intent-filter><action android:name="android.service.credentials.CredentialProviderService" /></intent-filter>
<meta-data android:name="android.credentials.provider" android:resource="@xml/credential_provider" />
</service>
<activity android:name=".providers.CredentialSelectionActivity" android:exported="false"
android:excludeFromRecents="true" android:taskAffinity="" android:windowSoftInputMode="adjustResize" />
<activity android:name=".MainActivity" android:exported="true"
android:windowSoftInputMode="adjustResize"
android:supportsPictureInPicture="false">
@@ -1,6 +1,7 @@
package de.rfc1437.ironstorage
import android.app.Application
import android.content.ComponentCallbacks2
import dagger.hilt.android.HiltAndroidApp
import de.rfc1437.ironstorage.core.AuthenticationLifecycle
import de.rfc1437.ironstorage.core.AuthenticationRepository
@@ -16,7 +17,12 @@ class IronStorageApplication : Application() {
@Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
override fun onTrimMemory(level: Int) {
authentication.lifecycle(AuthenticationLifecycle.MemoryPressure)
// UI/cache transitions also occur while the system picker holds a non-secret request.
if (level < ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN ||
level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE
) {
authentication.lifecycle(AuthenticationLifecycle.MemoryPressure)
}
super.onTrimMemory(level)
}
@@ -38,6 +38,13 @@ import kotlinx.coroutines.SupervisorJob
@Module
@InstallIn(SingletonComponent::class)
object ShellModule {
@Provides
@Singleton
fun credentials(
authentication: NativeAuthenticationRepository
): de.rfc1437.ironstorage.core.NativeCredentialRepository =
de.rfc1437.ironstorage.core.NativeCredentialRepository(authentication)
@Provides
@Singleton
fun androidSettingsPlatform(
@@ -0,0 +1,362 @@
package de.rfc1437.ironstorage.providers
import android.app.KeyguardManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.os.Bundle
import android.service.autofill.Dataset
import android.view.View
import android.view.WindowManager
import android.view.autofill.AutofillManager
import android.view.autofill.AutofillManager.EXTRA_AUTHENTICATION_RESULT_EPHEMERAL_DATASET
import android.view.autofill.AutofillValue
import android.widget.RemoteViews
import androidx.activity.compose.setContent
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.credentials.CreatePasswordRequest
import androidx.credentials.CreatePasswordResponse
import androidx.credentials.GetCredentialResponse
import androidx.credentials.GetPasswordOption
import androidx.credentials.PasswordCredential
import androidx.credentials.provider.PendingIntentHandler
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import de.rfc1437.ironstorage.core.AuthenticationLifecycle
import de.rfc1437.ironstorage.core.AuthenticationRepository
import de.rfc1437.ironstorage.core.NativeCredentialRepository
import de.rfc1437.ironstorage.core.ProviderRequest
import de.rfc1437.ironstorage.platform.AndroidKeystoreAdapter
import javax.inject.Inject
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
@AndroidEntryPoint
class CredentialSelectionActivity : FragmentActivity() {
@Inject lateinit var credentials: NativeCredentialRepository
@Inject lateinit var authentication: AuthenticationRepository
@Inject lateinit var keystore: AndroidKeystoreAdapter
private var context: ProviderContext? = null
private var request by mutableStateOf<ProviderRequest?>(null)
private var selected by mutableStateOf<String?>(null)
private var query by mutableStateOf("")
private var destination by mutableStateOf("")
private var passphrase by mutableStateOf("")
private var consent by mutableStateOf(false)
private var overwrite by mutableStateOf(false)
private var busy by mutableStateOf(false)
private var failure by mutableStateOf<String?>(null)
private var delivered = false
private var searchSequence = 0
private var cleanup: AutoCloseable? = null
private val screenState = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_SCREEN_OFF) cancel()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
window.decorView.filterTouchesWhenObscured = true
cleanup = authentication.presentation.register {
passphrase = ""
if (!delivered) finish()
}
val screenFilter = IntentFilter(Intent.ACTION_SCREEN_OFF)
if (Build.VERSION.SDK_INT >=
33
) {
registerReceiver(screenState, screenFilter, RECEIVER_NOT_EXPORTED)
} else {
@Suppress("UnspecifiedRegisterReceiverFlag")
registerReceiver(screenState, screenFilter)
}
if (Build.VERSION.SDK_INT >= 33) setRecentsScreenshotEnabled(false)
window.decorView.importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS
if (Build.VERSION.SDK_INT >=
30
) {
window.decorView.importantForContentCapture =
View.IMPORTANT_FOR_CONTENT_CAPTURE_NO_EXCLUDE_DESCENDANTS
}
setResult(RESULT_CANCELED)
keystore.attach(this)
context = ProviderRequests.get(intent)
selected = context?.selected
if (context == null || savedInstanceState != null ||
getSystemService(KeyguardManager::class.java).isDeviceLocked
) {
finish()
return
}
lifecycleScope.launch {
try {
val pending = requireNotNull(context)
when (pending.mode) {
"credential" -> {
if (Build.VERSION.SDK_INT < 34) error("Unsupported credential platform")
val supplied =
requireNotNull(
PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
)
require(
credentialCaller(supplied.callingAppInfo, credentials) == pending.caller
)
val options = supplied.credentialOptions
.filterIsInstance<GetPasswordOption>()
require(
options.size == 1 &&
options.single().allowedUserIds.toList().sorted() ==
pending.allowedUsernames.sorted()
)
}
"create" -> {
if (Build.VERSION.SDK_INT < 34) error("Unsupported credential platform")
val supplied =
requireNotNull(
PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
)
require(
credentialCaller(supplied.callingAppInfo, credentials) == pending.caller
)
val password = supplied.callingRequest as CreatePasswordRequest
credentials.stage(
pending.token,
pending.caller,
password.id,
password.password
)
}
"autofill" -> {
@Suppress("DEPRECATION")
val structure =
requireNotNull(
intent.getParcelableExtra<android.app.assist.AssistStructure>(
AutofillManager.EXTRA_ASSIST_STRUCTURE
)
)
val facts = autofillFacts(structure, credentials)
require(
facts.caller == pending.caller && facts.username == pending.username &&
facts.password == pending.password
)
}
"save" -> require(installedCaller(pending.caller.packageName) == pending.caller)
else -> error("Unsupported request")
}
// System requests can contain secrets. Retain only the bound metadata.
intent = Intent().setAction(intent.action).setData(intent.data)
request = credentials.request(pending.token, pending.caller)
} catch (_: Exception) {
cancel()
return@launch
}
while (!isFinishing) {
delay(1_000)
if (!busy) {
try {
credentials.request(pendingToken(), requireNotNull(context).caller, query)
} catch (
_: Exception
) {
cancel()
return@launch
}
}
}
}
setContent {
de.rfc1437.ironstorage.features.ProtectedContent(authentication.presentation) {
de.rfc1437.ironstorage.features.ApplicationTheme(authentication) {
de.rfc1437.ironstorage.features.CredentialSelectionScreen(
request, context?.mode in listOf("create", "save"), selected, query,
destination, passphrase, consent, overwrite, busy, failure,
onSelect = {
selected = it
consent = false
},
onQuery = {
query = it
search()
},
onDestination = {
destination = it
consent = false
overwrite = false
},
onPassphrase = { passphrase = it },
onConsent = { consent = it },
onOverwrite = { overwrite = it },
onConfirm = ::confirm,
onCancel = ::cancel
)
}
}
}
}
private fun pendingToken() = requireNotNull(context).token
private fun search() {
val sequence = ++searchSequence
lifecycleScope.launch {
delay(150)
if (sequence != searchSequence || busy) return@launch
try {
val result = credentials.request(
pendingToken(),
requireNotNull(context).caller,
query
)
if (sequence == searchSequence) request = result
} catch (_: Exception) {
if (sequence == searchSequence && !busy) cancel()
}
}
}
private fun confirm() {
if (busy) return
searchSequence++
busy = true
failure = null
val suppliedPassphrase = passphrase.takeIf { it.isNotEmpty() }
passphrase = ""
lifecycleScope.launch {
try {
val pending = requireNotNull(context)
check(!getSystemService(KeyguardManager::class.java).isDeviceLocked)
val caller = installedCaller(pending.caller.packageName)
.copy(origin = pending.caller.origin)
val output = Intent()
withTimeout(120_000) {
when (pending.mode) {
"create", "save" -> {
credentials.save(
pending.token,
caller,
destination,
consent,
overwrite,
suppliedPassphrase
)
if (pending.mode ==
"create"
) {
PendingIntentHandler.setCreateCredentialResponse(
output,
CreatePasswordResponse()
)
}
}
else -> {
val value = credentials.fill(
pending.token,
caller,
requireNotNull(selected),
consent,
suppliedPassphrase,
pending.allowedUsernames
)
if (pending.mode == "credential") {
PendingIntentHandler.setGetCredentialResponse(
output,
GetCredentialResponse(
PasswordCredential(value.username, value.password)
)
)
} else {
val display = RemoteViews(
packageName,
android.R.layout.simple_list_item_1
).apply {
setTextViewText(android.R.id.text1, requireNotNull(selected))
}
@Suppress("DEPRECATION")
val dataset = Dataset.Builder(display)
.setValue(
requireNotNull(pending.username),
AutofillValue.forText(value.username)
)
.setValue(
requireNotNull(pending.password),
AutofillValue.forText(value.password)
).build()
output.putExtra(
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
dataset
)
if (Build.VERSION.SDK_INT >=
31
) {
output.putExtra(
EXTRA_AUTHENTICATION_RESULT_EPHEMERAL_DATASET,
true
)
}
}
}
}
}
delivered = true
ProviderRequests.remove(pending.token)
setResult(RESULT_OK, output)
finish()
} catch (_: Exception) {
failure = authentication.state.value.failure?.let { value ->
"${value.title}\n${value.detail}"
} ?: "The request could not be completed. Cancel and request again."
} finally {
busy = false
}
}
}
private fun cancel() {
authentication.cancel()
passphrase = ""
setResult(RESULT_CANCELED)
finish()
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (!hasFocus && !keystore.authenticationPending && !delivered) {
passphrase = ""
authentication.lifecycle(AuthenticationLifecycle.Background)
finish()
}
}
override fun onUserLeaveHint() {
if (!keystore.authenticationPending && !delivered) cancel()
super.onUserLeaveHint()
}
override fun onStop() {
if (!keystore.authenticationPending && !delivered) cancel()
super.onStop()
}
override fun onPause() {
passphrase = ""
if (!keystore.authenticationPending) {
authentication.lifecycle(AuthenticationLifecycle.Background)
if (!delivered) finish()
}
super.onPause()
}
override fun onDestroy() {
cleanup?.close()
unregisterReceiver(screenState)
passphrase = ""
context?.let { pending ->
ProviderRequests.remove(pending.token)
}
authentication.lock()
keystore.detach(this)
super.onDestroy()
}
}
@@ -0,0 +1,173 @@
package de.rfc1437.ironstorage.providers
import android.app.assist.AssistStructure
import android.os.Build
import android.os.CancellationSignal
import android.service.autofill.AutofillService
import android.service.autofill.Dataset
import android.service.autofill.FillCallback
import android.service.autofill.FillRequest
import android.service.autofill.FillResponse
import android.service.autofill.InlinePresentation
import android.service.autofill.SaveCallback
import android.service.autofill.SaveInfo
import android.service.autofill.SaveRequest
import android.widget.RemoteViews
import androidx.autofill.inline.UiVersions
import androidx.autofill.inline.v1.InlineSuggestionUi
import dagger.hilt.android.AndroidEntryPoint
import de.rfc1437.ironstorage.core.NativeCredentialRepository
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
@AndroidEntryPoint
class IronAutofillService : AutofillService() {
@Inject lateinit var credentials: NativeCredentialRepository
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
@Suppress("DEPRECATION")
override fun onFillRequest(
request: FillRequest,
cancellationSignal: CancellationSignal,
callback: FillCallback
) {
var token: String? = null
val job = scope.launch {
try {
val facts = autofillFacts(request.fillContexts.last().structure, credentials)
val result = withTimeout(5_000) { credentials.begin(facts.caller) }
token = result.token
if (cancellationSignal.isCanceled) {
credentials.cancel(result.token)
return@launch
}
val pending = ProviderRequests.put(
this@IronAutofillService,
ProviderContext(
result.token,
facts.caller,
"autofill",
username = facts.username,
password = facts.password
),
"autofill"
)
val presentation = RemoteViews(
packageName,
android.R.layout.simple_list_item_1
).apply {
setTextViewText(android.R.id.text1, "Unlock and search IronStorage")
}
@Suppress("DEPRECATION")
val dataset = Dataset.Builder(presentation)
.setValue(facts.username, null)
.setValue(facts.password, null)
.setAuthentication(pending.intentSender)
if (Build.VERSION.SDK_INT >= 30) {
val spec = request.inlineSuggestionsRequest
?.inlinePresentationSpecs?.firstOrNull()
if (spec != null &&
UiVersions.getVersions(spec.style).contains(UiVersions.INLINE_UI_VERSION_1)
) {
val content: UiVersions.Content = InlineSuggestionUi.newContentBuilder(
ProviderRequests.attribution(this@IronAutofillService)
).setTitle("IronStorage").setSubtitle("Unlock and search").build()
dataset.setInlinePresentation(
InlinePresentation(content.slice, spec, false)
)
}
}
val response = FillResponse.Builder().addDataset(dataset.build())
.setSaveInfo(
SaveInfo.Builder(
SaveInfo.SAVE_DATA_TYPE_USERNAME or SaveInfo.SAVE_DATA_TYPE_PASSWORD,
arrayOf(facts.username, facts.password)
).build()
)
.build()
callback.onSuccess(response)
} catch (_: Exception) {
if (!cancellationSignal.isCanceled) callback.onSuccess(null)
}
}
cancellationSignal.setOnCancelListener {
job.cancel()
token?.let { value ->
ProviderRequests.remove(value)
scope.launch { credentials.cancel(value) }
}
}
}
override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
scope.launch {
try {
val structure = request.fillContexts.last().structure
val facts = autofillFacts(structure, credentials)
fun value(id: android.view.autofill.AutofillId): String {
fun find(node: AssistStructure.ViewNode): String? {
if (node.autofillId ==
id
) {
return node.autofillValue?.takeIf {
it.isText
}?.textValue?.toString()
}
for (index in 0 until node.childCount) {
find(node.getChildAt(index))?.let {
return it
}
}
return null
}
for (index in 0 until structure.windowNodeCount) {
find(
structure.getWindowNodeAt(index).rootViewNode
)?.let {
return it
}
}
error("The system did not supply the credential field.")
}
val result = withTimeout(5_000) {
credentials.begin(facts.caller).also {
credentials.stage(
it.token,
facts.caller,
value(facts.username),
value(facts.password)
)
}
}
val pending = ProviderRequests.put(
this@IronAutofillService,
ProviderContext(result.token, facts.caller, "save"),
"save"
)
if (Build.VERSION.SDK_INT >= 28) {
callback.onSuccess(pending.intentSender)
} else {
pending.send()
callback.onSuccess()
}
} catch (
_: Exception
) {
callback.onFailure(
"IronStorage could not offer this save. Open the entry editor to save manually."
)
}
}
}
override fun onDestroy() {
scope.cancel()
super.onDestroy()
}
}
@@ -0,0 +1,180 @@
package de.rfc1437.ironstorage.providers
import android.os.CancellationSignal
import android.os.OutcomeReceiver
import androidx.annotation.RequiresApi
import androidx.credentials.exceptions.ClearCredentialException
import androidx.credentials.exceptions.CreateCredentialException
import androidx.credentials.exceptions.CreateCredentialUnknownException
import androidx.credentials.exceptions.GetCredentialException
import androidx.credentials.exceptions.GetCredentialUnknownException
import androidx.credentials.provider.BeginCreateCredentialRequest
import androidx.credentials.provider.BeginCreateCredentialResponse
import androidx.credentials.provider.BeginCreatePasswordCredentialRequest
import androidx.credentials.provider.BeginGetCredentialRequest
import androidx.credentials.provider.BeginGetCredentialResponse
import androidx.credentials.provider.BeginGetPasswordOption
import androidx.credentials.provider.CreateEntry
import androidx.credentials.provider.CredentialProviderService
import androidx.credentials.provider.PasswordCredentialEntry
import androidx.credentials.provider.ProviderClearCredentialStateRequest
import dagger.hilt.android.AndroidEntryPoint
import de.rfc1437.ironstorage.core.AuthenticationRepository
import de.rfc1437.ironstorage.core.NativeCredentialRepository
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
@RequiresApi(34)
@AndroidEntryPoint
class IronCredentialProviderService : CredentialProviderService() {
@Inject lateinit var credentials: NativeCredentialRepository
@Inject lateinit var authentication: AuthenticationRepository
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
override fun onBeginGetCredentialRequest(
request: BeginGetCredentialRequest,
cancellationSignal: CancellationSignal,
callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>
) {
var token: String? = null
val job = scope.launch {
try {
val caller = credentialCaller(requireNotNull(request.callingAppInfo), credentials)
val options = request.beginGetCredentialOptions
.filterIsInstance<BeginGetPasswordOption>()
require(options.isNotEmpty() && options.size <= 16)
val result = withTimeout(5_000) { credentials.begin(caller) }
token = result.token
if (cancellationSignal.isCanceled) {
credentials.cancel(result.token)
return@launch
}
val budget = (20 / options.size - 1).coerceAtLeast(0)
val entries = options.flatMapIndexed { optionIndex, option ->
val candidates = result.candidates.take(budget).mapIndexed { index, candidate ->
PasswordCredentialEntry(
this@IronCredentialProviderService,
candidate.path,
ProviderRequests.put(
this@IronCredentialProviderService,
ProviderContext(
result.token,
caller,
"credential",
candidate.path,
allowedUsernames = option.allowedUserIds.toList()
),
"$optionIndex/$index"
),
option,
displayName = candidate.path,
isAutoSelectAllowed = false
)
}
// Manual search retains the selected OS option and its account constraint.
val search = PasswordCredentialEntry(
this@IronCredentialProviderService,
"Search IronStorage",
ProviderRequests.put(
this@IronCredentialProviderService,
ProviderContext(
result.token,
caller,
"credential",
allowedUsernames = option.allowedUserIds.toList()
),
"$optionIndex/search"
),
option,
isAutoSelectAllowed = false
)
candidates + search
}
callback.onResult(BeginGetCredentialResponse(credentialEntries = entries))
} catch (_: Exception) {
if (!cancellationSignal.isCanceled) {
callback.onError(
GetCredentialUnknownException("IronStorage could not prepare this request.")
)
}
}
}
cancellationSignal.setOnCancelListener {
job.cancel()
token?.let { value ->
ProviderRequests.remove(value)
scope.launch { credentials.cancel(value) }
}
}
}
override fun onBeginCreateCredentialRequest(
request: BeginCreateCredentialRequest,
cancellationSignal: CancellationSignal,
callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException>
) {
var token: String? = null
val job = scope.launch {
try {
require(request is BeginCreatePasswordCredentialRequest)
val caller = credentialCaller(requireNotNull(request.callingAppInfo), credentials)
val result = withTimeout(5_000) { credentials.begin(caller) }
token = result.token
if (cancellationSignal.isCanceled) {
credentials.cancel(result.token)
return@launch
}
callback.onResult(
BeginCreateCredentialResponse(
listOf(
CreateEntry(
"IronStorage",
ProviderRequests.put(
this@IronCredentialProviderService,
ProviderContext(result.token, caller, "create"),
"create"
),
description = "Choose a destination and confirm saving"
)
)
)
)
} catch (_: Exception) {
if (!cancellationSignal.isCanceled) {
callback.onError(
CreateCredentialUnknownException(
"IronStorage could not prepare this save request."
)
)
}
}
}
cancellationSignal.setOnCancelListener {
job.cancel()
token?.let { value ->
ProviderRequests.remove(value)
scope.launch { credentials.cancel(value) }
}
}
}
override fun onClearCredentialStateRequest(
request: ProviderClearCredentialStateRequest,
cancellationSignal: CancellationSignal,
callback: OutcomeReceiver<Void?, ClearCredentialException>
) {
ProviderRequests.clear()
authentication.lock()
callback.onResult(null)
}
override fun onDestroy() {
scope.cancel()
super.onDestroy()
}
}
@@ -0,0 +1,169 @@
package de.rfc1437.ironstorage.providers
import android.app.PendingIntent
import android.app.assist.AssistStructure
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.view.autofill.AutofillId
import androidx.annotation.RequiresApi
import androidx.credentials.provider.CallingAppInfo
import de.rfc1437.ironstorage.MainActivity
import de.rfc1437.ironstorage.core.CredentialAttribute
import de.rfc1437.ironstorage.core.CredentialCaller
import de.rfc1437.ironstorage.core.CredentialField
import de.rfc1437.ironstorage.core.NativeCredentialRepository
import java.security.MessageDigest
import java.util.concurrent.ConcurrentHashMap
internal data class ProviderContext(
val token: String,
val caller: CredentialCaller,
val mode: String,
val selected: String? = null,
val username: AutofillId? = null,
val password: AutofillId? = null,
val allowedUsernames: List<String> = emptyList(),
val created: Long = android.os.SystemClock.elapsedRealtime()
)
/** Contains only request metadata. Lost process state always requires a new system request. */
internal object ProviderRequests {
private val requests = ConcurrentHashMap<String, ProviderContext>()
fun attribution(context: Context): PendingIntent = PendingIntent.getActivity(
context,
0,
Intent(context, MainActivity::class.java)
.setAction("de.rfc1437.ironstorage.OPEN_PROVIDER"),
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
fun put(context: Context, request: ProviderContext, suffix: String): PendingIntent {
val now = android.os.SystemClock.elapsedRealtime()
requests.entries.removeIf { now - it.value.created >= 120_000 }
require(requests.size < 512)
val key = "${request.token}/$suffix"
requests[key] = request
val intent = Intent(context, CredentialSelectionActivity::class.java)
.setAction("de.rfc1437.ironstorage.SELECT_CREDENTIAL")
.setData(
Uri.Builder().scheme(
"ironstorage-provider"
).authority("request").appendEncodedPath(key).build()
)
return PendingIntent.getActivity(
context,
0,
intent,
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
}
fun get(intent: Intent): ProviderContext? =
intent.data?.path?.removePrefix("/")?.let(requests::get)
fun remove(token: String) {
requests.entries.removeIf { it.value.token == token }
}
fun clear() {
requests.clear()
}
}
internal fun Context.installedCaller(packageName: String): CredentialCaller {
@Suppress("DEPRECATION")
val info = packageManager.getPackageInfo(
packageName,
if (Build.VERSION.SDK_INT >=
28
) {
PackageManager.GET_SIGNING_CERTIFICATES
} else {
PackageManager.GET_SIGNATURES
}
)
@Suppress("DEPRECATION")
val signatures = if (Build.VERSION.SDK_INT >=
28
) {
info.signingInfo?.apkContentsSigners
} else {
info.signatures
}
val fingerprints = requireNotNull(signatures).map { signature ->
MessageDigest.getInstance("SHA-256").digest(signature.toByteArray()).joinToString("") {
"%02X".format(it)
}
}
return CredentialCaller(packageName, fingerprints, null)
}
@RequiresApi(34)
internal suspend fun Context.credentialCaller(
info: CallingAppInfo,
credentials: NativeCredentialRepository
): CredentialCaller {
val installed = installedCaller(info.packageName)
val supplied = info.signingInfoCompat.apkContentsSigners.map { signature ->
MessageDigest.getInstance("SHA-256").digest(signature.toByteArray()).joinToString("") {
"%02X".format(it)
}
}
require(installed.certificates.sorted() == supplied.sorted())
val origin = try {
info.getOrigin(credentials.privilegedCallers())
} catch (
_: IllegalStateException
) {
null
}
return installed.copy(origin = origin)
}
internal data class AutofillFacts(
val caller: CredentialCaller,
val username: AutofillId,
val password: AutofillId
)
internal suspend fun Context.autofillFacts(
structure: AssistStructure,
credentials: NativeCredentialRepository
): AutofillFacts {
val caller = installedCaller(structure.activityComponent.packageName)
val ids = mutableListOf<AutofillId>()
val fields = mutableListOf<CredentialField>()
var visited = 0
fun visit(node: AssistStructure.ViewNode, depth: Int) {
require(depth < 64 && ++visited <= 2048)
node.autofillId?.let { id ->
require(fields.size < 512)
val existing = ids.indexOf(id)
val index = if (existing < 0) {
ids += id
(ids.size - 1).toUInt()
} else {
existing.toUInt()
}
fields += CredentialField(
index,
node.autofillHints?.toList() ?: emptyList(),
node.htmlInfo?.attributes?.map { CredentialAttribute(it.first, it.second) }
?: emptyList(),
node.inputType.toUInt(),
node.isFocused,
node.autofillType == android.view.View.AUTOFILL_TYPE_TEXT
)
}
for (index in 0 until node.childCount) visit(node.getChildAt(index), depth + 1)
}
require(structure.windowNodeCount <= 16)
for (index in 0 until structure.windowNodeCount) {
visit(
structure.getWindowNodeAt(index).rootViewNode,
0
)
}
val form = credentials.form(fields)
return AutofillFacts(caller, ids[form.username.toInt()], ids[form.password.toInt()])
}
@@ -0,0 +1 @@
<resources><bool name="credential_provider_enabled">true</bool></resources>
@@ -0,0 +1 @@
<resources><bool name="credential_provider_enabled">false</bool></resources>
@@ -0,0 +1 @@
<autofill-service xmlns:android="http://schemas.android.com/apk/res/android" android:settingsActivity="de.rfc1437.ironstorage.MainActivity" />
@@ -0,0 +1,3 @@
<credential-provider xmlns:android="http://schemas.android.com/apk/res/android" android:settingsActivity="de.rfc1437.ironstorage.MainActivity">
<capabilities><capability name="android.credentials.TYPE_PASSWORD_CREDENTIAL" /></capabilities>
</credential-provider>
+2
View File
@@ -17,6 +17,7 @@ plugins {
}
val allowed = mapOf(
":provider-client" to emptySet(),
":performance" to setOf(":app"),
":app" to setOf(":features", ":core", ":platform"),
":features" to setOf(":core"),
@@ -133,6 +134,7 @@ tasks.register("dependencyInventory") {
}
tasks.register("androidVerification") {
dependsOn(":provider-client:assembleDebug")
dependsOn("checkArchitecture", "dependencyInventory", "auditNativePackages")
dependsOn(subprojects.map { "${it.path}:ktlintCheck" })
dependsOn(subprojects.filter { it.name != "performance" }.map { "${it.path}:lintDebug" })
@@ -0,0 +1,186 @@
package de.rfc1437.ironstorage.core
import androidx.test.platform.app.InstrumentationRegistry
import de.rfc1437.ironstorage.bridge.generated.MobileEntryEditorFieldKind
import de.rfc1437.ironstorage.bridge.generated.MobileEntryEditorInput
import java.io.File
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
/** Actual native GPG and repository transactions through the production provider facade. */
class NativeCredentialProviderTest {
@Test fun lockedFillAndSavesRejectStaleRequests(): Unit = runBlocking {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val name = NativeEntryViewerTest.rootName
val roots = listOf(context.filesDir, context.noBackupFilesDir, context.cacheDir).map {
File(it, name).apply { mkdirs() }
}
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
val session =
NativePrivateSession(context, {
PrivateDirectories(
roots[0].canonicalPath,
roots[1].canonicalPath,
roots[2].canonicalPath
)
}, NativeEntryViewerTest.MemoryKeystore())
val authentication = NativeAuthenticationRepository(session, scope, Dispatchers.IO)
val credentials = NativeCredentialRepository(authentication)
val caller =
CredentialCaller(
"example.nativeclient",
listOf("AA".repeat(32)),
"https://untrusted.example/"
)
val passphrase = "public-credential-fixture-passphrase"
suspend fun rejects(block: suspend () -> Unit) {
var rejected = false
try {
block()
} catch (_: Exception) {
rejected = true
}
assertTrue(rejected)
}
try {
withContext(Dispatchers.IO) {
session.initialize()
val setup = session.storage().androidOnboarding(session.backend())
val key =
requireNotNull(setup.generate("Credential fixture", passphrase).selectedKey)
setup.setupLocal(key, passphrase)
}
authentication.authenticated { auth ->
val editor = auth.beginCreateEntry("", "example.nativeclient", passphrase)
val page = auth.addEntryEditorField(
editor.id,
MobileEntryEditorFieldKind.FIELD,
"username",
"alice"
)
auth.saveEntryEditor(
editor.id,
page.fields.map {
MobileEntryEditorInput(
it.id,
it.name,
if (it.name ==
"username"
) {
"alice"
} else {
"fixture-password"
}
)
}
)
}
withContext(Dispatchers.Main.immediate) { authentication.lock() }
val request = credentials.begin(caller)
assertEquals("example.nativeclient", request.title)
assertFalse(authentication.state.value.unlocked)
assertTrue(request.candidates.single().matched)
assertEquals("example.nativeclient", request.candidates.single().path)
rejects {
credentials.fill(
request.token,
caller,
"example.nativeclient",
false,
passphrase,
emptyList()
)
}
rejects {
credentials.fill(
request.token,
caller,
"example.nativeclient",
true,
null,
emptyList()
)
}
val value = credentials.fill(
request.token,
caller,
"example.nativeclient",
true,
passphrase,
emptyList()
)
assertEquals("alice", value.username)
assertEquals("fixture-password", value.password)
assertEquals("CredentialValue([REDACTED])", value.toString())
rejects {
credentials.fill(
request.token,
caller,
"example.nativeclient",
true,
null,
emptyList()
)
}
val cancelled = credentials.begin(caller)
credentials.cancel(cancelled.token)
rejects { credentials.request(cancelled.token, caller) }
val stale = credentials.begin(caller)
authentication.authenticated {
it.replaceEntryField("example.nativeclient", 0u, "changed-password")
}
rejects {
credentials.fill(
stale.token,
caller,
"example.nativeclient",
true,
null,
emptyList()
)
}
val saving = credentials.begin(caller)
credentials.stage(saving.token, caller, "bob", "new-password")
rejects {
credentials.save(saving.token, caller, "example.nativeclient", true, false, null)
}
credentials.save(saving.token, caller, "example.nativeclient", true, true, null)
val updated = credentials.begin(caller)
assertEquals(
"bob",
credentials.fill(
updated.token,
caller,
"example.nativeclient",
true,
null,
emptyList()
).username
)
val locked = credentials.begin(caller)
withContext(Dispatchers.Main.immediate) { authentication.lock() }
rejects {
credentials.fill(
locked.token,
caller,
"example.nativeclient",
true,
passphrase,
emptyList()
)
}
} finally {
withContext(Dispatchers.Main.immediate) { authentication.lock() }
scope.cancel()
withContext(Dispatchers.IO) { roots.forEach { it.deleteRecursively() } }
}
}
}
@@ -0,0 +1,61 @@
package de.rfc1437.ironstorage.core
import de.rfc1437.ironstorage.bridge.generated.MobileCredentialAttribute
import de.rfc1437.ironstorage.bridge.generated.MobileCredentialCaller
import de.rfc1437.ironstorage.bridge.generated.MobileCredentialField
import de.rfc1437.ironstorage.bridge.generated.MobileProviderRequest
import de.rfc1437.ironstorage.bridge.generated.mobileCredentialForm
import de.rfc1437.ironstorage.bridge.generated.mobileCredentialPrivilegedCallers
data class CredentialCaller(
val packageName: String,
val certificates: List<String>,
val origin: String?
)
data class CredentialAttribute(val name: String, val value: String)
data class CredentialField(
val index: UInt,
val hints: List<String>,
val htmlAttributes: List<CredentialAttribute>,
val inputType: UInt,
val focused: Boolean,
val editable: Boolean
)
data class CredentialForm(val username: UInt, val password: UInt)
data class CredentialCandidate(val path: String, val matched: Boolean)
data class ProviderRequest(
val token: String,
val title: String,
val revision: String,
val candidates: List<CredentialCandidate>
)
class CredentialValue(val username: String, val password: String) {
override fun toString() = "CredentialValue([REDACTED])"
}
internal fun credentialForm(fields: List<CredentialField>): CredentialForm {
val result = mobileCredentialForm(
fields.map { field ->
MobileCredentialField(
field.index,
field.hints,
field.htmlAttributes.map {
MobileCredentialAttribute(it.name, it.value)
},
field.inputType,
field.focused,
field.editable
)
}
)
return CredentialForm(result.username, result.password)
}
internal fun privilegedCredentialCallers(): String = mobileCredentialPrivilegedCallers()
internal fun CredentialCaller.native() = MobileCredentialCaller(packageName, certificates, origin)
internal fun MobileProviderRequest.facade() = ProviderRequest(
token,
title,
revision,
candidates.map {
CredentialCandidate(it.path, it.matched)
}
)
@@ -46,6 +46,11 @@ class NativeAuthenticationRepository(
scope.launch {
while (true) {
delay(250)
try {
withContext(io) { current?.expireCredentialRequests() }
} catch (_: MobileAuthenticationFfiException) {
lock()
}
if (mutable.value.unlocked && !mutable.value.busy) {
val ticket = sequence.get()
try {
@@ -0,0 +1,56 @@
package de.rfc1437.ironstorage.core
/** Providers use the same Rust owner and lease as the entry viewer. */
class NativeCredentialRepository(private val authentication: NativeAuthenticationRepository) {
suspend fun form(fields: List<CredentialField>): CredentialForm =
authentication.authenticated { credentialForm(fields) }
suspend fun privilegedCallers(): String = authentication.authenticated {
privilegedCredentialCallers()
}
suspend fun begin(caller: CredentialCaller): ProviderRequest =
authentication.authenticated { it.beginCredentialRequest(caller.native()).facade() }
suspend fun request(token: String, caller: CredentialCaller, query: String = "") =
authentication.authenticated {
it.credentialRequest(token, caller.native(), query).facade()
}
suspend fun fill(
token: String,
caller: CredentialCaller,
path: String,
consent: Boolean,
passphrase: String?,
allowedUsernames: List<String>
): CredentialValue = authentication.authenticated {
it.fillCredential(
token,
caller.native(),
path,
consent,
passphrase,
allowedUsernames
).let { value ->
CredentialValue(value.username, value.password)
}
}
suspend fun stage(token: String, caller: CredentialCaller, username: String, password: String) =
authentication.authenticated {
it.stageCredentialSave(token, caller.native(), username, password)
}
suspend fun save(
token: String,
caller: CredentialCaller,
path: String,
consent: Boolean,
overwrite: Boolean,
passphrase: String?
) = authentication.authenticated {
it.saveStagedCredential(token, caller.native(), path, consent, overwrite, passphrase)
}
suspend fun cancel(token: String) = authentication.authenticated {
it.cancelCredentialRequest(token)
}
}
+75 -71
View File
@@ -1,6 +1,5 @@
{
"android": {
"rustls:rustls-platform-verifier:0.1.1": "MIT OR Apache-2.0",
"androidx.activity:activity-compose:1.12.0": "Apache-2.0",
"androidx.activity:activity-compose:1.13.0": "Apache-2.0",
"androidx.activity:activity-ktx:1.12.0": "Apache-2.0",
@@ -12,21 +11,41 @@
"androidx.annotation:annotation-experimental:1.4.1": "Apache-2.0",
"androidx.annotation:annotation-experimental:1.5.1": "Apache-2.0",
"androidx.annotation:annotation-jvm:1.11.0": "Apache-2.0",
"androidx.annotation:annotation-jvm:1.8.1": "Apache-2.0",
"androidx.appcompat:appcompat-resources:1.2.0": "Apache-2.0",
"androidx.appcompat:appcompat:1.2.0": "Apache-2.0",
"androidx.arch.core:core-common:2.1.0": "Apache-2.0",
"androidx.arch.core:core-common:2.2.0": "Apache-2.0",
"androidx.arch.core:core-runtime:2.1.0": "Apache-2.0",
"androidx.arch.core:core-runtime:2.2.0": "Apache-2.0",
"androidx.autofill:autofill:1.0.0": "Apache-2.0",
"androidx.autofill:autofill:1.3.0": "Apache-2.0",
"androidx.benchmark:benchmark-common:1.5.0": "Apache-2.0",
"androidx.benchmark:benchmark-macro-junit4:1.5.0": "Apache-2.0",
"androidx.benchmark:benchmark-macro:1.5.0": "Apache-2.0",
"androidx.benchmark:benchmark-traceprocessor-android:1.5.0": "Apache-2.0",
"androidx.biometric:biometric:1.1.0": "Apache-2.0",
"androidx.camera.featurecombinationquery:featurecombinationquery:1.6.2": "Apache-2.0",
"androidx.camera.viewfinder:viewfinder-core:1.5.1": "Apache-2.0",
"androidx.camera:camera-camera2-pipe:1.6.2": "Apache-2.0",
"androidx.camera:camera-camera2:1.6.2": "Apache-2.0",
"androidx.camera:camera-core:1.6.2": "Apache-2.0 AND BSD-3-Clause",
"androidx.camera:camera-lifecycle:1.6.2": "Apache-2.0",
"androidx.camera:camera-video:1.6.2": "Apache-2.0",
"androidx.camera:camera-view:1.6.2": "Apache-2.0",
"androidx.cardview:cardview:1.0.0": "Apache-2.0",
"androidx.collection:collection-jvm:1.5.0": "Apache-2.0",
"androidx.collection:collection-jvm:1.6.0": "Apache-2.0",
"androidx.collection:collection-ktx:1.5.0": "Apache-2.0",
"androidx.collection:collection-ktx:1.6.0": "Apache-2.0",
"androidx.collection:collection:1.0.0": "Apache-2.0",
"androidx.compose.animation:animation-android:1.12.1": "Apache-2.0",
"androidx.compose.animation:animation-core-android:1.12.1": "Apache-2.0",
"androidx.compose.foundation:foundation-android:1.12.1": "Apache-2.0",
"androidx.compose.foundation:foundation-layout-android:1.12.1": "Apache-2.0",
"androidx.compose.material3.adaptive:adaptive-android:1.3.0": "Apache-2.0",
"androidx.compose.material3.adaptive:adaptive-layout-android:1.3.0": "Apache-2.0",
"androidx.compose.material3.adaptive:adaptive-navigation-android:1.3.0": "Apache-2.0",
"androidx.compose.material3:material3-android:1.4.0": "Apache-2.0",
"androidx.compose.material:material-ripple-android:1.12.1": "Apache-2.0",
"androidx.compose.runtime:runtime-android:1.12.1": "Apache-2.0",
@@ -37,25 +56,34 @@
"androidx.compose.ui:ui-geometry-android:1.12.1": "Apache-2.0",
"androidx.compose.ui:ui-graphics-android:1.12.1": "Apache-2.0",
"androidx.compose.ui:ui-test-android:1.12.1": "Apache-2.0",
"androidx.compose.ui:ui-test-junit4-accessibility-android:1.12.1": "Apache-2.0",
"androidx.compose.ui:ui-test-junit4-android:1.12.1": "Apache-2.0",
"androidx.compose.ui:ui-text-android:1.12.1": "Apache-2.0",
"androidx.compose.ui:ui-unit-android:1.12.1": "Apache-2.0",
"androidx.compose.ui:ui-util-android:1.12.1": "Apache-2.0",
"androidx.concurrent:concurrent-futures-ktx:1.1.0": "Apache-2.0",
"androidx.concurrent:concurrent-futures-ktx:1.2.0": "Apache-2.0",
"androidx.concurrent:concurrent-futures:1.1.0": "Apache-2.0",
"androidx.concurrent:concurrent-futures:1.2.0": "Apache-2.0",
"androidx.coordinatorlayout:coordinatorlayout:1.1.0": "Apache-2.0",
"androidx.core:core-backported-fixes:1.0.0": "Apache-2.0",
"androidx.core:core-ktx:1.13.1": "Apache-2.0",
"androidx.core:core-ktx:1.16.0": "Apache-2.0",
"androidx.core:core-ktx:1.18.0": "Apache-2.0",
"androidx.core:core-ktx:1.19.1": "Apache-2.0",
"androidx.core:core-viewtree:1.0.0": "Apache-2.0",
"androidx.core:core:1.13.1": "Apache-2.0",
"androidx.core:core:1.16.0": "Apache-2.0",
"androidx.core:core:1.18.0": "Apache-2.0",
"androidx.core:core:1.19.1": "Apache-2.0",
"androidx.core:core:1.9.0": "Apache-2.0",
"androidx.credentials:credentials:1.6.0": "Apache-2.0",
"androidx.cursoradapter:cursoradapter:1.0.0": "Apache-2.0",
"androidx.customview:customview-poolingcontainer:1.0.0": "Apache-2.0",
"androidx.customview:customview:1.0.0": "Apache-2.0",
"androidx.drawerlayout:drawerlayout:1.0.0": "Apache-2.0",
"androidx.emoji2:emoji2:1.4.0": "Apache-2.0",
"androidx.exifinterface:exifinterface:1.4.2": "Apache-2.0",
"androidx.fragment:fragment:1.5.1": "Apache-2.0",
"androidx.fragment:fragment:1.9.1": "Apache-2.0",
"androidx.graphics:graphics-path:1.0.1": "Apache-2.0",
@@ -63,6 +91,7 @@
"androidx.lifecycle:lifecycle-common-java8:2.11.0": "Apache-2.0",
"androidx.lifecycle:lifecycle-common-jvm:2.10.0": "Apache-2.0",
"androidx.lifecycle:lifecycle-common-jvm:2.11.0": "Apache-2.0",
"androidx.lifecycle:lifecycle-common:2.3.1": "Apache-2.0",
"androidx.lifecycle:lifecycle-livedata-core-ktx:2.10.0": "Apache-2.0",
"androidx.lifecycle:lifecycle-livedata-core-ktx:2.11.0": "Apache-2.0",
"androidx.lifecycle:lifecycle-livedata-core:2.10.0": "Apache-2.0",
@@ -75,6 +104,7 @@
"androidx.lifecycle:lifecycle-runtime-compose-android:2.11.0": "Apache-2.0",
"androidx.lifecycle:lifecycle-runtime-ktx-android:2.10.0": "Apache-2.0",
"androidx.lifecycle:lifecycle-runtime-ktx-android:2.11.0": "Apache-2.0",
"androidx.lifecycle:lifecycle-runtime:2.3.1": "Apache-2.0",
"androidx.lifecycle:lifecycle-viewmodel-android:2.10.0": "Apache-2.0",
"androidx.lifecycle:lifecycle-viewmodel-android:2.11.0": "Apache-2.0",
"androidx.lifecycle:lifecycle-viewmodel-compose-android:2.11.0": "Apache-2.0",
@@ -85,11 +115,16 @@
"androidx.lifecycle:lifecycle-viewmodel:2.10.0": "Apache-2.0",
"androidx.lifecycle:lifecycle-viewmodel:2.11.0": "Apache-2.0",
"androidx.loader:loader:1.0.0": "Apache-2.0",
"androidx.media3:media3-common:1.9.0": "Apache-2.0",
"androidx.media3:media3-container:1.9.0": "Apache-2.0",
"androidx.media3:media3-muxer:1.9.0": "Apache-2.0",
"androidx.navigation3:navigation3-runtime-android:1.2.0": "Apache-2.0",
"androidx.navigation3:navigation3-ui-android:1.2.0": "Apache-2.0",
"androidx.navigationevent:navigationevent-android:1.1.1": "Apache-2.0",
"androidx.navigationevent:navigationevent-compose-android:1.1.1": "Apache-2.0",
"androidx.profileinstaller:profileinstaller:1.4.0": "Apache-2.0",
"androidx.profileinstaller:profileinstaller:1.4.1": "Apache-2.0",
"androidx.recyclerview:recyclerview:1.1.0": "Apache-2.0",
"androidx.savedstate:savedstate-android:1.4.0": "Apache-2.0",
"androidx.savedstate:savedstate-compose-android:1.4.0": "Apache-2.0",
"androidx.savedstate:savedstate-ktx:1.4.0": "Apache-2.0",
@@ -97,110 +132,79 @@
"androidx.test.espresso:espresso-core:3.7.0": "Apache-2.0",
"androidx.test.espresso:espresso-idling-resource:3.7.0": "Apache-2.0",
"androidx.test.ext:junit:1.1.5": "Apache-2.0",
"androidx.test.ext:junit:1.2.1": "Apache-2.0",
"androidx.test.services:storage:1.6.0": "Apache-2.0",
"androidx.test.uiautomator:uiautomator-shell-android:2.4.0": "Apache-2.0",
"androidx.test.uiautomator:uiautomator:2.4.0": "Apache-2.0",
"androidx.test:annotation:1.0.1": "Apache-2.0",
"androidx.test:core:1.7.0": "Apache-2.0",
"androidx.test:monitor:1.8.0": "Apache-2.0",
"androidx.test:rules:1.4.0": "Apache-2.0",
"androidx.test:rules:1.5.0": "Apache-2.0",
"androidx.test:runner:1.7.0": "Apache-2.0",
"androidx.tracing:tracing-android:2.0.0": "Apache-2.0",
"androidx.tracing:tracing-ktx:1.1.0": "Apache-2.0",
"androidx.tracing:tracing-ktx:2.0.0": "Apache-2.0",
"androidx.tracing:tracing-perfetto-binary:1.0.1": "Apache-2.0",
"androidx.tracing:tracing-perfetto-handshake:1.0.1": "Apache-2.0",
"androidx.tracing:tracing-perfetto:1.0.1": "Apache-2.0",
"androidx.tracing:tracing:1.1.0": "Apache-2.0",
"androidx.tracing:tracing:1.2.0": "Apache-2.0",
"androidx.transition:transition:1.2.0": "Apache-2.0",
"androidx.vectordrawable:vectordrawable-animated:1.1.0": "Apache-2.0",
"androidx.vectordrawable:vectordrawable:1.1.0": "Apache-2.0",
"androidx.versionedparcelable:versionedparcelable:1.1.1": "Apache-2.0",
"androidx.viewpager2:viewpager2:1.0.0": "Apache-2.0",
"androidx.viewpager:viewpager:1.0.0": "Apache-2.0",
"androidx.window:window-core-android:1.5.0": "Apache-2.0",
"androidx.window:window:1.5.0": "Apache-2.0",
"com.google.android.apps.common.testing.accessibility.framework:accessibility-test-framework:4.1.1": "Apache-2.0",
"com.google.android.material:material:1.2.0-rc01": "Apache-2.0",
"com.google.auto.value:auto-value-annotations:1.6.3": "Apache-2.0",
"com.google.code.findbugs:jsr305:3.0.2": "Apache-2.0",
"com.google.dagger:dagger-lint-aar:2.60.1": "Apache-2.0",
"com.google.dagger:dagger:2.59": "Apache-2.0",
"com.google.dagger:dagger:2.60.1": "Apache-2.0",
"com.google.dagger:hilt-android:2.60.1": "Apache-2.0",
"com.google.dagger:hilt-core:2.60.1": "Apache-2.0",
"com.google.errorprone:error_prone_annotations:2.28.0": "Apache-2.0",
"com.google.errorprone:error_prone_annotations:2.30.0": "Apache-2.0",
"com.google.guava:failureaccess:1.0.1": "Apache-2.0",
"com.google.guava:failureaccess:1.0.2": "Apache-2.0",
"com.google.guava:guava:31.0.1-android": "Apache-2.0",
"com.google.guava:guava:33.3.1-android": "Apache-2.0",
"com.google.guava:listenablefuture:1.0": "Apache-2.0",
"com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava": "Apache-2.0",
"com.google.j2objc:j2objc-annotations:1.3": "Apache-2.0",
"com.google.j2objc:j2objc-annotations:3.0.0": "Apache-2.0",
"com.google.protobuf:protobuf-javalite:3.19.1": "BSD-3-Clause",
"com.squareup.moshi:moshi:1.13.0": "Apache-2.0",
"com.squareup.okio:okio-jvm:3.17.0": "Apache-2.0",
"com.squareup.wire:wire-runtime-jvm:6.4.0": "Apache-2.0",
"jakarta.inject:jakarta.inject-api:2.0.1": "Apache-2.0",
"javax.inject:javax.inject:1": "Apache-2.0",
"junit:junit:4.13.2": "EPL-1.0",
"net.java.dev.jna:jna:5.19.1": "Apache-2.0 OR LGPL-2.1-or-later",
"org.checkerframework:checker-compat-qual:2.5.5": "MIT",
"org.checkerframework:checker-qual:3.22.1": "MIT",
"org.checkerframework:checker-qual:3.43.0": "MIT",
"org.hamcrest:hamcrest-core:1.3": "BSD-3-Clause",
"org.hamcrest:hamcrest-library:1.3": "BSD-3-Clause",
"org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0": "Apache-2.0",
"org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0": "Apache-2.0",
"org.jetbrains.kotlin:kotlin-stdlib:2.4.21": "Apache-2.0",
"org.jetbrains.kotlinx:atomicfu-jvm:0.28.0": "Apache-2.0",
"org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0": "Apache-2.0",
"org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0": "Apache-2.0",
"org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.11.0": "Apache-2.0",
"org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.9.0": "Apache-2.0",
"org.jetbrains.kotlinx:kotlinx-coroutines-test-jvm:1.11.0": "Apache-2.0",
"org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.7.3": "Apache-2.0",
"org.jetbrains:annotations:13.0": "Apache-2.0",
"org.jetbrains:annotations:23.0.0": "Apache-2.0",
"org.jspecify:jspecify:1.0.0": "Apache-2.0",
"androidx.compose.material3.adaptive:adaptive-layout-android:1.3.0": "Apache-2.0",
"androidx.compose.material3.adaptive:adaptive-navigation-android:1.3.0": "Apache-2.0",
"androidx.lifecycle:lifecycle-common:2.3.1": "Apache-2.0",
"androidx.cardview:cardview:1.0.0": "Apache-2.0",
"androidx.compose.ui:ui-test-junit4-accessibility-android:1.12.1": "Apache-2.0",
"androidx.coordinatorlayout:coordinatorlayout:1.1.0": "Apache-2.0",
"androidx.recyclerview:recyclerview:1.1.0": "Apache-2.0",
"androidx.test:rules:1.4.0": "Apache-2.0",
"androidx.transition:transition:1.2.0": "Apache-2.0",
"androidx.viewpager2:viewpager2:1.0.0": "Apache-2.0",
"com.google.android.apps.common.testing.accessibility.framework:accessibility-test-framework:4.1.1": "Apache-2.0",
"com.google.android.material:material:1.2.0-rc01": "Apache-2.0",
"com.google.guava:failureaccess:1.0.1": "Apache-2.0",
"com.google.guava:guava:31.0.1-android": "Apache-2.0",
"com.google.j2objc:j2objc-annotations:1.3": "Apache-2.0",
"com.google.protobuf:protobuf-javalite:3.19.1": "BSD-3-Clause",
"org.checkerframework:checker-compat-qual:2.5.5": "MIT",
"org.checkerframework:checker-qual:3.22.1": "MIT",
"org.jsoup:jsoup:1.15.1": "MIT",
"androidx.annotation:annotation-jvm:1.8.1": "Apache-2.0",
"androidx.arch.core:core-common:2.1.0": "Apache-2.0",
"androidx.arch.core:core-runtime:2.1.0": "Apache-2.0",
"androidx.benchmark:benchmark-common:1.5.0": "Apache-2.0",
"androidx.benchmark:benchmark-macro:1.5.0": "Apache-2.0",
"androidx.benchmark:benchmark-macro-junit4:1.5.0": "Apache-2.0",
"androidx.benchmark:benchmark-traceprocessor-android:1.5.0": "Apache-2.0",
"androidx.collection:collection:1.0.0": "Apache-2.0",
"androidx.core:core:1.9.0": "Apache-2.0",
"androidx.lifecycle:lifecycle-runtime:2.3.1": "Apache-2.0",
"androidx.profileinstaller:profileinstaller:1.4.1": "Apache-2.0",
"androidx.test:annotation:1.0.1": "Apache-2.0",
"androidx.test:rules:1.5.0": "Apache-2.0",
"androidx.test.ext:junit:1.2.1": "Apache-2.0",
"androidx.test.uiautomator:uiautomator:2.4.0": "Apache-2.0",
"androidx.test.uiautomator:uiautomator-shell-android:2.4.0": "Apache-2.0",
"androidx.tracing:tracing-ktx:1.1.0": "Apache-2.0",
"androidx.tracing:tracing-perfetto:1.0.1": "Apache-2.0",
"androidx.tracing:tracing-perfetto-binary:1.0.1": "Apache-2.0",
"androidx.tracing:tracing-perfetto-handshake:1.0.1": "Apache-2.0",
"com.squareup.moshi:moshi:1.13.0": "Apache-2.0",
"com.squareup.okio:okio-jvm:3.17.0": "Apache-2.0",
"com.squareup.wire:wire-runtime-jvm:6.4.0": "Apache-2.0",
"org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0": "Apache-2.0",
"org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0": "Apache-2.0",
"org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0": "Apache-2.0",
"org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.9.0": "Apache-2.0",
"androidx.camera.featurecombinationquery:featurecombinationquery:1.6.2": "Apache-2.0",
"androidx.camera.viewfinder:viewfinder-core:1.5.1": "Apache-2.0",
"androidx.camera:camera-camera2-pipe:1.6.2": "Apache-2.0",
"androidx.camera:camera-camera2:1.6.2": "Apache-2.0",
"androidx.camera:camera-core:1.6.2": "Apache-2.0 AND BSD-3-Clause",
"androidx.camera:camera-lifecycle:1.6.2": "Apache-2.0",
"androidx.camera:camera-video:1.6.2": "Apache-2.0",
"androidx.camera:camera-view:1.6.2": "Apache-2.0",
"androidx.concurrent:concurrent-futures-ktx:1.1.0": "Apache-2.0",
"androidx.core:core-backported-fixes:1.0.0": "Apache-2.0",
"androidx.exifinterface:exifinterface:1.4.2": "Apache-2.0",
"androidx.media3:media3-common:1.9.0": "Apache-2.0",
"androidx.media3:media3-container:1.9.0": "Apache-2.0",
"androidx.media3:media3-muxer:1.9.0": "Apache-2.0",
"androidx.tracing:tracing-ktx:2.0.0": "Apache-2.0",
"com.google.auto.value:auto-value-annotations:1.6.3": "Apache-2.0",
"com.google.dagger:dagger:2.59": "Apache-2.0",
"com.google.errorprone:error_prone_annotations:2.28.0": "Apache-2.0",
"com.google.guava:failureaccess:1.0.2": "Apache-2.0",
"com.google.guava:guava:33.3.1-android": "Apache-2.0",
"com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava": "Apache-2.0",
"com.google.j2objc:j2objc-annotations:3.0.0": "Apache-2.0",
"org.checkerframework:checker-qual:3.43.0": "MIT",
"org.jetbrains.kotlinx:atomicfu-jvm:0.28.0": "Apache-2.0"
"org.jspecify:jspecify:1.0.0": "Apache-2.0",
"rustls:rustls-platform-verifier:0.1.1": "MIT OR Apache-2.0"
},
"rust_licenses": [
"(Apache-2.0 OR MIT) AND BSD-3-Clause",
@@ -0,0 +1,172 @@
package de.rfc1437.ironstorage.features
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import de.rfc1437.ironstorage.core.ProviderRequest
@Composable
fun CredentialSelectionScreen(
request: ProviderRequest?,
saving: Boolean,
selected: String?,
query: String,
destination: String,
passphrase: String,
consent: Boolean,
overwrite: Boolean,
busy: Boolean,
failure: String?,
onSelect: (String?) -> Unit,
onQuery: (String) -> Unit,
onDestination: (String) -> Unit,
onPassphrase: (String) -> Unit,
onConsent: (Boolean) -> Unit,
onOverwrite: (Boolean) -> Unit,
onConfirm: () -> Unit,
onCancel: () -> Unit
) {
LazyColumn(
Modifier.fillMaxSize().padding(24.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
item {
Text(
stringResource(
if (saving) R.string.provider_save_title else R.string.provider_fill_title
),
style = MaterialTheme.typography.headlineSmall
)
Text(request?.title ?: stringResource(R.string.provider_checking))
}
if (saving) {
item {
OutlinedTextField(
destination,
onDestination,
label = { Text(stringResource(R.string.provider_destination)) },
enabled = !busy,
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
ProviderConsent(
overwrite,
!busy,
stringResource(R.string.provider_overwrite),
onOverwrite
)
}
} else if (selected == null) {
item {
OutlinedTextField(
query,
onQuery,
label = { Text(stringResource(R.string.provider_search)) },
enabled = !busy,
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
}
items(request?.candidates ?: emptyList(), key = { it.path }) { candidate ->
TextButton(onClick = {
onSelect(candidate.path)
}, enabled = !busy, modifier = Modifier.fillMaxWidth()) {
Text(candidate.path)
}
}
} else {
item {
Text(stringResource(R.string.provider_selected, selected))
TextButton(onClick = { onSelect(null) }, enabled = !busy) {
Text(stringResource(R.string.provider_choose_another))
}
}
}
if (saving || selected != null) {
item {
OutlinedTextField(
passphrase,
onPassphrase,
label = { Text(stringResource(R.string.provider_passphrase)) },
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
autoCorrectEnabled = false
),
enabled = !busy,
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
val recipient = request?.title ?: stringResource(R.string.provider_this_application)
val confirmation = if (saving) {
stringResource(
R.string.provider_confirm_save
)
} else {
stringResource(R.string.provider_confirm_fill, recipient)
}
ProviderConsent(consent, !busy, confirmation, onConsent)
}
}
item {
failure?.let { Text(it, color = MaterialTheme.colorScheme.error) }
Button(
onClick = onConfirm,
enabled = !busy && consent && request != null &&
(if (saving) destination.isNotBlank() else selected != null)
) {
Text(
stringResource(
if (busy) {
R.string.provider_working
} else if (saving) {
R.string.provider_save
} else {
R.string.provider_fill
}
)
)
}
TextButton(onClick = onCancel) { Text(stringResource(R.string.auth_cancel)) }
}
}
}
@Composable
private fun ProviderConsent(
checked: Boolean,
enabled: Boolean,
label: String,
onChange: (Boolean) -> Unit
) {
Row(
Modifier.fillMaxWidth().toggleable(
value = checked,
enabled = enabled,
role = Role.Checkbox,
onValueChange = onChange
)
) {
Checkbox(checked, null, enabled = enabled)
Text(label, Modifier.padding(top = 12.dp))
}
}
@@ -229,6 +229,13 @@ fun SettingsScreen(
}) { Text(stringResource(R.string.settings_rebuild)) }
}
"providers" -> {
Text(stringResource(R.string.settings_chrome_guidance))
TextButton(onClick = {
uri.openUri(
"https://support.google.com/chrome/answer/142893?co=GENIE.Platform%3DAndroid&hl=en"
)
}) { Text(stringResource(R.string.settings_chrome_help)) }
page?.let {
ProviderSetting(
stringResource(R.string.settings_autofill),
@@ -156,7 +156,7 @@
<string name="settings_git">Git commit author name and email</string>
<string name="settings_auth">Authentication, inactivity timeout, biometrics and appearance</string>
<string name="settings_cache">TOTP discovery cache</string>
<string name="settings_providers">Autofill and Credential Manager</string>
<string name="settings_providers">Autofill and Credential Manager providers</string>
<string name="settings_recovery">Recovery and offline use</string>
<string name="settings_about">Version, licenses, privacy and support</string>
<string name="settings_no_token">No application token stored</string>
@@ -266,4 +266,21 @@
<item quantity="one">%1$s password entry</item>
<item quantity="other">%1$s password entries</item>
</plurals>
<string name="settings_chrome_guidance">After selecting IronStorage in Android settings, open Chrome Settings → Autofill and passwords → Autofill settings. Select Autofill using another service and restart Chrome.</string>
<string name="settings_chrome_help">Chrome autofill help</string>
<string name="provider_fill_title">Fill from IronStorage</string>
<string name="provider_save_title">Save in IronStorage</string>
<string name="provider_checking">Checking the requesting application…</string>
<string name="provider_destination">Destination entry path</string>
<string name="provider_overwrite">Allow updating this destination if it exists</string>
<string name="provider_search">Search entries</string>
<string name="provider_selected">Selected: %1$s</string>
<string name="provider_choose_another">Choose another entry</string>
<string name="provider_passphrase">GPG passphrase (leave empty for biometric unlock)</string>
<string name="provider_this_application">this application</string>
<string name="provider_confirm_save">Save the supplied credential at the chosen destination</string>
<string name="provider_confirm_fill">Allow %1$s to receive this entry\'s username and password</string>
<string name="provider_working">Working…</string>
<string name="provider_save">Confirm save</string>
<string name="provider_fill">Unlock and fill</string>
</resources>
+5
View File
@@ -1,4 +1,6 @@
[versions]
credentials = "1.6.0"
autofill = "1.3.0"
camera = "1.6.2"
agp = "9.4.1"
kotlin = "2.4.21"
@@ -29,6 +31,9 @@ biometric = "1.1.0"
fragment = "1.9.1"
[libraries]
core-ktx = { module = "androidx.core:core-ktx", version = "1.19.1" }
autofill = { module = "androidx.autofill:autofill", version.ref = "autofill" }
credentials = { module = "androidx.credentials:credentials", version.ref = "credentials" }
camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camera" }
camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camera" }
camera-view = { module = "androidx.camera:camera-view", version.ref = "camera" }
+165
View File
@@ -6,6 +6,9 @@
</configuration>
<components>
<component group="androidx.activity" name="activity" version="1.1.0">
<artifact name="activity-1.1.0.aar">
<sha256 value="4f2b35916768032f7d0c20e250e28b29037ed4ce9ebf3da4fcd51bcb0c6067ef" origin="Generated by Gradle"/>
</artifact>
<artifact name="activity-1.1.0.pom">
<sha256 value="3690001fc5a57ad176cacf7a13f56219bffb73b93c7978dbac1e1e5db082d6c0" origin="Generated by Gradle"/>
</artifact>
@@ -89,6 +92,11 @@
<sha256 value="2e9372ba7780ef44952adbf86b66e1f08682c1e5277c926185f6564a13799efe" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.annotation" name="annotation" version="1.10.0">
<artifact name="annotation-1.10.0.module">
<sha256 value="5b2d4387f384e5913a4aec979a20ece13bb5ce992bc90a962dea1feea7fa89e9" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.annotation" name="annotation" version="1.11.0">
<artifact name="annotation-1.11.0.module">
<sha256 value="ac8410bea7adaabc9e71285f0627c9cb929c5addd00cfc6d0ebd421dc2da1ee0" origin="Generated by Gradle"/>
@@ -151,6 +159,14 @@
<sha256 value="ae1ff29a7f3965e6589acfe7f07ef39d36405d7ab278472327afd92fc6d72a3f" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.annotation" name="annotation-jvm" version="1.10.0">
<artifact name="annotation-jvm-1.10.0.jar">
<sha256 value="ddd072ddbb553178e205517ce777b2f05aa9e412982d9ecb4eedc74f1212f697" origin="Generated by Gradle"/>
</artifact>
<artifact name="annotation-jvm-1.10.0.module">
<sha256 value="42afca374194273e4067b937cfc447bd3c8c4fd441f41d44f954050dfd6106dd" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.annotation" name="annotation-jvm" version="1.11.0">
<artifact name="annotation-jvm-1.11.0.jar">
<sha256 value="a83b827cc17fd6b41c7bf5e7a2a2b40a0895b00b3e30fdfdb820b49dc7b667f3" origin="Generated by Gradle"/>
@@ -204,6 +220,11 @@
<sha256 value="75d9865bc6b0f779043b4f6523a944bd2ccb7526f5c1f5ef24624ac78dec3bd3" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.arch.core" name="core-common" version="2.0.0">
<artifact name="core-common-2.0.0.pom">
<sha256 value="4b6f1d459ddd146b4e85ed6d46e86eb8c2639c5de47904e6db4d698721334220" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.arch.core" name="core-common" version="2.1.0">
<artifact name="core-common-2.1.0.jar">
<sha256 value="fe1237bf029d063e7f29fe39aeaf73ef74c8b0a3658486fc29d3c54326653889" origin="Generated by Gradle"/>
@@ -244,6 +265,14 @@
<sha256 value="3677abead5d4e06cf76cbacb954151e051779c2274494451fb424e375aa9e450" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.autofill" name="autofill" version="1.3.0">
<artifact name="autofill-1.3.0.aar">
<sha256 value="b5a217cee7724ea42a382965a9b22a330b988d62cb97abe1b1af50e54370c322" origin="Generated by Gradle"/>
</artifact>
<artifact name="autofill-1.3.0.module">
<sha256 value="0bae265cafe02e953f13e873a343911d2263200ca89639120dfdd756286060f9" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.benchmark" name="benchmark-common" version="1.5.0">
<artifact name="benchmark-common-1.5.0.aar">
<sha256 value="1b915a31dae4d4c57a1641a16d063983c29069290ce4153c1f356e97bd247db4" origin="Generated by Gradle"/>
@@ -955,6 +984,9 @@
</artifact>
</component>
<component group="androidx.core" name="core" version="1.15.0">
<artifact name="core-1.15.0.aar">
<sha256 value="432b85a1974076e14b487ece4a28c59a84f1b9efc3fc8be72cd7f05d32055e51" origin="Generated by Gradle"/>
</artifact>
<artifact name="core-1.15.0.module">
<sha256 value="e8a6c386e1765d870012fed2221173d4a2e8d2fd5aec732c51afb4a9f4684519" origin="Generated by Gradle"/>
</artifact>
@@ -975,7 +1007,18 @@
<sha256 value="b67ff7a1f9be95687dfa86d6055147fc1d8bbe3b1dc19048c432afca18abfd95" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.core" name="core" version="1.19.1">
<artifact name="core-1.19.1.aar">
<sha256 value="08108ea780205bd54603088a62a6dd5378f63bd25542674be24cd4cdb8191a09" origin="Generated by Gradle"/>
</artifact>
<artifact name="core-1.19.1.module">
<sha256 value="0d8ca728e53e1b694b9a14f0b68824085919500bc123684746f1f43e9f7ea8cd" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.core" name="core" version="1.3.2">
<artifact name="core-1.3.2.aar">
<sha256 value="94de196cd67950cff6ef3e1ac59015f8eaaf61840bdc238f2cf54ddef8dd0be9" origin="Generated by Gradle"/>
</artifact>
<artifact name="core-1.3.2.pom">
<sha256 value="afb5ea494dd083ed404cd51f580d218e37362f8ae326e893bee521290ed34920" origin="Generated by Gradle"/>
</artifact>
@@ -1024,6 +1067,14 @@
<sha256 value="ab530b04e2fbe73205484899a3be56c90968873eb496fc52e5ea7596d91035d1" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.core" name="core-ktx" version="1.15.0">
<artifact name="core-ktx-1.15.0.aar">
<sha256 value="c72c97ff4a32308bcaf3061eea5ddf33455ce413cee4aa1d5c82440c5759d4bc" origin="Generated by Gradle"/>
</artifact>
<artifact name="core-ktx-1.15.0.module">
<sha256 value="bee9b44664ff37e2e222ebcde4b30e38294a3f927451edc611fec7f454ffa442" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.core" name="core-ktx" version="1.16.0">
<artifact name="core-ktx-1.16.0.aar">
<sha256 value="1766dbd82f64a12dcd359ecb6f15f3cf35db4d66d22961a7c51b2ffc6468314c" origin="Generated by Gradle"/>
@@ -1040,6 +1091,14 @@
<sha256 value="3fa579c11de0f6f378002c0f0bb4bbe71100634f80f8b282951789ea7d0fb849" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.core" name="core-ktx" version="1.19.1">
<artifact name="core-ktx-1.19.1.aar">
<sha256 value="9a3e2919e64026ad18b42bf9931e8fea90a6a70e95e13a26519841e926fe5913" origin="Generated by Gradle"/>
</artifact>
<artifact name="core-ktx-1.19.1.module">
<sha256 value="580b6ea6aced160520499602dc42634277a0b2dbc1ccab18105408dea2a4b202" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.core" name="core-ktx" version="1.2.0">
<artifact name="core-ktx-1.2.0.pom">
<sha256 value="29eaddba4e6a35e68a128e594ebae3b72ef7e767f1b273ef93dc2ed019ff97f6" origin="Generated by Gradle"/>
@@ -1066,6 +1125,14 @@
<sha256 value="11386cfa46cbbfddb6a4059f14354c00691cf65d3d63c3618818a83326ef3c7f" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.credentials" name="credentials" version="1.6.0">
<artifact name="credentials-1.6.0.aar">
<sha256 value="9051922592ee3c92370ce3f942d66661f1e9050ac5b370932c71f0a380acf227" origin="Generated by Gradle"/>
</artifact>
<artifact name="credentials-1.6.0.module">
<sha256 value="639ae0bdc1d09a4d0c3cd198850fd8530363cf65c55c7c870b17596883346163" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.cursoradapter" name="cursoradapter" version="1.0.0">
<artifact name="cursoradapter-1.0.0.aar">
<sha256 value="a81c8fe78815fa47df5b749deb52727ad11f9397da58b16017f4eb2c11e28564" origin="Generated by Gradle"/>
@@ -1141,6 +1208,9 @@
</artifact>
</component>
<component group="androidx.fragment" name="fragment" version="1.2.5">
<artifact name="fragment-1.2.5.aar">
<sha256 value="d19e82d142def6c4e136da70bf92f194c0ecc61d14ab4e84567b2ced0920fa93" origin="Generated by Gradle"/>
</artifact>
<artifact name="fragment-1.2.5.pom">
<sha256 value="df0aca46b62bb47cc662cbcee63372db6d2a2859478ee38b594fba3433fe30a5" origin="Generated by Gradle"/>
</artifact>
@@ -1198,6 +1268,11 @@
<sha256 value="2baaf7f17551765524bd1e4d2f954a2a0178a112039cf549ba8b7d4b41e66931" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.lifecycle" name="lifecycle-common" version="2.0.0">
<artifact name="lifecycle-common-2.0.0.pom">
<sha256 value="04d525073469214d0c99e81aaa875dd548ba32b82945abd8326bc50229df700d" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.lifecycle" name="lifecycle-common" version="2.1.0">
<artifact name="lifecycle-common-2.1.0.pom">
<sha256 value="692c2a60437a95220c8fbb6e8943196f255f535d1a7c58714e6c46530782f276" origin="Generated by Gradle"/>
@@ -1214,6 +1289,9 @@
</artifact>
</component>
<component group="androidx.lifecycle" name="lifecycle-common" version="2.2.0">
<artifact name="lifecycle-common-2.2.0.jar">
<sha256 value="63898dabf7cfe5ec5d7ed8b8c2564c1427be876e1496ead95c2703cf59d3734b" origin="Generated by Gradle"/>
</artifact>
<artifact name="lifecycle-common-2.2.0.pom">
<sha256 value="cc30a8a2a97d3767bd169b747c078727308096dd3ab7917d80b2f1f42d118bd6" origin="Generated by Gradle"/>
</artifact>
@@ -1239,6 +1317,14 @@
<sha256 value="93747a9145cb36bc71005f598ede32e2b1149ade5a16e62b0e4969345bc62d85" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.lifecycle" name="lifecycle-common" version="2.6.2">
<artifact name="lifecycle-common-2.6.2.jar">
<sha256 value="f34831b6c71cd844e1d35d1be49d5e79447c5ab856346531b1e8676fda7374b1" origin="Generated by Gradle"/>
</artifact>
<artifact name="lifecycle-common-2.6.2.module">
<sha256 value="0fa7f28f5cff8a406a4f7870b243cb0d6d7a7c20faa7a2becaff5907ae12f9cc" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.lifecycle" name="lifecycle-common" version="2.9.4">
<artifact name="lifecycle-common-2.9.4.module">
<sha256 value="c578bd755ea46d7a77811309a3539538e2d7fb86a05fc2ad81c655ecf7dff7a4" origin="Generated by Gradle"/>
@@ -1273,6 +1359,14 @@
<sha256 value="1d1837f3942b33ea30aa2301fdcea2639408a0fd6fd4368c22478fa8153aebbf" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.lifecycle" name="lifecycle-livedata" version="2.0.0">
<artifact name="lifecycle-livedata-2.0.0.aar">
<sha256 value="c82609ced8c498f0a701a30fb6771bb7480860daee84d82e0a81ee86edf7ba39" origin="Generated by Gradle"/>
</artifact>
<artifact name="lifecycle-livedata-2.0.0.pom">
<sha256 value="a84842ffc0f14e518db75c05cc112680a8a4a164fa78395be32d88304a439423" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.lifecycle" name="lifecycle-livedata" version="2.1.0">
<artifact name="lifecycle-livedata-2.1.0.pom">
<sha256 value="1e4c580c995e41cd18455cfb87e3e43955be9db9ec63a8fb3c7259f01b7c4e3c" origin="Generated by Gradle"/>
@@ -1307,6 +1401,14 @@
<sha256 value="e2659e87e890b52b66f300471828b387a205e5387a392713190166f399ecf641" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.lifecycle" name="lifecycle-livedata" version="2.6.2">
<artifact name="lifecycle-livedata-2.6.2.aar">
<sha256 value="67359f609dfc2bf65da1270b23033f856064ec279f058e0a70c715f7c9003031" origin="Generated by Gradle"/>
</artifact>
<artifact name="lifecycle-livedata-2.6.2.module">
<sha256 value="19a287b46d1b1ffe297bee0df7dee5183aeba95600ec0de8742adeb5b67c8bab" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.lifecycle" name="lifecycle-livedata-core" version="2.1.0">
<artifact name="lifecycle-livedata-core-2.1.0.pom">
<sha256 value="6839d2e68c8fee4cc3bbf04e6b291a6ae842403fcd5f8d944c27814d296deff3" origin="Generated by Gradle"/>
@@ -1329,6 +1431,9 @@
</artifact>
</component>
<component group="androidx.lifecycle" name="lifecycle-livedata-core" version="2.2.0">
<artifact name="lifecycle-livedata-core-2.2.0.aar">
<sha256 value="556c1f3af90aa9d7d0d330565adbf6da71b2429148bac91e07c485f4f9abf614" origin="Generated by Gradle"/>
</artifact>
<artifact name="lifecycle-livedata-core-2.2.0.pom">
<sha256 value="aa51621be3817d1f5d21336b0c0525fe96ae899cec656c13d8ea18a51ea32c2f" origin="Generated by Gradle"/>
</artifact>
@@ -1346,6 +1451,14 @@
<sha256 value="e9c0dc3f0ac54419c0cfed8ff5cecb82943a7c58f7a54169f0d849b2c60a3552" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.lifecycle" name="lifecycle-livedata-core" version="2.6.2">
<artifact name="lifecycle-livedata-core-2.6.2.aar">
<sha256 value="2256780a3cff4a1e57fbb3d442557c17dc363ab8af105bcaf5261d8e2d5db949" origin="Generated by Gradle"/>
</artifact>
<artifact name="lifecycle-livedata-core-2.6.2.module">
<sha256 value="527d0e1ac467d1f47cc20ed7c30db170217299fabb868154cf5d1765e4e4dad9" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.lifecycle" name="lifecycle-livedata-core-ktx" version="2.10.0">
<artifact name="lifecycle-livedata-core-ktx-2.10.0.aar">
<sha256 value="5f18429749a183e19178d8665ae42bf7b6310b50358ae9e0e0cc452afd6e0bc3" origin="Generated by Gradle"/>
@@ -1394,6 +1507,9 @@
</artifact>
</component>
<component group="androidx.lifecycle" name="lifecycle-runtime" version="2.2.0">
<artifact name="lifecycle-runtime-2.2.0.aar">
<sha256 value="2f866c07a1f33a8c9bb69a9545d4f20b4f0628cd0a155432386d7cb081e1e0bc" origin="Generated by Gradle"/>
</artifact>
<artifact name="lifecycle-runtime-2.2.0.pom">
<sha256 value="7f154066fed1c9162870f728b208352831b3d1f8f23f5d388958a50ca4e9f441" origin="Generated by Gradle"/>
</artifact>
@@ -1414,6 +1530,14 @@
<sha256 value="a4cbb01a42d07047bd8d870017c96a1b0b7b4673320e86b66317a13be2ec10c7" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.lifecycle" name="lifecycle-runtime" version="2.6.2">
<artifact name="lifecycle-runtime-2.6.2.aar">
<sha256 value="4867fd5279742fba8388821930cb2affe06d81a52814e7e41e70392ea0ef887c" origin="Generated by Gradle"/>
</artifact>
<artifact name="lifecycle-runtime-2.6.2.module">
<sha256 value="ea0131846abe1fe9dea59ac6dfe1f0fb9d8b6d600c9eff9a1fd4ad5ee5e7cbc7" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.lifecycle" name="lifecycle-runtime" version="2.9.4">
<artifact name="lifecycle-runtime-2.9.4.module">
<sha256 value="dcbc5b5b50666e84258a74bfd8e514918c593a4ffcef0c0358562a02fbd80c58" origin="Generated by Gradle"/>
@@ -1525,6 +1649,9 @@
</artifact>
</component>
<component group="androidx.lifecycle" name="lifecycle-viewmodel" version="2.2.0">
<artifact name="lifecycle-viewmodel-2.2.0.aar">
<sha256 value="967efab24d6c49dd414a8c0ac4a1cd09b018f0b8bb43b739ad360c4158ebde27" origin="Generated by Gradle"/>
</artifact>
<artifact name="lifecycle-viewmodel-2.2.0.pom">
<sha256 value="aaa3afaa49ff47e2bc9b2389f939d3e8137616c4616684e2dfb53ba8366aa11d" origin="Generated by Gradle"/>
</artifact>
@@ -1542,6 +1669,14 @@
<sha256 value="2b406faea5c12f2b8df4b7a60931f846648f2e1f4d78361e198d1184f19a4797" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.lifecycle" name="lifecycle-viewmodel" version="2.6.2">
<artifact name="lifecycle-viewmodel-2.6.2.aar">
<sha256 value="e4ff4338999e1c6c9c724719f5d4aa7dd61bf6f545d5256a27a9d375df9f2330" origin="Generated by Gradle"/>
</artifact>
<artifact name="lifecycle-viewmodel-2.6.2.module">
<sha256 value="74e9a9ea46842a25642c91de2190ac24dcc1ea017395aae0eb250057d32699c6" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.lifecycle" name="lifecycle-viewmodel" version="2.9.4">
<artifact name="lifecycle-viewmodel-2.9.4.module">
<sha256 value="964d69d6b877296e8b79a77bb826ef5fb186aecaa7a1a7ff776fb2250c993006" origin="Generated by Gradle"/>
@@ -1636,6 +1771,14 @@
<sha256 value="dc6750a6991bfaac0817469865fe2ccb01e26186a0386299a5d056bac2cc72cc" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.lifecycle" name="lifecycle-viewmodel-savedstate" version="2.2.0">
<artifact name="lifecycle-viewmodel-savedstate-2.2.0.aar">
<sha256 value="3ce866fb822b20fe2f188f974992869a0a6233fe40acbefcff090d6def5e7f33" origin="Generated by Gradle"/>
</artifact>
<artifact name="lifecycle-viewmodel-savedstate-2.2.0.pom">
<sha256 value="9f31254e547b214257067611f18fc8a3c08f8df9eba634f6e21badc848596db1" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.lifecycle" name="lifecycle-viewmodel-savedstate" version="2.5.1">
<artifact name="lifecycle-viewmodel-savedstate-2.5.1.module">
<sha256 value="29acd5fe614b3f89123eb838f688d625eaa8b422c8d1905b48ad8e760cd7ad8b" origin="Generated by Gradle"/>
@@ -1649,6 +1792,14 @@
<sha256 value="dafb8649763d29c29cda27bc22fcdab9a9efc53c0fff9ae3de90882eabaa8944" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.lifecycle" name="lifecycle-viewmodel-savedstate" version="2.6.2">
<artifact name="lifecycle-viewmodel-savedstate-2.6.2.aar">
<sha256 value="7bc7dcbab17636ec076f12afe4d02671265c389457b1b366b37a0e8cb91e2da0" origin="Generated by Gradle"/>
</artifact>
<artifact name="lifecycle-viewmodel-savedstate-2.6.2.module">
<sha256 value="79f9d92880c2fb7827ac6739eb747264245ae456f4c21f77cf39fdb6a50a76ae" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.lifecycle" name="lifecycle-viewmodel-savedstate-android" version="2.10.0">
<artifact name="lifecycle-viewmodel-savedstate-android-2.10.0.module">
<sha256 value="949d2a862b6b584cee91af70a9e017bdb117d225bb46096da9424a1728ca79e9" origin="Generated by Gradle"/>
@@ -1809,6 +1960,14 @@
<sha256 value="ff1ff2b5ad10315fea2994ebf95631d61d6e07c9bf3f89d24dd2096cc0032094" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.profileinstaller" name="profileinstaller" version="1.3.0">
<artifact name="profileinstaller-1.3.0.aar">
<sha256 value="34e8b2bfc74e23c1525e3da903ae449b7f1b440aef45e18159ee470e91997f48" origin="Generated by Gradle"/>
</artifact>
<artifact name="profileinstaller-1.3.0.module">
<sha256 value="a16fe511e599c2042da122be09569aecd3f4f90cad37ef08bd0bbc39118c92c8" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.profileinstaller" name="profileinstaller" version="1.3.1">
<artifact name="profileinstaller-1.3.1.aar">
<sha256 value="d0e402ec31f24028a1dc7eb6a0a3f9d9635c1459392cd734396343b73d673948" origin="Generated by Gradle"/>
@@ -1847,6 +2006,9 @@
</artifact>
</component>
<component group="androidx.savedstate" name="savedstate" version="1.0.0">
<artifact name="savedstate-1.0.0.aar">
<sha256 value="2510a5619c37579c9ce1a04574faaf323cd0ffe2fc4e20fa8f8f01e5bb402e83" origin="Generated by Gradle"/>
</artifact>
<artifact name="savedstate-1.0.0.pom">
<sha256 value="844d7d22fcea798c78bf5559229daf88ec9cad8136f5eea0a29aa76b3f8fde7c" origin="Generated by Gradle"/>
</artifact>
@@ -2208,6 +2370,9 @@
</artifact>
</component>
<component group="androidx.versionedparcelable" name="versionedparcelable" version="1.1.0">
<artifact name="versionedparcelable-1.1.0.aar">
<sha256 value="9a1d77140ac222b7866b5054ee7d159bc1800987ed2d46dd6afdd145abb710c1" origin="Generated by Gradle"/>
</artifact>
<artifact name="versionedparcelable-1.1.0.pom">
<sha256 value="c729c7be0cc06323bda829d460666e79dbd43b799a21089a44bd3b293dc253b5" origin="Generated by Gradle"/>
</artifact>
+2
View File
@@ -16,6 +16,7 @@ androidx.arch.core:core-common:2.1.0=benchmarkLintChecksClasspath,benchmarkRunti
androidx.arch.core:core-common:2.2.0=benchmarkTestedApks,debugTestedApks
androidx.arch.core:core-runtime:2.1.0=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath
androidx.arch.core:core-runtime:2.2.0=benchmarkTestedApks,debugTestedApks
androidx.autofill:autofill:1.3.0=benchmarkTestedApks,debugTestedApks
androidx.benchmark:benchmark-common:1.5.0=benchmarkCompileClasspath,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath
androidx.benchmark:benchmark-macro-junit4:1.5.0=benchmarkCompileClasspath,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath
androidx.benchmark:benchmark-macro:1.5.0=benchmarkCompileClasspath,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath
@@ -81,6 +82,7 @@ androidx.core:core-ktx:1.18.0=benchmarkTestedApks,debugTestedApks
androidx.core:core-viewtree:1.0.0=benchmarkTestedApks,debugTestedApks
androidx.core:core:1.18.0=benchmarkTestedApks,debugTestedApks
androidx.core:core:1.9.0=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath
androidx.credentials:credentials:1.6.0=benchmarkTestedApks,debugTestedApks
androidx.cursoradapter:cursoradapter:1.0.0=benchmarkTestedApks,debugTestedApks
androidx.customview:customview:1.0.0=benchmarkTestedApks,debugTestedApks
androidx.drawerlayout:drawerlayout:1.0.0=benchmarkTestedApks,debugTestedApks
+37
View File
@@ -0,0 +1,37 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins { alias(libs.plugins.android.application) }
android {
namespace = "de.rfc1437.ironstorage.providerclient"
compileSdk = libs.versions.compileSdk.get().toInt()
defaultConfig {
applicationId = "de.rfc1437.ironstorage.providerclient"
minSdk = libs.versions.minSdk.get().toInt()
targetSdk = libs.versions.compileSdk.get().toInt()
versionCode = 1
versionName = "1.0"
}
androidResources { noCompress += "html" }
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
lint {
abortOnError = true
warningsAsErrors = true
}
}
kotlin {
jvmToolchain(25)
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
allWarningsAsErrors.set(true)
}
}
dependencies {
implementation(libs.credentials)
implementation(libs.core.ktx)
implementation(libs.fragment)
implementation(libs.annotation)
implementation(libs.coroutines.android)
}
+205
View File
@@ -0,0 +1,205 @@
# This is a Gradle generated file for dependency locking.
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
# To regenerate this file, run: ./gradlew :provider-client:dependencies --write-locks
androidx.activity:activity-ktx:1.8.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.activity:activity:1.8.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.annotation:annotation-experimental:1.4.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.annotation:annotation-jvm:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.annotation:annotation:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.appcompat:appcompat-resources:1.2.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.appcompat:appcompat:1.2.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.arch.core:core-common:2.2.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.arch.core:core-runtime:2.2.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.biometric:biometric:1.1.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.collection:collection-jvm:1.4.2=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath
androidx.collection:collection-jvm:1.6.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.collection:collection:1.4.2=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath
androidx.collection:collection:1.6.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.concurrent:concurrent-futures:1.1.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.core:core-ktx:1.19.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.core:core-viewtree:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.core:core:1.19.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.credentials:credentials:1.6.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.cursoradapter:cursoradapter:1.0.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.customview:customview:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.drawerlayout:drawerlayout:1.0.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.fragment:fragment:1.9.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.interpolator:interpolator:1.0.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.lifecycle:lifecycle-common-jvm:2.10.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.lifecycle:lifecycle-common:2.10.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.lifecycle:lifecycle-livedata-core-ktx:2.10.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.lifecycle:lifecycle-livedata-core:2.10.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.lifecycle:lifecycle-livedata:2.10.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.lifecycle:lifecycle-runtime-android:2.10.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.lifecycle:lifecycle-runtime-ktx-android:2.10.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.lifecycle:lifecycle-runtime-ktx:2.10.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.lifecycle:lifecycle-runtime:2.10.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.lifecycle:lifecycle-viewmodel-android:2.10.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.lifecycle:lifecycle-viewmodel-ktx:2.10.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.lifecycle:lifecycle-viewmodel-savedstate-android:2.10.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.lifecycle:lifecycle-viewmodel-savedstate:2.10.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.lifecycle:lifecycle-viewmodel:2.10.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.loader:loader:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.profileinstaller:profileinstaller:1.4.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.savedstate:savedstate-android:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.savedstate:savedstate-ktx:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.savedstate:savedstate:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.startup:startup-runtime:1.1.1=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.tracing:tracing-android:2.0.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.tracing:tracing:2.0.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.vectordrawable:vectordrawable-animated:1.1.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.vectordrawable:vectordrawable:1.1.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.versionedparcelable:versionedparcelable:1.1.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.viewpager:viewpager:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
ch.qos.logback:logback-classic:1.3.15=ktlint
ch.qos.logback:logback-core:1.3.15=ktlint
com.android.tools.analytics-library:protos:32.4.1=androidLintTool
com.android.tools.analytics-library:shared:32.4.1=androidLintTool
com.android.tools.analytics-library:tracker:32.4.1=androidLintTool
com.android.tools.androidtest:android-test-engine-result-listener:1.0.1=unified-test-platform-gradle-work-action
com.android.tools.androidtest:android-test-engine:1.0.1=unified-test-platform-gradle-work-action
com.android.tools.androidtest:test-result-event-proto:1.0.1=unified-test-platform-gradle-work-action
com.android.tools.androidtest:test-result-proto:1.0.1=unified-test-platform-gradle-work-action
com.android.tools.build:aapt2-proto:9.4.1-15978811=androidLintTool
com.android.tools.build:builder-model:9.4.1=androidLintTool
com.android.tools.build:manifest-merger:32.4.1=androidLintTool
com.android.tools.ddms:ddmlib:32.4.1=androidLintTool
com.android.tools.external.com-intellij:intellij-core:32.4.1=androidLintTool
com.android.tools.external.com-intellij:kotlin-compiler:32.4.1=androidLintTool
com.android.tools.external.org-jetbrains:uast:32.4.1=androidLintTool
com.android.tools.layoutlib:layoutlib-api:32.4.1=androidLintTool
com.android.tools.lint:lint-api:32.4.1=androidLintTool
com.android.tools.lint:lint-checks:32.4.1=androidLintTool
com.android.tools.lint:lint-gradle:32.4.1=androidLintTool
com.android.tools.lint:lint-model:32.4.1=androidLintTool
com.android.tools.lint:lint-typedef-remover:32.4.1=androidLintTool
com.android.tools.lint:lint:32.4.1=androidLintTool
com.android.tools.utp:gradle-work-action:32.4.1=unified-test-platform-gradle-work-action
com.android.tools:annotations:32.4.1=androidLintTool
com.android.tools:common:32.4.1=androidLintTool
com.android.tools:dvlib:32.4.1=androidLintTool
com.android.tools:play-sdk-proto:32.4.1=androidLintTool
com.android.tools:repository:32.4.1=androidLintTool
com.android.tools:sdk-common:32.4.1=androidLintTool
com.android.tools:sdklib:32.4.1=androidLintTool
com.github.ajalt.clikt:clikt-jvm:5.0.3=ktlint
com.github.ajalt.clikt:clikt:5.0.3=ktlint
com.google.code.findbugs:jsr305:3.0.2=androidLintTool
com.google.code.gson:gson:2.11.0=androidLintTool
com.google.errorprone:error_prone_annotations:2.36.0=androidLintTool
com.google.guava:failureaccess:1.0.2=androidLintTool
com.google.guava:guava:33.4.0-jre=androidLintTool
com.google.guava:listenablefuture:1.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=androidLintTool
com.google.j2objc:j2objc-annotations:3.0.0=androidLintTool
com.google.jimfs:jimfs:1.1=androidLintTool
com.google.protobuf:protobuf-java:3.25.5=androidLintTool,unified-test-platform-gradle-work-action
com.pinterest.ktlint:ktlint-cli-reporter-baseline:1.7.1=ktlint,ktlintBaselineReporter
com.pinterest.ktlint:ktlint-cli-reporter-checkstyle:1.7.1=ktlint
com.pinterest.ktlint:ktlint-cli-reporter-core:1.7.1=ktlint,ktlintBaselineReporter
com.pinterest.ktlint:ktlint-cli-reporter-format:1.7.1=ktlint
com.pinterest.ktlint:ktlint-cli-reporter-html:1.7.1=ktlint
com.pinterest.ktlint:ktlint-cli-reporter-json:1.7.1=ktlint
com.pinterest.ktlint:ktlint-cli-reporter-plain-summary:1.7.1=ktlint
com.pinterest.ktlint:ktlint-cli-reporter-plain:1.7.1=ktlint
com.pinterest.ktlint:ktlint-cli-reporter-sarif:1.7.1=ktlint
com.pinterest.ktlint:ktlint-cli-ruleset-core:1.7.1=ktlint,ktlintRuleset
com.pinterest.ktlint:ktlint-cli:1.7.1=ktlint
com.pinterest.ktlint:ktlint-logger:1.7.1=ktlint,ktlintBaselineReporter,ktlintRuleset
com.pinterest.ktlint:ktlint-rule-engine-core:1.7.1=ktlint,ktlintBaselineReporter,ktlintRuleset
com.pinterest.ktlint:ktlint-rule-engine:1.7.1=ktlint
com.pinterest.ktlint:ktlint-ruleset-standard:1.7.1=ktlint,ktlintRuleset
com.sun.istack:istack-commons-runtime:3.0.8=androidLintTool
com.sun.xml.fastinfoset:FastInfoset:1.2.16=androidLintTool
commons-codec:commons-codec:1.17.1=androidLintTool
commons-io:commons-io:2.16.1=androidLintTool
commons-logging:commons-logging:1.2=androidLintTool
dev.drewhamilton.poko:poko-annotations-jvm:0.19.1=ktlint,ktlintBaselineReporter,ktlintRuleset
dev.drewhamilton.poko:poko-annotations:0.19.1=ktlint,ktlintBaselineReporter,ktlintRuleset
io.github.detekt.sarif4k:sarif4k-jvm:0.6.0=ktlint,ktlintReporter
io.github.detekt.sarif4k:sarif4k:0.6.0=ktlint,ktlintReporter
io.github.oshai:kotlin-logging-jvm:7.0.7=ktlint,ktlintBaselineReporter,ktlintReporter,ktlintRuleset
io.github.oshai:kotlin-logging:5.1.0=ktlint,ktlintBaselineReporter,ktlintReporter
jakarta.activation:jakarta.activation-api:1.2.1=androidLintTool
jakarta.xml.bind:jakarta.xml.bind-api:2.3.2=androidLintTool
javax.inject:javax.inject:1=androidLintTool
net.java.dev.jna:jna-platform:5.6.0=androidLintTool
net.java.dev.jna:jna:5.6.0=androidLintTool
net.sf.kxml:kxml2:2.3.0=androidLintTool
org.apache.commons:commons-compress:1.27.1=androidLintTool
org.apache.commons:commons-lang3:3.16.0=androidLintTool
org.apache.groovy:groovy-bom:4.0.29=androidLintTool
org.apache.groovy:groovy:4.0.29=androidLintTool
org.apache.httpcomponents:httpclient:4.5.6=androidLintTool
org.apache.httpcomponents:httpcore:4.4.16=androidLintTool
org.apache.httpcomponents:httpmime:4.5.6=androidLintTool
org.bouncycastle:bcpkix-jdk18on:1.80.2=androidLintTool
org.bouncycastle:bcprov-jdk18on:1.80.2=androidLintTool
org.bouncycastle:bcutil-jdk18on:1.80.2=androidLintTool
org.checkerframework:checker-qual:3.43.0=androidLintTool
org.ec4j.core:ec4j-core:1.1.1=ktlint,ktlintBaselineReporter,ktlintRuleset
org.glassfish.jaxb:jaxb-runtime:2.3.2=androidLintTool
org.glassfish.jaxb:txw2:2.3.2=androidLintTool
org.jetbrains.kotlin:kotlin-build-tools-api:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-build-tools-api:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-compat:2.4.21=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-cri-impl:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-build-tools-cri-impl:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-impl:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-build-tools-impl:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-compiler-embeddable:2.2.0=ktlint,ktlintBaselineReporter,ktlintRuleset
org.jetbrains.kotlin:kotlin-compiler-embeddable:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-compiler-embeddable:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-compiler-runner:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-compiler-runner:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-daemon-client:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-daemon-client:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-daemon-embeddable:2.2.0=ktlint,ktlintBaselineReporter,ktlintRuleset
org.jetbrains.kotlin:kotlin-daemon-embeddable:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-daemon-embeddable:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-reflect:1.6.10=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,ktlint,ktlintBaselineReporter,ktlintRuleset
org.jetbrains.kotlin:kotlin-reflect:2.2.10=androidLintTool
org.jetbrains.kotlin:kotlin-script-runtime:2.2.0=ktlint,ktlintBaselineReporter,ktlintRuleset
org.jetbrains.kotlin:kotlin-script-runtime:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-script-runtime:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-stdlib-common:2.2.0=ktlintReporter
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0=ktlintReporter
org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.2.10=androidLintTool,unified-test-platform-gradle-work-action
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0=ktlintReporter
org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.2.10=androidLintTool,unified-test-platform-gradle-work-action
org.jetbrains.kotlin:kotlin-stdlib:2.2.0=ktlint,ktlintBaselineReporter,ktlintReporter,ktlintRuleset
org.jetbrains.kotlin:kotlin-stdlib:2.2.10=androidLintTool,unified-test-platform-gradle-work-action
org.jetbrains.kotlin:kotlin-stdlib:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-stdlib:2.4.21=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlin:kotlin-tooling-core:2.4.21=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.9.0=androidLintTool,unified-test-platform-gradle-work-action
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,ktlint,ktlintBaselineReporter,ktlintRuleset
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.9.0=androidLintTool,unified-test-platform-gradle-work-action
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0=androidLintTool,unified-test-platform-gradle-work-action
org.jetbrains.kotlinx:kotlinx-serialization-bom:1.7.3=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.4.1=ktlintReporter
org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.7.3=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-serialization-core:1.4.1=ktlintReporter
org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.3=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.4.1=ktlintReporter
org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1=ktlintReporter
org.jetbrains:annotations:13.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,ktlint,ktlintBaselineReporter,ktlintReporter,ktlintRuleset
org.jetbrains:annotations:23.0.0=androidLintTool,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,unified-test-platform-gradle-work-action
org.jspecify:jspecify:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.junit.platform:junit-platform-commons:1.13.3=unified-test-platform-gradle-work-action
org.junit.platform:junit-platform-engine:1.13.3=unified-test-platform-gradle-work-action
org.junit.platform:junit-platform-launcher:1.13.3=unified-test-platform-gradle-work-action
org.junit:junit-bom:5.13.3=unified-test-platform-gradle-work-action
org.jvnet.staxex:stax-ex:1.8.1=androidLintTool
org.opentest4j:opentest4j:1.3.0=unified-test-platform-gradle-work-action
org.ow2.asm:asm-analysis:9.9=androidLintTool
org.ow2.asm:asm-commons:9.9=androidLintTool
org.ow2.asm:asm-tree:9.9=androidLintTool
org.ow2.asm:asm:9.9=androidLintTool
org.slf4j:slf4j-api:2.0.7=ktlint
empty=androidApis,androidJdkImage,androidTestUtil,coreLibraryDesugaring,debugAndroidTestAnnotationProcessorClasspath,debugAndroidTestImplementationDependenciesMetadata,debugAndroidTestRuntimeClasspath,debugAnnotationProcessorClasspath,debugImplementationDependenciesMetadata,debugReverseMetadataValues,debugUnitTestAnnotationProcessorClasspath,debugUnitTestImplementationDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease,lintChecks,lintPublish,releaseAnnotationProcessorClasspath,releaseImplementationDependenciesMetadata,releaseReverseMetadataValues
@@ -0,0 +1,14 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<!-- This password-only fixture invokes Credential Manager on API 34+ and declares no website association. -->
<application android:allowBackup="false" android:fullBackupContent="@xml/backup_rules"
android:dataExtractionRules="@xml/data_extraction_rules" android:icon="@android:drawable/ic_lock_lock" android:label="Credential provider fixture"
android:theme="@android:style/Theme.Material.Light.NoActionBar"
android:usesCleartextTraffic="false" tools:ignore="CredentialDependency,CredManMissingDal">
<activity android:name=".ProviderClientActivity" android:exported="true">
<intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter>
</activity>
<provider android:name=".BrowserFixtureProvider"
android:authorities="de.rfc1437.ironstorage.providerclient.fixture"
android:exported="false" android:grantUriPermissions="true" />
</application>
</manifest>
@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>IronStorage browser fixture</title>
<style>body{font:20px sans-serif;padding:24px}input,button{display:block;font:inherit;margin:16px 0;padding:12px;max-width:100%;box-sizing:border-box}</style>
<h1>Browser credential fixture</h1>
<p>This local page uses public synthetic credentials. It sends no network requests.</p>
<form autocomplete="on" onsubmit="return false">
<label for="username">Username</label>
<input id="username" name="username" type="text" autocomplete="username">
<label for="password">Password</label>
<input id="password" name="password" type="password" autocomplete="current-password">
<button type="button" onclick="document.getElementById('status').textContent=document.getElementById('username').value==='fixture-alice'&&document.getElementById('password').value==='public-provider-fixture-password'?'Chrome received the expected synthetic credential':'The fixture has not been filled'">Verify filled fixture</button>
</form><p id="status">Waiting for an explicit fill.</p>
</html>
@@ -0,0 +1,68 @@
package de.rfc1437.ironstorage.providerclient
import android.content.ContentProvider
import android.content.ContentValues
import android.content.res.AssetFileDescriptor
import android.database.Cursor
import android.database.MatrixCursor
import android.net.Uri
import android.os.ParcelFileDescriptor
import android.provider.OpenableColumns
import java.io.File
/** A single public, read-only fixture asset; there is no application-data access. */
class BrowserFixtureProvider : ContentProvider() {
private lateinit var page: File
override fun onCreate(): Boolean {
val app = requireNotNull(context)
page = File(app.cacheDir, "public_provider_form.html")
app.assets.open("provider_form.html").use { source ->
page.outputStream().use { target -> source.copyTo(target) }
}
return true
}
private fun checkPage(uri: Uri) {
require(uri.path == "/credential_form.html")
}
override fun getType(uri: Uri): String {
checkPage(uri)
return "text/html"
}
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor {
checkPage(uri)
return MatrixCursor(arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE)).apply {
addRow(
arrayOf<Any>(
"credential_form.html",
page.length()
)
)
}
}
override fun openAssetFile(uri: Uri, mode: String): AssetFileDescriptor {
checkPage(uri)
require(mode == "r")
return AssetFileDescriptor(openFile(uri, mode), 0, page.length())
}
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor {
checkPage(uri)
require(mode == "r")
return ParcelFileDescriptor.open(page, ParcelFileDescriptor.MODE_READ_ONLY)
}
override fun insert(uri: Uri, values: ContentValues?): Uri =
throw UnsupportedOperationException("Read-only fixture")
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int =
throw UnsupportedOperationException("Read-only fixture")
override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<out String>?
): Int = throw UnsupportedOperationException("Read-only fixture")
}
@@ -0,0 +1,162 @@
package de.rfc1437.ironstorage.providerclient
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.text.InputType
import android.view.View
import android.view.autofill.AutofillManager
import android.widget.Button
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.net.toUri
import androidx.credentials.CreatePasswordRequest
import androidx.credentials.CredentialManager
import androidx.credentials.GetCredentialRequest
import androidx.credentials.GetPasswordOption
import androidx.credentials.PasswordCredential
import androidx.credentials.exceptions.NoCredentialException
import androidx.fragment.app.FragmentActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
/** A separate test APK represents an ordinary requesting application. Secrets are synthetic. */
@SuppressLint("SetTextI18n")
class ProviderClientActivity : FragmentActivity() {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val form = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
setPadding(32, 60, 32, 32)
}
val status = TextView(this).apply { text = "Provider client: synthetic credentials only" }
val username = EditText(this).apply {
id = View.generateViewId()
hint = "Username"
inputType = InputType.TYPE_CLASS_TEXT
setAutofillHints(View.AUTOFILL_HINT_USERNAME)
}
val password = EditText(this).apply {
id = View.generateViewId()
hint = "Password"
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
setAutofillHints(View.AUTOFILL_HINT_PASSWORD)
}
form.addView(status)
form.addView(username)
form.addView(password)
fun button(title: String, action: () -> Unit) {
form.addView(
Button(this).apply {
text =
title
setOnClickListener { action() }
}
)
}
button("Open local Chrome fixture") {
val uri = "content://$packageName.fixture/credential_form.html".toUri()
startActivity(
Intent(
Intent.ACTION_VIEW
).setDataAndType(
uri,
"text/html"
).setPackage("com.android.chrome").addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
)
}
button("Request Autofill") {
val manager = getSystemService(AutofillManager::class.java)
manager.cancel()
username.text.clear()
password.text.clear()
manager.requestAutofill(username)
}
button("Open loopback Chrome fixture") {
startActivity(
Intent(Intent.ACTION_VIEW, "http://localhost:8765/provider_form.html".toUri())
.setPackage("com.android.chrome")
)
}
button("Verify filled form") {
status.text =
if (username.text.toString() == "fixture-alice" &&
password.text.toString() == "public-provider-fixture-password"
) {
"Autofill returned the expected synthetic credential"
} else {
"Autofill has not returned the expected fixture"
}
}
button("Request Credential Manager") {
if (Build.VERSION.SDK_INT <
34
) {
status.text = "Credential Manager provider requires API 34"
return@button
}
scope.launch {
try {
val response = CredentialManager.create(
this@ProviderClientActivity
).getCredential(
this@ProviderClientActivity,
GetCredentialRequest(listOf(GetPasswordOption()))
)
val value = response.credential as PasswordCredential
status.text =
if (value.id == "fixture-alice" &&
value.password == "public-provider-fixture-password"
) {
"Credential Manager returned the expected synthetic credential"
} else {
"Credential Manager returned an unexpected fixture"
}
} catch (_: NoCredentialException) {
status.text = "No enabled provider offered a password credential"
} catch (_: Exception) {
status.text =
"Credential Manager request cancelled or rejected"
}
}
}
button("Commit form for save offer") {
getSystemService(AutofillManager::class.java).commit()
}
button("Populate synthetic save fields") {
username.setText("fixture-bob")
password.setText("public-provider-new-password")
}
button("Request Credential Manager save") {
if (Build.VERSION.SDK_INT < 34) {
status.text = "Credential Manager provider requires API 34"
return@button
}
scope.launch {
try {
CredentialManager.create(
this@ProviderClientActivity
).createCredential(
this@ProviderClientActivity,
CreatePasswordRequest("fixture-bob", "public-provider-new-password")
)
status.text = "Credential Manager save confirmed"
} catch (_: Exception) {
status.text =
"Credential Manager save cancelled or rejected"
}
}
}
setContentView(form)
}
override fun onDestroy() {
scope.cancel()
super.onDestroy()
}
}
@@ -0,0 +1,11 @@
<full-backup-content>
<exclude domain="root" path="." />
<exclude domain="file" path="." />
<exclude domain="database" path="." />
<exclude domain="sharedpref" path="." />
<exclude domain="external" path="." />
<exclude domain="device_root" path="." />
<exclude domain="device_file" path="." />
<exclude domain="device_database" path="." />
<exclude domain="device_sharedpref" path="." />
</full-backup-content>
@@ -0,0 +1,24 @@
<data-extraction-rules>
<cloud-backup>
<exclude domain="root" path="." />
<exclude domain="file" path="." />
<exclude domain="database" path="." />
<exclude domain="sharedpref" path="." />
<exclude domain="external" path="." />
<exclude domain="device_root" path="." />
<exclude domain="device_file" path="." />
<exclude domain="device_database" path="." />
<exclude domain="device_sharedpref" path="." />
</cloud-backup>
<device-transfer>
<exclude domain="root" path="." />
<exclude domain="file" path="." />
<exclude domain="database" path="." />
<exclude domain="sharedpref" path="." />
<exclude domain="external" path="." />
<exclude domain="device_root" path="." />
<exclude domain="device_file" path="." />
<exclude domain="device_database" path="." />
<exclude domain="device_sharedpref" path="." />
</device-transfer>
</data-extraction-rules>
+1 -1
View File
@@ -35,4 +35,4 @@ dependencyResolutionManagement {
}
}
rootProject.name = "IronStorage"
include(":performance", ":app", ":features", ":core", ":platform", ":rust-bridge")
include(":provider-client", ":performance", ":app", ":features", ":core", ":platform", ":rust-bridge")