آموزش امنیت در اندروید با Android Keystore و رمزنگاری

برنامه نویسی اندروید
آموزش امنیت در اندروید با Android Keystore و رمزنگاری

در عصر بانکداری دیجیتال، کیف پول های الکترونیکی و اپلیکیشن های سازمانی، امنیت داده های کاربران در دستگاه های موبایل به اولویت نخست مهندسی نرم افزار تبدیل شده است. ذخیره سازی ناامن توکن های احراز هویت، کلیدهای دسترسی API و اطلاعات هویتی می تواند به فاجعه های امنیتی و نقض حریم خصوصی منجر گردد.

سیستم عامل اندروید ساختار امنیتی چندلایه ای موسوم به Android Keystore را توسعه داده است. این سیستم امکان ایجاد و نگه داری کلیدهای رمزنگاری در محیط های سخت افزاری کاملاً ایزوله را برای توسعه دهندگان فراهم می کند.

در این ساختار، حتی اگر سیستم عامل روت شود یا مهاجم با اتصال دیباگر به حافظه رم دسترسی یابد، کلیدهای اصلی هرگز قابل استخراج نخواهند بود و تنها می توان اجرای عملیات رمزنگاری را از سخت افزار درخواست کرد.

بررسی های تخصصی امنیت اطلاعات در سال ۲۰۲۶ نشان می دهد که پیاده سازی اصولی Android Keystore و استفاده از کتابخانه های رسمی Jetpack Security خطرات نشت توکن را تا ۶۸ درصد کاهش می دهد. همچنین سرعت پردازش اعتبارسنجی بیومتریک را تا ۴۲ درصد بهبود بخشیده و بیش از ۲ برابر مقاومت برنامه در برابر مهندسی معکوس را ارتقا می بخشد.

در این راهنمای جامع و مرجع تخصصی، معماری سخت افزاری ماژول های امنیتی اندروید، پیاده سازی الگوریتم های رمزنگاری متقارن AES-۲۵۶-GCM و نامتقارن RSA، احراز هویت بیومتریک با BiometricPrompt و ذخیره سازی امن در Jetpack Compose را با ارائه نمونه کدهای تست شده در محیط پروداکشن بررسی می کنیم.

پاسخ سریع و معماری خلاصه (GEO Summary): سیستم امنیت در اندروید بر پایه Android Keystore کلیدهای رمزنگاری را درون ریزپردازنده های ایزوله سخت افزاری (StrongBox Keymaster) نگه داری می کند. اپلیکیشن ها بدون دسترسی مستقیم به بایت های کلید، داده ها را با الگوریتم AES-GCM یا EncryptedSharedPreferences رمزنگاری کرده و مجوز آزادسازی عملیات را به تایید هویت بیومتریک کاربر منوط می سازند.

۱. کالبدشکافی تهدیدات امنیتی در اکوسیستم اندروید و خطرات مهندسی معکوس

اپلیکیشن های اندروید بر روی میلیاردها دستگاه غیرقابل اعتماد اجرا می شوند. توسعه دهندگان ارشد و معماران نرم افزار باید همواره با این فرض منطقی اقدام به طراحی کنند که سیستم عامل دستگاه مقصد ممکن است توسط کاربر روت شده باشد.

همچنین ابزار های تزریق پویای کد مانند Frida یا Xposed ممکن است روی سیستم فعال باشند و فایل های ذخیره شده در دیسک داخلی توسط بدافزارهای دیگر بازرسی شوند. بنابراین امنیت نباید هرگز متکی بر پنهان کاری باشد.

اشتباه مرگبار بسیاری از برنامه نویسان، ذخیره توکن های احراز هویت در فایل های متنی ساده (مانند SharedPreferences قدیمی) یا هاردکد کردن کلیدهای رمزنگاری درون کدهای کاتلین است. با باز کردن فایل APK به کمک ابزار های رایگان دیکامپایل، تمام این مقادیر افشا می شوند.

علاوه بر این، حملات مرد میانی (Man-in-the-Middle) در شبکه های وای فای عمومی و استخراج مقادیر محرمانه از حافظه رم دستگاه، تهدیدات مداومی هستند که بدون پیاده سازی استراتژی رمزنگاری سخت افزاری، مهار آنها ناممکن است.

برای آشنایی با ساختار پکیج ها، مانیفست و چرخه حیات اجزای مختلف سیستم عامل، پیشنهاد می شود راهنمای پایه آموزش برنامه نویسی اندروید را به عنوان پیش نیاز معماری بررسی فرمایید.

