5.1 创建密码输入界面

5.1.1 示例代码

创建密码输入界面时,这里描述了安全性方面需要考虑的一些要点。 这里仅提及与密码输入有关的内容。 对于如何保存密码,未来会发布另一篇文章。

要点:

  1. 输入的密码应该被屏蔽显示(用*显示)

  2. 提供以纯文本显示密码的选项。

  3. 警告用户以纯文本显示密码有风险。

要点:处理最后输入的密码时,请注意以下几点以及上述要点。

  1. 如果在初始界面中有最后输入的密码,则将黑点的固定数字显示为虚拟,以便不会猜到最后的密码的数字。

  2. 当显示虚拟密码,并按下“显示密码”按钮时,清除最后输入的密码并提供输入新密码的状态。

  3. 当最后输入的密码显示为虚拟时,如果用户尝试输入密码,请清除最后输入的密码,并将新的用户输入视为新密码。

password_activity.xml

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:layout_width="fill_parent"
  4. android:layout_height="fill_parent"
  5. android:orientation="vertical"
  6. android:padding="10dp" >
  7. <!-- Label for password item -->
  8. <TextView
  9. android:layout_width="fill_parent"
  10. android:layout_height="wrap_content"
  11. android:text="@string/password" />
  12. <!-- Label for password item -->
  13. <!-- *** POINT 1 *** The input password must be masked (Display with black dot) -->
  14. <EditText
  15. android:id="@+id/password_edit"
  16. android:layout_width="fill_parent"
  17. android:layout_height="wrap_content"
  18. android:hint="@string/hint_password"
  19. android:inputType="textPassword" />
  20. <!-- *** POINT 2 *** Provide the option to display the password in a plain text -->
  21. <CheckBox
  22. android:id="@+id/password_display_check"
  23. android:layout_width="fill_parent"
  24. android:layout_height="wrap_content"
  25. android:text="@string/display_password" />
  26. <!-- *** POINT 3 *** Alert a user that displaying password in a plain text has a risk. -->
  27. <TextView
  28. android:layout_width="fill_parent"
  29. android:layout_height="wrap_content"
  30. android:text="@string/alert_password" />
  31. <!-- Cancel/OK button -->
  32. <LinearLayout
  33. android:layout_width="fill_parent"
  34. android:layout_height="wrap_content"
  35. android:layout_marginTop="50dp"
  36. android:gravity="center"
  37. android:orientation="horizontal" >
  38. <Button
  39. android:layout_width="0dp"
  40. android:layout_height="wrap_content"
  41. android:layout_weight="1"
  42. android:onClick="onClickCancelButton"
  43. android:text="@android:string/cancel" />
  44. <Button
  45. android:layout_width="0dp"
  46. android:layout_height="wrap_content"
  47. android:layout_weight="1"
  48. android:onClick="onClickOkButton"
  49. android:text="@android:string/ok" />
  50. </LinearLayout>
  51. </LinearLayout>

位于PasswordActivity.java底部的 3 个方法的实现,应该取决于目的而调整。

  • private String getPreviousPassword()
  • private void onClickCancelButton(View view)
  • private void onClickOkButton(View view)

