Rumah > pembangunan bahagian belakang > Golang > Menyahmistikan OTP: logik di sebalik penjanaan token luar talian

Menyahmistikan OTP: logik di sebalik penjanaan token luar talian

Mary-Kate Olsen
Lepaskan: 2024-12-12 22:23:14
asal
361 orang telah melayarinya

Hello! Petang lain, dalam perjalanan pulang ke rumah, saya memutuskan untuk menyemak peti surat. Saya tidak maksudkan peti masuk e-mel saya, tetapi kotak sebenar sekolah lama di mana posmen meletakkan huruf fizikal. Dan yang sangat mengejutkan saya, saya menjumpai sampul surat di sana dengan sesuatu di dalamnya! Semasa membukanya, saya meluangkan beberapa saat berharap bahawa ia adalah surat tertangguh selama beberapa dekad daripada Hogwarts. Tetapi kemudian saya terpaksa turun semula ke Bumi, sebaik sahaja saya menyedari bahawa ia adalah surat "dewasa" yang membosankan daripada bank. Saya membaca sekilas teks dan menyedari bahawa bank "digital sahaja" saya untuk kanak-kanak hebat telah diperoleh oleh pemain terbesar di pasaran tempatan. Dan sebagai tanda permulaan baharu, mereka menambahkan ini pada sampul surat:

Demystifying OTPs: the logic behind the offline generation of tokens

Bersama dengan arahan tentang cara menggunakannya.

Jika anda seperti saya, dan tidak pernah menjumpai sekeping inovasi teknologi sedemikian, izinkan saya berkongsi apa yang saya pelajari daripada surat itu: pemilik baharu memutuskan untuk menguatkuasakan dasar keselamatan daripada syarikat mereka, yang bermaksud bahawa semua pengguna akaun mulai sekarang akan mendayakan MFA (pujian untuk itu, btw). Dan peranti yang anda boleh lihat di atas menjana token sekali 6 digit panjang yang digunakan sebagai faktor kedua semasa log masuk ke akaun bank anda. Pada asasnya, cara yang sama seperti apl seperti Authy, Google Authenticator atau 2FAS berfungsi, tetapi dalam bentuk fizikal.

Jadi, saya mencubanya dan proses log masuk berjalan lancar: peranti menunjukkan kepada saya kod 6 digit, saya memasukkannya dalam apl perbankan saya, dan ini membuatkan saya masuk. Hooray! Tetapi kemudian sesuatu menarik perhatian saya: bagaimana perkara ini berfungsi? Tidak ada cara ia disambungkan ke internet entah bagaimana, tetapi ia berjaya menjana kod yang betul yang diterima oleh pelayan bank saya. Hm... Bolehkah ia mempunyai kad SIM atau sesuatu yang serupa di dalamnya? Tidak boleh!

Menyedari bahawa hidup saya tidak akan pernah sama, saya mula tertanya-tanya tentang aplikasi yang saya nyatakan di atas (Authy dan rakan-rakan)? Penyelidik dalaman saya telah disedarkan, jadi saya menukar telefon saya ke dalam mod kapal terbang dan, yang mengejutkan saya, menyedari bahawa ia berfungsi dengan baik di luar talian: mereka terus menjana kod yang diterima oleh pelayan aplikasi. Menarik!

Tidak pasti tentang anda, tetapi saya sentiasa mengambil mudah aliran token sekali sahaja dan tidak pernah benar-benar memikirkannya (terutamanya kerana hari ini jarang telefon saya tidak mempunyai internet melainkan Saya sedang melakukan beberapa pengembaraan luar), jadi itulah punca kejutan saya. Jika tidak, ia masuk akal dari sudut pandangan keselamatan untuk berfungsi dengan cara ini, kerana proses penjanaan adalah tempatan semata-mata, sangat selamat daripada pelakon luar. Tetapi bagaimana ia berfungsi?

