在Android应用中存储用户名和密码的最佳选项是什么?

50

我正在开发一款Android应用程序,用户需要登录才能执行操作。但在大多数情况下,人们使用"保持登录状态"功能,这时我需要在我的应用程序中维护用户名密码的值。我应该使用SharedPreferences还是SQLite数据库,或者有其他更好的方法。
如何确保它的安全性呢?


这个是正确的:https://dev59.com/WWIk5IYBdhLWcg3wDaUW#19629701 - VahidHoseini
@VahidHoseini 那个回答目前得分为-4,绝不安全。 - Simon Forsberg
10个回答

36

在安卓上,这很棘手。你不想在偏好设置中存储明文密码,因为任何拥有root权限设备的人基本上都会向全世界显示他们的密码。另一方面,你不能使用加密密码,因为你必须在设备上某处存储加密/解密密钥,同样容易遭受root攻击。

我以前用过的一个解决方法是让服务器生成一个“票证”,将其传回设备,有效期间内都可以使用。该票证由设备用于所有通信,当然要使用SSL,以防止人们窃取你的票证。这样,用户只需在服务器上验证密码一次,服务器就会返回一个过期的票证,而密码则不会存储在设备上的任何地方。

几个三脚认证机制,如OpenID、Facebook甚至Google APIs,都使用此机制。缺点是每隔一段时间,当票证过期时,用户需要重新登录。

最终,这取决于你希望你的应用程序有多安全。如果这只是为了区分用户,并没有像银行账户或血型之类的超级机密信息被存储,那么在设备上以纯文本方式保存密码也是可以的:)

祝你好运,无论你决定采用哪种方法都是最适合你特定情况的!

编辑:我应该注意到这种技术将安全责任转移到服务器——你需要在服务器上使用盐散列进行密码比较,这个想法在此问题的一些其他评论中也可以看到。这样可以防止明文密码出现在设备上的EditText视图、与服务器的SSL通信以及服务器的RAM中(用于盐散列密码)。它不会存储在磁盘上,这是一件好事™。


6
从技术角度来看,我同意你所说的一切。但我觉得,你公开你的血型,就越有可能在紧急情况下得到拯救 ;) - Dirty Henry
5
数据不是绝密的并不重要,因为用户可能在许多地方使用相同的用户名和密码,攻击者可以获得这些信息并尝试在不同的服务上使用。 - Fernando Gallego
1
我应该指出,这种技术将安全责任转移给服务器。实际上,无论您是否使用此技术,服务器已经拥有了这个责任,因为它需要处理登录。 - RobCo
截至2022年,EncryptedSharedPreferences似乎是一个不错的选择。 - Simon Forsberg

27

正如其他人所说,在Android中没有一种安全的方式来完全保护数据存储密码。哈希/加密密码是个好主意,但它只会减慢“破解者”的速度。

在这种情况下,我做了以下操作:

1)我使用了这个simplecryto.java,需要提供一个 seed 和文本来进行加密。 2)我在私有模式下使用了 SharedPreferences 来保护保存文件,可以在非root设备上使用。 3)我用于 simplecryto 的种子是一组字节,相对于字符串来说更难以被反编译工具找到。

我的应用程序最近被我公司聘请的“白帽”安全小组进行了审核。他们标记了这个问题,并指出我应该使用OAUTH,但他们也将其列为低风险问题,这意味着它不是很好,但不足以阻止发布。

请记住,“破解者”需要物理访问设备并对其进行root操作,并且还要关心找到种子。

如果您真的关心安全,请不要提供“保持登录状态”的选项。


教程链接已损坏,所以我不确定是否相同,但是 Google 立即给我展示了一篇看起来非常类似的文章:http://www.androidsnippets.com/encryptdecrypt-strings,但我想到您可以使用随机生成的种子并将其存储在设备外以增加安全性。这个共享的秘密/种子将会对每个用户随机生成,并且设备外的存储将包含映射。只要您与该位置安全地交换信息,并且该存储是安全的,那么这样做不会更好吗? - batbrat
1
如果您要拥有服务器端持久的用户状态,建议使用oauth2而不是自己编写的东西。在我的示例中,没有用户状态,因此我没有可用的内容可以调用。 - knaak
谢谢您的澄清。我现在理解得更好了,并且同意。 - batbrat
我非常怀疑OAuth能否确保安全,最多只能增加破解的难度,最坏的情况是可能会因其自身的漏洞而使您处于暴露的状态。当攻击者拥有与您的应用程序和源代码相同的数据时,要确保其安全是不可能的。 - Velizar Hristov
不同的链接无法使用。请改用此链接:https://github.com/Medisana/Android-Standalone/blob/master/app/src/main/java/com/example/miguel/myapplication/service/SimpleCrypto.java - Jesper

7

至少,将其存储在SharedPreferences(私有模式)中,并记得对密码进行哈希处理。虽然这不会对恶意用户(或已获取root权限的设备)产生实质性影响,但仍然是必要的。


