mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-09-23 18:21:50 +01:00
Mark copied secrets as sensitive and auto-clear the backup passphrase.
Co-authored-by: Greyson Parrelil <greyson@signal.org>
This commit is contained in:
committed by
Cody Henthorne
co-authored by
Greyson Parrelil
parent
c6ff93af98
commit
e90e421fea
@@ -21,12 +21,15 @@ import android.app.ActivityManager;
|
||||
import android.app.AlarmManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipDescription;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Typeface;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.PersistableBundle;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
@@ -64,6 +67,9 @@ public class Util {
|
||||
|
||||
public static final String COPY_LABEL = "text\u00AD";
|
||||
|
||||
/** How long a secret sits on the clipboard before it is cleared, when the caller doesn't specify. */
|
||||
public static final int SENSITIVE_CLIPBOARD_TIMEOUT_SECONDS = 60;
|
||||
|
||||
public static <T> List<T> asList(T... elements) {
|
||||
List<T> result = new LinkedList<>();
|
||||
Collections.addAll(result, elements);
|
||||
@@ -436,9 +442,26 @@ public class Util {
|
||||
((ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE)).setPrimaryClip(ClipData.newPlainText(COPY_LABEL, text));
|
||||
}
|
||||
|
||||
public static void copyToClipboard(@NonNull Context context, @NonNull CharSequence text, int expiresInSeconds) {
|
||||
/**
|
||||
* Copies a secret to the clipboard, clearing it after {@link #SENSITIVE_CLIPBOARD_TIMEOUT_SECONDS}. See
|
||||
* {@link #copyToClipboardSensitive(Context, CharSequence, int)}.
|
||||
*/
|
||||
public static void copyToClipboardSensitive(@NonNull Context context, @NonNull CharSequence text) {
|
||||
copyToClipboardSensitive(context, text, SENSITIVE_CLIPBOARD_TIMEOUT_SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies a secret (backup passphrase, recovery phrase, key) to the clipboard.
|
||||
*
|
||||
* Flags the clip as sensitive, which stops the system clipboard UI on API 33+ from rendering a preview of the value and stops keyboards
|
||||
* from offering it as a paste suggestion. It also schedules an alarm to clear the clipboard after {@code expiresInSeconds}.
|
||||
*/
|
||||
public static void copyToClipboardSensitive(@NonNull Context context, @NonNull CharSequence text, int expiresInSeconds) {
|
||||
ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
clipboardManager.setPrimaryClip(ClipData.newPlainText(getPackageLabel(context), text));
|
||||
ClipData clip = ClipData.newPlainText(getPackageLabel(context), text);
|
||||
|
||||
markSensitive(clip);
|
||||
clipboardManager.setPrimaryClip(clip);
|
||||
|
||||
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
|
||||
Intent alarmIntent = new Intent(context, ClearClipboardAlarmReceiver.class);
|
||||
@@ -447,6 +470,19 @@ public class Util {
|
||||
alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(expiresInSeconds), pendingAlarmIntent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a clip as containing sensitive content. No-op below API 33. Does not prevent other apps from reading the clipboard on any API
|
||||
* level, just affects how the system and keyboards display the clip.
|
||||
*/
|
||||
private static void markSensitive(@NonNull ClipData clip) {
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
PersistableBundle extras = new PersistableBundle();
|
||||
extras.putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true);
|
||||
|
||||
clip.getDescription().setExtras(extras);
|
||||
}
|
||||
}
|
||||
|
||||
public static int parseInt(String integer, int defaultValue) {
|
||||
try {
|
||||
return Integer.parseInt(integer);
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package org.signal.core.util
|
||||
|
||||
import android.app.AlarmManager
|
||||
import android.app.Application
|
||||
import android.content.ClipDescription
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEmpty
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isGreaterThanOrEqualTo
|
||||
import assertk.assertions.isNotNull
|
||||
import assertk.assertions.isNull
|
||||
import assertk.assertions.isTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
import org.robolectric.Shadows.shadowOf
|
||||
import org.robolectric.annotation.Config
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Verifies that secrets copied to the clipboard are flagged sensitive and scheduled for clearing, and -- just as importantly -- that
|
||||
* ordinary text is not.
|
||||
*/
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(manifest = Config.NONE, application = Application::class)
|
||||
class UtilTest_copyToClipboard {
|
||||
|
||||
private val context: Context
|
||||
get() = RuntimeEnvironment.getApplication()
|
||||
|
||||
private val clipboardManager: ClipboardManager
|
||||
get() = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
|
||||
private val alarmManager: AlarmManager
|
||||
get() = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
|
||||
@Test
|
||||
@Config(sdk = [33])
|
||||
fun `copyToClipboardSensitive - marks the clip as sensitive`() {
|
||||
Util.copyToClipboardSensitive(context, SECRET, 60)
|
||||
|
||||
val extras = clipboardManager.primaryClip!!.description.extras
|
||||
assertThat(extras).isNotNull()
|
||||
assertThat(extras!!.getBoolean(ClipDescription.EXTRA_IS_SENSITIVE)).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(sdk = [33])
|
||||
fun `copyToClipboardSensitive - still copies the text`() {
|
||||
Util.copyToClipboardSensitive(context, SECRET, 60)
|
||||
|
||||
assertThat(clipboardManager.primaryClip!!.getItemAt(0).text.toString()).isEqualTo(SECRET)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(sdk = [33])
|
||||
fun `copyToClipboardSensitive - schedules a clear alarm`() {
|
||||
val before = System.currentTimeMillis()
|
||||
|
||||
Util.copyToClipboardSensitive(context, SECRET, 60)
|
||||
|
||||
val alarms = shadowOf(alarmManager).scheduledAlarms
|
||||
assertThat(alarms.size).isEqualTo(1)
|
||||
assertThat(alarms[0].type).isEqualTo(AlarmManager.RTC_WAKEUP)
|
||||
assertThat(alarms[0].triggerAtTime).isGreaterThanOrEqualTo(before + TimeUnit.SECONDS.toMillis(60))
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(sdk = [26])
|
||||
fun `copyToClipboardSensitive - below api 33, does not set extras`() {
|
||||
Util.copyToClipboardSensitive(context, SECRET, 60)
|
||||
|
||||
assertThat(clipboardManager.primaryClip!!.description.extras).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(sdk = [26])
|
||||
fun `copyToClipboardSensitive - below api 33, still schedules a clear alarm`() {
|
||||
Util.copyToClipboardSensitive(context, SECRET, 60)
|
||||
|
||||
assertThat(shadowOf(alarmManager).scheduledAlarms.size).isEqualTo(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(sdk = [33])
|
||||
fun `copyToClipboardSensitive - default overload marks the clip sensitive and uses the default timeout`() {
|
||||
val before = System.currentTimeMillis()
|
||||
|
||||
Util.copyToClipboardSensitive(context, SECRET)
|
||||
|
||||
val extras = clipboardManager.primaryClip!!.description.extras
|
||||
assertThat(extras).isNotNull()
|
||||
assertThat(extras!!.getBoolean(ClipDescription.EXTRA_IS_SENSITIVE)).isTrue()
|
||||
|
||||
val alarms = shadowOf(alarmManager).scheduledAlarms
|
||||
assertThat(alarms.size).isEqualTo(1)
|
||||
assertThat(alarms[0].triggerAtTime).isGreaterThanOrEqualTo(before + TimeUnit.SECONDS.toMillis(Util.SENSITIVE_CLIPBOARD_TIMEOUT_SECONDS.toLong()))
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(sdk = [33])
|
||||
fun `copyToClipboard - does not mark the clip as sensitive`() {
|
||||
Util.copyToClipboard(context, NOT_A_SECRET)
|
||||
|
||||
assertThat(clipboardManager.primaryClip!!.description.extras).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(sdk = [33])
|
||||
fun `copyToClipboard - does not schedule a clear alarm`() {
|
||||
Util.copyToClipboard(context, NOT_A_SECRET)
|
||||
|
||||
assertThat(shadowOf(alarmManager).scheduledAlarms).isEmpty()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SECRET = "12345 67890 12345 67890 12345 67890"
|
||||
private const val NOT_A_SECRET = "https://signal.me/#p/+15551234567"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user