feature/ui - zatim ne uplne uhlazena ale celkem pouzitelna appka #1

Merged
luke-20 merged 17 commits from feature/ui into main 2025-09-28 21:05:52 +02:00
4 changed files with 87 additions and 141 deletions
Showing only changes of commit 2b7e3f6c85 - Show all commits

View File

@ -3,6 +3,7 @@ package main
import ( import (
"errors" "errors"
encrypt "fckeuspy-go/lib" encrypt "fckeuspy-go/lib"
"os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -42,11 +43,20 @@ func NewUI() (stprageDir string, window fyne.Window) {
// ShowPasswordVaultDialog zobrazí dialog pro vytvoření nebo otevření trezoru. // 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. // 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)) { func ShowPasswordVaultDialog(w fyne.Window, vaultPath string, onResult func(create bool, password string)) {
pwEntry := widget.NewPasswordEntry() // Unlock tab
pwEntry.SetPlaceHolder("Heslo…") unlockPw := widget.NewPasswordEntry()
pw2Entry := widget.NewPasswordEntry() unlockPw.SetPlaceHolder("Heslo…")
pw2Entry.SetPlaceHolder("Znovu 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 := widget.NewProgressBar()
strengthBar.Min = 0 strengthBar.Min = 0
strengthBar.Max = 100 strengthBar.Max = 100
@ -56,41 +66,43 @@ func ShowPasswordVaultDialog(w fyne.Window, onResult func(create bool, password
strengthBar.SetValue(float64(score)) strengthBar.SetValue(float64(score))
strengthLabel.SetText(desc) strengthLabel.SetText(desc)
} }
pwEntry.OnChanged = updateStrength createPw1.OnChanged = updateStrength
genBtn := widget.NewButton("Generovat", func() { genBtn := widget.NewButton("Generovat", func() {
if v, err := encrypt.GenerateRandomPassword(20); err == nil { if v, err := encrypt.GenerateRandomPassword(20); err == nil {
pwEntry.SetText(v) createPw1.SetText(v)
pw2Entry.SetText(v) createPw2.SetText(v)
updateStrength(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) meter := container.NewVBox(strengthBar, strengthLabel)
topRow := container.NewBorder(nil, nil, nil, genBtn, meter) topCreate := container.NewBorder(nil, nil, nil, genBtn, meter)
content := container.NewVBox(toggle, topRow, form) createForm := widget.NewForm(
d := dialog.NewCustomConfirm("Trezor", "OK", "Zrušit", content, func(ok bool) { 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 { if !ok {
onResult(false, "") onResult(false, "")
return return
} }
pw := pwEntry.Text sel := tabs.Selected()
if modeCreate { if sel != nil && sel.Text == "Vytvořit" {
if pw != pw2Entry.Text { pw := createPw1.Text
if pw != createPw2.Text {
dialog.NewError(errors.New("Hesla se neshodují"), w).Show() dialog.NewError(errors.New("Hesla se neshodují"), w).Show()
return return
} }
@ -98,9 +110,14 @@ func ShowPasswordVaultDialog(w fyne.Window, onResult func(create bool, password
dialog.NewError(err, w).Show() dialog.NewError(err, w).Show()
return return
} }
onResult(true, pw)
return
} }
onResult(modeCreate, pw) // Unlock
onResult(false, unlockPw.Text)
}, w) }, w)
// Větší rozměr pro lepší přehlednost
d.Resize(fyne.NewSize(620, 440))
d.Show() d.Show()
} }

View File

@ -32,7 +32,6 @@ type Service struct {
PubPEM []byte `json:"pub,omitempty"` PubPEM []byte `json:"pub,omitempty"`
CertPEM []byte `json:"cert,omitempty"` CertPEM []byte `json:"cert,omitempty"`
dir string `json:"-"` dir string `json:"-"`
symKey []byte `json:"-"` // interní AES klíč pro lokální šifrování
} }
func NewService(storageDir string) (*Service, error) { func NewService(storageDir string) (*Service, error) {
@ -166,7 +165,6 @@ func (s *Service) loadOrGenerateKeys() error {
privPath := filepath.Join(s.dir, "identity_key.pem") privPath := filepath.Join(s.dir, "identity_key.pem")
pubPath := filepath.Join(s.dir, "public.pem") pubPath := filepath.Join(s.dir, "public.pem")
certPath := filepath.Join(s.dir, "identity.crt") 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) fmt.Printf("Using storage dir: %s\n", s.dir)
if fileExists(privPath) && fileExists(pubPath) && fileExists(certPath) { if fileExists(privPath) && fileExists(pubPath) && fileExists(certPath) {
@ -185,25 +183,6 @@ func (s *Service) loadOrGenerateKeys() error {
s.Priv = key s.Priv = key
s.PubPEM, _ = os.ReadFile(pubPath) s.PubPEM, _ = os.ReadFile(pubPath)
s.CertPEM, _ = os.ReadFile(certPath) 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 return nil
} }
@ -234,91 +213,9 @@ func (s *Service) loadOrGenerateKeys() error {
if err := os.WriteFile(certPath, s.CertPEM, 0o644); err != nil { if err := os.WriteFile(certPath, s.CertPEM, 0o644); err != nil {
return err return err
} }
// Vytvoř symetrický klíč
if err := s.generateAndPersistSymKey(symPath); err != nil {
return err
}
return nil 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) { func generateSelfSignedCert(priv *rsa.PrivateKey) ([]byte, error) {
tpl := &x509.Certificate{ tpl := &x509.Certificate{
SerialNumber: big.NewInt(time.Now().UnixNano()), SerialNumber: big.NewInt(time.Now().UnixNano()),

View File

@ -45,8 +45,9 @@ func runFyne() {
w.SetContent(placeholder) w.SetContent(placeholder)
showDialog := func() { showDialog := func() {
ShowPasswordVaultDialog(w, func(create bool, password string) { ShowPasswordVaultDialog(w, vaultPath, func(create bool, password string) {
if password == "" { if password == "" { // Cancel nebo zavření dialogu => ukonči app
fyne.CurrentApp().Quit()
return return
} }
var store encrypt.SecureJSONStore var store encrypt.SecureJSONStore
@ -71,7 +72,7 @@ func runFyne() {
} }
parts := buildEntries() parts := buildEntries()
fyne.CurrentApp().Driver().AllWindows()[0].SetTitle("Encryptor (Vault)") fyne.CurrentApp().Driver().AllWindows()[0].SetTitle("Encryptor (Vault)")
w.SetContent(buildTabbedUI(parts, vs)) w.SetContent(buildTabbedUI(parts, vs, vaultPath))
}) })
} }

39
ui.go
View File

@ -1,12 +1,16 @@
package main package main
import ( import (
"errors"
encrypt "fckeuspy-go/lib"
"image/color" "image/color"
"os"
"time" "time"
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
) )
@ -80,7 +84,7 @@ func (simpleTheme) Size(n fyne.ThemeSizeName) float32 { return theme.Defau
var forceDark = true var forceDark = true
// Build key section // 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) }) btnCopyPub := widget.NewButton("Copy public.pem", func() { copyClip(svc.PublicPEM(), parts) })
btnCopyCrt := widget.NewButton("Copy identity.crt", func() { copyClip(svc.PublicCert(), parts) }) btnCopyCrt := widget.NewButton("Copy identity.crt", func() { copyClip(svc.PublicCert(), parts) })
btnShowPub := widget.NewButton("Show pub", func() { parts.outKey.SetText(svc.PublicPEM()) }) 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("") }) btnClear := widget.NewButton("Clear", func() { parts.outKey.SetText("") })
btnPaste := widget.NewButton("Paste", func() { parts.outKey.SetText(fyne.CurrentApp().Clipboard().Content()) }) 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( group := container.NewVBox(
widget.NewLabelWithStyle("Moje identita", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), widget.NewLabelWithStyle("Moje identita", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
tileIdentity, tileIdentity,
@ -189,8 +220,8 @@ func buildDecryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
return container.NewVScroll(group) return container.NewVScroll(group)
} }
func buildTabbedUI(parts *uiParts, svc ServiceFacade) fyne.CanvasObject { func buildTabbedUI(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.CanvasObject {
idTab := container.NewTabItem("Identita", buildIdentityTab(parts, svc)) idTab := container.NewTabItem("Identita", buildIdentityTab(parts, svc, vaultPath))
encTab := container.NewTabItem("Šifrování", buildEncryptTab(parts, svc)) encTab := container.NewTabItem("Šifrování", buildEncryptTab(parts, svc))
decTab := container.NewTabItem("Dešifrování", buildDecryptTab(parts, svc)) decTab := container.NewTabItem("Dešifrování", buildDecryptTab(parts, svc))
tabs := container.NewAppTabs(idTab, encTab, decTab) tabs := container.NewAppTabs(idTab, encTab, decTab)