Nah, teknologi moden seperti Google atau ChatGPT memudahkan untuk mencari jawapan dengan mudah. Tetapi masalah teknikal ini kelihatan menyeronokkan kepada saya, jadi memutuskan untuk mencubanya dan menyelesaikannya sendiri terlebih dahulu.

Keperluan

Mari kita mulakan dengan apa yang kita ada:

  • peranti luar talian yang menjana kod 6 digit
  • pelayan yang menerima kod ini, mengesahkannya dan memberi isyarat hijau jika ia betul

Bahagian pengesahan pelayan membayangkan bahawa pelayan mesti dapat menjana kod yang sama seperti peranti luar talian untuk membandingkannya. Hm..itu boleh membantu.

Pemerhatian lanjut saya terhadap "mainan" baharu saya membawa lebih banyak penemuan:

  • jika saya mematikannya dan kemudian mematikannya, biasanya saya dapat melihat kod yang sama seperti sebelum ini
  • namun, kadangkala, ia berubah

Satu-satunya penjelasan logik yang boleh saya hasilkan ialah kod ini mempunyai jangka hayat tertentu. Saya ingin menceritakan kisah saya cuba mengira tempohnya dalam fesyen "1-2-3-...-N", tetapi ia tidak benar: Saya mendapat petunjuk besar daripada apl seperti Authy and Co, tempat saya melihat TTL 30 saat. Temuan yang bagus, mari tambahkan ini pada senarai fakta yang diketahui.

Mari kita ringkaskan keperluan yang kita ada setakat ini:

  • penjanaan kod boleh diramal (bukan rawak) dalam format 6 digit
  • logik penjanaan harus boleh dihasilkan semula, yang membolehkan mendapatkan hasil yang sama tanpa mengira platform
  • jangka hayat kod ialah 30 saat, yang bermaksud bahawa dalam jangka masa ini algoritma penjanaan menghasilkan nilai yang sama

Soalan besar

Baiklah, tetapi persoalan utama masih belum terjawab: bagaimana mungkin apl luar talian boleh menjana nilai yang sepadan dengan apl lain? Apakah persamaan mereka?

Jika anda menyukai alam semesta Lord of the Rings, anda mungkin masih ingat bagaimana Bilbo bermain teka-teki dengan Gollum, dan menyelesaikannya:

Perkara ini dimakan semua benda:
Burung, binatang, pokok, bunga;
Menggigit besi, menggigit keluli;
Mengisar batu keras untuk dimakan;
Membunuh raja, merosakkan bandar,
Dan mengalahkan gunung yang tinggi ke bawah.

Amaran spoiler, tetapi En. Baggins bernasib baik dan mendapat jawapan yang betul secara tidak sengaja - "Masa!". Percaya atau tidak, tetapi ini adalah jawapan kepada teka-teki kami juga: mana-mana 2 (atau lebih) apl mempunyai akses kepada masa yang sama selagi apl tersebut mempunyai jam terbenam di dalamnya. Yang terakhir tidak menjadi masalah pada hari ini, dan peranti yang dimaksudkan cukup besar untuk memuatkannya. Lihat sekeliling, dan kemungkinan masa pada jam tangan anda, telefon bimbit, TV, ketuhar dan jam di dinding adalah sama. Kami menyukai sesuatu di sini, nampaknya kami telah menemui asas untuk pengkomputeran OTP (kata laluan satu kali)!

Cabaran

Bergantung pada masa mempunyai set cabarannya sendiri:

  • zon waktu - yang manakah hendak digunakan?
  • jam cenderung tidak segerak, dan ini merupakan cabaran besar dalam sistem yang diedarkan