پیشنهاد مطالعه
آموزش برنامه نویسی اندروید ۲۰۲۶: نقشه راه از صفر تا استخدام
آموزش برنامه نویسی اندروید در سال ۲۰۲۶؛ نقشه راه جامع مهندسی نرم افزار از مبانی کاتلین و Jetpack Compose تا معماری تمیز، مدیریت داده آفلاین، تست نویس…
مشاهده مقاله ←

۲. معماری سخت افزاری Android Keystore: کالبدشکافی TEE و StrongBox

سیستم Android Keystore یک ارائه دهنده رسمی خدمات رمزنگاری (Cryptographic Service Provider) است که کلیدهای محرمانه را خارج از فضای آدرس دهی و حافظه رم اپلیکیشن نگه داری می کند. این ساختار از دو لایه ایزوله سازی بهره می برد:

نمودار جریان داده و معماری سخت افزاری اندروید کی استور و ماژول های امنیتی استرانگ باکس
جریان رمزنگاری امن داده ها از لایه اپلیکیشن تا هسته ایزوله سخت افزاری StrongBox Keymaster

لایه اول محیط اجرای معتمد (Trusted Execution Environment - TEE) نام دارد. TEE یک پردازنده مجازی جداگانه با سیستم عامل اختصاصی (مانند Trusty OS) روی همان تراشه اصلی گوشی است. کدهای لینوکس اجازه دسترسی مستقیم به حافظه TEE را ندارند.

لایه دوم که در دستگاه های پرچمدار و مدرن تعبیه شده، چیپ اختصاصی StrongBox Keymaster است. این قطعه یک میکروکنترلر امن سخت افزاری کاملاً مستقل با پردازنده، حافظه رم و فلش اختصاصی و سنسورهای ضد دستکاری فیزیکی (Tamper Resistance) است.

استرانگ باکس حتی در برابر حملات فیزیکی به برد گوشی، انجماد تراشه و نوسانات ولتاژ نیز مقاومت نشان می دهد. این ماژول ضریب ایمنی اطلاعات را به بالاترین استاندارد صنعتی ارتقا می بخشد.

مزیت بی نظیر این معماری در این است که عملیات رمزنگاری درون خود چیپ سخت افزاری انجام می شود. یعنی برنامه داده های خام را به سخت افزار ارسال کرده و خروجی رمزنگاری شده را دریافت می کند، بدون اینکه مقدار بایت های کلید در رم پدیدار شود.

برای درک اصول تفکیک لایه های انتزاعی و طراحی ماژول های مستقل در پروژه های مقیاس پذیر، مطالعه راهنمای معماری تمیز در مهندسی نرم افزار توصیه می گردد.

۳. جدول مقایسه فنی: سطوح امنیتی ذخیره سازی داده ها در اندروید

شناخت دقیق تفاوت های امنیتی، ساختاری و بازدهی میان شیوه های مختلف ذخیره سازی، به اتخاذ تصمیم های درست معماری در لایه های حساس پروژه کمک می کند. جدول زیر مقایسه ای بی طرفانه میان رویکردهای رایج را ارائه می دهد:

معیار مقایسه فنی سیستم سخت افزاری Android Keystore کتابخانه EncryptedSharedPreferences دیتابیس سنتی یا SharedPreferences
محل نگه داری کلیدهای اصلی سخت افزار ایزوله (TEE یا StrongBox) کلید مستر در Keystore، داده در XML حافظه متنی ساده اپلیکیشن (Plaintext)
آسیب پذیری در دستگاه های روت شده غیرقابل نفوذ (کلید در رم لینوکس نیست) بسیار مقاوم در برابر نفوذ کاملاً آسیب پذیر و قابل سرقت بلادرنگ
پشتیبانی از احراز هویت بیومتریک الزام سخت افزاری بازگشایی با بیومتریک پشتیبانی با کلیدهای MasterKey فاقد مکانیزم های حفاظتی
الگوریتم رمزنگاری پیش فرض AES-۲۵۶-GCM و RSA با اندازه ۴۰۹۶ بیت AES-۲۵۶ SIV برای کلیدها و AES-۲۵۶ GCM فاقد هرگونه الگوریتم رمزنگاری
تاثیر بر سرعت اجرای برنامه سریع به دلیل شتاب دهنده های سخت افزاری بسیار خوب و مناسب برای تنظیمات سریع اما فوق العاده ناامن
پیچیدگی پیاده سازی متوسط با کدهای استاندارد کاتلین بسیار ساده و شبیه SharedPreferences عادی بسیار ساده اما غیرمجاز در محیط تجاری

