根據官方文件, EncryptedSharedPreferences
使用 2-part system 來管理金鑰,並用金鑰加解密儲存的資料。
- 一組 Keyset(密鑰集合)包含一個或多個 Key(密鑰),用來加密解密資料。這組 Keyset 會儲存在
SharedPreferences
裡。 - 一把 Primary Key (主鑰) 負責加密所有的 Keysets,這把是存在 Android 的 KeyStore 裡面。
運作流程#
graph TD;
A[應用程式存取 EncryptedSharedPreferences] --> B{檢查 MasterKey 是否存在於 KeyStore}
B -- 存在 --> C[讀取 MasterKey]
B -- 不存在 --> D[建立新的 MasterKey]
C --> E{檢查 KeySet 是否存在於 SharedPreferences}
D --> E
E -- 存在 --> F[讀取 KeySet]
E -- 不存在 --> G[產生新的 KeySet 並加密存入 SharedPreferences]
F --> H[使用 KeySet 加密 Key & Value]
G --> H
H --> I[存入 shared_prefs/*.xml 已加密的數據]
資源引用#
1
2
3
4
5
6
7
8
9
10
11
| // Encrypted SharedPreference
implementation "androidx.security:security-crypto:1.0.0"
// For Identity Credential APIs
implementation "androidx.security:security-identity-credential:1.0.0-alpha03"
// For App Authentication APIs
implementation "androidx.security:security-app-authenticator:1.0.0-alpha02"
// For App Authentication API testing
androidTestImplementation "androidx.security:security-app-authenticator:1.0.0-alpha02"
|
這個套件基本上也可以對檔案加密,但本篇以實作 SharedPerefrences
為主,詳情可以參考 Work with data more securely。
基本實作#
因為 EncryptedSharedPreferences
是實作 SharedPreferences
介面,所以除了實例化的方式不太一樣外,其他的行為都是跟操作未加密的 SharedPreferences
一樣的。
1
2
3
4
5
6
7
8
9
10
| val masterKeys = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
val prefName = "MY_PREFERENCE_NAME"
val preference = EncryptedSharedPreferences.create(
prefName,
masterKeys,
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
|
如果是使用 1.1.0-alpha06 後的版本,會發現上面 MasterKeys
的寫法被 IDE 提示棄用。
要改用文件新建議的:
1
2
3
| val masterKey = new MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build();
|
2024 更新:建立的 SharedPreferences
必須排除在 Auto-Backup 外#
之前沒有注意到自動備份的問題,導致 SharedPreferences
建立的 XXXX.xml
被自動還原後,可能會因為 Key 遺失,導致 XML 內的資料無法解密。想看除錯紀錄,可以看這篇文章: Android EncryptedSharedPreference 系統升級後無法解密的錯誤與解決方案。
WARNING: The preference file should not be backed up with Auto Backup. When restoring the file it is likely the key used to encrypt it will no longer be present. You should exclude all EncryptedSharedPreference
s from backup using backup rules.
Doc: EncryptedSharedPreference
- 設定
android:allowBackup="false"
避免自動備份。 - 或在
backup_rules.xml
排除 EncryptedSharedPreferences 所建立的檔案。
整合成 PreferenceManager
#
為了方便在專案內使用,建立一個 PreferenceManager
類別,將 EncryptedSharedPreferences
的實例封裝起來。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
| import android.content.Context
import android.content.SharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
/**
* Created by DanielHuang
*/
open class PreferenceManager(context: Context,
prefName: String,
isEncrypted: Boolean) {
private val pref: SharedPreferences by lazy {
if (isEncrypted) {
val masterKeys = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
EncryptedSharedPreferences.create(
prefName,
masterKeys,
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
} else {
context.getSharedPreferences(prefName, Context.MODE_PRIVATE)
}
}
open fun setStringValue(key: String, value: String?) {
pref.edit().putString(key, value).apply()
}
open fun getStringValue(key: String): String? {
return pref.getString(key, "")
}
open fun getBooleanDefFalse(key: String): Boolean {
return pref.getBoolean(key, false)
}
open fun getBooleanDefTrue(key: String): Boolean {
return pref.getBoolean(key, true)
}
open fun setBooleanValue(key: String, value: Boolean) {
pref.edit().putBoolean(key, value).apply()
}
open fun setLongValue(key: String, value: Long) {
pref.edit().putLong(key, value).apply()
}
open fun getLongValue(key: String): Long {
return pref.getLong(key, -1)
}
open fun setIntValue(key: String, value: Int) {
pref.edit().putInt(key, value).apply()
}
open fun getIntValue(key: String, default: Int = -1): Int {
return pref.getInt(key, default)
}
/**
* 刪除資料
*/
fun remove(key: String) {
pref.edit().remove(key).apply()
}
/**
* 清除所有資料
*/
fun clearAll() {
pref.edit().clear().apply()
}
/**
* check sp is empty or not * @return
*/
val isEmpty: Boolean
get() = pref.all.isEmpty()
}
|
參考資料#