مقدمة
المصادقة الثنائية موجودة في كل مكان اليوم. شكرا لها ، لسرقة حساب ، مجرد كلمة مرور ليست كافية. وعلى الرغم من أن وجوده لا يضمن عدم إبعاد حسابك للالتفاف حوله ، فستكون هناك حاجة إلى هجوم أكثر تعقيدًا ومتعدد المستويات. كما تعلمون ، كلما زاد الأمر تعقيدًا في هذا العالم ، زاد احتمال عدم نجاحه.
أنا متأكد من أن كل شخص يقرأ هذه المقالة قد استخدم مصادقة ثنائية على الأقل (يشار إليها فيما يلي باسم 2FA ، عبارة طويلة مؤلمة) في حياتهم. أدعوك اليوم لمعرفة كيف تعمل هذه التكنولوجيا ، والتي تحمي حسابات لا تعد ولا تحصى يوميًا.
لكن بالنسبة للمبتدئين ، يمكنك إلقاء نظرة على العرض التوضيحي لما سنفعله اليوم.
الأساسيات
أول شيء جدير بالذكر حول كلمات المرور لمرة واحدة هو أنها من نوعين: HOTP و TOTP . وهي كلمة مرور واحدة تستند إلى HMAC و OTP تستند إلى الوقت . تعد TOTP مجرد إضافة إلى HOTP ، لذلك دعونا نتحدث عن خوارزمية أبسط أولاً.
يوصف HOTP بواسطة مواصفات RFC4226 . إنه صغير الحجم ، يحتوي على 35 صفحة فقط ، ويحتوي على كل ما تحتاجه: وصف رسمي ، مثال على تنفيذ وبيانات الاختبار. لنلقِ نظرة على المفاهيم الأساسية.
بادئ ذي بدء ، ما هو HMAC ؟ يرمز HMAC إلى رمز مصادقة الرسائل المستند إلى التجزئة ، أو "رمز مصادقة الرسائل باستخدام وظائف التجزئة" باللغة الروسية. MAC هي آلية للتحقق من مرسل الرسالة. تقوم خوارزمية MAC بإنشاء علامة MAC باستخدام مفتاح سري معروف فقط للمرسل والمستقبل. بعد تلقي الرسالة ، يمكنك إنشاء علامة MAC بنفسك ومقارنة العلامتين. إذا تزامن ذلك - كل شيء على ما يرام ، لم يكن هناك تدخل في عملية الاتصال. على سبيل المكافأة ، يمكنك بنفس الطريقة التحقق من تلف الرسالة أثناء الإرسال. بالطبع ، لن ينجح التمييز بين التداخل والضرر ، لكن حقيقة فساد المعلومات كافية.

ما هو التجزئة؟ التجزئة هي نتيجة تطبيق دالة هاش على رسالة. تأخذ وظائف التجزئة بياناتك وتجعلها ذات طول ثابت. مثال جيد على ذلك هو وظيفة MD5 المعروفة ، والتي تم استخدامها على نطاق واسع للتحقق من سلامة الملف.
MAC نفسها ليست خوارزمية محددة ، ولكن مجرد مصطلح عام. HMAC ، بدوره ، هو بالفعل تنفيذ ملموس. بشكل أكثر تحديدًا ، HMAC- X ، حيث X هي إحدى وظائف تجزئة التشفير. يأخذ HMAC وسيطين: مفتاح سري ورسالة ، ويخلط بينهما بطريقة معينة ، ويطبق دالة التجزئة المحددة مرتين ، ويعيد علامة MAC.
إذا كنت تفكر حاليًا بنفسك في ما يتعلق بكل هذا بكلمات مرور لمرة واحدة - لا تقلق ، فقد وصلنا إلى النقطة الرئيسية تقريبًا.
وفقًا للمواصفات ، يتم حساب HOTP استنادًا إلى قيمتين:
- K هو المفتاح السري الذي يعرفه العميل والخادم. يجب أن يكون طوله 128 بت على الأقل ، ويفضل أن يكون 160 بت ، ويتم إنشاؤه عند تكوين 2FA.
- C هو العداد .
العداد هو قيمة 8 بايت متزامنة بين العميل والخادم. يتم تحديثه أثناء إنشاء كلمات مرور جديدة. في مخطط HOTP ، يتم زيادة عداد جانب العميل في كل مرة تقوم فيها بإنشاء كلمة مرور جديدة. من جانب الخادم ، في كل مرة تمر كلمة المرور بنجاح. نظرًا لأنه من الممكن إنشاء كلمة مرور ، ولكن لا تستخدمها ، يسمح الخادم لقيمة العداد بالعمل قليلاً في النافذة المحددة. ومع ذلك ، إذا كنت تلعب أكثر من اللازم مع مولد كلمة المرور في مخطط HOTP ، فسيتعين عليك مزامنته مرة أخرى.
هكذا. كما لاحظت ، تأخذ HMAC أيضًا حجة اثنين. يعرّف RFC4226 وظيفة إنشاء HOTP كما يلي:
HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))
من المتوقع تمامًا ، يتم استخدام K كمفتاح سري. العداد ، بدوره ، يستخدم كرسالة. بعد قيام دالة HMAC بإنشاء علامة MAC ، تقوم دالة Truncate
الغامضة بسحب كلمة المرور لمرة واحدة والمألوفة لنا بالفعل ، والتي تراها في تطبيق المولد الخاص بك أو على الرمز المميز.
دعنا نبدأ في كتابة الكود والتعامل مع البقية كما نذهب.
خطة التنفيذ
للحصول على كلمات مرور لمرة واحدة ، نحتاج إلى اتباع هذه الخطوات.