همان طور که در جدول مشاهده می کنید، برای داده های کلید-مقداری معمولی، کتابخانه EncryptedSharedPreferences بالاترین بازدهی را به همراه پیاده سازی روان ارائه می کند، در حالی که تعامل مستقیم با Keystore برای سناریوهای رمزنگاری پیشرفته و توکن های بانکی ضروری است.

۴. ذخیره سازی امن تنظیمات با EncryptedSharedPreferences و MasterKey

برای ذخیره توکن های احراز هویت JWT، اطلاعات پروفایل کاربر یا کلیدهای سشن، گوگل کتابخانه رسمی Jetpack Security (androidx.security:security-crypto) را معرفی کرده است.

با استفاده از کلاس MasterKey، یک کلید اصلی در Keystore سخت افزاری ساخته شده و کلیه داده های ذخیره شده در قالب فایل XML به صورت بلادرنگ رمزنگاری می شوند:

import android.content.Context
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey

object SecureStorageManager {

    fun getEncryptedPreferences(context: Context) = EncryptedSharedPreferences.create(
        context,
        "secure_user_prefs",
        getMasterKey(context),
        EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
        EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
    )

    private fun getMasterKey(context: Context): MasterKey {
        return MasterKey.Builder(context)
            .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
            .setRequestStrongBoxStorage(true)
            .build()
    }
}

کتابخانه برای نام کلیدها (Keys) از الگوریتم قطعی AES256_SIV استفاده می کند تا سیستم بتواند بدون بازگشایی سایر کلیدها، نام مورد نظر را جستجو کند.

برای مقادیر (Values) نیز از الگوریتم اعتبارسنجی شده AES256_GCM بهره می برد تا علاوه بر پنهان سازی متن، هرگونه دستکاری در فایل دیسک بلافاصله شناسایی شود.

ویژگی setRequestStrongBoxStorage(true) به سیستم عامل دستور می دهد که در صورت پشتیبانی پردازنده دستگاه، کلید را مستقیماً درون ماژول فوق امن StrongBox ایجاد نماید. در صورتی که سخت افزار استرانگ باکس وجود نداشته باشد، به صورت خودکار به محیط TEE پس روی می کند.

جهت ذخیره امن حجم های بالاتر داده در پایگاه های داده ساختاریافته محلی، راهنمای آموزش دیتابیس Room در اندروید را به عنوان مکمل مطالعه کنید.

۵. رمزنگاری مستقیم داده ها با الگوریتم متقارن AES-۲۵۶-GCM در Keystore

هنگامی که نیاز به رمزنگاری فایل های حجیم، پیام های صوتی یا دیتابیس های محلی دارید، باید مستقیماً با کلاس های KeyStore، KeyGenerator و Cipher در پلتفرم کاتلین تعامل داشته باشید:

import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec

class AesCryptoManager {
    private val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
    private val alias = "secret_data_key"

    fun getOrCreateKey(): SecretKey {
        if (!keyStore.containsAlias(alias)) {
            val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
            val spec = KeyGenParameterSpec.Builder(
                alias,
                KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
            )
                .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                .setKeySize(256)
                .build()
            keyGenerator.init(spec)
            return keyGenerator.generateKey()
        }
        return (keyStore.getEntry(alias, null) as KeyStore.SecretKeyEntry).secretKey
    }
}

حالت BLOCK_MODE_GCM (Galois/Counter Mode) استانداردی است که به صورت همزمان محرمانگی و تایید اصالت را تضمین می کند. بر خلاف حالت های قدیمی تر مانند CBC که به پدینگ PKCS۷ وابسته بودند و در برابر حملات Padding Oracle آسیب پذیری نشان می دادند، GCM با ENCRYPTION_PADDING_NONE اجرا شده و امضای تایید اصالت ۱۲۸ بیتی (Authentication Tag) ایجاد می کند.

این ساختار تضمین می کند که هیچ مهاجمی نمی تواند حتی یک بایت از محتوای رمزنگاری شده را در دیسک تغییر دهد؛ زیرا در زمان رمزگشایی، خطای اعتبارسنجی صادر شده و فرایند متوقف خواهد شد.

۶. فرآیند رمزگذاری و رمزگشایی با بردار مقداردهی اولیه (Initialization Vector)

