From 04cce0ebd00da6470bce59782a641859c9148fd7 Mon Sep 17 00:00:00 2001 From: Lukas Batelka Date: Thu, 25 Sep 2025 23:24:34 +0200 Subject: [PATCH] feature/ui sifrovani v pop upu --- fyne_ui.go | 12 +- go.mod | 6 +- go.sum | 6 - lib/crypto_storage.go | 17 +- main.go | 6 +- ui.go | 414 ++++++++++++++++++++---------------------- vault_service.go | 20 +- 7 files changed, 232 insertions(+), 249 deletions(-) diff --git a/fyne_ui.go b/fyne_ui.go index 0d6c456..b249092 100644 --- a/fyne_ui.go +++ b/fyne_ui.go @@ -86,7 +86,7 @@ func NewUI() (stprageDir string, window fyne.Window) { // 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)) { +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 @@ -104,6 +104,8 @@ func ShowPasswordVaultDialog(w fyne.Window, vaultPath string, onResult func(crea 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 @@ -127,6 +129,7 @@ func ShowPasswordVaultDialog(w fyne.Window, vaultPath string, onResult func(crea // 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), ) @@ -208,7 +211,7 @@ func ShowPasswordVaultDialog(w fyne.Window, vaultPath string, onResult func(crea return } completed = true - onResult(false, pw) + onResult(false, pw, "") d.Hide() } createBtn.OnTapped = func() { @@ -224,7 +227,8 @@ func ShowPasswordVaultDialog(w fyne.Window, vaultPath string, onResult func(crea return } completed = true - onResult(true, pw1) + cn := strings.TrimSpace(commonNameEntry.Text) + onResult(true, pw1, cn) d.Hide() } body := container.NewVBox(header, container.NewPadded(stack), statusLabel) @@ -234,7 +238,7 @@ func ShowPasswordVaultDialog(w fyne.Window, vaultPath string, onResult func(crea close(countdownCancel) } if !completed { - onResult(false, "") + onResult(false, "", "") } }) d.Resize(fyne.NewSize(480, 320)) diff --git a/go.mod b/go.mod index 3a33fc3..9d30643 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,10 @@ go 1.24.0 require ( fyne.io/fyne/v2 v2.6.3 + github.com/liyue201/goqr v0.0.0-20200803022322-df443203d4ea + github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/spf13/cobra v1.10.1 + golang.org/x/crypto v0.42.0 ) require ( @@ -28,18 +31,15 @@ require ( github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect github.com/kr/text v0.2.0 // indirect - github.com/liyue201/goqr v0.0.0-20200803022322-df443203d4ea // indirect github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rymdport/portal v0.4.1 // indirect - github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect github.com/spf13/pflag v1.0.9 // indirect github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect github.com/stretchr/testify v1.10.0 // indirect github.com/yuin/goldmark v1.7.8 // indirect - golang.org/x/crypto v0.42.0 // indirect golang.org/x/image v0.24.0 // indirect golang.org/x/net v0.43.0 // indirect golang.org/x/sys v0.36.0 // indirect diff --git a/go.sum b/go.sum index c629956..b743ab5 100644 --- a/go.sum +++ b/go.sum @@ -84,16 +84,10 @@ golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= -golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= -golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/lib/crypto_storage.go b/lib/crypto_storage.go index 5c542cc..2cf7ba9 100644 --- a/lib/crypto_storage.go +++ b/lib/crypto_storage.go @@ -100,7 +100,7 @@ const ( ) // CreateEncryptedStore vytvoří nový soubor; error pokud již existuje. -func CreateEncryptedStore(path, password string, generateIdentity bool) (SecureJSONStore, error) { +func CreateEncryptedStore(path, password string, generateIdentity bool, commonName string) (SecureJSONStore, error) { if err := validatePasswordStrength(password); err != nil { return nil, err } @@ -116,7 +116,7 @@ func CreateEncryptedStore(path, password string, generateIdentity bool) (SecureJ kdfP: defaultKDFP, plain: internalPlain{Updated: time.Now().UTC(), Data: make(map[string]json.RawMessage)}, } - if err := s.initNew(password, generateIdentity); err != nil { + if err := s.initNew(password, generateIdentity, commonName); err != nil { return nil, err } if err := s.Flush(); err != nil { @@ -141,7 +141,7 @@ func OpenEncryptedStore(path, password string) (SecureJSONStore, error) { } // initNew vytvoří salt, odvodí klíč a vytvoří identitu pokud je třeba. -func (s *secureJSONStore) initNew(password string, generateIdentity bool) error { +func (s *secureJSONStore) initNew(password string, generateIdentity bool, commonName string) error { s.mu.Lock() defer s.mu.Unlock() salt := make([]byte, 16) @@ -155,14 +155,14 @@ func (s *secureJSONStore) initNew(password string, generateIdentity bool) error s.salt = salt s.key = key if generateIdentity { - if err := s.generateIdentityLocked(); err != nil { + if err := s.generateIdentityLocked(commonName); err != nil { return err } } return nil } -func (s *secureJSONStore) generateIdentityLocked() error { +func (s *secureJSONStore) generateIdentityLocked(commonName string) error { // RSA 2048 pk, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { @@ -171,8 +171,11 @@ func (s *secureJSONStore) generateIdentityLocked() error { privPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(pk)}) pubASN1, _ := x509.MarshalPKIXPublicKey(&pk.PublicKey) pubPEM := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubASN1}) - // Optional self-signed cert s rozumným CommonName (lze upravit později UI) - certPEM, _ := generateSelfSignedCert(pk, "Local User") + // Optional self-signed cert s CommonName uživatele (fallback pokud prázdný) + if strings.TrimSpace(commonName) == "" { + commonName = "Local User" + } + certPEM, _ := generateSelfSignedCert(pk, commonName) _ = s.putLocked("_identity_private_pem", string(privPEM)) _ = s.putLocked("_identity_public_pem", string(pubPEM)) _ = s.putLocked("_identity_cert_pem", string(certPEM)) diff --git a/main.go b/main.go index b4b15be..7f1e839 100644 --- a/main.go +++ b/main.go @@ -47,7 +47,7 @@ func runFyne() { w.SetContent(placeholder) showDialog := func() { - ShowPasswordVaultDialog(w, vaultPath, func(create bool, password string) { + ShowPasswordVaultDialog(w, vaultPath, func(create bool, password string, commonName string) { if password == "" { // Cancel nebo zavření dialogu => ukonči app fyne.CurrentApp().Quit() return @@ -55,7 +55,7 @@ func runFyne() { var store encrypt.SecureJSONStore var err error if create { - store, err = encrypt.CreateEncryptedStore(vaultPath, password, true) + store, err = encrypt.CreateEncryptedStore(vaultPath, password, true, commonName) } else { store, err = encrypt.OpenEncryptedStore(vaultPath, password) if err != nil { @@ -95,7 +95,7 @@ func RunWebApp() { } var store encrypt.SecureJSONStore if _, statErr := os.Stat(vaultPath); os.IsNotExist(statErr) { - s, err := encrypt.CreateEncryptedStore(vaultPath, pw, true) + s, err := encrypt.CreateEncryptedStore(vaultPath, pw, true, "") if err != nil { log.Fatal(err) } diff --git a/ui.go b/ui.go index a3dfae2..b45b71c 100644 --- a/ui.go +++ b/ui.go @@ -36,6 +36,9 @@ type uiParts struct { showPeerQR bool payloadQR *canvas.Image showPayloadQR bool + peerContactID string // ID kontaktu vloženého do šifrování (pro přesnou identifikaci i při duplicitním CN) + // callback nastavený v buildEncryptTab pro aktualizaci hlavičky peeru z jiných tabů (kontakty) + updatePeerInfo func() } func buildEntries() *uiParts { @@ -259,186 +262,7 @@ func copyClip(s string, parts *uiParts) { // assembleResponsive builds split view that collapses for narrow widths // Tab: Encrypt -func buildEncryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject { - parts.cipherOut.Disable() - - // Peer section with QR/Text toggle - peerContainer := container.NewVBox() - peerToggleAction := widget.NewToolbarAction(theme.VisibilityOffIcon(), nil) - var updatePeer func() - updatePeerQR := func(text string) { - if text == "" { - parts.peerQR.Image = nil - parts.peerQR.Refresh() - return - } - pngBytes, err := GenerateQRPNG(text, 512) - if err != nil { - return - } - img, err := LoadPNG(pngBytes) - if err != nil { - return - } - parts.peerQR.Image = img - parts.peerQR.Refresh() - } - updatePeer = func() { - peerContainer.Objects = nil - if parts.showPeerQR { - updatePeerQR(parts.peer.Text) - peerContainer.Add(parts.peerQR) // copy tlačítko jen v toolbaru - peerToggleAction.SetIcon(theme.VisibilityOffIcon()) - } else { - peerContainer.Add(parts.peer) - peerToggleAction.SetIcon(theme.VisibilityIcon()) - } - peerContainer.Refresh() - } - peerToggleAction.OnActivated = func() { parts.showPeerQR = !parts.showPeerQR; updatePeer() } - parts.peer.OnChanged = func(string) { - if parts.showPeerQR { - updatePeerQR(parts.peer.Text) - } - } - updatePeer() - - // Output section toggle (QR vs text) - outputContainer := container.NewVBox() - outputToggleAction := widget.NewToolbarAction(theme.VisibilityOffIcon(), nil) - updateQR := func(text string) { - if text == "" { - parts.cipherQR.Image = nil - parts.cipherQR.Refresh() - return - } - pngBytes, err := GenerateQRPNG(text, 512) - if err != nil { - parts.showToast("QR error") - return - } - img, err := LoadPNG(pngBytes) - if err != nil { - parts.showToast("PNG err") - return - } - parts.cipherQR.Image = img - parts.cipherQR.Refresh() - } - updateOutput := func() { - outputContainer.Objects = nil - if parts.showQR { // show QR mode - updateQR(parts.cipherOut.Text) - outputContainer.Add(parts.cipherQR) - outputToggleAction.SetIcon(theme.VisibilityOffIcon()) - } else { - outputContainer.Add(parts.cipherOut) - outputToggleAction.SetIcon(theme.VisibilityIcon()) - } - outputContainer.Refresh() - } - outputToggleAction.OnActivated = func() { parts.showQR = !parts.showQR; updateOutput() } - updateOutput() - - encAction := func() { - m := parts.msg.Text - p := parts.peer.Text - if m == "" || p == "" { - parts.showToast("Chybí data") - return - } - go func(msg, peer string) { - res, err := svc.Encrypt(msg, peer) - if err != nil { - fyne.Do(func() { parts.cipherOut.SetText(""); updateQR(""); parts.showToast("Chyba") }) - return - } - fyne.Do(func() { - parts.cipherOut.SetText(res) - // refresh whichever mode is active - updateOutput() - parts.showToast("OK") - }) - }(m, p) - } - - importPeerQR := func() { - fd := dialog.NewFileOpen(func(r fyne.URIReadCloser, err error) { - if err != nil || r == nil { - return - } - defer r.Close() - data, _ := io.ReadAll(r) - img, err := LoadPNG(data) - if err != nil { - dialog.NewError(err, fyne.CurrentApp().Driver().AllWindows()[0]).Show() - return - } - text, err := DecodeQR(img) - if err != nil { - dialog.NewError(err, fyne.CurrentApp().Driver().AllWindows()[0]).Show() - return - } - parts.peer.SetText(text) - }, fyne.CurrentApp().Driver().AllWindows()[0]) - fd.SetFilter(storage.NewExtensionFileFilter([]string{".png"})) - fd.Show() - } - - // Toolbars - peerToolbar := widget.NewToolbar( - widget.NewToolbarAction(theme.ContentPasteIcon(), func() { parts.peer.SetText(fyne.CurrentApp().Clipboard().Content()) }), - widget.NewToolbarAction(theme.ContentCopyIcon(), func() { copyClip(parts.peer.Text, parts) }), - widget.NewToolbarAction(theme.FolderOpenIcon(), importPeerQR), - widget.NewToolbarAction(theme.ContentClearIcon(), func() { parts.peer.SetText("") }), - widget.NewToolbarSeparator(), - peerToggleAction, - ) - outputToolbar := widget.NewToolbar( - widget.NewToolbarAction(theme.ContentCopyIcon(), func() { copyClip(parts.cipherOut.Text, parts) }), - widget.NewToolbarAction(theme.ContentClearIcon(), func() { - parts.cipherOut.SetText("") - parts.msg.SetText("") - if parts.showQR { - updateQR("") - } - }), - widget.NewToolbarSeparator(), - outputToggleAction, - ) - - // Šifrování automaticky při změně zprávy nebo peer - parts.msg.OnChanged = func(string) { encAction() } - parts.peer.OnChanged = func(string) { - if parts.showPeerQR { - updatePeerQR(parts.peer.Text) - } - encAction() - } - - // Sections - peerSection := container.NewVBox( - container.NewHBox(widget.NewLabel("Veřejný klíč příjemce"), layout.NewSpacer(), peerToolbar), - peerContainer, - ) - msgSection := container.NewVBox( - widget.NewLabel("Zpráva"), - parts.msg, - ) - outputSection := container.NewVBox( - container.NewHBox(widget.NewLabel("Výstup"), layout.NewSpacer(), outputToolbar), - outputContainer, - ) - // Split: message (top) vs output (bottom) for better prostor "do spodu" - split := container.NewVSplit(msgSection, outputSection) - split.SetOffset(0.40) - group := container.NewVBox( - widget.NewLabelWithStyle("Šifrování", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), - peerSection, - split, - ) - return group -} +// buildEncryptTab odstraněn – šifrování nyní pouze přes popup z listu kontaktů. // Tab: Decrypt func buildDecryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject { @@ -558,9 +382,9 @@ func buildDecryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject { func buildTabbedUI(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.CanvasObject { idTab := container.NewTabItem("Identita", buildIdentityTab(parts, svc, vaultPath)) contactsTab := container.NewTabItem("Kontakty", buildContactsTab(parts, svc)) - encTab := container.NewTabItem("Šifrování", buildEncryptTab(parts, svc)) + // Odebrána samostatná záložka Šifrování – nahrazena popupem z listu kontaktů. decTab := container.NewTabItem("Dešifrování", buildDecryptTab(parts, svc)) - tabs := container.NewAppTabs(idTab, contactsTab, encTab, decTab) + tabs := container.NewAppTabs(idTab, contactsTab, decTab) tabs.SetTabLocation(container.TabLocationTop) // apply theme; allow toggle via hidden shortcut Ctrl+T (demo) @@ -626,7 +450,95 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject { filtered = tmp } - // Left list (includes optional draft as first row) + // Inline popup builder for encryption + openEncryptPopup := func(ct Contact) { + // Minimalist popup: jméno kontaktu, zpráva, tlačítko Šifrovat, pouze QR kód výsledku + msgEntry := widget.NewMultiLineEntry() + msgEntry.SetPlaceHolder("Zpráva…") + msgEntry.SetMinRowsVisible(6) + qrImg := canvas.NewImageFromImage(nil) + qrImg.FillMode = canvas.ImageFillContain + qrImg.SetMinSize(fyne.NewSize(260, 260)) + status := widget.NewLabel("") + status.Wrapping = fyne.TextWrapWord + updateQR := func(text string) { + if strings.TrimSpace(text) == "" { + qrImg.Image = nil + qrImg.Refresh() + return + } + if b, err := GenerateQRPNG(text, 512); err == nil { + if im, err2 := LoadPNG(b); err2 == nil { + qrImg.Image = im + qrImg.Refresh() + } + } + } + var win fyne.Window = fyne.CurrentApp().Driver().AllWindows()[0] + lastCipher := "" + doEncrypt := func() { + m := strings.TrimSpace(msgEntry.Text) + fyne.Do(func() { + if m == "" { + status.SetText("Zpráva je prázdná") + lastCipher = "" + updateQR("") + return + } + status.SetText("Šifruji…") + }) + if m == "" { // už jsme UI vyřešili + return + } + go func(txt string) { + res, err := svc.Encrypt(txt, ct.Cert) + if err != nil { + fyne.Do(func() { status.SetText("Chyba: " + err.Error()) }) + return + } + fyne.Do(func() { + lastCipher = res + updateQR(res) + status.SetText("Hotovo") + }) + }(m) + } + + var autoTimer *time.Timer + msgEntry.OnChanged = func(string) { + if autoTimer != nil { + autoTimer.Stop() + } + autoTimer = time.AfterFunc(300*time.Millisecond, doEncrypt) + } + copyBtn := widget.NewButton("Kopírovat payload", func() { + if lastCipher != "" { + copyClip(lastCipher, parts) + status.SetText("Zkopírováno") + } + }) + popupContent := container.NewVBox( + widget.NewLabel("Zpráva"), + msgEntry, + widget.NewSeparator(), + container.NewHBox(widget.NewLabel("QR kód"), layout.NewSpacer(), copyBtn), + qrImg, + status, + ) + cn := extractCN(ct.Cert) + titleName := ct.Name + if titleName == "" { + titleName = "(bez názvu)" + } + if cn != "" && !strings.Contains(titleName, cn) { + titleName = fmt.Sprintf("%s (%s)", titleName, cn) + } + popup := dialog.NewCustom(fmt.Sprintf("Poslat zprávu: %s", titleName), "Zavřít", popupContent, win) + popup.Resize(fyne.NewSize(640, 520)) + popup.Show() + } + + // Left list (includes optional draft as first row) — custom row with label + button list := widget.NewList( func() int { if draft != nil { @@ -634,35 +546,65 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject { } return len(filtered) }, - func() fyne.CanvasObject { return widget.NewLabel("") }, + func() fyne.CanvasObject { + lbl := widget.NewLabel("") + btn := widget.NewButton("Poslat zprávu", nil) + btn.Importance = widget.LowImportance + row := container.NewBorder(nil, nil, nil, btn, lbl) + return row + }, func(i widget.ListItemID, o fyne.CanvasObject) { + row := o.(*fyne.Container) + lbl := row.Objects[0].(*widget.Label) + btn := row.Objects[1].(*widget.Button) if draft != nil { - if int(i) == 0 { // draft row + if int(i) == 0 { name := draft.Name if strings.TrimSpace(name) == "" { name = "(nový)" } - o.(*widget.Label).SetText("✳ " + name + " (nový)") + cn := extractCN(draft.Cert) + if cn != "" { + lbl.SetText(fmt.Sprintf("✳ %s (nový) (%s)", name, cn)) + } else { + lbl.SetText("✳ " + name + " (nový)") + } + btn.Disable() + btn.OnTapped = nil return } - // shift index for real contacts real := int(i) - 1 if real >= 0 && real < len(filtered) { - name := filtered[real].Name + c := filtered[real] + name := c.Name if name == "" { name = "(bez názvu)" } - o.(*widget.Label).SetText(name) + cn := extractCN(c.Cert) + if cn != "" { + lbl.SetText(fmt.Sprintf("%s (%s)", name, cn)) + } else { + lbl.SetText(name) + } + btn.Enable() + btn.OnTapped = func() { openEncryptPopup(c) } } return } - // no draft if i >= 0 && int(i) < len(filtered) { - name := filtered[i].Name + c := filtered[i] + name := c.Name if name == "" { name = "(bez názvu)" } - o.(*widget.Label).SetText(name) + cn := extractCN(c.Cert) + if cn != "" { + lbl.SetText(fmt.Sprintf("%s (%s)", name, cn)) + } else { + lbl.SetText(name) + } + btn.Enable() + btn.OnTapped = func() { openEncryptPopup(c) } } }, ) @@ -691,6 +633,7 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject { certEntry := widget.NewMultiLineEntry() certEntry.SetMinRowsVisible(8) certEntry.Wrapping = fyne.TextWrapWord + // (Původní okamžité dotazování na CN při vložení zrušeno – nyní se ptáme až při ukládání) // Detail toolbar pasteAction := widget.NewToolbarAction(theme.ContentPasteIcon(), func() { clip := fyne.CurrentApp().Clipboard().Content() @@ -742,32 +685,67 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject { parts.showToast("Chybí cert") return } - if name == "" || name == "Kontakt" || name == "Nový kontakt" { - name = makeDefaultName() + cn := extractCN(cert) + // always ask if CN exists and (a) name empty/reserved OR (b) name differs from CN + shouldAsk := cn != "" && (name == "" || name == "Kontakt" || name == "Nový kontakt" || name != cn) + proceed := func(finalName string) { + if finalName == "" || finalName == "Kontakt" || finalName == "Nový kontakt" { + finalName = makeDefaultName() + } + if draft != nil { // saving draft as new contact + _ = svc.SaveContact(Contact{Name: finalName, Cert: cert}) + draft = nil + searchQuery = "" // clear filter to show new + search.SetText("") + } else if selected < 0 || selected >= len(filtered) { // new without draft (fallback) + _ = svc.SaveContact(Contact{Name: finalName, Cert: cert}) + searchQuery = "" + search.SetText("") + } else { // update existing + c := filtered[selected] + c.Name = finalName + c.Cert = cert + _ = svc.SaveContact(c) + } + load() + applyFilter(searchQuery) + list.Refresh() + updateEmptyState() + parts.showToast("Uloženo") } - if draft != nil { // saving draft as new contact - _ = svc.SaveContact(Contact{Name: name, Cert: cert}) - draft = nil - searchQuery = "" // clear filter to show new - search.SetText("") - } else if selected < 0 || selected >= len(filtered) { // new without draft (fallback) - _ = svc.SaveContact(Contact{Name: name, Cert: cert}) - searchQuery = "" - search.SetText("") - } else { // update existing - c := filtered[selected] - c.Name = name - c.Cert = cert - _ = svc.SaveContact(c) + if shouldAsk { + // build message depending on situation + msg := fmt.Sprintf("Certifikát obsahuje Common Name:\n\n%s\n\nPoužít ho jako název kontaktu? (Aktuálně: %q)", cn, name) + dialog.NewCustomConfirm("Common Name", "Použít", "Ponechat", widget.NewLabel(msg), func(ok bool) { + if ok { + proceed(cn) + } else { + proceed(name) + } + }, fyne.CurrentApp().Driver().AllWindows()[0]).Show() + return } - load() - applyFilter(searchQuery) - list.Refresh() - updateEmptyState() - parts.showToast("Uloženo") + proceed(name) }) useBtn := widget.NewButtonWithIcon("Použít ve Šifrování", theme.ConfirmIcon(), func() { - parts.peer.SetText(certEntry.Text) + // Určit aktuální kontakt (draft nebo vybraný reálný) + var currentID string + if draft != nil && selected == -1 { + currentID = draft.ID + } else if selected >= 0 && selected < len(filtered) { + currentID = filtered[selected].ID + } + // Vždy vynutit změnu: smazat a hned nastavit + parts.peer.SetText("") + parts.peerContactID = "" // reset aby updatePeerInfo neukazoval staré jméno + fyne.Do(func() { + parts.peer.SetText(certEntry.Text) + parts.peerContactID = currentID + // Force refresh hlavičky peeru i v případě, že se formát certu liší (trim apod.) + if parts.updatePeerInfo != nil { + parts.updatePeerInfo() + } + }) parts.showToast("Kontakt vložen do Šifrování") }) @@ -862,12 +840,10 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject { defer r.Close() b, _ := io.ReadAll(r) txt := string(b) - nm := extractCN(txt) - if nm == "" { - nm = "Nový kontakt" + certEntry.SetText(txt) // OnChanged will offer CN usage if present + if extractCN(txt) == "" && strings.TrimSpace(nameEntry.Text) == "" { + nameEntry.SetText("Nový kontakt") } - nameEntry.SetText(nm) - certEntry.SetText(txt) selected = -1 }, fyne.CurrentApp().Driver().AllWindows()[0]) fd.Show() diff --git a/vault_service.go b/vault_service.go index c11a3b1..d6c1316 100644 --- a/vault_service.go +++ b/vault_service.go @@ -166,14 +166,20 @@ func generateUniqueContactID(existing []Contact) string { } func extractCN(pemText string) string { - block, _ := pem.Decode([]byte(pemText)) - if block == nil { - return "" - } - if block.Type == "CERTIFICATE" { - if cert, err := x509.ParseCertificate(block.Bytes); err == nil { - return cert.Subject.CommonName + // Support multiple concatenated PEM blocks (public key + cert, or chain) + rest := []byte(pemText) + for len(rest) > 0 { + var block *pem.Block + block, rest = pem.Decode(rest) + if block == nil { + break } + if block.Type == "CERTIFICATE" { + if cert, err := x509.ParseCertificate(block.Bytes); err == nil { + return cert.Subject.CommonName + } + } + // continue to next block until certificate found } return "" }