feat(2fa): rezervni (jednokratni) kodovi za 2FA
Alternativa TOTP-u kada uređaj nije dostupan. Po CLAUDE.md specifikaciji: 10 kodova pri aktivaciji, čuvani kao bcrypt heš. Backend: - migracija 039 (tabela rezervni_kodovi, FK CASCADE) - auth.GenerisiRezervneKodove (Crockford base32, XXXX-XXXX) + NormalizujRezervniKod - RezervniKodoviRepository (Zameni/Iskoristi/BrojPreostalih/Obrisi) + SQLite impl - žičenje u Handler (+ reinicijalizuj) Prijava: - VerifikujTotp prvo proba TOTP, pa rezervni kod (isto polje); kod je jednokratni - totp_provera.html: input opušten (slova/crtica), napomena o rezervnom kodu Profil: - aktivacija generiše i prikazuje kodove JEDNOM; dugme Regeneriši; brojač preostalo X/10 - deaktivacija briše kodove Testovi: auth (generisanje/format/normalizacija), repo (jednokratnost/regeneracija), prijava rezervnim kodom end-to-end. Ukupno 36 test funkcija.
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// alfabetKodova je Crockford base32 (32 znaka — deli 256 bez ostatka, pa nema
|
||||
// modulo pristrasnosti) — izostavlja dvosmislene znakove I, L, O, U.
|
||||
const alfabetKodova = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
|
||||
|
||||
// BrojRezervnihKodova je podrazumevani broj kodova koji se generiše pri aktivaciji.
|
||||
const BrojRezervnihKodova = 10
|
||||
|
||||
// GenerisiRezervneKodove vraća n nasumičnih jednokratnih kodova formata "XXXX-XXXX".
|
||||
// Vraća čist tekst — pozivalac ih prikazuje korisniku jednom i čuva samo bcrypt heš.
|
||||
func GenerisiRezervneKodove(n int) ([]string, error) {
|
||||
kodovi := make([]string, 0, n)
|
||||
for i := 0; i < n; i++ {
|
||||
kod, err := nasumicanKod()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ntech: auth.GenerisiRezervneKodove: %w", err)
|
||||
}
|
||||
kodovi = append(kodovi, kod)
|
||||
}
|
||||
return kodovi, nil
|
||||
}
|
||||
|
||||
// nasumicanKod gradi jedan kod "XXXX-XXXX" iz alfabetKodova
|
||||
func nasumicanKod() (string, error) {
|
||||
bajtovi := make([]byte, 8)
|
||||
if _, err := rand.Read(bajtovi); err != nil {
|
||||
return "", err
|
||||
}
|
||||
var sb strings.Builder
|
||||
for i, b := range bajtovi {
|
||||
if i == 4 {
|
||||
sb.WriteByte('-')
|
||||
}
|
||||
sb.WriteByte(alfabetKodova[int(b)%len(alfabetKodova)])
|
||||
}
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
// NormalizujRezervniKod dovodi korisnikov unos u kanonski oblik "XXXX-XXXX":
|
||||
// uklanja razmake/crtice, prebacuje u velika slova i ubacuje crticu na sredini.
|
||||
// Tako se isti kod prepoznaje bez obzira na to kako ga korisnik otkuca.
|
||||
func NormalizujRezervniKod(unos string) string {
|
||||
var sb strings.Builder
|
||||
for _, r := range strings.ToUpper(unos) {
|
||||
if (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
|
||||
sb.WriteRune(r)
|
||||
}
|
||||
}
|
||||
s := sb.String()
|
||||
if len(s) == 8 {
|
||||
return s[:4] + "-" + s[4:]
|
||||
}
|
||||
return s
|
||||
}
|
||||
Reference in New Issue
Block a user