From 1b778cece1efe159957324735d6fc173ade717b1 Mon Sep 17 00:00:00 2001 From: Lukas Batelka Date: Wed, 24 Sep 2025 22:13:03 +0200 Subject: [PATCH] feature/ui nastrel ukladani kontaktu --- identity.crt | 18 ----- identity_key.pem | 27 ------- lib/crypto.go | 176 ++--------------------------------------- lib/crypto_storage.go | 4 +- main.go | 41 +++++++++- public.pem | 9 --- ui.go | 180 +++++++++++++++++++++++++++++++++++++----- vault_service.go | 80 +++++++++++++++++++ 8 files changed, 285 insertions(+), 250 deletions(-) delete mode 100644 identity.crt delete mode 100644 identity_key.pem delete mode 100644 public.pem diff --git a/identity.crt b/identity.crt deleted file mode 100644 index cd64a53..0000000 --- a/identity.crt +++ /dev/null @@ -1,18 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIC4TCCAcmgAwIBAgIBATANBgkqhkiG9w0BAQsFADAjMSEwHwYDVQQDExhFbmNy -eXB0b3IgTG9jYWwgSWRlbnRpdHkwHhcNMjUwOTE0MTcwMTMzWhcNMjYwOTE0MTcw -MTMzWjAjMSEwHwYDVQQDExhFbmNyeXB0b3IgTG9jYWwgSWRlbnRpdHkwggEiMA0G -CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3FzA4W5AaH2WVmvaCeVcBAPpN8IjT -bOaFkWtWR/p/m1e7o8mSgTYaDlETuR4mO/Cc0GppzQc2coufPconne8KCvPv5wPj -tXs5rKKRaxmOwKVfbjqdGXHP7CAtoimJl1U6/rDFto4BLr4CEF2/OxFFA89o3Tiy -Z6jaGBAVj5w3N9a5QGk+K8brT/XQAV17rk1fzAShsWIoqW3KR6EN1G3GhwjZTg9y -6u9wzWN9aywewtRDWB9LhwXrz/GpjNkF90tn64FxscYEdoeaigsPaCg7SbR2ltJQ -xu2LmNYJhgCNHFwYDD8PylxXoh4cjnzvvA2UgkTUBSvK0uJEQuEiX6EnAgMBAAGj -IDAeMA4GA1UdDwEB/wQEAwIFoDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUA -A4IBAQAdPX998IM8vE5haCO9B4iD+oDEFaTXPUVCwpEmFDpmko004kDg/iLljZJb -S9984MTEiMluzBYTVIbCZ/UJzrxN2N+r3o+osg5AveGSlIaoStjo9px3s/RV2v4d -jsc+r1IOJF/35Cial9Ie+AT87uEAMi5UkD2Pk356ICaz+oKTh6kbvr5U2GCXw93g -Hg1HeuIyWiDpT16GCi0Qs/JmfePgsgW6SYybUp83ZCsMAdw1QkT6nA4vEaZn8uGs -CwyEorw3LW1oK4ZKCdlV9L/K3GdY2WdUGDJ8iI/NVIJGi31g88g1fzx27fU2acpU -aXA1WFUYcfCrD8EndrHCEkUfECRb ------END CERTIFICATE----- diff --git a/identity_key.pem b/identity_key.pem deleted file mode 100644 index 0900c74..0000000 --- a/identity_key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEogIBAAKCAQEAtxcwOFuQGh9llZr2gnlXAQD6TfCI02zmhZFrVkf6f5tXu6PJ -koE2Gg5RE7keJjvwnNBqac0HNnKLnz3KJ53vCgrz7+cD47V7OayikWsZjsClX246 -nRlxz+wgLaIpiZdVOv6wxbaOAS6+AhBdvzsRRQPPaN04smeo2hgQFY+cNzfWuUBp -PivG60/10AFde65NX8wEobFiKKltykehDdRtxocI2U4PcurvcM1jfWssHsLUQ1gf -S4cF68/xqYzZBfdLZ+uBcbHGBHaHmooLD2goO0m0dpbSUMbti5jWCYYAjRxcGAw/ -D8pcV6IeHI5877wNlIJE1AUrytLiRELhIl+hJwIDAQABAoIBABWYoZ0e3RoyYTbK -sZBlpFKU/Uaw5vgcB1DypzjlLUdcrnY5SpnXrecC5fiv7ujO9yxiv7qSbusINdnn -CEUGBwp7N5eGUK93r6eiP7FsHaFOA0kEuoXfWM0QFXeXLNPISvW2JxRwhRg0nZWm -vTgaTTOD0IpBAI1irB3AQDLFiW/7GMB2aZD4YaUk2kbEbvSOF7xNqQtth0caP+/L -04pI635/6y+a1S/pQgnHBmltAzReUgQtYnNZMIzNiJUCA92Szb0Q/q4lKcn8sZ3r -yIFr6pKPAs7qYX73P8gi4Jbajn1HSoAE2jWokmi2TmRSYp7fiWYsPrAakAoxOv2x -Fv83ookCgYEA1tl+g7wOREV/7FtRwheWx3m//bxxiGRuxliNqrAaMXjO6cAdZJN3 -+TM3kFUYW7DSDhW5O7QDVibMZNbnYhtjaJSjnKZFPv+zqwuR8/hMboJjmqSRb+m8 -4FG7SxZPPhXQ22BZKjIsMLi8D+QRyXPfaVN4Yd3tGCNawMXxFmESepsCgYEA2ih8 -hFUXAp0epB8Z2m8KbrcTMMu1dNRIuioajSyOfwqGDIwGmDb7lyEgDpjtSrcfBlz5 -h5Tgg9MTEaJl8MxD4LXOHIZTkimre8iIt43TyTofEiD5nU4OUiwk+9SQwiPqVfy/ -ErUOwKVJ2oQv81LQheg13p9gUhvMQE4h8wOq5mUCgYBZ4mAViOWHFnRwU7wesXO5 -PGxgISh2YV4eyQlrsYUj0WDvVhp162Qz84N5dMBeC9m1Xs1B9wu2TUERpv7igobS -R+0zKjSqKJvoIU0MfoKrcQ1usw7NfUIxrr/mqAy68rGQNfzXtGnccEztcQMn/rwm -+m7QsuHwSUo8gBNew3nRpQKBgCHcPtdbTaL+OA9JNH4O1hOxHq1oMNXdTRx4BH78 -93EIdR3lbfCaOBqQ7aTWX55FJe+a5rAAj4hmboNCLYhea/qovUD4KGh7Rz6DNZUn -0kNdXg02SQf9YYOnjmX37C/12x1ViWKh75Q/E2NzOO4PYIYoMJRJMG4OGnmwptxN -KW1xAoGARjaCGJ+QdDrlybbcftAKgUQxev8TnMZz66iil8/8J2SJc5EOKQRwDBpx -HTbAcKQPdJZyBOf6rdoPgpCNws8dLukCTBgDCynuyRXqXQk0fNMZKGN5Lk7a5i4U -eH9YPMkxkg7n3Ke7yaBFXkpahdw44sQtVz/jgRRhO7NZTUxOXeo= ------END RSA PRIVATE KEY----- diff --git a/lib/crypto.go b/lib/crypto.go index 69f7d37..99903e6 100644 --- a/lib/crypto.go +++ b/lib/crypto.go @@ -2,127 +2,18 @@ package encrypt import ( "bytes" - "crypto/aes" - "crypto/cipher" "crypto/rand" "crypto/rsa" - "crypto/sha256" "crypto/x509" "crypto/x509/pkix" - "encoding/base64" - "encoding/json" "encoding/pem" "errors" "fmt" - "io/fs" "math/big" - "os" - "path/filepath" "time" ) -type Envelope struct { - EK string `json:"ek"` - N string `json:"n"` - CT string `json:"ct"` -} - -type Service struct { - Priv *rsa.PrivateKey `json:"priv,omitempty"` - PubPEM []byte `json:"pub,omitempty"` - CertPEM []byte `json:"cert,omitempty"` - dir string `json:"-"` -} - -func NewService(storageDir string) (*Service, error) { - s := &Service{dir: storageDir} - if err := s.loadOrGenerateKeys(); err != nil { - return nil, err - } - return s, nil -} - -func (s *Service) PublicPEM() string { return string(s.PubPEM) } -func (s *Service) PublicCert() string { return string(s.CertPEM) } - -func (s *Service) Encrypt(message, peerPEMorCert string) (string, error) { - pubKey, err := parsePeerPublicKey(peerPEMorCert) - if err != nil { - return "", err - } - - aesKey := make([]byte, 32) - if _, err := rand.Read(aesKey); err != nil { - return "", err - } - - block, err := aes.NewCipher(aesKey) - 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 - } - - ciphertext := gcm.Seal(nil, nonce, []byte(message), nil) - label := []byte{} - ek, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, pubKey, aesKey, label) - if err != nil { - return "", err - } - - env := Envelope{ - EK: base64.StdEncoding.EncodeToString(ek), - N: base64.StdEncoding.EncodeToString(nonce), - CT: base64.StdEncoding.EncodeToString(ciphertext), - } - out, _ := json.MarshalIndent(env, "", " ") - return string(out), nil -} - -func (s *Service) Decrypt(payload string) (string, error) { - var env Envelope - if err := json.Unmarshal([]byte(payload), &env); err != nil { - return "", fmt.Errorf("invalid JSON: %w", err) - } - ek, err := base64.StdEncoding.DecodeString(env.EK) - if err != nil { - return "", fmt.Errorf("ek b64: %w", err) - } - nonce, err := base64.StdEncoding.DecodeString(env.N) - if err != nil { - return "", fmt.Errorf("n b64: %w", err) - } - ct, err := base64.StdEncoding.DecodeString(env.CT) - if err != nil { - return "", fmt.Errorf("ct b64: %w", err) - } - - label := []byte{} - aesKey, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, s.Priv, ek, label) - if err != nil { - return "", fmt.Errorf("RSA-OAEP decrypt: %w", err) - } - - block, err := aes.NewCipher(aesKey) - if err != nil { - return "", err - } - gcm, err := cipher.NewGCM(block) - if err != nil { - return "", err - } - plain, err := gcm.Open(nil, nonce, ct, nil) - if err != nil { - return "", fmt.Errorf("GCM open: %w", err) - } - return string(plain), nil -} +// No envelope here; server/UI define their own payload structs. func parsePeerPublicKey(pemOrCert string) (*rsa.PublicKey, error) { block, _ := pem.Decode([]byte(pemOrCert)) @@ -161,65 +52,13 @@ func ParsePeerPublicKey(pemOrCert string) (*rsa.PublicKey, error) { return parsePeerPublicKey(pemOrCert) } -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") - - fmt.Printf("Using storage dir: %s\n", s.dir) - if fileExists(privPath) && fileExists(pubPath) && fileExists(certPath) { - pkBytes, err := os.ReadFile(privPath) - if err != nil { - return err - } - block, _ := pem.Decode(pkBytes) - if block == nil || block.Type != "RSA PRIVATE KEY" { - return errors.New("invalid private key PEM") - } - key, err := x509.ParsePKCS1PrivateKey(block.Bytes) - if err != nil { - return err - } - s.Priv = key - s.PubPEM, _ = os.ReadFile(pubPath) - s.CertPEM, _ = os.ReadFile(certPath) - return nil +func generateSelfSignedCert(priv *rsa.PrivateKey, commonName string) ([]byte, error) { + if commonName == "" { + commonName = "Encryptor Local Identity" } - - key, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return err - } - s.Priv = key - - pubASN1, _ := x509.MarshalPKIXPublicKey(&s.Priv.PublicKey) - s.PubPEM = pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubASN1}) - - cert, err := generateSelfSignedCert(s.Priv) - if err != nil { - return err - } - s.CertPEM = cert - - if err := os.MkdirAll(s.dir, 0o700); err != nil { - return err - } - if err := os.WriteFile(privPath, pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(s.Priv)}), fs.FileMode(0o600)); err != nil { - return err - } - if err := os.WriteFile(pubPath, s.PubPEM, 0o644); err != nil { - return err - } - if err := os.WriteFile(certPath, s.CertPEM, 0o644); err != nil { - return err - } - return nil -} - -func generateSelfSignedCert(priv *rsa.PrivateKey) ([]byte, error) { tpl := &x509.Certificate{ SerialNumber: big.NewInt(time.Now().UnixNano()), - Subject: pkix.Name{CommonName: "Encryptor Local Identity"}, + Subject: pkix.Name{CommonName: commonName}, NotBefore: time.Now().Add(-1 * time.Hour), NotAfter: time.Now().AddDate(1, 0, 0), KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, @@ -233,8 +72,3 @@ func generateSelfSignedCert(priv *rsa.PrivateKey) ([]byte, error) { _ = pem.Encode(buf, &pem.Block{Type: "CERTIFICATE", Bytes: der}) return buf.Bytes(), nil } - -func fileExists(p string) bool { - info, err := os.Stat(p) - return err == nil && !info.IsDir() -} diff --git a/lib/crypto_storage.go b/lib/crypto_storage.go index 459ace6..5c542cc 100644 --- a/lib/crypto_storage.go +++ b/lib/crypto_storage.go @@ -171,8 +171,8 @@ 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 (krátký CN) - certPEM, _ := generateSelfSignedCert(pk) + // Optional self-signed cert s rozumným CommonName (lze upravit později UI) + certPEM, _ := generateSelfSignedCert(pk, "Local User") _ = 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 37a3e8e..b4b15be 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,8 @@ package main import ( "crypto/rsa" + "crypto/x509" + "encoding/pem" encrypt "fckeuspy-go/lib" "html/template" "log" @@ -82,12 +84,45 @@ func runFyne() { } func RunWebApp() { - var err error - // 2) inicializuj šifrovací službu - _, err = encrypt.NewService("") + // Otevři nebo vytvoř šifrovaný trezor a načti identitu pouze z něj + vaultPath := os.Getenv("VAULT_PATH") + if vaultPath == "" { + vaultPath = "./vault.enc" + } + pw := os.Getenv("VAULT_PASSWORD") + if pw == "" { + log.Fatal("VAULT_PASSWORD must be set for web mode") + } + var store encrypt.SecureJSONStore + if _, statErr := os.Stat(vaultPath); os.IsNotExist(statErr) { + s, err := encrypt.CreateEncryptedStore(vaultPath, pw, true) + if err != nil { + log.Fatal(err) + } + store = s + } else { + s, err := encrypt.OpenEncryptedStore(vaultPath, pw) + if err != nil { + log.Fatal(err) + } + store = s + } + // Načti privátní klíč a veřejné materiály + privPEM := store.IdentityPrivatePEM() + if privPEM == "" { + log.Fatal("missing private key in vault") + } + block, _ := pem.Decode([]byte(privPEM)) + if block == nil || block.Type != "RSA PRIVATE KEY" { + log.Fatal("invalid private key PEM in vault") + } + key, err := x509.ParsePKCS1PrivateKey(block.Bytes) if err != nil { log.Fatal(err) } + priv = key + pubPEM = []byte(store.IdentityPublicPEM()) + certPEM = []byte(store.IdentityCertPEM()) // 2) šablony tmpl = template.Must(template.ParseGlob("templates/*.html")) diff --git a/public.pem b/public.pem deleted file mode 100644 index 33bb19a..0000000 --- a/public.pem +++ /dev/null @@ -1,9 +0,0 @@ ------BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtxcwOFuQGh9llZr2gnlX -AQD6TfCI02zmhZFrVkf6f5tXu6PJkoE2Gg5RE7keJjvwnNBqac0HNnKLnz3KJ53v -Cgrz7+cD47V7OayikWsZjsClX246nRlxz+wgLaIpiZdVOv6wxbaOAS6+AhBdvzsR -RQPPaN04smeo2hgQFY+cNzfWuUBpPivG60/10AFde65NX8wEobFiKKltykehDdRt -xocI2U4PcurvcM1jfWssHsLUQ1gfS4cF68/xqYzZBfdLZ+uBcbHGBHaHmooLD2go -O0m0dpbSUMbti5jWCYYAjRxcGAw/D8pcV6IeHI5877wNlIJE1AUrytLiRELhIl+h -JwIDAQAB ------END PUBLIC KEY----- diff --git a/ui.go b/ui.go index b6e1c49..a6f7f3c 100644 --- a/ui.go +++ b/ui.go @@ -54,15 +54,15 @@ func buildEntries() *uiParts { showPayloadQR: true, } p.cipherQR.FillMode = canvas.ImageFillContain - p.cipherQR.SetMinSize(fyne.NewSize(260, 260)) + p.cipherQR.SetMinSize(fyne.NewSize(220, 220)) p.pubQR.FillMode = canvas.ImageFillContain - p.pubQR.SetMinSize(fyne.NewSize(220, 220)) + p.pubQR.SetMinSize(fyne.NewSize(200, 200)) p.crtQR.FillMode = canvas.ImageFillContain - p.crtQR.SetMinSize(fyne.NewSize(220, 220)) + p.crtQR.SetMinSize(fyne.NewSize(200, 200)) p.peerQR.FillMode = canvas.ImageFillContain - p.peerQR.SetMinSize(fyne.NewSize(220, 220)) + p.peerQR.SetMinSize(fyne.NewSize(200, 200)) p.payloadQR.FillMode = canvas.ImageFillContain - p.payloadQR.SetMinSize(fyne.NewSize(260, 260)) + p.payloadQR.SetMinSize(fyne.NewSize(220, 220)) p.outKey.SetPlaceHolder("Veřejný klíč / certifikát…") p.msg.SetPlaceHolder("Sem napiš zprávu…") p.peer.SetPlaceHolder("-----BEGIN PUBLIC KEY----- … nebo CERTIFICATE …") @@ -70,12 +70,12 @@ func buildEntries() *uiParts { p.payload.SetPlaceHolder(`{"ek":"…","n":"…","ct":"…"}`) p.plainOut.SetPlaceHolder("Dešifrovaná zpráva…") // Zvýšení výšky (více řádků viditelně) - p.outKey.SetMinRowsVisible(10) - p.peer.SetMinRowsVisible(6) - p.msg.SetMinRowsVisible(8) - p.cipherOut.SetMinRowsVisible(8) - p.payload.SetMinRowsVisible(8) - p.plainOut.SetMinRowsVisible(8) + p.outKey.SetMinRowsVisible(6) + p.peer.SetMinRowsVisible(4) + p.msg.SetMinRowsVisible(5) + p.cipherOut.SetMinRowsVisible(5) + p.payload.SetMinRowsVisible(5) + p.plainOut.SetMinRowsVisible(5) p.toastLabel.Hide() return p } @@ -201,11 +201,16 @@ func buildIdentityTab(parts *uiParts, svc ServiceFacade, vaultPath string) fyne. toggleBtn.OnTapped = func() { parts.showQR = !parts.showQR; rebuild() } rebuild() - tileIdentity := buttonTile(btnCopyPub, btnCopyCrt, btnPaste, btnShowPub, btnShowCrt, btnClear, deleteBtn, toggleBtn) + // Group buttons by function: data viewing vs clipboard vs destructive + clipboardRow := buttonTile(btnCopyPub, btnCopyCrt, btnPaste, btnClear) + viewRow := buttonTile(btnShowPub, btnShowCrt, toggleBtn) + destroyRow := buttonTile(deleteBtn) group := container.NewVBox( widget.NewLabelWithStyle("Moje identita", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), - tileIdentity, + viewRow, identityContainer, + clipboardRow, + destroyRow, ) return container.NewVScroll(group) } @@ -218,6 +223,10 @@ type ServiceFacade interface { Decrypt(json string) (string, error) PublicPEM() string PublicCert() string + // Contacts API (only implemented by VaultService) + ListContacts() ([]Contact, error) + SaveContact(c Contact) error + DeleteContact(id string) error } func copyClip(s string, parts *uiParts) { @@ -367,13 +376,26 @@ func buildEncryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject { widget.NewButton("Import Key QR", importPeerQR), ) - group := container.NewVBox( - widget.NewLabelWithStyle("Šifrování", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), - container.NewHBox(widget.NewLabel("Veřejný klíč příjemce"), peerToggle), peerBtns, peerContainer, - widget.NewLabel("Zpráva"), msgBtns, parts.msg, + peerSection := container.NewVBox( + container.NewHBox(widget.NewLabel("Veřejný klíč příjemce"), peerToggle), + peerContainer, + peerBtns, + ) + msgSection := container.NewVBox( + widget.NewLabel("Zpráva"), + parts.msg, + msgBtns, + ) + outputSection := container.NewVBox( container.NewHBox(widget.NewLabel("Výstup"), toggleBtn), outputContainer, ) + group := container.NewVBox( + widget.NewLabelWithStyle("Šifrování", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), + peerSection, + msgSection, + outputSection, + ) return container.NewVScroll(group) } @@ -480,19 +502,30 @@ func buildDecryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject { } updateMode() plainBtns := buttonTile(widget.NewButton("Copy", func() { copyClip(parts.plainOut.Text, parts) })) + payloadSection := container.NewVBox( + container.NewHBox(widget.NewLabel("Payload"), payloadToggle), + payloadContainer, + payloadBtns, + ) + resultSection := container.NewVBox( + widget.NewLabel("Výsledek"), + parts.plainOut, + plainBtns, + ) group := container.NewVBox( widget.NewLabelWithStyle("Dešifrování", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), - container.NewHBox(widget.NewLabel("Payload"), payloadToggle), payloadBtns, payloadContainer, - widget.NewLabel("Výsledek"), plainBtns, parts.plainOut, + payloadSection, + resultSection, ) return container.NewVScroll(group) } 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)) decTab := container.NewTabItem("Dešifrování", buildDecryptTab(parts, svc)) - tabs := container.NewAppTabs(idTab, encTab, decTab) + tabs := container.NewAppTabs(idTab, contactsTab, encTab, decTab) tabs.SetTabLocation(container.TabLocationTop) // apply fixed dark theme once @@ -532,3 +565,110 @@ func buttonTile(btns ...fyne.CanvasObject) fyne.CanvasObject { padded := container.NewPadded(grid) return container.NewStack(rect, padded) } + +// --- Contacts UI --- + +func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject { + list := widget.NewList( + func() int { items, _ := svc.ListContacts(); return len(items) }, + func() fyne.CanvasObject { return widget.NewLabel("") }, + func(i widget.ListItemID, o fyne.CanvasObject) { + items, _ := svc.ListContacts() + if i >= 0 && i < len(items) { + o.(*widget.Label).SetText(items[i].Name) + } + }, + ) + var itemsCache []Contact + selected := -1 + refresh := func() { + items, _ := svc.ListContacts() + itemsCache = items + list.Refresh() + } + list.OnSelected = func(id widget.ListItemID) { + selected = int(id) + if id < 0 || id >= len(itemsCache) { + return + } + c := itemsCache[id] + nameEntry := widget.NewEntry() + nameEntry.SetText(c.Name) + certEntry := widget.NewMultiLineEntry() + certEntry.SetMinRowsVisible(6) + certEntry.SetText(c.Cert) + form := widget.NewForm( + widget.NewFormItem("Název", nameEntry), + widget.NewFormItem("Certifikát / Public key", certEntry), + ) + dialog.ShowCustomConfirm("Kontakt", "Uložit", "Zrušit", form, func(ok bool) { + if !ok { + return + } + c.Name = nameEntry.Text + c.Cert = certEntry.Text + _ = svc.SaveContact(c) + refresh() + }, fyne.CurrentApp().Driver().AllWindows()[0]) + } + list.OnUnselected = func(id widget.ListItemID) { selected = -1 } + + addFromClipboard := widget.NewButton("Přidat z clipboardu", func() { + txt := fyne.CurrentApp().Clipboard().Content() + if txt == "" { + parts.showToast("Schází data v clipboardu") + return + } + name := extractCN(txt) + if name == "" { + name = "Kontakt" + } + _ = svc.SaveContact(Contact{Name: name, Cert: txt}) + refresh() + }) + addFromFile := widget.NewButton("Přidat ze souboru", func() { + fd := dialog.NewFileOpen(func(r fyne.URIReadCloser, err error) { + if err != nil || r == nil { + return + } + defer r.Close() + b, _ := io.ReadAll(r) + txt := string(b) + name := extractCN(txt) + if name == "" { + name = "Kontakt" + } + _ = svc.SaveContact(Contact{Name: name, Cert: txt}) + refresh() + }, fyne.CurrentApp().Driver().AllWindows()[0]) + fd.Show() + }) + deleteBtn := widget.NewButton("Smazat", func() { + sel := selected + if sel < 0 || sel >= len(itemsCache) { + return + } + _ = svc.DeleteContact(itemsCache[sel].ID) + refresh() + }) + copyBtn := widget.NewButton("Zkopírovat cert", func() { + sel := selected + if sel < 0 || sel >= len(itemsCache) { + return + } + copyClip(itemsCache[sel].Cert, parts) + }) + useBtn := widget.NewButton("Použít ve Šifrování", func() { + sel := selected + if sel < 0 || sel >= len(itemsCache) { + return + } + parts.peer.SetText(itemsCache[sel].Cert) + parts.showToast("Kontakt vložen do Šifrování") + }) + + header := container.NewHBox(addFromClipboard, addFromFile, deleteBtn, copyBtn, useBtn) + content := container.NewBorder(header, nil, nil, nil, list) + refresh() + return content +} diff --git a/vault_service.go b/vault_service.go index 539b4ed..ccbc358 100644 --- a/vault_service.go +++ b/vault_service.go @@ -77,6 +77,86 @@ type hybridEnvelope struct { CT string `json:"ct"` } +// --- Contacts management --- + +type Contact struct { + ID string `json:"id"` + Name string `json:"name"` + Cert string `json:"cert"` // full PEM cert (or public key PEM) +} + +const contactsKey = "contacts" + +func (v *VaultService) ListContacts() ([]Contact, error) { + var list []Contact + if !v.store.Has(contactsKey) { + return []Contact{}, nil + } + if err := v.store.Get(contactsKey, &list); err != nil { + return nil, err + } + return list, nil +} + +func (v *VaultService) SaveContact(c Contact) error { + list, _ := v.ListContacts() + // upsert by ID, if empty ID derive from CN or hash + if c.ID == "" { + c.ID = deriveContactID(c) + } + replaced := false + for i := range list { + if list[i].ID == c.ID { + list[i] = c + replaced = true + break + } + } + if !replaced { + list = append(list, c) + } + if err := v.store.Put(contactsKey, list); err != nil { + return err + } + return v.store.Flush() +} + +func (v *VaultService) DeleteContact(id string) error { + list, _ := v.ListContacts() + out := list[:0] + for _, c := range list { + if c.ID != id { + out = append(out, c) + } + } + if err := v.store.Put(contactsKey, out); err != nil { + return err + } + return v.store.Flush() +} + +func deriveContactID(c Contact) string { + // simple stable ID: prefer CN from cert; fallback to sha256 of cert + if cn := extractCN(c.Cert); cn != "" { + return cn + } + sum := sha256.Sum256([]byte(c.Cert)) + return fmt.Sprintf("%x", sum[:8]) +} + +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 + } + } + return "" +} + func encryptHybrid(priv *rsa.PrivateKey, message, peerPEMorCert string) (string, error) { pubKey, err := encrypt.ParsePeerPublicKey(peerPEMorCert) if err != nil {