Merge pull request #476 from greenart7c3/claude/sensitive-clipboard-keys-86dxv2

Flag nsec, ncryptsec and seed words clipboard copies as sensitive
This commit is contained in:
greenart7c3
2026-06-19 06:04:32 -03:00
committed by GitHub
4 changed files with 79 additions and 17 deletions
@@ -1,15 +1,14 @@
package com.greenart7c3.nostrsigner.models
import android.content.ClipData
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.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(
ClipData.newPlainText("", nsec),
),
)
clipboardManager.setSensitiveClip("", nsec)
Toast.makeText(
Amber.instance,
@@ -0,0 +1,73 @@
package com.greenart7c3.nostrsigner.ui
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
* 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
}
/**
* 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("", "")))
}
}
@@ -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
@@ -70,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
@@ -107,6 +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.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(ClipData.newPlainText("", key)))
clipboardManager.setSensitiveClip("", key)
Toast.makeText(context, context.getString(R.string.secret_key_copied_to_clipboard), Toast.LENGTH_SHORT).show()
}
onLoading(false)
@@ -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
@@ -20,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
@@ -32,6 +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.setSensitiveClip
import com.greenart7c3.nostrsigner.ui.verticalScrollbar
import kotlinx.coroutines.launch
@@ -74,11 +73,7 @@ fun SeedWordsPage(
AmberButton(
onClick = {
scope.launch {
clipboardManager.setClipEntry(
ClipEntry(
ClipData.newPlainText("", seedWords.joinToString(" ")),
),
)
clipboardManager.setSensitiveClip("", seedWords.joinToString(" "))
}
},
text = stringResource(R.string.copy_to_clipboard),