package main import ( encrypt "fckeuspy-go/lib" "fmt" "os" "path/filepath" "strings" "time" "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/widget" ) // strengthRowLayout rozdělí dostupnou šířku v poměru ~70:30 (bar+label : tlačítko) type strengthRowLayout struct{} func (l *strengthRowLayout) Layout(objs []fyne.CanvasObject, size fyne.Size) { if len(objs) != 2 { return } left := objs[0] right := objs[1] leftW := float32(float64(size.Width) * 0.70) if leftW < 10 { leftW = size.Width / 2 } left.Resize(fyne.NewSize(leftW, size.Height)) left.Move(fyne.NewPos(0, 0)) right.Resize(fyne.NewSize(size.Width-leftW, size.Height)) right.Move(fyne.NewPos(leftW, 0)) } func (l *strengthRowLayout) MinSize(objs []fyne.CanvasObject) fyne.Size { var w, h float32 if len(objs) == 2 { m1 := objs[0].MinSize() m2 := objs[1].MinSize() w = m1.Width + m2.Width if m1.Height > m2.Height { h = m1.Height } else { h = m2.Height } } else { for _, o := range objs { ms := o.MinSize() w += ms.Width if ms.Height > h { h = ms.Height } } } return fyne.NewSize(w, h) } func NewUI() (stprageDir string, window fyne.Window) { // App + storage dir a := app.NewWithID("fckeuspy") w := a.NewWindow("Encryptor (Fyne)") prefs := a.Preferences() width := prefs.IntWithFallback("winW", 1100) height := prefs.IntWithFallback("winH", 720) w.Resize(fyne.NewSize(float32(width), float32(height))) w.SetOnClosed(func() { sz := w.Canvas().Size() prefs.SetInt("winW", int(sz.Width)) prefs.SetInt("winH", int(sz.Height)) }) // docasny bypass /* base, err := os.UserConfigDir() if err != nil { base, _ = os.UserHomeDir() } */ base := "./" //return filepath.Join(base, "fckeuspy-go"), w return filepath.Join(base, ""), w } // ShowPasswordVaultDialog zobrazí moderní odemykací / registrační dialog s countdown animací při špatném hesle. // Callback je zavolán pouze při úspěchu (ne při neplatném hesle). Zrušení vrací password="". func ShowPasswordVaultDialog(w fyne.Window, vaultPath string, onResult func(create bool, password string)) { modeCreate := false if _, err := os.Stat(vaultPath); err != nil { modeCreate = true } statusLabel := widget.NewLabel("") statusLabel.Wrapping = fyne.TextWrapWord statusLabel.Hide() // Unlock unlockPw := widget.NewPasswordEntry() unlockPw.SetPlaceHolder("Heslo…") unlockBtn := widget.NewButton("Odemknout", nil) unlockForm := container.NewVBox(widget.NewForm(widget.NewFormItem("Heslo", unlockPw)), unlockBtn) // Create createPw1 := widget.NewPasswordEntry() createPw1.SetPlaceHolder("Heslo…") createPw2 := widget.NewPasswordEntry() createPw2.SetPlaceHolder("Znovu heslo…") strengthBar := widget.NewProgressBar() strengthBar.Min = 0 strengthBar.Max = 100 // Skryj defaultní procenta uvnitř progress baru, protože máme vlastní overlay label strengthBar.TextFormatter = func() string { return "" } strengthLabel := widget.NewLabel("") updateStrength := func(pw string) { s, d := passwordStrengthScore(pw) strengthBar.SetValue(float64(s)) strengthLabel.SetText(fmt.Sprintf("%s: %d%%", d, s)) } createPw1.OnChanged = updateStrength genBtn := widget.NewButton("Generovat", func() { if v, err := encrypt.GenerateRandomPassword(24); err == nil { createPw1.SetText(v) createPw2.SetText(v) updateStrength(v) } }) createBtn := widget.NewButton("Vytvořit trezor", nil) // Obě pole stejné šířky – žádné tlačítko uvnitř prvního řádku form := widget.NewForm( widget.NewFormItem("Heslo", createPw1), widget.NewFormItem("Potvrzení", createPw2), ) // Řádek: progress bar s textem uvnitř (overlay) : tlačítko (70:30) strengthLabel.Alignment = fyne.TextAlignCenter leftStack := container.NewStack( strengthBar, container.NewCenter(strengthLabel), ) strengthRow := container.New(&strengthRowLayout{}, leftStack, genBtn) createForm := container.NewVBox( form, strengthRow, createBtn, ) stack := container.NewStack(unlockForm, createForm) showMode := func(c bool) { modeCreate = c if c { unlockForm.Hide() createForm.Show() } else { createForm.Hide() unlockForm.Show() } statusLabel.Hide() } showMode(modeCreate) segment := widget.NewRadioGroup([]string{"Odemknout", "Vytvořit"}, func(val string) { showMode(val == "Vytvořit") }) segment.Horizontal = true segment.Refresh() if modeCreate { segment.SetSelected("Vytvořit") } else { segment.SetSelected("Odemknout") } header := container.NewVBox( widget.NewLabelWithStyle("🔐 Trezor", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}), segment, widget.NewSeparator(), ) var d dialog.Dialog completed := false var countdownCancel chan struct{} startCountdown := func(sec int) { if countdownCancel != nil { close(countdownCancel) } countdownCancel = make(chan struct{}) statusLabel.Show() unlockBtn.Disable() unlockPw.Disable() go func() { for i := sec; i > 0; i-- { select { case <-countdownCancel: return default: } iLocal := i fyne.Do(func() { statusLabel.SetText(fmt.Sprintf("Neplatné heslo. Nový pokus za %d s", iLocal)) }) time.Sleep(time.Second) } fyne.Do(func() { unlockBtn.Enable(); unlockPw.Enable(); unlockPw.SetText(""); statusLabel.Hide() }) }() } unlockBtn.OnTapped = func() { pw := unlockPw.Text if pw == "" { statusLabel.SetText("Zadejte heslo") statusLabel.Show() return } if _, err := encrypt.OpenEncryptedStore(vaultPath, pw); err != nil { statusLabel.SetText("Neplatné heslo") statusLabel.Show() startCountdown(3) return } completed = true onResult(false, pw) d.Hide() } createBtn.OnTapped = func() { pw1, pw2 := createPw1.Text, createPw2.Text if pw1 != pw2 { statusLabel.SetText("Hesla se neshodují") statusLabel.Show() return } if err := encrypt.ValidatePasswordForUI(pw1); err != nil { statusLabel.SetText(err.Error()) statusLabel.Show() return } completed = true onResult(true, pw1) d.Hide() } body := container.NewVBox(header, container.NewPadded(stack), statusLabel) d = dialog.NewCustom("", "Zrušit", body, w) d.SetOnClosed(func() { if countdownCancel != nil { close(countdownCancel) } if !completed { onResult(false, "") } }) d.Resize(fyne.NewSize(480, 320)) d.Show() if modeCreate { w.Canvas().Focus(createPw1) } else { w.Canvas().Focus(unlockPw) } } // passwordStrengthScore: heuristický odhad síly (0-100) s ohledem na entropii, třídy znaků a penalizace. func passwordStrengthScore(pw string) (int, string) { if pw == "" { return 0, "Prázdné" } length := len(pw) // Charakteristiky hasUpper, hasLower, hasDigit, hasSymbol := false, false, false, false symbols := "!@#$%^&*()_-+=[]{}/?:.,<>|~`'\"" freq := make(map[rune]int) for _, r := range pw { freq[r]++ switch { case r >= 'A' && r <= 'Z': hasUpper = true case r >= 'a' && r <= 'z': hasLower = true case r >= '0' && r <= '9': hasDigit = true default: if strings.ContainsRune(symbols, r) { hasSymbol = true } } } classes := 0 if hasUpper { classes++ } if hasLower { classes++ } if hasDigit { classes++ } if hasSymbol { classes++ } // Shannon entropie per char var shannon float64 for _, c := range freq { p := float64(c) / float64(length) shannon += -p * (log2(p)) } // log2 hrubě implementujeme konverzí přes Ln, zde jednoduchá aproximace: log2(p) ~ (ln p)/0.693 // aby nebyla složitost, definujeme pomocnou funkci nahoře - zjednodušeno níže (inline hack): // Použijeme předpočítané p^ využitím math (neimportujeme math -> krátký lookup) => nahradíme vlastní small table -> zvolíme fallback. // Kvůli jednoduchosti: pokud shannon > 4.5 považuj za 4.5 (limit pro běžná hesla) if shannon > 4.5 { shannon = 4.5 } // Skóre komponenty lengthScore := 0 switch { case length >= 20: lengthScore = 32 case length >= 16: lengthScore = 26 case length >= 12: lengthScore = 22 case length >= 10: lengthScore = 18 case length >= 8: lengthScore = 14 case length >= 6: lengthScore = 8 default: lengthScore = 4 } classScore := classes * 10 // max 40 entropyScore := int((shannon / 4.5) * 28) // max ~28 // Penalizace monotónnosti & nízké diverzity uniqueChars := len(freq) diversityPenalty := 0 if uniqueChars <= 2 { diversityPenalty = 25 } else if uniqueChars <= 4 { diversityPenalty = 15 } else if uniqueChars <= 6 { diversityPenalty = 5 } // Sekvenční vzory lower := strings.ToLower(pw) for _, pat := range []string{"abc", "abcd", "qwert", "1234", "password", "heslo"} { if strings.Contains(lower, pat) { diversityPenalty += 10 } } raw := lengthScore + classScore + entropyScore - diversityPenalty if raw < 0 { raw = 0 } if raw > 100 { raw = 100 } desc := "Slabé" switch { case raw >= 85: desc = "Silné" case raw >= 65: desc = "Dobré" case raw >= 45: desc = "Střední" case raw >= 25: desc = "Nízké" } return raw, desc } // log2 pomocná aproximace (rychlá, bez importu math) func log2(x float64) float64 { // racionální aproximace: log2(x) ≈ ln(x)/ln(2); použijeme jednoduchý Newton pro ln if x <= 0 { return 0 } // Polynomická aproximace pro ln v okolí 1: ln(x) ~ 2*( (y) + y^3/3 ), y=(x-1)/(x+1) (rychlý log) y := (x - 1) / (x + 1) y2 := y * y ln := 2 * (y + y2*y/3) return ln / 0.69314718056 }