Prepare Android release artifacts and listing (#112)
Add Rust distribution auditing, pinned post-build AAB signing, complete offline third-party notices, an Android privacy policy and reviewable Play listing assets. Preserve the application ID and audit both ARM ABIs, native symbols, baseline profiles and R8 mapping. Fix dialog focus ownership, navigation-triggered cleanup loops and stale authentication errors after first-run setup, with native and real-activity regressions. Google Play publication is deferred by the user. Issue #112 remains open for production signing, Console declarations and actual Play install/update verification.
This commit is contained in:
1 parent
b54636b7bc
commit
bf83a0ba41
51 files changed
+1493
-41
No files matched your search
@@ -54,6 +54,7 @@ import androidx.compose.ui.semantics.stateDescription
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.compose.LifecycleEventEffect
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import de.rfc1437.ironstorage.core.AuthenticationFailureKind
|
||||
import de.rfc1437.ironstorage.core.AuthenticationRepository
|
||||
@@ -101,7 +102,11 @@ fun PasswordScreen(
|
||||
entry?.id
|
||||
)
|
||||
}
|
||||
LifecycleEventEffect(Lifecycle.Event.ON_STOP) {
|
||||
// Navigation disposal must not revoke again while cleanup rebuilds the protected root.
|
||||
LifecycleEventEffect(
|
||||
Lifecycle.Event.ON_STOP,
|
||||
lifecycleOwner = LocalProtectedLifecycleOwner.current ?: LocalLifecycleOwner.current
|
||||
) {
|
||||
manual = false
|
||||
requested = false
|
||||
repository.leave()
|
||||
|
||||
@@ -22,11 +22,13 @@ import androidx.compose.ui.platform.InterceptPlatformTextInput
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.platform.PlatformTextInputMethodRequest
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import de.rfc1437.ironstorage.core.ProtectedPresentation
|
||||
|
||||
private val LocalProtectedPresentation = staticCompositionLocalOf<ProtectedPresentation?> { null }
|
||||
private val LocalProtectedRoot = staticCompositionLocalOf<View?> { null }
|
||||
internal val LocalProtectedLifecycleOwner = staticCompositionLocalOf<LifecycleOwner?> { null }
|
||||
|
||||
/** Register before showing a dialog/popup so its focus transfer does not lock the activity. */
|
||||
@Composable
|
||||
@@ -46,7 +48,8 @@ fun ProtectedContent(presentation: ProtectedPresentation, content: @Composable (
|
||||
val focused = remember(view) { mutableStateOf(view.hasWindowFocus()) }
|
||||
// Read here so cleanup also reruns the parent surface's reveal effect.
|
||||
val generation = presentation.generation.collectAsState().value
|
||||
val lifecycle by LocalLifecycleOwner.current.lifecycle.currentStateFlow.collectAsState()
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
val lifecycle by lifecycleOwner.lifecycle.currentStateFlow.collectAsState()
|
||||
DisposableEffect(presentation, view) {
|
||||
val registration = presentation.register(surface::cover)
|
||||
val focus = ViewTreeObserver.OnWindowFocusChangeListener { focused.value = it }
|
||||
@@ -71,7 +74,8 @@ fun ProtectedContent(presentation: ProtectedPresentation, content: @Composable (
|
||||
}) {
|
||||
CompositionLocalProvider(
|
||||
LocalProtectedPresentation provides presentation,
|
||||
LocalProtectedRoot provides view
|
||||
LocalProtectedRoot provides view,
|
||||
LocalProtectedLifecycleOwner provides lifecycleOwner
|
||||
) {
|
||||
key(generation) { content() }
|
||||
}
|
||||
|
||||
+62
-13
@@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.selection.toggleable
|
||||
@@ -35,6 +36,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.platform.testTag
|
||||
@@ -81,7 +83,8 @@ fun SettingsScreen(
|
||||
var query by remember { mutableStateOf("") }
|
||||
var edit by remember { mutableStateOf<String?>(null) }
|
||||
var showSetup by remember { mutableStateOf(false) }
|
||||
var licenses by remember { mutableStateOf(false) }
|
||||
var document by remember { mutableStateOf<Int?>(null) }
|
||||
var showNotices by remember { mutableStateOf(false) }
|
||||
val uri = LocalUriHandler.current
|
||||
val setupState by onboarding.setup.collectAsStateWithLifecycle()
|
||||
LaunchedEffect(setupState.busy, setupState.connected, setupState.selectedKey) {
|
||||
@@ -261,16 +264,16 @@ fun SettingsScreen(
|
||||
}
|
||||
Text(stringResource(R.string.settings_privacy))
|
||||
TextButton(onClick = {
|
||||
licenses = true
|
||||
document = R.raw.privacy
|
||||
}) { Text(stringResource(R.string.settings_privacy_policy)) }
|
||||
TextButton(onClick = {
|
||||
document = R.raw.license
|
||||
}) { Text(stringResource(R.string.settings_licenses)) }
|
||||
TextButton(onClick = {
|
||||
uri.openUri("https://git.rfc1437.de/hugo/IronStorage/issues")
|
||||
}) { Text(stringResource(R.string.settings_support)) }
|
||||
TextButton(onClick = {
|
||||
uri.openUri(
|
||||
"https://git.rfc1437.de/hugo/IronStorage/" +
|
||||
"src/branch/main/android/dependency-policy.json"
|
||||
)
|
||||
showNotices = true
|
||||
}) {
|
||||
Text(stringResource(R.string.settings_dependency_licenses))
|
||||
}
|
||||
@@ -300,23 +303,69 @@ fun SettingsScreen(
|
||||
}
|
||||
}
|
||||
if (state.transfer.open && !showSetup) KeyTransferDialog(repository, authentication)
|
||||
if (licenses) {
|
||||
if (showNotices) {
|
||||
ProtectedWindow()
|
||||
val context = LocalContext.current
|
||||
val loading = stringResource(R.string.settings_license_loading)
|
||||
val paragraphs by produceState(listOf(loading), context) {
|
||||
value = withContext(Dispatchers.IO) {
|
||||
context.assets.open("third-party-notices.txt").bufferedReader().use {
|
||||
it.readText().split("\n\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
HingeSafeAlertDialog(
|
||||
onDismissRequest = { showNotices = false },
|
||||
title = { Text(stringResource(R.string.settings_dependency_licenses)) },
|
||||
text = {
|
||||
ProtectedInputSurface(authentication.presentation)
|
||||
LazyColumn(
|
||||
Modifier.fillMaxWidth().heightIn(max = 360.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(paragraphs) { Text(it) }
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
showNotices = false
|
||||
}) { Text(stringResource(R.string.settings_done)) }
|
||||
}
|
||||
)
|
||||
}
|
||||
document?.let { resource ->
|
||||
ProtectedWindow()
|
||||
val resources = LocalResources.current
|
||||
val loading = stringResource(R.string.settings_license_loading)
|
||||
val text by produceState(loading, resources) {
|
||||
val text by produceState(loading, resources, resource) {
|
||||
value = withContext(Dispatchers.IO) {
|
||||
resources.openRawResource(R.raw.license).bufferedReader().use { it.readText() }
|
||||
resources.openRawResource(resource).bufferedReader().use { it.readText() }
|
||||
}
|
||||
}
|
||||
HingeSafeAlertDialog(
|
||||
onDismissRequest = {
|
||||
licenses = false
|
||||
document = null
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
stringResource(
|
||||
if (resource ==
|
||||
R.raw.privacy
|
||||
) {
|
||||
R.string.settings_privacy_policy
|
||||
} else {
|
||||
R.string.settings_licenses
|
||||
}
|
||||
)
|
||||
)
|
||||
},
|
||||
text = {
|
||||
ProtectedInputSurface(authentication.presentation)
|
||||
Text(text, modifier = Modifier.verticalScroll(rememberScrollState()))
|
||||
},
|
||||
title = { Text(stringResource(R.string.settings_licenses)) },
|
||||
text = { Text(text, modifier = Modifier.verticalScroll(rememberScrollState())) },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
licenses = false
|
||||
document = null
|
||||
}) { Text(stringResource(R.string.settings_done)) }
|
||||
}
|
||||
)
|
||||
|
||||
@@ -35,6 +35,7 @@ import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.compose.LifecycleEventEffect
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import de.rfc1437.ironstorage.core.AuthenticationFailureKind
|
||||
import de.rfc1437.ironstorage.core.AuthenticationRepository
|
||||
@@ -50,7 +51,10 @@ fun TotpScreen(repository: TotpRepository, authentication: AuthenticationReposit
|
||||
var manual by remember { mutableStateOf(false) }
|
||||
var discovery by remember { mutableStateOf(false) }
|
||||
var requested by remember { mutableStateOf(false) }
|
||||
LifecycleEventEffect(Lifecycle.Event.ON_STOP) { repository.leave() }
|
||||
LifecycleEventEffect(
|
||||
Lifecycle.Event.ON_STOP,
|
||||
lifecycleOwner = LocalProtectedLifecycleOwner.current ?: LocalLifecycleOwner.current
|
||||
) { repository.leave() }
|
||||
BackHandler(state.detail) { repository.back() }
|
||||
androidx.compose.runtime.LaunchedEffect(lease.failure, state.loading, entry.loading) {
|
||||
if (requested && !state.loading && !entry.loading && !lease.busy) {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
IronStorage for Android — Privacy policy
|
||||
Updated 15 October 2026
|
||||
|
||||
IronStorage does not operate a password-storage service. The developer receives no passwords, keys, analytics, advertising identifiers, app inventory or crash reports from the app. There are no advertising or analytics SDKs.
|
||||
|
||||
On your device
|
||||
Your password-store repository, settings, imported/generated GPG key material and the TOTP discovery cache remain in the app's private, backup-excluded storage. Password entries use the upstream pass GPG format. The discovery cache records entry paths and ciphertext fingerprints so that unchanged entries can be found while locked; it does not store passwords or OTP seeds in plaintext. Passphrase-protected private GPG keys require their passphrase. Optional saved passphrases and HTTPS credentials are encrypted using Android Keystore keys. Biometric templates remain with Android; IronStorage receives only the authentication result and authorized cryptographic operation.
|
||||
|
||||
Optional network access
|
||||
Local-first use needs no server or IronStorage account. When you configure HTTPS Git synchronization, IronStorage contacts your selected Git server. The server receives the encrypted Git repository plus its unencrypted Git metadata (including entry paths and configured commit name/email), network address and the authentication credentials needed by that server. The server administrator controls retention and access. The developer receives none of this traffic unless you independently choose a server operated by the developer. TLS protects the connection; GPG protects entry contents for their configured recipients. Git metadata and HTTPS authentication are not end-to-end encrypted by GPG.
|
||||
|
||||
Camera and QR transfer
|
||||
Camera permission is requested only when you choose Scan QR. Frames are processed on the device and are not retained or uploaded. QR export can contain private keys when you explicitly select that option and confirm the warning. Anyone who photographs the displayed code may obtain its contents. Import/export does not send QR payloads to the developer.
|
||||
|
||||
Autofill, Credential Manager and clipboard
|
||||
Provider use is optional and must be enabled in Android settings. IronStorage verifies the requesting app's package identity and signing certificate locally. Broad package visibility supports this verification; it is not used to inventory apps, advertise or track you. Selected credentials are returned only through the Android provider request after the app's confirmation/authentication flow. Copy actions place the chosen value on the system clipboard; other apps or keyboards may have access under Android's rules. IronStorage marks sensitive clipboard content and schedules clearing it.
|
||||
|
||||
Deletion, uninstall and backup
|
||||
Android cloud backup and device-transfer backup are disabled for IronStorage. Uninstalling or clearing app storage removes the local repository, keys, settings and cache and makes the app's Keystore-protected records unavailable. Reinstalling does not restore them. Preserve an independent compatible password-store repository and the GPG private keys/passphrases you need before uninstalling. Data already pushed to a Git server remains there until removed under that server's policy. Removing a local entry does not rewrite older Git history. No developer-side account or deletion request is needed because IronStorage creates no such account or database.
|
||||
|
||||
Support
|
||||
https://git.rfc1437.de/hugo/IronStorage/issues
|
||||
Support submissions are voluntary and public. Do not include passwords, tokens, private keys, private repository URLs or identifying diagnostics. Linked websites and your configured Git host have their own privacy policies.
|
||||
@@ -183,8 +183,9 @@
|
||||
<string name="settings_version">IronStorage %1$s</string>
|
||||
<string name="settings_privacy">Passwords and private keys stay in protected app storage. No analytics or advertising are included. HTTPS synchronization connects only to the configured server. Secrets are hidden from screenshots, saved form state, logs and personalized keyboard learning.</string>
|
||||
<string name="settings_licenses">MIT license</string>
|
||||
<string name="settings_privacy_policy">Privacy policy</string>
|
||||
<string name="settings_support">Support and issue tracker</string>
|
||||
<string name="settings_dependency_licenses">Dependency license inventory</string>
|
||||
<string name="settings_dependency_licenses">Third-party notices</string>
|
||||
<string name="settings_done">Done</string>
|
||||
<string name="settings_provider_unsupported">Not supported on this Android version or device</string>
|
||||
<string name="settings_provider_not_installed">The IronStorage provider service is not included in this build</string>
|
||||
@@ -223,7 +224,7 @@
|
||||
<item quantity="one">%1$d result</item>
|
||||
<item quantity="other">%1$d results</item>
|
||||
</plurals>
|
||||
<string name="settings_license_loading">Loading license…</string>
|
||||
<string name="settings_license_loading">Loading document…</string>
|
||||
<string name="qr_scan">Scan GPG QR codes</string>
|
||||
<string name="qr_pause">Pause camera</string>
|
||||
<string name="qr_resume">Resume camera</string>
|
||||
|
||||
Reference in new issue
Block a user