6.疑难问题

在 Android 中,由于 Android 操作系统的规范或 Android 操作系统提供的功能,应用程序实现难以保证安全。如果这些功能被恶意第三方滥用或被用户随意使用,就有导致信息泄露等安全问题的风险。本章指出了开发人员可以针对这些功能采取的风险缓解计划,并选择性地讲述了一些需要关注的主题。

6.1.剪贴板中的信息泄露风险

复制和粘贴是用户经常随意使用的功能。例如,许多用户使用这些功能将邮件或网页中需要记住的奇怪信息或重要信息存储在记事本中,或者从存储密码以防忘记的记事本中复制或粘贴密码。这些操作看起来非常非常正常,但实际上可能存在用户处理信息被盗的潜在风险。

此风险与 Android 系统中的复制和粘贴机制有关。用户或应用程序复制的信息会存储在名为剪贴板的缓冲区中。当用户或应用程序粘贴时,剪贴板中存储的信息将分发到其他应用程序。因此,此剪贴板功能存在导致信息泄露的风险。这是因为剪贴板实体在系统中仅有一个,任何应用程序都可以使用 ClipboardManager 随时获取剪贴板中存储的信息。这意味着用户复制/剪切的所有信息都有可能泄露给恶意应用程序。

因此,考虑到 Android 操作系统规范,应用程序开发人员需要采取措施将信息泄露的可能性降至最低。

6.1.1.示例代码

大致说来,有两种应对措施可降低剪贴板中的信息泄露风险。

  1. 从其他应用程序复制到您的应用程序时的应对措施。
  2. 从您的应用程序复制到其他应用程序时的应对措施。

首先,我们来讨论上面的对策 1。假设用户从记事本、Web 浏览器或邮件程序等其他应用程序复制字符串,然后将其粘贴到您的应用程序中的 EditText。事实证明,在这种情况下,没有根本对策来防止由于复制和粘贴而导致的敏感信息泄露。Android 中没有用于控制第三方应用程序的复制操作的功能。

因此,对于对策 1,除了向用户解释复制和粘贴敏感信息的风险之外,没有其他方法,只能不断提醒用户减少此类操作。

接下来要讨论是上述对策 2,假设是用户复制您的应用程序中显示的敏感信息的情形。在这种情况下,防止信息泄漏的有效办法是禁止从视图(TextView、EditText 等)中执行复制/剪切操作。如果视图中没有输入/输出敏感信息(如个人信息)的复制/剪切功能,那么在您的应用程序就永远不会发生因剪贴板引起的信息泄露。

有几种方法可以禁止复印/剪切。本节描述了简单有效的几种方法:一种方法是禁用长按视图,另一种方法是在选择字符串时从菜单中删除复制/剪切项目。

根据图 6.1.1 可确定必要的对策。图 6.1.1 “输入类型固定为密码属性”表示应用程序运行时,输入类型必须是以下三种之一。在这种情况下,不需要应对措施,因为默认情况下禁止复制/剪切。

  • InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD
  • InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD
  • InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD
_images/image86.png

图 6.1.1 是否需要应对措施的决策流程

以下小节详细介绍了每个应对措施,并提供了示例代码。

6.1.1.1.选择字符串时从菜单中删除“复制/剪切”

通过 TextView.setCustomSelectionActionMODECallback() 方法,可以自定义选择字符串时的菜单。通过使用此功能,如果在选择字符串时可以从菜单中删除复制/剪切项目,用户将无法再复制/剪切字符串。

用于从 EditText 字符串选择的菜单中删除复制/剪切项目的示例代码,如下所示。

要点:

  1. 从字符串选择菜单中删除 android.R.id.copy。
  2. 从字符串选择菜单中删除 android.R.id.cut。
UncopyableActivity.java
package org.jssec.android.clipboard.leakage;

import android.app.Activity;
import android.os.Bundle;
import android.support.v4.app.NavUtils;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.EditText;

