This commit is contained in:
Lukas Batelka 2025-09-25 22:34:08 +02:00
parent 68b8233d2d
commit 24d482c81d
7 changed files with 167 additions and 61 deletions

View File

@ -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. // 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="". // 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 modeCreate := false
if _, err := os.Stat(vaultPath); err != nil { if _, err := os.Stat(vaultPath); err != nil {
modeCreate = true modeCreate = true
@ -104,6 +104,8 @@ func ShowPasswordVaultDialog(w fyne.Window, vaultPath string, onResult func(crea
createPw1.SetPlaceHolder("Heslo…") createPw1.SetPlaceHolder("Heslo…")
createPw2 := widget.NewPasswordEntry() createPw2 := widget.NewPasswordEntry()
createPw2.SetPlaceHolder("Znovu heslo…") createPw2.SetPlaceHolder("Znovu heslo…")
commonNameEntry := widget.NewEntry()
commonNameEntry.SetPlaceHolder("Jméno identity (např. Alice)…")
strengthBar := widget.NewProgressBar() strengthBar := widget.NewProgressBar()
strengthBar.Min = 0 strengthBar.Min = 0
strengthBar.Max = 100 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 // Obě pole stejné šířky žádné tlačítko uvnitř prvního řádku
form := widget.NewForm( form := widget.NewForm(
widget.NewFormItem("Jméno", commonNameEntry),
widget.NewFormItem("Heslo", createPw1), widget.NewFormItem("Heslo", createPw1),
widget.NewFormItem("Potvrzení", createPw2), widget.NewFormItem("Potvrzení", createPw2),
) )
@ -208,7 +211,7 @@ func ShowPasswordVaultDialog(w fyne.Window, vaultPath string, onResult func(crea
return return
} }
completed = true completed = true
onResult(false, pw) onResult(false, pw, "")
d.Hide() d.Hide()
} }
createBtn.OnTapped = func() { createBtn.OnTapped = func() {
@ -224,7 +227,8 @@ func ShowPasswordVaultDialog(w fyne.Window, vaultPath string, onResult func(crea
return return
} }
completed = true completed = true
onResult(true, pw1) cn := strings.TrimSpace(commonNameEntry.Text)
onResult(true, pw1, cn)
d.Hide() d.Hide()
} }
body := container.NewVBox(header, container.NewPadded(stack), statusLabel) body := container.NewVBox(header, container.NewPadded(stack), statusLabel)
@ -234,7 +238,7 @@ func ShowPasswordVaultDialog(w fyne.Window, vaultPath string, onResult func(crea
close(countdownCancel) close(countdownCancel)
} }
if !completed { if !completed {
onResult(false, "") onResult(false, "", "")
} }
}) })
d.Resize(fyne.NewSize(480, 320)) d.Resize(fyne.NewSize(480, 320))

6
go.mod
View File