Mari kita atasinya satu persatu:

  • zon waktu: penyelesaian paling mudah di sini ialah memastikan semua peranti bergantung pada zon yang sama dan UTC boleh menjadi calon agnostik lokasi yang baik
  • mengenai jam yang tidak segerak: sebenarnya, kita mungkin tidak perlu menyelesaikannya malah menerima sebagai sesuatu yang tidak dapat dielakkan, selagi hanyut berada dalam satu atau dua saat, yang mungkin boleh diterima memandangkan TTL 30 saat. Pengeluar perkakasan peranti seharusnya dapat meramalkan bila hanyut tersebut akan dicapai, maka peranti akan menggunakannya sebagai tarikh tamat tempohnya, dan bank hanya akan menggantikannya dengan yang baharu, atau akan mempunyai cara untuk menyambungkannya ke rangkaian untuk menentukur jam. Sekurang-kurangnya, itulah pemikiran saya di sini.

Perlaksanaan

Ok, ini telah diselesaikan, jadi mari cuba laksanakan versi pertama algoritma kami menggunakan masa sebagai asas. Memandangkan kami berminat dengan keputusan 6 digit, nampaknya pilihan bijak untuk bergantung pada cap masa dan bukannya tarikh yang boleh dibaca manusia. Jom mulakan dari situ:

// gets current timestamp:
current := time.Now().Unix()
fmt.Println("Current timestamp: ", current)
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

Menurut dokumen Go, .Unix() kembali

bilangan saat berlalu sejak 1 Januari 1970 UTC.

Ini yang dicetak ke terminal:

Current timestamp:  1733691162
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

Itu permulaan yang baik, tetapi jika kita menjalankan semula kod itu, nilai cap masa akan berubah, sementara kita ingin memastikan ia stabil selama 30 saat. Nah, sekeping kek, mari bahagikannya dengan 30 dan gunakan nilai itu sebagai asas:

// gets current timestamp
current := time.Now().Unix()
fmt.Println("Current timestamp: ", current)

// gets a number that is stable within 30 seconds
base := current / 30
fmt.Println("Base: ", base)
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

Jom jalankan:

Current timestamp:  1733691545
Base:  57789718
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

Dan sekali lagi:

Current timestamp:  1733691552
Base:  57789718
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

Nilai asas kekal sama. Mari tunggu sebentar, dan jalankan semula:

Current timestamp:  1733691571
Base:  57789719
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

Nilai asas telah berubah, apabila tetingkap 30 saat telah berlalu - bagus!

Jika logik "bahagi dengan 30" tidak masuk akal, izinkan saya menerangkannya dengan contoh mudah:

  • bayangkan bahawa cap masa kami mengembalikan 1
  • jika kita membahagi 1 dengan 30, hasilnya akan menjadi 0, seperti apabila kita menggunakan bahasa pengaturcaraan yang ditaip ketat, membahagikan integer dengan integer mengembalikan integer lain, yang tidak mengambil berat tentang bahagian titik terapung
  • ini bermakna untuk 30 saat seterusnya kita akan mendapat 0 manakala cap masa antara 0 dan 29
  • apabila cap masa mencapai nilai 30, hasil pembahagian ialah 1, sehingga 60 (di mana ia menjadi 2), dan seterusnya

Saya harap ia lebih masuk akal sekarang.

Walau bagaimanapun, belum semua keperluan dipenuhi, kerana kami memerlukan hasil 6 digit, manakala asas semasa mempunyai 8 digit pada hari ini, tetapi pada satu ketika pada masa hadapan ia mungkin mencapai 9 titik digit, dan seterusnya . Baiklah, mari kita gunakan satu lagi helah matematik mudah: biar bahagikan asas dengan 1 000 000, dan dapatkan bakinya, yang akan sentiasa mempunyai tepat 6 digit, kerana peringatan boleh berupa sebarang nombor dari 0 hingga 999 999, tetapi tidak lebih besar:

// gets current timestamp:
current := time.Now().Unix()
fmt.Println("Current timestamp: ", current)
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

Bahagian fmt.Sprintf("d", kod) menambahkan sifar pendahuluan sekiranya nilai kod kami mempunyai kurang daripada 6 digit. Sebagai contoh, 1234 akan ditukar kepada 001234.
Keseluruhan kod untuk siaran ini boleh didapati di sini.

