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..276a90db 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/models/Account.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/models/Account.kt @@ -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, 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..fef0e4c6 --- /dev/null +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/SensitiveClipboard.kt @@ -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("", ""))) + } +} 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..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 @@ -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) 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..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 @@ -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),