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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015enAP3dJTvB521BuULqU8P
This commit is contained in:
Claude
2026-06-18 19:23:42 +00:00
parent a203638392
commit 2500cd7d61
4 changed files with 55 additions and 17 deletions
@@ -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,
@@ -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("", "")))
}
}
@@ -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)
@@ -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),