From a203638392488e9f831f19dbeea1b7211350bda7 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 19:19:05 +0000 Subject: [PATCH 1/2] Flag nsec, ncryptsec and seed words clipboard copies as sensitive Mark clipboard entries for the secret key (nsec), encrypted secret key (ncryptsec) and seed words as sensitive content so Android 13+ omits them from clipboard previews. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_015enAP3dJTvB521BuULqU8P --- .../greenart7c3/nostrsigner/models/Account.kt | 4 ++-- .../nostrsigner/ui/SensitiveClipboard.kt | 24 +++++++++++++++++++ .../ui/actions/AccountBackupScreen.kt | 4 ++-- .../ui/components/SeedWordsPage.kt | 4 ++-- 4 files changed, 30 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/com/greenart7c3/nostrsigner/ui/SensitiveClipboard.kt diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/models/Account.kt b/app/src/main/java/com/greenart7c3/nostrsigner/models/Account.kt index 60ef217a..28e9ff85 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/models/Account.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/models/Account.kt @@ -1,6 +1,5 @@ package com.greenart7c3.nostrsigner.models -import android.content.ClipData import android.widget.Toast import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable @@ -10,6 +9,7 @@ import com.greenart7c3.nostrsigner.Amber import com.greenart7c3.nostrsigner.DataStoreAccess import com.greenart7c3.nostrsigner.R import com.greenart7c3.nostrsigner.service.nip44v3.Nip44v3 +import com.greenart7c3.nostrsigner.ui.newSensitivePlainText import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray @@ -122,7 +122,7 @@ class Account( Amber.instance.applicationIOScope.launch(Dispatchers.Main) { clipboardManager.setClipEntry( ClipEntry( - ClipData.newPlainText("", nsec), + newSensitivePlainText("", nsec), ), ) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/SensitiveClipboard.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/SensitiveClipboard.kt new file mode 100644 index 00000000..a25a31da --- /dev/null +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/SensitiveClipboard.kt @@ -0,0 +1,24 @@ +package com.greenart7c3.nostrsigner.ui + +import android.content.ClipData +import android.content.ClipDescription +import android.os.Build +import android.os.PersistableBundle + +/** + * Creates a [ClipData] flagged as sensitive so the system (Android 13+) avoids + * showing the copied content in clipboard previews. Use this for secrets such as + * the nsec, ncryptsec and seed words. + */ +fun newSensitivePlainText(label: CharSequence, text: CharSequence): ClipData { + val clipData = ClipData.newPlainText(label, text) + val extras = PersistableBundle().apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true) + } else { + putBoolean("android.content.extra.IS_SENSITIVE", true) + } + } + clipData.description.extras = extras + return clipData +} diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/AccountBackupScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/AccountBackupScreen.kt index 6bb17949..d884f232 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/AccountBackupScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/AccountBackupScreen.kt @@ -1,7 +1,6 @@ package com.greenart7c3.nostrsigner.ui.actions import android.app.Activity -import android.content.ClipData import android.content.Context import android.view.WindowManager import android.widget.Toast @@ -107,6 +106,7 @@ import com.greenart7c3.nostrsigner.ui.components.IconRow import com.greenart7c3.nostrsigner.ui.components.MarkdownText import com.greenart7c3.nostrsigner.ui.components.SeedWordsPage import com.greenart7c3.nostrsigner.ui.navigation.Route +import com.greenart7c3.nostrsigner.ui.newSensitivePlainText import com.greenart7c3.nostrsigner.ui.theme.Size35dp import com.greenart7c3.nostrsigner.ui.theme.fromHex import com.vitorpamplona.quartz.nip06KeyDerivation.Nip06 @@ -797,7 +797,7 @@ private fun encryptCopyNSec( account.didBackup = true onBackupDone() Amber.instance.applicationIOScope.launch(Dispatchers.Main) { - clipboardManager.setClipEntry(ClipEntry(ClipData.newPlainText("", key))) + clipboardManager.setClipEntry(ClipEntry(newSensitivePlainText("", key))) Toast.makeText(context, context.getString(R.string.secret_key_copied_to_clipboard), Toast.LENGTH_SHORT).show() } onLoading(false) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/SeedWordsPage.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/SeedWordsPage.kt index 8f106a89..dbb84703 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/SeedWordsPage.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/SeedWordsPage.kt @@ -1,6 +1,5 @@ package com.greenart7c3.nostrsigner.ui.components -import android.content.ClipData import android.view.WindowManager import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -32,6 +31,7 @@ import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner import com.greenart7c3.nostrsigner.Amber import com.greenart7c3.nostrsigner.R +import com.greenart7c3.nostrsigner.ui.newSensitivePlainText import com.greenart7c3.nostrsigner.ui.verticalScrollbar import kotlinx.coroutines.launch @@ -76,7 +76,7 @@ fun SeedWordsPage( scope.launch { clipboardManager.setClipEntry( ClipEntry( - ClipData.newPlainText("", seedWords.joinToString(" ")), + newSensitivePlainText("", seedWords.joinToString(" ")), ), ) } From 2500cd7d61dfd2d3b373f92a4015fed82fc58704 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 19:23:42 +0000 Subject: [PATCH 2/2] Clear sensitive clipboard content after a delay After copying the nsec, ncryptsec or seed words, schedule the clipboard to be cleared after 60 seconds. The clipboard is only cleared if it still holds the copied secret, so later copies are left untouched. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_015enAP3dJTvB521BuULqU8P --- .../greenart7c3/nostrsigner/models/Account.kt | 9 +--- .../nostrsigner/ui/SensitiveClipboard.kt | 49 +++++++++++++++++++ .../ui/actions/AccountBackupScreen.kt | 5 +- .../ui/components/SeedWordsPage.kt | 9 +--- 4 files changed, 55 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/models/Account.kt b/app/src/main/java/com/greenart7c3/nostrsigner/models/Account.kt index 28e9ff85..276a90db 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/models/Account.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/models/Account.kt @@ -3,13 +3,12 @@ package com.greenart7c3.nostrsigner.models import android.widget.Toast import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable -import androidx.compose.ui.platform.ClipEntry import androidx.compose.ui.platform.Clipboard import com.greenart7c3.nostrsigner.Amber import com.greenart7c3.nostrsigner.DataStoreAccess import com.greenart7c3.nostrsigner.R import com.greenart7c3.nostrsigner.service.nip44v3.Nip44v3 -import com.greenart7c3.nostrsigner.ui.newSensitivePlainText +import com.greenart7c3.nostrsigner.ui.setSensitiveClip import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray @@ -120,11 +119,7 @@ class Account( didBackup = true val nsec = getNsec() Amber.instance.applicationIOScope.launch(Dispatchers.Main) { - clipboardManager.setClipEntry( - ClipEntry( - newSensitivePlainText("", nsec), - ), - ) + clipboardManager.setSensitiveClip("", nsec) Toast.makeText( Amber.instance, diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/SensitiveClipboard.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/SensitiveClipboard.kt index a25a31da..fef0e4c6 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/SensitiveClipboard.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/SensitiveClipboard.kt @@ -4,6 +4,15 @@ import android.content.ClipData import android.content.ClipDescription import android.os.Build import android.os.PersistableBundle +import androidx.compose.ui.platform.ClipEntry +import androidx.compose.ui.platform.Clipboard +import com.greenart7c3.nostrsigner.Amber +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +/** How long a copied secret stays on the clipboard before it is cleared. */ +const val SENSITIVE_CLIPBOARD_CLEAR_DELAY_MS = 60_000L /** * Creates a [ClipData] flagged as sensitive so the system (Android 13+) avoids @@ -22,3 +31,43 @@ fun newSensitivePlainText(label: CharSequence, text: CharSequence): ClipData { clipData.description.extras = extras return clipData } + +/** + * Copies a secret to the clipboard flagged as sensitive content and schedules it + * to be cleared after [clearAfterMillis]. The clipboard is only cleared if it + * still contains the copied secret, so anything the user copies afterwards is + * left untouched. + */ +suspend fun Clipboard.setSensitiveClip( + label: CharSequence, + text: CharSequence, + scope: CoroutineScope = Amber.instance.applicationIOScope, + clearAfterMillis: Long = SENSITIVE_CLIPBOARD_CLEAR_DELAY_MS, +) { + setClipEntry(ClipEntry(newSensitivePlainText(label, text))) + scheduleSensitiveClear(text, scope, clearAfterMillis) +} + +private fun Clipboard.scheduleSensitiveClear( + copiedValue: CharSequence, + scope: CoroutineScope, + delayMillis: Long, +) { + scope.launch { + delay(delayMillis) + val currentText = getClipEntry()?.clipData?.let { clip -> + if (clip.itemCount > 0) clip.getItemAt(0).text?.toString() else null + } + if (currentText == copiedValue.toString()) { + clearClipboard() + } + } +} + +private suspend fun Clipboard.clearClipboard() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + nativeClipboard.clearPrimaryClip() + } else { + setClipEntry(ClipEntry(ClipData.newPlainText("", ""))) + } +} diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/AccountBackupScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/AccountBackupScreen.kt index d884f232..18264f60 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/AccountBackupScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/AccountBackupScreen.kt @@ -69,7 +69,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.autofill.ContentType import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.ClipEntry import androidx.compose.ui.platform.Clipboard import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalContext @@ -106,7 +105,7 @@ import com.greenart7c3.nostrsigner.ui.components.IconRow import com.greenart7c3.nostrsigner.ui.components.MarkdownText import com.greenart7c3.nostrsigner.ui.components.SeedWordsPage import com.greenart7c3.nostrsigner.ui.navigation.Route -import com.greenart7c3.nostrsigner.ui.newSensitivePlainText +import com.greenart7c3.nostrsigner.ui.setSensitiveClip import com.greenart7c3.nostrsigner.ui.theme.Size35dp import com.greenart7c3.nostrsigner.ui.theme.fromHex import com.vitorpamplona.quartz.nip06KeyDerivation.Nip06 @@ -797,7 +796,7 @@ private fun encryptCopyNSec( account.didBackup = true onBackupDone() Amber.instance.applicationIOScope.launch(Dispatchers.Main) { - clipboardManager.setClipEntry(ClipEntry(newSensitivePlainText("", key))) + clipboardManager.setSensitiveClip("", key) Toast.makeText(context, context.getString(R.string.secret_key_copied_to_clipboard), Toast.LENGTH_SHORT).show() } onLoading(false) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/SeedWordsPage.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/SeedWordsPage.kt index dbb84703..8a4a8e98 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/SeedWordsPage.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/SeedWordsPage.kt @@ -19,7 +19,6 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.ClipEntry import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -31,7 +30,7 @@ import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner import com.greenart7c3.nostrsigner.Amber import com.greenart7c3.nostrsigner.R -import com.greenart7c3.nostrsigner.ui.newSensitivePlainText +import com.greenart7c3.nostrsigner.ui.setSensitiveClip import com.greenart7c3.nostrsigner.ui.verticalScrollbar import kotlinx.coroutines.launch @@ -74,11 +73,7 @@ fun SeedWordsPage( AmberButton( onClick = { scope.launch { - clipboardManager.setClipEntry( - ClipEntry( - newSensitivePlainText("", seedWords.joinToString(" ")), - ), - ) + clipboardManager.setSensitiveClip("", seedWords.joinToString(" ")) } }, text = stringResource(R.string.copy_to_clipboard),