feat(auth): šifrovanje TOTP tajni u mirovanju (AES-256-GCM)
TOTP tajne se više ne čuvaju kao čist tekst u koloni korisnici.totp_tajna. Uvedene auth.Sifruj/auth.Desifruj (AES-256-GCM) u internal/auth/kripto.go. sqliteKorisniciRepo šifruje pri SacuvajTotpTajnu i dešifruje pri čitanju (DohvatiPoImenu/PoID/Lista), pa ostatak programa i dalje vidi čistu tajnu. Ključ je 32-bajtni NTECH_TOTP_KEY (base64), učitava se ili generiše pri pokretanju (ucitajTotpKljuc) i upisuje u ntech.env van baze. Stare nešifrovane tajne se tolerišu pri čitanju (fallback) i jednokratno šifruju pri startu (ZasifrujPostojeceTotp). RequireAuth i Handler provode ključ do repo-a. Dodati prvi testovi u repozitorijumu (internal/auth/kripto_test.go).
This commit is contained in:
+55
-2
@@ -2,7 +2,9 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
@@ -92,6 +94,19 @@ func main() {
|
|||||||
log.Printf("Dozvole: uklonjeno %d zastarelih redova", br)
|
log.Printf("Dozvole: uklonjeno %d zastarelih redova", br)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ključ za šifrovanje TOTP tajni u mirovanju (AES-256-GCM)
|
||||||
|
totpKljuc, err := ucitajTotpKljuc()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Greška pri učitavanju ključa za TOTP: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// jednokratno šifruj eventualne stare TOTP tajne koje su ostale kao čist tekst
|
||||||
|
if br, err := sqlite.ZasifrujPostojeceTotp(context.Background(), db, totpKljuc); err != nil {
|
||||||
|
log.Printf("Upozorenje: greška pri šifrovanju postojećih TOTP tajni: %v", err)
|
||||||
|
} else if br > 0 {
|
||||||
|
log.Printf("TOTP: šifrovano %d postojećih tajni", br)
|
||||||
|
}
|
||||||
|
|
||||||
napraviBackup(db, putanjaBaze)
|
napraviBackup(db, putanjaBaze)
|
||||||
|
|
||||||
// periodični automatski backup — interval se čita iz podešavanja u svakom ciklusu,
|
// periodični automatski backup — interval se čita iz podešavanja u svakom ciklusu,
|
||||||
@@ -118,7 +133,7 @@ func main() {
|
|||||||
|
|
||||||
os.MkdirAll("web/static/uploads", 0755)
|
os.MkdirAll("web/static/uploads", 0755)
|
||||||
|
|
||||||
h := handler.Novi(db)
|
h := handler.Novi(db, totpKljuc)
|
||||||
h.Verzija = Verzija
|
h.Verzija = Verzija
|
||||||
// verzija statičkih fajlova za cache-busting — menja se pri svakom pokretanju,
|
// verzija statičkih fajlova za cache-busting — menja se pri svakom pokretanju,
|
||||||
// pa novi build/restart natera brauzer da povuče sveži CSS/JS (umesto starog iz keša)
|
// pa novi build/restart natera brauzer da povuče sveži CSS/JS (umesto starog iz keša)
|
||||||
@@ -172,7 +187,7 @@ func main() {
|
|||||||
|
|
||||||
// zaštićene rute — zahtevaju prijavljenog korisnika
|
// zaštićene rute — zahtevaju prijavljenog korisnika
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
r.Use(ntechmw.RequireAuth(db))
|
r.Use(ntechmw.RequireAuth(db, totpKljuc))
|
||||||
|
|
||||||
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Redirect(w, r, "/dashboard", http.StatusFound)
|
http.Redirect(w, r, "/dashboard", http.StatusFound)
|
||||||
@@ -283,6 +298,44 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ucitajTotpKljuc vraća 32-bajtni ključ za šifrovanje TOTP tajni. Čita ga iz
|
||||||
|
// NTECH_TOTP_KEY (base64). Ako env nije postavljen, generiše nov nasumičan ključ
|
||||||
|
// i dopisuje ga u ntech.env (perm 0600), tako da postojeće instalacije dobiju
|
||||||
|
// ključ automatski pri prvom pokretanju nove verzije.
|
||||||
|
//
|
||||||
|
// VAŽNO: ako se ovaj ključ izgubi ili promeni, postojeće šifrovane TOTP tajne se
|
||||||
|
// više ne mogu dešifrovati (korisnici moraju ponovo aktivirati 2FA).
|
||||||
|
func ucitajTotpKljuc() ([]byte, error) {
|
||||||
|
if v := os.Getenv("NTECH_TOTP_KEY"); v != "" {
|
||||||
|
kljuc, err := base64.StdEncoding.DecodeString(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("NTECH_TOTP_KEY nije ispravan base64: %w", err)
|
||||||
|
}
|
||||||
|
if len(kljuc) != auth.DuzinaTotpKljuca {
|
||||||
|
return nil, fmt.Errorf("NTECH_TOTP_KEY mora imati %d bajta (trenutno %d)", auth.DuzinaTotpKljuca, len(kljuc))
|
||||||
|
}
|
||||||
|
return kljuc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// nema ključa — generiši nov i upiši ga u ntech.env
|
||||||
|
kljuc := make([]byte, auth.DuzinaTotpKljuca)
|
||||||
|
if _, err := rand.Read(kljuc); err != nil {
|
||||||
|
return nil, fmt.Errorf("generisanje ključa: %w", err)
|
||||||
|
}
|
||||||
|
enkodiran := base64.StdEncoding.EncodeToString(kljuc)
|
||||||
|
f, err := os.OpenFile("ntech.env", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("otvaranje ntech.env: %w", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
if _, err := f.WriteString("\nNTECH_TOTP_KEY=" + enkodiran + "\n"); err != nil {
|
||||||
|
return nil, fmt.Errorf("upis ključa u ntech.env: %w", err)
|
||||||
|
}
|
||||||
|
os.Setenv("NTECH_TOTP_KEY", enkodiran)
|
||||||
|
log.Println("Generisan nov NTECH_TOTP_KEY i upisan u ntech.env")
|
||||||
|
return kljuc, nil
|
||||||
|
}
|
||||||
|
|
||||||
// napraviBackup kreira konzistentnu kopiju baze i briše najstarije preko zadatog broja kopija.
|
// napraviBackup kreira konzistentnu kopiju baze i briše najstarije preko zadatog broja kopija.
|
||||||
// Koristi već otvorenu vezu ka bazi (VACUUM INTO je bezbedan na pooled konekciji).
|
// Koristi već otvorenu vezu ka bazi (VACUUM INTO je bezbedan na pooled konekciji).
|
||||||
func napraviBackup(db *sql.DB, putanjaBaze string) {
|
func napraviBackup(db *sql.DB, putanjaBaze string) {
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DuzinaTotpKljuca je obavezna dužina ključa za AES-256 (32 bajta).
|
||||||
|
const DuzinaTotpKljuca = 32
|
||||||
|
|
||||||
|
// Sifruj šifruje tekst pomoću AES-256-GCM i vraća base64 zapis u kome je nonce
|
||||||
|
// zalepljen ispred šifrata i autentifikacionog taga. Ključ mora biti tačno 32
|
||||||
|
// bajta. Koristi se za TOTP tajne u mirovanju (šifrovane u bazi).
|
||||||
|
func Sifruj(tekst string, kljuc []byte) (string, error) {
|
||||||
|
if len(kljuc) != DuzinaTotpKljuca {
|
||||||
|
return "", fmt.Errorf("ntech: auth.Sifruj: ključ mora imati %d bajta", DuzinaTotpKljuca)
|
||||||
|
}
|
||||||
|
blok, err := aes.NewCipher(kljuc)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("ntech: auth.Sifruj: %w", err)
|
||||||
|
}
|
||||||
|
gcm, err := cipher.NewGCM(blok)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("ntech: auth.Sifruj: %w", err)
|
||||||
|
}
|
||||||
|
// svaka enkripcija dobija nasumičan nonce; on se čuva uz šifrat radi dešifrovanja
|
||||||
|
nonce := make([]byte, gcm.NonceSize())
|
||||||
|
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||||
|
return "", fmt.Errorf("ntech: auth.Sifruj: %w", err)
|
||||||
|
}
|
||||||
|
// Seal dodaje šifrat i tag iza nonce-a (prvi argument je „prefiks" rezultata)
|
||||||
|
zapis := gcm.Seal(nonce, nonce, []byte(tekst), nil)
|
||||||
|
return base64.StdEncoding.EncodeToString(zapis), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Desifruj dešifruje zapis koji je napravio Sifruj. Vraća grešku ako zapis nije
|
||||||
|
// ispravan base64, ako je prekratak, ili ako provera integriteta (GCM tag) ne
|
||||||
|
// prođe. Ta greška se namerno koristi i za prepoznavanje starih, nešifrovanih
|
||||||
|
// tajni — pozivalac u tom slučaju tretira vrednost kao već čist tekst.
|
||||||
|
func Desifruj(zapis string, kljuc []byte) (string, error) {
|
||||||
|
if len(kljuc) != DuzinaTotpKljuca {
|
||||||
|
return "", fmt.Errorf("ntech: auth.Desifruj: ključ mora imati %d bajta", DuzinaTotpKljuca)
|
||||||
|
}
|
||||||
|
podaci, err := base64.StdEncoding.DecodeString(zapis)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("ntech: auth.Desifruj: %w", err)
|
||||||
|
}
|
||||||
|
blok, err := aes.NewCipher(kljuc)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("ntech: auth.Desifruj: %w", err)
|
||||||
|
}
|
||||||
|
gcm, err := cipher.NewGCM(blok)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("ntech: auth.Desifruj: %w", err)
|
||||||
|
}
|
||||||
|
if len(podaci) < gcm.NonceSize() {
|
||||||
|
return "", errors.New("ntech: auth.Desifruj: zapis je prekratak")
|
||||||
|
}
|
||||||
|
nonce, sifrat := podaci[:gcm.NonceSize()], podaci[gcm.NonceSize():]
|
||||||
|
otvoren, err := gcm.Open(nil, nonce, sifrat, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("ntech: auth.Desifruj: %w", err)
|
||||||
|
}
|
||||||
|
return string(otvoren), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// napraviKljuc vraća nasumičan 32-bajtni ključ za test
|
||||||
|
func napraviKljuc(t *testing.T) []byte {
|
||||||
|
t.Helper()
|
||||||
|
k := make([]byte, DuzinaTotpKljuca)
|
||||||
|
if _, err := rand.Read(k); err != nil {
|
||||||
|
t.Fatalf("rand: %v", err)
|
||||||
|
}
|
||||||
|
return k
|
||||||
|
}
|
||||||
|
|
||||||
|
// šifrovanje pa dešifrovanje vraća original
|
||||||
|
func TestSifrujDesifrujRoundTrip(t *testing.T) {
|
||||||
|
kljuc := napraviKljuc(t)
|
||||||
|
tajna := "JBSWY3DPEHPK3PXP"
|
||||||
|
|
||||||
|
zapis, err := Sifruj(tajna, kljuc)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Sifruj: %v", err)
|
||||||
|
}
|
||||||
|
if zapis == tajna {
|
||||||
|
t.Fatal("šifrat je jednak originalu — nije šifrovano")
|
||||||
|
}
|
||||||
|
|
||||||
|
nazad, err := Desifruj(zapis, kljuc)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Desifruj: %v", err)
|
||||||
|
}
|
||||||
|
if nazad != tajna {
|
||||||
|
t.Fatalf("očekivano %q, dobijeno %q", tajna, nazad)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isti tekst dva puta daje različite šifrate (nasumičan nonce)
|
||||||
|
func TestSifrujNasumicanNonce(t *testing.T) {
|
||||||
|
kljuc := napraviKljuc(t)
|
||||||
|
a, _ := Sifruj("isti tekst", kljuc)
|
||||||
|
b, _ := Sifruj("isti tekst", kljuc)
|
||||||
|
if a == b {
|
||||||
|
t.Fatal("dva šifrovanja istog teksta dala isti rezultat — nonce nije nasumičan")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dešifrovanje pogrešnim ključem mora da padne (integritet)
|
||||||
|
func TestDesifrujPogresanKljuc(t *testing.T) {
|
||||||
|
zapis, _ := Sifruj("tajna", napraviKljuc(t))
|
||||||
|
if _, err := Desifruj(zapis, napraviKljuc(t)); err == nil {
|
||||||
|
t.Fatal("dešifrovanje pogrešnim ključem je uspelo — GCM provera ne radi")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stari, nešifrovan plain text mora da padne na Desifruj (osnova fallback logike)
|
||||||
|
func TestDesifrujPrepoznajePlainText(t *testing.T) {
|
||||||
|
kljuc := napraviKljuc(t)
|
||||||
|
if _, err := Desifruj("JBSWY3DPEHPK3PXP", kljuc); err == nil {
|
||||||
|
t.Fatal("plain text je prošao kao šifrat - fallback ne bi prepoznao stare tajne")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pogrešna dužina ključa je greška, ne panika
|
||||||
|
func TestSifrujKratakKljuc(t *testing.T) {
|
||||||
|
if _, err := Sifruj("x", []byte("prekratak")); err == nil {
|
||||||
|
t.Fatal("kratak ključ nije odbijen")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,10 +6,16 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"ntech/internal/auth"
|
||||||
"ntech/internal/model"
|
"ntech/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
type sqliteKorisniciRepo struct{ db *sql.DB }
|
// sqliteKorisniciRepo drži ključ za šifrovanje TOTP tajni (AES-256-GCM) jer se
|
||||||
|
// tajna šifruje pri upisu (SacuvajTotpTajnu) i dešifruje pri čitanju.
|
||||||
|
type sqliteKorisniciRepo struct {
|
||||||
|
db *sql.DB
|
||||||
|
kljuc []byte
|
||||||
|
}
|
||||||
|
|
||||||
// dodeliOpcijeKorisnika popunjava bool i opciona polja korisnika iz skeniranih
|
// dodeliOpcijeKorisnika popunjava bool i opciona polja korisnika iz skeniranih
|
||||||
// NULL vrednosti — deljeno između skeniraiKorisnika (jedan red) i Lista (više redova)
|
// NULL vrednosti — deljeno između skeniraiKorisnika (jedan red) i Lista (više redova)
|
||||||
@@ -49,9 +55,23 @@ func skeniraiKorisnika(row interface{ Scan(...any) error }) (*model.Korisnik, er
|
|||||||
return k, nil
|
return k, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NoviKorisniciRepo kreira SQLite implementaciju KorisniciRepository
|
// NoviKorisniciRepo kreira SQLite implementaciju KorisniciRepository.
|
||||||
func NoviKorisniciRepo(db *sql.DB) *sqliteKorisniciRepo {
|
// kljuc je 32-bajtni ključ za šifrovanje TOTP tajni u mirovanju.
|
||||||
return &sqliteKorisniciRepo{db: db}
|
func NoviKorisniciRepo(db *sql.DB, kljuc []byte) *sqliteKorisniciRepo {
|
||||||
|
return &sqliteKorisniciRepo{db: db, kljuc: kljuc}
|
||||||
|
}
|
||||||
|
|
||||||
|
// desifrujTotpTajnu pretvara šifrovanu TOTP tajnu (kako stoji u bazi) u čist
|
||||||
|
// tekst u memoriji. Tolerantno je na stare nešifrovane tajne: ako dešifrovanje
|
||||||
|
// ne uspe (greška GCM provere), vrednost se ostavlja kakva jeste — to je plain
|
||||||
|
// text iz verzija pre uvođenja enkripcije.
|
||||||
|
func (r *sqliteKorisniciRepo) desifrujTotpTajnu(k *model.Korisnik) {
|
||||||
|
if k.TotpTajna == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if cisto, err := auth.Desifruj(k.TotpTajna, r.kljuc); err == nil {
|
||||||
|
k.TotpTajna = cisto
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *sqliteKorisniciRepo) Kreiraj(ctx context.Context, korisnickoIme, lozinkaHash, uloga string) (*model.Korisnik, error) {
|
func (r *sqliteKorisniciRepo) Kreiraj(ctx context.Context, korisnickoIme, lozinkaHash, uloga string) (*model.Korisnik, error) {
|
||||||
@@ -77,6 +97,7 @@ func (r *sqliteKorisniciRepo) DohvatiPoImenu(ctx context.Context, korisnickoIme
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ntech: korisnici.DohvatiPoImenu: %w", err)
|
return nil, fmt.Errorf("ntech: korisnici.DohvatiPoImenu: %w", err)
|
||||||
}
|
}
|
||||||
|
r.desifrujTotpTajnu(k)
|
||||||
return k, nil
|
return k, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,6 +113,7 @@ func (r *sqliteKorisniciRepo) DohvatiPoID(ctx context.Context, id int64) (*model
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ntech: korisnici.DohvatiPoID: %w", err)
|
return nil, fmt.Errorf("ntech: korisnici.DohvatiPoID: %w", err)
|
||||||
}
|
}
|
||||||
|
r.desifrujTotpTajnu(k)
|
||||||
return k, nil
|
return k, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,6 +145,7 @@ func (r *sqliteKorisniciRepo) Lista(ctx context.Context) ([]model.Korisnik, erro
|
|||||||
dodeliOpcijeKorisnika(&k, aktivan, koristiLokalnuTemu, lokalnaTema,
|
dodeliOpcijeKorisnika(&k, aktivan, koristiLokalnuTemu, lokalnaTema,
|
||||||
lokalnaPozadina, lokalnaPozadinaOpacity, lokalnaPozadinaBlur,
|
lokalnaPozadina, lokalnaPozadinaOpacity, lokalnaPozadinaBlur,
|
||||||
lokalnaPozadinaBlurPozadine, lokalnaPozadinaGlassOpacity, datumKreiranja)
|
lokalnaPozadinaBlurPozadine, lokalnaPozadinaGlassOpacity, datumKreiranja)
|
||||||
|
r.desifrujTotpTajnu(&k)
|
||||||
lista = append(lista, k)
|
lista = append(lista, k)
|
||||||
}
|
}
|
||||||
return lista, nil
|
return lista, nil
|
||||||
@@ -191,7 +214,12 @@ func (r *sqliteKorisniciRepo) SacuvajTotpTajnu(ctx context.Context, id int64, ta
|
|||||||
if tajna == "" {
|
if tajna == "" {
|
||||||
_, err = r.db.ExecContext(ctx, `UPDATE korisnici SET totp_tajna = NULL WHERE id = ?`, id)
|
_, err = r.db.ExecContext(ctx, `UPDATE korisnici SET totp_tajna = NULL WHERE id = ?`, id)
|
||||||
} else {
|
} else {
|
||||||
_, err = r.db.ExecContext(ctx, `UPDATE korisnici SET totp_tajna = ? WHERE id = ?`, tajna, id)
|
// tajna se čuva šifrovana (AES-256-GCM) — nikad kao čist tekst
|
||||||
|
sifrovana, errSifra := auth.Sifruj(tajna, r.kljuc)
|
||||||
|
if errSifra != nil {
|
||||||
|
return fmt.Errorf("ntech: korisnici.SacuvajTotpTajnu: %w", errSifra)
|
||||||
|
}
|
||||||
|
_, err = r.db.ExecContext(ctx, `UPDATE korisnici SET totp_tajna = ? WHERE id = ?`, sifrovana, id)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("ntech: korisnici.SacuvajTotpTajnu: %w", err)
|
return fmt.Errorf("ntech: korisnici.SacuvajTotpTajnu: %w", err)
|
||||||
@@ -199,6 +227,55 @@ func (r *sqliteKorisniciRepo) SacuvajTotpTajnu(ctx context.Context, id int64, ta
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ZasifrujPostojeceTotp jednokratno šifruje sve TOTP tajne koje su u bazi ostale
|
||||||
|
// kao čist tekst (iz verzija pre uvođenja enkripcije). Idempotentno je: tajne koje
|
||||||
|
// se već dešifruju datim ključem preskače. Vraća broj ažuriranih redova. Poziva se
|
||||||
|
// pri pokretanju programa, posle migracija.
|
||||||
|
func ZasifrujPostojeceTotp(ctx context.Context, db *sql.DB, kljuc []byte) (int, error) {
|
||||||
|
rows, err := db.QueryContext(ctx,
|
||||||
|
`SELECT id, totp_tajna FROM korisnici WHERE totp_tajna IS NOT NULL AND totp_tajna != ''`)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("ntech: korisnici.ZasifrujPostojeceTotp: %w", err)
|
||||||
|
}
|
||||||
|
// prvo skupljamo redove, pa tek onda upisujemo — da ne čitamo i pišemo
|
||||||
|
// istovremeno preko iste konekcije
|
||||||
|
type red struct {
|
||||||
|
id int64
|
||||||
|
tajna string
|
||||||
|
}
|
||||||
|
var zaSifrovanje []red
|
||||||
|
for rows.Next() {
|
||||||
|
var rd red
|
||||||
|
if err := rows.Scan(&rd.id, &rd.tajna); err != nil {
|
||||||
|
rows.Close()
|
||||||
|
return 0, fmt.Errorf("ntech: korisnici.ZasifrujPostojeceTotp: %w", err)
|
||||||
|
}
|
||||||
|
// ako se već dešifruje, znači da je šifrovana — preskoči je
|
||||||
|
if _, err := auth.Desifruj(rd.tajna, kljuc); err == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
zaSifrovanje = append(zaSifrovanje, rd)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
rows.Close()
|
||||||
|
return 0, fmt.Errorf("ntech: korisnici.ZasifrujPostojeceTotp: %w", err)
|
||||||
|
}
|
||||||
|
rows.Close()
|
||||||
|
|
||||||
|
br := 0
|
||||||
|
for _, rd := range zaSifrovanje {
|
||||||
|
sifrovana, err := auth.Sifruj(rd.tajna, kljuc)
|
||||||
|
if err != nil {
|
||||||
|
return br, fmt.Errorf("ntech: korisnici.ZasifrujPostojeceTotp: %w", err)
|
||||||
|
}
|
||||||
|
if _, err := db.ExecContext(ctx, `UPDATE korisnici SET totp_tajna = ? WHERE id = ?`, sifrovana, rd.id); err != nil {
|
||||||
|
return br, fmt.Errorf("ntech: korisnici.ZasifrujPostojeceTotp: %w", err)
|
||||||
|
}
|
||||||
|
br++
|
||||||
|
}
|
||||||
|
return br, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *sqliteKorisniciRepo) Obrisi(ctx context.Context, id int64) error {
|
func (r *sqliteKorisniciRepo) Obrisi(ctx context.Context, id int64) error {
|
||||||
_, err := r.db.ExecContext(ctx, `DELETE FROM korisnici WHERE id = ?`, id)
|
_, err := r.db.ExecContext(ctx, `DELETE FROM korisnici WHERE id = ?`, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -35,12 +35,15 @@ type Handler struct {
|
|||||||
AssetV string // verzija statičkih fajlova za cache-busting (postavlja se pri pokretanju)
|
AssetV string // verzija statičkih fajlova za cache-busting (postavlja se pri pokretanju)
|
||||||
Templates map[string]*template.Template
|
Templates map[string]*template.Template
|
||||||
TemplatesFS fs.FS
|
TemplatesFS fs.FS
|
||||||
|
totpKljuc []byte // ključ za šifrovanje TOTP tajni (prosleđuje se KorisniciRepo)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Novi kreira novi Handler sa datom bazom
|
// Novi kreira novi Handler sa datom bazom.
|
||||||
func Novi(baza *sql.DB) *Handler {
|
// totpKljuc je 32-bajtni ključ za šifrovanje TOTP tajni u mirovanju.
|
||||||
|
func Novi(baza *sql.DB, totpKljuc []byte) *Handler {
|
||||||
return &Handler{
|
return &Handler{
|
||||||
DB: baza,
|
DB: baza,
|
||||||
|
totpKljuc: totpKljuc,
|
||||||
Artikli: sqlite.NoviArtikalRepo(baza),
|
Artikli: sqlite.NoviArtikalRepo(baza),
|
||||||
KategorijeRepo: sqlite.NovaKategorijaRepo(baza),
|
KategorijeRepo: sqlite.NovaKategorijaRepo(baza),
|
||||||
DobavljaciRepo: sqlite.NoviDobavljacRepo(baza),
|
DobavljaciRepo: sqlite.NoviDobavljacRepo(baza),
|
||||||
@@ -50,7 +53,7 @@ func Novi(baza *sql.DB) *Handler {
|
|||||||
ServisniDeloviRepo: sqlite.NoviServisniDeloviRepo(baza),
|
ServisniDeloviRepo: sqlite.NoviServisniDeloviRepo(baza),
|
||||||
MagacinskePromeneRepo: sqlite.NoviMagacinskePromeneRepo(baza),
|
MagacinskePromeneRepo: sqlite.NoviMagacinskePromeneRepo(baza),
|
||||||
ProdajaRepo: sqlite.NoviProdajaRepo(baza),
|
ProdajaRepo: sqlite.NoviProdajaRepo(baza),
|
||||||
KorisniciRepo: sqlite.NoviKorisniciRepo(baza),
|
KorisniciRepo: sqlite.NoviKorisniciRepo(baza, totpKljuc),
|
||||||
SesijeRepo: sqlite.NoviSesijeRepo(baza),
|
SesijeRepo: sqlite.NoviSesijeRepo(baza),
|
||||||
PodsetnikRepo: sqlite.NoviPodsetnikRepo(baza),
|
PodsetnikRepo: sqlite.NoviPodsetnikRepo(baza),
|
||||||
PokusajiRepo: sqlite.NoviPokusajiPrijaveRepo(baza),
|
PokusajiRepo: sqlite.NoviPokusajiPrijaveRepo(baza),
|
||||||
@@ -71,7 +74,7 @@ func (h *Handler) reinicijalizujRepozitorijume(novaDB *sql.DB) {
|
|||||||
h.ServisniDeloviRepo = sqlite.NoviServisniDeloviRepo(novaDB)
|
h.ServisniDeloviRepo = sqlite.NoviServisniDeloviRepo(novaDB)
|
||||||
h.MagacinskePromeneRepo = sqlite.NoviMagacinskePromeneRepo(novaDB)
|
h.MagacinskePromeneRepo = sqlite.NoviMagacinskePromeneRepo(novaDB)
|
||||||
h.ProdajaRepo = sqlite.NoviProdajaRepo(novaDB)
|
h.ProdajaRepo = sqlite.NoviProdajaRepo(novaDB)
|
||||||
h.KorisniciRepo = sqlite.NoviKorisniciRepo(novaDB)
|
h.KorisniciRepo = sqlite.NoviKorisniciRepo(novaDB, h.totpKljuc)
|
||||||
h.SesijeRepo = sqlite.NoviSesijeRepo(novaDB)
|
h.SesijeRepo = sqlite.NoviSesijeRepo(novaDB)
|
||||||
h.PodsetnikRepo = sqlite.NoviPodsetnikRepo(novaDB)
|
h.PodsetnikRepo = sqlite.NoviPodsetnikRepo(novaDB)
|
||||||
h.PokusajiRepo = sqlite.NoviPokusajiPrijaveRepo(novaDB)
|
h.PokusajiRepo = sqlite.NoviPokusajiPrijaveRepo(novaDB)
|
||||||
|
|||||||
@@ -16,10 +16,11 @@ type kontekstKljuc string
|
|||||||
// KljucKorisnika je ključ za korisnika u request contextu
|
// KljucKorisnika je ključ za korisnika u request contextu
|
||||||
const KljucKorisnika kontekstKljuc = "korisnik"
|
const KljucKorisnika kontekstKljuc = "korisnik"
|
||||||
|
|
||||||
// RequireAuth je chi middleware koji proverava sesiju i injektuje korisnika u context
|
// RequireAuth je chi middleware koji proverava sesiju i injektuje korisnika u context.
|
||||||
func RequireAuth(db *sql.DB) func(http.Handler) http.Handler {
|
// totpKljuc je ključ za dešifrovanje TOTP tajne (korisRepo ga koristi pri čitanju).
|
||||||
|
func RequireAuth(db *sql.DB, totpKljuc []byte) func(http.Handler) http.Handler {
|
||||||
sesijeRepo := sqlite.NoviSesijeRepo(db)
|
sesijeRepo := sqlite.NoviSesijeRepo(db)
|
||||||
korisRepo := sqlite.NoviKorisniciRepo(db)
|
korisRepo := sqlite.NoviKorisniciRepo(db, totpKljuc)
|
||||||
|
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
Reference in New Issue
Block a user