public class UncopyableActivity extends Activity {
    private EditText copyableEdit;
    private EditText uncopyableEdit;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.uncopyable);

        copyableEdit = (EditText) findViewById(R.id.copyable_edit);
        uncopyableEdit = (EditText) findViewById(R.id.uncopyable_edit);
        // By setCustomSelectionActionMODECallback method,
        // Possible to customize menu of character string selection.
        uncopyableEdit.setCustomSelectionActionModeCallback(actionModeCallback);
    }

    private ActionMode.Callback actionModeCallback = new ActionMode.Callback() {
        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
            return false;
        }

        public void onDestroyActionMode(ActionMode mode) {
        }

        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
            // *** POINT 1 *** Delete android.R.id.copy from the menu of character string selection.
            MenuItem itemCopy = menu.findItem(android.R.id.copy);
            if (itemCopy != null) {
                menu.removeItem(android.R.id.copy);
            }
            // *** POINT 2 *** Delete android.R.id.cut from the menu of character string selection.
            MenuItem itemCut = menu.findItem(android.R.id.cut);
            if (itemCut != null) {
                menu.removeItem(android.R.id.cut);
            }
            return true;
        }

        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
            return false;
        }
    };

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.uncopyable, menu);
        return true;
    }


    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case android.R.id.home: 
                NavUtils.navigateUpFromSameTask(this);
                return true;
        }
        return super.onOptionsItemSelected(item);
    }

}

6.1.1.2.禁用长按视图

通过禁用长按视图也可禁止复制/剪切。可以在 xml 布局文件中指定禁用长按视图。

要点:

  1. 在视图中将 android:longClickable 设为 false 以禁止复制/剪切。
unlongclickable.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/unlongclickable_description" />

    <!-- EditText to prohibit copy/cut EditText -->
    <!-- *** POINT 1 *** Set false to android:longClickable in View to prohibit copy/cut.-->
    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:longClickable="false"
        android:hint="@string/unlongclickable_hint" />
</LinearLayout>

6.1.2.规则手册

将敏感信息从您的应用程序复制到其他应用程序时,请遵循以下规则。

  1. 禁用视图中显示的复制/剪切字符串功能(必需)

6.1.2.1.禁用视图中显示的复制/剪切字符串功能(必需)

如果视图在应用程序中显示敏感信息,并且允许在视图(例如 EditText)中复制/剪切信息,则信息可能通过剪贴板泄露。因此,必须在显示敏感信息的视图中禁用复制/剪切。

有两种方法可禁用复制/剪切。一种方法是从字符串选择菜单中删除复制/剪切的项目,另一种方法是禁用长按视图。

请参阅“6.1.3.1.应用规则时的注意事项”。

6.1.3.高级主题

6.1.3.1.应用规则时的预防措施

在 TextView 中,选择字符串不可能为默认值,因此通常不需要应对措施,但在某些情况下,复制内容可能为默认值,取决于应用程序的规格。选择/复制字符串的可能性可通过使用 TextView.setTextIsSelectable() 方法动态确定。在 TextView 中能设置复制时,调查 TextView 中是否有可能显示任何敏感信息,如果有任何可能,则不应将其设置为可复制。

此外,“ 6.1.1.示例代码”描述了关于输入类型(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD 等)EditText 的决策流程,通常不需要任何应对措施,因为默认情况下禁止复制字符串。但是,如“ 5.1.2.2.提供以纯文本显示密码的选项(必需)”中所述,如果准备了[以纯文本格式显示密码]选项,则在以纯文本格式显示密码时,输入类型将更改并启用复制/剪切。因此,此时需要相同的应对措施。

请注意,在应用规则时,开发人员还应考虑应用程序的可用性。例如,对于用户可以自由输入文本的视图,如果由于输入敏感信息的可能性很小而禁用了复制/剪切,用户可能会感到不便。当然,该规则应无条件地应用于处理高度重要的信息或独立敏感信息的视图,但如果是其他类型的视图,以下问题将帮助开发人员了解如何正确处理视图。

  • 准备一些其他组件专门用于敏感信息
  • 在粘贴到应用程序选项显而易见时使用备用方法发送信息
  • 提醒用户了解有关输入/输出信息的注意事项
  • 重新考虑视图的必要性