- توليد تجزئة HMAC-SHA1 من المعلمات K و C. سيكون هذا سلسلة 20 بايت.
- اسحب 4 بايت من هذه السلسلة بطريقة محددة.
- قم بتحويل القيمة المسحوبة إلى رقم وقسمه على 10 ^ n ، حيث n = عدد الأرقام في كلمة المرور لمرة واحدة (عادةً n = 6). وأخيرا ، خذ ما تبقى من هذا التقسيم. ستكون كلمة المرور الخاصة بنا.
هذا لا يبدو صعبا للغاية ، أليس كذلك؟ لنبدأ مع جيل التجزئة.
توليد HMAC-SHA1
ربما هذا هو أسهل الخطوات المذكورة أعلاه. لن نحاول إعادة إنشاء الخوارزمية بمفردنا (لا نحتاج أبدًا إلى محاولة تنفيذ شيء ما من التشفير بمفردنا). بدلاً من ذلك ، سوف نستخدم API Crypto API . المشكلة الصغيرة هي أن واجهة برمجة التطبيقات للمواصفات هذه متوفرة فقط ضمن سياق آمن (HTTPS). بالنسبة لنا ، هذا محفوف بحقيقة أننا لا نستطيع استخدامه دون إعداد HTTPS على خادم التطوير. يمكن الاطلاع هنا على القليل من التاريخ والمناقشة حول كيفية اتخاذ هذا القرار الصحيح.
لحسن الحظ ، في Firefox يمكنك استخدام Web Crypto في سياق غير آمن ، وليس عليك إعادة اختراع العجلة أو السحب في مكتبات الجهات الخارجية. لذلك ، لأغراض تطوير العرض التوضيحي ، أقترح استخدام FF.
يتم تعريف Crypto API نفسها في window.crypto.subtle
. إذا كنت مندهشًا من الاسم ، أقتبس من المواصفات:
يُطلق على API اسم SubtleCrypto
باعتباره انعكاسًا لحقيقة أن العديد من الخوارزميات لها متطلبات استخدام محددة. فقط عندما يتم استيفاء هذه المتطلبات ، فإنها تحتفظ بمتانتها.
دعنا نذهب على الطرق التي نحتاجها. ملاحظة: جميع الطرق المذكورة هنا غير متزامنة وإرجاع Promise
.
أولاً ، نحتاج إلى طريقة importKey
، حيث أننا importKey
، ولن importKey
في المتصفح. يأخذ importKey
5 وسيطات:
importKey( format, keyData, algorithm, extractable, usages );
في حالتنا:
- سيكون
format
'raw'
، أي سوف نقدم المفتاح كصفيف بايت ArrayBuffer
. keyData
هو نفسه ArrayBuffer. قريبا جدا سنتحدث عن كيفية توليدها.algorithm
، وفقًا للمواصفات ، ستكون HMAC-SHA1
. يجب أن تطابق هذه الوسيطة تنسيق HmacImportParams .extractable
إلى false ، لأنه ليس لدينا خطط لتصدير المفتاح السري- وأخيرا ، من جميع
usages
نحتاج فقط إلى 'sign'
.
سيكون المفتاح السري لدينا سلسلة عشوائية طويلة. في العالم الواقعي ، قد يكون هذا سلسلة من البايتات ، والتي قد تكون غير قابلة للطباعة ، ولكن ، للراحة ، في هذه المقالة سننظر في سلسلة. لتحويله إلى ArrayBuffer
سوف نستخدم واجهة TextEncoder
. مع ذلك ، يتم إعداد المفتاح في سطرين من التعليمات البرمجية:
const encoder = new TextEncoder('utf-8'); const secretBytes = encoder.encode(secret);
الآن دعونا نضع كل ذلك معا:
const Crypto = window.crypto.subtle; const encoder = new TextEncoder('utf-8'); const secretBytes = encoder.encode(secret); const key = await Crypto.importKey( 'raw', secretBytes, { name: 'HMAC', hash: { name: 'SHA-1' } }, false, ['sign'] );
! ممتاز تم إعداد التشفير. الآن سنتعامل مع العداد ونوقع الرسالة أخيرًا.
وفقًا للمواصفات ، يجب أن يكون طول العداد 8 بايت. سوف نعمل معها مرة أخرى ، كما هو الحال مع ArrayBuffer
. لترجمته إلى هذا النموذج ، سنستخدم الخدعة التي يتم استخدامها عادة في JS لحفظ الأصفار في الأرقام العليا للرقم. بعد ذلك ، سنضع كل بايت في ArrayBuffer
باستخدام DataView
. ضع في اعتبارك أن التنسيق لكل المواصفات الثنائية هو endian كبير .
function padCounter(counter) { const buffer = new ArrayBuffer(8); const bView = new DataView(buffer); const byteString = '0'.repeat(64);

أخيرًا ، بعد إعداد المفتاح والعداد ، يمكنك إنشاء علامة تجزئة! للقيام بذلك ، سوف نستخدم وظيفة sign
SubtleCrypto
.
const counterArray = padCounter(counter); const HS = await Crypto.sign('HMAC', key, counterArray);
وعلى هذا أكملنا الخطوة الأولى. في الإخراج ، نحصل على قيمة غامضة تسمى HS. وعلى الرغم من أن هذا ليس هو أفضل اسم لمتغير ، فهذا ما يطلق عليه (وبعض ما يلي) في المواصفات. سنترك هذه الأسماء لتسهيل مقارنة الشفرة بها. ما التالي؟
الخطوة 2: إنشاء سلسلة 4 بايت (اقتطاع ديناميكي)
دع Sbits = DT (HS) // DT ، المعرفة أدناه ،
// بإرجاع سلسلة 31 بت
DT لتقف على الاقتطاع الديناميكي. وهنا كيف تعمل:
function DT(HS) {

لاحظ كيف طبقنا bitwise و على البايت الأول من HS. 0x7f
في النظام الثنائي هو 0b01111111
، لذلك نحن نتجاهل البتة الأولى. في JS ، هذا هو المكان الذي ينتهي فيه معنى هذا التعبير ، ولكن في لغات أخرى ، سيوفر أيضًا قطع بت الإشارة لإزالة الارتباك بين الأرقام الموجبة / السالبة وتقديم هذا الرقم على أنه غير موقع.
انتهى تقريبا! يبقى فقط تحويل القيمة التي تم الحصول عليها من DT إلى رقم وإعادة توجيه إلى الخطوة الثالثة.
function truncate(uKey) { const Sbits = DT(uKey); const Snum = parseInt(Sbits, 2); return Snum; }
الخطوة الثالثة هي أيضا صغيرة جدا. كل ما يجب القيام به هو تقسيم العدد الناتج على 10 ** ( )
، ثم أخذ باقي هذا القسم. وبالتالي ، قمنا بقطع أرقام N الأخيرة من هذا الرقم. وفقًا للمواصفات ، يجب أن يكون الرمز الخاص بنا قادراً على سحب كلمات مرور مكونة من ستة أرقام على الأقل ومن المحتمل أن تكون 7 و 8 أرقام. من الناحية النظرية ، نظرًا لأن هذا الرقم مكون من 31 بت ، فقد كان بإمكاننا سحب 9 أحرف ، لكن في الواقع ، لم أر شخصياً أكثر من 6 أحرف. ماذا عنك
في هذه الحالة ، سيبدو رمز الوظيفة النهائية ، والذي يجمع بين كل الوظائف السابقة ، مثل هذا:
async function generateHOTP(secret, counter) { const key = await generateKey(secret, counter); const uKey = new Uint8Array(key); const Snum = truncate(uKey);
الصيحة! ولكن كيف تحقق الآن أن الكود لدينا هو الصحيح؟
تجريب
لاختبار التطبيق ، سوف نستخدم أمثلة من RFC. يشتمل الملحق D على قيم اختبار للمفتاح السري "12345678901234567890"
وقيم العداد من 0 إلى 9. وهناك أيضًا تجزئات HMAC محسوبة ونتائج وسيطة لوظيفة Truncate. مفيدة جدا لتصحيح جميع خطوات الخوارزمية. فيما يلي مثال صغير لهذا الجدول (يتم ترك العداد و HOTP فقط):
Count HOTP 0 755224 1 287082 2 359152 3 969429 ...
إذا لم تكن قد شاهدت العرض التوضيحي ، فقد حان الوقت الآن. يمكنك أن تدفع فيه القيم من RFC. ونعود لأننا بدأنا TOTP.
TOTP
لذلك وصلنا في النهاية إلى الجزء الأكثر حداثة من 2FA. عندما تفتح مولد كلمة المرور لمرة واحدة وترى مؤقتًا صغيرًا يحسب عدد الرموز التي ستكون صالحة ، يكون TOTP. ما هو الفرق؟
على أساس الوقت يعني أنه بدلاً من القيمة الثابتة ، يتم استخدام الوقت الحالي كعداد. أو بتعبير أدق ، "الفاصل الزمني" (الخطوة الزمنية). أو حتى عدد الفاصل الزمني الحالي. لحساب ذلك ، نأخذ وقت يونكس (عدد المللي ثانية منذ منتصف ليل 1 يناير 1970 بالتوقيت العالمي المنسق) ونقسم حسب النافذة صلاحية كلمة المرور (عادةً 30 ثانية). عادة ما يتحمل الخادم الانحرافات الصغيرة بسبب تزامن الساعة غير الكامل. عادة 1 فاصل ذهابًا وإيابًا حسب التكوين.
من الواضح أن هذا أكثر أمانًا من مخطط HOTP. في مخطط زمني محدد ، يتغير الرمز الصحيح كل 30 ثانية ، حتى لو لم يتم استخدامه. في الخوارزمية الأصلية ، يتم تحديد كلمة مرور صالحة من خلال قيمة العداد الحالية في إطار الخادم + التسامح. إذا لم تقم بالمصادقة ، فلن يتم تغيير كلمة المرور إلى أجل غير مسمى. يمكنك قراءة المزيد عن TOTP في RFC6238 .
نظرًا لأن المخطط المستند إلى الوقت هو إضافة إلى الخوارزمية الأصلية ، لا نحتاج إلى إجراء تغييرات على التطبيق الأصلي. سوف نستخدم requestAnimationFrame
وسوف نتحقق من كل إطار ما إذا كنا لا نزال داخل الفترة الزمنية. إذا لم يكن الأمر كذلك ، فقم بإنشاء عداد جديد وحساب HOTP مرة أخرى. حذف كل رمز التحكم ، يبدو الحل كالتالي:
let stepWindow = 30 * 1000;
اللمسات الأخيرة - دعم رمز الاستجابة السريعة
عادة ، عندما نقوم بتكوين 2FA ، نقوم بمسح المعلمات الأولية باستخدام رمز الاستجابة السريعة. أنه يحتوي على جميع المعلومات اللازمة: المخطط المحدد ، المفتاح السري ، اسم الحساب ، اسم المزود ، عدد الأرقام في كلمة المرور.
في مقال سابق ، تحدثت عن كيفية مسح رموز QR مباشرةً من الشاشة باستخدام واجهة برمجة تطبيقات getDisplayMedia
. بناءً على هذه المادة ، قمت بإنشاء مكتبة صغيرة ، والتي سوف نستخدمها الآن. تدعى المكتبة " دفق العرض" ، وبالإضافة إلى ذلك نستخدم حزمة jsQR الرائعة.
يحتوي الارتباط المشفر بواسطة QR بالتنسيق التالي:
otpauth://TYPE/LABEL?PARAMETERS
على سبيل المثال:
otpauth://totp/label?secret=oyu55d4q5kllrwhy4euqh3ouw7hebnhm5qsflfcqggczoafxu75lsagt&algorithm=SHA1&digits=6&period=30
سأحذف الكود الذي ينشئ عملية بدء التقاط الشاشة والتعرف عليها ، حيث يمكن العثور على كل هذا في الوثائق. بدلاً من ذلك ، إليك كيفية تحليل هذا الرابط:
const setupFromQR = data => { const url = new URL(data);
في العالم الحقيقي ، سيكون المفتاح السري عبارة عن سلسلة مشفرة أساسها 32 (!) ، حيث قد تكون بعض وحدات البايت غير قابلة للطباعة. ولكن لبساطه مظاهرة ، ونحن نغفل هذه النقطة. لسوء الحظ ، لم أستطع العثور على معلومات عن سبب الأساس 32 أو هذا التنسيق فقط. على ما يبدو ، لا توجد مواصفات رسمية لتنسيق عنوان URL هذا ، والتنسيق نفسه صاغته Google. يمكنك قراءة القليل عنه هنا.
لإنشاء رموز QR للاختبار ، أوصي باستخدام FreeOTP .
استنتاج
وهذا كل شيء! مرة أخرى ، لا تنس مشاهدة التجريبي . هناك أيضًا رابط للمستودع مع الكود الذي يقف وراء كل ذلك.
اليوم قمنا بتفكيك تكنولوجيا مهمة إلى حد ما نستخدمها على أساس يومي. أتمنى أن تكون قد تعلمت شيئًا جديدًا لنفسك. استغرق هذا المقال وقتا أطول مما كنت اعتقد. ومع ذلك ، من المثير للاهتمام تحويل مواصفات الورق إلى شيء فعال ومألوف.
اراك قريبا!