1
有没有关于如何哈希密码的教程?我对这个想法很感兴趣。使用这种技术,没有人可以检索存储的密码吗? - androniennn
3
@androniennn,有很多关于如何哈希密码的教程,我会让你自己去谷歌。需要记住的一件事是,哈希只是为了阻止“随便”窥探者。当设备被获取root权限(例如),恶意用户也能够访问你的二进制文件时,这种技术没有任何作用。想想看,如果有人可以反向工程你的程序,那么他们就可以轻松地弄清楚你是如何哈希密码的。你只需要意识到这些可能性就行了 :) - Marvin Pinto
在SharedPreferences中存储的数据如果我的设备重新启动,就会被清除。这只是发生在我身上吗? - Gnanam R
1
为什么不使用像Sha-1这样的单向哈希,然后您只需要检查哈希是否匹配,而不是解密明文是否匹配。 - Tyler
@styler1972,这与使用服务器令牌相同,1-way哈希可以被反转(即使不容易,但仍然可能),1-way哈希令牌是最佳安全选项。即使有人花费时间和精力生成与哈希令牌匹配的数据,那时它已经过期了,如果他找到了您的密码,如果是银行账户,那么就要告别您的钱财了。 令牌很好,哈希令牌更好。即使哈希,也永远不要保留密码。 但是,如上所述,如果数据不敏感,则由您决定。 - bakriawad

6
你可以使用Jetpack安全库中的EncryptedSharedPreferences。它非常适合键值类型的设置。
它包装了SharedPreferences,提供安全的加密/解密功能,同时保持与SharedPreferences相同的API。
就像他们的示例中所示:
  String masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC);

  SharedPreferences sharedPreferences = EncryptedSharedPreferences.create(
      "secret_shared_prefs",
      masterKeyAlias,
      context,
      EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
      EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
  );

  // use the shared preferences and editor as you normally would
  SharedPreferences.Editor editor = sharedPreferences.edit();

设备被盗或者被root后,是否有可能泄露数据? - Girish
很不可能,@Girish。如果它配备了硬件加密设备,那么它真的非常困难,几乎是不可能的。 - Climax
此外,您可以使用密钥认证来确定设备仍然值得信任的额外保障级别(至少对于Google而言)。 - Climax
请问您能否举一个密钥认证的例子? - Girish
@Girish,我无法比文档更好地表述:https://developer.android.com/training/articles/security-key-attestation - Climax
警告 如果您正在开发一个系统应用程序(_sharedUserId="android.uid.system"_),请绝对避免使用它,原因在这里:https://stackoverflow.com/questions/66988384/does-android-keystore-and-clear-data-action-of-the-settings-app-are-related - Max_Payne

2

我想把密码保存在SharedPreferences中,所以我先私下实现了以下代码:

public class PrefManager {

  private SharedPreferences pref;
  private SharedPreferences.Editor editor;

  public PrefManager(Context context) {
    pref = context.getSharedPreferences("PROJECT_NAME", Context.MODE_PRIVATE);
    editor = pref.edit();
  }

}

我使用了一种算法来加密和解密密码。

加密算法

 public void setPassword(String password) {
      int len = password.length();
      len /= 2;
      StringBuilder b1 = new StringBuilder(password.substring(0, len));
      StringBuilder b2 = new StringBuilder(password.substring(len));
      b1.reverse();
      b2.reverse();
      password = b1.toString() + b2.toString();

    editor.putString("password", password);
    editor.apply();
  }

decrypt algorithm

  public String getPassword() {
    String password = pref.getString("password", null);
    int len = password.length();
    len /= 2;
    StringBuilder b1 = new StringBuilder(password.substring(0, len));
    StringBuilder b2 = new StringBuilder(password.substring(len));
    password = b1.reverse().toString() + b2.reverse().toString();
    return password;
  }

注意:

在这个简单的算法中,我将密码从中间分成两半,倒过来,然后重新拼接起来。这只是一个想法,你可以使用你自己的算法来改变如何保存密码。

完整代码

import android.content.Context;
import android.content.SharedPreferences;

public class PrefManager {

  private SharedPreferences pref;
  private SharedPreferences.Editor editor;

  public PrefManager(Context context) {
    pref = context.getSharedPreferences("PROJECT_NAME", Context.MODE_PRIVATE);
    editor = pref.edit();
  }
  public String getPassword() {
    String password = pref.getString("password", null);
    int len = password.length();
    len /= 2;
    StringBuilder b1 = new StringBuilder(password.substring(0, len));
    StringBuilder b2 = new StringBuilder(password.substring(len));
    password = b1.reverse().toString() + b2.reverse().toString();
    return password;
  }

  public void setPassword(String password) {
      int len = password.length();
      len /= 2;
      StringBuilder b1 = new StringBuilder(password.substring(0, len));
      StringBuilder b2 = new StringBuilder(password.substring(len));
      b1.reverse();
      b2.reverse();
      password = b1.toString() + b2.toString();

    editor.putString("password", password);
    editor.apply();
  }
}