信息泄露风险的根本原因是 Android 操作系统中剪贴板和 ClipboardManager 的规范并未考虑安全风险。应用程序开发人员需要从用户完整性、可用性、功能等方面出发,创建更高质量的应用程序。

6.1.3.2.剪贴板中存储的操作信息

如“6.1.剪贴板中的信息泄露风险”所述,应用程序可以使用 ClipboardManager 处理剪贴板中存储的信息。此外,无需为使用 ClipboardManager 设置特定权限,因此应用程序无需用户识别即可使用 ClipboardManager。

剪贴板中存储的信息,称为 ClipData,可通过 ClipboardManager.getPrimaryClip() 方法获取。如果通过 ClipboardManager.addPrimaryClipChangedListener() 方法实施 OnPrimaryClipChangedListener 注册 ClipboardManager,则每次用户执行复制/剪切操作时都会调用监听器。因此,无需忽略定时,即可获得 ClipData。在任何应用程序中执行复制/剪切操作时,都将执行监听器调用。

下面显示了服务的源代码,只要在设备中执行复制/剪切,就会获取 ClipData,并通过 Toast 显示。您可以看到存储在剪贴板中的信息因为下面所示的简单代码而被泄露。需要注意的是,请勿让敏感信息被以下源代码获取。

ClipboardListeningService.java
package org.jssec.android.clipboard;

import android.app.Service;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.ClipboardManager.OnPrimaryClipChangedListener;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;
import android.widget.Toast;

public class ClipboardListeningService extends Service {
    private static final String TAG = "ClipboardListeningService";
    private ClipboardManager mClipboardManager;

    @Override
    public IBinder onBind(Intent arg0) {
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        mClipboardManager = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
        if (mClipboardManager != null) {
            mClipboardManager.addPrimaryClipChangedListener(clipListener);
        } else {
            Log.e(TAG, "Failed to get ClipboardService .Service is closed.");
            this.stopSelf();
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (mClipboardManager != null) {
            mClipboardManager.removePrimaryClipChangedListener(clipListener);
        }
    }

    private OnPrimaryClipChangedListener clipListener = new OnPrimaryClipChangedListener() {
        public void onPrimaryClipChanged() {
            if (mClipboardManager != null && mClipboardManager.hasPrimaryClip()) {
                ClipData data = mClipboardManager.getPrimaryClip();
                ClipData.Item item = data.getItemAt(0);
                Toast
                    .makeText(
                        getApplicationContext(),
                        "Character stirng that is copied or cut:\n"
                            + item.coerceToText(getApplicationContext()),
                        Toast.LENGTH_SHORT)
                    .show();
            }
        }
    };
}

接下来的内容展示的是上述使用 ClipboardListeningService 的活动的代码示例。

ClipboardListeningActivity.java
package org.jssec.android.clipboard;

import android.app.Activity;
import android.content.ComponentName;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;

public class ClipboardListeningActivity extends Activity {
    private static final String TAG = "ClipboardListeningActivity";

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_clipboard_listening);
    }

    public void onClickStartService(View view) {
        if (view.getId() != R.id.start_service_button) {
            Log.w(TAG, "View ID is incorrect.");
        } else {
            ComponentName cn = startService(
                new Intent(ClipboardListeningActivity.this, ClipboardListeningService.class));
            if (cn == null) {
                Log.e(TAG, "Failed to launch the service.");
            }
        }
    }

    public void onClickStopService(View view) {
        if (view.getId() != R.id.stop_service_button) {
            Log.w(TAG, "View ID is incorrect.");
        } else {
            stopService(new Intent(ClipboardListeningActivity.this, ClipboardListeningService.class));
        }
    }
}

到目前为止,我们已经介绍了获取存储在剪贴板上的数据的方法。也可以使用 ClipboardManager.setPrimaryClip() 方法将新数据存储在剪贴板上。

请注意,setPrimaryClip() 方法将覆盖剪贴板中存储的信息,因此用户复制/剪切存储的信息可能会丢失。当使用这些方法提供自定义复制/剪切功能时,有必要进行设计/执行,以防止将剪贴板中存储的内容更改为非预期内容,方法是根据需要显示一个对话框以通知内容将被更改。