Android应用中存储SharedPreferences
的公共位置是:
/data/data/<package name>/shared_prefs/<filename.xml>
具有root权限的用户可以导航到此位置并可以更改其值。保护它的需要非常重要。
我们可以通过多少种方式加密整个shared_pref's xml
文件?
我们都知道我们可以在shared_pref's xml
文件中加密和保存数据,但这不仅100%安全,因此需要使用密钥加密整个文件。需要帮助了解加密整个xml
文件的各种方法。这是一般性问题,这里讨论的各种加密方法可以帮助所有开发人员保护应用程序。
答案 0 :(得分:35)
您应该注意Android的共享首选项是基于XML键值的。你不能改变这个事实(因为它会破坏它的解析器),你最多可以加密密钥和值,所以root用户可以阅读,但不会有任何想法他正在阅读。
为此,您可以使用像这样的简单加密
public static String encrypt(String input) {
// This is base64 encoding, which is not an encryption
return Base64.encodeToString(input.getBytes(), Base64.DEFAULT);
}
public static String decrypt(String input) {
return new String(Base64.decode(input, Base64.DEFAULT));
}
这就是你如何使用这个
// Write
SharedPreferences preferences = getSharedPreferences("some_prefs_name", MODE_PRIVATE);
SharedPreferences.Editor editor = preferences.edit();
editor.putString(encrypt("password"), encrypt("dummypass"));
editor.apply(); // Or commit if targeting old devices
// Read
SharedPreferences preferences = getSharedPreferences("some_prefs_name", MODE_PRIVATE);
String passEncrypted = preferences.getString(encrypt("password"), encrypt("default"));
String pass = decrypt(passEncrypted);
你应该知道,SharedPreferences
从来没有建立起来是安全的,这只是一种坚持数据的简单方法。
你也应该意识到我使用的加密并不是最安全的,但它很简单。
有几个库可以提供更好的加密,比如这些
但他们都认为文件的格式仍然是XML,而且它是基于键值的。你无法改变这一事实。见下文。
cat /data/data/your.package.application/shared_prefs/prefs-test.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="JopRH053b7Ogw17Yxmh7Og==">0AB7Y28XEvbQcnXpEZ4j9PtqzFLtm2V3KBXjTO1V704=</string>
</map>
The key is "hemmelighet" and the value is "dette er en hemmelighet".
如果安全问题超出SharedPreferences
仍然是基于键值且XML格式的事实,则需要完全避免它。
答案 1 :(得分:5)
您应该加密数据并写入SharedPreferences。当您想要获取此数据时,应从SharedPreferences中解密。您需要以下帮助程序类
public class Encryption {
private final Builder mBuilder;
private Encryption(Builder builder) {
mBuilder = builder;
}
public static Encryption getDefault(String key, String salt, byte[] iv) {
try {
return Builder.getDefaultBuilder(key, salt, iv).build();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return null;
}
}
private String encrypt(String data) throws UnsupportedEncodingException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, InvalidKeyException, InvalidKeySpecException, BadPaddingException, IllegalBlockSizeException {
if (data == null) return null;
SecretKey secretKey = getSecretKey(hashTheKey(mBuilder.getKey()));
byte[] dataBytes = data.getBytes(mBuilder.getCharsetName());
Cipher cipher = Cipher.getInstance(mBuilder.getAlgorithm());
cipher.init(Cipher.ENCRYPT_MODE, secretKey, mBuilder.getIvParameterSpec(), mBuilder.getSecureRandom());
return Base64.encodeToString(cipher.doFinal(dataBytes), mBuilder.getBase64Mode());
}
public String encryptOrNull(String data) {
try {
return encrypt(data);
} catch (Exception e) {
e.printStackTrace();
return "";
}
}
public void encryptAsync(final String data, final Callback callback) {
if (callback == null) return;
new Thread(new Runnable() {
@Override
public void run() {
try {
String encrypt = encrypt(data);
if (encrypt == null) {
callback.onError(new Exception("Encrypt return null, it normally occurs when you send a null data"));
}
callback.onSuccess(encrypt);
} catch (Exception e) {
callback.onError(e);
}
}
}).start();
}
private String decrypt(String data) throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
if (data == null) return null;
byte[] dataBytes = Base64.decode(data, mBuilder.getBase64Mode());
SecretKey secretKey = getSecretKey(hashTheKey(mBuilder.getKey()));
Cipher cipher = Cipher.getInstance(mBuilder.getAlgorithm());
cipher.init(Cipher.DECRYPT_MODE, secretKey, mBuilder.getIvParameterSpec(), mBuilder.getSecureRandom());
byte[] dataBytesDecrypted = (cipher.doFinal(dataBytes));
return new String(dataBytesDecrypted);
}
public String decryptOrNull(String data) {
try {
return decrypt(data);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public void decryptAsync(final String data, final Callback callback) {
if (callback == null) return;
new Thread(new Runnable() {
@Override
public void run() {
try {
String decrypt = decrypt(data);
if (decrypt == null) {
callback.onError(new Exception("Decrypt return null, it normally occurs when you send a null data"));
}
callback.onSuccess(decrypt);
} catch (Exception e) {
callback.onError(e);
}
}
}).start();
}
private SecretKey getSecretKey(char[] key) throws NoSuchAlgorithmException, UnsupportedEncodingException, InvalidKeySpecException {
SecretKeyFactory factory = SecretKeyFactory.getInstance(mBuilder.getSecretKeyType());
KeySpec spec = new PBEKeySpec(key, mBuilder.getSalt().getBytes(mBuilder.getCharsetName()), mBuilder.getIterationCount(), mBuilder.getKeyLength());
SecretKey tmp = factory.generateSecret(spec);
return new SecretKeySpec(tmp.getEncoded(), mBuilder.getKeyAlgorithm());
}
private char[] hashTheKey(String key) throws UnsupportedEncodingException, NoSuchAlgorithmException {
MessageDigest messageDigest = MessageDigest.getInstance(mBuilder.getDigestAlgorithm());
messageDigest.update(key.getBytes(mBuilder.getCharsetName()));
return Base64.encodeToString(messageDigest.digest(), Base64.NO_PADDING).toCharArray();
}
public interface Callback {
void onSuccess(String result);
void onError(Exception exception);
}
private static class Builder {
private byte[] mIv;
private int mKeyLength;
private int mBase64Mode;
private int mIterationCount;
private String mSalt;
private String mKey;
private String mAlgorithm;
private String mKeyAlgorithm;
private String mCharsetName;
private String mSecretKeyType;
private String mDigestAlgorithm;
private String mSecureRandomAlgorithm;
private SecureRandom mSecureRandom;
private IvParameterSpec mIvParameterSpec;
public static Builder getDefaultBuilder(String key, String salt, byte[] iv) {
return new Builder()
.setIv(iv)
.setKey(key)
.setSalt(salt)
.setKeyLength(128)
.setKeyAlgorithm("AES")
.setCharsetName("UTF8")
.setIterationCount(1)
.setDigestAlgorithm("SHA1")
.setBase64Mode(Base64.DEFAULT)
.setAlgorithm("AES/CBC/PKCS5Padding")
.setSecureRandomAlgorithm("SHA1PRNG")
.setSecretKeyType("PBKDF2WithHmacSHA1");
}
private Encryption build() throws NoSuchAlgorithmException {
setSecureRandom(SecureRandom.getInstance(getSecureRandomAlgorithm()));
setIvParameterSpec(new IvParameterSpec(getIv()));
return new Encryption(this);
}
private String getCharsetName() {
return mCharsetName;
}
private Builder setCharsetName(String charsetName) {
mCharsetName = charsetName;
return this;
}
private String getAlgorithm() {
return mAlgorithm;
}
private Builder setAlgorithm(String algorithm) {
mAlgorithm = algorithm;
return this;
}
private String getKeyAlgorithm() {
return mKeyAlgorithm;
}
private Builder setKeyAlgorithm(String keyAlgorithm) {
mKeyAlgorithm = keyAlgorithm;
return this;
}
private int getBase64Mode() {
return mBase64Mode;
}
private Builder setBase64Mode(int base64Mode) {
mBase64Mode = base64Mode;
return this;
}
private String getSecretKeyType() {
return mSecretKeyType;
}
private Builder setSecretKeyType(String secretKeyType) {
mSecretKeyType = secretKeyType;
return this;
}
private String getSalt() {
return mSalt;
}
private Builder setSalt(String salt) {
mSalt = salt;
return this;
}
private String getKey() {
return mKey;
}
private Builder setKey(String key) {
mKey = key;
return this;
}
private int getKeyLength() {
return mKeyLength;
}
public Builder setKeyLength(int keyLength) {
mKeyLength = keyLength;
return this;
}
private int getIterationCount() {
return mIterationCount;
}
public Builder setIterationCount(int iterationCount) {
mIterationCount = iterationCount;
return this;
}
private String getSecureRandomAlgorithm() {
return mSecureRandomAlgorithm;
}
public Builder setSecureRandomAlgorithm(String secureRandomAlgorithm) {
mSecureRandomAlgorithm = secureRandomAlgorithm;
return this;
}
private byte[] getIv() {
return mIv;
}
public Builder setIv(byte[] iv) {
mIv = iv;
return this;
}
private SecureRandom getSecureRandom() {
return mSecureRandom;
}
public Builder setSecureRandom(SecureRandom secureRandom) {
mSecureRandom = secureRandom;
return this;
}
private IvParameterSpec getIvParameterSpec() {
return mIvParameterSpec;
}
public Builder setIvParameterSpec(IvParameterSpec ivParameterSpec) {
mIvParameterSpec = ivParameterSpec;
return this;
}
private String getDigestAlgorithm() {
return mDigestAlgorithm;
}
public Builder setDigestAlgorithm(String digestAlgorithm) {
mDigestAlgorithm = digestAlgorithm;
return this;
}
}}
然后您可以通过如下加密数据来编写SharedPreferences
Encryption encryption = Encryption.getDefault("Key", "Salt", new byte[16]);
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
SharedPreferences.Editor editor = preferences.edit();
editor.putString("token", encryption.encryptOrNull(userModel.getToken()));
editor.apply()
您最终可以通过以下方式从SharedPreferences数据中读取。这样,将敏感信息保留在手机的硬件级别上将更加安全
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
Encryption encryption = Encryption.getDefault("Key", "Salt", new byte[16]);
String token = encryption.decryptOrNull(preferences.getString("token",""));
答案 2 :(得分:3)
Base64未加密!不要使用它!是的,“ root”用户可以访问该数据。您可以做的一件事是使用AES加密该数据或使用单个NoSQL数据库文件并加密该文件。应用打开后,您将对数据库进行解密,并使用该数据库存储信息或独立加密所有文件。
看这里:https://code.tutsplus.com/tutorials/storing-data-securely-on-android--cms-30558
答案 3 :(得分:3)
完整答案(API级别23+)。首先,您需要从androidx加密。
implementation "androidx.security:security-crypto:1.0.0-alpha02"
注意:SharedPreferences和EncryptedSharedPreferences之间存在明显的性能差异。 您应该注意到, EncryptedSharedPreferences.create(...)并没有那么快,因此您必须只存储一个实例。
然后,您必须使用它来检索 EncryptedSharedPreferences 。
public SharedPreferences getEncryptedSharedPreferences(){
String masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC);
SharedPreferences sharedPreferences = EncryptedSharedPreferences.create(
"secret_shared_prefs_file",
masterKeyAlias,
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
);
return sharedPreferences;
}
您只需要使用“标准方式”之类的首选项即可。要保存它:
getEncryptedSharedPreferences().edit()
.putString("ENCRYPTDATA", text)
.apply()
获取首选项值。
getEncryptedSharedPreferences().getString("ENCRYPTDATA", "defvalue")
答案 4 :(得分:2)
Google已发布EncryptedSharedPreferences
作为其androidx的一部分,我认为这应该是加密首选项的首选方法。
请参见https://developer.android.com/reference/androidx/security/crypto/EncryptedSharedPreferences
答案 5 :(得分:1)
public class NodeCrypto {
private String iv = "fedcba9876543210";//Dummy iv (CHANGE IT!)
private IvParameterSpec ivspec;
private SecretKeySpec keyspec;
private Cipher cipher;
private String SecretKey = "0123456789abcdef";//Dummy secretKey (CHANGE IT!)
public void doKey(String key)
{
ivspec = new IvParameterSpec(iv.getBytes());
key = padRight(key,16);
Log.d("hi",key);
keyspec = new SecretKeySpec(key.getBytes(), "AES");
try {
cipher = Cipher.getInstance("AES/CBC/NoPadding");
} catch (NoSuchAlgorithmException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (NoSuchPaddingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public byte[] encrypt(String text,String key) throws Exception
{
if(text == null || text.length() == 0)
throw new Exception("Empty string");
doKey(key);
byte[] encrypted = null;
try {
cipher.init(Cipher.ENCRYPT_MODE, keyspec, ivspec);
encrypted = cipher.doFinal(padString(text).getBytes());
} catch (Exception e)
{
throw new Exception("[encrypt] " + e.getMessage());
}
return encrypted;
}
public byte[] decrypt(String code,String key) throws Exception
{
if(code == null || code.length() == 0)
throw new Exception("Empty string");
byte[] decrypted = null;
doKey(key);
try {
cipher.init(Cipher.DECRYPT_MODE, keyspec, ivspec);
decrypted = cipher.doFinal(hexToBytes(code));
} catch (Exception e)
{
throw new Exception("[decrypt] " + e.getMessage());
}
return decrypted;
}
public static String bytesToHex(byte[] data)
{
if (data==null)
{
return null;
}
int len = data.length;
String str = "";
for (int i=0; i<len; i++) {
if ((data[i]&0xFF)<16)
str = str + "0" + java.lang.Integer.toHexString(data[i]&0xFF);
else
str = str + java.lang.Integer.toHexString(data[i]&0xFF);
}
return str;
}
public static byte[] hexToBytes(String str) {
if (str==null) {
return null;
} else if (str.length() < 2) {
return null;
} else {
int len = str.length() / 2;
byte[] buffer = new byte[len];
for (int i=0; i<len; i++) {
buffer[i] = (byte) Integer.parseInt(str.substring(i*2,i*2+2),16);
}
return buffer;
}
}
private static String padString(String source)
{
char paddingChar = ' ';
int size = 16;
int x = source.length() % size;
int padLength = size - x;
for (int i = 0; i < padLength; i++)
{
source += paddingChar;
}
return source;
}
public static String padRight(String s, int n) {
return String.format("%1$-" + n + "s", s);
}
}
-----------------------------------------------
from your activity or class call encrypt or decrypt method before saving or retriving from SharedPreference
答案 6 :(得分:1)
如果您想支持 Android 5.0(API 级别 21)及更高版本
使用以下实现:
implementation "androidx.security:security-crypto:1.0.0-rc04"
获取最新的
然后
首先创建一个主密钥如下:
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
创建 shared preferences
后如下:
val sharedPreferences = EncryptedSharedPreferences.create(
context,
"secret_shared_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
然后像往常一样使用它,例如:
with(sharedPreferences.edit()) {
putString(Values.SP_USER_ID, personId)
putString(Values.SP_USER_NAME, binding.editTextTextPersonName.text.toString())
apply()
}
答案 7 :(得分:0)
Kotlin 示例,用于使用anrdoidx的安全性加密库(最低API 23)进行双重用途的加密和未加密共享首选项。
我在需要的地方使用Dagger2将其作为@Singleton注入。
在Dagger模块中使用@Name批注来区分SharedPreferences实例,并且可以有2个独立的.xml文件(1个加密,1个未加密)来读写。
添加到build.gradle中的依赖项:
implementation "androidx.security:security-crypto:1.0.0-beta01"
import android.content.Context
import android.content.SharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences
class Prefs(prefsName: String, context: Context) {
private lateinit var ANDX_SECURITY_KEY_KEYSET: String
private lateinit var ANDX_SECURITY_VALUE_KEYSET: String
private lateinit var cntext: Context
private lateinit var prefName: String
private lateinit var prefs: SharedPreferences
constructor(
prefsName: String,
context: Context,
masterKeyAlias: String,
prefKeyEncryptionScheme: EncryptedSharedPreferences.PrefKeyEncryptionScheme,
prefValueEncryptionScheme: EncryptedSharedPreferences.PrefValueEncryptionScheme
): this(prefsName, context) {
ANDX_SECURITY_KEY_KEYSET = "__androidx_security_crypto_encrypted_prefs_key_keyset__"
ANDX_SECURITY_VALUE_KEYSET = "__androidx_security_crypto_encrypted_prefs_value_keyset__"
cntext = context
prefName = prefsName
prefs =
EncryptedSharedPreferences.create(
prefsName,
masterKeyAlias,
context,
prefKeyEncryptionScheme,
prefValueEncryptionScheme
)
}
init {
if (!::ANDX_SECURITY_KEY_KEYSET.isInitialized) {
prefs =
context.getSharedPreferences(
prefsName,
Context.MODE_PRIVATE
)
}
}
companion object {
const val INVALID_BOOLEAN: Boolean = false
const val INVALID_FLOAT: Float = -11111111111F
const val INVALID_INT: Int = -1111111111
const val INVALID_LONG: Long = -11111111111L
const val INVALID_STRING: String = "INVALID_STRING"
val INVALID_STRING_SET: Set<String> = setOf(INVALID_STRING)
}
/**
* OnChangeListener
* */
fun registerOnSharedPreferenceChangeListener(
listener: SharedPreferences.OnSharedPreferenceChangeListener) =
prefs.registerOnSharedPreferenceChangeListener(listener)
fun unregisterOnSharedPreferenceChangeListener(
listener: SharedPreferences.OnSharedPreferenceChangeListener) =
prefs.unregisterOnSharedPreferenceChangeListener(listener)
/**
* Read Shared Prefs
* */
fun contains(key: String): Boolean =
prefs.contains(key)
fun getAll(): Map<String, *> =
prefs.all
// Returns null if the Boolean value is not in
// Shared Preferences
fun read(key: String): Boolean? =
if (contains(key)) {
read(key, INVALID_BOOLEAN)
} else {
null
}
// Boolean
fun read(key: String, returnIfInvalid: Boolean): Boolean =
prefs.getBoolean(key, returnIfInvalid)
// Float
fun read(key: String, returnIfInvalid: Float): Float =
prefs.getFloat(key, returnIfInvalid)
// Int
fun read(key: String, returnIfInvalid: Int): Int =
prefs.getInt(key, returnIfInvalid)
// Long
fun read(key: String, returnIfInvalid: Long): Long =
prefs.getLong(key, returnIfInvalid)
// Set<String>
fun read(key: String, returnIfInvalid: Set<String>): Set<String>? =
prefs.getStringSet(key, returnIfInvalid)
// String
fun read(key: String, returnIfInvalid: String): String? =
prefs.getString(key, returnIfInvalid)
/**
* Modify Shared Prefs
* */
fun clear() {
if (::ANDX_SECURITY_KEY_KEYSET.isInitialized) {
val clearTextPrefs = cntext.getSharedPreferences(prefName, Context.MODE_PRIVATE)
val keyKeyset = clearTextPrefs.getString(ANDX_SECURITY_KEY_KEYSET, INVALID_STRING)
val valueKeyset = clearTextPrefs.getString(ANDX_SECURITY_VALUE_KEYSET, INVALID_STRING)
if (keyKeyset != null && keyKeyset != INVALID_STRING
&& valueKeyset != null && valueKeyset != INVALID_STRING) {
if (!clearTextPrefs.edit().clear().commit()) {
clearTextPrefs.edit().clear().apply()
}
if (!clearTextPrefs.edit().putString(ANDX_SECURITY_KEY_KEYSET, keyKeyset).commit()) {
clearTextPrefs.edit().putString(ANDX_SECURITY_KEY_KEYSET, keyKeyset).apply()
}
if (!clearTextPrefs.edit().putString(ANDX_SECURITY_VALUE_KEYSET, valueKeyset).commit()) {
clearTextPrefs.edit().putString(ANDX_SECURITY_VALUE_KEYSET, valueKeyset).apply()
}
}
} else {
if (!prefs.edit().clear().commit()) {
prefs.edit().clear().apply()
}
}
}
fun remove(key: String) {
if (!prefs.edit().remove(key).commit()) {
prefs.edit().remove(key).apply()
}
}
// Boolean
fun write(key: String, value: Boolean) {
if (!prefs.edit().putBoolean(key, value).commit()) {
prefs.edit().putBoolean(key, value).apply()
}
}
// Float
fun write(key: String, value: Float) {
if (!prefs.edit().putFloat(key, value).commit()) {
prefs.edit().putFloat(key, value).apply()
}
}
// Int
fun write(key: String, value: Int) {
if (!prefs.edit().putInt(key, value).commit()) {
prefs.edit().putInt(key, value).apply()
}
}
// Long
fun write(key: String, value: Long) {
if (!prefs.edit().putLong(key, value).commit()) {
prefs.edit().putLong(key, value).apply()
}
}
// Set<String>
fun write(key: String, value: Set<String>) {
if (!prefs.edit().putStringSet(key, value).commit()) {
prefs.edit().putStringSet(key, value).apply()
}
}
// String
fun write(key: String, value: String) {
if (!prefs.edit().putString(key, value).commit()) {
prefs.edit().putString(key, value).apply()
}
}
}
using Dagger2 to inject as a @Singleton的另一种选择是:
AppPrefs.kt
object AppPrefs {
lateinit var encryptedPrefs: Prefs
lateinit var prefs: Prefs
// Add your key strings here...
fun initEncryptedPrefs(context: Context) {
encryptedPrefs =
Prefs(
"ENCRYPTED_PREFS",
context,
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}
fun initPrefs(context: Context) {
prefs = Prefs("PREFS", context)
}
}
Application.kt
class Application: Application() {
override fun onCreate() {
super.onCreate()
AppPrefs.initEncryptedPrefs(this.applicationContext)
AppPrefs.initPrefs(this.applicationContext)
}
}
然后仅从AppPrefs.prefs
或AppPrefs.encryptedPrefs
的任何地方拨打电话
答案 8 :(得分:0)
对于API 23以下的版本,请查看Datum库。它异步加密数据:
StringDatum stringDatum = provider.new StringDatum("string_key", "default");
stringDatum.setValue("new value");
stringDatum.getValue(value->{
//got value
});
答案 9 :(得分:0)
您需要在API 23下处理Verisons
Overscroll