1
你的伪算法并不安全,因为该算法要求保密,否则它将无法工作,而这并不会增加安全性,因为没有任何秘密算法比开源算法更安全。无论如何,对字符串进行这种类型的操作是可预测的。 - Nicola Revelant
并不需要天才才能发现“ssapdrow”实际上是“password”。请不要将您的伪算法称为“加密”/“解密”。这样会极易产生误导。 - Simon Forsberg
当然,您是绝对正确的。如果用户使用简单密码,它很容易被识别,但任何开发人员都可以创建自己的算法来更改存储的字符串或向其添加一些值。我只是想说一种有点创造性的方式。@SimonForsberg - abolfazl bazghandi
@abolfazlbazghandi 当涉及到加密时,我强烈建议开发人员不要尝试自己编写加密算法,而是使用其中一个标准,例如AES256。 - Simon Forsberg

1
谷歌提供了AccountManager机制。这是创建账户的标准机制。登录数据将存储在Android认为合适的位置,例如,如果设备提供了安全区域,则会使用该区域。当然,已经root的设备仍然是一个问题,但至少这是使用标准机制而不是自己编写的机制,也不能从Android系统更新中受益。这也有一个优点,即该帐户列在Android设置中,“同步”功能是另一个积极的特性,它使应用程序和后端系统之间的数据同步,因此您不只是得到登录功能。
除此之外,使用用户名和密码已经不再是最好的选择。现在所有更好的应用都使用OAuth。这里值得注意的区别是,在登录期间仅传输一次密码以换取访问令牌。访问令牌通常具有过期日期,并且也可以在服务器上撤销。这减轻了密码被拦截并且不存储在设备上的风险。您的后端应支持此功能。

1

在不危及安全的情况下,最安全的方法是使用共享首选项仅存储上次登录用户的用户名。

此外,在用户表中引入一个列来保存数字布尔值(1或0),以表示该人是否勾选了“记住我”复选框。

启动应用程序时,使用getSharedPreferences()函数获取用户名,并使用它查询托管的数据库,以查看signedin列是1还是0,其中1表示该人已勾选“记住我”复选框。


1
好像你已经解决了所有的身份验证问题。除了任何人一旦发现就可以向服务器发送你的用户名(如果很难找到,我们也会以明文保存密码)。 - Nicola Revelant

1
使用NDK进行加密和解密,并在其中定义String Key变量,而不是将其保存在共享首选项中或在string xml中定义,可以帮助防止大多数脚本小子窃取秘钥。生成的密文将存储在共享首选项中。 此链接可能有关于示例代码的帮助

0
 //encode password
 pass_word_et = (EditText) v.findViewById(R.id.password_et);
 String pwd = pass_word_et.getText().toString();
                byte[] data = new byte[0];
                try {
                    data = pwd.getBytes("UTF-8");
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                }
                String base64 = Base64.encodeToString(data, Base64.DEFAULT);
                hbha_pref_helper.saveStringValue("pass_word", base64);

 //decode password
 String base64=hbha_pref_helper.getStringValue("pass_word");
            byte[] data = Base64.decode(base64, Base64.DEFAULT);
            String decrypt_pwd="";
            try {
                 decrypt_pwd = new String(data, "UTF-8");
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }

2
欢迎来到StackOverflow!您能否提供一份解释,以帮助PO理解您的答案? - FrankS101
13
Base64不是加密,而是编码算法,非常容易识别和解码。 - Miha_x64

0
Follow below steps :

1> create checkbox in xml file.
 <CheckBox
                android:id="@+id/cb_remember"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:layout_marginTop="@dimen/_25sdp"
                android:background="@drawable/rememberme_background"
                android:buttonTint="@android:color/white"
                android:paddingLeft="@dimen/_10sdp"
                android:paddingTop="@dimen/_5sdp"
                android:paddingRight="@dimen/_10sdp"
                android:paddingBottom="@dimen/_5sdp"
                android:text="REMEMBER ME"
                android:textColor="@android:color/white"
                android:textSize="@dimen/_12sdp" />

2> put this below code in java file.
  cb_remember.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
                if(b){
                    Log.d("mytag","checkbox is-----true----");
                    Prefs.getPrefInstance().setValue(LoginActivity.this, Const.CHECKBOX_STATUS, "1");
                    String userName =Prefs.getPrefInstance().getValue(context, Const.LOGIN_USERNAME, "");
                    String password =Prefs.getPrefInstance().getValue(context, Const.LOGIN_PASSWORD, "");
                    Log.d("mytag","userName and password id----"+userName +"         "+password);
                    edt_user_name.setText(userName);
                    edt_pwd.setText(password);

                }else{
                    Log.d("mytag","checkbox is-----false----");
                    Prefs.getPrefInstance().setValue(LoginActivity.this, Const.CHECKBOX_STATUS, "0");
                }
            }
        });

3> add this below code in java file before we check the checkbox.
  String stst =Prefs.getPrefInstance().getValue(LoginActivity.this, Const.CHECKBOX_STATUS, "");
        Log.d("mytag","statyus of the checkbox is----"+stst);
        if(stst.equals("1")){
            cb_remember.setChecked(true);
        }else{
            cb_remember.setChecked(false);
        }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接