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:
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 }