تأمین امنیت داده های حساس در اندروید — از صفر تا صد
ذخیرهسازی دادههای حساس در اپلیکیشن اندروید تاکنون کار پر ریسکی بوده است، اما این وضعیت به زودی با معرفی کتابخانه Shared-preference رمزگذاری شده نیتیو از سوی خانواده AndroidX تغییر مییابد. در این مقاله در مورد روشهای تأمین امنیت داده های حساس در اندروید صحبت خواهیم کرد.
چنان که اشاره کردیم کتابخانه Shared-preference رمزگذاری شده موجب میشود که امنیت دادهها در اندروید ارتقا یابد، اما این کتابخانه نیز مانند همه چیزهای دیگر در اندروید، دارای نقصهایی است. این کتابخانه تنها از اندروید نسخه 6 به بالا پشتیبانی میکند. در ادامه در مورد شیوه استفاده از کتابخانه Shared-preference رمزگذاری شده توضیح میدهیم و سپس فرایند رمزگذاری را برای دستگاههای اندروید 6 معرفی میکنیم.
یکپارچهسازی کتابخانه
کمینه نسخه SDK مورد نیاز برای اپلیکیشن شما باید 23 (اندروید 6) باشد.
minSdkVersion 23
همچنین باید خط زیر را در build.gradle اپلیکیشن وارد کنید. این کتابخانه همچنان در مرحله آلفا است، بنابراین باید مطمئن شوید که از آن در بیلد پروداکشن خود استفاده نمیکنید.
implementation "androidx.security:security-crypto:1.0.0-alpha02"
طرز کار Preference رمزگذاری شده
class EncryptedSharedPreferences: SharedPreferences
shared-preference رمزگذاری شده اساساً کلاس shared-preference را بسط میدهد، یعنی لازم نیست چیز تازهای به جز پیکربندی کتابخانه جدید، در مورد آن یاد بگیرید.
گام 1
در گام نخست باید یک master از Keystore به صورت زیر ایجاد و یا دریافت کنیم:
1val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
گام 2
اکنون باید با ارسال برخی پارامترهای خاص مانند نام preference، کلید مستر که در گام قبلی به دست آمد و context اپلیکیشن و برخی اسکیماهای رمزگذاری که در ادامه نمایش یافته، وهلهای از preference رمزگذاری شده را از تابع ایجادش بسازیم. قویاً توصیه شده است که از همان اسکیماهای رمزگذاری که در اینجا نمایش دادهایم، استفاده شود:
1val sharedPreferences = EncryptedSharedPreferences.create(
2 "PreferencesFilename",
3 masterKeyAlias,
4 applicationContext,
5 EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
6 EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
7)
گام 3
اکنون زمان آن رسیده است که برخی دادهها را در preference نوشته و بخوانیم. این کار به صورت زیر قابل اجرا است:
1// Write into encrypted preference
2encrytedSharedPreferences.edit()
3 .putString("NAME", nameText.text.toString())
4 .apply()
5
6// Read from encrypted preference
7val value = encrytedSharedPreferences.getString("NAME", "")
این کار تا حدود زیادی شبیه به دسترسی به shared-preference قدیمی و آشنای قبلی است.
مزایا و معایب
بزرگترین مزیت معرفی این کتابخانه جدید آن است که اکنون امکان رمزگذاری پیشفرض دادههای حساس در مقیاس کوچک در اندروید فراهم شده است. آن چه به صورت داخلی رخ میدهد این است که اسکیماها و کلیدهای مستر Keystore در زمان ایجاد وهلهای از preference ارائه میشوند و یک الگوریتم رمزگذاری، همه جفتهای کلید-مقدار را رمزگذاری میکند، به طوری که دادهها امن میشوند و همچنین یک فایل preference با حفاظت رمز عبور ایجاد میشود.
از سوی دیگر، بزرگترین عیب آن این است که تنها از اندروید 6 و بالاتر پشتیبانی میکند. اما اغلب اپلیکیشنها هم اینک همچنان از اندروید Kitkat (نسخه 4.4) پشتیبانی میکنند. برای حل این مشکل میتوانید از روشی که در بخش بعدی توضیح داده شده است استفاده کنید.
Preference امن برای اندرویدهای قبل از نسخه 6
زمانی که شروع به جستجو برای Preference امن بکنید، دیری طول نمیکشد که با کتابخانه شگفتانگیزی به نام secure preference (+) مواجه میشوید. اما پیش از آغاز به کار روی آن باید به یک سؤال مهم پاسخ دهیم:
ما مشغول کار روی امنیت هستیم؛ آیا میتوانیم به یک کتابخانه شخص ثالث تصادفی که به درستی نمیشناسیم، برای ذخیرهسازی دادههای حساس خود اعتماد کنیم؟
پاسخی که ما به سؤال فوق میدهیم یک نه بزرگ است!
به همین دلیل از گزینه بعدی استفاده میکنیم. یک گام به عقب بردارید و در مورد شیوه عمل این کتابخانه و همچنین preference امن AndroidX تأمل کنید. در واقع کار اصلی که این کتابخانهها انجام میدهند، رمزگذاری جفتهای کلید-مقدار پیش از ذخیرهسازی آنها در preference و یا رمزگذاری خود فایل preference است.
به نظر میرسد که گزینه نخست یعنی رمزگذاری جفتهای کلید-مقدار پیش از ذخیرهسازی در preference مناسبتر است. به این ترتیب حتی اگر جفتهای کلید-مقدار در preference به نحوی افشا شوند، امکان دانستن محتوای آنها به جز آشنایی با الگوریتم رمزگذاری شما و «کلید سری» (secret key) وجود نخواهد داشت. پیشنهاد میکنیم کلید سری را در فایل build.gradle به صورت فیلد buildconfig ذخیره کنید.
اینک زمان آن رسیده است که preference رمزگذاری شده را کدنویسی کنیم. ابتدا باید کلاس EncryptedPreference را ایجاد کنیم که در آن الگوریتم رمزگذاری کلید و مقدار را مینویسیم. کد به صورت زیر است:
1import android.content.Context
2import android.content.SharedPreferences
3import android.util.Base64
4import java.io.UnsupportedEncodingException
5import java.nio.charset.Charset
6import java.security.*
7import javax.crypto.Cipher
8import javax.crypto.spec.IvParameterSpec
9import javax.crypto.spec.SecretKeySpec
10
11class EncryptedPreference @Throws(SecurePreferencesException::class)
12 constructor(context: Context, preferenceName: String,
13 secureKey: String, encryptKeys: Boolean) {
14
15 private val encryptKeys: Boolean
16 private val writer: Cipher
17 private val reader: Cipher
18 private val keyWriter: Cipher
19 private val preferences: SharedPreferences
20
21 protected val iv: IvParameterSpec
22 get() {
23 val iv = ByteArray(writer.blockSize)
24 System.arraycopy("fldsjfodasjifudslfjdsaofshaufihadsf".toByteArray(), 0,
25 iv, 0, writer.blockSize)
26 return IvParameterSpec(iv)
27 }
28
29 class SecurePreferencesException(e: Throwable) : RuntimeException(e)
30
31 init {
32 try {
33 this.writer = Cipher.getInstance(TRANSFORMATION)
34 this.reader = Cipher.getInstance(TRANSFORMATION)
35 this.keyWriter = Cipher.getInstance(KEY_TRANSFORMATION)
36
37 initCiphers(secureKey)
38
39 this.preferences = context.getSharedPreferences(preferenceName,
40 Context.MODE_PRIVATE)
41
42 this.encryptKeys = encryptKeys
43 } catch (e: GeneralSecurityException) {
44 throw SecurePreferencesException(e)
45 } catch (e: UnsupportedEncodingException) {
46 throw SecurePreferencesException(e)
47 }
48
49 }
50
51 @Throws(UnsupportedEncodingException::class, NoSuchAlgorithmException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
52 protected fun initCiphers(secureKey: String) {
53 val ivSpec = iv
54 val secretKey = getSecretKey(secureKey)
55
56 writer.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec)
57 reader.init(Cipher.DECRYPT_MODE, secretKey, ivSpec)
58 keyWriter.init(Cipher.ENCRYPT_MODE, secretKey)
59 }
60
61 @Throws(UnsupportedEncodingException::class, NoSuchAlgorithmException::class)
62 protected fun getSecretKey(key: String): SecretKeySpec {
63 val keyBytes = createKeyBytes(key)
64 return SecretKeySpec(keyBytes, TRANSFORMATION)
65 }
66
67 @Throws(UnsupportedEncodingException::class, NoSuchAlgorithmException::class)
68 protected fun createKeyBytes(key: String): ByteArray {
69 val md = MessageDigest
70 .getInstance(SECRET_KEY_HASH_TRANSFORMATION)
71 md.reset()
72 return md.digest(key.toByteArray(charset(CHARSET)))
73 }
74
75 fun putString(key: String, value: String?) {
76 if (value == null) {
77 preferences.edit().remove(toKey(key)).commit()
78 } else {
79 putValue(toKey(key), value)
80 }
81 }
82
83 fun putBoolean(key: String, value: Boolean?) {
84 if (value == null) {
85 preferences.edit().remove(toKey(key)).commit()
86 } else {
87 putValue(toKey(key), java.lang.Boolean.toString(value))
88 }
89 }
90
91 fun putLong(key: String, value: Long) {
92
93 putValue(toKey(key), java.lang.Long.toString(value))
94
95 }
96
97 fun putInt(key: String, value: Int) {
98
99 putValue(toKey(key), Integer.toString(value))
100
101 }
102
103 fun containsKey(key: String): Boolean {
104 return preferences.contains(toKey(key))
105 }
106
107 fun removeValue(key: String) {
108 preferences.edit().remove(toKey(key)).commit()
109 }
110
111 @Throws(SecurePreferencesException::class)
112 fun getString(key: String, value: String): String {
113 if (preferences.contains(toKey(key))) {
114 val securedEncodedValue = preferences.getString(toKey(key), "")
115 return decrypt(securedEncodedValue)
116 }
117 return value
118 }
119
120 @Throws(SecurePreferencesException::class)
121 fun getLong(key: String, value: Long): Long {
122 if (preferences.contains(toKey(key))) {
123 val securedEncodedValue = preferences.getString(toKey(key), "")
124 return java.lang.Long.parseLong(decrypt(securedEncodedValue))
125 }
126 return value
127 }
128
129 @Throws(SecurePreferencesException::class)
130 fun getBoolean(key: String, value: Boolean): Boolean {
131 if (preferences.contains(toKey(key))) {
132 val securedEncodedValue = preferences.getString(toKey(key), "")
133 return java.lang.Boolean.parseBoolean(decrypt(securedEncodedValue))
134 }
135 return value
136 }
137
138 @Throws(SecurePreferencesException::class)
139 fun getInt(key: String, value: Int): Int {
140 if (preferences.contains(toKey(key))) {
141 val securedEncodedValue = preferences.getString(toKey(key), "")
142 return Integer.parseInt(decrypt(securedEncodedValue))
143 }
144 return value
145 }
146
147 fun commit() {
148 preferences.edit().commit()
149 }
150
151 fun clear() {
152 preferences.edit().clear().commit()
153 }
154
155 private fun toKey(key: String): String {
156 return if (encryptKeys)
157 encrypt(key, keyWriter)
158 else
159 key
160 }
161
162 @Throws(SecurePreferencesException::class)
163 private fun putValue(key: String, value: String) {
164 val secureValueEncoded = encrypt(value, writer)
165
166 preferences.edit().putString(key, secureValueEncoded).commit()
167 }
168
169 @Throws(SecurePreferencesException::class)
170 protected fun encrypt(value: String, writer: Cipher): String {
171 val secureValue: ByteArray
172 try {
173 secureValue = convert(writer, value.toByteArray(charset(CHARSET)))
174 } catch (e: UnsupportedEncodingException) {
175 throw SecurePreferencesException(e)
176 }
177
178 return Base64.encodeToString(secureValue,
179 Base64.NO_WRAP)
180 }
181
182 protected fun decrypt(securedEncodedValue: String): String {
183 val securedValue = Base64
184 .decode(securedEncodedValue, Base64.NO_WRAP)
185 val value = convert(reader, securedValue)
186 try {
187 return String(value, Charset.forName(CHARSET))
188 } catch (e: UnsupportedEncodingException) {
189 throw SecurePreferencesException(e)
190 }
191
192 }
193
194 companion object {
195
196 private val TRANSFORMATION = "AES/CBC/PKCS5Padding"
197 private val KEY_TRANSFORMATION = "AES/ECB/PKCS5Padding"
198 private val SECRET_KEY_HASH_TRANSFORMATION = "SHA-256"
199 private val CHARSET : String = "UTF-8"
200 private var instance: EncryptedPreference? = null
201
202 fun getInstance(contxt: Context, prefName: String): EncryptedPreference {
203 if (instance == null) {
204 instance = EncryptedPreference(contxt, prefName,
205 BuildConfig.securekey, true)
206 }
207 return instance as EncryptedPreference
208 }
209
210 @Throws(SecurePreferencesException::class)
211 private fun convert(cipher: Cipher, bs: ByteArray): ByteArray {
212 try {
213 return cipher.doFinal(bs)
214 } catch (e: Exception) {
215 throw SecurePreferencesException(e)
216 }
217
218 }
219 }
220}
اکنون کافی است تابع getinstance را با ارسال context و نام preference خود فراخوانی کنید. به خاطر داشته باشید که باید کلید امنیتی را در فایل build.gradle ذکر کنید. شیوه نوشتن و خواندن دادهها نیز به صورت زیر است:
1// Write data
2val encryptedPreference = EncryptedPreference.getInstance(context,preferncename)
3encryptedPreference.putString("name",usertext.text.toString())
4
5// Read data
6encryptedPreference.getString("name","")
بدین ترتیب به پایان این مقاله میرسیم.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای پروژه محور برنامه نویسی اندروید
- مجموعه آموزشهای برنامهنویسی
- آموزش ساخت اپلیکیشن کتاب و کار با داده ها در اندروید
- نکات کلیدی اندروید ۱۰ برای توسعه دهندگان — راهنمای کاربردی
- طراحی انیمیشن های ساده برای اپلیکیشن های اندرویدی — به زبان ساده
==