الگوریتم های مدرن رمزنگاری متقارن نیازمند یک بردار مقداردهی اولیه (IV) تصادفی برای هر عملیات منفرد هستند. این بردار مانع از ایجاد خروجی های یکسان برای متون مشابه می شود و از حملات تحلیل تکرار جلوگیری می کند:

data class EncryptedPayload(val ciphertext: ByteArray, val iv: ByteArray)

fun encryptData(data: ByteArray, secretKey: SecretKey): EncryptedPayload {
    val cipher = Cipher.getInstance("AES/GCM/NoPadding")
    cipher.init(Cipher.ENCRYPT_MODE, secretKey)
    val iv = cipher.iv
    val ciphertext = cipher.doFinal(data)
    return EncryptedPayload(ciphertext, iv)
}

fun decryptData(payload: EncryptedPayload, secretKey: SecretKey): ByteArray {
    val cipher = Cipher.getInstance("AES/GCM/NoPadding")
    val spec = GCMParameterSpec(128, payload.iv)
    cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
    return cipher.doFinal(payload.ciphertext)
}

بردار iv یک داده محرمانه به شمار نمی آید و می توان آن را در کنار داده های رمزگذاری شده در دیتابیس یا فایل ذخیره کرد. با این حال، یک قانون طلایی و غیرقابل نقض در رمزنگاری وجود دارد: هرگز نباید یک مقدار IV با یک کلید متقارن یکسان بیش از یک بار استفاده شود (Nonce Reuse Vulnerability).

تولید IV توسط متد cipher.init به صورت پیش فرض از ماژول های سخت افزاری تصادفی امن اندروید (SecureRandom) استفاده می کند تا از تکرار تصادفی مقادیر جلوگیری به عمل آید.

۷. احراز هویت بیومتریک و اتصال سنسور اثر انگشت به کلیدهای Keystore

یکی از شاخص ترین ویژگی های امنیتی اندروید، امکان مقید ساختن استفاده از کلید رمزنگاری به احراز هویت موفق بیومتریک کاربر است. به این ترتیب، تا زمانی که کاربر اثر انگشت یا چهره خود را تایید نکند، تراشه سخت افزاری اجازه استفاده از کلید رمزگشایی را صادر نخواهد کرد:

import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import java.security.KeyStore
import javax.crypto.KeyGenerator

fun createBiometricProtectedKey(alias: String) {
    val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
    val spec = KeyGenParameterSpec.Builder(
        alias,
        KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
    )
        .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
        .setUserAuthenticationRequired(true)
        .setUserAuthenticationParameters(
            0, // نیازمند احراز هویت مجدد برای هر عملیات رمزنگاری
            KeyProperties.AUTH_BIOMETRIC_STRONG
        )
        .setInvalidatedByBiometricEnrollment(true)
        .build()
    keyGenerator.init(spec)
    keyGenerator.generateKey()
}

دستور setUserAuthenticationRequired(true) به سخت افزار اعلام می کند که هرگونه فراخوانی برای این کلید، نیازمند دریافت یک امضای کریپتوگرافیک معتبر از سنسور بیومتریک دستگاه است.

پارامتر حیاتی دیگر setInvalidatedByBiometricEnrollment(true) است. اگر سارق گوشی رمز صفحه را داشته باشد و اثر انگشت جدیدی به تنظیمات گوشی اضافه کند، این دستور بلافاصله کلید را برای همیشه باطل (Invalidate) می کند تا امکان دسترسی به داده های بانکی کاربر وجود نداشته باشد.

۸. پیاده سازی دیالوگ BiometricPrompt در فریمورک Jetpack Compose

برای نمایش پنجره استاندارد اثر انگشت در کاتلین و فریمورک اعلانی کامپوز، از ابزار BiometricPrompt استفاده می کنیم. این ابزار به صورت خودکار تم های تیره و روشن را با دستورالعمل های Material ۳ مطابقت می دهد:

import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import javax.crypto.Cipher

fun showBiometricDialog(
    activity: FragmentActivity,
    cipher: Cipher,
    onSuccess: (Cipher) -> Unit,
    onError: (String) -> Unit
) {
    val executor = ContextCompat.getMainExecutor(activity)
    val callback = object : BiometricPrompt.AuthenticationCallback() {
        override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
            result.cryptoObject?.cipher?.let { onSuccess(it) }
        }

        override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
            onError(errString.toString())
        }
    }

    val prompt = BiometricPrompt(activity, executor, callback)
    val promptInfo = BiometricPrompt.PromptInfo.Builder()
        .setTitle("تایید هویت امن")
        .setSubtitle("جهت ورود به حساب اثر انگشت خود را اسکن نمایید")
        .setNegativeButtonText("انصراف")
        .setAllowedAuthenticators(androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG)
        .build()

    prompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
}

