feature/ui - zatim ne uplne uhlazena ale celkem pouzitelna appka #1
18
identity.crt
18
identity.crt
@ -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-----
|
||||
@ -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-----
|
||||
176
lib/crypto.go
176
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
|
||||
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()
|
||||
}
|
||||
|
||||
@ -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
41
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"))
|
||||
|
||||
@ -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
180
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
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user