PasswordActivity.java

  1. package org.jssec.android.password.passwordinputui;
  2. import android.app.Activity;
  3. import android.os.Bundle;
  4. import android.text.Editable;
  5. import android.text.InputType;
  6. import android.text.TextWatcher;
  7. import android.view.View;
  8. import android.view.WindowManager;
  9. import android.widget.CheckBox;
  10. import android.widget.CompoundButton;
  11. import android.widget.CompoundButton.OnCheckedChangeListener;
  12. import android.widget.EditText;
  13. import android.widget.Toast;
  14. public class PasswordActivity extends Activity {
  15. // Key to save the state
  16. private static final String KEY_DUMMY_PASSWORD = "KEY_DUMMY_PASSWORD";
  17. // View inside Activity
  18. private EditText mPasswordEdit;
  19. private CheckBox mPasswordDisplayCheck;
  20. // Flag to show whether password is dummy display or not
  21. private boolean mIsDummyPassword;
  22. @Override
  23. public void onCreate(Bundle savedInstanceState) {
  24. super.onCreate(savedInstanceState);
  25. setContentView(R.layout.password_activity);
  26. // Set Disabling Screen Capture
  27. getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
  28. // Get View
  29. mPasswordEdit = (EditText) findViewById(R.id.password_edit);
  30. mPasswordDisplayCheck = (CheckBox) findViewById(R.id.password_display_check);
  31. // Whether last Input password exist or not.
  32. if (getPreviousPassword() != null) {
  33. // *** POINT 4 *** In the case there is the last input password in an initial display,
  34. // display the fixed digit numbers of black dot as dummy in order not that the digits number of last password is guessed.
  35. // Display should be dummy password.
  36. mPasswordEdit.setText("**********");
  37. // To clear the dummy password when inputting password, set text change listener.
  38. mPasswordEdit.addTextChangedListener(new PasswordEditTextWatcher());
  39. // Set dummy password flag
  40. mIsDummyPassword = true;
  41. }
  42. // Set a listner to change check state of password display option.
  43. mPasswordDisplayCheck
  44. .setOnCheckedChangeListener(new OnPasswordDisplayCheckedChangeListener());
  45. }
  46. @Override
  47. public void onSaveInstanceState(Bundle outState) {
  48. super.onSaveInstanceState(outState);
  49. // Unnecessary when specifying not to regenerate Activity by the change in screen aspect ratio.
  50. // Save Activity state
  51. outState.putBoolean(KEY_DUMMY_PASSWORD, mIsDummyPassword);
  52. }
  53. @Override
  54. public void onRestoreInstanceState(Bundle savedInstanceState) {
  55. super.onRestoreInstanceState(savedInstanceState);
  56. // Unnecessary when specifying not to regenerate Activity by the change in screen aspect ratio.
  57. // Restore Activity state
  58. mIsDummyPassword = savedInstanceState.getBoolean(KEY_DUMMY_PASSWORD);
  59. }
  60. /**
  61. * Process in case password is input
  62. */
  63. private class PasswordEditTextWatcher implements TextWatcher {
  64. public void beforeTextChanged(CharSequence s, int start, int count, int after) {
  65. // Not used
  66. }
  67. public void onTextChanged(CharSequence s, int start, int before, int count) {
  68. // *** POINT 6 *** When last Input password is displayed as dummy, in the case an user tries to input password,
  69. // Clear the last Input password, and treat new user input as new password.
  70. if (mIsDummyPassword) {
  71. // Set dummy password flag
  72. mIsDummyPassword = false;
  73. // Trim space
  74. CharSequence work = s.subSequence(start, start + count);
  75. mPasswordEdit.setText(work);
  76. // Cursor position goes back the beginning, so bring it at the end.
  77. mPasswordEdit.setSelection(work.length());
  78. }
  79. }
  80. public void afterTextChanged(Editable s) {
  81. // Not used
  82. }
  83. }
  84. /**
  85. * Process when check of password display option is changed.
  86. */
  87. private class OnPasswordDisplayCheckedChangeListener implements OnCheckedChangeListener {
  88. public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
  89. // *** POINT 5 *** When the dummy password is displayed and the "Show password" button is pressed,
  90. // clear the last input password and provide the state for new password input.
  91. if (mIsDummyPassword && isChecked) {
  92. // Set dummy password flag
  93. mIsDummyPassword = false;
  94. // Set password empty
  95. mPasswordEdit.setText(null);
  96. }
  97. // Cursor position goes back the beginning, so memorize the current cursor position.
  98. int pos = mPasswordEdit.getSelectionStart();
  99. // *** POINT 2 *** Provide the option to display the password in a plain text
  100. // Create InputType
  101. int type = InputType.TYPE_CLASS_TEXT;
  102. if (isChecked) {
  103. // Plain display when check is ON.
  104. type |= InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD;
  105. } else {
  106. // Masked display when check is OFF.
  107. type |= InputType.TYPE_TEXT_VARIATION_PASSWORD;
  108. }
  109. // Set InputType to password EditText
  110. mPasswordEdit.setInputType(type);
  111. // Set cursor position
  112. mPasswordEdit.setSelection(pos);
  113. }
  114. }
  115. // Implement the following method depends on application
  116. /**
  117. * Get the last Input password
  118. *
  119. * @return Last Input password
  120. */
  121. private String getPreviousPassword() {
  122. // When need to restore the saved password, return password character string
  123. // For the case password is not saved, return null
  124. return "hirake5ma";
  125. }
  126. /**
  127. * Process when cancel button is clicked
  128. *
  129. * @param view
  130. */
  131. public void onClickCancelButton(View view) {
  132. // Close Activity
  133. finish();
  134. }
  135. /**
  136. * Process when OK button is clicked
  137. *
  138. * @param view
  139. */
  140. public void onClickOkButton(View view) {
  141. // Execute necessary processes like saving password or using for authentication
  142. String password = null;
  143. if (mIsDummyPassword) {
  144. // When dummy password is displayed till the final moment, grant last iInput password as fixed password.
  145. password = getPreviousPassword();
  146. } else {
  147. // In case of not dummy password display, grant the user input password as fixed password.
  148. password = mPasswordEdit.getText().toString();
  149. }
  150. // Display password by Toast
  151. Toast.makeText(this, "password is ¥"" + password + "¥"", Toast.LENGTH_SHORT).show();
  152. // Close Activity
  153. finish();
  154. }
  155. }