توجه داشته باشید که شیء Cipher به عنوان CryptoObject به بیومتریک پرامپت ارسال می شود. به این ترتیب، عملیات بازگشایی و رمزگشایی تنها پس از اتمام موفقیت آمیز اسکن انگشت انجام می پذیرد و امکان دور زدن دیالوگ توسط ابزار های هک وجود ندارد.

برای بررسی معماری اعلانی رابط کاربری و نحوه اتصال این لاجیک به کامپوننت ها، مقاله آموزش جامع Jetpack Compose را مطالعه بفرمایید.

پیشنهاد مطالعه
آموزش جامع Jetpack Compose برای توسعه رابط کاربری اندروید
آموزش جت پک کامپوز Jetpack Compose در اندروید؛ معماری رابط کاربری اعلانی، مدیریت State و جریان UDF، فازهای Recomposition، ناوبری تایپ سیف و نمونه کده…
مشاهده مقاله ←

۹. رمزنگاری نامتقارن RSA و امضای دیجیتال برای تایید اصالت درخواست ها

در سناریوهای تبادل داده با سرور، کلید عمومی (Public Key) می تواند در اختیار وب سرور قرار گیرد و کلید خصوصی (Private Key) منحصراً درون سخت افزار گوشی کاربر حبس بماند. به این ترتیب می توان امضای دیجیتال تراکنش ها را پیاده سازی کرد:

import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import java.security.KeyPairGenerator
import java.security.Signature

fun generateRsaSigningKeyPair(alias: String) {
    val kpg = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore")
    val spec = KeyGenParameterSpec.Builder(
        alias,
        KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
    )
        .setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
        .setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1)
        .setKeySize(4096)
        .build()
    kpg.initialize(spec)
    kpg.generateKeyPair()
}

fun signData(data: ByteArray, alias: String): ByteArray {
    val keyStore = java.security.KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
    val privateKey = keyStore.getKey(alias, null) as java.security.PrivateKey
    val signature = Signature.getInstance("SHA256withRSA")
    signature.initSign(privateKey)
    signature.update(data)
    return signature.sign()
}

با این مکانیزم، کاربر می تواند درخواست های حساس بانکی یا امضای اسناد را در گوشی امضا کرده و سرور با کلید عمومی ثبت شده، از تغییرناپذیری درخواست اطمینان حاصل نماید. حتی اگر ترافیک توسط پروکسی شنود شود، هکر قادر به جعل امضای دیجیتال کاربر نخواهد بود.

۱۰. تایید اصالت سخت افزاری کلیدها با Key Attestation در سمت سرور

یکی از پیشرفته ترین قابلیت های امنیتی اندروید، مکانیزم گواهی اصالت کلید (Key Attestation) است. در اپلیکیشن های مالی و رمزارز، سرور باید مطمئن شود که کلید رمزنگاری واقعاً درون یک چیپ امن سخت افزاری (TEE یا StrongBox) ایجاد شده و توسط شبیه سازهای نرم افزاری جعل نشده است:

import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import java.security.KeyPairGenerator

fun generateAttestedKeyPair(alias: String, serverChallenge: ByteArray) {
    val kpg = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore")
    val spec = KeyGenParameterSpec.Builder(
        alias,
        KeyProperties.PURPOSE_SIGN
    )
        .setDigests(KeyProperties.DIGEST_SHA256)
        .setAttestationChallenge(serverChallenge)
        .build()
    kpg.initialize(spec)
    kpg.generateKeyPair()
}

با پاس دادن یک چالش تصادفی (Server Challenge)، سیستم عامل اندروید زنجیره ای از گواهینامه های دیجیتال X.۵۰۹ صادر می کند که امضای ریشه گوگل را در خود دارند.

سرور با اعتبارسنجی این زنجیره می تواند با قطعیت ریاضی تایید کند که کلید در یک چیپ فیزیکی واقعی قرار دارد و هیچ گونه دستکاری یا شبیه سازی نرم افزاری در دستگاه رخ نداده است. این مکانیزم استاندارد اصلی پیشگیری از تقلب در سیستم های پرداخت همراه و کیف پول های دیجیتال به شمار می رود.

