From 2b7e3f6c850412f791d1d392dcd4f7d12de88b1b Mon Sep 17 00:00:00 2001 From: Lukas Batelka Date: Mon, 22 Sep 2025 19:16:49 +0200 Subject: [PATCH] feature/ui vylepsene ui pro vytvareni a odemykani identity --- fyne_ui.go | 79 +++++++++++++++++++++++--------------- lib/crypto.go | 103 -------------------------------------------------- main.go | 7 ++-- ui.go | 39 +++++++++++++++++-- 4 files changed, 87 insertions(+), 141 deletions(-) diff --git a/fyne_ui.go b/fyne_ui.go index dc57fba..783b771 100644 --- a/fyne_ui.go +++ b/fyne_ui.go @@ -3,6 +3,7 @@ package main import ( "errors" encrypt "fckeuspy-go/lib" + "os" "path/filepath" "strings" @@ -42,11 +43,20 @@ func NewUI() (stprageDir string, window fyne.Window) { // ShowPasswordVaultDialog zobrazí dialog pro vytvoření nebo otevření trezoru. // Zatím pouze skeleton: vrací heslo přes callback, reálné volání CreateEncryptedStore/OpenEncryptedStore mimo. -func ShowPasswordVaultDialog(w fyne.Window, onResult func(create bool, password string)) { - pwEntry := widget.NewPasswordEntry() - pwEntry.SetPlaceHolder("Heslo…") - pw2Entry := widget.NewPasswordEntry() - pw2Entry.SetPlaceHolder("Znovu heslo…") +func ShowPasswordVaultDialog(w fyne.Window, vaultPath string, onResult func(create bool, password string)) { + // Unlock tab + unlockPw := widget.NewPasswordEntry() + unlockPw.SetPlaceHolder("Heslo…") + unlockForm := widget.NewForm( + widget.NewFormItem("Heslo", unlockPw), + ) + unlockContent := container.NewVBox(unlockForm) + + // Create tab + createPw1 := widget.NewPasswordEntry() + createPw1.SetPlaceHolder("Heslo…") + createPw2 := widget.NewPasswordEntry() + createPw2.SetPlaceHolder("Znovu heslo…") strengthBar := widget.NewProgressBar() strengthBar.Min = 0 strengthBar.Max = 100 @@ -56,41 +66,43 @@ func ShowPasswordVaultDialog(w fyne.Window, onResult func(create bool, password strengthBar.SetValue(float64(score)) strengthLabel.SetText(desc) } - pwEntry.OnChanged = updateStrength + createPw1.OnChanged = updateStrength genBtn := widget.NewButton("Generovat", func() { if v, err := encrypt.GenerateRandomPassword(20); err == nil { - pwEntry.SetText(v) - pw2Entry.SetText(v) + createPw1.SetText(v) + createPw2.SetText(v) updateStrength(v) } }) - modeCreate := true - var toggle *widget.Button - toggle = widget.NewButton("Režim: Vytvořit (klikni pro Otevřít)", func() { - modeCreate = !modeCreate - if modeCreate { - toggle.SetText("Režim: Vytvořit (klikni pro Otevřít)") - pw2Entry.Enable() - } else { - toggle.SetText("Režim: Otevřít (klikni pro Vytvořit)") - pw2Entry.Disable() - } - }) - form := &widget.Form{Items: []*widget.FormItem{ - {Text: "Heslo", Widget: pwEntry}, - {Text: "Potvrzení", Widget: pw2Entry}, - }} meter := container.NewVBox(strengthBar, strengthLabel) - topRow := container.NewBorder(nil, nil, nil, genBtn, meter) - content := container.NewVBox(toggle, topRow, form) - d := dialog.NewCustomConfirm("Trezor", "OK", "Zrušit", content, func(ok bool) { + topCreate := container.NewBorder(nil, nil, nil, genBtn, meter) + createForm := widget.NewForm( + widget.NewFormItem("Heslo", createPw1), + widget.NewFormItem("Potvrzení", createPw2), + ) + createContent := container.NewVBox(topCreate, createForm) + + tabs := container.NewAppTabs( + container.NewTabItem("Odemknout", unlockContent), + container.NewTabItem("Vytvořit", createContent), + ) + tabs.SetTabLocation(container.TabLocationTop) + // Výběr aktivní záložky dle existence souboru + if _, err := os.Stat(vaultPath); err != nil { // neexistuje => vytvořit + tabs.SelectIndex(1) + } else { + tabs.SelectIndex(0) + } + + d := dialog.NewCustomConfirm("Trezor", "OK", "Zrušit", tabs, func(ok bool) { if !ok { onResult(false, "") return } - pw := pwEntry.Text - if modeCreate { - if pw != pw2Entry.Text { + sel := tabs.Selected() + if sel != nil && sel.Text == "Vytvořit" { + pw := createPw1.Text + if pw != createPw2.Text { dialog.NewError(errors.New("Hesla se neshodují"), w).Show() return } @@ -98,9 +110,14 @@ func ShowPasswordVaultDialog(w fyne.Window, onResult func(create bool, password dialog.NewError(err, w).Show() return } + onResult(true, pw) + return } - onResult(modeCreate, pw) + // Unlock + onResult(false, unlockPw.Text) }, w) + // Větší rozměr pro lepší přehlednost + d.Resize(fyne.NewSize(620, 440)) d.Show() } diff --git a/lib/crypto.go b/lib/crypto.go index 993bf76..69f7d37 100644 --- a/lib/crypto.go +++ b/lib/crypto.go @@ -32,7 +32,6 @@ type Service struct { PubPEM []byte `json:"pub,omitempty"` CertPEM []byte `json:"cert,omitempty"` dir string `json:"-"` - symKey []byte `json:"-"` // interní AES klíč pro lokální šifrování } func NewService(storageDir string) (*Service, error) { @@ -166,7 +165,6 @@ func (s *Service) loadOrGenerateKeys() error { privPath := filepath.Join(s.dir, "identity_key.pem") pubPath := filepath.Join(s.dir, "public.pem") certPath := filepath.Join(s.dir, "identity.crt") - symPath := filepath.Join(s.dir, "sym.key") // base64(klic) fmt.Printf("Using storage dir: %s\n", s.dir) if fileExists(privPath) && fileExists(pubPath) && fileExists(certPath) { @@ -185,25 +183,6 @@ func (s *Service) loadOrGenerateKeys() error { s.Priv = key s.PubPEM, _ = os.ReadFile(pubPath) s.CertPEM, _ = os.ReadFile(certPath) - // Načti symetrický klíč (pokud existuje); pokud ne, vytvoř. - if fileExists(symPath) { - b, err := os.ReadFile(symPath) - if err != nil { - return err - } - decoded, err := base64.StdEncoding.DecodeString(string(b)) - if err != nil { - return fmt.Errorf("sym.key base64 decode: %w", err) - } - if l := len(decoded); l != 16 && l != 24 && l != 32 { - return fmt.Errorf("unsupported sym key length: %d", l) - } - s.symKey = decoded - } else { - if err := s.generateAndPersistSymKey(symPath); err != nil { - return err - } - } return nil } @@ -234,91 +213,9 @@ func (s *Service) loadOrGenerateKeys() error { if err := os.WriteFile(certPath, s.CertPEM, 0o644); err != nil { return err } - // Vytvoř symetrický klíč - if err := s.generateAndPersistSymKey(symPath); err != nil { - return err - } return nil } -// generateAndPersistSymKey vytvoří nový AES-256 klíč a uloží jej (pokud existuje dir). -func (s *Service) generateAndPersistSymKey(path string) error { - k := make([]byte, 32) - if _, err := rand.Read(k); err != nil { - return err - } - s.symKey = k - if s.dir == "" { // ephemeral (např. web mód bez persistence) - return nil - } - enc := base64.StdEncoding.EncodeToString(k) - return os.WriteFile(path, []byte(enc), 0o600) -} - -// SymEnvelope je malý formát pro interní symetrické šifrování. -type SymEnvelope struct { - N string `json:"n"` // base64(nonce) - CT string `json:"ct"` // base64(ciphertext||tag) -} - -// SymmetricEncrypt šifruje plaintext pomocí interního AES-GCM klíče. -func (s *Service) SymmetricEncrypt(plain string) (string, error) { - if len(s.symKey) == 0 { - return "", errors.New("sym key not initialized") - } - block, err := aes.NewCipher(s.symKey) - if err != nil { - return "", err - } - gcm, err := cipher.NewGCM(block) - if err != nil { - return "", err - } - nonce := make([]byte, gcm.NonceSize()) - if _, err := rand.Read(nonce); err != nil { - return "", err - } - ct := gcm.Seal(nil, nonce, []byte(plain), nil) - env := SymEnvelope{ - N: base64.StdEncoding.EncodeToString(nonce), - CT: base64.StdEncoding.EncodeToString(ct), - } - out, _ := json.Marshal(env) - return string(out), nil -} - -// SymmetricDecrypt dešifruje JSON envelope vytvořený SymmetricEncrypt. -func (s *Service) SymmetricDecrypt(payload string) (string, error) { - if len(s.symKey) == 0 { - return "", errors.New("sym key not initialized") - } - var env SymEnvelope - if err := json.Unmarshal([]byte(payload), &env); err != nil { - return "", fmt.Errorf("invalid sym envelope json: %w", err) - } - nonce, err := base64.StdEncoding.DecodeString(env.N) - if err != nil { - return "", fmt.Errorf("nonce b64: %w", err) - } - ct, err := base64.StdEncoding.DecodeString(env.CT) - if err != nil { - return "", fmt.Errorf("ct b64: %w", err) - } - block, err := aes.NewCipher(s.symKey) - if err != nil { - return "", err - } - gcm, err := cipher.NewGCM(block) - if err != nil { - return "", err - } - pt, err := gcm.Open(nil, nonce, ct, nil) - if err != nil { - return "", fmt.Errorf("gcm open: %w", err) - } - return string(pt), nil -} - func generateSelfSignedCert(priv *rsa.PrivateKey) ([]byte, error) { tpl := &x509.Certificate{ SerialNumber: big.NewInt(time.Now().UnixNano()), diff --git a/main.go b/main.go index 19361d4..37a3e8e 100644 --- a/main.go +++ b/main.go @@ -45,8 +45,9 @@ func runFyne() { w.SetContent(placeholder) showDialog := func() { - ShowPasswordVaultDialog(w, func(create bool, password string) { - if password == "" { + ShowPasswordVaultDialog(w, vaultPath, func(create bool, password string) { + if password == "" { // Cancel nebo zavření dialogu => ukonči app + fyne.CurrentApp().Quit() return } var store encrypt.SecureJSONStore @@ -71,7 +72,7 @@ func runFyne() { } parts := buildEntries() fyne.CurrentApp().Driver().AllWindows()[0].SetTitle("Encryptor (Vault)") - w.SetContent(buildTabbedUI(parts, vs)) + w.SetContent(buildTabbedUI(parts, vs, vaultPath)) }) } diff --git a/ui.go b/ui.go index 8418067..96b8e71 100644 --- a/ui.go +++ b/ui.go @@ -1,12 +1,16 @@ package main import ( + "errors" + encrypt "fckeuspy-go/lib" "image/color" + "os" "time" "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" ) @@ -80,7 +84,7 @@ func (simpleTheme) Size(n fyne.ThemeSizeName) float32 { return theme.Defau var forceDark = true // Build key section -func buildIdentityTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject { +func buildIdentityTab(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.CanvasObject { btnCopyPub := widget.NewButton("Copy public.pem", func() { copyClip(svc.PublicPEM(), parts) }) btnCopyCrt := widget.NewButton("Copy identity.crt", func() { copyClip(svc.PublicCert(), parts) }) btnShowPub := widget.NewButton("Show pub", func() { parts.outKey.SetText(svc.PublicPEM()) }) @@ -88,7 +92,34 @@ func buildIdentityTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject { btnClear := widget.NewButton("Clear", func() { parts.outKey.SetText("") }) btnPaste := widget.NewButton("Paste", func() { parts.outKey.SetText(fyne.CurrentApp().Clipboard().Content()) }) - tileIdentity := buttonTile(btnCopyPub, btnCopyCrt, btnPaste, btnShowPub, btnShowCrt, btnClear) + deleteBtn := widget.NewButton("Smazat identitu", func() { + // dialog pro heslo + pwEntry := widget.NewPasswordEntry() + pwEntry.SetPlaceHolder("Heslo pro potvrzení…") + content := widget.NewForm(widget.NewFormItem("Heslo", pwEntry)) + dialog.ShowCustomConfirm("Potvrdit smazání", "Smazat", "Zrušit", content, func(ok bool) { + if !ok { + return + } + pw := pwEntry.Text + if pw == "" { + dialog.NewError(errors.New("heslo je prázdné"), fyne.CurrentApp().Driver().AllWindows()[0]).Show() + return + } + if _, err := encrypt.OpenEncryptedStore(vaultPath, pw); err != nil { + dialog.NewError(errors.New("neplatné heslo"), fyne.CurrentApp().Driver().AllWindows()[0]).Show() + return + } + if err := os.Remove(vaultPath); err != nil { + dialog.NewError(err, fyne.CurrentApp().Driver().AllWindows()[0]).Show() + return + } + // Quit app po odstranění (uživatel musí znovu spustit a vytvořit nový vault) + fyne.CurrentApp().Quit() + }, fyne.CurrentApp().Driver().AllWindows()[0]) + }) + + tileIdentity := buttonTile(btnCopyPub, btnCopyCrt, btnPaste, btnShowPub, btnShowCrt, btnClear, deleteBtn) group := container.NewVBox( widget.NewLabelWithStyle("Moje identita", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), tileIdentity, @@ -189,8 +220,8 @@ func buildDecryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject { return container.NewVScroll(group) } -func buildTabbedUI(parts *uiParts, svc ServiceFacade) fyne.CanvasObject { - idTab := container.NewTabItem("Identita", buildIdentityTab(parts, svc)) +func buildTabbedUI(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.CanvasObject { + idTab := container.NewTabItem("Identita", buildIdentityTab(parts, svc, vaultPath)) encTab := container.NewTabItem("Šifrování", buildEncryptTab(parts, svc)) decTab := container.NewTabItem("Dešifrování", buildDecryptTab(parts, svc)) tabs := container.NewAppTabs(idTab, encTab, decTab)