mirror of
https://github.com/greenart7c3/Amber.git
synced 2026-10-05 19:08:23 +00:00
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:
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user