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

۲۴ بازدید
آخرین به‌روزرسانی: ۱۷ دی ۱۳۹۸
زمان مطالعه: ۵ دقیقه

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

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

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

==

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

نظر شما چیست؟

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