Ανάμεσα στα θεμελιώδη κρυπτογραφικά πρωτόκολλα της σύγχρονης ασφάλειας ιστού, το RFC 6238 (TOTP: Time-Based One-Time Password Algorithm) ξεχωρίζει για τη μαθηματική του απλότητα. Δεν απαιτεί διαδραστικές χειραψίες δικτύου, καμία κεντρική υποδομή δημοσίου κλειδιού (PKI) και κανέναν κύκλο επικοινωνίας με το cloud. Με ένα μόνο κοινόχρηστο μυστικό κλειδί Base32 και τη συνεχή εμπρόσθια πορεία του φυσικού χρόνου Unix, ο client και ο διακομιστής υπολογίζουν πανομοιότυπους, εφήμερους 6-ψήφιους κωδικούς ταυτοποίησης κάθε 30 δευτερόλεπτα με 100% μαθηματική βεβαιότητα.
Σε αυτόν τον περιεκτικό οδηγό, παρακάμπτουμε τις βιβλιοθήκες τρίτων τύπου "μαύρου κουτιού" (όπως το pyotp ή το otplib) για να κατασκευάσουμε ολόκληρο τον κρυπτογραφικό αγωγό του RFC 6238 από το μηδέν σε Python, JavaScript και Go, επαληθεύοντας κάθε υλοποίηση έναντι των επίσημων διανυσμάτων δοκιμής του IETF.
01. Η κομψότητα του ελέγχου ταυτότητας βάσει χρόνου
Για να κατανοήσουμε το TOTP, πρέπει πρώτα να ανατρέξουμε στον προκάτοχό του: RFC 4226 HOTP (HMAC-Based One-Time Password).
Στο HOTP, ο client και ο διακομιστής μοιράζονται έναν μετρητή συμβάντων $C$. Κάθε φορά που ο χρήστης παράγει έναν κωδικό, ο μετρητής αυξάνεται κατά ένα ($C = C + 1$), και ο κωδικός προκύπτει από το $\text{HMAC-SHA-1}(K, C)$. Παρόλο που είναι μαθηματικά άρτιο, το HOTP υπέφερε από μια λειτουργική αδυναμία: τον αποσυγχρονισμό του μετρητή. Εάν ένας χρήστης πατούσε ακούσια το κουμπί στο φυσικό του dongle πέντε φορές στην τσέπη του, ο μετρητής του client έφτανε στο $C=6$ ενώ ο διακομιστής ελέγχου ταυτότητας παρέμενε στο $C=1$, οδηγώντας σε επίμονες απορρίψεις σύνδεσης.
Δημοσιευμένο τον Μάιο του 2011 από τους N. M'Raihi, S. Machani, M. Pei και J. Rydell, το RFC 6238 έλυσε το πρόβλημα αποσυγχρονισμού του μετρητή αντικαθιστώντας τον αυξανόμενο ακέραιο με τον τυποποιημένο χρόνο εποχής Unix (Unix epoch time). Επειδή ο φυσικός χρόνος προχωρά ομοιόμορφα σε ολόκληρο τον πλανήτη, τόσο ο client όσο και ο διακομιστής προωθούν τους εσωτερικούς τους μετρητές ταυτόχρονα, χωρίς να στείλουν ούτε ένα πακέτο δικτύου.
02. Τα 5 κρυπτογραφικά μαθηματικά βήματα του TOTP
Η μετατροπή ενός αρχικού μυστικού κλειδιού Base32 16 χαρακτήρων (π.χ. JBSWY3DPEHPK3PXP) σε έναν κυλιόμενο κωδικό 6 ψηφίων (π.χ. 284915) περιλαμβάνει πέντε ακριβείς κρυπτογραφικές φάσεις:
Τα κλειδιά εγγραφής ανταλλακτηρίων και υπηρεσιών κωδικοποιούνται κατά το RFC 4648 Base32 χρησιμοποιώντας το αλφάβητο A-Z και 2-7 (παραλείποντας ασαφείς χαρακτήρες όπως 0, 1, 8, 9). Ο αλγόριθμος πρώτα κανονικοποιεί αυτή τη συμβολοσειρά, αφαιρώντας τα κενά διαστήματα και εφαρμόζοντας χαρακτήρες συμπλήρωσης (padding με =) ώστε το μήκος της να είναι πολλαπλάσιο του 8, προτού την αποκωδικοποιήσει στα αρχικά δυαδικά byte κλειδιού $K$.
Η τρέχουσα χρονική σήμανση Unix (δευτερόλεπτα που έχουν παρέλθει από την 1η Ιανουαρίου 1970 UTC) αντιστοιχίζεται στον ακέραιο μετρητή $T$:
Με τυπικές παραμέτρους $T_0 = 0$ και διάρκεια βήματος $X = 30$ δευτερόλεπτα. Αυτός ο ακέραιος $T$ στη συνέχεια κωδικοποιείται ως ένας ανυπόγραφος ακέραιος 64-bit big-endian 8 byte.
Χρησιμοποιώντας τα raw bytes του κλειδιού $K$ ως μυστικό κλειδί, υπολογίζουμε τη σύνοψη HMAC-SHA1 πάνω στο buffer χρόνου των 8 byte big-endian, παράγοντας έναν κατακερματισμό ελέγχου ταυτότητας μηνύματος $HS$ μήκους 20 byte (160 bit):
Για τη μετατροπή των 20 raw byte σε έναν ακέραιο, το RFC 4226 καθορίζει τη Δυναμική Περικοπή (Dynamic Truncation):
- Εξαγωγή μετατόπισης (Offset): Εξετάστε το τελευταίο byte $HS[19]$ και εφαρμόστε μάσκα με
0x0Fγια να εξαγάγετε τα 4 κατώτερα bit του, δίνοντας μια τιμή μετατόπισης μεταξύ 0 και 15:Offset = HS[19] & 0x0F; - Απόσπαση 4 Byte: Εξαγάγετε 4 διαδοχικά byte ξεκινώντας από το Offset: $P = HS[Offset \dots Offset+3]$;
- Μάσκα bit προσήμου: Εφαρμόστε μάσκα στο πιο σημαντικό bit (MSB) με
0x7Fγια να αποτρέψετε την ασάφεια υπερχείλισης προσημασμένου ακεραίου:
BinaryCode = ((P[0] & 0x7F) << 24) | ((P[1] & 0xFF) << 16) | ((P[2] & 0xFF) << 8) | (P[3] & 0xFF)
Τέλος, εφαρμόστε αναγωγή του BinaryCode modulo $10^6$ (1.000.000) για τυπικούς κωδικούς 6 ψηφίων. Συμπληρώστε το αποτέλεσμα με μηδενικά στα αριστερά για να εξασφαλίσετε μια σταθερή συμβολοσειρά εξόδου 6 χαρακτήρων:
03. Εκτελέσιμος κώδικας 1: Τυπική βιβλιοθήκη Python 3
Αυτή η πλήρης υλοποίηση χρησιμοποιεί τα ενσωματωμένα αρθρώματα base64, struct, hmac και hashlib της Python, χωρίς να απαιτείται καμία εξωτερική βιβλιοθήκη (zero dependencies):
import base64 import hashlib import hmac import struct import timedef generate_totp(secret_base32: str, timestamp: int = None, step: int = 30, digits: int = 6) -> str: """ Δημιουργία κωδικού TOTP συμβατού με το RFC 6238. :param secret_base32: Κοινόχρηστο μυστικό κλειδί σε μορφή Base32 (π.χ. ‘JBSWY3DPEHPK3PXP’) :param timestamp: Χρονική σήμανση εποχής Unix σε δευτερόλεπτα. Προεπιλογή σε time.time() :param step: Παράθυρο χρονικού βήματος σε δευτερόλεπτα (προεπιλογή: 30) :param digits: Αριθμός ψηφίων εξόδου (προεπιλογή: 6) :return: Συμπληρωμένη με μηδενικά συμβολοσειρά OTP (π.χ. ‘045812’) """ if timestamp is None: timestamp = int(time.time())
# 1. Κανονικοποίηση συμβολοσειράς Base32 και προσθήκη χαρακτήρων συμπλήρωσης '=' normalized_secret = secret_base32.replace(" ", "").upper() padding_needed = (8 - len(normalized_secret) % 8) % 8 normalized_secret += "=" * padding_needed key_bytes = base64.b32decode(normalized_secret) # 2. Υπολογισμός 64-bit big-endian μετρητή χρονικού βήματος T time_counter = timestamp // step time_bytes = struct.pack(">Q", time_counter) # 8 bytes big-endian unsigned long long # 3. Υπολογισμός σύνοψης HMAC-SHA1 (20 bytes) hmac_digest = hmac.new(key_bytes, time_bytes, hashlib.sha1).digest() # 4. Εκτέλεση δυναμικής περικοπής (Dynamic Truncation) offset = hmac_digest[-1] & 0x0F # Χαμηλότερα 4 bits του 20ού byte # Εξαγωγή 4 bytes στο offset και απαλοιφή του MSB sign bit binary_code = struct.unpack(">I", hmac_digest[offset : offset + 4])[0] & 0x7FFFFFFF # 5. Αριθμητική modulo και μορφοποίηση με συμπλήρωση μηδενικών otp_code = binary_code % (10 ** digits) return str(otp_code).zfill(digits)
if name == “main”: test_secret = “JBSWY3DPEHPK3PXP” print(f”Generated TOTP Code: {generate_totp(test_secret)}”)
04. Εκτελέσιμος κώδικας 2: JavaScript & Node.js Crypto
Υλοποίηση σε καθαρή JavaScript για περιβάλλοντα backend Node.js με χρήση του εγγενούς crypto.createHmac:
const crypto = require('crypto');// RFC 4648 Base32 Decoder in Pure JavaScript function base32Decode(base32Str) { const alphabet = ‘ABCDEFGHIJKLMNOPQRSTUVWXYZ234567’; const cleaned = base32Str.replace(/=+$/, ”).toUpperCase().replace(/\s+/g, ”); let bits = 0, value = 0, index = 0; const output = new Uint8Array(Math.floor((cleaned.length * 5) / 8));
for (let i = 0; i < cleaned.length; i++) { const val = alphabet.indexOf(cleaned[i]); if (val === -1) continue; value = (value << 5) | val; bits += 5; if (bits >= 8) { output[index++] = (value >>> (bits - 8)) & 255; bits -= 8; } } return Buffer.from(output.buffer, 0, index); }
function generateTOTP(secretBase32, timestamp = Math.floor(Date.now() / 1000), step = 30, digits = 6) { const key = base32Decode(secretBase32); const counter = Math.floor(timestamp / step);
// Allocate 8-byte buffer in Big-Endian format const buffer = Buffer.alloc(8); buffer.writeBigUInt64BE(BigInt(counter), 0);
// Compute HMAC-SHA1 const hmac = crypto.createHmac(‘sha1’, key).update(buffer).digest();
// Dynamic Truncation const offset = hmac[hmac.length - 1] & 0x0f; const binary = ((hmac[offset] & 0x7f) << 24) | ((hmac[offset + 1] & 0xff) << 16) | ((hmac[offset + 2] & 0xff) << 8) | (hmac[offset + 3] & 0xff);
const otp = binary % (10 ** digits); return otp.toString().padStart(digits, ‘0’); }
console.log(“Calculated TOTP:”, generateTOTP(“JBSWY3DPEHPK3PXP”));
Εκτέλεση στον browser (Client-Side) μέσω του W3C Web Crypto API:
Για σύγχρονες εφαρμογές ιστού που απαιτούν 100% ασφάλεια εκτός σύνδεσης (offline) - την ακριβή αρχιτεκτονική που τροφοδοτεί το 2FA-Tool - το W3C Web Crypto API παρέχει ασύγχρονες, επιταχυνόμενες από το υλικό κρυπτογραφικές συναρτήσεις απευθείας στο sandbox του προγράμματος περιήγησης χωρίς εξωτερικά πακέτα npm:
async function generateBrowserTOTP(secretBase32, timestamp = Math.floor(Date.now() / 1000)) { const keyBytes = base32Decode(secretBase32); const counter = Math.floor(timestamp / 30);// 1. Import raw secret key into SubtleCrypto const cryptoKey = await window.crypto.subtle.importKey( “raw”, keyBytes, { name: “HMAC”, hash: { name: “SHA-1” } }, false, [“sign”] );
// 2. Encode 8-byte Big-Endian counter buffer const buffer = new ArrayBuffer(8); const view = new DataView(buffer); view.setBigUint64(0, BigInt(counter), false); // false = Big-Endian
// 3. Perform cryptographic HMAC sign const signature = await window.crypto.subtle.sign(“HMAC”, cryptoKey, buffer); const hmacResult = new Uint8Array(signature);
// 4. Dynamic Truncation const offset = hmacResult[hmacResult.length - 1] & 0x0f; const binary = ((hmacResult[offset] & 0x7f) << 24) | ((hmacResult[offset + 1] & 0xff) << 16) | ((hmacResult[offset + 2] & 0xff) << 8) | (hmacResult[offset + 3] & 0xff);
const otp = binary % 1000000; return otp.toString().padStart(6, ‘0’); }
05. Εκτελέσιμος κώδικας 3: Go (Golang) για περιβάλλον παραγωγής
Τα microservices υψηλής απόδοσης επωφελούνται από τη δοκιμασμένη σε συνθήκες παραγωγής τυπική βιβλιοθήκη της Go:
package mainimport ( “crypto/hmac” “crypto/sha1” “encoding/base32” “encoding/binary” “fmt” “math” “strings” “time” )
// GenerateTOTP computes standard RFC 6238 time-based tokens func GenerateTOTP(secretBase32 string, timestamp int64, step int64, digits int) (string, error) { // 1. Sanitize Base32 seed and decode cleaned := strings.ToUpper(strings.ReplaceAll(secretBase32, ” ”, "")) key, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(cleaned) if err != nil { key, err = base32.StdEncoding.DecodeString(cleaned) if err != nil { return "", err } }
// 2. Encode 64-bit Big-Endian time-step counter counter := uint64(timestamp / step) buf := make([]byte, 8) binary.BigEndian.PutUint64(buf, counter) // 3. Compute HMAC-SHA1 mac := hmac.New(sha1.New, key) mac.Write(buf) hash := mac.Sum(nil) // 4. Dynamic Truncation offset := hash[len(hash)-1] & 0x0f binaryCode := binary.BigEndian.Uint32(hash[offset:offset+4]) & 0x7fffffff // 5. Modulo arithmetic mod := uint32(math.Pow10(digits)) otp := binaryCode % mod format := fmt.Sprintf("%%0%dd", digits) return fmt.Sprintf(format, otp), nil}
func main() { code, err := GenerateTOTP(“JBSWY3DPEHPK3PXP”, time.Now().Unix(), 30, 6) if err != nil { panic(err) } fmt.Printf(“Golang Computed TOTP: %s\n”, code) }
06. Επαλήθευση επίσημων διανυσμάτων δοκιμής RFC 6238 Παράρτημα B
Για να αποδειχθεί η πλήρης συμμόρφωση με το πρότυπο, κάθε κρυπτογραφική υλοποίηση πρέπει να επαληθεύεται έναντι των επίσημων διανυσμάτων δοκιμής (test vectors) που δημοσιεύονται στο IETF RFC 6238 Παράρτημα B.
Η επίσημη σουίτα δοκιμών χρησιμοποιεί το μυστικό ASCII "12345678901234567890" (20 bytes), το οποίο κωδικοποιείται σε Base32 ως εξής:
GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ (αξιολογείται για εξαγωγή 8 ψηφίων με χρονικά βήματα 30 δευτερολέπτων):
| Χρονική σήμανση Unix Epoch | Ημερομηνία & Ώρα UTC | Επίσημος αναμενόμενος κωδικός RFC | Αποτέλεσμα υλοποίησης |
|---|---|---|---|
| 59 | 1970-01-01 00:00:59 | 94287082 | ✓ ΕΠΙΤΥΧΙΑ (Ακριβής αντιστοίχιση) |
| 1111111109 | 2005-03-18 01:58:29 | 07081804 | ✓ ΕΠΙΤΥΧΙΑ (Ακριβής αντιστοίχιση) |
| 1111111111 | 2005-03-18 01:58:31 | 14050471 | ✓ ΕΠΙΤΥΧΙΑ (Ακριβής αντιστοίχιση) |
| 1234567890 | 2009-02-13 23:31:30 | 89005924 | ✓ ΕΠΙΤΥΧΙΑ (Ακριβής αντιστοίχιση) |
| 2000000000 | 2033-05-18 03:33:20 | 69279037 | ✓ ΕΠΙΤΥΧΙΑ (Ακριβής αντιστοίχιση) |
07. Μηχανική παραγωγής: Απόκλιση ρολογιού, επανάληψη & όρια ρυθμού
Κατά την ανάπτυξη της επαλήθευσης TOTP σε συστήματα ελέγχου ταυτότητας υψηλής διαθεσιμότητας, πρέπει να επιβάλλονται τρεις κρίσιμες απαιτήσεις ασφαλείας:
Λόγω μικρών ανακριβειών στα ρολόγια των κινητών συσκευών, οι διακομιστές θα πρέπει να αξιολογούν το παράθυρο [T - 1, T, T + 1] (ελέγχοντας τα προηγούμενα 30 δευτερόλεπτα, τα τρέχοντα 30 δευτερόλεπτα και τα επόμενα 30 δευτερόλεπτα). Μην διευρύνετε ποτέ το παράθυρο πέρα από $\pm 1$ βήμα, καθώς κάθε επιπλέον βήμα διευρύνει την επιφάνεια επίθεσης επανάληψης (replay attack) κατά 30 δευτερόλεπτα.
Ένας κωδικός πρέπει να ακυρώνεται αμέσως μετά την επιτυχή χρήση του. Αποθηκεύστε το Last_Verified_Time_Step στο Redis ή σε μια in-memory προσωρινή μνήμη (cache). Εάν ένας client επιχειρήσει να υποβάλει τον ίδιο κωδικό μέσα στο ίδιο χρονικό παράθυρο 30 δευτερολέπτων, απορρίψτε το αίτημα για να εξουδετερώσετε επιθέσεις επανάληψης τύπου man-in-the-middle.
Επειδή οι 6-ψήφιοι κωδικοί καλύπτουν μόνο 1.000.000 πιθανούς συνδυασμούς, τα τελικά σημεία (endpoints) χωρίς περιορισμό ρυθμού αιτημάτων μπορούν να παραβιαστούν ταχύτατα με επιθέσεις brute-force. Επιβάλετε προοδευτικές καθυστερήσεις και κλειδώματα λογαριασμού μετά από 3 διαδοχικές αποτυχίες. Στον κώδικα, εκτελείτε συγκρίσεις χρησιμοποιώντας συναρτήσεις ισότητας σταθερού χρόνου (π.χ. crypto/subtle.ConstantTimeCompare στην Go ή crypto.timingSafeEqual στο Node.js) για να αποτρέψετε την ανάλυση χρονισμού πλευρικού καναλιού (timing attacks).
08. Συχνές Ερωτήσεις (FAQ)
Ε1: Εφόσον το SHA-1 θεωρείται κρυπτογραφικά ευάλωτο, γιατί το TOTP εξακολουθεί να χρησιμοποιεί προεπιλεγμένα το HMAC-SHA1; ▼
Οι θεωρητικές επιθέσεις σύγκρουσης έναντι του απλού SHA-1 δεν παραβιάζουν τη δομή HMAC (RFC 2104). Στο HMAC, η υποκείμενη συνάρτηση κατακερματισμού είναι φωλιασμένη μέσα σε εσωτερικά και εξωτερικά μυστικά κλειδιά, γεγονός που εξουδετερώνει τις επιθέσεις που βασίζονται σε συγκρούσεις (collision attacks). Μέχρι σήμερα, το HMAC-SHA1 παραμένει ασφαλές για 2FA. Επιπλέον, το RFC 6238 υποστηρίζει πλήρως τα HMAC-SHA256 και HMAC-SHA512 εάν απαιτούνται υψηλότερα επίπεδα διασφάλισης.
Ε2: Επηρεάζουν οι ζώνες ώρας ή η θερινή ώρα (DST) τον συγχρονισμό του TOTP; ▼
Όχι. Ο χρόνος εποχής Unix (Unix epoch time) ορίζεται ως τα δευτερόλεπτα που έχουν παρέλθει από τις 1970-01-01 00:00:00 UTC. Είναι αυστηρά ανεξάρτητος από ζώνες ώρας, άλματα δευτερολέπτων και μεταβάσεις θερινής ώρας. Σε οποιαδήποτε ακριβή χρονική στιγμή, ένας διακομιστής στο Τόκιο και ένα smartphone στην Καλιφόρνια έχουν την ίδια ακριβώς ακέραια χρονική σήμανση.
Ε3: Σε τι διαφέρει ο έλεγχος ταυτότητας 5 γραμμάτων του Steam Guard από το RFC 6238; ▼
Το Steam Guard είναι μια ιδιόκτητη παραλλαγή που βασίζεται στο RFC 6238. Χρησιμοποιεί το ίδιο ακριβώς χρονικό βήμα 30 δευτερολέπτων και τον ίδιο υπολογισμό HMAC-SHA1, αλλά στην τελική φάση αναγωγής, αντιστοιχίζει τον εξαγόμενο ακέραιο μέσω ενός προσαρμοσμένου αλφαριθμητικού πίνακα 26 χαρακτήρων (23456789BCDFGHJKMNPQRTVWXY) για να δημιουργήσει μια συμβολοσειρά 5 χαρακτήρων.
Δοκιμάστε ζωντανά την 100% Client-Side παραγωγή κωδικών TOTP
Το 2FA-Tool υλοποιεί αυτήν ακριβώς την κρυπτογραφική ροή απευθείας μέσα στο πρόγραμμα περιήγησής σας χρησιμοποιώντας το W3C Web Crypto API. Μηδενική μετάδοση σε διακομιστές, μηδενική αποθήκευση κωδικών, άμεση επικύρωση.