Implement Android Credential Manager and Autofill providers (#109)
This commit is contained in:
1 parent
316a875d63
commit
0c8366d34f
48 files changed
+4138
-78
No files matched your search
@@ -70,6 +70,8 @@ kotlin {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.credentials)
|
||||
implementation(libs.autofill)
|
||||
implementation(project(":features"))
|
||||
implementation(project(":core"))
|
||||
implementation(project(":platform"))
|
||||
|
||||
@@ -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
|
||||
|
||||
+108
@@ -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() }
|
||||
}
|
||||
}
|
||||
+108
@@ -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
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
+362
@@ -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()
|
||||
}
|
||||
}
|
||||
+180
@@ -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>
|
||||
@@ -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" })
|
||||
|
||||
+186
@@ -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)
|
||||
}
|
||||
)
|
||||
+5
@@ -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 {
|
||||
|
||||
+56
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
+172
@@ -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>
|
||||
@@ -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" }
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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>
|
||||
+68
@@ -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")
|
||||
}
|
||||
+162
@@ -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>
|
||||
@@ -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")
|
||||
Reference in new issue
Block a user