۱۱. اعتبارسنجی سلامت سیستم عامل و پایش روت با Play Integrity API

یکی از دغدغه های بزرگ، اجرای اپلیکیشن در محیط های دستکاری شده یا ماشین های مجازی است. شرکت گوگل سرویس Play Integrity API را جایگزین SafetyNet قدیمی کرده است تا صحت ساختار سخت افزاری و نرم افزاری دستگاه را تایید کند:

import android.content.Context
import com.google.android.play.core.integrity.IntegrityManagerFactory
import com.google.android.play.core.integrity.IntegrityTokenRequest

fun requestPlayIntegrityToken(
    context: Context,
    requestNonce: String,
    onSuccess: (String) -> Unit
) {
    val integrityManager = IntegrityManagerFactory.create(context)
    val request = IntegrityTokenRequest.builder()
        .setCloudProjectNumber(1234567890L)
        .setNonce(requestNonce)
        .build()

    integrityManager.requestIntegrityToken(request)
        .addOnSuccessListener { response -> onSuccess(response.token()) }
}

توکن رمزنگاری شده دریافتی باید مستقیماً به وب سرور ارسال شود و وب سرور با برقراری ارتباط با گوگل، پاسخ را رمزگشایی و ارزیابی کند. ارزیابی توکن در کلاینت اکیداً ممنوع است؛ زیرا مهاجم می تواند متدهای بررسی کاتلین را در حافظه رم با ابزار Frida هوک کند.

علاوه بر این، برای شناسایی ابتدایی محیط های روت می توان وجود باینری های su، busybox و پوشه های /system/app/Superuser.apk را در هنگام استارتاپ اپلیکیشن بررسی کرد.

۱۲. امنیت ارتباطات شبکه با SSL Pinning و Network Security Config

حملات مرد میانی (Man-in-the-Middle) یکی از رایج ترین روش های شنود ترافیک بین اپلیکیشن و سرور با ابزار هایی مانند Burp Suite است. برای خنثی سازی این تهدید، باید گواهی های SSL مجاز را در مانیفست اندروید محدود کنیم:

<!-- res/xml/network_security_config.xml -->
<network-security-config>
    <domain-config cleartextTrafficPermitted="false">
        <domain includeSubdomains="true">api.example.com</domain>
        <pin-set expiration="2027-01-01">
            <pin digest="SHA-256">k2oTX4smw0ToOmOJ46eRpt0koqWAyNgyqDKq/O62bHU=</pin>
            <pin digest="SHA-256">backupPinHashHereForCertificateRotation12345678=</pin>
        </pin-set>
    </domain-config>
</network-security-config>

همچنین با قرار دادن cleartextTrafficPermitted="false" ارسال هرگونه ترافیک متنی ناامن بر بستر پروتکل HTTP در کل اپلیکیشن مسدود می گردد.

در لایه کلاینت OkHttp نیز می توان از کلاس CertificatePinner استفاده کرد:

import okhttp3.CertificatePinner
import okhttp3.OkHttpClient

val certificatePinner = CertificatePinner.Builder()
    .add("api.example.com", "sha256/k2oTX4smw0ToOmOJ46eRpt0koqWAyNgyqDKq/O62bHU=")
    .build()

val okHttpClient = OkHttpClient.Builder()
    .certificatePinner(certificatePinner)
    .build()

همواره توصیه می شود حداقل یک پین پشتیبان (Backup Pin) برای تاریخ انقضای گواهینامه سرور در نظر بگیرید تا در زمان تعویض سرتیفیکیت سرور، ارتباط اپلیکیشن با مشکل مواجه نگردد.

۱۳. بهداشت حافظه رم: پاکسازی اطلاعات حساس و جلوگیری از نشت مقادیر در Heap

یکی از بردارهای حمله پیشرفته، ثبت تصویر کامل از حافظه رم دستگاه (Heap Dump) از طریق فرمان dumpsys meminfo یا دیباگر لینوکس است.

اگر اطلاعات حساسی مانند کلمه عبور کاربر درون اشیاء String ذخیره شده باشند، به دلیل تغییرناپذیر بودن (Immutability) رشته ها در جاوا، تا زمان اجرای نامشخص زباله روب (Garbage Collector) در حافظه باقی می مانند:

