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

ذخیرهسازی دادههای حساس در اپلیکیشن اندروید تاکنون کار پر ریسکی بوده است، اما این وضعیت به زودی با معرفی کتابخانه 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 به صورت زیر ایجاد و یا دریافت کنیم:
val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
گام 2
اکنون باید با ارسال برخی پارامترهای خاص مانند نام preference، کلید مستر که در گام قبلی به دست آمد و context اپلیکیشن و برخی اسکیماهای رمزگذاری که در ادامه نمایش یافته، وهلهای از preference رمزگذاری شده را از تابع ایجادش بسازیم. قویاً توصیه شده است که از همان اسکیماهای رمزگذاری که در اینجا نمایش دادهایم، استفاده شود:
val sharedPreferences = EncryptedSharedPreferences.create( "PreferencesFilename", masterKeyAlias, applicationContext, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM )
گام 3
اکنون زمان آن رسیده است که برخی دادهها را در preference نوشته و بخوانیم. این کار به صورت زیر قابل اجرا است:
// Write into encrypted preference encrytedSharedPreferences.edit() .putString("NAME", nameText.text.toString()) .apply() // Read from encrypted preference val 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 را ایجاد کنیم که در آن الگوریتم رمزگذاری کلید و مقدار را مینویسیم. کد به صورت زیر است:
import android.content.Context import android.content.SharedPreferences import android.util.Base64 import java.io.UnsupportedEncodingException import java.nio.charset.Charset import java.security.* import javax.crypto.Cipher import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.SecretKeySpec class EncryptedPreference @Throws(SecurePreferencesException::class) constructor(context: Context, preferenceName: String, secureKey: String, encryptKeys: Boolean) { private val encryptKeys: Boolean private val writer: Cipher private val reader: Cipher private val keyWriter: Cipher private val preferences: SharedPreferences protected val iv: IvParameterSpec get() { val iv = ByteArray(writer.blockSize) System.arraycopy("fldsjfodasjifudslfjdsaofshaufihadsf".toByteArray(), 0, iv, 0, writer.blockSize) return IvParameterSpec(iv) } class SecurePreferencesException(e: Throwable) : RuntimeException(e) init { try { this.writer = Cipher.getInstance(TRANSFORMATION) this.reader = Cipher.getInstance(TRANSFORMATION) this.keyWriter = Cipher.getInstance(KEY_TRANSFORMATION) initCiphers(secureKey) this.preferences = context.getSharedPreferences(preferenceName, Context.MODE_PRIVATE) this.encryptKeys = encryptKeys } catch (e: GeneralSecurityException) { throw SecurePreferencesException(e) } catch (e: UnsupportedEncodingException) { throw SecurePreferencesException(e) } } @Throws(UnsupportedEncodingException::class, NoSuchAlgorithmException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class) protected fun initCiphers(secureKey: String) { val ivSpec = iv val secretKey = getSecretKey(secureKey) writer.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec) reader.init(Cipher.DECRYPT_MODE, secretKey, ivSpec) keyWriter.init(Cipher.ENCRYPT_MODE, secretKey) } @Throws(UnsupportedEncodingException::class, NoSuchAlgorithmException::class) protected fun getSecretKey(key: String): SecretKeySpec { val keyBytes = createKeyBytes(key) return SecretKeySpec(keyBytes, TRANSFORMATION) } @Throws(UnsupportedEncodingException::class, NoSuchAlgorithmException::class) protected fun createKeyBytes(key: String): ByteArray { val md = MessageDigest .getInstance(SECRET_KEY_HASH_TRANSFORMATION) md.reset() return md.digest(key.toByteArray(charset(CHARSET))) } fun putString(key: String, value: String?) { if (value == null) { preferences.edit().remove(toKey(key)).commit() } else { putValue(toKey(key), value) } } fun putBoolean(key: String, value: Boolean?) { if (value == null) { preferences.edit().remove(toKey(key)).commit() } else { putValue(toKey(key), java.lang.Boolean.toString(value)) } } fun putLong(key: String, value: Long) { putValue(toKey(key), java.lang.Long.toString(value)) } fun putInt(key: String, value: Int) { putValue(toKey(key), Integer.toString(value)) } fun containsKey(key: String): Boolean { return preferences.contains(toKey(key)) } fun removeValue(key: String) { preferences.edit().remove(toKey(key)).commit() } @Throws(SecurePreferencesException::class) fun getString(key: String, value: String): String { if (preferences.contains(toKey(key))) { val securedEncodedValue = preferences.getString(toKey(key), "") return decrypt(securedEncodedValue) } return value } @Throws(SecurePreferencesException::class) fun getLong(key: String, value: Long): Long { if (preferences.contains(toKey(key))) { val securedEncodedValue = preferences.getString(toKey(key), "") return java.lang.Long.parseLong(decrypt(securedEncodedValue)) } return value } @Throws(SecurePreferencesException::class) fun getBoolean(key: String, value: Boolean): Boolean { if (preferences.contains(toKey(key))) { val securedEncodedValue = preferences.getString(toKey(key), "") return java.lang.Boolean.parseBoolean(decrypt(securedEncodedValue)) } return value } @Throws(SecurePreferencesException::class) fun getInt(key: String, value: Int): Int { if (preferences.contains(toKey(key))) { val securedEncodedValue = preferences.getString(toKey(key), "") return Integer.parseInt(decrypt(securedEncodedValue)) } return value } fun commit() { preferences.edit().commit() } fun clear() { preferences.edit().clear().commit() } private fun toKey(key: String): String { return if (encryptKeys) encrypt(key, keyWriter) else key } @Throws(SecurePreferencesException::class) private fun putValue(key: String, value: String) { val secureValueEncoded = encrypt(value, writer) preferences.edit().putString(key, secureValueEncoded).commit() } @Throws(SecurePreferencesException::class) protected fun encrypt(value: String, writer: Cipher): String { val secureValue: ByteArray try { secureValue = convert(writer, value.toByteArray(charset(CHARSET))) } catch (e: UnsupportedEncodingException) { throw SecurePreferencesException(e) } return Base64.encodeToString(secureValue, Base64.NO_WRAP) } protected fun decrypt(securedEncodedValue: String): String { val securedValue = Base64 .decode(securedEncodedValue, Base64.NO_WRAP) val value = convert(reader, securedValue) try { return String(value, Charset.forName(CHARSET)) } catch (e: UnsupportedEncodingException) { throw SecurePreferencesException(e) } } companion object { private val TRANSFORMATION = "AES/CBC/PKCS5Padding" private val KEY_TRANSFORMATION = "AES/ECB/PKCS5Padding" private val SECRET_KEY_HASH_TRANSFORMATION = "SHA-256" private val CHARSET : String = "UTF-8" private var instance: EncryptedPreference? = null fun getInstance(contxt: Context, prefName: String): EncryptedPreference { if (instance == null) { instance = EncryptedPreference(contxt, prefName, BuildConfig.securekey, true) } return instance as EncryptedPreference } @Throws(SecurePreferencesException::class) private fun convert(cipher: Cipher, bs: ByteArray): ByteArray { try { return cipher.doFinal(bs) } catch (e: Exception) { throw SecurePreferencesException(e) } } } }
اکنون کافی است تابع getinstance را با ارسال context و نام preference خود فراخوانی کنید. به خاطر داشته باشید که باید کلید امنیتی را در فایل build.gradle ذکر کنید. شیوه نوشتن و خواندن دادهها نیز به صورت زیر است:
// Write data val encryptedPreference = EncryptedPreference.getInstance(context,preferncename) encryptedPreference.putString("name",usertext.text.toString()) // Read data encryptedPreference.getString("name","")
بدین ترتیب به پایان این مقاله میرسیم.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای پروژه محور برنامه نویسی اندروید
- مجموعه آموزشهای برنامهنویسی
- آموزش ساخت اپلیکیشن کتاب و کار با داده ها در اندروید
- نکات کلیدی اندروید ۱۰ برای توسعه دهندگان — راهنمای کاربردی
- طراحی انیمیشن های ساده برای اپلیکیشن های اندرویدی — به زبان ساده
==