آموزش رمزنگاری در اندروید
آیا تا به حال به این فکر کرده اید که چگونه می توانید از رمزنگاری (encryption)داده ها برای ایمن سازی اطلاعات خصوصی کاربر خود در برابر هکرها استفاده کنید؟ ما در این مقاله به شما این کار را آموزش خواهیم داد!
با وجود تمام نقضهای اخیر دادهها و قوانین جدید حفظ حریم خصوصی، مانند GDPR، اعتبار و قابل قبول بودن برنامه شما به نحوه مدیریت دادههای کاربر بستگی دارد. API های قدرتمندی در اندروید وجود دارند که بر روی رمزنگاری داده ها تمرکز دارند که گاهی اوقات هنگام شروع یک پروژه نادیده گرفته می شوند. می توانید به امنیت از پایه فکر کنید و از آنها استفاده نمایید.
در این آموزش، شما اپلیکیشنی را برای کلینیک های دامپزشکی ایمن خواهید کرد که اطلاعات پزشکی را ذخیره می کند. در طول فرآیند، شما یاد خواهید گرفت که چگونه:
- مجوزهای برنامه را محکم کاری(tighten) کنید
- داده های خود را رمزنگاری کنید
- از KeyStore استفاده کنید
توجه: در این آموزش فرض می شود که شما قبلاً با اصول توسعه اندروید و اندروید استودیو آشنا هستید. اگر شما تازه با توسعه اندروید آشنا شده اید، ابتدا آموزش های ابتدایی توسعه اندروید و کاتلین برای اندروید را بخوانید.
شروع
یک صفحه ثبت نام(sign-up) ساده خواهید دید. پس از وارد کردن password و انتخاب Signup ، در راه اندازی بعدی برنامه از شما خواسته می شود که password را وارد کنید. پس از این مرحله، لیستی از حیوانات خانگی دریافت خواهید کرد. بیشتر برنامه کامل است، بنابراین روی ایمن کردن آن تمرکز خواهید کرد. برای نشان دادن اطلاعات پزشکی حیوان خانگی، روی یک مورد در لیست کلیک کنید:
اگر در Android 7+ با خطای java.lang.SecurityException: MODE_WORLD_READABLE no longer supported ، مواجه شدید نگران نباشید. بزودی درستش می کنید.
ایمن سازی زیرساخت ها
برای شروع رمزنگاری برنامه های خود و ایمن سازی داده های مهم، ابتدا باید از نشت داده ها به سایر نقاط جلوگیری کنید. بدین ترتیب باید از دادههای مبتنی بر کاربر شما در برابر خواندن توسط هر برنامه دیگری محافظت شود و مکان نصب برنامهها نیز محدود گردد. بیایید ابتدا این کار را انجام دهیم تا بتوانید رمزنگاری اطلاعات خصوصی را شروع کنید.
به کار بردن Permission ها
هنگامی که برای اولین بار شروع به ساخت برنامه خود می کنید، به این فکر کنید که واقعاً چه مقدار از داده های کاربر را باید نگه دارید. این روزها، بهترین روش این است که اگر مجبور نیستید ، از ذخیره داده های خصوصی (private data) خودداری کنید .
از Android 6.0 به بعد ، فایلها و SharedPreferences که ذخیره میکنید با ثابت MODE_PRIVATE تنظیم میشوند. این بدان معناست که فقط برنامه شما می تواند به داده ها دسترسی داشته باشد. اندروید 7 هیچ گزینه دیگری را اجازه نمی دهد. بنابراین اول از همه، مطمئن خواهید شد که پروژه به طور ایمن تنظیم شده است.
فایل MainActivity.kt را باز کنید. متوجه خواهید شد که دو اخطار deprecate برای MODE_WORLD_READABLE و MODE_WORLD_WRITABLE وجود دارد. این امکان دسترسی عمومی به فایل های شما در نسخه های قبلی اندروید را فراهم می کند. خطی را که MODE_WORLD_WRITABLE را تنظیم می کند پیدا کنید و آن را با خط زیر جایگزین کنید:
val preferences = getSharedPreferences(“MyPrefs”, Context.MODE_PRIVATE)
سپس، خطی را که MODE_WORD_READABLE را تنظیم می کند، پیدا کنید و آن را با این جایگزین کنید:
val editor = getSharedPreferences(“MyPrefs”, Context.MODE_PRIVATE).edit()عالی است، شما فقط تنظیمات خود را کمی ایمن تر کرده اید! علاوه بر این، اگر اکنون برنامه را Build و Run کنید ، نباید با crashکردن قبلی مواجه شوید. اکنون باید یک مکان امن برای دایرکتوری نصب برنامه خود اعمال کنید.
محدود کردن دایرکتوری های نصب
یکی از مشکلات بزرگتری که اندروید در چند سال گذشته با آن روبرو بوده، نداشتن حافظه کافی برای نصب تعداد زیادی اپلیکیشن است. که بیشتر به دلیل ظرفیت ذخیرهسازی پایین دستگاهها بود، اما از آنجایی که تکنولوژی پیشرفت کرده است و تلفنها تا حدودی ارزانتر شدهاند، اکنون بیشتر دستگاهها فضای ذخیرهسازی زیادی را برای تعداد زیادی برنامه دارند. با این حال، برای کاهش حافظه ناکافی، Android به شما اجازه می دهد برنامه ها را در حافظه خارجی نصب کنید. این روش عملکرد بسیار خوبی داشت، اما در طول سال ها، باعث نگرانی های امنیتی زیادی در مورد این رویکرد مطرح شده است. نصب برنامهها روی کارتهای SD خارجی یک راه جالب برای حفظ فضای ذخیرهسازی است، اما این کار یک نقص امنیتی است، زیرا هر کسی که به کارت SD دسترسی دارد به دادههای برنامه نیز دسترسی دارد. و این داده ها می توانند اطلاعات حساسی را در خود نگه دارند. به همین دلیل است که توصیه می شود نصب برنامه خود را در حافظه داخلی محدود کنید.
برای انجام این کار، فایل AndroidManifest.xml را باز کنید و خطی را پیدا کنید که android:installLocation=”auto” را میخواند و آن را با عبارت زیر جایگزین کنید:
android:installLocation="internalOnly"
اکنون، مکان نصب(install location) در دستگاه محدود شده است، اما همچنان میتوانید از برنامه و دادههای آن نسخه پشتیبان تهیه کنید. این بدان معناست که کاربران می توانند با استفاده از نسخه پشتیبان adb ، به محتویات پوشه داده های خصوصی برنامه دسترسی پیدا کنند. برای ممنوع کردن پشتیبانگیری، خطی را پیدا کنید که android:allowBackup=”true” را میخواند و مقدار را با “false” جایگزین کنید.
با پیروی از این تکنیک ها، تا حدی برنامه خود را پایدار کرده اید. با این حال، میتوانید این معیارهای مجوز را در دستگاه روت شده دور بزنید. راه حل این است که داده ها را با بخشی از اطلاعات که attacker های مستعد، نمی توانند پیدا کنند، رمزنگاری کنید.
ایمن سازی اطلاعات کاربر با رمز عبور
داده ها را با یک استاندارد توصیه شده معروف، استاندارد رمزنگاری پیشرفته(Advanced Encryption Standard) (AES) رمزنگاری خواهید کرد. AES از شبکه جانشینی جایگشت (Substitution Permutation Network) برای رمزنگاری داده های شما با یک کلید استفاده می کند. با استفاده از این رویکرد، بایتهای یک جدول را با بایتهای جدول دیگر جایگزین میکند و به این ترتیب جایگشت دادهها را ایجاد میکند. برای شروع استفاده از AES، ابتدا باید کلید رمزنگاری را ایجاد کنید، پس بیایید این کار را انجام دهیم.
ایجاد یک کلید
همانطور که در بالا ذکر شد، AES از یک کلید برای رمزنگاری استفاده می کند. از همین کلید برای رمزگشایی داده ها نیز استفاده می شود. به این روش رمزنگاری متقارن می گویند. طول کلید می تواند متفاوت باشد، اما 256 بیت استاندارد است. استفاده مستقیم از رمز عبور کاربر برای رمزنگاری خطرناک است. احتمالاً random یا به اندازه کافی بزرگ نخواهد بود. به این ترتیب رمز عبور کاربر با کلید رمزنگاری متفاوت است.
تابعی به نام Password-Based Key Derivation Function (PBKDF2) به کمک می آید. یک رمز عبور می گیرد و با hash کردن چندین بار آن با داده های تصادفی، یک کلید ایجاد می کند. داده های تصادفی salt نامیده می شود. این کار یک کلید قوی و منحصر به فرد ایجاد می کند، حتی اگر شخص دیگری از رمز عبور مشابه استفاده کند.
از آنجایی که هر کلید منحصر به فرد است، اگر یک attacker، کلید را بدزدد و به صورت آنلاین منتشر کند، کلید همه کاربرانی که از رمز عبور یکسان استفاده کردهاند افشا نخواهد شد.
با تولید salt شروع کنید. فایل Encryption.kt را باز کنید و کد زیر را به جای خط TODO: Add code here// به روش رمزنگاری اول اضافه کنید،
val random = SecureRandom()
val salt = ByteArray(256)
random.nextBytes(salt)
در اینجا، شما از کلاس SecureRandom استفاده می کنید، که اطمینان حاصل می کند که پیش بینی خروجی دشوار است. به آن تولید کننده اعداد تصادفی رمزنگاری قوی (cryptographically strong random number generator) می گویند.
اکنون، یک کلید با رمز عبور کاربری و salt ایجاد خواهید کرد. زیر کدی که به تازگی اضافه کرده اید، این خط را اضافه کنید:
val pbKeySpec = PBEKeySpec(password, salt, 1324, 256) // 1
val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1") // 2
val keyBytes = secretKeyFactory.generateSecret(pbKeySpec).encoded // 3
val keySpec = SecretKeySpec(keyBytes, "AES") // 4
شرح کد :
- salt و رمز عبور را در PBEKeySpec، یک شی رمزنگاری مبتنی بر رمز عبور قرار دهید. سازنده تعداد تکرار می گیرد (1324). هرچه این عدد بیشتر باشد، مدت زمان بیشتری طول میکشد تا روی مجموعهای از کلیدها در طول یک حمله brute force عمل کنید.
- PBEKeySpec را به SecretKeyFactory منتقل کرد.
- کلید به صورت ByteArray تولید شد.
- ByteArray خام را در یک شی SecretKeySpec پیچیده کرد.
توجه: برای رمز عبور، بیشتر این توابع به جای آبجکت String با CharArray کار می کنند. به این دلیل که آبجکت هایی مانند String تغییر ناپذیر هستند. CharArray را می توان رونویسی (overwrite) کرد، که به شما امکان می دهد اطلاعات حساس را پس از پایان کار با آن از حافظه پاک کنید.
اضافه کردن بُردار اولیه (Initialization Vector)
شما تقریباً آماده رمزنگاری داده ها هستید، اما یک کار دیگر وجود دارد.
AES در حالت های مختلف کار می کند. حالت استاندارد، زنجیره بلوک رمز (CBC) نامیده می شود. CBC داده های شما را یک تکه در یک زمان رمزنگاری می کند.
CBC ایمن است زیرا هر بلوک داده درpipeline با بلوک قبلی که رمزنگاری شده، XOR است و وابستگی به بلوک های قبلی رمزنگاری را قوی می کند، اما آیا می توانید مشکلی را مشاهده کنید؟ در مورد بلوک اول چطور؟
اگر پیامی را رمزنگاری کنید که مانند پیام دیگری شروع می شود، اولین بلوک رمزنگاری شده یکسان خواهد بود! این یک سرنخ برای یک مهاجم فراهم می کند. برای رفع این مشکل، از بردار اولیه (IV) استفاده خواهید کرد.
IV یک اصطلاح فانتزی برای بلوکی از داده های تصادفی است که با همان بلوک اول XOR دریافت می کند. به یاد داشته باشید که هر بلوک به تمام بلوک های پردازش شده تا آن مرحله متکی است. این بدان معناست که مجموعههای یکسانی از دادهها که با کلید یکسان رمزنگاری شدهاند، خروجیهای یکسانی تولید نمیکنند.
اکنون با افزودن کد زیر درست بعد از کدی که به تازگی اضافه کرده اید، یک IV ایجاد کنید:
val ivRandom = SecureRandom() //not caching previous seeded instance of SecureRandom
// 1
val iv = ByteArray(16)
ivRandom.nextBytes(iv)
val ivSpec = IvParameterSpec(iv) // 2
اینجا، شما:
16 بایت داده تصادفی ایجاد کرد.
آن را در یک آبجکت IvParameterSpec بسته بندی(Package) کرد.
توجه: در اندروید 4.3 و پایین تر، یک آسیب پذیری و ضعف در SecureRandom وجود داشت. این مربوط به مقداردهی اولیه نامناسب تولید کننده اعداد شبه تصادفی (PRNG) بود. اگر از اندروید 4.3 و پایینتر پشتیبانی میکنید، راه حلی برای رفع این مشکل در دسترس است.
رمزنگاری داده ها
اکنون که تمام قطعات لازم را دارید، کد زیر را برای انجام رمزنگاری اضافه کنید:
val cipher = Cipher.getInstance("AES/CBC/PKCS7Padding") // 1
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec)
val encrypted = cipher.doFinal(dataToEncrypt) // 2
- در یک String ، مقدار “AES/CBC/PKCS7Padding” را پاس می دهد و AES را با حالت زنجیره ای بلوک رمز انتخاب می کند. PKCS7Padding یک استاندارد شناخته شده برای روش padding است. از آنجایی که با بلوکها کار میکنید، همه دادهها کاملاً در اندازه بلوک قرار نمیگیرند، بنابراین باید فضای باقیمانده را خالی کنید. به هر حال، بلوک ها 128 بیت هستند و AES قبل از رمزنگاری ، روش padding را اضافه می کند.
- doFinal رمزنگاری واقعی را انجام می دهد.
بعد موارد زیر را اضافه کنید:
map["salt"] = salt
map["iv"] = iv
map["encrypted"] = encrypted
شما داده های رمزنگاری شده را در یک HashMap آماده کردید. شما همچنین salt وinitialization vector را به map اضافه کردید. این به این دلیل است که همه بخش های آن برای رمزگشایی داده ها ضروری هستند.
اگر مراحل را به درستی دنبال کرده باشید، نباید هیچ خطایی داشته باشید و تابع رمزنگاری برای ایمن کردن برخی از داده ها آماده است! ذخیره salt ها و IV ها اشکالی ندارد، اما استفاده مجدد یا افزایش متوالی آنها امنیت را تضعیف می کند. اما هرگز نباید کلید را ذخیره کنید! در حال حاضر، شما ابزاری برای رمزنگاری دادهها ساختهاید، اما برای خواندن آنها بعداً، هنوز باید آنها را رمزگشایی کنید. بیایید ببینیم چگونه این کار را انجام دهیم.
رمزگشایی داده ها
اکنون، شما مقداری داده رمزنگاری شده دارید. برای رمزگشایی آن، باید حالت رمز را در روش init از ENCRYPT_MODE به DECRYPT_MODE تغییر دهید. موارد زیر را به روش رمزگشایی در فایل Encryption.kt اضافه کنید، درست در جایی که خط //TODO خوانده می شود: کد را در اینجا اضافه کنید: (//TODO: Add code here)
// 1
val salt = map["salt"]
val iv = map["iv"]
val encrypted = map["encrypted"]
// 2
//regenerate key from password
val pbKeySpec = PBEKeySpec(password, salt, 1324, 256)
val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
val keyBytes = secretKeyFactory.generateSecret(pbKeySpec).encoded
val keySpec = SecretKeySpec(keyBytes, "AES")
// 3
//Decrypt
val cipher = Cipher.getInstance("AES/CBC/PKCS7Padding")
val ivSpec = IvParameterSpec(iv)
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec)
decrypted = cipher.doFinal(encrypted)
شرح کد :
از HashMap استفاده کرده که حاوی داده های رمزنگاری شده، salt و IV لازم برای رمزگشایی است.
با توجه به آن اطلاعات به علاوه رمز عبور کاربر، کلید را بازسازی کرد.
داده ها را رمزگشایی کرد و به صورت ByteArray برگرداند.
توجه داشته باشید که چگونه از همان پیکربندی برای رمزگشایی استفاده کرده اید، اما مراحل خود را به عقب ردیابی کرده اید. این به این دلیل است که شما از یک الگوریتم رمزنگاری متقارن استفاده می کنید. اکنون می توانید داده ها را رمزنگاری کنید و همچنین آن ها را رمزگشایی کنید!
وهمیشه در نظر داشته باشید که کلید را هرگز ذخیره نکنید!
ذخیره داده های رمزنگاری شده
اکنون که فرآیند رمزنگاری کامل شده است، باید آن داده ها را ذخیره کنید. برنامه از قبل در حال خواندن و نوشتن داده ها در فضای ذخیره سازی است. شما آن روش ها را به روز می کنید تا با داده های رمزنگاری شده کار کند.
در فایل MainActivity.kt، همه چیز را در متد createDataSource با این قطعه کد جایگزین کنید:
val inputStream = applicationContext.assets.open(filename)
val bytes = inputStream.readBytes()
inputStream.close()
val password = CharArray(login_password.length())
login_password.text.getChars(0, login_password.length(), password, 0)
val map = Encryption().encrypt(bytes, password)
ObjectOutputStream(FileOutputStream(outFile)).use {
it -> it.writeObject(map)
}
در کد به روز شده، data file را به عنوان یک input stream باز کردید و داده ها را به روش رمزگذاری وارد کردید. شما HashMap را با استفاده از کلاس ObjectOutputStream ، serialized کردید و سپس آن را در حافظه ذخیره کردید.
برنامه را Build و Run کنید. توجه داشته باشید که حیوانات خانگی اکنون در لیست ناپدید شده اند:
به این دلیل که داده های ذخیره شده ، رمزنگاری شده است. برای خواندن محتوای رمزنگاری شده باید کد را به روز کنید. در متد loadPets فایل PetViewModel.kt، نشانگرهای نظر /* و */ را حذف کنید. سپس، کد زیر را درست در جایی که میخواند TODO: Add decrypt call here// اضافه کنید.
decrypted = Encryption().decrypt(
hashMapOf("iv" to iv, "salt" to salt, "encrypted" to encrypted), password)
شما متد رمزگشایی را با استفاده از داده های رمزنگاری شده، IV و salt فراخوانی کردید. اکنون که input stream به جای File از ByteArray می آید، خطی را که val inputStream = file.inputStream() می خواند با این یکی جایگزین کنید:
val inputStream = ByteArrayInputStream(decrypted)
اگر اکنون برنامه را build و run کنید، باید چند چهره دوست داشتنی ببینید!
ایمن سازی SharedPreferences
این برنامه همچنین آخرین زمان دسترسی را در SharedPreferences پیگیری می کند، بنابراین مکان دیگری در برنامه است که نیاز به محافظت دارد. ذخیره اطلاعات حساس در SharedPreferences میتواند ناامن باشد، زیرا همچنان میتوانید اطلاعات را از داخل برنامه خود به بیرون درز کنید، حتی با flag Context.MODE_PRIVATE. اما کمی بعد درستش می کنی
فایل MainActivity.kt را باز کنید و متد saveLastLoggedInTime را با این کد جایگزین کنید:
//Get password
val password = CharArray(login_password.length())
login_password.text.getChars(0, login_password.length(), password, 0)
//Base64 the data
val currentDateTimeString = DateFormat.getDateTimeInstance().format(Date())
// 1
val map =
Encryption().encrypt(currentDateTimeString.toByteArray(Charsets.UTF_8), password)
// 2
val valueBase64String = Base64.encodeToString(map["encrypted"], Base64.NO_WRAP)
val saltBase64String = Base64.encodeToString(map["salt"], Base64.NO_WRAP)
val ivBase64String = Base64.encodeToString(map["iv"], Base64.NO_WRAP)
//Save to shared prefs
val editor = getSharedPreferences("MyPrefs", Context.MODE_PRIVATE).edit()
// 3
editor.putString("l", valueBase64String)
editor.putString("lsalt", saltBase64String)
editor.putString("liv", ivBase64String)
editor.apply()
اینجا، شما:
رشته را با رمزنگاری UTF-8 به ByteArray تبدیل کرده و آن را رمزنگاری کرده اید. در کد قبلی یک فایل را به صورت باینری باز کردید، اما در مورد کار با رشته ها، باید رمزنگاری کاراکتر را در نظر بگیرید.
داده های خام را به یک نمایش رشته ای تبدیل کرد. SharedPreferences نمی تواند یک ByteArray را مستقیماً ذخیره کند، اما می تواند با String کار کند. Base64 استانداردی است که داده های خام را به نمایش رشته ای تبدیل می کند.
رشته ها را در SharedPreferences ذخیره کرده و می توانید به صورت اختیاری هم کلید ترجیحی و هم مقدار را رمزنگاری کنید. به این ترتیب، Attacker ها نمی تواند با نگاه کردن به کلید متوجه شود که چه مقدار ممکن است داشته باشد، و استفاده از کلیدهایی مانند «رمز عبور» برای brute forcing کار نمی کند، زیرا آن نیز رمزنگاری می شود.
اکنون، متد lastLoggedIn را جایگزین کنید تا بایت های رمزنگاری شده را برگردانید:
//Get password
val password = CharArray(login_password.length())
login_password.text.getChars(0, login_password.length(), password, 0)
//Retrieve shared prefs data
// 1
val preferences = getSharedPreferences("MyPrefs", Context.MODE_PRIVATE)
val base64Encrypted = preferences.getString("l", "")
val base64Salt = preferences.getString("lsalt", "")
val base64Iv = preferences.getString("liv", "")
//Base64 decode
// 2
val encrypted = Base64.decode(base64Encrypted, Base64.NO_WRAP)
val iv = Base64.decode(base64Iv, Base64.NO_WRAP)
val salt = Base64.decode(base64Salt, Base64.NO_WRAP)
//Decrypt
// 3
val decrypted = Encryption().decrypt(
hashMapOf("iv" to iv, "salt" to salt, "encrypted" to encrypted), password)
var lastLoggedIn: String? = null
decrypted?.let {
lastLoggedIn = String(it, Charsets.UTF_8)
}
return lastLoggedIn
شرح کد
نمایشهای رشتهای برای دادههای رمزگذاریشده، IV و salt بازیابی شد.
یک رمزگشایی Base64 روی رشته ها اعمال کرد تا آنها را به بایت های خام تبدیل کند.
آن داده ها را در HashMap به روش رمزگشایی منتقل کرد.
اکنون که فضای ذخیرهسازی را بهطور ایمن تنظیم کردهاید، با رفتن به Settings ▸ Apps ▸ PetMed 2 ▸ Storage ▸ Clear data ، دوباره start کنید.
برنامه را build و run کنید. اگر همه چیز کار کرد، پس از ورود به سیستم، باید حیوانات خانگی را دوباره روی صفحه ببینید.
استفاده از کلید از یک سرور
نمایش یک صفحه password علاوه بر صفحه login ممکن است تجربه کاربری(UX) خوبی نباشد. برای نیازهایی مانند این، شما چند گزینه دارید.
اولین مورد استفاده از رمز ورود برای استخراج کلید است. همچنین میتوانید به جای آن از سرور بخواهید آن کلید را تولید کند. کلید منحصر به فرد خواهد بود و زمانی که کاربر با اعتبار خود احراز هویت کرد، به طور ایمن منتقل می شود.
اگر مسیر سرور را میروید، مهم است بدانید که از آنجایی که سرور کلید را تولید میکند، ظرفیت رمزگشایی دادههای ذخیره شده در دستگاه را دارد. این احتمال وجود دارد که کسی به کلید نفوذ کند.
اگر هیچ یک از این راه حل ها برای شما کار نمی کند، می توانید از امنیت دستگاه برای ایمن سازی برنامه استفاده کنید.
استفاده از KeyStore
اندروید M قابلیت کار با کلید AES را با استفاده از KeyStore API معرفی کرد. این روش چند مزیت اضافه دارد. KeyStore به شما اجازه می دهد تا بدون افشای محتوای مخفی کلید، روی آن کار کنید. فقط شی، و نه داده های خصوصی، از فضای برنامه قابل دسترسی است.
ایجاد یک کلید تصادفی جدید
در فایل Encryption.kt کد زیر را به متد keystoreTest اضافه کنید تا یک کلید تصادفی تولید شود. این بار، KeyStore از کلید محافظت می کند:
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore") // 1
val keyGenParameterSpec = KeyGenParameterSpec.Builder("MyKeyAlias",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
//.setUserAuthenticationRequired(true) // 2 requires lock screen, invalidated if lock screen is disabled
//.setUserAuthenticationValidityDurationSeconds(120) // 3 only available x seconds from password authentication. -1 requires finger print - every time
.setRandomizedEncryptionRequired(true) // 4 different ciphertext for same plaintext on each call
.build()
keyGenerator.init(keyGenParameterSpec)
keyGenerator.generateKey()
شرح کد
- شما یک نمونه KeyGenerator ایجاد کردید و آن را روی ارائهدهنده «AndroidKeyStore» تنظیم کردید .
- به صورت اختیاری، شما .setUserAuthenticationRequired(true) را اضافه کردید که نیاز به تنظیم صفحه قفل دارد.
- شما به صورت اختیاری .setUserAuthenticationValidityDurationSeconds(120) را اضافه کردید تا کلید 120 ثانیه پس از احراز هویت دستگاه در دسترس باشد.
- شما .setRandomizedEncryptionRequired(true) را فراخوانی می کنید. این گزینه به KeyStore میگوید که هر بار از یک IV جدید استفاده کند. همانطور که قبلاً آموختید، این بدان معناست که اگر برای بار دوم داده های یکسان را رمزگذاری کنید، خروجی رمزگذاری شده یکسان نخواهد بود. همچنین مانع از دستیابی مهاجمان به سرنخ در مورد داده های رمزگذاری شده بر اساس تغذیه (feeding) در همان ورودی ها می شود.
چند چیز دیگر در مورد گزینه های KeyStore وجود دارد که باید در مورد آنها بدانید:
- برای .setUserAuthenticationValidityDurationSeconds() ، میتوانید هر بار که میخواهید به کلید دسترسی داشته باشید، نیاز به احراز هویت اثر انگشت داشته باشید.
- فعال کردن(Enabling) الزامات screen lockبه محض اینکه کاربر پین یا رمز عبور صفحه قفل را حذف یا تغییر دهد، کلیدها را باطل می کند.
- ذخیره کردن یک کلید در همان مکانی که داده ها رمزگذاری شده ، مانند قرار دادن کلید زیر درب است. KeyStore سعی می کند از کلید با مجوزهای دقیق و kernel level code محافظت کند. در برخی دستگاهها، کلیدها دارای پشتیبان سختافزاری هستند.
- می توانید از .setUserAuthenticationValidWhileOnBody(boolean remainsValid) استفاده کنید و باعث میشود وقتی دستگاه تشخیص دهد که شخصی نیست، کلید در دسترس نباشد.
رمزگذاری داده ها
اکنون، از آن کلیدی که در KeyStore ذخیره شده است استفاده خواهید کرد. در فایل Encryption.kt موارد زیر را دقیقاً در زیر TODO: Add code here// به متد keystoreEncrypt اضافه کنید:
// 1
//Get the key
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
val secretKeyEntry =
keyStore.getEntry("MyKeyAlias", null) as KeyStore.SecretKeyEntry
val secretKey = secretKeyEntry.secretKey
// 2
//Encrypt data
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
val ivBytes = cipher.iv
val encryptedBytes = cipher.doFinal(dataToEncrypt)
// 3
map["iv"] = ivBytes
map["encrypted"] = encryptedBytes
شرح کد :
- در این زمان، کلید را از KeyStore بازیابی می کنید.
- با توجه به SecretKey، داده ها را با استفاده از شی Cipher رمزگذاری کرده اید.
- مانند قبل، یک HashMap حاوی داده های رمزگذاری شده و IV مورد نیاز برای رمزگشایی داده ها را برمی گردانید.
رمزگشایی به صورت Byte Array
موارد زیر را دقیقاً در زیر //TODO: Add code here به متد keystoreDecrypt اضافه کنید:
// 1
//Get the key
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
val secretKeyEntry =
keyStore.getEntry("MyKeyAlias", null) as KeyStore.SecretKeyEntry
val secretKey = secretKeyEntry.secretKey
// 2
//Extract info from map
val encryptedBytes = map["encrypted"]
val ivBytes = map["iv"]
// 3
//Decrypt data
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
val spec = GCMParameterSpec(128, ivBytes)
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
decrypted = cipher.doFinal(encryptedBytes)
شرح کد
دوباره کلید از KeyStore دریافت می شود.
اطلاعات لازم از map استخراج می شود.
شی Cipher را با استفاده از ثابت DECRYPT_MODE مقدار دهی اولیه می کند و داده ها را در یک ByteArray رمزگشایی می کند.
تست کردن مثال
اکنون که راههایی برای رمزگذاری و رمزگشایی دادهها با استفاده از KeyStore API ایجاد کردهاید، وقت آن است که آنها را آزمایش کنید. موارد زیر را به انتهای متد keystoreTest اضافه کنید:
// 1
val map = keystoreEncrypt("My very sensitive string!".toByteArray(Charsets.UTF_8))
// 2
val decryptedBytes = keystoreDecrypt(map)
decryptedBytes?.let {
val decryptedString = String(it, Charsets.UTF_8)
Log.e("MyApp", "The decrypted string is: $decryptedString")
}
در کد به روز شده، شما:
یک رشته آزمایشی ایجاد شد و آن را رمزگذاری کرد.
متد decrypt را در خروجی رمزگذاری شده فراخوانی کرد تا آزمایش کند که همه چیز کار می کند.
در متد onCreate فایل MainActivity.kt، خطی که Encryption().keystoreTest() //را می خواند، ازکامنت بردارید. برنامه را build و run کنید و بررسی کنید که کار می کند. شما باید string رمزگشایی شده را ببینید:
از اینجا کجا بریم ؟
تبریک می گوییم، شما روش های رمزنگاری و رمزگشایی داده ها را در اندروید یاد گرفته اید!
همچنین روشهای دیگر کار با کلیدها را با استفاده از Keystore یاد گرفتید. میتوانید پروژه نهایی را با استفاده از دکمه Download Materials در بالا یا پایین این آموزش دانلود کنید، در صورتی که برخی از مراحل را رد کردهاید، پروژه به طور کامل کار میکند و تمام کدها پر میشوند.
این عالی است که بدانید چگونه امنیت را به درستی پیاده سازی کنید. با داشتن این دانش، میتوانید تأیید کنید که آیا کتابخانههای امنیتی third party از بهترین شیوهها برخوردار هستند یا خیر. با این حال، اجرای همه آن توسط خودتان، به خصوص اگر عجله داشته باشید، می تواند منجر به اشتباه شود.
پنهان کردن(conceal) یک انتخاب عالی برای یک کتابخانه رمزگذاری third party است.. برنامه هایی که پیاده سازی سفارشی دارند اغلب در برابر حملات گسترده و دارای اسکریپت مصون هستند.
مدیر حساب(The Account Manager) بخشی از سیستم عامل اندروید است و یک API مربوطه دارد. این یک مدیر متمرکز برای اعتبار حساب کاربری است، بنابراین برنامه شما مجبور نیست مستقیماً رمزهای عبور و ورود به سیستم را ذخیره کند یا با آن کار کند. شناخته شده ترین مثال در مورد درخواست توکن OAuth2 است.
Keychain API که در اندروید 4.0 (API Level 14) معرفی شد، با مدیریت کلید سروکار دارد و به طور خاص با اشیاء PrivateKey و X509 Certificate کار می کند و محفظه ایمن تری نسبت به استفاده از ذخیره سازی داده برنامه شما فراهم می کند. می توانید از آن برای نصب گواهی ها و استفاده مستقیم از آبجکت کلید خصوصی استفاده کنید.
کد امنیتی شما برای محافظت از برنامه شما به خوبی کار می کند، تا زمانی که کسی آن را دستکاری نکند. با ProGuard از مهندسی معکوس یا دستکاری کدهای مرتبط با امنیت خود جلوگیری می کنید.
دیدگاهتان را بنویسید