fckeuspy-go/fyne_ui.go
Lukas Batelka 9e402f9d8f feature/ui - vylepsene dialogy zadavani hesla a mazani identity
- odpocet casu pred dalsim pokusem o odemceni
- indikator sily hesla
2025-09-25 21:05:27 +02:00

375 lines
9.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}