@ -4,7 +4,10 @@ go 1.24.0
require ( require (
fyne.io/fyne/v2 v2.6.3 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 github.com/spf13/cobra v1.10.1
golang.org/x/crypto v0.42.0
) )
require ( require (
@ -28,18 +31,15 @@ require (
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
github.com/kr/text v0.2.0 // 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/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rymdport/portal v0.4.1 // 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/spf13/pflag v1.0.9 // indirect
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
github.com/stretchr/testify v1.10.0 // indirect github.com/stretchr/testify v1.10.0 // indirect
github.com/yuin/goldmark v1.7.8 // 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/image v0.24.0 // indirect
golang.org/x/net v0.43.0 // indirect golang.org/x/net v0.43.0 // indirect
golang.org/x/sys v0.36.0 // indirect golang.org/x/sys v0.36.0 // indirect

6
go.sum
View File

@ -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/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 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= 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 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= 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 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 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 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= 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= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -100,7 +100,7 @@ const (
) )
// CreateEncryptedStore vytvoří nový soubor; error pokud již existuje. // 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 { if err := validatePasswordStrength(password); err != nil {
return nil, err return nil, err
} }
@ -116,7 +116,7 @@ func CreateEncryptedStore(path, password string, generateIdentity bool) (SecureJ
kdfP: defaultKDFP, kdfP: defaultKDFP,
plain: internalPlain{Updated: time.Now().UTC(), Data: make(map[string]json.RawMessage)}, 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 return nil, err
} }
if err := s.Flush(); err != nil { 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. // 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() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
salt := make([]byte, 16) salt := make([]byte, 16)
@ -155,14 +155,14 @@ func (s *secureJSONStore) initNew(password string, generateIdentity bool) error
s.salt = salt s.salt = salt
s.key = key s.key = key
if generateIdentity { if generateIdentity {
if err := s.generateIdentityLocked(); err != nil { if err := s.generateIdentityLocked(commonName); err != nil {
return err return err
} }
} }
return nil return nil
} }
func (s *secureJSONStore) generateIdentityLocked() error { func (s *secureJSONStore) generateIdentityLocked(commonName string) error {
// RSA 2048 // RSA 2048
pk, err := rsa.GenerateKey(rand.Reader, 2048) pk, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil { 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)}) privPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(pk)})
pubASN1, _ := x509.MarshalPKIXPublicKey(&pk.PublicKey) pubASN1, _ := x509.MarshalPKIXPublicKey(&pk.PublicKey)
pubPEM := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubASN1}) pubPEM := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubASN1})
// Optional self-signed cert s rozumným CommonName (lze upravit později UI) // Optional self-signed cert s CommonName uživatele (fallback pokud prázdný)
certPEM, _ := generateSelfSignedCert(pk, "Local User") if strings.TrimSpace(commonName) == "" {
commonName = "Local User"
}
certPEM, _ := generateSelfSignedCert(pk, commonName)
_ = s.putLocked("_identity_private_pem", string(privPEM)) _ = s.putLocked("_identity_private_pem", string(privPEM))
_ = s.putLocked("_identity_public_pem", string(pubPEM)) _ = s.putLocked("_identity_public_pem", string(pubPEM))
_ = s.putLocked("_identity_cert_pem", string(certPEM)) _ = s.putLocked("_identity_cert_pem", string(certPEM))

View File

