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
8 changed files with 285 additions and 250 deletions
Showing only changes of commit 1b778cece1 - Show all commits

View File

@ -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-----

View File

@ -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-----

View File

@ -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
func generateSelfSignedCert(priv *rsa.PrivateKey, commonName string) ([]byte, error) {
if commonName == "" {
commonName = "Encryptor Local Identity"
}
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
}
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()
}

View File

@ -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))

41
main.go
View File

@ -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"))

View File

@ -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-----

180
ui.go
View File

@ -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
}

View File

@ -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 {