Mari jalankan kod ini:

Current timestamp:  1733691162
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

Baiklah, kami mendapat kod 6 digit kami, hore! Tetapi ada sesuatu yang tidak sesuai di sini, bukan? Jika saya memberi anda kod ini, dan anda akan menjalankannya pada masa yang sama seperti yang saya lakukan, anda akan mendapat kod yang sama, seperti saya. Ini tidak menjadikannya kata laluan sekali sahaja yang selamat, bukan? Inilah keperluan baharu:

  • hasilnya harus berbeza untuk pengguna yang berbeza

Sudah tentu, beberapa perlanggaran tidak dapat dielakkan, jika kami mempunyai melebihi 1 juta pengguna, kerana ini adalah nilai unik maksimum yang mungkin bagi setiap 6 digit. Tetapi ini adalah perlanggaran yang jarang berlaku dan tidak dapat dielakkan secara teknikal, bukan kecacatan reka bentuk algoritma seperti yang kita ada sekarang.

Saya tidak fikir sebarang helah matematik yang bijak akan membantu kami di sini sendiri: jika kami memerlukan hasil yang berasingan bagi setiap pengguna, kami memerlukan keadaan khusus pengguna untuk merealisasikannya. Sebagai jurutera dan, pada masa yang sama, pengguna banyak perkhidmatan, kami tahu bahawa untuk memberikan akses kepada API mereka, perkhidmatan bergantung pada kunci peribadi, yang unik bagi setiap pengguna. Mari perkenalkan kunci peribadi untuk kes penggunaan kami juga untuk membezakan antara pengguna.

Kunci peribadi

Logik mudah untuk menjana kunci peribadi sebagai integer antara 1 000 000 dan 999 999 999:

// gets current timestamp
current := time.Now().Unix()
fmt.Println("Current timestamp: ", current)

// gets a number that is stable within 30 seconds
base := current / 30
fmt.Println("Base: ", base)
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

Kami menggunakan peta pkDb sebagai cara untuk menghalang pendua antara kunci peribadi, dan jika pendua telah dikesan, kami menjalankan logik penjanaan sekali lagi sehingga kami mendapat hasil yang unik.

Mari jalankan kod ini untuk mendapatkan sampel kunci peribadi:

Current timestamp:  1733691545
Base:  57789718
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

Mari kita gunakan kunci peribadi ini dalam logik penjanaan kod kami untuk memastikan kami mendapat hasil yang berbeza bagi setiap kunci peribadi. Memandangkan kunci persendirian kami adalah daripada jenis integer, perkara paling mudah yang boleh kami lakukan ialah menambahkannya pada nilai asas dan mengekalkan algoritma yang tinggal seperti sedia ada:

Current timestamp:  1733691552
Base:  57789718
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

Mari pastikan ia menghasilkan hasil yang berbeza untuk kunci peribadi yang berbeza:

Current timestamp:  1733691571
Base:  57789719
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

Hasilnya kelihatan seperti yang kami mahu dan jangkakan:

// gets current timestamp:
current := time.Now().Unix()
fmt.Println("Current timestamp: ", current)

// gets a number that is stable within 30 seconds:
base := current / 30
fmt.Println("Base: ", base)

// makes sure it has only 6 digits:
code := base % 1_000_000

// adds leading zeros if necessary:
formattedCode := fmt.Sprintf("%06d", code)
fmt.Println("Code: ", formattedCode)
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

Berfungsi seperti daya tarikan! Ini bermakna kunci persendirian harus disuntik ke dalam peranti yang menjana kod sebelum ia dihantar kepada pengguna seperti saya: itu tidak sepatutnya menjadi masalah sama sekali bagi bank.