5.1.2 规则书

实现密码输入界面时,遵循以下规则。

5.1.2.1 如果输入了密码,提供屏蔽显示功能(必需)

智能手机通常用在火车或公共汽车等拥挤的地方,而且存在密码被某人偷窥的风险。 因此,屏蔽显示密码的功能是应用规范所必需的。

有两种方法可以将EditText显示为密码:在布局 XML 中静态指定此值,或通过从程序中切换显示来动态指定此值。 前者通过为android:inputType属性指定textPassword或使用android:password属性来实现。 后者通过使用EditText类的setInputType()方法,将InputType.TYPE_TEXT_VARIATION_PASSWORD添加到其输入类型,来实现的。

下面展示了每个的示例代码。

在布局 XML 中屏蔽密码。

password_activity.xml

  1. <!—Password input item -->
  2. <!—Set true for the android:password attribute -->
  3. <EditText
  4. android:id="@+id/password_edit"
  5. android:layout_width="fill_parent"
  6. android:layout_height="wrap_content"
  7. android:hint="@string/hint_password"
  8. android:password="true" />

在活动中屏蔽密码。

PasswordActivity.java

  1. // Set password display type
  2. // Set TYPE_TEXT_VARIATION_PASSWORD for InputType.
  3. EditText passwordEdit = (EditText) findViewById(R.id.password_edit);
  4. int type = InputType.TYPE_CLASS_TEXT
  5. | InputType.TYPE_TEXT_VARIATION_PASSWORD;
  6. passwordEdit.setInputType(type);

5.1.2.2 提供以纯文本展示密码的选项(必需)

智能手机的密码输入通过触摸面板输入完成,因此与 PC 上的键盘输入相比,容易发生误输入。由于输入不便,用户可能会使用简单的密码,这样做会更危险。此外,当有多次密码输入失败导致帐户锁定等机制时,必须尽可能避免误输入。作为这些问题的解决方案,通过准备以纯文本显示密码的选项,用户可以使用安全密码。

但是,以纯文本显示密码时,可能会被嗅探,所以使用此选项时。有必要提醒用户注意来自后面的嗅探。此外,如果存在以纯文本显示的选项,则还需要为系统准备,来自动取消纯文本显示,如设置纯文本显示的时间。密码纯文本显示的限制,在未来版本的另一篇文章中发布。因此,密码纯文本显示的限制不包含在示例代码中。

5.1.md - 图1

通过指定EditTextInputType,可以切换屏蔽显示和纯文本显示。

PasswordActivity.java

  1. /**
  2. * Process when check of password display option is changed.
  3. */
  4. private class OnPasswordDisplayCheckedChangeListener implements
  5. OnCheckedChangeListener {
  6. public void onCheckedChanged(CompoundButton buttonView,
  7. boolean isChecked) {
  8. // *** POINT 5 *** When the dummy password is displayed and the "Show password" button is pr
  9. essed,
  10. // Clear the last input password and provide the state for new password input.
  11. if (mIsDummyPassword && isChecked) {
  12. // Set dummy password flag
  13. mIsDummyPassword = false;
  14. // Set password empty
  15. mPasswordEdit.setText(null);
  16. }
  17. // Cursor position goes back the beginning, so memorize the current cursor position.
  18. int pos = mPasswordEdit.getSelectionStart();
  19. // *** POINT 2 *** Provide the option to display the password in a plain text
  20. // Create InputType
  21. int type = InputType.TYPE_CLASS_TEXT;
  22. if (isChecked) {
  23. // Plain display when check is ON.
  24. type |= InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD;
  25. } else {
  26. // Masked display when check is OFF.
  27. type |= InputType.TYPE_TEXT_VARIATION_PASSWORD;
  28. }
  29. // Set InputType to password EditText
  30. mPasswordEdit.setInputType(type);
  31. // Set cursor position
  32. mPasswordEdit.setSelection(pos);
  33. }
  34. }