@ -47,7 +47,7 @@ func runFyne() {
w.SetContent(placeholder) w.SetContent(placeholder)
showDialog := func() { 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 if password == "" { // Cancel nebo zavření dialogu => ukonči app
fyne.CurrentApp().Quit() fyne.CurrentApp().Quit()
return return
@ -55,7 +55,7 @@ func runFyne() {
var store encrypt.SecureJSONStore var store encrypt.SecureJSONStore
var err error var err error
if create { if create {
store, err = encrypt.CreateEncryptedStore(vaultPath, password, true) store, err = encrypt.CreateEncryptedStore(vaultPath, password, true, commonName)
} else { } else {
store, err = encrypt.OpenEncryptedStore(vaultPath, password) store, err = encrypt.OpenEncryptedStore(vaultPath, password)
if err != nil { if err != nil {
@ -95,7 +95,7 @@ func RunWebApp() {
} }
var store encrypt.SecureJSONStore var store encrypt.SecureJSONStore
if _, statErr := os.Stat(vaultPath); os.IsNotExist(statErr) { 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 { if err != nil {
log.Fatal(err) log.Fatal(err)
} }

121
ui.go
View File

@ -36,6 +36,9 @@ type uiParts struct {
showPeerQR bool showPeerQR bool
payloadQR *canvas.Image payloadQR *canvas.Image
showPayloadQR bool 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 { func buildEntries() *uiParts {
@ -262,6 +265,47 @@ func copyClip(s string, parts *uiParts) {
func buildEncryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject { func buildEncryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
parts.cipherOut.Disable() parts.cipherOut.Disable()
// Jeden společný dynamický label (zabrání rozlámaní po znacích)
peerHeaderLabel := widget.NewLabel("Veřejný klíč příjemce:")
peerHeaderLabel.Wrapping = fyne.TextWrapOff
// helper: najde kontakt podle přesného matchu certu/public key (nebo substringu) a vrátí zobrazení
updatePeerInfo := func() {
text := strings.TrimSpace(parts.peer.Text)
if text == "" {
peerHeaderLabel.SetText("Veřejný klíč příjemce:")
return
}
cn := extractCN(text)
var contactName string
if list, err := svc.ListContacts(); err == nil {
for _, c := range list {
if parts.peerContactID != "" && c.ID == parts.peerContactID { // prefer explicit ID
contactName = c.Name
break
}
if c.Cert == text { // přesný match obsahu
contactName = c.Name
parts.peerContactID = c.ID
break
}
if contactName == "" && parts.peerContactID == "" && cn != "" && extractCN(c.Cert) == cn { // fallback podle CN
contactName = c.Name
parts.peerContactID = c.ID
}
}
}
var suffix string
if contactName != "" && cn != "" {
suffix = fmt.Sprintf(" %s (%s)", contactName, cn)
} else if contactName != "" {
suffix = " " + contactName
} else if cn != "" {
suffix = " CN: " + cn
}
peerHeaderLabel.SetText("Veřejný klíč příjemce:" + suffix)
}
// Peer section with QR/Text toggle // Peer section with QR/Text toggle
peerContainer := container.NewVBox() peerContainer := container.NewVBox()
peerToggleAction := widget.NewToolbarAction(theme.VisibilityOffIcon(), nil) peerToggleAction := widget.NewToolbarAction(theme.VisibilityOffIcon(), nil)
@ -413,12 +457,13 @@ func buildEncryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
if parts.showPeerQR { if parts.showPeerQR {
updatePeerQR(parts.peer.Text) updatePeerQR(parts.peer.Text)
} }
updatePeerInfo()
encAction() encAction()
} }
// Sections // Sections
peerSection := container.NewVBox( peerSection := container.NewVBox(
container.NewHBox(widget.NewLabel("Veřejný klíč příjemce"), layout.NewSpacer(), peerToolbar), container.NewHBox(peerHeaderLabel, layout.NewSpacer(), peerToolbar),
peerContainer, peerContainer,
) )
msgSection := container.NewVBox( msgSection := container.NewVBox(
@ -437,6 +482,10 @@ func buildEncryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
peerSection, peerSection,
split, split,
) )
// expose callback pro ostatní taby (Kontakty -> Použít ve Šifrování)
parts.updatePeerInfo = updatePeerInfo
// počáteční pokus o naplnění labelu pokud něco už je
updatePeerInfo()
return group return group
} }
@ -642,7 +691,13 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
if strings.TrimSpace(name) == "" { if strings.TrimSpace(name) == "" {
name = "(nový)" name = "(nový)"
} }
// include CN from draft cert if any
cn := extractCN(draft.Cert)
if cn != "" {
o.(*widget.Label).SetText(fmt.Sprintf("✳ %s (nový) (%s)", name, cn))
} else {
o.(*widget.Label).SetText("✳ " + name + " (nový)") o.(*widget.Label).SetText("✳ " + name + " (nový)")
}
return return
} }
// shift index for real contacts // shift index for real contacts
@ -652,8 +707,13 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
if name == "" { if name == "" {
name = "(bez názvu)" name = "(bez názvu)"
} }
cn := extractCN(filtered[real].Cert)
if cn != "" {
o.(*widget.Label).SetText(fmt.Sprintf("%s (%s)", name, cn))
} else {
o.(*widget.Label).SetText(name) o.(*widget.Label).SetText(name)
} }
}
return return
} }
// no draft // no draft
@ -662,8 +722,13 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
if name == "" { if name == "" {
name = "(bez názvu)" name = "(bez názvu)"
} }
cn := extractCN(filtered[i].Cert)
if cn != "" {
o.(*widget.Label).SetText(fmt.Sprintf("%s (%s)", name, cn))
} else {
o.(*widget.Label).SetText(name) o.(*widget.Label).SetText(name)
} }
}
}, },
) )
@ -691,6 +756,7 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
certEntry := widget.NewMultiLineEntry() certEntry := widget.NewMultiLineEntry()
certEntry.SetMinRowsVisible(8) certEntry.SetMinRowsVisible(8)
certEntry.Wrapping = fyne.TextWrapWord 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 // Detail toolbar
pasteAction := widget.NewToolbarAction(theme.ContentPasteIcon(), func() { pasteAction := widget.NewToolbarAction(theme.ContentPasteIcon(), func() {
clip := fyne.CurrentApp().Clipboard().Content() clip := fyne.CurrentApp().Clipboard().Content()
@ -742,21 +808,25 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
parts.showToast("Chybí cert") parts.showToast("Chybí cert")
return return
} }
if name == "" || name == "Kontakt" || name == "Nový kontakt" { cn := extractCN(cert)
name = makeDefaultName() // 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 if draft != nil { // saving draft as new contact
_ = svc.SaveContact(Contact{Name: name, Cert: cert}) _ = svc.SaveContact(Contact{Name: finalName, Cert: cert})
draft = nil draft = nil
searchQuery = "" // clear filter to show new searchQuery = "" // clear filter to show new
search.SetText("") search.SetText("")
} else if selected < 0 || selected >= len(filtered) { // new without draft (fallback) } else if selected < 0 || selected >= len(filtered) { // new without draft (fallback)
_ = svc.SaveContact(Contact{Name: name, Cert: cert}) _ = svc.SaveContact(Contact{Name: finalName, Cert: cert})
searchQuery = "" searchQuery = ""
search.SetText("") search.SetText("")
} else { // update existing } else { // update existing
c := filtered[selected] c := filtered[selected]
c.Name = name c.Name = finalName
c.Cert = cert c.Cert = cert
_ = svc.SaveContact(c) _ = svc.SaveContact(c)
} }
@ -765,9 +835,40 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
list.Refresh() list.Refresh()
updateEmptyState() updateEmptyState()
parts.showToast("Uloženo") parts.showToast("Uloženo")
}
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
}
proceed(name)
}) })
useBtn := widget.NewButtonWithIcon("Použít ve Šifrování", theme.ConfirmIcon(), func() { useBtn := widget.NewButtonWithIcon("Použít ve Šifrování", theme.ConfirmIcon(), func() {
// 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.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í") parts.showToast("Kontakt vložen do Šifrování")
}) })
@ -862,12 +963,10 @@ func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
defer r.Close() defer r.Close()
b, _ := io.ReadAll(r) b, _ := io.ReadAll(r)
txt := string(b) txt := string(b)
nm := extractCN(txt) certEntry.SetText(txt) // OnChanged will offer CN usage if present
if nm == "" { if extractCN(txt) == "" && strings.TrimSpace(nameEntry.Text) == "" {
nm = "Nový kontakt" nameEntry.SetText("Nový kontakt")
} }
nameEntry.SetText(nm)
certEntry.SetText(txt)
selected = -1 selected = -1
}, fyne.CurrentApp().Driver().AllWindows()[0]) }, fyne.CurrentApp().Driver().AllWindows()[0])
fd.Show() fd.Show()

View File

@ -166,15 +166,21 @@ func generateUniqueContactID(existing []Contact) string {
} }
func extractCN(pemText string) string { func extractCN(pemText string) string {
block, _ := pem.Decode([]byte(pemText)) // 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 { if block == nil {
return "" break
} }
if block.Type == "CERTIFICATE" { if block.Type == "CERTIFICATE" {
if cert, err := x509.ParseCertificate(block.Bytes); err == nil { if cert, err := x509.ParseCertificate(block.Bytes); err == nil {
return cert.Subject.CommonName return cert.Subject.CommonName
} }
} }
// continue to next block until certificate found
}
return "" return ""
} }