379 lines
9.6 KiB
Go
Executable File
379 lines
9.6 KiB
Go
Executable File
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, commonName 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…")
|
||
commonNameEntry := widget.NewEntry()
|
||
commonNameEntry.SetPlaceHolder("Jméno identity (např. Alice)…")
|
||
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("Jméno", commonNameEntry),
|
||
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
|
||
cn := strings.TrimSpace(commonNameEntry.Text)
|
||
onResult(true, pw1, cn)
|
||
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
|
||
}
|