5.1.2.3 活动加载时屏蔽密码(必需)

为防止密码被偷窥,当活动启动时,密码显示选项的默认值应该设置为OFF。 基本上,默认值应该总是定义为更安全的一方。

5.1.2.4 显示最后输入密码时,必须显示虚拟密码(必需)

当指定最后输入的密码时,不要给第三方任何密码提示,它应该显示为带有屏蔽字符(*等)的固定位数的虚拟值。 另外,在虚拟显示时按下“显示密码”的情况下,清除密码并切换到纯文本显示模式。 它有助于防止最后输入的密码被嗅探的风险,即使设备被传递给第三方,比如它被盗时。 仅供参考,在虚拟显示的情况下以及用户尝试输入密码时,应取消虚拟显示,需要变成正常输入状态。

显示最后输入的密码时,显示虚拟密码。

PasswordActivity.java

  1. public void onCreate(Bundle savedInstanceState) {
  2. super.onCreate(savedInstanceState);
  3. setContentView(R.layout.password_activity);
  4. // Get View
  5. mPasswordEdit = (EditText) findViewById(R.id.password_edit);
  6. mPasswordDisplayCheck = (CheckBox) findViewById(R.id.password_display_check);
  7. // Whether last Input password exist or not.
  8. if (getPreviousPassword() != null) {
  9. // *** POINT 4 *** In the case there is the last input password in an initial display,
  10. // display the fixed digit numbers of black dot as dummy in order not that the digits number of last password is guessed.
  11. // Display should be dummy password.
  12. mPasswordEdit.setText("**********");
  13. // To clear the dummy password when inputting password, set text change listener.
  14. mPasswordEdit.addTextChangedListener(new PasswordEditTextWatcher());
  15. // Set dummy password flag
  16. mIsDummyPassword = true;
  17. }
  18. [...]
  19. }
  20. /**
  21. * Get the last input password.
  22. *
  23. * @return the last input password
  24. */
  25. private String getPreviousPassword() {
  26. // To restore the saved password, return the password character string.
  27. // For the case password is not saved, return null.
  28. return "hirake5ma";
  29. }

在虚拟显示的情况下,当密码显示选项打开时,请清除显示的内容。

PasswordActivity.java

  1. /**
  2. * Process when check of password display option is changed.
  3. */
  4. private class OnPasswordDisplayCheckedChangeListener implements
  5. OnCheckedChangeListener {
  6. public void onCheckedChanged(CompoundButton buttonView,
  7. boolean isChecked) {
  8. // *** POINT 5 *** When the dummy password is displayed and the "Show password" button is pressed,
  9. // Clear the last input password and provide the state for new password input.
  10. if (mIsDummyPassword && isChecked) {
  11. // Set dummy password flag
  12. mIsDummyPassword = false;
  13. // Set password empty
  14. mPasswordEdit.setText(null);
  15. }
  16. [...]
  17. }
  18. }

在虚拟显示的情况下,当用户尝试输入密码时,清除虚拟显示。

PasswordActivity.java

  1. // Key to save the state
  2. private static final String KEY_DUMMY_PASSWORD = "KEY_DUMMY_PASSWORD";
  3. [...]
  4. // Flag to show whether password is dummy display or not.
  5. private boolean mIsDummyPassword;
  6. @Override
  7. public void onCreate(Bundle savedInstanceState) {
  8. [...]
  9. // Whether last Input password exist or not.
  10. if (getPreviousPassword() != null) {
  11. // *** POINT 4 *** In the case there is the last input password in an initial display,
  12. // display the fixed digit numbers of black dot as dummy in order not that the digits number of last password is guessed.
  13. // Display should be dummy password.
  14. mPasswordEdit.setText("**********");
  15. // To clear the dummy password when inputting password, set text change listener.
  16. mPasswordEdit.addTextChangedListener(new PasswordEditTextWatcher());
  17. // Set dummy password flag
  18. mIsDummyPassword = true;
  19. }
  20. [...]
  21. }
  22. @Override
  23. public void onSaveInstanceState(Bundle outState) {
  24. super.onSaveInstanceState(outState);
  25. // Unnecessary when specifying not to regenerate Activity by the change in screen aspect ratio.
  26. // Save Activity state
  27. outState.putBoolean(KEY_DUMMY_PASSWORD, mIsDummyPassword);
  28. }
  29. @Override
  30. public void onRestoreInstanceState(Bundle savedInstanceState) {
  31. super.onRestoreInstanceState(savedInstanceState);
  32. // Unnecessary when specifying not to regenerate Activity by the change in screen aspect ratio.
  33. // Restore Activity state
  34. mIsDummyPassword = savedInstanceState.getBoolean(KEY_DUMMY_PASSWORD);
  35. }
  36. /**
  37. * Process when inputting password.
  38. */
  39. private class PasswordEditTextWatcher implements TextWatcher {
  40. public void beforeTextChanged(CharSequence s, int start, int count,
  41. int after) {
  42. // Not used
  43. }
  44. public void onTextChanged(CharSequence s, int start, int before,
  45. int count) {
  46. // *** POINT 6 *** When last Input password is displayed as dummy, in the case an user tries to input password,
  47. // Clear the last Input password, and treat new user input as new password.
  48. if (mIsDummyPassword) {
  49. // Set dummy password flag
  50. mIsDummyPassword = false;
  51. // Trim space
  52. CharSequence work = s.subSequence(start, start + count);
  53. mPasswordEdit.setText(work);
  54. // Cursor position goes back the beginning, so bring it at the end.
  55. mPasswordEdit.setSelection(work.length());
  56. }
  57. }
  58. public void afterTextChanged(Editable s) {
  59. // Not used
  60. }
  61. }