Sudahkah kita selesai sekarang? Nah, hanya jika kita berpuas hati dengan senario buatan yang kita gunakan. Jika anda pernah mendayakan MFA untuk mana-mana perkhidmatan / tapak web yang anda gunakan akaun, anda mungkin perasan bahawa sumber web meminta anda mengimbas kod QR dengan apl faktor kedua pilihan anda (Authy, Google Authenticator, 2FAS, dsb. ) yang akan memasukkan kod rahsia ke dalam apl anda dan mula menjana kod 6 digit mulai saat itu. Sebagai alternatif, anda boleh memasukkan kod secara manual.

Saya membawa perkara ini untuk menyebut bahawa adalah mungkin untuk melihat format kunci peribadi sebenar yang digunakan dalam industri. Biasanya rentetan berkod Base32 sepanjang 16-32 aksara yang kelihatan seperti ini:

// gets current timestamp:
current := time.Now().Unix()
fmt.Println("Current timestamp: ", current)
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

Seperti yang anda lihat, ini agak berbeza daripada kunci peribadi integer yang kami gunakan dan pelaksanaan semasa algoritma kami tidak akan berfungsi jika kami ingin menukar kepada format ini. Bagaimanakah kita boleh menyesuaikan logik kita?

Kunci peribadi sebagai rentetan

Mari kita mulakan dengan pendekatan mudah: kod kami tidak akan dikompil, kerana baris ini:

Current timestamp:  1733691162
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

kerana pk adalah daripada jenis rentetan mulai sekarang. Jadi mengapa kita tidak menukarnya kepada integer? Walaupun terdapat cara yang lebih elegan dan berprestasi untuk melakukannya, berikut adalah perkara paling mudah yang saya hasilkan:

// gets current timestamp
current := time.Now().Unix()
fmt.Println("Current timestamp: ", current)

// gets a number that is stable within 30 seconds
base := current / 30
fmt.Println("Base: ", base)
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

Ini sangat diilhamkan oleh pelaksanaan Java hashCode() untuk jenis data String, yang menjadikannya cukup baik untuk senario kami.

Berikut ialah logik yang dilaraskan:

Current timestamp:  1733691545
Base:  57789718
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

Berikut ialah output terminal:

Current timestamp:  1733691552
Base:  57789718
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

Kod 6 digit yang bagus, bagus. Mari tunggu untuk sampai ke tetingkap kali seterusnya dan jalankannya semula:

Current timestamp:  1733691571
Base:  57789719
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

Hm...ia berfungsi, tetapi kod itu, pada asasnya adalah kenaikan nilai sebelumnya, yang tidak baik, kerana dengan cara ini OTP boleh diramal, dan mempunyai nilainya serta mengetahui masanya, ia sangat mudah untuk mula menjana nilai yang sama tanpa perlu mengetahui kunci peribadi. Berikut ialah pseudokod mudah untuk penggodaman ini:

// gets current timestamp:
current := time.Now().Unix()
fmt.Println("Current timestamp: ", current)

// gets a number that is stable within 30 seconds:
base := current / 30
fmt.Println("Base: ", base)

// makes sure it has only 6 digits:
code := base % 1_000_000

// adds leading zeros if necessary:
formattedCode := fmt.Sprintf("%06d", code)
fmt.Println("Code: ", formattedCode)
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

di mana keepWithinSixDigits akan memastikan bahawa selepas 999 999 nilai seterusnya ialah 000 000 dan seterusnya untuk mengekalkan nilai dalam kemungkinan had 6 digit.

Seperti yang anda lihat, ia adalah kecacatan keselamatan yang serius. Mengapa ia berlaku? Jika kita melihat logik pengiraan asas, kita akan melihat bahawa ia bergantung pada 2 faktor:

  • cap masa semasa dibahagikan dengan 30
  • cincang kunci persendirian

Cincang menghasilkan nilai yang sama untuk kunci yang sama, jadi nilainya adalah malar. Bagi semasa / 30 , ia mempunyai nilai yang sama selama 30 saat, tetapi apabila tetingkap telah berlalu, nilai seterusnya akan menjadi kenaikan yang sebelumnya. Kemudian asas % 1_000_000 berkelakuan seperti yang kita lihat. Pelaksanaan kami sebelum ini (dengan kunci peribadi sebagai integer) mempunyai kelemahan yang sama, tetapi kami tidak menyedarinya - kekurangan ujian untuk dipersalahkan.

