app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit cba63fa6f57648f54300b3f2d5d4b854b851b01c
parent 9e8d0683844362bb6174676a5792795da7f81da5
Author: triesap <tyson@radroots.org>
Date:   Sun,  2 Aug 2026 20:10:10 +0000

fix(security): clear imported secrets immediately

- clear the masked Compose draft when an import command is accepted
- retain secret text only in the unavoidable in-flight JVM argument
- keep busy commands from discarding an unsubmitted draft
- verify UI state clears before the native coroutine executes

Diffstat:
Mapp/desktop/src/main/kotlin/org/radroots/studio/application/StudioAppStore.kt | 8+++-----
Mapp/desktop/src/test/kotlin/org/radroots/studio/application/StudioAppStoreTest.kt | 21++++++++++++++++++++-
2 files changed, 23 insertions(+), 6 deletions(-)

diff --git a/app/desktop/src/main/kotlin/org/radroots/studio/application/StudioAppStore.kt b/app/desktop/src/main/kotlin/org/radroots/studio/application/StudioAppStore.kt @@ -58,12 +58,10 @@ class StudioAppStore( } fun importSecretKey() { + if (command?.isActive == true) return val input = mutableState.value.importDraft - runCommand { - gateway.importSecretKey(input).also { - mutableState.value = mutableState.value.copy(importDraft = "") - } - } + mutableState.value = mutableState.value.copy(importDraft = "") + runCommand { gateway.importSecretKey(input) } } fun selectAccount(publicKeyHex: String) { diff --git a/app/desktop/src/test/kotlin/org/radroots/studio/application/StudioAppStoreTest.kt b/app/desktop/src/test/kotlin/org/radroots/studio/application/StudioAppStoreTest.kt @@ -64,6 +64,22 @@ class StudioAppStoreTest { assertEquals(1, gateway.signOutCalls) store.close() } + + @Test + fun `clears imported secret draft as soon as command is accepted`() = runTest { + val gateway = FakeStudioCoreGateway(snapshot(0UL)) + val store = StudioAppStore(gateway, this) + advanceUntilIdle() + store.editImportDraft("nsec1secret") + + store.importSecretKey() + + assertEquals("", store.state.value.importDraft) + assertEquals(emptyList(), gateway.importedSecrets) + advanceUntilIdle() + assertEquals(listOf("nsec1secret"), gateway.importedSecrets) + store.close() + } } private class FakeStudioCoreGateway( @@ -73,6 +89,7 @@ private class FakeStudioCoreGateway( var closed = false var subscriptionClosed = false var signOutCalls = 0 + val importedSecrets = mutableListOf<String>() override fun snapshot(): AppSnapshotDto = current @@ -94,7 +111,9 @@ private class FakeStudioCoreGateway( return GeneratedAccountDto(account(), next, "nsec1secret") } - override suspend fun importSecretKey(secretKey: String): AppSnapshotDto = current + override suspend fun importSecretKey(secretKey: String): AppSnapshotDto = current.also { + importedSecrets += secretKey + } override suspend fun selectAccount(publicKeyHex: String): AppSnapshotDto = current override suspend fun activateAccount(publicKeyHex: String): AppSnapshotDto = current override suspend fun signOut(): AppSnapshotDto = current.also { signOutCalls += 1 }