5.1.3 高级话题

5.1.3.1 登录过程

需要密码输入的代表性示例是登录过程。以下是一些在登录过程中需要注意的事项。

登录失败时的错误信息

在登录过程中,需要输入两个信息,ID(账号)和密码。 登录失败时有两种情况。 一个是 ID 不存在。 另一个是 ID 存在,但密码不正确。 如果这两种情况中的任何一种,有所区分并显示在登录失败消息中,则攻击者可以猜测指定的 ID 是否存在。 为了阻止这种猜测,这两种情况不应该在登录失败消息中区分,并且该消息应该按照下面的方式显示。

消息示例:登录 ID 或密码不正确。

自动登录功能

存在一个功能,可以完成成功登录过程一次后,通过省略下次登录的 ID /密码输入来执行自动登录。自动登录功能可以省去复杂的输入。因此,便利性会增加,但另一方面,当智能手机被盗时,第三方恶意使用的风险将随之而来。

只有在恶意第三方造成的损害可以接受时,或者只有在可以采取足够安全措施的情况下,才能使用自动登录功能。例如,在网上银行应用的情况下,当设备由第三方运营时,可能会造成财务损失。所以在这种情况下,与自动登录功能配套的安全措施是必需的。存在一些可能的应对措施,例如【在付款过程等财务流程前需要重新输入密码】,【设置自动登录时,请求用户注意并提示用户锁定设备】等。使用自动登录时,有必要仔细考虑方便性和风险以及假定的对策。

5.1.3.2 修改密码

更改曾经设置的密码时,应在屏幕上准备以下输入项目。

  • 当前密码
  • 新密码
  • 新密码(确认)

当引入自动登录功能时,第三方可能使用应用。 在这种情况下,为了避免意外更改密码,需要输入当前的密码。 另外,为了减少由于错误输入新密码,而进入不可用状态的风险,有必要要求输入两次新的密码。

5.1.3.3 关于“使密码可见”设置

Android 设置菜单中有一个名为“使密码可见”的设置。 在 Android 4.4 的情况下,如下所示。

  1. 设置 -> 安全 -> 使密码可见

5.1.md - 图2

打开“使密码可见”设置时,最后输入的字符以纯文本显示。 经过一定的时间(约两秒),或输入下一个字符后,以纯文本显示的字符将被屏蔽。 关闭时,输入后会立即屏蔽。 此设置影响整个系统,并且它适用于使用EditText的密码显示功能的所有应用。

5.1.md - 图3

5.1.3.4 禁用屏幕截图

在密码输入屏幕中,密码可以在屏幕上清晰显示。 在处理个人信息的屏幕中,如果屏幕截图功能在默认情况下处于启用状态,则可能会从屏幕截图文件中泄漏,它存储在外部存储器上。因此建议对密码输入屏幕禁用屏幕截图功能。 通过附加下面的代码可以禁用屏幕截图。

PasswordActivity.java

  1. @Override
  2. public void onCreate(Bundle saveInstanceState) {
  3. [...]
  4. Window window = getWindow();
  5. window.addFlags(WindowManager.LayoutParams.FLAG_SECURE);
  6. setContentView(R.layout.passwordInputScreen);
  7. [...]
  8. }