Kita perlu mengubah arus / 30 menjadi sesuatu untuk menjadikan perubahan nilainya lebih ketara.

Nilai OTP teragih

Terdapat pelbagai cara untuk mencapainya, dan beberapa helah matematik yang hebat wujud di luar sana, tetapi untuk tujuan pendidikan mari kita utamakan kebolehbacaan penyelesaian yang akan kita gunakan: mari kita ekstrak semasa / 30 ke dalam pangkalan pembolehubah yang berasingan dan masukkan ia ke dalam logik pengiraan cincang:

// gets current timestamp:
current := time.Now().Unix()
fmt.Println("Current timestamp: ", current)
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

Dengan cara ini, walaupun asas akan berubah dengan 1 setiap 30 saat, selepas digunakan dalam logik fungsi hash(), berat beza akan meningkat disebabkan oleh siri pendaraban yang dilakukan.

Berikut ialah contoh kod yang dikemas kini:

Current timestamp:  1733691162
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

Jom jalankan:

// gets current timestamp
current := time.Now().Unix()
fmt.Println("Current timestamp: ", current)

// gets a number that is stable within 30 seconds
base := current / 30
fmt.Println("Base: ", base)
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

Boom! Bagaimanakah kita mendapat nilai tolak di sini? Nah, nampaknya kita kehabisan julat int64, jadi ia mengehadkan nilai kepada tolak dan bermula semula - rakan Java saya sudah biasa dengan ini daripada tingkah laku hashCode(). Penyelesaiannya mudah: mari kita ambil nilai mutlak daripada hasilnya, maka tanda tolak diabaikan:

Current timestamp:  1733691545
Base:  57789718
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

Berikut ialah keseluruhan sampel kod dengan pembetulan:

Current timestamp:  1733691552
Base:  57789718
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

Jom jalankan:

Current timestamp:  1733691571
Base:  57789719
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

Mari kita jalankan sekali lagi untuk memastikan bahawa nilai OTP diedarkan sekarang:

// gets current timestamp:
current := time.Now().Unix()
fmt.Println("Current timestamp: ", current)

// gets a number that is stable within 30 seconds:
base := current / 30
fmt.Println("Base: ", base)

// makes sure it has only 6 digits:
code := base % 1_000_000

// adds leading zeros if necessary:
formattedCode := fmt.Sprintf("%06d", code)
fmt.Println("Code: ", formattedCode)
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

Bagus, akhirnya penyelesaian yang baik!

Sebenarnya, itulah saat saya menghentikan proses pelaksanaan manual saya, kerana saya berseronok dan mempelajari sesuatu yang baharu. Walau bagaimanapun, ia bukan penyelesaian terbaik mahupun penyelesaian yang saya akan buat secara langsung. Antara lain, ia mempunyai kelemahan yang besar: seperti yang anda lihat, logik kami sentiasa berurusan dengan nombor yang besar disebabkan oleh logik pencincangan dan nilai cap masa, yang bermaksud bahawa sangat tidak mungkin kami dapat menjana hasil yang bermula dengan 1 atau lebih sifar: cth., 012345 , 001234, dsb., walaupun ia sah sepenuhnya. Disebabkan itu, kami adalah 100 000 nilai kemungkinan pendek, iaitu 10% daripada bilangan kemungkinan hasil algoritma - kemungkinan perlanggaran adalah lebih tinggi dengan cara ini. Tidak hebat!

Ke mana hendak pergi dari sini

Saya tidak akan mendalami pelaksanaan yang digunakan dalam aplikasi sebenar, tetapi bagi mereka yang ingin tahu, saya akan berkongsi dua RFC yang patut dilihat:

  • HOTP: Algoritma Kata Laluan Satu Masa Berasaskan HMAC
  • TOTP: Algoritma Kata Laluan Satu Masa Berasaskan Masa

