مرحبًا٪ username٪. بغض النظر عن موضوع التقرير ، يتم سؤالي باستمرار في المؤتمرات نفس السؤال - "كيفية تخزين الرموز المميزة بأمان على جهاز المستخدم؟". عادة أحاول الإجابة ، لكن الوقت لا يسمح بالكشف الكامل عن الموضوع. مع هذه المقالة ، أريد إغلاق هذه المشكلة تمامًا.
لقد قمت بتحليل عشرات التطبيقات لمعرفة كيفية عملها مع الرموز. جميع التطبيقات التي قمت بتحليلها للبيانات الهامة التي تم معالجتها وسمحت لي بتعيين رمز PIN للإدخال كحماية إضافية. دعونا نلقي نظرة على الأخطاء الأكثر شيوعًا:
- إرسال رمز PIN إلى واجهة برمجة التطبيقات مع RefreshToken لتأكيد المصادقة وتلقي الرموز المميزة الجديدة. - ضعيف ، إن RefreshToken غير آمن في التخزين المحلي ، مع إمكانية الوصول الفعلي إلى الجهاز أو النسخ الاحتياطي ، يمكنك استخراجه ، كما يمكن للبرنامج الضار القيام بذلك.
- حفظ رمز PIN في الرسالة باستخدام RefreshToken ، ثم التحقق المحلي من رمز PIN وإرسال RefreshToken إلى واجهة برمجة التطبيقات. - إن الكابوس ، RefreshToken غير آمن مع الدبوس ، مما يسمح باستخراجه ، بالإضافة إلى ظهور ناقل آخر يشير إلى تجاوز المصادقة المحلية.
- تشفير Bad RefreshToken مع رمز PIN ، والذي يسمح لك باستعادة رمز PIN و RefreshToken من نص التشفير. - حالة خاصة لخطأ سابق ، تم استغلاله بشكل أكثر تعقيدًا. لكن لاحظ أن هذا هو الطريق الصحيح.
بعد النظر في الأخطاء الشائعة ، يمكنك متابعة التفكير في منطق التخزين الآمن للرموز المميزة في تطبيقك. يجدر البدء بالأصول الأساسية المرتبطة بالمصادقة / التفويض أثناء تشغيل التطبيق وطرح بعض المتطلبات لها:
يتم استخدام بيانات الاعتماد - (اسم المستخدم + كلمة المرور) لمصادقة المستخدم في النظام.
+ لا يتم تخزين كلمة المرور مطلقًا على الجهاز ويجب مسحها على الفور من ذاكرة الوصول العشوائي بعد إرسالها إلى واجهة برمجة التطبيقات
لا يتم إرسال + بواسطة طريقة GET في معلمات الاستعلام لطلب HTTP ، يتم استخدام طلبات POST بدلاً من ذلك
+ تم تعطيل ذاكرة التخزين المؤقت للوحة المفاتيح لحقول نص معالجة كلمة المرور
+ يتم تعطيل الحافظة للحقول النصية التي تحتوي على كلمة مرور
+ لا يتم الكشف عن كلمة المرور عبر واجهة المستخدم (يستخدمون العلامات النجمية) ، أيضًا ، لا تدخل كلمة المرور في لقطات الشاشة
AccessToken - يستخدم لتأكيد إذن المستخدم.
+ لم يتم تخزينها في الذاكرة طويلة المدى وتخزينها إلا في ذاكرة الوصول العشوائي
لا يتم إرسال + بواسطة طريقة GET في معلمات الاستعلام لطلب HTTP ، يتم استخدام طلبات POST بدلاً من ذلك
RefreshToken - يستخدم للحصول على حزمة AccessToken + RefreshToken جديدة.
+ لا يتم تخزينه بأي شكل من الأشكال في ذاكرة الوصول العشوائي ويجب إزالته منه فورًا بعد الاستلام من واجهة برمجة التطبيقات والحفظ في الذاكرة طويلة المدى أو بعد الاستلام من الذاكرة طويلة المدى والاستخدام
+ المخزنة فقط في شكل مشفر في الذاكرة طويلة المدى
+ مشفرة باستخدام دبوس باستخدام قواعد سحرية وبعض القواعد (سيتم وصف القواعد أدناه) ، إذا لم يتم تعيين الدبوس ، فلا تقم بحفظه على الإطلاق
لا يتم إرسال + بواسطة طريقة GET في معلمات الاستعلام لطلب HTTP ، يتم استخدام طلبات POST بدلاً من ذلك
PIN - (عادة مكون من 4 أو 6 أرقام) - يستخدم لتشفير / فك تشفير RefreshToken.
+ لم يتم تخزينها في أي مكان على الجهاز ويجب مسحها على الفور من ذاكرة الوصول العشوائي بعد الاستخدام
+ لا تترك حدود التطبيق مطلقًا ، ولا يتم نقلها في أي مكان
+ يستخدم فقط للتشفير / فك التشفير RefreshToken
OTP هو رمز لمرة واحدة لـ 2FA.
+ لا يتم تخزين كلمة المرور لمرة واحدة على الجهاز مطلقًا ويجب مسحها على الفور من ذاكرة الوصول العشوائي بعد إرسالها إلى واجهة برمجة التطبيقات
لا يتم إرسال + بواسطة طريقة GET في معلمات الاستعلام لطلب HTTP ، يتم استخدام طلبات POST بدلاً من ذلك
+ تعطيل ذاكرة التخزين المؤقت للوحة المفاتيح لمعالجة حقول النص OTP
+ تم تعطيل الحافظة لحقول النص التي تحتوي على OTP
+ لا يدخل OTP في لقطات الشاشة
+ يزيل التطبيق OTP من الشاشة عندما ينتقل إلى الخلفية
الآن دعنا ننتقل إلى
سحر التشفير. الشرط الرئيسي هو أنه لا يجب عليك تحت أي ظرف من الظروف السماح بتنفيذ آلية التشفير RefreshToken ، حيث يمكنك التحقق من نتيجة فك التشفير محليًا. بمعنى أنه إذا استولى المهاجم على النص المشفر ، فلن يتمكن من التقاط المفتاح. يجب أن يكون المدقق الوحيد هو API. هذه هي الطريقة الوحيدة للحد من محاولات الاختيار الرئيسية وإبطال الرموز المميزة في حالة هجوم القوة الغاشمة.
سأعطي مثال جيد ، لنفترض أننا نريد تشفير UUID
aec27f0f-b8a3-43cb-b076-e075a095abfe
مع هذه المجموعة من AES / CBC / PKCS5Padding ، باستخدام PIN كمفتاح. يبدو أن الخوارزمية جيدة ، كل شيء مبني على الإرشادات ، ولكن هناك نقطة رئيسية - المفتاح يحتوي على القليل جدًا من الإنتروبيا. دعونا نرى ما يؤدي هذا إلى:
- الحشو - نظرًا لأن الرمز المميز الخاص بنا يأخذ 36 بايت ، ولدى AES وضع تشفير كتلة مع كتلة 128 بت ، ثم تحتاج الخوارزمية إلى إنهاء الرمز المميز حتى 48 بايت (وهو مضاعف 128 بت). في نسختنا ، ستتم إضافة الذيل وفقًا لمعيار PKCS5Padding ، أي قيمة كل بايت المضافة تساوي عدد البايتات المضافة
01
02 02
03 03 03
04 04 04 04
05 05 05 05 05
06 06 06 06 06 06
الخ.
سوف تبدو كتلتنا الأخيرة على النحو التالي:
... | 61 62 66 65 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C |
وهناك مشكلة ، بالنظر إلى هذه المساحة المتروكة يمكننا تصفية البيانات (بواسطة الكتلة الأخيرة غير الصالحة) التي تم فك تشفيرها باستخدام المفتاح الخاطئ ، وبالتالي تحديد RefreshToken الصحيح من كومة الذاكرة الملتوية. - التنسيق المتوقع للرمز المميز - حتى إذا جعلنا الرمز المميز لدينا مضاعفات 128 بت (على سبيل المثال ، إزالة الواصلات) لتجنب إضافة الحشو ، فسوف نواجه المشكلة التالية. تكمن المشكلة في أن كل نفس كومة الذاكرة المؤقتة الملتوية يمكننا جمع الخطوط وتحديد الخط الذي يقع ضمن تنسيق UUID. يتكون UUID في شكله النصي المقبول من 32 رقمًا بتنسيق سداسي عشري مفصولة بواصلة إلى 5 مجموعات 8-4-4-4-12
xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx
حيث M هو الإصدار و N هو الخيار. كل هذا يكفي لتصفية الرموز المميزة التي تم فك تشفيرها باستخدام المفتاح الخطأ ، تاركًا تنسيق UUID RefreshToken مناسبًا.
بالنظر إلى كل ما سبق ، يمكنك المتابعة إلى التنفيذ ، لقد اخترت خيارًا بسيطًا لإنشاء 64 بايت عشوائي ولفها في base64:
public String createRefreshToken() { byte[] refreshToken = new byte[64]; final SecureRandom secureRandom = new SecureRandom(); secureRandom.nextBytes(refreshToken); return Base64.getUrlEncoder().withoutPadding() .encodeToString(refreshToken); }
فيما يلي مثال لمثل هذا الرمز المميز:
YmI8rF9pwB1KjJAZKY9JzqsCu3kFz4xt4GkRCzXS9-FS_kbN3-CF9RGiRuuGqwqMo-VxFDhgQNmgjlQFD2GvbA
الآن دعونا نرى كيف تبدو خوارزمية (على Android و iOS ستكون الخوارزمية نفسها):
private static final String ALGORITHM = "AES"; private static final String CIPHER_SUITE = "AES/CBC/NoPadding"; private static final int AES_KEY_SIZE = 16; private static final int AES_BLOCK_SIZE = 16; public String encryptToken(String token, String pin) { decodedToken = decodeToken(token);
ما هي الخطوط التي تستحق الانتباه إليها:
private static final String CIPHER_SUITE = "AES/CBC/NoPadding";
لا حشوة ، تذكرون.
decodedToken = decodeToken(token);
لا يمكنك فقط أخذ وتشفير رمز مميز في تمثيل base64 ، لأن هذا التمثيل له تنسيق معين (حسنًا ، تذكر).
byte[] key = kdf.deriveKey(rawPin, salt, AES_KEY_SIZE);
عند الإخراج ، نحصل على مفتاح بحجم AES_KEY_SIZE ، مناسب لخوارزمية AES. يمكن استخدام أي وظيفة اشتقاق رئيسية أوصت بها Argon2 ، SHA-3 ، Scrypt كـ kdf في حالة الحياة السيئة pbkdf2 (وهي متوازية بشكل جيد جدًا مع FPGA).
يمكن تخزين الرمز المميز المشفر النهائي بأمان على الجهاز ولا تقلق من أن أي شخص يمكنه سرقته ، سواء كان برنامجًا ضارًا أو كيانًا لا تثقل كاهله المبادئ الأخلاقية.
المزيد من التوصيات:
- استبعاد الرموز المميزة من النسخ الاحتياطية.
- على iOS ، قم بتخزين الرمز المميز في keychain باستخدام السمة kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly.
- لا تبعثر الأصول التي تمت مناقشتها في هذه المقالة (المفتاح ، الرقم السري ، كلمة المرور ، إلخ) في جميع أنحاء التطبيق.
- استبدل الأصول بمجرد أن تصبح غير ضرورية ، لا تحتفظ بها في ذاكرتك لفترة أطول من اللازم.
- استخدم SecureRandom على Android و SecRandomCopyBytes على iOS لإنشاء وحدات بايت عشوائية في سياق تشفير.
قمنا بفحص عدد معين من المزالق عند تخزين الرموز المميزة ، والتي ، في رأيي ، يجب أن تكون معروفة لكل شخص يقوم بتطوير تطبيقات تعمل مع البيانات الهامة. هذا الموضوع ، حيث يمكنك الخلط في أي خطوة ، إذا كان لديك أسئلة ، اطرحها في التعليقات. التعليقات على النص مرحب بها ايضا
المراجع:
CWE-311: فقدان تشفير البيانات الحساسةCWE-327: استخدام خوارزمية تشفير مكسورة أو محفوفة بالمخاطرCWE-327: CWE-338: استخدام مولد الأرقام العشوائية الضعيفة المشفر (PRNG)CWE-598: التعرض للمعلومات من خلال سلاسل الاستعلام في طلب GET