Implement Android Credential Manager and Autofill providers (#109)
This commit is contained in:
1 parent
316a875d63
commit
0c8366d34f
48 files changed
+4137
-77
No files matched your search
@@ -20,7 +20,7 @@ import unittest
|
|||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
ABIS = ("arm64-v8a", "armeabi-v7a")
|
ABIS = ("arm64-v8a", "armeabi-v7a")
|
||||||
MODULES = ("app", "core", "features", "platform", "rust-bridge")
|
MODULES = ("app", "core", "features", "platform", "rust-bridge", "provider-client")
|
||||||
SDK = Path(os.environ.get("ANDROID_HOME", os.environ.get("ANDROID_SDK_ROOT", "")))
|
SDK = Path(os.environ.get("ANDROID_HOME", os.environ.get("ANDROID_SDK_ROOT", "")))
|
||||||
CARGO = os.environ.get("CARGO", "cargo")
|
CARGO = os.environ.get("CARGO", "cargo")
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ kotlin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation(libs.credentials)
|
||||||
|
implementation(libs.autofill)
|
||||||
implementation(project(":features"))
|
implementation(project(":features"))
|
||||||
implementation(project(":core"))
|
implementation(project(":core"))
|
||||||
implementation(project(":platform"))
|
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.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-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.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.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=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,profileLintChecksClasspath,profileRuntimeClasspath,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.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.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
|
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-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-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.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.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-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
|
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.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<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-permission android:name="android.permission.ACCESS_NETWORK_STATE" tools:node="remove" />
|
||||||
<uses-feature android:name="android.hardware.camera.any" android:required="false" />
|
<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
|
<application
|
||||||
|
tools:ignore="CredentialDependency"
|
||||||
android:name=".IronStorageApplication"
|
android:name=".IronStorageApplication"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
@@ -15,6 +20,18 @@
|
|||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.IronStorage"
|
android:theme="@style/Theme.IronStorage"
|
||||||
android:usesCleartextTraffic="false">
|
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"
|
<activity android:name=".MainActivity" android:exported="true"
|
||||||
android:windowSoftInputMode="adjustResize"
|
android:windowSoftInputMode="adjustResize"
|
||||||
android:supportsPictureInPicture="false">
|
android:supportsPictureInPicture="false">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package de.rfc1437.ironstorage
|
package de.rfc1437.ironstorage
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.content.ComponentCallbacks2
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
import de.rfc1437.ironstorage.core.AuthenticationLifecycle
|
import de.rfc1437.ironstorage.core.AuthenticationLifecycle
|
||||||
import de.rfc1437.ironstorage.core.AuthenticationRepository
|
import de.rfc1437.ironstorage.core.AuthenticationRepository
|
||||||
@@ -16,7 +17,12 @@ class IronStorageApplication : Application() {
|
|||||||
|
|
||||||
@Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
|
@Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
|
||||||
override fun onTrimMemory(level: Int) {
|
override fun onTrimMemory(level: Int) {
|
||||||
|
// 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)
|
authentication.lifecycle(AuthenticationLifecycle.MemoryPressure)
|
||||||
|
}
|
||||||
super.onTrimMemory(level)
|
super.onTrimMemory(level)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,13 @@ import kotlinx.coroutines.SupervisorJob
|
|||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
object ShellModule {
|
object ShellModule {
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun credentials(
|
||||||
|
authentication: NativeAuthenticationRepository
|
||||||
|
): de.rfc1437.ironstorage.core.NativeCredentialRepository =
|
||||||
|
de.rfc1437.ironstorage.core.NativeCredentialRepository(authentication)
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun androidSettingsPlatform(
|
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(
|
val allowed = mapOf(
|
||||||
|
":provider-client" to emptySet(),
|
||||||
":performance" to setOf(":app"),
|
":performance" to setOf(":app"),
|
||||||
":app" to setOf(":features", ":core", ":platform"),
|
":app" to setOf(":features", ":core", ":platform"),
|
||||||
":features" to setOf(":core"),
|
":features" to setOf(":core"),
|
||||||
@@ -133,6 +134,7 @@ tasks.register("dependencyInventory") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tasks.register("androidVerification") {
|
tasks.register("androidVerification") {
|
||||||
|
dependsOn(":provider-client:assembleDebug")
|
||||||
dependsOn("checkArchitecture", "dependencyInventory", "auditNativePackages")
|
dependsOn("checkArchitecture", "dependencyInventory", "auditNativePackages")
|
||||||
dependsOn(subprojects.map { "${it.path}:ktlintCheck" })
|
dependsOn(subprojects.map { "${it.path}:ktlintCheck" })
|
||||||
dependsOn(subprojects.filter { it.name != "performance" }.map { "${it.path}:lintDebug" })
|
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 {
|
scope.launch {
|
||||||
while (true) {
|
while (true) {
|
||||||
delay(250)
|
delay(250)
|
||||||
|
try {
|
||||||
|
withContext(io) { current?.expireCredentialRequests() }
|
||||||
|
} catch (_: MobileAuthenticationFfiException) {
|
||||||
|
lock()
|
||||||
|
}
|
||||||
if (mutable.value.unlocked && !mutable.value.busy) {
|
if (mutable.value.unlocked && !mutable.value.busy) {
|
||||||
val ticket = sequence.get()
|
val ticket = sequence.get()
|
||||||
try {
|
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": {
|
"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.12.0": "Apache-2.0",
|
||||||
"androidx.activity:activity-compose:1.13.0": "Apache-2.0",
|
"androidx.activity:activity-compose:1.13.0": "Apache-2.0",
|
||||||
"androidx.activity:activity-ktx:1.12.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.4.1": "Apache-2.0",
|
||||||
"androidx.annotation:annotation-experimental:1.5.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.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-resources:1.2.0": "Apache-2.0",
|
||||||
"androidx.appcompat:appcompat: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-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.arch.core:core-runtime:2.2.0": "Apache-2.0",
|
||||||
"androidx.autofill:autofill:1.0.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.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.5.0": "Apache-2.0",
|
||||||
"androidx.collection:collection-jvm:1.6.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.5.0": "Apache-2.0",
|
||||||
"androidx.collection:collection-ktx:1.6.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-android:1.12.1": "Apache-2.0",
|
||||||
"androidx.compose.animation:animation-core-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-android:1.12.1": "Apache-2.0",
|
||||||
"androidx.compose.foundation:foundation-layout-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-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.material3:material3-android:1.4.0": "Apache-2.0",
|
||||||
"androidx.compose.material:material-ripple-android:1.12.1": "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",
|
"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-geometry-android:1.12.1": "Apache-2.0",
|
||||||
"androidx.compose.ui:ui-graphics-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-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-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-text-android:1.12.1": "Apache-2.0",
|
||||||
"androidx.compose.ui:ui-unit-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.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-ktx:1.2.0": "Apache-2.0",
|
||||||
"androidx.concurrent:concurrent-futures:1.1.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.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.13.1": "Apache-2.0",
|
||||||
"androidx.core:core-ktx:1.16.0": "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.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-viewtree:1.0.0": "Apache-2.0",
|
||||||
"androidx.core:core:1.13.1": "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.16.0": "Apache-2.0",
|
||||||
"androidx.core:core:1.18.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.cursoradapter:cursoradapter:1.0.0": "Apache-2.0",
|
||||||
"androidx.customview:customview-poolingcontainer: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.customview:customview:1.0.0": "Apache-2.0",
|
||||||
"androidx.drawerlayout:drawerlayout: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.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.5.1": "Apache-2.0",
|
||||||
"androidx.fragment:fragment:1.9.1": "Apache-2.0",
|
"androidx.fragment:fragment:1.9.1": "Apache-2.0",
|
||||||
"androidx.graphics:graphics-path:1.0.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-java8:2.11.0": "Apache-2.0",
|
||||||
"androidx.lifecycle:lifecycle-common-jvm:2.10.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-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.10.0": "Apache-2.0",
|
||||||
"androidx.lifecycle:lifecycle-livedata-core-ktx:2.11.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",
|
"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-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.10.0": "Apache-2.0",
|
||||||
"androidx.lifecycle:lifecycle-runtime-ktx-android:2.11.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.10.0": "Apache-2.0",
|
||||||
"androidx.lifecycle:lifecycle-viewmodel-android:2.11.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",
|
"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.10.0": "Apache-2.0",
|
||||||
"androidx.lifecycle:lifecycle-viewmodel:2.11.0": "Apache-2.0",
|
"androidx.lifecycle:lifecycle-viewmodel:2.11.0": "Apache-2.0",
|
||||||
"androidx.loader:loader:1.0.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-runtime-android:1.2.0": "Apache-2.0",
|
||||||
"androidx.navigation3:navigation3-ui-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-android:1.1.1": "Apache-2.0",
|
||||||
"androidx.navigationevent:navigationevent-compose-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.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-android:1.4.0": "Apache-2.0",
|
||||||
"androidx.savedstate:savedstate-compose-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",
|
"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-core:3.7.0": "Apache-2.0",
|
||||||
"androidx.test.espresso:espresso-idling-resource: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.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.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:core:1.7.0": "Apache-2.0",
|
||||||
"androidx.test:monitor:1.8.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.test:runner:1.7.0": "Apache-2.0",
|
||||||
"androidx.tracing:tracing-android:2.0.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.1.0": "Apache-2.0",
|
||||||
"androidx.tracing:tracing:1.2.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-animated:1.1.0": "Apache-2.0",
|
||||||
"androidx.vectordrawable:vectordrawable: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.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.viewpager:viewpager:1.0.0": "Apache-2.0",
|
||||||
"androidx.window:window-core-android:1.5.0": "Apache-2.0",
|
"androidx.window:window-core-android:1.5.0": "Apache-2.0",
|
||||||
"androidx.window:window: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.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-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:dagger:2.60.1": "Apache-2.0",
|
||||||
"com.google.dagger:hilt-android: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.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.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: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",
|
"jakarta.inject:jakarta.inject-api:2.0.1": "Apache-2.0",
|
||||||
"javax.inject:javax.inject:1": "Apache-2.0",
|
"javax.inject:javax.inject:1": "Apache-2.0",
|
||||||
"junit:junit:4.13.2": "EPL-1.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",
|
"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-core:1.3": "BSD-3-Clause",
|
||||||
"org.hamcrest:hamcrest-library: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.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.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.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-coroutines-test-jvm:1.11.0": "Apache-2.0",
|
||||||
"org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.7.3": "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:13.0": "Apache-2.0",
|
||||||
"org.jetbrains:annotations:23.0.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",
|
"org.jsoup:jsoup:1.15.1": "MIT",
|
||||||
"androidx.annotation:annotation-jvm:1.8.1": "Apache-2.0",
|
"org.jspecify:jspecify:1.0.0": "Apache-2.0",
|
||||||
"androidx.arch.core:core-common:2.1.0": "Apache-2.0",
|
"rustls:rustls-platform-verifier:0.1.1": "MIT OR 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"
|
|
||||||
},
|
},
|
||||||
"rust_licenses": [
|
"rust_licenses": [
|
||||||
"(Apache-2.0 OR MIT) AND BSD-3-Clause",
|
"(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)) }
|
}) { Text(stringResource(R.string.settings_rebuild)) }
|
||||||
}
|
}
|
||||||
"providers" -> {
|
"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 {
|
page?.let {
|
||||||
ProviderSetting(
|
ProviderSetting(
|
||||||
stringResource(R.string.settings_autofill),
|
stringResource(R.string.settings_autofill),
|
||||||
|
|||||||
@@ -156,7 +156,7 @@
|
|||||||
<string name="settings_git">Git commit author name and email</string>
|
<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_auth">Authentication, inactivity timeout, biometrics and appearance</string>
|
||||||
<string name="settings_cache">TOTP discovery cache</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_recovery">Recovery and offline use</string>
|
||||||
<string name="settings_about">Version, licenses, privacy and support</string>
|
<string name="settings_about">Version, licenses, privacy and support</string>
|
||||||
<string name="settings_no_token">No application token stored</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="one">%1$s password entry</item>
|
||||||
<item quantity="other">%1$s password entries</item>
|
<item quantity="other">%1$s password entries</item>
|
||||||
</plurals>
|
</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>
|
</resources>
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
[versions]
|
[versions]
|
||||||
|
credentials = "1.6.0"
|
||||||
|
autofill = "1.3.0"
|
||||||
camera = "1.6.2"
|
camera = "1.6.2"
|
||||||
agp = "9.4.1"
|
agp = "9.4.1"
|
||||||
kotlin = "2.4.21"
|
kotlin = "2.4.21"
|
||||||
@@ -29,6 +31,9 @@ biometric = "1.1.0"
|
|||||||
fragment = "1.9.1"
|
fragment = "1.9.1"
|
||||||
|
|
||||||
[libraries]
|
[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-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camera" }
|
||||||
camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camera" }
|
camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camera" }
|
||||||
camera-view = { module = "androidx.camera:camera-view", version.ref = "camera" }
|
camera-view = { module = "androidx.camera:camera-view", version.ref = "camera" }
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
</configuration>
|
</configuration>
|
||||||
<components>
|
<components>
|
||||||
<component group="androidx.activity" name="activity" version="1.1.0">
|
<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">
|
<artifact name="activity-1.1.0.pom">
|
||||||
<sha256 value="3690001fc5a57ad176cacf7a13f56219bffb73b93c7978dbac1e1e5db082d6c0" origin="Generated by Gradle"/>
|
<sha256 value="3690001fc5a57ad176cacf7a13f56219bffb73b93c7978dbac1e1e5db082d6c0" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
@@ -89,6 +92,11 @@
|
|||||||
<sha256 value="2e9372ba7780ef44952adbf86b66e1f08682c1e5277c926185f6564a13799efe" origin="Generated by Gradle"/>
|
<sha256 value="2e9372ba7780ef44952adbf86b66e1f08682c1e5277c926185f6564a13799efe" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</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">
|
<component group="androidx.annotation" name="annotation" version="1.11.0">
|
||||||
<artifact name="annotation-1.11.0.module">
|
<artifact name="annotation-1.11.0.module">
|
||||||
<sha256 value="ac8410bea7adaabc9e71285f0627c9cb929c5addd00cfc6d0ebd421dc2da1ee0" origin="Generated by Gradle"/>
|
<sha256 value="ac8410bea7adaabc9e71285f0627c9cb929c5addd00cfc6d0ebd421dc2da1ee0" origin="Generated by Gradle"/>
|
||||||
@@ -151,6 +159,14 @@
|
|||||||
<sha256 value="ae1ff29a7f3965e6589acfe7f07ef39d36405d7ab278472327afd92fc6d72a3f" origin="Generated by Gradle"/>
|
<sha256 value="ae1ff29a7f3965e6589acfe7f07ef39d36405d7ab278472327afd92fc6d72a3f" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</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">
|
<component group="androidx.annotation" name="annotation-jvm" version="1.11.0">
|
||||||
<artifact name="annotation-jvm-1.11.0.jar">
|
<artifact name="annotation-jvm-1.11.0.jar">
|
||||||
<sha256 value="a83b827cc17fd6b41c7bf5e7a2a2b40a0895b00b3e30fdfdb820b49dc7b667f3" origin="Generated by Gradle"/>
|
<sha256 value="a83b827cc17fd6b41c7bf5e7a2a2b40a0895b00b3e30fdfdb820b49dc7b667f3" origin="Generated by Gradle"/>
|
||||||
@@ -204,6 +220,11 @@
|
|||||||
<sha256 value="75d9865bc6b0f779043b4f6523a944bd2ccb7526f5c1f5ef24624ac78dec3bd3" origin="Generated by Gradle"/>
|
<sha256 value="75d9865bc6b0f779043b4f6523a944bd2ccb7526f5c1f5ef24624ac78dec3bd3" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</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">
|
<component group="androidx.arch.core" name="core-common" version="2.1.0">
|
||||||
<artifact name="core-common-2.1.0.jar">
|
<artifact name="core-common-2.1.0.jar">
|
||||||
<sha256 value="fe1237bf029d063e7f29fe39aeaf73ef74c8b0a3658486fc29d3c54326653889" origin="Generated by Gradle"/>
|
<sha256 value="fe1237bf029d063e7f29fe39aeaf73ef74c8b0a3658486fc29d3c54326653889" origin="Generated by Gradle"/>
|
||||||
@@ -244,6 +265,14 @@
|
|||||||
<sha256 value="3677abead5d4e06cf76cbacb954151e051779c2274494451fb424e375aa9e450" origin="Generated by Gradle"/>
|
<sha256 value="3677abead5d4e06cf76cbacb954151e051779c2274494451fb424e375aa9e450" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</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">
|
<component group="androidx.benchmark" name="benchmark-common" version="1.5.0">
|
||||||
<artifact name="benchmark-common-1.5.0.aar">
|
<artifact name="benchmark-common-1.5.0.aar">
|
||||||
<sha256 value="1b915a31dae4d4c57a1641a16d063983c29069290ce4153c1f356e97bd247db4" origin="Generated by Gradle"/>
|
<sha256 value="1b915a31dae4d4c57a1641a16d063983c29069290ce4153c1f356e97bd247db4" origin="Generated by Gradle"/>
|
||||||
@@ -955,6 +984,9 @@
|
|||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</component>
|
||||||
<component group="androidx.core" name="core" version="1.15.0">
|
<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">
|
<artifact name="core-1.15.0.module">
|
||||||
<sha256 value="e8a6c386e1765d870012fed2221173d4a2e8d2fd5aec732c51afb4a9f4684519" origin="Generated by Gradle"/>
|
<sha256 value="e8a6c386e1765d870012fed2221173d4a2e8d2fd5aec732c51afb4a9f4684519" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
@@ -975,7 +1007,18 @@
|
|||||||
<sha256 value="b67ff7a1f9be95687dfa86d6055147fc1d8bbe3b1dc19048c432afca18abfd95" origin="Generated by Gradle"/>
|
<sha256 value="b67ff7a1f9be95687dfa86d6055147fc1d8bbe3b1dc19048c432afca18abfd95" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</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">
|
<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">
|
<artifact name="core-1.3.2.pom">
|
||||||
<sha256 value="afb5ea494dd083ed404cd51f580d218e37362f8ae326e893bee521290ed34920" origin="Generated by Gradle"/>
|
<sha256 value="afb5ea494dd083ed404cd51f580d218e37362f8ae326e893bee521290ed34920" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
@@ -1024,6 +1067,14 @@
|
|||||||
<sha256 value="ab530b04e2fbe73205484899a3be56c90968873eb496fc52e5ea7596d91035d1" origin="Generated by Gradle"/>
|
<sha256 value="ab530b04e2fbe73205484899a3be56c90968873eb496fc52e5ea7596d91035d1" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</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">
|
<component group="androidx.core" name="core-ktx" version="1.16.0">
|
||||||
<artifact name="core-ktx-1.16.0.aar">
|
<artifact name="core-ktx-1.16.0.aar">
|
||||||
<sha256 value="1766dbd82f64a12dcd359ecb6f15f3cf35db4d66d22961a7c51b2ffc6468314c" origin="Generated by Gradle"/>
|
<sha256 value="1766dbd82f64a12dcd359ecb6f15f3cf35db4d66d22961a7c51b2ffc6468314c" origin="Generated by Gradle"/>
|
||||||
@@ -1040,6 +1091,14 @@
|
|||||||
<sha256 value="3fa579c11de0f6f378002c0f0bb4bbe71100634f80f8b282951789ea7d0fb849" origin="Generated by Gradle"/>
|
<sha256 value="3fa579c11de0f6f378002c0f0bb4bbe71100634f80f8b282951789ea7d0fb849" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</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">
|
<component group="androidx.core" name="core-ktx" version="1.2.0">
|
||||||
<artifact name="core-ktx-1.2.0.pom">
|
<artifact name="core-ktx-1.2.0.pom">
|
||||||
<sha256 value="29eaddba4e6a35e68a128e594ebae3b72ef7e767f1b273ef93dc2ed019ff97f6" origin="Generated by Gradle"/>
|
<sha256 value="29eaddba4e6a35e68a128e594ebae3b72ef7e767f1b273ef93dc2ed019ff97f6" origin="Generated by Gradle"/>
|
||||||
@@ -1066,6 +1125,14 @@
|
|||||||
<sha256 value="11386cfa46cbbfddb6a4059f14354c00691cf65d3d63c3618818a83326ef3c7f" origin="Generated by Gradle"/>
|
<sha256 value="11386cfa46cbbfddb6a4059f14354c00691cf65d3d63c3618818a83326ef3c7f" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</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">
|
<component group="androidx.cursoradapter" name="cursoradapter" version="1.0.0">
|
||||||
<artifact name="cursoradapter-1.0.0.aar">
|
<artifact name="cursoradapter-1.0.0.aar">
|
||||||
<sha256 value="a81c8fe78815fa47df5b749deb52727ad11f9397da58b16017f4eb2c11e28564" origin="Generated by Gradle"/>
|
<sha256 value="a81c8fe78815fa47df5b749deb52727ad11f9397da58b16017f4eb2c11e28564" origin="Generated by Gradle"/>
|
||||||
@@ -1141,6 +1208,9 @@
|
|||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</component>
|
||||||
<component group="androidx.fragment" name="fragment" version="1.2.5">
|
<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">
|
<artifact name="fragment-1.2.5.pom">
|
||||||
<sha256 value="df0aca46b62bb47cc662cbcee63372db6d2a2859478ee38b594fba3433fe30a5" origin="Generated by Gradle"/>
|
<sha256 value="df0aca46b62bb47cc662cbcee63372db6d2a2859478ee38b594fba3433fe30a5" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
@@ -1198,6 +1268,11 @@
|
|||||||
<sha256 value="2baaf7f17551765524bd1e4d2f954a2a0178a112039cf549ba8b7d4b41e66931" origin="Generated by Gradle"/>
|
<sha256 value="2baaf7f17551765524bd1e4d2f954a2a0178a112039cf549ba8b7d4b41e66931" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</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">
|
<component group="androidx.lifecycle" name="lifecycle-common" version="2.1.0">
|
||||||
<artifact name="lifecycle-common-2.1.0.pom">
|
<artifact name="lifecycle-common-2.1.0.pom">
|
||||||
<sha256 value="692c2a60437a95220c8fbb6e8943196f255f535d1a7c58714e6c46530782f276" origin="Generated by Gradle"/>
|
<sha256 value="692c2a60437a95220c8fbb6e8943196f255f535d1a7c58714e6c46530782f276" origin="Generated by Gradle"/>
|
||||||
@@ -1214,6 +1289,9 @@
|
|||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</component>
|
||||||
<component group="androidx.lifecycle" name="lifecycle-common" version="2.2.0">
|
<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">
|
<artifact name="lifecycle-common-2.2.0.pom">
|
||||||
<sha256 value="cc30a8a2a97d3767bd169b747c078727308096dd3ab7917d80b2f1f42d118bd6" origin="Generated by Gradle"/>
|
<sha256 value="cc30a8a2a97d3767bd169b747c078727308096dd3ab7917d80b2f1f42d118bd6" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
@@ -1239,6 +1317,14 @@
|
|||||||
<sha256 value="93747a9145cb36bc71005f598ede32e2b1149ade5a16e62b0e4969345bc62d85" origin="Generated by Gradle"/>
|
<sha256 value="93747a9145cb36bc71005f598ede32e2b1149ade5a16e62b0e4969345bc62d85" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</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">
|
<component group="androidx.lifecycle" name="lifecycle-common" version="2.9.4">
|
||||||
<artifact name="lifecycle-common-2.9.4.module">
|
<artifact name="lifecycle-common-2.9.4.module">
|
||||||
<sha256 value="c578bd755ea46d7a77811309a3539538e2d7fb86a05fc2ad81c655ecf7dff7a4" origin="Generated by Gradle"/>
|
<sha256 value="c578bd755ea46d7a77811309a3539538e2d7fb86a05fc2ad81c655ecf7dff7a4" origin="Generated by Gradle"/>
|
||||||
@@ -1273,6 +1359,14 @@
|
|||||||
<sha256 value="1d1837f3942b33ea30aa2301fdcea2639408a0fd6fd4368c22478fa8153aebbf" origin="Generated by Gradle"/>
|
<sha256 value="1d1837f3942b33ea30aa2301fdcea2639408a0fd6fd4368c22478fa8153aebbf" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</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">
|
<component group="androidx.lifecycle" name="lifecycle-livedata" version="2.1.0">
|
||||||
<artifact name="lifecycle-livedata-2.1.0.pom">
|
<artifact name="lifecycle-livedata-2.1.0.pom">
|
||||||
<sha256 value="1e4c580c995e41cd18455cfb87e3e43955be9db9ec63a8fb3c7259f01b7c4e3c" origin="Generated by Gradle"/>
|
<sha256 value="1e4c580c995e41cd18455cfb87e3e43955be9db9ec63a8fb3c7259f01b7c4e3c" origin="Generated by Gradle"/>
|
||||||
@@ -1307,6 +1401,14 @@
|
|||||||
<sha256 value="e2659e87e890b52b66f300471828b387a205e5387a392713190166f399ecf641" origin="Generated by Gradle"/>
|
<sha256 value="e2659e87e890b52b66f300471828b387a205e5387a392713190166f399ecf641" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</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">
|
<component group="androidx.lifecycle" name="lifecycle-livedata-core" version="2.1.0">
|
||||||
<artifact name="lifecycle-livedata-core-2.1.0.pom">
|
<artifact name="lifecycle-livedata-core-2.1.0.pom">
|
||||||
<sha256 value="6839d2e68c8fee4cc3bbf04e6b291a6ae842403fcd5f8d944c27814d296deff3" origin="Generated by Gradle"/>
|
<sha256 value="6839d2e68c8fee4cc3bbf04e6b291a6ae842403fcd5f8d944c27814d296deff3" origin="Generated by Gradle"/>
|
||||||
@@ -1329,6 +1431,9 @@
|
|||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</component>
|
||||||
<component group="androidx.lifecycle" name="lifecycle-livedata-core" version="2.2.0">
|
<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">
|
<artifact name="lifecycle-livedata-core-2.2.0.pom">
|
||||||
<sha256 value="aa51621be3817d1f5d21336b0c0525fe96ae899cec656c13d8ea18a51ea32c2f" origin="Generated by Gradle"/>
|
<sha256 value="aa51621be3817d1f5d21336b0c0525fe96ae899cec656c13d8ea18a51ea32c2f" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
@@ -1346,6 +1451,14 @@
|
|||||||
<sha256 value="e9c0dc3f0ac54419c0cfed8ff5cecb82943a7c58f7a54169f0d849b2c60a3552" origin="Generated by Gradle"/>
|
<sha256 value="e9c0dc3f0ac54419c0cfed8ff5cecb82943a7c58f7a54169f0d849b2c60a3552" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</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">
|
<component group="androidx.lifecycle" name="lifecycle-livedata-core-ktx" version="2.10.0">
|
||||||
<artifact name="lifecycle-livedata-core-ktx-2.10.0.aar">
|
<artifact name="lifecycle-livedata-core-ktx-2.10.0.aar">
|
||||||
<sha256 value="5f18429749a183e19178d8665ae42bf7b6310b50358ae9e0e0cc452afd6e0bc3" origin="Generated by Gradle"/>
|
<sha256 value="5f18429749a183e19178d8665ae42bf7b6310b50358ae9e0e0cc452afd6e0bc3" origin="Generated by Gradle"/>
|
||||||
@@ -1394,6 +1507,9 @@
|
|||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</component>
|
||||||
<component group="androidx.lifecycle" name="lifecycle-runtime" version="2.2.0">
|
<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">
|
<artifact name="lifecycle-runtime-2.2.0.pom">
|
||||||
<sha256 value="7f154066fed1c9162870f728b208352831b3d1f8f23f5d388958a50ca4e9f441" origin="Generated by Gradle"/>
|
<sha256 value="7f154066fed1c9162870f728b208352831b3d1f8f23f5d388958a50ca4e9f441" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
@@ -1414,6 +1530,14 @@
|
|||||||
<sha256 value="a4cbb01a42d07047bd8d870017c96a1b0b7b4673320e86b66317a13be2ec10c7" origin="Generated by Gradle"/>
|
<sha256 value="a4cbb01a42d07047bd8d870017c96a1b0b7b4673320e86b66317a13be2ec10c7" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</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">
|
<component group="androidx.lifecycle" name="lifecycle-runtime" version="2.9.4">
|
||||||
<artifact name="lifecycle-runtime-2.9.4.module">
|
<artifact name="lifecycle-runtime-2.9.4.module">
|
||||||
<sha256 value="dcbc5b5b50666e84258a74bfd8e514918c593a4ffcef0c0358562a02fbd80c58" origin="Generated by Gradle"/>
|
<sha256 value="dcbc5b5b50666e84258a74bfd8e514918c593a4ffcef0c0358562a02fbd80c58" origin="Generated by Gradle"/>
|
||||||
@@ -1525,6 +1649,9 @@
|
|||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</component>
|
||||||
<component group="androidx.lifecycle" name="lifecycle-viewmodel" version="2.2.0">
|
<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">
|
<artifact name="lifecycle-viewmodel-2.2.0.pom">
|
||||||
<sha256 value="aaa3afaa49ff47e2bc9b2389f939d3e8137616c4616684e2dfb53ba8366aa11d" origin="Generated by Gradle"/>
|
<sha256 value="aaa3afaa49ff47e2bc9b2389f939d3e8137616c4616684e2dfb53ba8366aa11d" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
@@ -1542,6 +1669,14 @@
|
|||||||
<sha256 value="2b406faea5c12f2b8df4b7a60931f846648f2e1f4d78361e198d1184f19a4797" origin="Generated by Gradle"/>
|
<sha256 value="2b406faea5c12f2b8df4b7a60931f846648f2e1f4d78361e198d1184f19a4797" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</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">
|
<component group="androidx.lifecycle" name="lifecycle-viewmodel" version="2.9.4">
|
||||||
<artifact name="lifecycle-viewmodel-2.9.4.module">
|
<artifact name="lifecycle-viewmodel-2.9.4.module">
|
||||||
<sha256 value="964d69d6b877296e8b79a77bb826ef5fb186aecaa7a1a7ff776fb2250c993006" origin="Generated by Gradle"/>
|
<sha256 value="964d69d6b877296e8b79a77bb826ef5fb186aecaa7a1a7ff776fb2250c993006" origin="Generated by Gradle"/>
|
||||||
@@ -1636,6 +1771,14 @@
|
|||||||
<sha256 value="dc6750a6991bfaac0817469865fe2ccb01e26186a0386299a5d056bac2cc72cc" origin="Generated by Gradle"/>
|
<sha256 value="dc6750a6991bfaac0817469865fe2ccb01e26186a0386299a5d056bac2cc72cc" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</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">
|
<component group="androidx.lifecycle" name="lifecycle-viewmodel-savedstate" version="2.5.1">
|
||||||
<artifact name="lifecycle-viewmodel-savedstate-2.5.1.module">
|
<artifact name="lifecycle-viewmodel-savedstate-2.5.1.module">
|
||||||
<sha256 value="29acd5fe614b3f89123eb838f688d625eaa8b422c8d1905b48ad8e760cd7ad8b" origin="Generated by Gradle"/>
|
<sha256 value="29acd5fe614b3f89123eb838f688d625eaa8b422c8d1905b48ad8e760cd7ad8b" origin="Generated by Gradle"/>
|
||||||
@@ -1649,6 +1792,14 @@
|
|||||||
<sha256 value="dafb8649763d29c29cda27bc22fcdab9a9efc53c0fff9ae3de90882eabaa8944" origin="Generated by Gradle"/>
|
<sha256 value="dafb8649763d29c29cda27bc22fcdab9a9efc53c0fff9ae3de90882eabaa8944" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</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">
|
<component group="androidx.lifecycle" name="lifecycle-viewmodel-savedstate-android" version="2.10.0">
|
||||||
<artifact name="lifecycle-viewmodel-savedstate-android-2.10.0.module">
|
<artifact name="lifecycle-viewmodel-savedstate-android-2.10.0.module">
|
||||||
<sha256 value="949d2a862b6b584cee91af70a9e017bdb117d225bb46096da9424a1728ca79e9" origin="Generated by Gradle"/>
|
<sha256 value="949d2a862b6b584cee91af70a9e017bdb117d225bb46096da9424a1728ca79e9" origin="Generated by Gradle"/>
|
||||||
@@ -1809,6 +1960,14 @@
|
|||||||
<sha256 value="ff1ff2b5ad10315fea2994ebf95631d61d6e07c9bf3f89d24dd2096cc0032094" origin="Generated by Gradle"/>
|
<sha256 value="ff1ff2b5ad10315fea2994ebf95631d61d6e07c9bf3f89d24dd2096cc0032094" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</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">
|
<component group="androidx.profileinstaller" name="profileinstaller" version="1.3.1">
|
||||||
<artifact name="profileinstaller-1.3.1.aar">
|
<artifact name="profileinstaller-1.3.1.aar">
|
||||||
<sha256 value="d0e402ec31f24028a1dc7eb6a0a3f9d9635c1459392cd734396343b73d673948" origin="Generated by Gradle"/>
|
<sha256 value="d0e402ec31f24028a1dc7eb6a0a3f9d9635c1459392cd734396343b73d673948" origin="Generated by Gradle"/>
|
||||||
@@ -1847,6 +2006,9 @@
|
|||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</component>
|
||||||
<component group="androidx.savedstate" name="savedstate" version="1.0.0">
|
<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">
|
<artifact name="savedstate-1.0.0.pom">
|
||||||
<sha256 value="844d7d22fcea798c78bf5559229daf88ec9cad8136f5eea0a29aa76b3f8fde7c" origin="Generated by Gradle"/>
|
<sha256 value="844d7d22fcea798c78bf5559229daf88ec9cad8136f5eea0a29aa76b3f8fde7c" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
@@ -2208,6 +2370,9 @@
|
|||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</component>
|
||||||
<component group="androidx.versionedparcelable" name="versionedparcelable" version="1.1.0">
|
<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">
|
<artifact name="versionedparcelable-1.1.0.pom">
|
||||||
<sha256 value="c729c7be0cc06323bda829d460666e79dbd43b799a21089a44bd3b293dc253b5" origin="Generated by Gradle"/>
|
<sha256 value="c729c7be0cc06323bda829d460666e79dbd43b799a21089a44bd3b293dc253b5" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</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-common:2.2.0=benchmarkTestedApks,debugTestedApks
|
||||||
androidx.arch.core:core-runtime:2.1.0=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath
|
androidx.arch.core:core-runtime:2.1.0=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath
|
||||||
androidx.arch.core:core-runtime:2.2.0=benchmarkTestedApks,debugTestedApks
|
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-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-junit4:1.5.0=benchmarkCompileClasspath,benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath
|
||||||
androidx.benchmark:benchmark-macro: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-viewtree:1.0.0=benchmarkTestedApks,debugTestedApks
|
||||||
androidx.core:core:1.18.0=benchmarkTestedApks,debugTestedApks
|
androidx.core:core:1.18.0=benchmarkTestedApks,debugTestedApks
|
||||||
androidx.core:core:1.9.0=benchmarkLintChecksClasspath,benchmarkRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath
|
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.cursoradapter:cursoradapter:1.0.0=benchmarkTestedApks,debugTestedApks
|
||||||
androidx.customview:customview:1.0.0=benchmarkTestedApks,debugTestedApks
|
androidx.customview:customview:1.0.0=benchmarkTestedApks,debugTestedApks
|
||||||
androidx.drawerlayout:drawerlayout: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"
|
rootProject.name = "IronStorage"
|
||||||
include(":performance", ":app", ":features", ":core", ":platform", ":rust-bridge")
|
include(":provider-client", ":performance", ":app", ":features", ":core", ":platform", ":rust-bridge")
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
//! Android provider bindings; all credential decisions remain in storage.
|
||||||
|
use crate::{MobileAuthentication, MobileAuthenticationFfiError};
|
||||||
|
use ironstorage::mobile_credentials as storage;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, uniffi::Record)]
|
||||||
|
pub struct MobileCredentialCaller {
|
||||||
|
pub package: String,
|
||||||
|
pub certificates: Vec<String>,
|
||||||
|
pub origin: Option<String>,
|
||||||
|
}
|
||||||
|
impl From<MobileCredentialCaller> for storage::CredentialCaller {
|
||||||
|
fn from(value: MobileCredentialCaller) -> Self {
|
||||||
|
Self {
|
||||||
|
package: value.package,
|
||||||
|
certificates: value.certificates,
|
||||||
|
origin: value.origin,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Clone, Debug, uniffi::Record)]
|
||||||
|
pub struct MobileCredentialAttribute {
|
||||||
|
pub name: String,
|
||||||
|
pub value: String,
|
||||||
|
}
|
||||||
|
#[derive(Clone, Debug, uniffi::Record)]
|
||||||
|
pub struct MobileCredentialField {
|
||||||
|
pub index: u32,
|
||||||
|
pub hints: Vec<String>,
|
||||||
|
pub html_attributes: Vec<MobileCredentialAttribute>,
|
||||||
|
pub input_type: u32,
|
||||||
|
pub focused: bool,
|
||||||
|
pub editable: bool,
|
||||||
|
}
|
||||||
|
#[derive(Clone, Debug, uniffi::Record)]
|
||||||
|
pub struct MobileCredentialForm {
|
||||||
|
pub username: u32,
|
||||||
|
pub password: u32,
|
||||||
|
}
|
||||||
|
#[derive(Clone, Debug, uniffi::Record)]
|
||||||
|
pub struct MobileCredentialCandidate {
|
||||||
|
pub path: String,
|
||||||
|
pub matched: bool,
|
||||||
|
}
|
||||||
|
#[derive(Clone, Debug, uniffi::Record)]
|
||||||
|
pub struct MobileProviderRequest {
|
||||||
|
pub token: String,
|
||||||
|
pub title: String,
|
||||||
|
pub revision: String,
|
||||||
|
pub candidates: Vec<MobileCredentialCandidate>,
|
||||||
|
}
|
||||||
|
impl From<storage::CredentialRequest> for MobileProviderRequest {
|
||||||
|
fn from(value: storage::CredentialRequest) -> Self {
|
||||||
|
Self {
|
||||||
|
token: value.token,
|
||||||
|
title: value.identity.title,
|
||||||
|
revision: value.revision,
|
||||||
|
candidates: value
|
||||||
|
.candidates
|
||||||
|
.into_iter()
|
||||||
|
.map(|candidate| MobileCredentialCandidate {
|
||||||
|
path: candidate.path,
|
||||||
|
matched: candidate.matched,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(uniffi::Record)]
|
||||||
|
pub struct MobileCredentialValue {
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
impl std::fmt::Debug for MobileCredentialValue {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str("MobileCredentialValue([REDACTED])")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn rejected(detail: &str) -> MobileAuthenticationFfiError {
|
||||||
|
MobileAuthenticationFfiError::Failed {
|
||||||
|
kind: crate::MobileAuthenticationErrorKind::Entry,
|
||||||
|
title: "Credential Request Rejected".into(),
|
||||||
|
detail: detail.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[uniffi::export]
|
||||||
|
pub fn mobile_credential_form(
|
||||||
|
fields: Vec<MobileCredentialField>,
|
||||||
|
) -> Result<MobileCredentialForm, MobileAuthenticationFfiError> {
|
||||||
|
let fields: Vec<_> = fields
|
||||||
|
.into_iter()
|
||||||
|
.map(|field| storage::CredentialField {
|
||||||
|
index: field.index,
|
||||||
|
hints: field.hints,
|
||||||
|
html_attributes: field
|
||||||
|
.html_attributes
|
||||||
|
.into_iter()
|
||||||
|
.map(|attribute| (attribute.name, attribute.value))
|
||||||
|
.collect(),
|
||||||
|
input_type: field.input_type,
|
||||||
|
focused: field.focused,
|
||||||
|
editable: field.editable,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let form = storage::credential_form(&fields).map_err(rejected)?;
|
||||||
|
Ok(MobileCredentialForm {
|
||||||
|
username: form.username,
|
||||||
|
password: form.password,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
#[uniffi::export]
|
||||||
|
impl MobileAuthentication {
|
||||||
|
pub fn expire_credential_requests(&self) -> Result<(), MobileAuthenticationFfiError> {
|
||||||
|
self.authentication
|
||||||
|
.expire_credential_requests()
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
pub fn begin_credential_request(
|
||||||
|
&self,
|
||||||
|
caller: MobileCredentialCaller,
|
||||||
|
) -> Result<MobileProviderRequest, MobileAuthenticationFfiError> {
|
||||||
|
Ok(self
|
||||||
|
.authentication
|
||||||
|
.begin_credential_request(caller.into())?
|
||||||
|
.into())
|
||||||
|
}
|
||||||
|
pub fn credential_request(
|
||||||
|
&self,
|
||||||
|
token: String,
|
||||||
|
caller: MobileCredentialCaller,
|
||||||
|
query: String,
|
||||||
|
) -> Result<MobileProviderRequest, MobileAuthenticationFfiError> {
|
||||||
|
Ok(self
|
||||||
|
.authentication
|
||||||
|
.search_credential_request(&token, &caller.into(), &query)?
|
||||||
|
.into())
|
||||||
|
}
|
||||||
|
pub fn cancel_credential_request(
|
||||||
|
&self,
|
||||||
|
token: String,
|
||||||
|
) -> Result<(), MobileAuthenticationFfiError> {
|
||||||
|
self.authentication
|
||||||
|
.cancel_credential_request(&token)
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
pub fn stage_credential_save(
|
||||||
|
&self,
|
||||||
|
token: String,
|
||||||
|
caller: MobileCredentialCaller,
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
) -> Result<(), MobileAuthenticationFfiError> {
|
||||||
|
self.authentication
|
||||||
|
.stage_credential_save(
|
||||||
|
&token,
|
||||||
|
&caller.into(),
|
||||||
|
ironstorage::repository::SecretBytes::new(username.into_bytes()),
|
||||||
|
ironstorage::repository::SecretBytes::new(password.into_bytes()),
|
||||||
|
)
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
pub fn save_staged_credential(
|
||||||
|
&self,
|
||||||
|
token: String,
|
||||||
|
caller: MobileCredentialCaller,
|
||||||
|
path: String,
|
||||||
|
consent: bool,
|
||||||
|
overwrite: bool,
|
||||||
|
passphrase: Option<String>,
|
||||||
|
) -> Result<(), MobileAuthenticationFfiError> {
|
||||||
|
self.authentication
|
||||||
|
.save_staged_credential(
|
||||||
|
&token,
|
||||||
|
&caller.into(),
|
||||||
|
&path,
|
||||||
|
consent,
|
||||||
|
overwrite,
|
||||||
|
passphrase.map(|text| ironstorage::repository::SecretBytes::new(text.into_bytes())),
|
||||||
|
)
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
pub fn fill_credential(
|
||||||
|
&self,
|
||||||
|
token: String,
|
||||||
|
caller: MobileCredentialCaller,
|
||||||
|
path: String,
|
||||||
|
consent: bool,
|
||||||
|
passphrase: Option<String>,
|
||||||
|
allowed_usernames: Vec<String>,
|
||||||
|
) -> Result<MobileCredentialValue, MobileAuthenticationFfiError> {
|
||||||
|
let value = self.authentication.fill_credential(
|
||||||
|
&token,
|
||||||
|
&caller.into(),
|
||||||
|
&path,
|
||||||
|
consent,
|
||||||
|
passphrase.map(|text| ironstorage::repository::SecretBytes::new(text.into_bytes())),
|
||||||
|
&allowed_usernames,
|
||||||
|
)?;
|
||||||
|
Ok(MobileCredentialValue {
|
||||||
|
username: String::from_utf8(value.username.expose().to_vec())
|
||||||
|
.map_err(|_| rejected("Invalid username text."))?,
|
||||||
|
password: String::from_utf8(value.password.expose().to_vec())
|
||||||
|
.map_err(|_| rejected("Invalid password text."))?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[uniffi::export]
|
||||||
|
pub fn mobile_credential_privileged_callers() -> String {
|
||||||
|
storage::privileged_callers_json()
|
||||||
|
}
|
||||||
@@ -3021,3 +3021,6 @@ impl MobileAuthentication {
|
|||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod credentials;
|
||||||
|
pub use credentials::*;
|
||||||
@@ -32,6 +32,8 @@ pub mod mobile;
|
|||||||
#[cfg(feature = "full")]
|
#[cfg(feature = "full")]
|
||||||
pub mod mobile_authentication;
|
pub mod mobile_authentication;
|
||||||
#[cfg(feature = "full")]
|
#[cfg(feature = "full")]
|
||||||
|
pub mod mobile_credentials;
|
||||||
|
#[cfg(feature = "full")]
|
||||||
pub mod mobile_entry;
|
pub mod mobile_entry;
|
||||||
#[cfg(feature = "full")]
|
#[cfg(feature = "full")]
|
||||||
pub mod mobile_home;
|
pub mod mobile_home;
|
||||||
|
|||||||
@@ -387,6 +387,8 @@ pub struct MobileAuthentication {
|
|||||||
backend: Arc<dyn SecretStoreBackend>,
|
backend: Arc<dyn SecretStoreBackend>,
|
||||||
status: Mutex<MobileAuthenticationStatus>,
|
status: Mutex<MobileAuthenticationStatus>,
|
||||||
totp_discovery: Mutex<()>,
|
totp_discovery: Mutex<()>,
|
||||||
|
credential_requests: Mutex<crate::mobile_credentials::CredentialRequests>,
|
||||||
|
credential_clock: Arc<dyn AuthenticationClock>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MobileAuthentication {
|
impl MobileAuthentication {
|
||||||
@@ -444,7 +446,7 @@ impl MobileAuthentication {
|
|||||||
Arc::clone(&backend),
|
Arc::clone(&backend),
|
||||||
SecretProtectionPolicy::current_biometry_for_openpgp(),
|
SecretProtectionPolicy::current_biometry_for_openpgp(),
|
||||||
config.authentication_timeout(),
|
config.authentication_timeout(),
|
||||||
clock,
|
Arc::clone(&clock),
|
||||||
);
|
);
|
||||||
let watch_snapshot =
|
let watch_snapshot =
|
||||||
WatchSnapshotSender::load(config.source().with_file_name("watch-snapshot.toml"));
|
WatchSnapshotSender::load(config.source().with_file_name("watch-snapshot.toml"));
|
||||||
@@ -469,9 +471,344 @@ impl MobileAuthentication {
|
|||||||
session,
|
session,
|
||||||
backend,
|
backend,
|
||||||
totp_discovery: Mutex::new(()),
|
totp_discovery: Mutex::new(()),
|
||||||
|
credential_requests: Mutex::default(),
|
||||||
|
credential_clock: clock,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn expire_credential_requests(&self) -> Result<(), MobileAuthenticationError> {
|
||||||
|
self.credential_requests
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| locked_error())?
|
||||||
|
.expire(self.credential_clock.now());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn begin_credential_request(
|
||||||
|
&self,
|
||||||
|
caller: crate::mobile_credentials::CredentialCaller,
|
||||||
|
) -> Result<crate::mobile_credentials::CredentialRequest, MobileAuthenticationError> {
|
||||||
|
let revision = self.repository.mutation_revision().map_err(entry_error)?;
|
||||||
|
let paths = self
|
||||||
|
.repository
|
||||||
|
.snapshot()
|
||||||
|
.map_err(entry_error)?
|
||||||
|
.entries()
|
||||||
|
.map(|entry| entry.path().to_string())
|
||||||
|
.collect();
|
||||||
|
self.credential_requests
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| locked_error())?
|
||||||
|
.begin(&caller, revision, paths, self.credential_clock.now())
|
||||||
|
.map_err(credential_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn credential_request(
|
||||||
|
&self,
|
||||||
|
token: &str,
|
||||||
|
caller: &crate::mobile_credentials::CredentialCaller,
|
||||||
|
) -> Result<crate::mobile_credentials::CredentialRequest, MobileAuthenticationError> {
|
||||||
|
let revision = self.repository.mutation_revision().map_err(entry_error)?;
|
||||||
|
self.credential_requests
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| locked_error())?
|
||||||
|
.validate(token, caller, &revision, self.credential_clock.now())
|
||||||
|
.map_err(credential_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn search_credential_request(
|
||||||
|
&self,
|
||||||
|
token: &str,
|
||||||
|
caller: &crate::mobile_credentials::CredentialCaller,
|
||||||
|
query: &str,
|
||||||
|
) -> Result<crate::mobile_credentials::CredentialRequest, MobileAuthenticationError> {
|
||||||
|
if query.len() > 256 || query.chars().any(char::is_control) {
|
||||||
|
return Err(credential_error("The search text is invalid."));
|
||||||
|
}
|
||||||
|
let mut request = self.credential_request(token, caller)?;
|
||||||
|
let query = query.trim().to_lowercase();
|
||||||
|
request
|
||||||
|
.candidates
|
||||||
|
.retain(|candidate| candidate.path.to_lowercase().contains(&query));
|
||||||
|
request.candidates.truncate(100);
|
||||||
|
Ok(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cancel_credential_request(&self, token: &str) -> Result<(), MobileAuthenticationError> {
|
||||||
|
self.credential_requests
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| locked_error())?
|
||||||
|
.cancel(token);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fill_credential(
|
||||||
|
&self,
|
||||||
|
token: &str,
|
||||||
|
caller: &crate::mobile_credentials::CredentialCaller,
|
||||||
|
path: &str,
|
||||||
|
consent: bool,
|
||||||
|
passphrase: Option<SecretBytes>,
|
||||||
|
allowed_usernames: &[String],
|
||||||
|
) -> Result<crate::mobile_credentials::CredentialValue, MobileAuthenticationError> {
|
||||||
|
let request = self.credential_request(token, caller)?;
|
||||||
|
if !consent
|
||||||
|
|| !request
|
||||||
|
.candidates
|
||||||
|
.iter()
|
||||||
|
.any(|candidate| candidate.path == path)
|
||||||
|
{
|
||||||
|
return Err(credential_error(
|
||||||
|
"Select an entry and explicitly allow this caller to receive its username and password.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
self.ensure_repository_idle()?;
|
||||||
|
self.unlock_entry(path, passphrase)?;
|
||||||
|
let document = self.open_active_document(path)?;
|
||||||
|
let value = crate::mobile_credentials::credential_value(&document, allowed_usernames)
|
||||||
|
.map_err(credential_error)?;
|
||||||
|
let revision = self.repository.mutation_revision().map_err(entry_error)?;
|
||||||
|
let mut requests = self
|
||||||
|
.credential_requests
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| locked_error())?;
|
||||||
|
requests
|
||||||
|
.validate(token, caller, &revision, self.credential_clock.now())
|
||||||
|
.map_err(credential_error)?;
|
||||||
|
self.ensure_active()?;
|
||||||
|
requests.cancel(token);
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stage_credential_save(
|
||||||
|
&self,
|
||||||
|
token: &str,
|
||||||
|
caller: &crate::mobile_credentials::CredentialCaller,
|
||||||
|
username: SecretBytes,
|
||||||
|
password: SecretBytes,
|
||||||
|
) -> Result<(), MobileAuthenticationError> {
|
||||||
|
self.credential_request(token, caller)?;
|
||||||
|
for value in [&username, &password] {
|
||||||
|
let text = std::str::from_utf8(value.expose())
|
||||||
|
.map_err(|_| credential_error("Invalid credential text."))?;
|
||||||
|
if text.is_empty() || text.len() > 16_384 || text.chars().any(char::is_control) {
|
||||||
|
return Err(credential_error("Invalid credential field."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut body = Vec::with_capacity(4 + username.expose().len() + password.expose().len());
|
||||||
|
body.extend_from_slice(&(username.expose().len() as u32).to_be_bytes());
|
||||||
|
body.extend_from_slice(username.expose());
|
||||||
|
body.extend_from_slice(password.expose());
|
||||||
|
// The chosen destination may use a different private key than the default.
|
||||||
|
let recipients = self
|
||||||
|
.keys
|
||||||
|
.infos()
|
||||||
|
.filter(|key| key.has_secret() && key.can_encrypt())
|
||||||
|
.map(|key| self.keys.resolve(key.fingerprint().as_str()))
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.map_err(key_error)?;
|
||||||
|
let sealed = self
|
||||||
|
.keys
|
||||||
|
.encrypt(SecretBytes::new(body), &recipients)
|
||||||
|
.map_err(key_error)?;
|
||||||
|
self.credential_requests
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| locked_error())?
|
||||||
|
.stage(token, sealed, self.credential_clock.now())
|
||||||
|
.map_err(credential_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_staged_credential(
|
||||||
|
&self,
|
||||||
|
token: &str,
|
||||||
|
caller: &crate::mobile_credentials::CredentialCaller,
|
||||||
|
path: &str,
|
||||||
|
consent: bool,
|
||||||
|
overwrite: bool,
|
||||||
|
passphrase: Option<SecretBytes>,
|
||||||
|
) -> Result<(), MobileAuthenticationError> {
|
||||||
|
if !consent {
|
||||||
|
return Err(credential_error(
|
||||||
|
"Confirm the destination before unlocking this save request.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
self.credential_request(token, caller)?;
|
||||||
|
let sealed = self
|
||||||
|
.credential_requests
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| locked_error())?
|
||||||
|
.staged(token, self.credential_clock.now())
|
||||||
|
.map_err(credential_error)?;
|
||||||
|
let target = EntryPath::parse(path).map_err(entry_error)?;
|
||||||
|
let recipients = RecipientPolicyManager::new(&self.repository, &self.keys)
|
||||||
|
.resolve_for_entry(&target, None)
|
||||||
|
.map_err(|error| entry_detail("Recipients Are Unavailable", error))?;
|
||||||
|
let probe = self
|
||||||
|
.keys
|
||||||
|
.encrypt(
|
||||||
|
SecretBytes::new(b"IronStorage credential destination authentication".to_vec()),
|
||||||
|
recipients.recipients(),
|
||||||
|
)
|
||||||
|
.map_err(key_error)?;
|
||||||
|
self.unlock_ciphertext(&probe, passphrase)?;
|
||||||
|
let (handle, key) = {
|
||||||
|
let status = self.status()?;
|
||||||
|
let active = status.active.as_ref().ok_or_else(locked_error)?;
|
||||||
|
(active.handle.clone(), active.key.clone())
|
||||||
|
};
|
||||||
|
let mut provider = KeyOnlyProvider::new(handle, &key);
|
||||||
|
let body = self
|
||||||
|
.keys
|
||||||
|
.decrypt(&sealed, &mut provider)
|
||||||
|
.map_err(key_error)?;
|
||||||
|
let header: [u8; 4] = body
|
||||||
|
.expose()
|
||||||
|
.get(..4)
|
||||||
|
.ok_or_else(|| credential_error("Invalid staged credential."))?
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| credential_error("Invalid staged credential."))?;
|
||||||
|
let length = u32::from_be_bytes(header) as usize;
|
||||||
|
let username = SecretBytes::new(
|
||||||
|
body.expose()
|
||||||
|
.get(4..4 + length)
|
||||||
|
.ok_or_else(|| credential_error("Invalid staged credential."))?
|
||||||
|
.to_vec(),
|
||||||
|
);
|
||||||
|
let password = SecretBytes::new(
|
||||||
|
body.expose()
|
||||||
|
.get(4 + length..)
|
||||||
|
.ok_or_else(|| credential_error("Invalid staged credential."))?
|
||||||
|
.to_vec(),
|
||||||
|
);
|
||||||
|
self.save_credential(
|
||||||
|
token,
|
||||||
|
caller,
|
||||||
|
crate::mobile_credentials::CredentialSaveDestination {
|
||||||
|
path,
|
||||||
|
consent,
|
||||||
|
overwrite,
|
||||||
|
},
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_credential(
|
||||||
|
&self,
|
||||||
|
token: &str,
|
||||||
|
caller: &crate::mobile_credentials::CredentialCaller,
|
||||||
|
destination: crate::mobile_credentials::CredentialSaveDestination<'_>,
|
||||||
|
username: SecretBytes,
|
||||||
|
password: SecretBytes,
|
||||||
|
passphrase: Option<SecretBytes>,
|
||||||
|
) -> Result<(), MobileAuthenticationError> {
|
||||||
|
let crate::mobile_credentials::CredentialSaveDestination {
|
||||||
|
path,
|
||||||
|
consent,
|
||||||
|
overwrite,
|
||||||
|
} = destination;
|
||||||
|
self.credential_request(token, caller)?;
|
||||||
|
if !consent {
|
||||||
|
return Err(credential_error(
|
||||||
|
"Confirm the destination before saving this credential.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let target = EntryPath::parse(path).map_err(entry_error)?;
|
||||||
|
self.ensure_repository_idle()?;
|
||||||
|
{
|
||||||
|
let status = self.status()?;
|
||||||
|
if status
|
||||||
|
.editors
|
||||||
|
.values()
|
||||||
|
.any(|draft| draft.document().path() == &target)
|
||||||
|
|| status
|
||||||
|
.editor_checkpoints
|
||||||
|
.values()
|
||||||
|
.any(|checkpoint| checkpoint.document.path() == &target)
|
||||||
|
{
|
||||||
|
return Err(credential_error(
|
||||||
|
"Close or discard this entry's existing editor before updating it.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let exists = VaultWriter::new(&self.repository, &self.keys)
|
||||||
|
.entry_exists(path)
|
||||||
|
.map_err(|error| entry_detail("Destination Is Unavailable", error))?;
|
||||||
|
if exists && !overwrite {
|
||||||
|
return Err(credential_error(
|
||||||
|
"This destination exists. Explicitly confirm updating it.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let editor = if exists {
|
||||||
|
self.unlock_entry(path, passphrase)?;
|
||||||
|
self.begin_entry_editor(path)?
|
||||||
|
} else {
|
||||||
|
let name = target
|
||||||
|
.as_path()
|
||||||
|
.file_name()
|
||||||
|
.and_then(|name| name.to_str())
|
||||||
|
.ok_or_else(|| credential_error("Invalid destination."))?;
|
||||||
|
self.begin_create_entry(
|
||||||
|
&target.parent_directory().as_path().to_string_lossy(),
|
||||||
|
name,
|
||||||
|
passphrase,
|
||||||
|
)?
|
||||||
|
};
|
||||||
|
let editor_id = editor.id();
|
||||||
|
let result = (|| {
|
||||||
|
let mut document = {
|
||||||
|
let mut status = self.status()?;
|
||||||
|
status
|
||||||
|
.editors
|
||||||
|
.remove(&editor_id)
|
||||||
|
.ok_or_else(editor_missing)?
|
||||||
|
.into_document()
|
||||||
|
};
|
||||||
|
let original_plaintext = document.serialize();
|
||||||
|
crate::mobile_credentials::set_credential(&mut document, username, password)
|
||||||
|
.map_err(credential_error)?;
|
||||||
|
self.credential_request(token, caller)?;
|
||||||
|
self.ensure_active()?;
|
||||||
|
if exists && document.serialize().expose() == original_plaintext.expose() {
|
||||||
|
let current = self.repository.read_entry(&target).map_err(entry_error)?;
|
||||||
|
if Some(¤t) != document.original_ciphertext() {
|
||||||
|
return Err(credential_error(
|
||||||
|
"The destination changed during this save.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
self.credential_request(token, caller)?;
|
||||||
|
self.ensure_active()?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
self.reserve_entry_mutation()?;
|
||||||
|
let result = (|| {
|
||||||
|
let mut commit = AutomaticEntryCommitter::for_entry(
|
||||||
|
&self.repository,
|
||||||
|
path,
|
||||||
|
self.git_identity()?,
|
||||||
|
)
|
||||||
|
.map_err(|error| entry_detail("Credential Could Not Be Saved", error))?;
|
||||||
|
let mut guarded = CredentialCommitter {
|
||||||
|
authentication: self,
|
||||||
|
token,
|
||||||
|
commit: &mut commit,
|
||||||
|
};
|
||||||
|
EntryDocumentService::new(&self.repository, &self.keys)
|
||||||
|
.save_recoverable(&document, None, &mut guarded)
|
||||||
|
.map_err(document_error)?;
|
||||||
|
self.reconcile_saved_document(&document);
|
||||||
|
Ok(())
|
||||||
|
})();
|
||||||
|
self.release_entry_mutation();
|
||||||
|
result
|
||||||
|
})();
|
||||||
|
let _ = self.discard_entry_editor(editor_id);
|
||||||
|
if result.is_ok() {
|
||||||
|
self.cancel_credential_request(token)?;
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
/// Validate an entry unlock and retain only the key passphrase for the shared lease.
|
/// Validate an entry unlock and retain only the key passphrase for the shared lease.
|
||||||
pub fn unlock_entry(
|
pub fn unlock_entry(
|
||||||
&self,
|
&self,
|
||||||
@@ -2202,6 +2539,10 @@ impl MobileAuthentication {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn manual_lock(&self) -> Result<(), MobileAuthenticationError> {
|
pub fn manual_lock(&self) -> Result<(), MobileAuthenticationError> {
|
||||||
|
self.credential_requests
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| locked_error())?
|
||||||
|
.clear();
|
||||||
self.session
|
self.session
|
||||||
.manual_lock()
|
.manual_lock()
|
||||||
.map_err(MobileAuthenticationError::authentication)?;
|
.map_err(MobileAuthenticationError::authentication)?;
|
||||||
@@ -2236,6 +2577,10 @@ impl MobileAuthentication {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn cancel(&self) -> Result<(), MobileAuthenticationError> {
|
pub fn cancel(&self) -> Result<(), MobileAuthenticationError> {
|
||||||
|
self.credential_requests
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| locked_error())?
|
||||||
|
.clear();
|
||||||
self.session
|
self.session
|
||||||
.cancel()
|
.cancel()
|
||||||
.map_err(MobileAuthenticationError::authentication)?;
|
.map_err(MobileAuthenticationError::authentication)?;
|
||||||
@@ -2792,6 +3137,41 @@ fn entry_detail(title: &str, error: impl fmt::Display) -> MobileAuthenticationEr
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn credential_error(detail: &str) -> MobileAuthenticationError {
|
||||||
|
MobileAuthenticationError::new(
|
||||||
|
MobileAuthenticationErrorKind::Entry,
|
||||||
|
"Credential Request Rejected",
|
||||||
|
detail,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CredentialCommitter<'a> {
|
||||||
|
authentication: &'a MobileAuthentication,
|
||||||
|
token: &'a str,
|
||||||
|
commit: &'a mut AutomaticEntryCommitter,
|
||||||
|
}
|
||||||
|
impl crate::write::EntryCommitter for CredentialCommitter<'_> {
|
||||||
|
fn commit(
|
||||||
|
&mut self,
|
||||||
|
change: &crate::write::EntryCommit,
|
||||||
|
) -> Result<(), crate::write::EntryCommitError> {
|
||||||
|
self.authentication.ensure_active().map_err(|_| {
|
||||||
|
crate::write::EntryCommitError::new("Credential authentication expired before saving.")
|
||||||
|
})?;
|
||||||
|
let requests = self
|
||||||
|
.authentication
|
||||||
|
.credential_requests
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| crate::write::EntryCommitError::new("Credential request cancelled."))?;
|
||||||
|
if !requests.active(self.token, self.authentication.credential_clock.now()) {
|
||||||
|
return Err(crate::write::EntryCommitError::new(
|
||||||
|
"Credential request cancelled or expired.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
self.commit.commit(change)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
@@ -2844,6 +3224,234 @@ mod tests {
|
|||||||
(root, authentication, clock)
|
(root, authentication, clock)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn credential_selection_authentication_cancellation_revision_and_timeout() {
|
||||||
|
use crate::{mobile_credentials::CredentialCaller, repository::SecretBytes};
|
||||||
|
let (_root, auth, clock) =
|
||||||
|
otp_fixture("username: alice\nurl: https://example.com/\nnotes: keep me");
|
||||||
|
let caller = CredentialCaller {
|
||||||
|
package: "example.client".into(),
|
||||||
|
certificates: vec!["AA".repeat(32)],
|
||||||
|
origin: Some("https://spoofed.example/".into()),
|
||||||
|
};
|
||||||
|
let request = auth.begin_credential_request(caller.clone()).unwrap();
|
||||||
|
assert_eq!(request.identity.title, "example.client");
|
||||||
|
assert!(
|
||||||
|
request
|
||||||
|
.candidates
|
||||||
|
.iter()
|
||||||
|
.any(|candidate| candidate.path == "otp")
|
||||||
|
);
|
||||||
|
assert!(!auth.state().unwrap().unlocked());
|
||||||
|
assert!(
|
||||||
|
auth.fill_credential(&request.token, &caller, "otp", false, None, &[])
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
auth.fill_credential(&request.token, &caller, "otp", true, None, &[])
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
let value = auth
|
||||||
|
.fill_credential(
|
||||||
|
&request.token,
|
||||||
|
&caller,
|
||||||
|
"otp",
|
||||||
|
true,
|
||||||
|
Some(SecretBytes::new(b"fixture-alice-passphrase".to_vec())),
|
||||||
|
&[],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(value.username.expose(), b"alice");
|
||||||
|
assert_eq!(value.password.expose(), b"fixture-password");
|
||||||
|
assert!(
|
||||||
|
auth.fill_credential(&request.token, &caller, "otp", true, None, &[])
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
let request = auth.begin_credential_request(caller.clone()).unwrap();
|
||||||
|
assert!(
|
||||||
|
auth.fill_credential(&request.token, &caller, "otp", true, None, &["bob".into()])
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
auth.manual_lock().unwrap();
|
||||||
|
assert!(auth.credential_request(&request.token, &caller).is_err());
|
||||||
|
let request = auth.begin_credential_request(caller.clone()).unwrap();
|
||||||
|
clock.0.store(120, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
assert!(auth.credential_request(&request.token, &caller).is_err());
|
||||||
|
auth.expire_credential_requests().unwrap();
|
||||||
|
assert!(auth.credential_requests.lock().unwrap().pending_is_empty());
|
||||||
|
clock.0.store(121, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
let request = auth.begin_credential_request(caller.clone()).unwrap();
|
||||||
|
auth.unlock_entry(
|
||||||
|
"otp",
|
||||||
|
Some(SecretBytes::new(b"fixture-alice-passphrase".to_vec())),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let page = auth.entry_page("otp", 0).unwrap();
|
||||||
|
let field = page.page().sections()[0].fields()[0].id();
|
||||||
|
auth.replace_entry_field("otp", field, "changed".into())
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
auth.fill_credential(&request.token, &caller, "otp", true, None, &[])
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn provider_save_authenticates_the_destination_key_instead_of_the_default_key() {
|
||||||
|
use crate::{
|
||||||
|
crypto::KeyStore,
|
||||||
|
git::{GitIdentity, GitRepository},
|
||||||
|
mobile_credentials::CredentialCaller,
|
||||||
|
repository::{Repository, SecretBytes},
|
||||||
|
};
|
||||||
|
let (_root, private, backend) = crate::mobile_onboarding::android::tests::home_fixture();
|
||||||
|
let config = private.configuration().unwrap().unwrap();
|
||||||
|
std::fs::write(
|
||||||
|
config.key_material().join("bob.asc"),
|
||||||
|
include_bytes!("../tests/fixtures/compatibility/keys/bob-secret.asc"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let keys = KeyStore::load(config.key_material()).unwrap();
|
||||||
|
let bob = keys
|
||||||
|
.infos()
|
||||||
|
.find(|key| key.user_ids().iter().any(|id| id.contains("Bob")))
|
||||||
|
.unwrap();
|
||||||
|
let repository = Repository::open(config.vault()).unwrap();
|
||||||
|
std::fs::create_dir(config.vault().join("bob")).unwrap();
|
||||||
|
std::fs::write(
|
||||||
|
config.vault().join("bob/.gpg-id"),
|
||||||
|
format!("{}\n", bob.fingerprint()),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
GitRepository::open(&repository, GitIdentity::ironstorage())
|
||||||
|
.unwrap()
|
||||||
|
.commit_all("Add Bob recipient fixture")
|
||||||
|
.unwrap();
|
||||||
|
let auth = private
|
||||||
|
.authentication(
|
||||||
|
backend,
|
||||||
|
std::sync::Arc::new(OtpClock(std::sync::atomic::AtomicU64::new(0))),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let caller = CredentialCaller {
|
||||||
|
package: "example.client".into(),
|
||||||
|
certificates: vec!["AA".repeat(32)],
|
||||||
|
origin: None,
|
||||||
|
};
|
||||||
|
let request = auth.begin_credential_request(caller.clone()).unwrap();
|
||||||
|
auth.stage_credential_save(
|
||||||
|
&request.token,
|
||||||
|
&caller,
|
||||||
|
SecretBytes::new(b"bob".to_vec()),
|
||||||
|
SecretBytes::new(b"destination-password".to_vec()),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
auth.save_staged_credential(
|
||||||
|
&request.token,
|
||||||
|
&caller,
|
||||||
|
"bob/credential",
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
Some(SecretBytes::new(b"fixture-bob-passphrase".to_vec())),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let document = auth.open_active_document("bob/credential").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
crate::mobile_credentials::credential_value(&document, &[])
|
||||||
|
.unwrap()
|
||||||
|
.password
|
||||||
|
.expose(),
|
||||||
|
b"destination-password"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn provider_saves_require_destination_consent_and_preserve_unrelated_fields() {
|
||||||
|
use crate::{mobile_credentials::CredentialCaller, repository::SecretBytes};
|
||||||
|
let (_root, auth, _clock) = otp_fixture("username: alice\nnotes: preserve this");
|
||||||
|
let caller = CredentialCaller {
|
||||||
|
package: "example.client".into(),
|
||||||
|
certificates: vec!["AA".repeat(32)],
|
||||||
|
origin: None,
|
||||||
|
};
|
||||||
|
let request = auth.begin_credential_request(caller.clone()).unwrap();
|
||||||
|
auth.stage_credential_save(
|
||||||
|
&request.token,
|
||||||
|
&caller,
|
||||||
|
SecretBytes::new(b"bob".to_vec()),
|
||||||
|
SecretBytes::new(b"new-password".to_vec()),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(!auth.state().unwrap().unlocked());
|
||||||
|
assert!(
|
||||||
|
auth.save_staged_credential(
|
||||||
|
&request.token,
|
||||||
|
&caller,
|
||||||
|
"otp",
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
Some(SecretBytes::new(b"fixture-alice-passphrase".to_vec()))
|
||||||
|
)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
assert!(!auth.state().unwrap().unlocked());
|
||||||
|
assert!(
|
||||||
|
auth.save_staged_credential(
|
||||||
|
&request.token,
|
||||||
|
&caller,
|
||||||
|
"otp",
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
Some(SecretBytes::new(b"fixture-alice-passphrase".to_vec()))
|
||||||
|
)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
auth.save_staged_credential(&request.token, &caller, "otp", true, true, None)
|
||||||
|
.unwrap();
|
||||||
|
let document = auth.open_active_document("otp").unwrap();
|
||||||
|
let value = crate::mobile_credentials::credential_value(&document, &[]).unwrap();
|
||||||
|
assert_eq!(value.username.expose(), b"bob");
|
||||||
|
assert_eq!(value.password.expose(), b"new-password");
|
||||||
|
assert!(
|
||||||
|
document
|
||||||
|
.fields()
|
||||||
|
.iter()
|
||||||
|
.any(|field| field.value() == b"preserve this")
|
||||||
|
);
|
||||||
|
let target = crate::repository::EntryPath::parse("otp").unwrap();
|
||||||
|
let original = auth.repository.read_entry(&target).unwrap();
|
||||||
|
let repeated = auth.begin_credential_request(caller.clone()).unwrap();
|
||||||
|
auth.stage_credential_save(
|
||||||
|
&repeated.token,
|
||||||
|
&caller,
|
||||||
|
SecretBytes::new(b"bob".to_vec()),
|
||||||
|
SecretBytes::new(b"new-password".to_vec()),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
auth.save_staged_credential(&repeated.token, &caller, "otp", true, true, None)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(auth.repository.read_entry(&target).unwrap(), original);
|
||||||
|
assert!(auth.credential_request(&repeated.token, &caller).is_err());
|
||||||
|
let request = auth.begin_credential_request(caller.clone()).unwrap();
|
||||||
|
auth.stage_credential_save(
|
||||||
|
&request.token,
|
||||||
|
&caller,
|
||||||
|
SecretBytes::new(b"carol".to_vec()),
|
||||||
|
SecretBytes::new(b"created-password".to_vec()),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
auth.save_staged_credential(&request.token, &caller, "created", true, false, None)
|
||||||
|
.unwrap();
|
||||||
|
let document = auth.open_active_document("created").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
crate::mobile_credentials::credential_value(&document, &[])
|
||||||
|
.unwrap()
|
||||||
|
.username
|
||||||
|
.expose(),
|
||||||
|
b"carol"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn otp_presentation_matches_rfc_vectors_and_boundaries_without_renewing_the_lease() {
|
fn otp_presentation_matches_rfc_vectors_and_boundaries_without_renewing_the_lease() {
|
||||||
use crate::{otp::OtpCodeValidity, repository::SecretBytes};
|
use crate::{otp::OtpCodeValidity, repository::SecretBytes};
|
||||||
|
|||||||
@@ -0,0 +1,584 @@
|
|||||||
|
//! Request-bound credentials shared by Android Credential Manager and Autofill.
|
||||||
|
//! Callers supply OS facts; trust, matching, selection and entry parsing live here.
|
||||||
|
|
||||||
|
use std::{collections::BTreeMap, time::Duration};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
document::{EntryDocument, EntryFieldKind},
|
||||||
|
repository::SecretBytes,
|
||||||
|
};
|
||||||
|
use rand::RngCore;
|
||||||
|
|
||||||
|
const REQUEST_LIFETIME: Duration = Duration::from_secs(120);
|
||||||
|
const CHROME_RELEASE: &str = "F0FD6C5B410F25CB25C3B53346C8972FAE30F8EE7411DF910480AD6B2D60DB83";
|
||||||
|
const FIREFOX_RELEASE: &str = "A78B62A5165B4494B2FEAD9E76A280D22D937FEE6251AECE599446B2EA319B04";
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct CredentialCaller {
|
||||||
|
pub package: String,
|
||||||
|
/// Current signers of the installed package, obtained from PackageManager.
|
||||||
|
pub certificates: Vec<String>,
|
||||||
|
pub origin: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct CredentialIdentity {
|
||||||
|
pub title: String,
|
||||||
|
pub binding: String,
|
||||||
|
pub match_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn caller_identity(caller: &CredentialCaller) -> Result<CredentialIdentity, &'static str> {
|
||||||
|
let package = &caller.package;
|
||||||
|
if package.len() > 255
|
||||||
|
|| !package.contains('.')
|
||||||
|
|| package.split('.').any(|part| {
|
||||||
|
part.is_empty()
|
||||||
|
|| !part
|
||||||
|
.bytes()
|
||||||
|
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
|
||||||
|
})
|
||||||
|
{
|
||||||
|
return Err("The requesting application is invalid.");
|
||||||
|
}
|
||||||
|
if caller.certificates.is_empty() || caller.certificates.len() > 8 {
|
||||||
|
return Err("The requesting application's signature is unavailable.");
|
||||||
|
}
|
||||||
|
let mut certificates = Vec::new();
|
||||||
|
for certificate in &caller.certificates {
|
||||||
|
let fingerprint = certificate.replace(':', "").to_ascii_uppercase();
|
||||||
|
if fingerprint.len() != 64 || !fingerprint.bytes().all(|byte| byte.is_ascii_hexdigit()) {
|
||||||
|
return Err("The requesting application's signature is invalid.");
|
||||||
|
}
|
||||||
|
certificates.push(fingerprint);
|
||||||
|
}
|
||||||
|
certificates.sort();
|
||||||
|
certificates.dedup();
|
||||||
|
let privileged = match package.as_str() {
|
||||||
|
"com.android.chrome" => certificates.as_slice() == [CHROME_RELEASE],
|
||||||
|
"org.mozilla.firefox" => certificates.as_slice() == [FIREFOX_RELEASE],
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
let app_binding = format!("app:{package}:{}", certificates.join(","));
|
||||||
|
if let Some(origin) = &caller.origin {
|
||||||
|
if origin.len() > 2048 {
|
||||||
|
return Err("The website identity is invalid.");
|
||||||
|
}
|
||||||
|
// Untrusted clients cannot turn an asserted webDomain into a website permission.
|
||||||
|
if privileged {
|
||||||
|
let url = url::Url::parse(origin).map_err(|_| "The website identity is invalid.")?;
|
||||||
|
if url.scheme() != "https"
|
||||||
|
|| !url.username().is_empty()
|
||||||
|
|| url.password().is_some()
|
||||||
|
|| url.host_str().is_none()
|
||||||
|
|| url.query().is_some()
|
||||||
|
|| url.fragment().is_some()
|
||||||
|
|| url.path() != "/"
|
||||||
|
{
|
||||||
|
return Err("Only a verified HTTPS website origin is supported.");
|
||||||
|
}
|
||||||
|
let canonical = url.origin().ascii_serialization();
|
||||||
|
return Ok(CredentialIdentity {
|
||||||
|
title: canonical.clone(),
|
||||||
|
binding: format!("web:{canonical}:{app_binding}"),
|
||||||
|
match_name: url.host_str().unwrap_or_default().to_owned(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(CredentialIdentity {
|
||||||
|
title: package.clone(),
|
||||||
|
binding: app_binding,
|
||||||
|
match_name: package.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reviewed release signing identities, consumed by AndroidX's trusted-origin verifier.
|
||||||
|
pub fn privileged_callers_json() -> String {
|
||||||
|
format!(
|
||||||
|
r#"{{"apps":[{{"type":"android","info":{{"package_name":"com.android.chrome","signatures":[{{"build":"release","cert_fingerprint_sha256":"{}"}}]}}}},{{"type":"android","info":{{"package_name":"org.mozilla.firefox","signatures":[{{"build":"release","cert_fingerprint_sha256":"{}"}}]}}}}]}}"#,
|
||||||
|
fingerprint_colons(CHROME_RELEASE),
|
||||||
|
fingerprint_colons(FIREFOX_RELEASE)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fn fingerprint_colons(value: &str) -> String {
|
||||||
|
value
|
||||||
|
.as_bytes()
|
||||||
|
.chunks(2)
|
||||||
|
.map(|chunk| std::str::from_utf8(chunk).unwrap_or_default())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(":")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Raw AssistStructure facts, with the OS AutofillId retained only in Kotlin.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct CredentialField {
|
||||||
|
pub index: u32,
|
||||||
|
pub hints: Vec<String>,
|
||||||
|
pub html_attributes: Vec<(String, String)>,
|
||||||
|
pub input_type: u32,
|
||||||
|
pub focused: bool,
|
||||||
|
pub editable: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct CredentialForm {
|
||||||
|
pub username: u32,
|
||||||
|
pub password: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn credential_form(fields: &[CredentialField]) -> Result<CredentialForm, &'static str> {
|
||||||
|
if fields.len() > 512 {
|
||||||
|
return Err("The client form is too large.");
|
||||||
|
}
|
||||||
|
let mut users = Vec::new();
|
||||||
|
let mut passwords = Vec::new();
|
||||||
|
let mut indexes = std::collections::BTreeSet::new();
|
||||||
|
for field in fields {
|
||||||
|
if !indexes.insert(field.index)
|
||||||
|
|| field.hints.len() > 16
|
||||||
|
|| field.hints.iter().any(|hint| hint.len() > 128)
|
||||||
|
{
|
||||||
|
return Err("The client form is invalid.");
|
||||||
|
}
|
||||||
|
if !field.editable {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if field.html_attributes.len() > 32
|
||||||
|
|| field
|
||||||
|
.html_attributes
|
||||||
|
.iter()
|
||||||
|
.any(|(key, value)| key.len() > 128 || value.len() > 256)
|
||||||
|
{
|
||||||
|
return Err("The HTML form is invalid.");
|
||||||
|
}
|
||||||
|
let html_password = field.html_attributes.iter().any(|(key, value)| {
|
||||||
|
(key == "type" && value == "password")
|
||||||
|
|| (key == "autocomplete" && value == "current-password")
|
||||||
|
});
|
||||||
|
let html_user = field.html_attributes.iter().any(|(key, value)| {
|
||||||
|
key == "autocomplete" && matches!(value.as_str(), "username" | "email")
|
||||||
|
});
|
||||||
|
let password = html_password
|
||||||
|
|| field.hints.iter().any(|hint| hint == "password")
|
||||||
|
|| matches!(field.input_type & 0xfff, 0x81 | 0x91 | 0xe1 | 0x12);
|
||||||
|
let user = html_user
|
||||||
|
|| field
|
||||||
|
.hints
|
||||||
|
.iter()
|
||||||
|
.any(|hint| matches!(hint.as_str(), "username" | "emailAddress"))
|
||||||
|
|| field.input_type & 0xfff == 0x21;
|
||||||
|
if password && user {
|
||||||
|
return Err("The client form has conflicting field roles.");
|
||||||
|
}
|
||||||
|
if password {
|
||||||
|
passwords.push(field.index);
|
||||||
|
}
|
||||||
|
if user {
|
||||||
|
users.push(field.index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match (users.as_slice(), passwords.as_slice()) {
|
||||||
|
([username], [password]) if username != password => Ok(CredentialForm {
|
||||||
|
username: *username,
|
||||||
|
password: *password,
|
||||||
|
}),
|
||||||
|
_ => Err("Select a form with exactly one username and one password field."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct CredentialCandidate {
|
||||||
|
pub path: String,
|
||||||
|
pub matched: bool,
|
||||||
|
}
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct CredentialRequest {
|
||||||
|
pub token: String,
|
||||||
|
pub identity: CredentialIdentity,
|
||||||
|
pub revision: String,
|
||||||
|
pub candidates: Vec<CredentialCandidate>,
|
||||||
|
}
|
||||||
|
struct PendingCredentialRequest {
|
||||||
|
request: CredentialRequest,
|
||||||
|
deadline: Duration,
|
||||||
|
save: Option<crate::repository::EncryptedEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub(crate) struct CredentialRequests {
|
||||||
|
pending: BTreeMap<String, PendingCredentialRequest>,
|
||||||
|
}
|
||||||
|
impl CredentialRequests {
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn pending_is_empty(&self) -> bool {
|
||||||
|
self.pending.is_empty()
|
||||||
|
}
|
||||||
|
pub fn expire(&mut self, now: Duration) {
|
||||||
|
self.pending.retain(|_, request| now < request.deadline);
|
||||||
|
}
|
||||||
|
pub fn stage(
|
||||||
|
&mut self,
|
||||||
|
token: &str,
|
||||||
|
value: crate::repository::EncryptedEntry,
|
||||||
|
now: Duration,
|
||||||
|
) -> Result<(), &'static str> {
|
||||||
|
let pending = self
|
||||||
|
.pending
|
||||||
|
.get_mut(token)
|
||||||
|
.filter(|pending| now < pending.deadline)
|
||||||
|
.ok_or("The save request expired.")?;
|
||||||
|
pending.save = Some(value);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn staged(
|
||||||
|
&self,
|
||||||
|
token: &str,
|
||||||
|
now: Duration,
|
||||||
|
) -> Result<crate::repository::EncryptedEntry, &'static str> {
|
||||||
|
let value = self
|
||||||
|
.pending
|
||||||
|
.get(token)
|
||||||
|
.filter(|pending| now < pending.deadline)
|
||||||
|
.and_then(|pending| pending.save.as_ref())
|
||||||
|
.ok_or("The save request expired or was cancelled.")?;
|
||||||
|
Ok(value.clone())
|
||||||
|
}
|
||||||
|
pub fn active(&self, token: &str, now: Duration) -> bool {
|
||||||
|
self.pending
|
||||||
|
.get(token)
|
||||||
|
.is_some_and(|request| now < request.deadline)
|
||||||
|
}
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.pending.clear();
|
||||||
|
}
|
||||||
|
pub fn cancel(&mut self, token: &str) {
|
||||||
|
self.pending.remove(token);
|
||||||
|
}
|
||||||
|
pub fn begin(
|
||||||
|
&mut self,
|
||||||
|
caller: &CredentialCaller,
|
||||||
|
revision: String,
|
||||||
|
paths: Vec<String>,
|
||||||
|
now: Duration,
|
||||||
|
) -> Result<CredentialRequest, &'static str> {
|
||||||
|
self.pending.retain(|_, pending| now < pending.deadline);
|
||||||
|
if self.pending.len() >= 16 {
|
||||||
|
return Err("Too many credential requests are pending.");
|
||||||
|
}
|
||||||
|
let identity = caller_identity(caller)?;
|
||||||
|
let deadline = now
|
||||||
|
.checked_add(REQUEST_LIFETIME)
|
||||||
|
.ok_or("The credential request deadline is invalid.")?;
|
||||||
|
let mut random = [0; 32];
|
||||||
|
rand::thread_rng().fill_bytes(&mut random);
|
||||||
|
let token = data_encoding::HEXLOWER.encode(&random);
|
||||||
|
let mut candidates: Vec<_> = paths
|
||||||
|
.into_iter()
|
||||||
|
.map(|path| {
|
||||||
|
let matched = path
|
||||||
|
.split('/')
|
||||||
|
.any(|part| part.eq_ignore_ascii_case(&identity.match_name));
|
||||||
|
CredentialCandidate { path, matched }
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
candidates.sort_by(|a, b| b.matched.cmp(&a.matched).then_with(|| a.path.cmp(&b.path)));
|
||||||
|
let request = CredentialRequest {
|
||||||
|
token: token.clone(),
|
||||||
|
identity,
|
||||||
|
revision,
|
||||||
|
candidates,
|
||||||
|
};
|
||||||
|
self.pending.insert(
|
||||||
|
token,
|
||||||
|
PendingCredentialRequest {
|
||||||
|
request: request.clone(),
|
||||||
|
deadline,
|
||||||
|
save: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Ok(request)
|
||||||
|
}
|
||||||
|
pub fn validate(
|
||||||
|
&self,
|
||||||
|
token: &str,
|
||||||
|
caller: &CredentialCaller,
|
||||||
|
revision: &str,
|
||||||
|
now: Duration,
|
||||||
|
) -> Result<CredentialRequest, &'static str> {
|
||||||
|
let pending = self
|
||||||
|
.pending
|
||||||
|
.get(token)
|
||||||
|
.ok_or("This credential request was cancelled or the process restarted.")?;
|
||||||
|
if now >= pending.deadline {
|
||||||
|
return Err("This credential request expired. Request it again.");
|
||||||
|
}
|
||||||
|
if pending.request.identity != caller_identity(caller)? {
|
||||||
|
return Err("The credential caller changed.");
|
||||||
|
}
|
||||||
|
if pending.request.revision != revision {
|
||||||
|
return Err("The repository changed. Request credentials again.");
|
||||||
|
}
|
||||||
|
Ok(pending.request.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CredentialSaveDestination<'a> {
|
||||||
|
pub path: &'a str,
|
||||||
|
pub consent: bool,
|
||||||
|
pub overwrite: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Secret buffers are erased on drop and never included in Debug output.
|
||||||
|
pub struct CredentialValue {
|
||||||
|
pub username: SecretBytes,
|
||||||
|
pub password: SecretBytes,
|
||||||
|
}
|
||||||
|
pub fn credential_value(
|
||||||
|
document: &EntryDocument,
|
||||||
|
allowed_usernames: &[String],
|
||||||
|
) -> Result<CredentialValue, &'static str> {
|
||||||
|
let fields = document.fields();
|
||||||
|
let passwords: Vec<_> = fields
|
||||||
|
.iter()
|
||||||
|
.filter(|field| field.metadata().kind() == EntryFieldKind::Password)
|
||||||
|
.collect();
|
||||||
|
let usernames: Vec<_> = fields
|
||||||
|
.iter()
|
||||||
|
.filter(|field| {
|
||||||
|
matches!(
|
||||||
|
field.metadata().kind(),
|
||||||
|
EntryFieldKind::Username | EntryFieldKind::Email
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let ([password], [username]) = (passwords.as_slice(), usernames.as_slice()) else {
|
||||||
|
return Err("This entry must contain exactly one username or email and one password.");
|
||||||
|
};
|
||||||
|
for field in [password, username] {
|
||||||
|
let value =
|
||||||
|
std::str::from_utf8(field.value()).map_err(|_| "The credential is not UTF-8 text.")?;
|
||||||
|
if value.is_empty() || value.len() > 16_384 || value.chars().any(char::is_control) {
|
||||||
|
return Err(
|
||||||
|
"The credential field is empty, too large, or contains control characters.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !allowed_usernames.is_empty()
|
||||||
|
&& !allowed_usernames
|
||||||
|
.iter()
|
||||||
|
.any(|name| name.as_bytes() == username.value())
|
||||||
|
{
|
||||||
|
return Err("The client requested a different account.");
|
||||||
|
}
|
||||||
|
Ok(CredentialValue {
|
||||||
|
username: SecretBytes::new(username.value().to_vec()),
|
||||||
|
password: SecretBytes::new(password.value().to_vec()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Preserve all unrelated pass fields when explicitly updating a credential.
|
||||||
|
pub fn set_credential(
|
||||||
|
document: &mut EntryDocument,
|
||||||
|
username: SecretBytes,
|
||||||
|
password: SecretBytes,
|
||||||
|
) -> Result<(), &'static str> {
|
||||||
|
use crate::document::EntryFieldDraft;
|
||||||
|
for value in [&username, &password] {
|
||||||
|
let text =
|
||||||
|
std::str::from_utf8(value.expose()).map_err(|_| "The credential is not UTF-8 text.")?;
|
||||||
|
if text.is_empty() || text.len() > 16_384 || text.chars().any(char::is_control) {
|
||||||
|
return Err(
|
||||||
|
"The credential field is empty, too large, or contains control characters.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let passwords: Vec<_> = document
|
||||||
|
.fields()
|
||||||
|
.iter()
|
||||||
|
.filter(|field| field.metadata().kind() == EntryFieldKind::Password)
|
||||||
|
.map(|field| field.id())
|
||||||
|
.collect();
|
||||||
|
let users: Vec<_> = document
|
||||||
|
.fields()
|
||||||
|
.iter()
|
||||||
|
.filter(|field| {
|
||||||
|
matches!(
|
||||||
|
field.metadata().kind(),
|
||||||
|
EntryFieldKind::Username | EntryFieldKind::Email
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.map(|field| {
|
||||||
|
(
|
||||||
|
field.id(),
|
||||||
|
field.metadata().name().unwrap_or("username").to_owned(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
if passwords.len() != 1 || users.len() > 1 {
|
||||||
|
return Err("This entry has ambiguous credential fields; edit it in IronStorage first.");
|
||||||
|
}
|
||||||
|
document
|
||||||
|
.update(
|
||||||
|
passwords[0],
|
||||||
|
EntryFieldDraft::line(password.expose().to_vec())
|
||||||
|
.map_err(|_| "Invalid password field.")?,
|
||||||
|
)
|
||||||
|
.map_err(|_| "Could not update the password.")?;
|
||||||
|
let (id, name) = users
|
||||||
|
.first()
|
||||||
|
.map(|(id, name)| (Some(*id), name.as_str()))
|
||||||
|
.unwrap_or((None, "username"));
|
||||||
|
let replacement = EntryFieldDraft::field(name, username.expose().to_vec())
|
||||||
|
.map_err(|_| "Invalid username field.")?;
|
||||||
|
if let Some(id) = id {
|
||||||
|
document
|
||||||
|
.update(id, replacement)
|
||||||
|
.map_err(|_| "Could not update the username.")?;
|
||||||
|
} else {
|
||||||
|
document
|
||||||
|
.add(document.fields().len(), replacement)
|
||||||
|
.map_err(|_| "Could not add the username.")?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
fn caller() -> CredentialCaller {
|
||||||
|
CredentialCaller {
|
||||||
|
package: "example.client".into(),
|
||||||
|
certificates: vec!["AA".repeat(32)],
|
||||||
|
origin: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn untrusted_origins_stay_app_scoped_and_signature_changes_fail() {
|
||||||
|
let mut client = caller();
|
||||||
|
client.origin = Some("https://accounts.example/".into());
|
||||||
|
let identity = caller_identity(&client).unwrap();
|
||||||
|
assert_eq!(identity.title, "example.client");
|
||||||
|
let mut requests = CredentialRequests::default();
|
||||||
|
let request = requests
|
||||||
|
.begin(
|
||||||
|
&client,
|
||||||
|
"r1".into(),
|
||||||
|
vec!["example.client/user".into()],
|
||||||
|
Duration::ZERO,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(request.candidates[0].matched);
|
||||||
|
client.certificates = vec!["BB".repeat(32)];
|
||||||
|
assert!(
|
||||||
|
requests
|
||||||
|
.validate(&request.token, &client, "r1", Duration::ZERO)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn pinned_browser_origin_and_hostile_origins() {
|
||||||
|
let mut client = caller();
|
||||||
|
client.package = "com.android.chrome".into();
|
||||||
|
client.certificates = vec![CHROME_RELEASE.into()];
|
||||||
|
client.origin = Some("https://EXAMPLE.com:443/".into());
|
||||||
|
assert_eq!(
|
||||||
|
caller_identity(&client).unwrap().title,
|
||||||
|
"https://example.com"
|
||||||
|
);
|
||||||
|
for origin in [
|
||||||
|
"http://example.com/",
|
||||||
|
"https://user@example.com/",
|
||||||
|
"https://example.com/login",
|
||||||
|
"https://example.com/?spoof=true",
|
||||||
|
] {
|
||||||
|
client.origin = Some(origin.into());
|
||||||
|
assert!(caller_identity(&client).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn cancellation_deadline_revision_and_recreation_reject_requests() {
|
||||||
|
let client = caller();
|
||||||
|
let mut requests = CredentialRequests::default();
|
||||||
|
let request = requests
|
||||||
|
.begin(&client, "r1".into(), vec![], Duration::ZERO)
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
requests
|
||||||
|
.validate(&request.token, &client, "r2", Duration::ZERO)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
requests
|
||||||
|
.validate(&request.token, &client, "r1", REQUEST_LIFETIME)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
requests.cancel(&request.token);
|
||||||
|
assert!(
|
||||||
|
requests
|
||||||
|
.validate(&request.token, &client, "r1", Duration::ZERO)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn html_and_duplicate_os_ids_use_the_same_strict_form_contract() {
|
||||||
|
let user = CredentialField {
|
||||||
|
index: 1,
|
||||||
|
hints: vec![],
|
||||||
|
html_attributes: vec![("autocomplete".into(), "username".into())],
|
||||||
|
input_type: 0,
|
||||||
|
focused: true,
|
||||||
|
editable: true,
|
||||||
|
};
|
||||||
|
let password = CredentialField {
|
||||||
|
index: 2,
|
||||||
|
hints: vec![],
|
||||||
|
html_attributes: vec![("type".into(), "password".into())],
|
||||||
|
input_type: 0,
|
||||||
|
focused: false,
|
||||||
|
editable: true,
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
credential_form(&[user.clone(), password.clone()])
|
||||||
|
.unwrap()
|
||||||
|
.username,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
let mut duplicate = password;
|
||||||
|
duplicate.index = 1;
|
||||||
|
assert!(credential_form(&[user.clone(), duplicate]).is_err());
|
||||||
|
assert!(credential_form(&vec![user; 513]).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ambiguous_and_conflicting_forms_are_rejected() {
|
||||||
|
let user = CredentialField {
|
||||||
|
index: 1,
|
||||||
|
hints: vec!["username".into()],
|
||||||
|
html_attributes: vec![],
|
||||||
|
input_type: 1,
|
||||||
|
focused: true,
|
||||||
|
editable: true,
|
||||||
|
};
|
||||||
|
let password = CredentialField {
|
||||||
|
index: 2,
|
||||||
|
hints: vec![],
|
||||||
|
html_attributes: vec![],
|
||||||
|
input_type: 0x81,
|
||||||
|
focused: false,
|
||||||
|
editable: true,
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
credential_form(&[user.clone(), password.clone()]).unwrap(),
|
||||||
|
CredentialForm {
|
||||||
|
username: 1,
|
||||||
|
password: 2
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let mut duplicate = password.clone();
|
||||||
|
duplicate.index = 3;
|
||||||
|
assert!(credential_form(&[user.clone(), password, duplicate]).is_err());
|
||||||
|
let mut conflict = user;
|
||||||
|
conflict.hints.push("password".into());
|
||||||
|
assert!(credential_form(&[conflict]).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -448,6 +448,10 @@ impl MobileEntryDraft {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn into_document(self) -> EntryDocument {
|
||||||
|
self.document
|
||||||
|
}
|
||||||
|
|
||||||
pub fn document(&self) -> &EntryDocument {
|
pub fn document(&self) -> &EntryDocument {
|
||||||
&self.document
|
&self.document
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
# Android password providers
|
||||||
|
|
||||||
|
IronStorage supplies password credentials through AndroidX Credential Provider on
|
||||||
|
Android 14 and newer and through framework Autofill on Android 8 and newer. Both
|
||||||
|
use the same `NativeCredentialRepository` facade and storage-owned request,
|
||||||
|
matching, search, authentication, credential-field and save APIs. Services and
|
||||||
|
selection run inside the private application UID and share one Rust owner;
|
||||||
|
requesting applications run outside that boundary. There is no second secret
|
||||||
|
cache or service-specific authentication lease.
|
||||||
|
|
||||||
|
Enable IronStorage in its Preferences → Autofill and Credential Manager providers
|
||||||
|
section. The two status rows open Android's provider settings. For current Chrome,
|
||||||
|
select the provider in Android settings, then open Chrome Settings → Autofill and
|
||||||
|
passwords → Autofill settings, choose **Autofill using another service**, and
|
||||||
|
restart Chrome. See [Chrome's instructions](https://support.google.com/chrome/answer/142893?co=GENIE.Platform%3DAndroid&hl=en).
|
||||||
|
|
||||||
|
## Requests and identity
|
||||||
|
|
||||||
|
Candidate responses contain entry paths only. Rust ranks exact path components
|
||||||
|
matching the verified application package or website host. Manual search is also
|
||||||
|
Rust-owned and returns up to 100 matching paths; refine the search to find another
|
||||||
|
entry. Listing candidates never decrypts an entry or restores a passphrase.
|
||||||
|
|
||||||
|
The OS adapter supplies installed package signing fingerprints.
|
||||||
|
The Android 11+ package-visibility permission is required to recheck the current
|
||||||
|
signers of arbitrary requesting apps, including clients with no launcher intent.
|
||||||
|
IronStorage queries only the package supplied by the OS request and never
|
||||||
|
enumerates installed apps. See [Android's package visibility contract](https://developer.android.com/training/package-visibility/declaring).
|
||||||
|
The manifest suppresses only the lint recommendation to replace this permission
|
||||||
|
with static queries: arbitrary OS-mediated clients have no common discoverable
|
||||||
|
intent filter. A Google Play release must declare and justify this use under
|
||||||
|
[Play's package visibility policy](https://support.google.com/googleplay/android-developer/answer/10158779).
|
||||||
|
Credential Manager's supplied signing identity must agree with PackageManager. Rust's
|
||||||
|
reviewed release-signature list is passed to AndroidX `CallingAppInfo.getOrigin`;
|
||||||
|
Rust then validates and canonicalizes the verified HTTPS origin. The list covers
|
||||||
|
release Chrome and Firefox. Unsupported or untrusted origin claims remain bound
|
||||||
|
to the verified application identity. Framework Autofill never treats raw
|
||||||
|
AssistStructure `webDomain` assertions as a website permission.
|
||||||
|
|
||||||
|
Kotlin translates AssistStructure identifiers, hints, HTML attributes and input
|
||||||
|
facts. Rust chooses exactly one username/email and one password role and rejects
|
||||||
|
ambiguous roles, duplicate identifiers and oversized facts. It uses the lossless
|
||||||
|
pass document's typed username/email/password fields after explicit selection;
|
||||||
|
it rejects ambiguous entries and filters any client-specified account constraint.
|
||||||
|
|
||||||
|
Requests have random 256-bit, process-local identifiers, a two-minute monotonic
|
||||||
|
deadline, an exact caller binding and the repository mutation revision. At most
|
||||||
|
16 Rust requests are outstanding. The final secret read checks identity, revision,
|
||||||
|
deadline and the shared authentication lease again and consumes the request.
|
||||||
|
Cancellation, relock, device lock, memory pressure and process recreation revoke
|
||||||
|
requests. Secret reads and metadata refreshes do not renew the lease.
|
||||||
|
|
||||||
|
The selection activity is not exported. PendingIntents have an explicit component,
|
||||||
|
a fixed action and unique immutable base data; mutability exists only so Android
|
||||||
|
can attach its complete request. The activity verifies that request against its
|
||||||
|
original caller and form. It uses secure-window and recents protections, rejects
|
||||||
|
obscured touches and clears its passphrase when focus or foreground ownership is
|
||||||
|
lost. It revokes the lease after successful handoff as well as cancellation.
|
||||||
|
|
||||||
|
## Filling and saving
|
||||||
|
|
||||||
|
Users select an entry, explicitly consent to the named recipient receiving its
|
||||||
|
username and password, and authenticate with a valid lease, biometrics or the GPG
|
||||||
|
passphrase. Only then does Rust release the selected fields to the OS response.
|
||||||
|
Autofill supplies an authentication suggestion, including an inline presentation
|
||||||
|
when the IME advertises the supported AndroidX style. Its attribution PendingIntent
|
||||||
|
is immutable and opens the ordinary app; the private authentication PendingIntent
|
||||||
|
is not exposed as IME attribution. Responses contain no other
|
||||||
|
entry fields. Credentials are never written to logs, saved activity state,
|
||||||
|
notifications or an application dataset cache. Android 12+ authentication results
|
||||||
|
use the framework's ephemeral-dataset flag. Older Android versions retain the
|
||||||
|
handed-off values in their current framework autofill session; IronStorage does
|
||||||
|
not keep a dataset cache.
|
||||||
|
|
||||||
|
Save offers use the same request binding. Submitted values are immediately sealed
|
||||||
|
with the installed encryption-capable private-key identities and retained in bounded process memory only.
|
||||||
|
They expire with the request. Users choose an entry path, confirm saving and
|
||||||
|
separately authorize updating an existing destination. Rust authenticates the chosen destination's recipient policy, including a different key than the default, and validates the
|
||||||
|
repository and authentication again, preserves unrelated fields, rejects an open
|
||||||
|
editor of the target and uses the recoverable structured-editor/Git transaction.
|
||||||
|
Its commit guard refuses cancelled, expired or relocked requests. There is no
|
||||||
|
silent capture and no competing password-store format.
|
||||||
|
An explicitly confirmed update containing identical fields succeeds without
|
||||||
|
rewriting the ciphertext, after rechecking the original destination and request.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
Rust tests use real encrypted stores to cover selection, missing consent, missing
|
||||||
|
lease, account constraints, one-use consumption, cancellation, changed signatures,
|
||||||
|
untrusted and invalid origins, stale repositories, expiry, ambiguous forms and
|
||||||
|
explicit create/update preservation. Native instrumentation exercises the same
|
||||||
|
production facade with real GPG and repository transactions on API 26 ARM64 and
|
||||||
|
strict 16-KB API 37 ARM64.
|
||||||
|
|
||||||
|
`android/provider-client` builds a standalone fixture APK with synthetic fields and actual
|
||||||
|
Credential Manager client calls. Its public HTML asset can be served on host loopback port 8765 and reached with emulator `adb reverse tcp:8765 tcp:8765`; the form verifies synthetic values locally and submits nothing.
|
||||||
|
The opt-in `ProviderFixtureSetupTest` seeds an
|
||||||
|
unconfigured retained emulator through production onboarding and refuses to
|
||||||
|
replace existing keys or a configured store. Its debug-only Hilt access hook is
|
||||||
|
absent from release builds. The fixture passphrase and credentials are public
|
||||||
|
synthetic values in the test source, not user credentials.
|
||||||
|
|
||||||
|
Computer-use validation on the retained strict 16-KB API 37 ARM64 emulator
|
||||||
|
confirmed real Credential Manager and native framework Autofill fills, plus a
|
||||||
|
Chrome fill whose local form checked both exact synthetic values. Chrome
|
||||||
|
145.0.7632.218 was used as explicitly requested by the user; no browser update
|
||||||
|
was installed. On this fixture Chrome offered Autofill when its password field
|
||||||
|
was focused. Before authentication, only the unlock/search suggestion and entry
|
||||||
|
paths were shown. Every handoff relocked the provider, and interrupted requests
|
||||||
|
expired without filling.
|
||||||
|
|
||||||
|
Credential Manager also saved `fixture-bob` at an explicitly selected new
|
||||||
|
synthetic destination after consent and authentication. The opt-in
|
||||||
|
`providerSavedFixtureValidation=true` instrumentation read the resulting username
|
||||||
|
and password through the production Rust credential facade and verified both
|
||||||
|
submitted fields.
|
||||||
|
Framework Autofill's actual system save offer also created a second explicitly
|
||||||
|
selected destination; the same readback test verified both submitted fields there.
|
||||||
|
Overwrite without its separate confirmation was rejected; a confirmed identical
|
||||||
|
update completed successfully through the actual system dialog.
|
||||||
|
The separate `providerFixtureValidation=true` regression checks
|
||||||
|
installed caller visibility and verifies that normal UI/cache trim callbacks
|
||||||
|
preserve locked metadata requests while genuine memory pressure revokes them.
|
||||||
|
|
||||||
|
The integration follows the official [Credential Provider guide](https://developer.android.com/identity/sign-in/credential-provider),
|
||||||
|
[Autofill service contract](https://developer.android.com/reference/android/service/autofill/AutofillService)
|
||||||
|
and [inline suggestion contract](https://developer.android.com/identity/autofill/ime-autofill).
|
||||||
|
AndroidX Credentials 1.6.0 and Autofill 1.3.0 are pinned with locks, checksums and
|
||||||
|
Apache-2.0 license inventory entries. Emulator evidence is ARM-only; no physical
|
||||||
|
or x86_64 runtime coverage is claimed.
|
||||||
@@ -61,8 +61,20 @@ fn manifest_text(source: &str) -> Result<(), Box<dyn Error>> {
|
|||||||
"de.rfc1437.ironstorage.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION",
|
"de.rfc1437.ironstorage.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION",
|
||||||
"android.permission.INTERNET",
|
"android.permission.INTERNET",
|
||||||
"android.permission.CAMERA",
|
"android.permission.CAMERA",
|
||||||
|
"android.permission.QUERY_ALL_PACKAGES",
|
||||||
];
|
];
|
||||||
for node in root.children().filter(|node| node.is_element()) {
|
for node in root.children().filter(|node| node.is_element()) {
|
||||||
|
if node.attribute((ANDROID, "name")) == Some("android.permission.QUERY_ALL_PACKAGES")
|
||||||
|
&& !application.children().any(|service| {
|
||||||
|
service.has_tag_name("service")
|
||||||
|
&& service.attribute((ANDROID, "name"))
|
||||||
|
== Some("de.rfc1437.ironstorage.providers.IronAutofillService")
|
||||||
|
&& service.attribute((ANDROID, "permission"))
|
||||||
|
== Some("android.permission.BIND_AUTOFILL_SERVICE")
|
||||||
|
})
|
||||||
|
{
|
||||||
|
return Err("package visibility requires the reviewed autofill provider".into());
|
||||||
|
}
|
||||||
if node.tag_name().name().starts_with("uses-permission")
|
if node.tag_name().name().starts_with("uses-permission")
|
||||||
&& !allowed.contains(&node.attribute((ANDROID, "name")).unwrap_or(""))
|
&& !allowed.contains(&node.attribute((ANDROID, "name")).unwrap_or(""))
|
||||||
{
|
{
|
||||||
@@ -109,6 +121,46 @@ fn manifest_text(source: &str) -> Result<(), Box<dyn Error>> {
|
|||||||
{
|
{
|
||||||
return Err("unreviewed launcher intent".into());
|
return Err("unreviewed launcher intent".into());
|
||||||
}
|
}
|
||||||
|
} else if node.has_tag_name("service")
|
||||||
|
&& matches!(
|
||||||
|
node.attribute((ANDROID, "name")),
|
||||||
|
Some(
|
||||||
|
"de.rfc1437.ironstorage.providers.IronAutofillService"
|
||||||
|
| "de.rfc1437.ironstorage.providers.IronCredentialProviderService"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
let autofill = node.attribute((ANDROID, "name"))
|
||||||
|
== Some("de.rfc1437.ironstorage.providers.IronAutofillService");
|
||||||
|
let (permission, action, metadata) = if autofill {
|
||||||
|
(
|
||||||
|
"android.permission.BIND_AUTOFILL_SERVICE",
|
||||||
|
"android.service.autofill.AutofillService",
|
||||||
|
"android.autofill",
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
"android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE",
|
||||||
|
"android.service.credentials.CredentialProviderService",
|
||||||
|
"android.credentials.provider",
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let actions: Vec<_> = node
|
||||||
|
.descendants()
|
||||||
|
.filter(|node| node.has_tag_name("action") || node.has_tag_name("category"))
|
||||||
|
.filter_map(|node| node.attribute((ANDROID, "name")))
|
||||||
|
.collect();
|
||||||
|
if node.attribute((ANDROID, "exported")) != Some("true")
|
||||||
|
|| node.attribute((ANDROID, "permission")) != Some(permission)
|
||||||
|
|| actions != [action]
|
||||||
|
|| !node.children().any(|node| {
|
||||||
|
node.has_tag_name("meta-data")
|
||||||
|
&& node.attribute((ANDROID, "name")) == Some(metadata)
|
||||||
|
&& node.attribute((ANDROID, "resource")).is_some()
|
||||||
|
})
|
||||||
|
{
|
||||||
|
return Err("credential services must retain their system-only binding permissions and reviewed actions".into());
|
||||||
|
}
|
||||||
} else if node.has_tag_name("receiver")
|
} else if node.has_tag_name("receiver")
|
||||||
&& node.attribute((ANDROID, "name"))
|
&& node.attribute((ANDROID, "name"))
|
||||||
== Some("androidx.profileinstaller.ProfileInstallReceiver")
|
== Some("androidx.profileinstaller.ProfileInstallReceiver")
|
||||||
@@ -442,6 +494,10 @@ mod tests {
|
|||||||
r#"<manifest xmlns:android="{ANDROID}" package="de.rfc1437.ironstorage"><application android:allowBackup="false" android:usesCleartextTraffic="false" android:fullBackupContent="@xml/backup_rules" android:dataExtractionRules="@xml/data_extraction_rules"><activity android:name="de.rfc1437.ironstorage.MainActivity" 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="fixture" android:exported="false"/></application></manifest>"#
|
r#"<manifest xmlns:android="{ANDROID}" package="de.rfc1437.ironstorage"><application android:allowBackup="false" android:usesCleartextTraffic="false" android:fullBackupContent="@xml/backup_rules" android:dataExtractionRules="@xml/data_extraction_rules"><activity android:name="de.rfc1437.ironstorage.MainActivity" 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="fixture" android:exported="false"/></application></manifest>"#
|
||||||
);
|
);
|
||||||
assert!(manifest_text(&source).is_ok());
|
assert!(manifest_text(&source).is_ok());
|
||||||
|
assert!(manifest_text(&source.replace(
|
||||||
|
"<application",
|
||||||
|
"<uses-permission android:name=\"android.permission.QUERY_ALL_PACKAGES\"/><application",
|
||||||
|
)).is_err());
|
||||||
assert!(
|
assert!(
|
||||||
manifest_text(&source.replace(
|
manifest_text(&source.replace(
|
||||||
"<application",
|
"<application",
|
||||||
@@ -486,6 +542,45 @@ mod tests {
|
|||||||
assert!(manifest_text(&launcher.replace("INSTALL_PROFILE", "UNREVIEWED")).is_err());
|
assert!(manifest_text(&launcher.replace("INSTALL_PROFILE", "UNREVIEWED")).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn provider_services_require_exact_system_binding_permissions() {
|
||||||
|
let source = format!(
|
||||||
|
r#"<manifest xmlns:android="{ANDROID}" package="de.rfc1437.ironstorage"><application android:allowBackup="false" android:usesCleartextTraffic="false" android:fullBackupContent="@xml/backup_rules" android:dataExtractionRules="@xml/data_extraction_rules"><activity android:name="de.rfc1437.ironstorage.MainActivity" android:exported="true"><intent-filter><action android:name="android.intent.action.MAIN"/><category android:name="android.intent.category.LAUNCHER"/></intent-filter></activity><service android:name="de.rfc1437.ironstorage.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></application></manifest>"#
|
||||||
|
);
|
||||||
|
assert!(manifest_text(&source).is_ok());
|
||||||
|
let visible = source.replace(
|
||||||
|
"<application",
|
||||||
|
"<uses-permission android:name=\"android.permission.QUERY_ALL_PACKAGES\"/><application",
|
||||||
|
);
|
||||||
|
assert!(manifest_text(&visible).is_ok());
|
||||||
|
assert!(
|
||||||
|
manifest_text(&visible.replace("IronAutofillService", "UnreviewedService")).is_err()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
manifest_text(&source.replace(
|
||||||
|
"android.permission.BIND_AUTOFILL_SERVICE",
|
||||||
|
"android.permission.INTERNET"
|
||||||
|
))
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
manifest_text(&source.replace(
|
||||||
|
"android.service.autofill.AutofillService",
|
||||||
|
"unreviewed.action"
|
||||||
|
))
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
let credentials = source
|
||||||
|
.replace("IronAutofillService", "IronCredentialProviderService")
|
||||||
|
.replace("BIND_AUTOFILL_SERVICE", "BIND_CREDENTIAL_PROVIDER_SERVICE")
|
||||||
|
.replace(
|
||||||
|
"android.service.autofill.AutofillService",
|
||||||
|
"android.service.credentials.CredentialProviderService",
|
||||||
|
)
|
||||||
|
.replace("android.autofill", "android.credentials.provider");
|
||||||
|
assert!(manifest_text(&credentials).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn backup_requires_both_sections_all_domains_and_no_include() {
|
fn backup_requires_both_sections_all_domains_and_no_include() {
|
||||||
let exclusions = DOMAINS
|
let exclusions = DOMAINS
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ fn check(root: &Path) -> Result<(), Box<dyn Error>> {
|
|||||||
for (module, allowed) in [
|
for (module, allowed) in [
|
||||||
("app", &["features", "core", "platform"][..]),
|
("app", &["features", "core", "platform"][..]),
|
||||||
("performance", &["app"][..]),
|
("performance", &["app"][..]),
|
||||||
|
("provider-client", &[][..]),
|
||||||
("features", &["core"][..]),
|
("features", &["core"][..]),
|
||||||
("core", &["rust-bridge"][..]),
|
("core", &["rust-bridge"][..]),
|
||||||
("platform", &["core"][..]),
|
("platform", &["core"][..]),
|
||||||
|
|||||||
Reference in new issue
Block a user