Dan berikut ialah pelaksanaan pseudokod yang akan berfungsi dengan cara yang dimaksudkan berdasarkan RFC di atas:

Current timestamp:  1733692423
Base:  57789747
Code:  789747
Salin selepas log masuk

Seperti yang anda lihat, kami sangat hampir dengan itu, tetapi algoritma asal menggunakan pencincangan yang lebih maju (HMAC-SHA1 dalam contoh ini) dan melakukan beberapa operasi bitwise untuk menormalkan output.

Pertimbangan keselamatan: gunakan semula dan bukannya bina sendiri

Walau bagaimanapun, ada satu lagi perkara yang ingin saya bincangkan sebelum kita menyebutnya sehari: keselamatan. Saya amat menggalakkan anda supaya tidak melaksanakan logik penjanaan OTP sendiri, kerana terdapat banyak perpustakaan di luar sana yang telah melakukannya untuk kami. Ruang untuk kesilapan adalah besar, dan ia adalah jarak yang dekat dengan kelemahan yang akan ditemui dan dieksploitasi oleh pelakon jahat di luar sana.

Walaupun anda mendapat logik penjanaan dengan betul dan akan menutupnya dengan ujian, terdapat perkara lain yang boleh menjadi salah. Sebagai contoh, pada pendapat anda, berapa banyak yang diperlukan untuk memaksa kod 6 digit? Mari bereksperimen:

// gets current timestamp:
current := time.Now().Unix()
fmt.Println("Current timestamp: ", current)
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

Mari jalankan kod ini:

Current timestamp:  1733691162
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

Dan sekali lagi:

// gets current timestamp
current := time.Now().Unix()
fmt.Println("Current timestamp: ", current)

// gets a number that is stable within 30 seconds
base := current / 30
fmt.Println("Base: ", base)
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

Seperti yang anda lihat, ia mengambil masa kira-kira 70 ms untuk meneka kod melalui gelung for-forcing yang mudah. Itu 400 kali lebih cepat daripada seumur hidup OTP! Pelayan apl/tapak web yang menggunakan mekanisme OTP perlu menghalangnya dengan, sebagai contoh, tidak menerima kod baharu untuk 5 atau 10 saat seterusnya selepas 3 percubaan yang gagal. Dengan cara ini penyerang hanya mendapat 18 atau 9 percubaan yang sepadan dalam tetingkap 30 saat, yang tidak mencukupi untuk kumpulan 1 juta nilai yang mungkin.

Dan ada lagi perkara seperti ini yang mudah terlepas pandang. Jadi, izinkan saya ulangi: jangan bina ini dari awal, tetapi bergantung pada penyelesaian sedia ada.

Apa pun, saya harap anda mempelajari sesuatu yang baharu hari ini, dan logik OTP tidak akan menjadi misteri untuk anda mulai saat ini. Selain itu, jika pada satu ketika dalam hidup, anda perlu menjadikan peranti luar talian anda menjana beberapa nilai menggunakan algoritma yang boleh dihasilkan semula, anda mempunyai idea yang baik untuk bermula.

Terima kasih atas masa yang anda luangkan membaca siaran ini, dan bergembiralah! =)

P.S. Terima e-mel setelah saya menerbitkan siaran baharu - langgan di sini

P.P.S. Seperti kanak-kanak lain yang hebat, saya telah mencipta akaun Bluesky sejak kebelakangan ini, jadi tolong bantu saya untuk menjadikan suapan saya lebih menyeronokkan =)

Atas ialah kandungan terperinci Menyahmistikan OTP: logik di sebalik penjanaan token luar talian. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

sumber:dev.to
Kenyataan Laman Web ini
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn
Artikel terbaru oleh pengarang
Tutorial Popular
Lagi>
Muat turun terkini
Lagi>
kesan web
Kod sumber laman web
Bahan laman web
Templat hujung hadapan