سلام سلام ..
باتوجه به اینکه از مقاله های قبلی استقبال خوبی شد؛ تصمیم گرفتم یه مقاله جدید که مکمل خیلی خوبی برای این مقاله هست رو با یه پیاده سازی کامل تر از یه رمزنگاری امن و چفت شده با بایومتریک که در مقابل حملات هکرها مقاوم هست رو برای شما بنویسم(لینک سورس کد در گیت هاب).
خب بریم سراغ اصل موضوع :)
هدف اصلی این مقاله اینه که ما رمزنگاری رو با بیشترین حد ممکن که در مقابل حملات Frida مقاوم باشه رو انجام بدیم.
فریدا یک ابزار سازگار با کد پویا است که به مهندسان معکوس اجازه میدهد تا پارچههایی از جاوااسکریپت یا کتابخانه را به برنامههای نیتیو بر روی iOS و Android تزریق کنند و یک فرآیند در حال اجرا را دیباگ کنند. به عبارت دیگر، به شما اجازه میدهد تا کد جاوااسکریپت سفارشی خود را تزریق کرده و به صورت برنامهای و تعاملی فرآیندهای در حال اجرا را بررسی و تغییر دهید. زیرا فریدا ابزاری برای ابزاردهی پویا استفاده میکند، احتیاجی به دسترسی به کد منبع برنامه ندارد. علاوه بر این، اگر دستگاه شما دسترسی روت یا Jailbreak ندارد، نگران نباشید، فریدا همچنین بر روی دستگاههای عادی نیز کار میکند.
یکی از حملاتی که با فریدا انجام میدن "حمله دور زدن احراز هویت بیومتریک" هست. بطور خیلی خلاصه بهتون بگم که این حمله با یه کد جاوا اسکریپت(لینک سورس کد حمله در گیت هاب) باعث صدا زده شدن متد onAuthenticationSucceeded در BiometricPrompt میشود. حالا اگر شما اگر موقع باز کردن بایومتریک آبجکت BiometricPrompt.CryptoObject(cipher) رو به اون پاس ندید و حین رمزنگاری و رمزگشایی از cipher موجود در BiometricPrompt.AuthenticationResult استفاده نکنید یه کیس خیلی خوبی برای این حمله هستید.
بریم سراغ یه پیاده سازی خفن که بصورت کامل همه چیز به هم دیگه چفت شده و قابل دور زدن با این حملات نباشه :)
۱. الگوریتم انتخابی ما AES هست که جزو الگوریتم های پیچیده و امن توی دنیای آی تی شناخته میشه. بعلاوه اینکه این کلید رو با استفاده از AndroidKeyStore میسازیم. همونطور که میدونید KeyStore یه بخش محافظت شده ای هست که دسترسی به کلید تولید شده رو خیلی سخت میکنه:
KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
۲. برای این کلید یه اسم انتخاب میکنیم. بعلاوه اینکه هدف مون از ایجاد کلید که رمزنگاری و رمزگشایی هست رو بهش ست میکنیم:
KeyGenParameterSpec.Builder( val cipher = Cipher.getInstance("AES/GCM/NoPadding") val secretKey = keyStoreManager.getKeyWithAlias("sample_symmetric_key_alias") cipher.init(Cipher.ENCRYPT_MODE, secretKey), KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT )
۳. سایز این کلید بعلاوه بلاک مود و همچنین نوع پدینگ رمزنگاری رو ست میکنیم:
setKeySize(256) setBlockModes(KeyProperties.BLOCK_MODE_GCM) setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
۴. این کلید فقط از طریق بایومتریک قابل دسترس باشه ولذا وقتی بخواییم رمزنگاری یا رمزگشایی کنیم کاربر باید فینگرپرینت فعال داشته باشه و از طریق دیالوگ prompt فینگرپرینت سیستم عامل مطمن بشیم که یوزر صاحب دیوایس هست:
setUserAuthenticationRequired(true)
۵. در هربار تولید متن رمزشده یک متن رمزشده رندوم تولید بشه تا از حملات ممکنه جلوگیری کنه:
setRandomizedEncryptionRequired(true)
۶. اگر فینگرپرینت جدیدی توی دیوایس اضافه شد این کلید ما نامعتبر بشه:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { setInvalidatedByBiometricEnrollment(true) }
۷. موقع رمزگشایی حتما قفل صفحه باز باشه و اگر دیوایس از StrongBox پشتیبانی میکنه حتما فرآیند تولید کلید داخل اون انجام بشه(دو فضای ایجاد کلید TEE, StrongBox داریم)
val hasStrongBox = context.packageManager.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { setUnlockedDeviceRequired(true) if(hasStrongBox) setIsStrongBoxBacked(true) }
ابتدا باید یه cipher اولیه با استفاده از کلیدی که تو مرحله قبلی ایجاد کردیم رو مقداردهی اولیه بکنیم:
val encryptCipher: Cipher = Cipher.getInstance("AES/GCM/NoPadding") val secretKey: SecretKey = keyStoreManager.getKeyWithAlias("sample_symmetric_key_alias") encryptCipher.init(Cipher.ENCRYPT_MODE, secretKey)
بعدش باید با استفاده از این cipher ی که init شده یه آبجکت کریپتو بسازیم و به BiometricPrompt پاس بدیم:
val prompt = BiometricPrompt(activity, ContextCompat.getMainExecutor(activity), AuthenticationCallback(out)) val info = PromptInfo.Builder() .setTitle("title") .setNegativeButtonText("description") .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG) .build() val crypto = BiometricPrompt.CryptoObject(encryptCipher) prompt.authenticate(info, crypto)
حالا بعد از بالا اومدن بایومتریک باید تو onAuthenticationSucceeded از آبجکت CryptoObject مقدار cipher رو استفاده کنیم:
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { super.onAuthenticationSucceeded(result) val newEncryptCipher = result.cryptoObject?.cipher ... }
از cipher مرحله قبل برای بخش رمزنگاری استفاده میکنیم:
val initializationVector = newEncryptCipher?.iv // we must save it val plainText = ... // user input text val cipheredBytes = newEncryptCipher?.doFinal(plaintext.toByteArray(StandardCharsets.UTF_8)) val base64EncryptedText = Base64.getEncoder().encode(cipheredBytes)
در این مرحله رمزنگاری ما به اتمام میرسه و میتونیم مقدار رمزشده رو بصورت base64 encode شده یا hex encode شده ذخیره سازی کنیم.
نکته: مقدار initialization vector یا iv تو این مرحله رو باید ذخیره کنیم(داخل سورس کد من با یه روشی ذخیره کردم که میتونید ازش استفاده کنید).
توی این بخش هم باید ابتدا یه cipher اولیه با استفاده از کلیدی که تو مرحله قبلی ایجاد کردیم و همچنین مقدار initialization vector ذخیره شده؛ مقداردهی اولیه بکنیم:
val decryptCipher = Cipher.getInstance("AES/GCM/NoPadding") val secretKey = keyStoreManager.getKeyWithAlias("sample_symmetric_key_alias") decryptCipher.init(Cipher.DECRYPT_MODE, secretKey, GCMParameterSpec(128, initializationVector))
بعدش باید با استفاده از این cipher ی که init شده یه آبجکت کریپتو بسازیم و به BiometricPrompt پاس بدیم:
val prompt = BiometricPrompt(activity, ContextCompat.getMainExecutor(activity), AuthenticationCallback(out)) val info = PromptInfo.Builder() .setTitle("title") .setNegativeButtonText("description") .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG) .build() val crypto = BiometricPrompt.CryptoObject(decryptCiphr) prompt.authenticate(info, crypto)
حالا بعد از بالا اومدن بایومتریک باید تو onAuthenticationSucceeded از آبجکت CryptoObject مقدار cipher رو استفاده کنیم:
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { super.onAuthenticationSucceeded(result) val newDecryptCipher = result.cryptoObject?.cipher ... }
بعد از دیکد کردن متن رمزشده ذخیره شده در مرحله رمزنگاری و همچنین استفاده از cipher مرحله قبل؛ رمزگشایی رو انجام میدیم:
val encryptedBytes = Base64.getDecoder().decode(base64EncryptedText) val text = newDecryptCipher?.doFinal(encryptedBytes) val plaintext = String(text, StandardCharsets.UTF_8)
توی این مرحله هم رمزگشایی ما به پایان میرسه و میتونیم از متن حاصل شده تو جاهای مختلف استفاده کنیم.
در پایان ما یاد گرفتیم که چطور یک کلید متقارن و امن با کلی ویژگی های امنیتی در اندروید تولید کنیم و بعد از اون نحوه رمزنگاری و رمزگشایی با استفاده از اون کلید که کاملا چفت شده با بایومتریک و مقاوم در مقابل حملات فریدا باشه رو یاد گرفتیم. منتظر نظرات سازنده شما هستم ..