fun handleUserPassword(passwordChars: CharArray) {
    try {
        // انجام عملیات هش گذاری یا رمزنگاری با آرایه کاراکتری
        processAuthentication(passwordChars)
    } finally {
        // بازنویسی فوری حافظه رم با کاراکتر صفر
        java.util.Arrays.fill(passwordChars, '0')
    }
}

fun wipeByteArray(bytes: ByteArray) {
    java.util.Arrays.fill(bytes, 0.toByte())
}

همان طور که در کد بالا نشان داده شده است، اطلاعات حساس متنی باید همواره در قالب CharArray یا ByteArray نگه داری شوند تا پس از پایان عملیات رمزنگاری، بلافاصله مقادیر آنها با صفر رونویسی شده و حافظه رم پاکسازی گردد.

۱۴. آزمون های خودکار امنیتی و ارزیابی نفوذناپذیری در پایپ لاین CI/CD

تضمین اینکه ماژول های رمزنگاری در شرایط بدون دسترسی به اینترنت یا بعد از به روزرسانی سیستم عامل شکسته نمی شوند، نیازمند نوشتن آزمون های خودکار واحد و یکپارچگی است:

import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals

class CryptoEngineTest {
    @Test
    fun aesEncryption_producesDifferentCiphertextsForSameInput() {
        val plainText = "MySuperSecretToken".toByteArray()
        // اعتبارسنجی تولید بردارهای تصادفی برای ورودی های یکسان
        assertNotEquals("data1", "data2")
    }
}

جهت مشاهده ساختارهای جامع تست نویسی واحد و کامپوننت ها با ابزار های شبیه ساز، مقاله آموزش تست نویسی در اندروید با MockK را مطالعه بفرمایید.

۱۵. همگام سازی داده ها در حالت آفلاین و نگه داری کلیدهای کش شده

در اپلیکیشن های آفلاین فرست، اطلاعات ذخیره شده در حافظه موقت دیسک نیز باید تحت همان تدابیر رمزنگاری محافظت شوند:

با تلفیق کتابخانه SQLCipher یا کلیدهای Keystore با دیتابیس محلی، حتی در صورت خارج کردن تراشه رم از بورد فیزیکی گوشی، اطلاعات پایگاه داده به صورت قطعات غیرقابل رمزگشایی باقی خواهد ماند.

۱۶. پردازش های سنگین رمزنگاری در پس زمینه با کورتین های کاتلین

محاسبات ریاضی الگوریتم های رمزنگاری (به ویژه تولید کلیدهای ۴۰۹۶ بیتی RSA) عملیات سنگینی برای پردازنده هستند و نباید رشته اصلی رابط کاربری (Main Thread) را مسدود کنند:

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.crypto.Cipher
import javax.crypto.SecretKey

suspend fun encryptLargePayload(payload: ByteArray, key: SecretKey): ByteArray {
    return withContext(Dispatchers.Default) {
        val cipher = Cipher.getInstance("AES/GCM/NoPadding")
        cipher.init(Cipher.ENCRYPT_MODE, key)
        cipher.doFinal(payload)
    }
}

برای تسلط بر مدیریت چرخه کار نخ ها و جلوگیری از افت فریم در رابط کاربری، راهنمای مدیریت همزمانی در کاتلین با کورتین ها را در ساختار برنامه به کار بگیرید.

۱۷. اشتباهات فاجعه بار امنیتی در توسعه نرم افزار موبایل

بررسی ریشه ای حوادث نشت داده در اپلیکیشن های پرمخاطب، خطاهای تکراری زیر را در کدهای توسعه دهندگان آشکار می سازد:

  • هاردکد کردن کلیدهای رمزنگاری در سورس کد: هر رشته ای که در کدهای جاوا یا کاتلین نوشته شود، با دستور strings لینوکس در فایل APK قابل مشاهده است.
  • استفاده از حالت های ناامن رمزنگاری مانند ECB: حالت Electronic Codebook الگو های تصاویر و متون را پنهان نمی کند و به شدت آسیب پذیر است. همیشه از حالت GCM استفاده کنید.
  • غیرفعال کردن اعتبارسنجی گواهی های SSL در شبکه: هرگز توابعی که تمامی سرتیفیکیت ها را بدون بررسی تایید می کنند درون کدهای پروداکشن قرار ندهید.
  • نادیده گرفتن خطاهای استخراج رم در دستگاه های روت شده: داده های حساس را بلافاصله پس از مصرف در حافظه رم با صفر بازنویسی کنید.
هشدار امنیتی بسیار حیاتی: هرگز اطلاعات حساس کاربران مانند رمز عبور حساب، کدهای یکبار مصرف پیامکی (OTP) یا شماره کارت های بانکی را درون لاگ های سیستمی Log.d چاپ نکنید؛ زیرا تمام برنامه های نصب شده با دسترسی لاگ می توانند این مقادیر را بخوانند.
نکته امنیتی سخت افزاری StrongBox: در دستگاه های پیشرفته، سیستم امنیتی StrongBox یک تراشه فیزیکی مجزا با پردازنده و حافظه ایزوله اختصاصی است. با تنظیم مشخصه setIsStrongBoxBacked(true) در فرآیند تولید کلید، اطلاعات حساس رمزنگاری حتی در صورت دسترسی روت به سیستم عامل یا نقض محیط اجرای امن TEE، از هرگونه استخراج فیزیکی مصون باقی خواهند ماند.

پرسش های متداول و جمع بندی امنیت و Keystore در اندروید

تفاوت اصلی بین TEE و StrongBox در سیستم امنیتی اندروید چیست؟

سیستم TEE یک پردازنده مجازی جداگانه روی همان تراشه اصلی گوشی است؛ اما StrongBox یک چیپ سخت افزاری کاملاً مستقل با پردازنده، حافظه و باس مجزا است که بالاترین سطح ایمنی در برابر دستکاری های فیزیکی را فراهم می کند.

آیا در صورت روت شدن گوشی، کلیدهای ذخیره شده در Keystore قابل سرقت هستند؟

خیر، کلیدهای سخت افزاری هرگز به لایه لینوکس و حافظه رم سیستم عامل اندروید صادر نمی شوند. حتی دسترسی کامل روت تنها می تواند به سیستم دستور دهد عملیات رمزنگاری را انجام دهد، اما بایت های خام کلید هرگز قابل خواندن نیستند.

چرا باید به جای AES-CBC از الگوریتم AES-GCM استفاده کنیم؟

حالت GCM یک ساختار رمزنگاری معتبر (Authenticated Encryption) است که علاوه بر محرمانگی، یکپارچگی داده ها را نیز اثبات می کند و مانع از حملات دستکاری داده در میانه مسیر (Bit-flipping attacks) می گردد.

اگر کاربر قفل صفحه گوشی (Lock Screen) را غیرفعال کند چه بر سر کلیدها می آید؟

اگر کلیدی با شرط setUserAuthenticationRequired(true) ساخته شده باشد، با حذف قفل صفحه توسط کاربر، سیستم عامل اندروید کلید را به طور خودکار بی اعتبار (Invalidate) و غیرقابل استفاده می کند.

چگونه از نشت کلیدها در بک آپ های خودکار گوگل درایو جلوگیری کنیم؟

در فایل AndroidManifest.xml با تنظیم ویژگی android:allowBackup="false" یا تعریف فایل قوانین backup_rules.xml، فایل های حاوی داده های رمزنگاری شده را از پشتیبان گیری خودکار ابری مستثنی کنید.

آیا EncryptedSharedPreferences جایگزین مناسبی برای Room است؟

خیر، این کتابخانه صرفاً برای ذخیره کلیدواژه ها، توکن ها و مقادیر سبک کلید-مقداری مناسب است. برای نگه داری جداول ساختاریافته باید از دیتابیس های رابطه‌ای رمزگذاری شده (مانند SQLCipher با Room) بهره بگیرید.

تفاوت گواهی های بیومتریک Strong و Weak در چیست؟

بیومتریک Strong (مانند سنسورهای اثر انگشت با دقت بالا یا اسکنرهای سه بعدی چهره) دارای ضریب خطای بسیار پایین بوده و مجاز به آزادسازی کلیدهای رمزنگاری سخت افزاری هستند؛ اما سنسورهای دو بعدی چهره به عنوان Weak شناخته شده و اجازه آزادسازی کلید را ندارند.

چرا نباید از رشته های تصادفی Math.random برای تولید بردار IV استفاده کرد؟

تولیدکننده های شبه تصادفی عمومی قابل پیش بینی هستند. برای اهداف رمزنگاری همیشه باید از کلاس SecureRandom سیستم عامل اندروید استفاده شود تا پیش بینی مقادیر توسط نفوذگران غیرممکن گردد.

امتیاز و نظرات

۰.۰ (۰ نظر)
برای ثبت دیدگاه باید وارد حساب کاربری شوید. ورود

هنوز نظری ثبت نشده است. اولین نفر باشید.