تأمین امنیت داده های حساس در اندروید — از صفر تا صد

۱۲۵ بازدید
آخرین به‌روزرسانی: ۱۸ شهریور ۱۴۰۲
زمان مطالعه: ۵ دقیقه
تأمین امنیت داده های حساس در اندروید — از صفر تا صد

ذخیره‌سازی داده‌های حساس در اپلیکیشن اندروید تاکنون کار پر ریسکی بوده است، اما این وضعیت به زودی با معرفی کتابخانه 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","")

بدین ترتیب به پایان این مقاله می‌رسیم.

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۱ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
android-dev-hacks
نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *